Repository navigation
Conversation
…tem (refs CodebuffAI#1450) Adds user-controllable, enforced execution boundaries so the agent cannot run arbitrary shell commands or touch files outside the project by default policy, rather than relying on model instructions. New per-run, immutable ExecutionPolicy (sdk/src/permissions/execution-policy.ts): - no module-global policy state (concurrent runs cannot cross-contaminate) - classifyTool() maps every tool to a capability; unknown names (MCP, custom, overrides) are 'external' and denied under strict policies unless the host opts in via allowExternalTools - authorizeToolCall() evaluated before ANY dispatch path in handleToolCall - noShell denies shell; readOnly/projectOnly deny unrestricted shell; askShell requires a correlated approval callback - realpath-based, symlink-aware containment + platform-correct (Windows) case/separator handling - guardFileSystem() wraps the run's fs adapter as defense in depth CLI: --no-shell, --ask-shell, --project-only, --read-only (both products). Approval lifecycle: new correlated shell-approval bridge (unique ids, FIFO, timeout/abort/shutdown -> deny, never an implicit approve). Fixes apply_patch target resolution (operation.path, matching applyPatchTool). Tests: 34 passing (25 policy + 9 approval-bridge).
|
Good instincts here: moving enforcement out of the model and into the runtime is the right call, the no-global-policy-state design is correct for an embeddable SDK, the symlink-aware Three things that need work before this is portable as-is:
Given the size (+1337/-10) and that it rewrites a security-critical dispatch path in |
What this does
Adds enforceable, user-controllable execution boundaries for shell commands and filesystem access, addressing #1450. The permission decision is enforced by the runtime, not delegated to the model.
Four CLI flags are wired through both products and enforced at the SDK tool-dispatch layer:
--no-shell— deny shell execution entirely--ask-shell— require an explicit human approval before each command--project-only— confine filesystem reads/writes to the project root--read-only— deny write operationsImplementation
sdk/src/permissions/execution-policy.ts(new) — a per-run, immutable policy:ExecutionPolicyinrunOnce()and captures it in the tool-dispatch closure, so two concurrent SDK runs cannot influence each other's permissions.classifyTool()maps every tool to a capability (shell/fs-read/fs-write/network/agent/inert/external). Unrecognized names — MCP tools, custom tool definitions, host overrides — classify asexternaland are denied while any restriction is active unless the host opts in viaallowExternalTools: true.authorizeToolCall()is evaluated inhandleToolCall()before any dispatch path (MCP, custom tools, overrides, built-ins), so none bypasses policy.noShelldenies shell unconditionally.readOnlyandprojectOnlyalso deny unrestricted shell execution, because an unrestricted shell can read/write anywhere outside the SDK's control.askShellrequires a callback and denies when none is configured.guardFileSystem()wraps the run's filesystem adapter as defense in depth: reads/writes outside the root (or any write in read-only mode) are rejected even if a dispatcher branch missed a check.common/src/utils/shell-approval-bridge.ts(new) — correlated approval lifecycle. The existingAskUserBridgeholds a single pending request, so a second concurrent request silently replaces the first. The new bridge correlates by unique id, serializes requests FIFO, and resolvesdenyon timeout, abort, or UI shutdown — neverapprovewithout an explicit matching submit.sdk/src/run.ts— thread the policy throughrunOnce()/handleToolCall(); fixapply_patchtarget resolution to useoperation.path(matchingapplyPatchTool()), which the earlier draft checked incorrectly.CLI — flags in
cli-args.ts; approval callback bound to the bridge incodebuff-client.ts; approvals surfaced through the existing question form.Why
Reported in #1450: the agent could run shell commands and reach files outside the project with no user-configurable gate, and destructive operations had to be recovered after the fact. Prompting the model not to do this is not enforcement.
Verification
sdk/src/permissions/__tests__/execution-policy.test.ts(per-run isolation, shell modes incl. background, fs modes, external-tool denial, symlink escape, fs-adapter guard, Windows containment) and 9 incommon/src/utils/__tests__/shell-approval-bridge.test.ts(id matching, timeout, abort, concurrent requests, queued-id rejection, shutdown cancel, stale approval).tsc --noEmitonsdkandcommon: no new type errors in modified files. Three pre-existing errors in untouched files (common/src/util/messages.ts,packages/agent-runtime/src/tools/stream-parser.ts,sdk/src/impl/llm.ts) are unrelated AI-SDK type mismatches and are present onmain.parseArgsfor both flag states.Known limitations (not yet done)
--read-onlyand--project-onlydeny unrestricted shell rather than sandboxing it. Genuinely allowing shell under those modes needs an OS-enforced boundary (the repo's existingsponsored-sandbox/sponsored-rooted-filesystemprimitives are a candidate). This PR does not adopt them.--no-shelltest not included — the public mirror is missing thecli/test/setup-scm-loader.tspreload, so the CLI test suite does not boot here.Scope note
I'd rather not guess at the sandbox direction. If maintainers want the OS-level containment pursued (reusing the existing sandbox primitives), say so and I'll follow that shape rather than inventing a second mechanism.
Inference for this work was provided by FreeInference.org. FreeInference did not direct, review, or sponsor this contribution; they provide inference to researchers and others who might not otherwise have access. If anything here was useful, consider sponsoring or contributing to FreeInference.org.