Resolve the task sandbox from config.toml and add task --read-only#508
Resolve the task sandbox from config.toml and add task --read-only#508cubicj wants to merge 3 commits into
Conversation
…d-only (port openai#508) (#33) * feat(sandbox): resolve task sandbox from config.toml + add task --read-only (port openai#508) Port of @cubicj's openai#508. The plugin hardcoded the sandbox ("read-only"/"workspace-write"), ignoring sandbox_mode in ~/.codex/config.toml. Now the task path sends sandbox: null when the caller does not pin one, letting the app-server resolve it from config — the same mechanism already used for model. Explicit --write still forces workspace-write; review paths keep their pinned read-only. - buildThreadParams/buildResumeParams: sandbox ?? null (config-driven default) - task --read-only flag: per-run read-only pin, mutually exclusive with --write - stop-review-gate hook's internal task passes --read-only explicitly so the gate stays read-only regardless of config - rescue command/agent/skill docs updated for --read-only Merged onto the fork (conflicts in codex.mjs, codex-companion.mjs, rescue.md, fake-codex-fixture, runtime.test): kept our reasoning-validation + turn-timeout-ms + model aliases + --cwd, layered cubicj's sandbox-null + --read-only on top. Our fake-codex fixture already recorded sandbox in lastThreadStart/lastThreadResume, so openai#508's assertions pass as-is. Verified: npm run build clean; npm test 174/171 pass / 3 pre-existing (was 4 — the background test that previously asserted a foreign model-luna value now passes). All 6 new sandbox tests green. Co-Authored-By: Claude <noreply@anthropic.com> * fix(sandbox): fail closed when a read-only pin is not honored on resume (cycle-review critical) Codex adversarial review of #33 flagged — and a real-codex smoke confirmed — that Codex 0.144.6 keeps a loaded thread's sandbox and ignores thread/resume overrides: `task --read-only --resume-last` on a write-capable thread ran with workspace-write access, violating the --read-only contract. Verified empirically (codex-cli 0.144.6): write-thread -> resume with --read-only returned resolved.sandbox = workspaceWrite. The code never compared the resolved sandbox to the explicit pin. Fix: assertExplicitSandboxHonored() in runAppServerTurn compares an explicit read-only pin to the app-server's resolved sandbox and throws on mismatch — fail closed instead of running write-capable under a read-only flag. Only the read-only pin is enforced (a write request on a read-only resolved sandbox is a downgrade, not a safety leak). TDD: a resume-ignores-sandbox fake-codex behavior reproduces the leak (RED), the assertion turns it green. Also confirms the port's cubicj claim: a plain task with sandbox:null resolves to read-only from config (smoke: resolved readOnly), so stock behavior is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
|
@cubicj — just wanted to say thanks for this PR (and #506). I maintain a fork (axisrow/codex-plugin-cc) where the plugin has drifted enough from upstream that merging here has been slow, and your config-driven sandbox work is genuinely useful — I ported #508 into my fork this week. One thing came up while porting that's worth flagging back to you, since it's about the My fork's fix (in case it's useful): after the thread/start-or-resume response, compare the requested pin to the app-server's resolved sandbox and fail closed on mismatch — Fork PR (port + the guard), with attribution to this PR: axisrow#33. Pure sharing — take it, adapt it, or set it aside. Either way, thanks for the PRs; they're the kind of substantive contribution this repo needs more of. |
Ships accumulated fixes since fork.4: - #29 broker-busy hang (PR #30): bound broker handshake - #31 spawned-app-server handshake (PR #32): bound request() primitive - openai#508 config-driven sandbox + --read-only (PR #33) + resume-sandbox fail-closed - #34 model/effort in /codex:rescue launch line (PR #35) - #28 turn-timeout gaps (already in fork.4 base) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6207a5144
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@axisrow I've pushed a guard in the same fail-closed shape you described: after The silent ignore itself looks like an app-server issue — |
|
Thanks for reproducing it independently on 0.145.0-alpha.29 — that confirms the loaded-thread boundary and makes the guard feel a lot less like a one-off workaround. Appreciate you carrying it upstream. |
Summary
The plugin always overrides the user's configured
sandbox_modewith hardcoded sandbox values (#482).buildThreadParams/buildResumeParamsfall back to"read-only"whenever the caller passes nothing, and the task path hardcodes the--writeternary to"workspace-write"/"read-only", so asandbox_modein~/.codex/config.tomlnever reaches any job. Users who want config-driven sandboxes (e.g.danger-full-accessfor trusted delegation setups) end up patching the installed plugin by hand.Change
Task paths now send
sandbox: nullwhen the caller does not ask for a specific sandbox, letting the app-server resolve it fromconfig.toml— the same mechanism the plugin already relies on formodel(model: options.model ?? nullfalls back to the configured model today). An explicit--writestill forcesworkspace-write, and both review paths (native and adversarial) deliberately keep their pinnedread-onlysandbox.lib/codex.mjs:buildThreadParamsandbuildResumeParamschangeoptions.sandbox ?? "read-only"tooptions.sandbox ?? null.ThreadStartParams.sandboxis declaredSandboxMode | null, so this is type-clean.codex-companion.mjs: the task path changesrequest.write ? "workspace-write" : "read-only"torequest.write ? "workspace-write" : request.readOnly ? "read-only" : null.Because omitting
--writenow defers to config instead of guaranteeing read-only, the same change adds atask --read-onlyflag as the per-run way to pin the read-only sandbox: it is mutually exclusive with--write(a combined call fails with a clear error), survives the background request round trip into the detached worker, and applies to resumed threads. The rescue command/agent/skill texts are updated one line each so that read-only intent means passing--read-onlyrather than merely omitting--write. The stop-review-gate hook's internal task now passes--read-onlyexplicitly, so every review-shaped run — native review, adversarial review, and the stop-time gate task — keeps a pinned read-only sandbox regardless of config.Behavior only changes for users who set
sandbox_modeinconfig.toml: when it is absent, the app-server's own default for an unset sandbox remains read-only, so stock installations keep today's behavior. Inheriting a permissive mode from config is a deliberate user choice made in their own Codex configuration, and #506 (resolved model/effort/sandbox capture, from #481) makes the inherited value visible on every job record. An explicit per-run escalation flag (#145, PR #147) is complementary to this config-driven default and intentionally out of scope here.Verification
thread/startwithsandbox: nullandconfig.tomlsandbox_mode = "danger-full-access"returned the config-resolved{"type":"dangerFullAccess"}sandbox in the response echo; explicit values still override.npm test: 94/94 passing. The fake-codex fixture now recordsthread/start/thread/resumeparams, with assertions covering every path: a plain task sendssandbox: null,--writesendsworkspace-write,--read-onlypinsread-onlyon fresh, resumed, and background-worker threads,--write --read-onlyis rejected, and native review, adversarial review, and the stop-review-gate hook's task all still sendread-only.npm run build(typecheck viatsconfig.app-server.json): clean.git diff --checkagainst the base commit: clean.Related Issues
Fixes #482