Tool capabilities reference
The capability framework gates external access for every tool. A tool declares a ToolCapabilities object; the framework validates the declaration against the personality policy, resolves scoped context objects at call time, and injects them into ToolContext.
Source
Types in packages/types/src/tool-capabilities.ts. Re-exported from @ethosagent/types.
Enforcement in packages/core/src/capability-validator.ts (registration-time) and packages/core/src/capability-resolver.ts (call-time). Scoped implementations in packages/core/src/scoped/.
ToolCapabilities
Signature
import type { ToolCapabilities, SecretRef, StorageScope } from '@ethosagent/types';
export type SecretRef = string;
export type StorageScope = 'tool-private' | 'session' | 'personality';
export interface ToolCapabilities {
network?: {
allowedHosts: string[];
};
secrets?: SecretRef[];
storage?: {
scope: StorageScope;
kind: 'kv';
ttlSecondsDefault?: number;
};
fs_reach?: {
read?: string[] | 'from-personality';
write?: string[] | 'from-personality';
};
process?: {
allowedBinaries: string[];
};
}
Fields
| Field | Type | Description |
|---|---|---|
network | { allowedHosts: string[] } | undefined | Hosts the tool may fetch. Supports exact match (api.github.com), wildcard (*), and subdomain wildcard (*.github.com). Intersected with personality safety.network.allow at call time. |
secrets | SecretRef[] | undefined | Secret reference names the tool may read. Each ref is resolved by the configured secrets backend at call time. |
storage | { scope, kind, ttlSecondsDefault? } | undefined | Key-value storage request. scope determines the namespace; kind is always 'kv'; ttlSecondsDefault sets a default TTL in seconds for entries (optional). |
fs_reach | { read?, write? } | undefined | Filesystem paths the tool may access. Each direction is either a string[] of absolute paths or 'from-personality' to defer to the personality's fs_reach config. |
process | { allowedBinaries: string[] } | undefined | Binary names the tool may spawn. Supports exact match (git, npm) and wildcard (*). |
Notes
- A tool that touches no external surface declares
capabilities: {}. Thecapabilitiesfield is required onTool<TArgs>. network.allowedHosts: ['*']means "whatever the personality allows." The wildcard is not "all hosts" -- it defers to the personality'ssafety.network.allowlist. If the personality has no allow list, the resolved host set is empty.fs_reach.read: 'from-personality'andfs_reach.write: 'from-personality'are the conventional choice for generic file tools (read_file,write_file). Tool-specific paths are for tools that know their exact filesystem footprint (e.g. a config reader that only touches~/.ethos/config.yaml).
StorageScope
Signature
export type StorageScope = 'tool-private' | 'session' | 'personality';
Values
| Value | Resolved scope id | Lifecycle | Typical use |
|---|---|---|---|
'tool-private' | tool:<toolName> | Persists across sessions. Private to this tool. | Caches, tool-internal state, rate-limit counters. |
'session' | session:<sessionId> | Lives for the duration of the session. Shared across tools in the same session. | Scratch state, session-local accumulators. |
'personality' | personality:<personalityId> | Persists across sessions. Scoped to the active personality. Falls back to session:<sessionId> if no personality is active. | Personality-specific tool state, cross-session memory. |
Notes
- The scope id is computed by
resolveCapabilitiesinpackages/core/src/capability-resolver.ts. The tool never constructs its own scope id. - The
KeyValueStoreinstance the tool receives is already namespaced. CallingkvStore.set('foo', 'bar')stores under the resolved scope id; the tool does not prefix keys. ttlSecondsDefaulton thestoragedeclaration sets a default TTL for all entries. Individualsetcalls can override with{ ttlSeconds }in the options.
KeyValueStore
Signature
import type { KeyValueStore } from '@ethosagent/types';
export interface KeyValueStore {
get(key: string): Promise<string | null>;
set(key: string, value: string, opts?: { ttlSeconds?: number }): Promise<void>;
delete(key: string): Promise<void>;
list(prefix: string): Promise<string[]>;
}
Methods
| Method | Returns | Description |
|---|---|---|
get(key) | string | null | Read a value by key. Returns null if the key does not exist or has expired. |
set(key, value, opts?) | void | Write a value. opts.ttlSeconds overrides the default TTL from the capability declaration. |
delete(key) | void | Remove a key. No-op if the key does not exist. |
list(prefix) | string[] | List keys matching a prefix. Returns key names, not values. |
Notes
- The store is injected into
ToolContext.kvStorewhen the tool declaresstoragein its capabilities. Absent if the tool does not declare storage or nokvStoreFactorybackend is configured. - Values are strings. Tools that need structured data should serialise to JSON.
ScopedFetch
Signature
import type { ScopedFetch } from '@ethosagent/types';
export interface ScopedFetch {
fetch(url: string | URL, init?: RequestInit): Promise<Response>;
}
Behaviour
The implementation (ScopedFetchImpl in packages/core/src/scoped/scoped-fetch.ts) parses the URL, extracts the hostname, and checks it against the resolved host set (intersection of the tool's allowedHosts and the personality's safety.network.allow). If the host is not in the set, the call throws with HOST_NOT_ALLOWED. Otherwise it delegates to globalThis.fetch.
Host matching supports:
- Exact match:
'api.github.com'matchesapi.github.com. - Wildcard:
'*'matches any host. - Subdomain wildcard:
'*.github.com'matchesapi.github.comandraw.github.combut notgithub.comitself.
Notes
- Injected into
ToolContext.scopedFetchwhen the tool declaresnetwork. Absent otherwise. - The resolved host set may be smaller than the tool's declared set. A tool declaring
['api.github.com', 'api.stripe.com']under a personality that allows['*.github.com']gets aScopedFetchthat permits onlyapi.github.com.
ScopedFs
Signature
import type { ScopedFs } from '@ethosagent/types';
export interface ScopedFs {
read(path: string): Promise<string>;
write(path: string, content: string | Buffer): Promise<void>;
exists(path: string): Promise<boolean>;
list(path: string): Promise<string[]>;
}
Methods
| Method | Returns | Description |
|---|---|---|
read(path) | string | Read utf-8 text. Throws PATH_NOT_REACHABLE if the path is outside the declared read set. Throws if the file does not exist. |
write(path, content) | void | Write utf-8 text or a Buffer. Throws PATH_NOT_REACHABLE if the path is outside the declared write set. |
exists(path) | boolean | Check existence. Throws PATH_NOT_REACHABLE if the path is outside the declared read set. |
list(path) | string[] | List directory children. Throws PATH_NOT_REACHABLE if the path is outside the declared read set. |
Notes
- Injected into
ToolContext.scopedFswhen the tool declaresfs_reach. Absent otherwise. - The implementation (
ScopedFsImplinpackages/core/src/scoped/scoped-fs.ts) resolves paths vianode:path.resolveandnormalizebefore checking. Relative paths are resolved againstprocess.cwd(). - Delegates to the
Storageinterface, not to rawnode:fs. The sameStorageabstraction powersScopedStoragefrom the pre-existing filesystem boundary. ScopedFsis distinct fromScopedStorage.ScopedStorage(from@ethosagent/storage-fs) is the personality-level decorator on the fullStorageinterface.ScopedFsis the capability-gated subset exposed to individual tools.
ScopedSecretsResolver
Signature
import type { ScopedSecretsResolver, SecretRef } from '@ethosagent/types';
export interface ScopedSecretsResolver {
get(ref: SecretRef): Promise<string>;
}
Behaviour
The implementation (ScopedSecretsImpl in packages/core/src/scoped/scoped-secrets.ts) checks the requested ref against the tool's declared secrets set. If the ref is not declared, the call throws with SECRET_NOT_DECLARED. Otherwise it delegates to the configured secrets backend.
Notes
- Injected into
ToolContext.secretsResolverwhen the tool declaressecretsand asecretsBackendis configured. Absent otherwise. - The secrets backend is a function
(ref: SecretRef) => Promise<string>provided viaCapabilityBackends.secretsBackend. The framework does not mandate where secrets are stored -- the backend is a wiring decision.
ScopedProcess
Signature
import type { ScopedProcess, SpawnOpts, ProcessResult } from '@ethosagent/types';
export interface SpawnOpts {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
export interface ProcessResult {
exitCode: number;
stdout: string;
stderr: string;
}
export interface ScopedProcess {
spawn(binary: string, args: string[], opts?: SpawnOpts): Promise<ProcessResult>;
}
Behaviour
The implementation (ScopedProcessImpl in packages/core/src/scoped/scoped-process.ts) checks the requested binary against the tool's declared allowedBinaries set. Wildcard ('*') permits any binary. If the binary is not in the set, the call throws with BINARY_NOT_ALLOWED. Otherwise it spawns the process via node:child_process.spawn.
Notes
- Injected into
ToolContext.scopedProcesswhen the tool declaresprocess. Absent otherwise. opts.envis merged withprocess.env-- the tool's declared env vars are additive, not replacing.opts.timeoutis passed directly tochild_process.spawn'stimeoutoption. The child is killed if it exceeds the timeout.
Error catalog
All errors thrown by scoped context implementations use a stable prefix in the error message. Catch by message.startsWith(code) or by checking the Error.message directly.
| Code | Thrown by | When |
|---|---|---|
HOST_NOT_ALLOWED | ScopedFetchImpl | The requested hostname is not in the resolved allowedHosts set. |
SECRET_NOT_DECLARED | ScopedSecretsImpl | The requested secret ref is not in the tool's declared secrets set. |
PATH_NOT_REACHABLE | ScopedFsImpl | The requested path is not covered by the tool's declared fs_reach (read or write). |
BINARY_NOT_ALLOWED | ScopedProcessImpl | The requested binary is not in the tool's declared allowedBinaries set. |
Error message format
Each error follows the pattern <CODE>: <detail>:
HOST_NOT_ALLOWED: api.stripe.com is not in the declared allowedHosts
SECRET_NOT_DECLARED: STRIPE_KEY is not in the tool's declared secrets
PATH_NOT_REACHABLE: read not permitted for /etc/passwd
BINARY_NOT_ALLOWED: rm is not in the declared allowedBinaries
Notes
- These errors propagate to
ToolResultas{ ok: false, code: 'execution_failed', error: err.message }via theexecuteParallelcatch handler. The LLM sees the full error message. - Registration-time validation errors (
CapabilityValidationError) are a separate type with{ tool, capability, message }fields. They are not thrown -- they are returned as an array fromvalidateRegistrationandvalidateToolsForPersonality.
CapabilityBackends
Signature
import type { CapabilityBackends } from '@ethosagent/core';
export interface CapabilityBackends {
kvStoreFactory?: (tool: string, scopeId: string) => KeyValueStore;
secretsBackend?: (ref: SecretRef) => Promise<string>;
storage?: Storage;
personalityFsReach?: { read: string[]; write: string[] };
personalityNetworkAllow?: string[];
}
Fields
| Field | Type | Description |
|---|---|---|
kvStoreFactory | (tool, scopeId) => KeyValueStore | Factory for scoped key-value stores. Called once per tool per turn. |
secretsBackend | (ref) => Promise<string> | Resolves a secret ref to its value. Called by ScopedSecretsImpl. |
storage | Storage | Filesystem abstraction passed to ScopedFsImpl. |
personalityFsReach | { read: string[]; write: string[] } | The active personality's fs_reach config. Used to resolve 'from-personality' declarations and to intersect with tool-declared paths. |
personalityNetworkAllow | string[] | The active personality's network allow list. Used to intersect with tool-declared hosts. |
Notes
- Passed to
DefaultToolRegistryat construction. Shared across all tool executions. - If absent, tools that declare real capabilities (
needsBackendsreturns true) fail closed withnot_available. personalityFsReachandpersonalityNetworkAlloware set per personality, typically updated when the personality changes.
Used by
| Consumer | Role |
|---|---|
packages/types/src/tool-capabilities.ts | Type definitions for all capability interfaces. |
packages/types/src/tool.ts | Tool.capabilities field; ToolContext scoped fields (kvStore, secretsResolver, scopedFetch, scopedFs, scopedProcess). |
packages/core/src/capability-validator.ts | validateRegistration -- registration-time policy check. |
packages/core/src/capability-resolver.ts | resolveCapabilities -- call-time context building. |
packages/core/src/tool-registry.ts | needsBackends guard; executeParallel capability resolution. |
packages/core/src/scoped/scoped-fetch.ts | ScopedFetchImpl -- host-gated fetch. |
packages/core/src/scoped/scoped-fs.ts | ScopedFsImpl -- path-gated filesystem. |
packages/core/src/scoped/scoped-secrets.ts | ScopedSecretsImpl -- ref-gated secrets. |
packages/core/src/scoped/scoped-process.ts | ScopedProcessImpl -- binary-gated process spawning. |
See also
- Why capabilities -- the architectural motivation for the framework
- Tool isolation model -- how the enforcement points work together
- Tool interface reference --
Tool<TArgs>,ToolResult,ToolContext - Storage interface reference --
Storage,ScopedStorage,BoundaryError - Tool registry reference --
DefaultToolRegistry.executeParalleland the capability wiring