Skip to content

feat(agent-core-v2): add OS-level sandbox for Bash and file tools#1934

Open
xuwenhao wants to merge 1 commit into
MoonshotAI:mainfrom
xuwenhao:feat/agent-sandbox
Open

feat(agent-core-v2): add OS-level sandbox for Bash and file tools#1934
xuwenhao wants to merge 1 commit into
MoonshotAI:mainfrom
xuwenhao:feat/agent-sandbox

Conversation

@xuwenhao

Copy link
Copy Markdown

Summary

Add an optional OS-level sandbox (comparable to claude-code's sandbox-runtime and codex's workspace-write mode) configured through a new [sandbox] section in config.toml. It addresses two risks: the agent reading sensitive local files (ssh keys, .env, credentials) and accidental destructive writes/deletes outside the workspace.

  • New session/sandbox domain (L3): config section + schema, policy resolution, bwrap (Linux) and seatbelt (macOS) backends, and a session-scoped ISandboxService that decides per command (sandboxed / excluded / unsandboxed / blocked).
  • Bash integration: the tool wraps its shell argv when sandboxed — / is bound read-only, workspace roots + tmpdir are writable, deny_read paths are masked (tmpfs over dirs, /dev/null over files), deny_write subtrees are re-bound read-only, and the network is off by default (--unshare-net). Denial signatures in the output get an explanatory hint appended.
  • Permission chain (3 new policies): sandboxed Bash auto-approve (auto_allow_sandboxed_bash, default on); sandbox filesystem hard denies (deny_read / deny_write / read-only-mode writes / sensitive files, promoted from ask to deny) evaluated before auto/yolo approvals; file tools accessing paths outside the writable roots fall back to an ask prompt.
  • Fail-open by design: when no backend is available (e.g. bwrap not installed, or Windows) the CLI warns once and runs unsandboxed; require = true switches to fail-closed. Commands matching excluded_commands bypass the sandbox and go through the normal permission chain.
  • Network domain allowlisting (allowed_domains / allow_unix_sockets) is reserved in the schema for a follow-up phase (proxy-based, or adopting @anthropic-ai/sandbox-runtime) and currently emits a one-time warning.

Example config:

[sandbox]
enabled = true
mode = "workspace-write"   # or "read-only"
filesystem.deny_read = ["~/.ssh", "~/.aws"]
network.enabled = false

Full reference: docs/en/configuration/sandbox.md (+ zh translation).

Test plan

  • 49 new unit tests (policy resolution, bwrap argv, seatbelt profile, config round-trip, service decide branches, 3 permission policies + chain order)
  • 6 real-bwrap integration tests (skipped when bwrap is absent): workspace write OK, $HOME write denied, deny_read file/dir masked, deny_write subtree read-only, network blocked
  • node scripts/check-domain-layers.mjs OK; tsc --noEmit OK
  • Full agent-core-v2 suite: 3694/3696 pass — the 2 skillCatalog failures reproduce on a clean HEAD and are unrelated

Notes / follow-ups

  • macOS seatbelt profile generation is unit-tested but not yet verified on real macOS hardware
  • Background Bash tasks don't get the denial hint appended (foreground only)
  • External hooks and Grep's rg subprocess are intentionally not sandboxed (documented); hook parity with claude-code

Add an optional [sandbox] config section that wraps Bash command
execution in an OS-level sandbox (bubblewrap on Linux, sandbox-exec on
macOS) and tightens file-tool path policies through the permission
chain:

- New session/sandbox domain (L3): config section, policy resolution,
  bwrap/seatbelt backends, and a session-scoped ISandboxService that
  decides per command (sandboxed / excluded / unsandboxed / blocked).
- Bash tool wraps its shell argv when sandboxed and appends an
  explanatory hint when the output shows sandbox-denial signatures.
- Three new permission policies: sandboxed Bash auto-approve
  (auto_allow_sandboxed_bash), sandbox filesystem hard denies
  (deny_read / deny_write / read-only mode / sensitive files), and
  outside-workspace ask.
- Fail-open with a one-time warning when no backend is available;
  require = true switches to fail-closed.
- Network domain allowlisting config is reserved for a follow-up
  (Phase 3); setting allowed_domains emits a warning for now.
- Docs: docs/en|zh/configuration/sandbox.md.
@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1a2404e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a2404e78d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return {
mode,
writableRoots: dedupe(writableRoots.map(expand)),
denyRead: dedupe((config.filesystem?.denyRead ?? []).map(expand)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mask sensitive paths before auto-approving Bash

When sandbox.enabled is on with the default auto_allow_sandboxed_bash = true, SandboxedBashApprovePermissionPolicyService approves sandboxed Bash without prompting, but Bash does not declare file accesses for SandboxFsDenyPermissionPolicyService to inspect. Because denyRead defaults to only the user-specified list here, a sandboxed command such as cat ~/.ssh/id_rsa or cat .env is still readable through the backend's read-only / bind unless the user preconfigures filesystem.deny_read, so the advertised sensitive-file protection does not apply to Bash.

Useful? React with 👍 / 👎.

Comment on lines +130 to +131
const additionalDirs =
cwd === workDir ? this.workspace.additionalDirs : [cwd, ...this.workspace.additionalDirs];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep arbitrary Bash cwd out of writable roots

When the model supplies a cwd outside the session workspace, this prepends that path to additionalDirs; resolveSandboxPolicy then turns every additional dir into a writable root, and the sandboxed Bash auto-approval policy skips the prompt. For example, Bash({ cwd: "/home/alice", command: "touch .pwn" }) gets writable access to /home/alice even though it is outside the configured workspace, defeating the workspace-write boundary; only include cwd when it is already inside an allowed root, or force the normal approval path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant