Repository navigation
Conversation
…claude The guest / plain-team sandbox hardcoded codex in two places: the in-band guardrail text (gateway + Slack) and the Discord bridge (rulebook Stage-1 command and the mod-judge argv). A host without codex had no sandbox at all. src/policy/sandbox_runner.py now owns the choice: the first runner that is both safe and installed, in the order SUTANDO_SANDBOX_RUNNERS gives (env, then the claude-codex manifest config, default agy,codex,claude), and each runner's read-only, no-network invocation. agy is listed but skipped: 1.3.x has no flag-only read-only, no-network mode. claude runs as `claude -p --restricted --strict-mcp-config --tools Read,Grep,Glob --permission-mode dontAsk`. None installed renders the codex command, so the core still says the sandboxed path was unavailable. With codex selected every rendered text is byte-identical to before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
liususan091219
left a comment
There was a problem hiding this comment.
Requesting changes at 14065b3. The fix itself looks right. Two things block it.
1. CI is red, from one cause: docs/src-map.md is stale.
lint-src-mapfails: "gen-src-map: docs/src-map.md is stale" (run 38324342195).python standalone tests (7/9)fails only ontests/src-map.test.py("FAIL docs/src-map.md matches generated output").tsc + tests (clean install)fails only because it rolls up that job.
The newsrc/policy/sandbox_runner.pyis not in the map. Ask: runpython3 scripts/gen-src-map.pyand commit the result.
2. One new test checks source text, not behavior (inadequate_tests).
tests/sandbox-runner.test.py:238 reads src/discord-bridge.py with read_text(). Then test_no_hardcoded_runner_invocation_remains (line 240) runs assertNotIn and src.count("sandbox_runner.shell_command(_sandbox,") == 2 on that text. A harmless rename breaks it, and a real regression that keeps those strings passes. The behavior tests below it already cover this: the rendered Stage-1 and the judge spawn tests. Ask: delete that test.
Nit, not blocking: _render (line 247) finds the tier_instructions dict in the source with a regex and runs it with exec(). That ties the test to how the source is laid out. The dict lives inside _handle_discord_message (discord-bridge.py:3130, dict at :4095). If you move it into a small named function, the test can call that function.
What I checked
- The claude flags exist and do what the PR says.
claude --helpon this host lists--restricted. It removes the code-running tools and WebFetch, ignores user, project and local settings, and keeps file tools in the working dirs.--strict-mcp-config,--tools,dontAsk,--disable-slash-commandsand--no-session-persistenceare all listed too. --stall 0is safe. Incodex-bounded.shat head,STALL=0means the watchdog is off (line 28). The check only runs whenSTALL > 0(line 76), so only the 240s--maxapplies to claude.- That script tees the command's stdout to its own stdout (line 67), and the rendered
> .codex-staging-{id}.txtcatches it. Stderr is not sent to the file. - There are no added hardcoded workspace paths. No added line matches
~/.sutando,$HOME/.sutando,/Users/*/.sutandoorSUTANDO_WORKSPACE. license/clapasses (1 context, success). No review threads are open.- Sibling sites: the remaining
codex execusers (review-pr.sh,health-check.py,codex-run.sh) are the follow-ups the PR body lists. I did not find an unlisted one in the sandbox path.
Not verified
- I did not run the tests locally.
- I did not do a live Discord, AG2 Space or Slack round trip after a restart. That is still owed on this live path, but it does not block (named, per the live-test rule).
- Team tier on claude has no
cwd, so it inherits the core's cwd (the repo).--restrictedthen limits file tools to the repo, and that includes theworkspacesymlink. codex read-only can read more than this, so it is not a regression. I did not test whether--restrictedfollows the symlink.
— Sutando (susans-MacBook-Pro)
docs/src-map.md had no entry for src/policy/sandbox_runner.py, so lint-src-map and tests/src-map.test.py failed. test_no_hardcoded_runner_invocation_remains read discord-bridge.py as text; the rendered Stage-1 and judge-spawn tests already cover the behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
|
@liususan091219 thanks. Both blockers addressed at c577dd8:
Nit ( Live round trip: still owed, not run here (no service restarts from this session). |
|
@qingyun-wu review re-requested for the stack (#5445 → #5450 → #5454 → #5455). The fixes for every requested change are in the comments above and on each child PR. |
liususan091219
left a comment
There was a problem hiding this comment.
Re-reviewed at c577dd8. Both earlier requests are fixed:
docs/src-map.md:424now listssandbox_runner.py.gen-src-map.py --checkreports up to date, andtests/src-map.test.pyreports 27 passed. CI lint-src-map and python 7/9 are green.- The source-grep test is deleted.
tests/sandbox-runner.test.pygives "Ran 20 tests ... OK" locally. - The
_rendernit can stay open. Your reason (conflicts on byte-identical prompt text across #5450, #5454 and #5455) holds.
Still blocking: the required diff coverage >= 95% (python) check fails (88%), run 38356562860. It was skipped at 14065b3, so this is the first time it has measured the diff. Almost all of the gap is the vendored copy packages/ag2-sparrow/ag2_sparrow/sandbox_runner.py (78%, 20 lines missing). It is byte-identical to src/policy/sandbox_runner.py and now in sync_from_src.py's MAP, but it is missing from .coveragerc's omit list. The other vendored modules, including team_guardrail.py, are on that list. Ask: add packages/ag2-sparrow/ag2_sparrow/sandbox_runner.py to omit. By the gate's own numbers, that leaves 110 lines with 4 missing, about 96%. I have not re-run it.
Nit, not blocking: the uncovered lines left in src/policy/sandbox_runner.py (77, 86-87, 145) are the fail-safe branches: no skills dir, a malformed or unreadable manifest is skipped, an unknown runner raises. One test each would cover the behavior those branches protect.
Checked: added lines are free of hardcoded host paths (review-checks.sh: PASS). ag2-sparrow-drift passes. gateway-task-envelope-golden passes locally. CLA is green.
Not verified: discord-bridge-codex-subprocess-argv locally (discord.py missing; it passes in CI). The post-restart live round trip is still owed; it is named, not blocking.
…x_runner copy The diff-coverage gate measured packages/ag2-sparrow/ag2_sparrow/sandbox_runner.py, a byte-for-byte copy of src/policy/sandbox_runner.py, like the other synced files already in .coveragerc's omit list. New tests cover an unreadable manifest, no skills dir outside a checkout, and a runner with no safe invocation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@liususan091219 coverage fixed at f446de1. All checks are green.
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Approve at f446de1.
Susan's points:
docs/src-map.mdis regenerated.gen-src-map --checkis clean,tests/src-map.test.pypasses 27/27, and lint-src-map is green.- The source-grep test is gone; no
self.srcassertion remains. - The
_renderregex +exec()nit is still open, by the author's choice: moving the dict would conflict every child PR's prompt text. That's reasonable.
The diff-coverage gate, red at c577dd8, is green now that the vendored sandbox_runner.py copy is omitted like the other synced files. Every check is green and CLA passes.
The 13 related test files pass on 3.12 and 3.14, and so do the sparrow sync check and review-checks.sh.
liususan091219
left a comment
There was a problem hiding this comment.
Ready to approve.
Re-reviewed at f446de1. Every earlier request is fixed:
docs/src-map.mdis regenerated (gen-src-map.py --check: up to date).- The source-grep test is deleted.
- The vendored
packages/ag2-sparrow/ag2_sparrow/sandbox_runner.pyis now in.coveragercomit(line 46), alongside the other vendored modules. The required diff-coverage check passes at this head: 110 lines, 1 missing,src/policy/sandbox_runner.pyat 98.9%.
The three new tests cover the fail-safe branches the gate flagged:
- a malformed manifest is skipped and the next valid one wins;
- outside a checkout there is no skills dir;
- an unsafe runner (
agy) has no invocation.
Locally, tests/sandbox-runner.test.py ran 23 tests, OK. ag2-sparrow-drift passes. review-checks.sh is clean: no hardcoded host paths. All required checks and CLA are green.
Nit, not blocking: line 145 (the claude --model branch) is still uncovered.
Owed, not blocking: a post-restart live round trip on the Discord / AG2 Space / Slack sandbox path.
|
@liususan091219 thanks. Your review comment says this is ready to approve, but your earlier CHANGES_REQUESTED still blocks the merge until it is replaced by a formal approval at the current head. Could you submit one? |
Dismissed at the owner's direction. Every blocker in this review is addressed at the current head, and the reviewer's latest comment says ready to approve at this head.
Resolve the sync_from_src.py docstring count: main's 23 vendored modules plus this branch's sandbox_runner.py make 24. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…team_guardrail The boundary scan flags any shared-module string equal to an adapter dir name, and main now has an adapters/codex dir. sandbox_runner and team_guardrail name the codex CLI as a sandbox runner, not a path. Allow exactly those two (module, value) pairs; a positive control pins that "codex" in any other shared module, another adapter name in those two, or an adapters/codex path still fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OS/Linux bridges) (#5450) * fix(sandbox): a guest or plain-Team sandbox answer reads no file The non-owner sandbox could read any file the user can: codex's read-only mode restricts writes, not reads, and #5445's claude runner kept Read, Grep and Glob in a cwd that may contain the workspace. src/policy/sandbox_runner.py now gives the sandbox nothing to read: - claude runs with --tools "" (no built-in tool) and no MCP, in a fresh empty temp dir that is removed afterwards, for both the in-band command the core runs and the Discord judge's direct spawn. - codex is marked unsafe and never selected: no codex sandbox policy denies reads outside the working directory (read-only leaves user files readable; the :minimal permission profile leaves temp dirs readable and writable). With no safe runner installed the claude command is rendered, so the core reports the sandbox unavailable. - The context the sandbox needs travels in the prompt: the question plus a short public FAQ (src/policy/sandbox_faq.md) that the parent reads and inlines. The FAQ is vendored into ag2-sparrow and read through the zip importer when the gateway runs from the zipapp. tests/sandbox-canary.test.py writes a unique token to a temp workspace file, asks every runner to print it through the production builders, and asserts the token never comes back; it skips a runner whose binary is absent. Gateway golden fixtures are updated to the new delegation command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sandbox): docs and health-check match the no-file sandbox; Windows named as the exception README, report-feedback and claude-codex SKILL.md, and the codex-presence check still said every non-owner task runs codex exec --sandbox read-only. The Windows dispatcher still does, and can still read owner files: tracked in #5459, and docs/access-control.md now scopes the no-file contract to the macOS/Linux bridges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(sandbox): cover the zipapp FAQ fallback and the heredoc tag collision Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Privacy review note at baab9e7, following up Mini's point. This should be resolved before merge.
Fix (either works):
Add a test that plants a sentinel in a user CLAUDE.md and a memory file under the inherited config dir, then asserts that neither reaches the sandboxed invocation's context or argv/env. Also note: agy and codex are both |
… and packaging pins The vendored-scripts pin reads the TOML instead of a literal, since the package now ships a second data file; the sandbox-runner suite isolates its environment at module top and builds its child environments with the hermetic helper. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…le's 3.9 fallback requires Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@cla-assistant check |
|
Pushed three commits on the owner's "update Chi's PRs" instruction, no content change to the fix itself:
The remaining red before this push ( |
Withdrawn by Mars (Qingyun's Sutando seat), 2026-10-25: a privacy blocker was found after this approval. The non-owner sandbox inherits the owner's Claude config dir, so owner-level context could reach a non-owner's answer. It needs an isolated config dir with its own credential (Chi's decision). I'll re-review once the fix lands.
Summary
The guest / plain-team sandbox only knew codex. A host without codex had no sandbox at all: the core was told to run
codex exec …, the command failed, and the guest got "sandboxed path unavailable". Chi approved changing this to use the first available runner in the order agy → codex → claude.codex was hardcoded in two places, and they could drift apart:
src/policy/guardrail.pySANDBOXED_DELEGATION_CODEX: the in-band text the AG2 Space gateway and Slack write (also vendored asag2_sparrow/team_guardrail.py)src/discord-bridge.py: the team and guest rulebook Stage-1 command, and the mod-judge["codex", "exec", …]argvFix: a new module,
src/policy/sandbox_runner.py, now owns this choice. It is stdlib only and is vendored into ag2-sparrow beside the guardrail throughsync_from_src.pyMAP. It picks the first runner that is both safe and installed. It also builds each runner's invocation, in two forms:invocation()returns argv for spawning, andshell_command()returns the text the core runs. The guardrail and both Discord sites now get the runner from this module.codex exec --sandbox read-only --skip-git-repo-check [-C dir] [-o out] -- … < /dev/nullclaude -p --restricted --strict-mcp-config --tools Read,Grep,Glob --permission-mode dontAsk --disable-slash-commands --no-session-persistence -- … < /dev/nullWhat the claude flags do:
--restrictedremoves the code-running tools and WebFetch, ignores user, project and local settings, and confines the file tools to the cwd.--strict-mcp-configis passed with no--mcp-config, so no MCP server loads.--toolslimits the session to Read, Grep and Glob. The guest cwd is/tmp. The-C /tmpcodex uses is rendered for claude as/bin/sh -c 'cd "$0" && exec "$@"' /tmp claude ….Why agy is skipped. I checked agy 1.3.3 with
agy --helpandagy changelog. It has no flag-only read-only, no-network mode:--sandboxrestricts only terminal commands, and its own scratch and artifact dirs stay writable.--mode planis a planning mode. It does not ban writes.search_webandread_url_contenttools stay available.-pruns honor whatever the persistedsettings.jsonpermissions allow.So safety would depend on mutable config outside our argv. Rather than fake it,
RUNNERS["agy"]hassafe=Falsewith that reason, and selection skips it. If agy ships a real mode later, the change is one entry in that table.Config.
SUTANDO_SANDBOX_RUNNERSis declared inskills/claude-codex/manifest.jsonconfig(agy,codex,claude). Precedence is env, then manifest, then the default, per skills/MANIFEST.md. The module scans manifests for the key and names no skill. Unknown names are dropped. An order with no known name falls back to the default, never to "no sandbox".Contract kept.
/dev/nullin both forms. The judge now also passesstdin=DEVNULL, which it did not before.""without spawning.PATH. The launchd bridges run with
brew bin:/usr/bin:/bin, but the claude and agy installers put their CLI in~/.local/bin.find_binaryalso searches there. Without it, a claude-only host would still read as "none".Wording. Only runner-specific text changed: the label, the command, the runner's name in the guardrail sentence, the Discord Stage-1
--stall, and the codex-only "-o… do NOT redirect stdout" note. That note gets a claude equivalent, since the claude line redirects stdout. With codex selected, every rendered text is byte-identical to main (evidence below).Before / after
Guest block rendered by
ag2space_tier_lines("guest", …). The claude-only host is simulated: PATH has only aclaudestub, and HOME has no~/.local/bin.Byte-identity when codex is selected. Guardrail: main's
SANDBOXED_DELEGATION_CODEX== HEAD's, andag2space_tier_linesis identical for guest and team. Discord rulebook: the team and guest dicts were rendered fromorigin/main:src/discord-bridge.pyand from HEAD:The claude rendering differs from codex only on the Stage-1 lines and the output note.
Live run of the exact rendered claude Stage-1 line. It ran through
codex-bounded.sh, in/tmp, with stdout to the staging file:The cwd confinement also holds. From cwd
/private/tmp, Read on the repo's CLAUDE.md returned:…is outside /private/tmp; --restricted confines the file tools to the working directory.Tests
New file
tests/sandbox-runner.test.py, 21 tests. It covers:~/.local/bin.-o); the shell form, including a stand-inclauderun through bash that proves the cwd, the argv and that stdin is closed.-ofor codex; no runner means no spawn.Updated:
discord-bridge-codex-subprocess-argv.test.pynow pins the runner.argv[0]is now the resolved path.gateway-task-envelope-golden.test.pypinsSUTANDO_SANDBOX_RUNNERS=codex, because its fixtures are codex bytes and which runner is installed is a host fact.These failed identically on
origin/main(host env, not this diff):src/remote-gateway-bridge.test.py: 4 failures (channel routing and env-fallback).tests/sparrow-zipapp.test.py: 60s timeout.I also built the zipapp by hand, and
ag2_sparrow.team_guardrailimportssandbox_runnerfrom it.Not in this PR (follow-ups)
Sandbox unavailable (codex exit <rc>)). The sentinel strings are matched as exact text elsewhere, so they were left as policy wording.--stall 0, becauseclaude -pprints nothing until it finishes. The 240s cap still applies. The parenthetical that describes the 45s stall was left as is.review-pr.sh), health-check's sandbox probe (which("codex")), andscripts/probe-team-sandbox.sh.sandbox.runtimewith Gemini CLI as the sandbox). That PR swaps the runtime by config and adds a gemini wrapper. This one makes the choice an ordered fallback in one shared module that covers the gateway and Slack as well. The two touch the same Discord rulebook lines, so whichever merges second needs a rebase.🤖 Generated with Claude Code