Coding agents and shell tools
A workspace agent with bounded shell output and host-owned tool policy.
shellTools builds the six workspace tools any agent can hold: bash, read_file, grep, write_file, edit_file, and read_tool_output. Each one caps its raw output before anything else looks at it, and RunOptions.toolPolicy decides, outside model output, which calls are allowed to run at all. @caveman-ai/coding-agent is the whole interactive session built on them.
import { agent, auto, run, shellTools } from "@caveman-ai/agent";
const reviewer = agent({
id: "reviewer",
instructions: "Read the workspace and explain what changed.",
model: auto(),
sandbox: "host",
tools: shellTools({ workspace: process.cwd(), tools: ["bash", "read_file", "read_tool_output"] }),
});
await run(reviewer, "count the lines in notes.txt");Unreleased: this page documents @caveman-ai/agent 0.2.0 from source at commit 311be40 in caveman-ai/agent-sdk. Build it from the checkout as described on Install.
workspace is a hard boundary: every candidate path is canonicalized and compared against the workspace's real path, so a symlink out of it is refused. sandbox: "host" is required here, because these closures need real host access. Host execution runs tool code with your own privileges, and Sandbox and execution is where the containment options live.
Bounded output#
Every tool truncates its own raw output before any transform runs, so the cap on a runaway cat holds with the engine absent. cat of a 4,000-line file, captured from a scripted run:
line 3998
line 3999
[caveman-code: output capped from 38889 captured bytes; use read_tool_output with handle tool_7eabe62d…]| Tool | Cap, raw bytes |
|---|---|
read_file, bash, read_tool_output | 24,000 |
grep | 16,000 |
write_file, edit_file | 2,000 |
Override any of them with outputCaps. The handle appears when read_tool_output is one of the tools you selected, and an over-long result is then stored and pageable. Leave that tool out and a capped result says it was capped and stops there. The store is in memory for the life of the agent, 8 MiB per entry and 16 MiB in total, oldest evicted first.
bash times out at 120 seconds and captures at most 4 MiB. grep returns at most 200 matches. Interactive bash sessions are local-backend only: at most 8 at a time, 30 seconds of wait per read, 64 KiB of input.
Deciding which calls run#
toolPolicy is your authorization decision. It runs after the kernel's own admission checks (deadline, budget, caps, breakers, sandbox posture, argument shape) for every declared tool call, in the root agent, in subagents, and in nested calls from a composite tool. Framework cave_* tools bypass it.
await run(reviewer, "count the lines", {
toolPolicy: ({ name, effect, agentPath }) =>
effect === "write" ? { deny: "read_only_review" } : undefined,
});policy: {"name":"bash","effect":"external","agentPath":[]}
policy: {"name":"write_file","effect":"write","agentPath":[]}
receipt.tools: [
{ "name": "bash", "calls": 1, "errors": 0 },
{ "name": "write_file", "calls": 1, "errors": 1, "denied": 1 }
]A denied call stops before execution. The model reads cave_tool_denied:read_only_review as the tool result, the receipt counts it under denied, and the run continues. undefined or { allow: true } admits the call, and a policy decides admission alone, because the durable journal binds each call to the digest of the arguments it was admitted with.
The input carries runId, agentId, agentPath (the subagent tool names leading to this call), toolCallId, parentToolCallId for a nested call, name, effect, and the validated args.
An answer the runtime cannot read ends the run before the tool. A policy that throws, returns a malformed decision, or takes longer than TOOL_POLICY_TIMEOUT_MS, which is 10,000 ms, stops there: cave_tool_policy_failed, cave_tool_policy_decision_invalid, or cave_tool_policy_reason_invalid for a deny code outside [a-z][a-z0-9_]*. Deny codes are short identifiers rather than tenant text, so receipts and journals stay content-blind.
decideToolCall(policy, input) is the same evaluator, exported so a deployment can test its own policy on its own:
import { decideToolCall } from "@caveman-ai/agent";
const decision = await decideToolCall(policy, {
runId: "r1", agentId: "reviewer", agentPath: [], toolCallId: "c1",
name: "write_file", effect: "write", args: { path: "out.txt" },
});
console.log(decision);{ block: true, reason: 'cave_tool_denied:read_only_review' }On a durable resume the policy is evaluated again, before replay. Denying a call the crashed attempt already settled ends the resume with cave_durable_tool_replay_incomplete rather than skipping it quietly: Durable runs.
The interactive coding agent#
npm install <checkout>/packages/agent <checkout>/packages/coding-agent
npx caveman-code --workspace .Usage: caveman-code [options]
Options:
--workspace <path> workspace root (default: current directory)
--model <id> provider/model override
--observe-only disable Cave runtime transforms
--max-cost-usd <usd> best-effort per-turn public-catalog spend cap
--no-start-runtime probe runtime without trying to start it
-h, --help show helpThe library form is the same session, and the repository's whole examples/coding-agent is this:
#!/usr/bin/env node
import { createCodingAgent, runCodingSession } from "@caveman-ai/coding-agent";
export async function main(options = {}) {
const agent = createCodingAgent({ workspace: options.workspace ?? process.cwd() });
return runCodingSession({ agent, ...options });
}
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
main().catch((error) => {
process.stderr.write(`caveman-code: ${error instanceof Error ? error.message : error}\n`);
process.exitCode = 1;
});
}createCodingAgent returns { definition, plan, workspace, modelID, toolMode, samples, close() }. The definition is an ordinary agent() with sandbox: "host", reasoning: "off", the six shell tools, and a default efficiency plan that only uses routes whose output can be recovered exactly. runCodingSession adds the prompt loop, the per-turn bill, and the observe-only banner.
Useful options on createCodingAgent: model (provider/model, else CAVE_MODEL, else the configured provider), instructions appended to the built-in coding instructions, outputCaps, executionBackend to run the tools elsewhere, memory, and toolMode: "programmatic" to collapse the six tools into one code cell (Programmatic tools).
runCodingSession takes maxCostUsd or budget (not both), breakers, modelRouter, cacheRetention, onNotice, and cave: "off" to skip the local runtime entirely.
Limits#
Without the local Caveman engine and gateway, a session runs straight to your provider and announces that it is in observe-only mode on every prompt. Provider usage and local context estimates still work; context transforms and gateway telemetry are off. Reductions a session does report are inferred, which Numbers and limits defines, and none of them become verified savings.
The tool output store never crosses a process restart, so a handle from an earlier session is gone. Two coding agents in one workspace can stale each other's reads; give each one its own worktree.