fix(codex): let a Bedrock-backed codex read ~/.aws/config inside its credential mask - #9329
Conversation
|
Intent: Let a Bedrock-backed codex authenticate inside the enforced-adapter sandbox by excluding |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified against the repository. Every mechanism the PR cites pre-exists ( First-Principles-Verdict: PASS Verify on a real macOS host (codex + Bedrock, including What this change shipsIntent: Make a Bedrock-backed codex able to start a session at all, by letting it read the one file it authenticates through while its credential directory stays masked — a FIX. Inventory (8 items)
[FIRST-PRINCIPLES-REVIEWED] 53d93d8 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Both facts I needed are confirmed: the mask ( Design-Verdict: CONCERNS The expose projection re-derives home anchoring instead of inheriting the mask's, so the fix silently no-ops on the hosts the mask dual-anchors for. Watch
Suggestions
[DESIGN-REVIEWED] 53d93d8 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe only candidate hinges on macOS Seatbelt resolving symlinks before matching The relevant comment ( Under that model, a codex child opens I cannot re-derive (a)/(b)/(c) at 80+ from code I opened; the harm is an unconfirmed edge-case auth failure, not a demonstrable defect. It dies under falsification. No new grounded findings surfaced: the read-only carve-out never emits an allow-without-deny, writes/hardlinks stay denied, the Linux copy and Seatbelt dedup paths are pinned by tests, and the static-key residual is the pre-existing cc-tier posture, not a new boundary the child can cross. No findings. [OPUS-REVIEWED] 53d93d8 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
c1c6974 to
7ae80b6
Compare
|
self-added: no
|
|
self-added: no
|
|
self-added: no
|
7ae80b6 to
b47ad4e
Compare
|
self-added: yes
|
|
self-added: no
|
|
self-added: no
Maintainer override line for the current head, if you agree with the ruling:
|
|
self-added: yes
|
|
self-added: no
|
|
self-added: yes
|
|
self-added: yes
|
c6867da to
f62de32
Compare
|
self-added: yes
Maintainer override line for the current head, if you agree with the ruling (also covers the unchanged static-keys residual, span e6da0e9c22e0, ×3):
|
|
self-added: yes
|
|
self-added: no
|
|
self-added: yes
|
|
self-added: yes
|
|
self-added: no
Maintainer override line (also covers the static-keys residual, span e6da0e9c22e0):
|
|
@iamwhatever thanks for this. Two notes from a cross-PR audit of the open pull requests. The audit read this branch at Scope. At the audited sha the change excluded the whole Conflict to expect. #9013 edits the same "Extra hidden paths" row in Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
…credential mask
The enforced-adapter credential mask derived from the read-gate floor hid the
whole ~/.aws directory from the codex-acp child. A codex whose model provider
is Bedrock authenticates through ~/.aws/config (credential_process), so every
session failed at start with 'failed to load AWS credentials', surfaced to the
operator as an opaque '-32000 Authentication required'. Running codex directly
in a terminal worked, because no sandbox.
Re-expose exactly that one file, read-only, on both platforms -- the cc tier's
own Linux posture (_CC_EXPOSE_FILES) -- and keep the rest of ~/.aws hidden:
~/.aws/credentials and ~/.aws/sso/cache stay masked from a child whose passive
reads never reach the gate.
Plumbing: a new extra_expose_files kwarg threads through wrap_argv_async ->
wrap_argv -> {namespace_argv -> _build_launcher_script (EXPOSE_FILES deduped by
source: cc mode and the adapter both name .aws/config, and a repeated entry
re-opened the 0444 copy for write and crashed the launcher),
sandbox_exec_argv -> _build_seatbelt_profile}. Linux appends to the same
EXPOSE_FILES list cc mode populates (a 0444 copy restored into the empty bind
mount). macOS emits `(deny file-read* (require-all (subpath <dir>)
(require-not (literal <file>))))` on the extra-hidden dir, the shape the strict
tier already uses for .ssh/known_hosts; write and hardlink denies stay blanket.
The carve-out is folded into the tier loop as well as the extra-hidden loop:
under strict the tier list already hides .aws, and Seatbelt cannot cancel an
earlier blanket deny with a later narrower one.
acp_tool_gate.adapter_expose_files() is the projection (empty for every
unenforced harness, so the kiro/claude spawn arguments stay byte-identical),
and ADAPTER_EXPOSED_CREDENTIAL_LEAVES is the table.
The launcher reads the exposed file with the same plain open() the cc tier
has always used: nothing about how ~/.aws/config is read changes for Claude
Code, and codex is held to the same bar rather than a higher one. A symlink,
hardlink or bind alias the OPERATOR plants at ~/.aws/config is followed as it is
today; the child cannot plant one (~/.aws is bind-masked in its session and the
agent's file tools are fenced from it), so that is operator self-exposure of
their own store, the same pre-existing residual. Accepted residual, now documented in the gate table and
the spec row: static aws_access_key_id/aws_secret_access_key lines an operator
writes INTO config are readable through the carve-out, as they already are for
cc's _CC_EXPOSE_FILES; the Linux copy is kept byte-identical rather than
redacted on one platform only.
The read gate still fences ~/.aws for the agent's own file tools; only the
child's OS-boundary mask changes.
Mutation-verified: dropping the expose leaf, excluding .aws from the mask,
disabling the Seatbelt carve-out, dropping the carve-out from the tier loop, and
dropping the EXPOSE_FILES dedupe each fail their own test.
f62de32 to
53d93d8
Compare
Problem / Motivation
Switching the provider to Codex fails every session at start. The dashboard shows
-32000 Authentication required, then-32602 Invalid paramson the already-broken session. Runningcodexdirectly in a terminal works fine.Root cause: the enforced-adapter credential mask (
adapter_hidden_credential_dirs) is derived from the whole read-gate floor, so it hides~/.awsfrom the codex-acp child. A codex whose model provider isamazon-bedrockauthenticates through~/.aws/config(credential_process). With the directory masked, codex reportsfailed to load AWS credentials, which codex-acp surfaces asAuthentication required. The terminal run has no sandbox, so it works.Why it matters
Every Bedrock-backed codex user is fully broken: not one session can start. There is no configuration workaround — setting the sandbox to
offis refused byenforce_sandbox_floorfor an enforced adapter.What changed (motivation → approach → change)
Reproduced both ways: codex-acp outside the mask answers
pong; with only~/.awshidden it reproduces the exact failure.The cc tier already solves this for Claude Code on Linux: the namespace launcher hides
.awsand restores only a read-only copy of.aws/config(_CC_EXPOSE_FILES), so~/.aws/credentialsand~/.aws/sso/cachestay hidden. This PR gives the enforced adapter that same posture on both platforms. It does not unmask~/.aws.Change:
acp_tool_gate.pygainsADAPTER_EXPOSED_CREDENTIAL_LEAVES(.aws/configfor codex) andadapter_expose_files(), which the spawn path hands to a newextra_expose_fileskwarg onwrap_argv_async→wrap_argv. On Linuxnamespace_argv→_build_launcher_scriptappends it to the sameEXPOSE_FILESlist cc mode uses (deduped by source path, since cc mode and the codex adapter both name~/.aws/configand a repeated entry would re-open the0444copy for write), so the launcher restores a0444copy ofconfiginside the now-empty.awsmount. On macOSsandbox_exec_argv→_build_seatbelt_profileturns the extra-hidden dir's read deny into(deny file-read* (require-all (subpath ~/.aws) (require-not (literal ~/.aws/config))))— the shape the strict tier already uses for.ssh/known_hosts— while the write and hardlink denies stay blanket. Empty for every unenforced harness, so the kiro and claude spawn arguments stay byte-identical. The launcher reads the exposed file with the same plainopen()the cc tier has always used; nothing about how~/.aws/configis read changes for Claude Code, and codex is held to the same bar rather than a higher one. The read gate (is_sensitive_path) still fences~/.awsfor the agent's own file tools.One residual is carried knowingly and is now recorded in the
ADAPTER_EXPOSED_CREDENTIAL_LEAVESdocstring and the spec row: the AWS CLI also acceptsaws_access_key_id/aws_secret_access_keywritten directly into~/.aws/config. An operator with that layout hands those keys to the child through the carve-out, exactly as cc's_CC_EXPOSE_FILESalready does. The Linux copy is deliberately not redacted per line, because the Seatbelt carve-out exposes the real inode and cannot match, and a one-platform redaction would be a silent asymmetry. Operators should keep static keys incredentials, which stays masked.flowchart LR subgraph Before A1[codex child]:::ctx --> B1[mask hides all of ~/.aws]:::ctx --> C1[credential_process unreachable]:::removed end subgraph After A2[codex child]:::ctx --> B2[mask hides ~/.aws]:::ctx --> B3[config re-exposed read-only]:::added --> C2[credential_process resolves]:::added B2 --> D2[credentials + sso/cache stay hidden]:::ctx end classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px classDef changed fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px classDef removed fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-dasharray:4 3 classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E linkStyle 0,1 stroke:#DC2626,stroke-dasharray:4 3 linkStyle 3,4 stroke:#16A34A,stroke-width:2px🟩 added · 🟨 changed · 🟥 removed · 🟦 unchanged
The codex child can now read
~/.aws/configand nothing else under~/.aws, on both platforms.Tests
test_aws_stays_masked_with_only_config_reexposed—~/.awsis in the codex mask,adapter_expose_filesreturns exactly~/.aws/config, and.awsstays on the read-gate floor.test_every_exposed_leaf_sits_under_a_masked_dir— a re-exposure outside the mask would be a grant, not a narrowing; pinned at the table.test_unenforced_harness_gets_no_expose_files— kiro and claude get an empty tuple.test_sandbox_cc_mode.py: two launcher tests pin thatextra_expose_fileslands inEXPOSE_FILESas(source, basename)pairs; three Seatbelt tests pin therequire-not (literal)carve-out on the read deny (write/link denies unchanged), that the carve-out also applies when the tier itself already hides the dir (strict +.aws— a later narrower deny cannot cancel an earlier blanket one), and that a file under no hidden dir emits no rule at all.test_sandbox_cc_mode.py:test_an_extra_expose_file_already_in_the_tier_list_appears_oncepins that a cc-mode codex spawn carries~/.aws/configexactly once inEXPOSE_FILES(a duplicate would crash the launcher on the restore)..awsfrom the mask, disabling the Seatbelt carve-out, dropping the carve-out from the tier loop, and dropping theEXPOSE_FILESdedupe each fail their own test.Manual verification
Reproduced the original failure by running codex-acp with only
~/.awshidden, and the success path outside the mask. The Linux restored-copy path is the mechanism cc mode already ships with for.aws/config. No test in this repo executessandbox-exec, so the macOS carve-out is verified at the profile-text level, same as every other Seatbelt rule here; post-merge, a gateway restart on macOS with the codex provider selected should start sessions normally.Related Issues
no linked issue: reported directly in chat with a dashboard screenshot; no tracked issue exists.
Pattern harvest
Rule candidate: review-prompt
Pattern: an OS credential mask derived from a deny floor must still let the sandboxed child reach the one file it AUTHENTICATES through — re-expose that file via the backend's existing per-file carve-out primitive, never by unmasking the whole directory.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)