@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
149 lines • 7.61 kB
text/typescript
/**
* Node implementation of the `agent_toolset_20260401` tools — `bash`, `read`,
* `write`, `edit`, `glob`, `grep` — plus the workdir/skills
* {@link AgentToolContext}.
*
* This mirrors `@anthropic-ai/sdk/tools/memory/node`: it is the explicit,
* Node-only entry point for these implementations. Importing it pulls in
* `node:child_process`, `node:fs`, etc., so it is kept separate from the rest of
* the SDK — depending on it is an opt-in.
*
* **Node 22+ is required** for this module: the `glob` tool uses the native
* `fs.glob`, added in Node 22. The rest of the SDK still supports Node 18+; only
* the agent toolset has this requirement.
*
* The result of {@link betaAgentToolset20260401} is a plain `BetaRunnableTool[]`;
* hand it to any tool runner — `client.beta.messages.toolRunner({ …, tools })`
* for the Messages API, or `client.beta.sessions.events.toolRunner({ …, tools })`
* for a managed-agents session:
*
* ```ts
* import { betaAgentToolset20260401 } from '@anthropic-ai/sdk/tools/agent-toolset/node';
*
* const tools = betaAgentToolset20260401({ workdir: '/work' });
* const tools2 = betaAgentToolset20260401({ workdir: '/work' }).filter((t) => t.name !== 'bash');
* ```
*
* Trust model: the file tools confine to `workdir` (symlink-aware) and are safe
* without a sandbox; `bash` is unrestricted and should run inside one. See
* {@link AgentToolContext}.
*/
import type { Anthropic } from "../../client.mjs";
import type { BetaRunnableTool } from "../../lib/tools/BetaRunnableTool.mjs";
export { setupSkills, resolveSkillVersion, extractSkillArchive } from "./skills.mjs";
/**
* Workdir + path-policy for the agent toolset.
*
* Trust model — two tiers:
*
* - The file tools ({@link betaReadTool}, {@link betaWriteTool},
* {@link betaEditTool}, {@link betaGlobTool}, {@link betaGrepTool}) confine to
* `workdir` unless `unrestrictedPaths` is set. {@link resolvePath}
* canonicalizes the target (resolving every symlink, including the leaf)
* before the check *and* returns that canonical path for the operation, so a
* symlink inside the workdir that points outside it neither passes the check
* nor gets followed afterwards — this is a real boundary, not a lexical hint
* (modulo the residual TOCTOU noted on {@link resolvePath}).
* - {@link betaBashTool} runs an unrestricted `/bin/bash` and cannot be
* confined. Run it — and, for defense in depth, the whole toolset — inside a
* sandbox the host controls (e.g. a self-hosted environment runner).
*/
export interface AgentToolContext {
/** Base directory for resolving relative tool paths. */
workdir: string;
/**
* When `false` (default), the file tools reject paths that resolve outside
* `workdir` (symlinks resolved). Does **not** constrain {@link betaBashTool}.
*/
unrestrictedPaths?: boolean;
/**
* Anthropic client. Optional — the bare toolset needs no client; it is only
* used by `setupSkills`, which (together with {@link AgentToolContext.sessionId})
* fetches the session's resolved agent and downloads each of its skills into
* `{workdir}/skills/<name>/`.
*/
client?: Anthropic;
/** Session whose agent's skills `setupSkills` should download. */
sessionId?: string;
/**
* Optional environment for the bash subprocess. When unset, the bash tool
* inherits the process environment with the runner's `ANTHROPIC_*`
* credentials scrubbed. When provided, it FULLY REPLACES that default
* environment — the mapping is used verbatim and is NOT merged with or added
* to the scrubbed process environment. To keep the defaults plus extra vars,
* build the combined mapping yourself before passing it.
*/
env?: NodeJS.ProcessEnv;
/**
* Size cap for the `read` and `edit` tools, which both load the whole file into
* memory. `undefined` (default) uses the built-in 256 KiB cap; a positive number
* sets a custom cap; `null` disables the cap entirely. Disabling it reintroduces
* the OOM risk on a model-controlled path, so pass `null` only when the sandbox
* can absorb arbitrarily large files. The non-regular-file (FIFO/device) guard
* always applies regardless of this value.
*/
maxFileBytes?: number | null;
}
/**
* Returns the `agent_toolset_20260401` implementations bound to `ctx`. The
* result is a plain array of `BetaRunnableTool`; filter or extend it before
* handing it to a tool runner:
*
* ```ts
* const tools = [...betaAgentToolset20260401(ctx), myCustomTool];
* const tools = betaAgentToolset20260401(ctx).filter((t) => t.name !== 'grep');
* ```
*
* Concurrency note: `client.beta.sessions.events.toolRunner` dispatches a
* session's tool calls serially (the sessions API delivers one `agent.tool_use`
* at a time). `client.beta.messages.toolRunner` runs a turn's `tool.run` calls
* via `Promise.all`. The toolset below is safe under either model —
* {@link betaBashTool} serializes its persistent shell internally and the FS
* tools are independent per call — but {@link betaEditTool}/{@link betaWriteTool}
* cannot synchronize concurrent writes to the *same* file across processes, so a
* multi-edit turn touching one path is still subject to inherent FS lost-update
* races. Custom tools that close over mutable state should do their own queueing.
*/
export declare function betaAgentToolset20260401(ctx: AgentToolContext): BetaRunnableTool[];
/**
* Resolve `p` against `ctx.workdir`. Absolute and relative inputs go through
* the same canonicalise-then-contain check — an absolute path that lands inside
* the workdir is permitted, only paths that resolve *outside* are rejected.
* Every symlink in `p` (including the leaf, even a dangling one) is resolved
* before the workdir check, and the resolved path is what the tool then operates
* on, so a symlink inside the workdir that points outside it can neither pass
* the check nor be followed afterwards. See the trust model on
* {@link AgentToolContext}.
*
* Residual TOCTOU: a component could still be swapped for a symlink between this
* call and the eventual `fs` operation. Closing that fully needs per-component
* `O_NOFOLLOW`/`openat`, which Node does not expose ergonomically; the same
* residual exposure exists in `tools/memory/node` and is why a sandbox is still
* recommended for the toolset as a whole.
*/
export declare function resolvePath(ctx: AgentToolContext, p: string): Promise<string>;
/**
* A persistent /bin/bash process. State (cwd, env, background jobs) survives
* across exec() calls. Uses pipes rather than a PTY so input is never echoed.
*/
export declare class BashSession {
#private;
constructor(dir: string, env?: NodeJS.ProcessEnv);
/** Whether the underlying shell process has exited. */
get closed(): boolean;
exec(command: string, opts?: {
timeoutMs?: number;
signal?: AbortSignal | null | undefined;
}): Promise<{
output: string;
exitCode: number;
}>;
close(): void;
}
export declare function betaBashTool(ctx: AgentToolContext): BetaRunnableTool;
export declare function betaReadTool(ctx: AgentToolContext): BetaRunnableTool;
export declare function betaWriteTool(ctx: AgentToolContext): BetaRunnableTool;
export declare function betaEditTool(ctx: AgentToolContext): BetaRunnableTool;
export declare function betaGlobTool(ctx: AgentToolContext): BetaRunnableTool;
export declare function betaGrepTool(ctx: AgentToolContext): BetaRunnableTool;
//# sourceMappingURL=node.d.mts.map