Skip to content

fix(codex): let a Bedrock-backed codex read ~/.aws/config inside its credential mask - #9329

Merged
iamwhatever merged 1 commit into
mainfrom
fix/codex-mask-keep-aws-readable
Sep 8, 2026
Merged

fix(codex): let a Bedrock-backed codex read ~/.aws/config inside its credential mask#9329
iamwhatever merged 1 commit into
mainfrom
fix/codex-mask-keep-aws-readable

Conversation

@iamwhatever

@iamwhatever iamwhatever commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

Switching the provider to Codex fails every session at start. The dashboard shows -32000 Authentication required, then -32602 Invalid params on the already-broken session. Running codex directly 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 ~/.aws from the codex-acp child. A codex whose model provider is amazon-bedrock authenticates through ~/.aws/config (credential_process). With the directory masked, codex reports failed to load AWS credentials, which codex-acp surfaces as Authentication 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 off is refused by enforce_sandbox_floor for an enforced adapter.

What changed (motivation → approach → change)

Reproduced both ways: codex-acp outside the mask answers pong; with only ~/.aws hidden it reproduces the exact failure.

The cc tier already solves this for Claude Code on Linux: the namespace launcher hides .aws and restores only a read-only copy of .aws/config (_CC_EXPOSE_FILES), so ~/.aws/credentials and ~/.aws/sso/cache stay hidden. This PR gives the enforced adapter that same posture on both platforms. It does not unmask ~/.aws.

Change: acp_tool_gate.py gains ADAPTER_EXPOSED_CREDENTIAL_LEAVES (.aws/config for codex) and adapter_expose_files(), which the spawn path hands to a new extra_expose_files kwarg on wrap_argv_asyncwrap_argv. On Linux namespace_argv_build_launcher_script appends it to the same EXPOSE_FILES list cc mode uses (deduped by source path, since cc mode and the codex adapter both name ~/.aws/config and a repeated entry would re-open the 0444 copy for write), so the launcher restores a 0444 copy of config inside the now-empty .aws mount. On macOS sandbox_exec_argv_build_seatbelt_profile turns 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 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. The read gate (is_sensitive_path) still fences ~/.aws for the agent's own file tools.

One residual is carried knowingly and is now recorded in the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring and the spec row: the AWS CLI also accepts aws_access_key_id/aws_secret_access_key written 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_FILES already 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 in credentials, 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
Loading

🟩 added · 🟨 changed · 🟥 removed · 🟦 unchanged

The codex child can now read ~/.aws/config and nothing else under ~/.aws, on both platforms.

Tests

  • test_aws_stays_masked_with_only_config_reexposed~/.aws is in the codex mask, adapter_expose_files returns exactly ~/.aws/config, and .aws stays 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 that extra_expose_files lands in EXPOSE_FILES as (source, basename) pairs; three Seatbelt tests pin the require-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_once pins that a cc-mode codex spawn carries ~/.aws/config exactly once in EXPOSE_FILES (a duplicate would crash the launcher on the restore).
  • 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.
  • Locally: 927 passed across gate / parity / sandbox / spawn-audit / governance / docs-lint suites; black, isort, flake8, mypy, docs-lint and brand-name gates clean.

Manual verification

Reproduced the original failure by running codex-acp with only ~/.aws hidden, 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 executes sandbox-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

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@iamwhatever
iamwhatever requested a review from a team as a code owner September 8, 2026 00:16
@iamwhatever
iamwhatever requested a review from cixuuz September 8, 2026 00:16
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Intent: Let a Bedrock-backed codex authenticate inside the enforced-adapter sandbox by excluding ~/.aws from the child's OS credential mask, matching the trade the cc tier already makes for Claude Code.
Not a goal: Changing the read-gate floor, the agent's own file-tool fencing of ~/.aws, or the mask for any other harness.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 53d93d8e2042a4a2a50522e8c63cff22e59e3c00 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified against the repository. Every mechanism the PR cites pre-exists (_CC_EXPOSE_FILES at sandbox.py:1935 with exactly .aws/config; the require-not (literal) carve-out shape already shipped for .ssh/known_hosts and the cc expose loop; the restore loop's write-then-chmod 0444 that makes the dedupe load-bearing), .aws is on the sensitive floor (paths.py:87) so the defect is mechanically derivable from base, and enforce_sandbox_floor confirms the "no configuration workaround" claim. adapter_expose_files has exactly 1 consumer (acp/client.py:4917), consistent with the module's established table-driven pattern.

First-Principles-Verdict: PASS

Verify on a real macOS host (codex + Bedrock, including strict tier): the Seatbelt carve-out is proven only at profile-text level, as the author states.

What this change ships

Intent: 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)
  1. A Bedrock-backed codex session starts again: the child can read ~/.aws/config — justified
  2. ~/.aws/credentials and the SSO cache stay hidden from that child on both platforms — justified
  3. New spawn plumbing extra_expose_files threaded through wrap_argv into both sandbox backends — justified
  4. New gate table ADAPTER_EXPOSED_CREDENTIAL_LEAVES + exported adapter_expose_files() — justified
  5. Seatbelt hidden-dir read denies can now carry per-file carve-outs, in both the tier and extra-hidden loops — justified
  6. Linux launcher dedupes EXPOSE_FILES by source, so a cc-tier codex spawn no longer double-restores .aws/config — justified
  7. Kiro, claude and every unenforced harness keep byte-identical spawn arguments — justified
  8. Static keys an operator writes into ~/.aws/config are readable through the carve-out — declared residual, same one cc mode already ships — justified

[FIRST-PRINCIPLES-REVIEWED] 53d93d8

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 53d93d8e2042a4a2a50522e8c63cff22e59e3c00 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 53d93d8

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 53d93d8e2042a4a2a50522e8c63cff22e59e3c00: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 53d93d8e2042a4a2a50522e8c63cff22e59e3c00 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Both facts I needed are confirmed: the mask (sandbox_credential_targets) deliberately emits both home spellings (logical + resolved) plus override re-anchors, precisely because "denying only the resolved form would leave every credential reachable through the symlinked one" — while the new adapter_expose_files projects its leaf under a single os.path.expanduser("~") anchor. That asymmetry is my one design finding.

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

  • Anchoring asymmetry between mask and carve-out. adapter_hidden_credential_dirs rides sandbox_credential_targets, which emits both home spellings on a symlinked home ("denying only the resolved form would leave every credential reachable through the symlinked one") — that function's own docstring calls single-anchor projection "exactly the drift a hand-maintained list already produced once." adapter_expose_files projects .aws/config under one expanduser("~") anchor, so on a symlinked-home macOS host the resolved-spelling .aws deny stays blanket (the Seatbelt loop carves only the matching prefix), and a Bedrock codex there keeps failing with the exact opaque error this PR fixes. test_every_exposed_leaf_sits_under_a_masked_dir passes on CI hosts only because their homes aren't symlinks. Consequence is a partial fix, not a regression — hence CONCERNS.
    Clears when: adapter_expose_files projects the leaf under the same anchors sandbox_credential_targets uses (or delegates to a companion in security/paths.py), with a test pinning that every mask spelling of .aws carries a matching carve-out.

Suggestions

  • Move the leaf→absolute projection next to sandbox_credential_targets in security/paths.py so mask and carve-out can never anchor apart; acp_tool_gate keeps only the leaf table.

[DESIGN-REVIEWED] 53d93d8

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 53d93d8e2042a4a2a50522e8c63cff22e59e3c00 — this comment is updated in place on each push.

Review details

The only candidate hinges on macOS Seatbelt resolving symlinks before matching file-read* subpath rules. Let me verify the codebase's stated model on that, since it's the load-bearing premise.

The relevant comment (security/paths.py:2039-2044) explicitly states the design assumption: a sandbox deny list "gets no such normalisation -- it denies the paths it is handed." The dual-spelling emission exists precisely because Seatbelt matches the as-requested literal path, not a resolved one — otherwise emitting both spellings would be pointless.

Under that model, a codex child opens ~/.aws/config, which expands through its own $HOME (the logical spelling), matches the logical-anchored carve-out, and reads fine. The resolved-spelling blanket deny never matches a path the child actually requests. The candidate's failure only materializes under the opposite premise ("If Seatbelt evaluates the request against the RESOLVED path") — which it admits it could not confirm and which contradicts the codebase's own load-bearing assumption. It requires an exotic symlinked $HOME on macOS, and its own confidence is "low," framed as an "if."

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 53d93d8e2042a4a2a50522e8c63cff22e59e3c00.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 53d93d8e2042a4a2a50522e8c63cff22e59e3c00: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever
iamwhatever force-pushed the fix/codex-mask-keep-aws-readable branch from c1c6974 to 7ae80b6 Compare September 8, 2026 03:05
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no
mechanism: extra_expose_files kwarg through wrap_argv -> launcher EXPOSE_FILES (Linux) / Seatbelt require-not (literal) carve-out (macOS); adapter_expose_files() + ADAPTER_EXPOSED_CREDENTIAL_LEAVES in acp_tool_gate.py

  • Codex can passively read the operator's AWS credentials — span=e6da0e9c22e0 — fixed in 7ae80b6.

.aws is no longer excluded from the codex mask on any platform; ADAPTER_OWN_CREDENTIAL_LEAVES is back to (".codex/auth.json",).
Only ~/.aws/config is re-exposed READ-ONLY, through each backend's existing per-file primitive: Linux restores a 0444 copy into the empty bind mount (the cc tier's _CC_EXPOSE_FILES mechanism); macOS emits (deny file-read* (require-all (subpath ~/.aws) (require-not (literal ~/.aws/config)))) with write and hardlink denies left blanket.
~/.aws/credentials and ~/.aws/sso/cache stay hidden from the child on standard, cc and strict; the carve-out is folded into the tier loop too, so a strict-tier blanket deny cannot pre-empt it.
Findings of the class "the adapter mask exposes a sibling credential file under ~/.aws" are covered by this ruling: the mask hides the directory and names exactly one file to re-expose, pinned by test_aws_stays_masked_with_only_config_reexposed and test_every_exposed_leaf_sits_under_a_masked_dir.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no
mechanism: extra_expose_files kwarg through wrap_argv -> launcher EXPOSE_FILES (Linux) / Seatbelt require-not (literal) carve-out (macOS); adapter_expose_files() + ADAPTER_EXPOSED_CREDENTIAL_LEAVES in acp_tool_gate.py

  • Codex credential mask now exposes ~/.aws/credentials to the self-approving Codex child — span=cb3e020ed5e0 — fixed in 7ae80b6.

Taken as suggested: .aws stays masked, and only .aws/config is re-exposed via a sub-leaf mechanism modelled on _CC_EXPOSE_FILES.
Linux reuses the launcher's EXPOSE_FILES list (0444 copy restored into the empty mount). macOS gets the equivalent carve-out in _build_seatbelt_profile: require-not (literal ~/.aws/config) on the read deny, write/link denies blanket — the same shape the strict tier uses for .ssh/known_hosts.
~/.aws/credentials and ~/.aws/sso/cache remain fenced from the child under standard, cc and strict. The SSO cache is not re-exposed: the reproduced failure was the credential_process lookup in config, and nothing in the causal chain needed the cache.
The cc-tier parity claim is corrected in the comment, the PR body and the spec row: the adapter now matches cc's LINUX posture (hide the dir, re-expose config read-only) on every platform.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Whole-tree, read-write, unconditional unmask of .aws (Watch 1 + Watch 2 + Suggestion) — fixed in 7ae80b6, by a narrower route than the suggestion.

Watch 1 (whole-tree / read-write / cc parity only holds on macOS): the exclusion is gone. .aws stays hidden on every platform and only ~/.aws/config is re-exposed read-only, through the cc tier's own Linux primitive (_CC_EXPOSE_FILES / launcher EXPOSE_FILES) and its Seatbelt equivalent (require-not (literal) on the read deny; write and hardlink denies stay blanket). ~/.aws/credentials and the SSO cache are never readable or writable by the child. The spec row in agent-host-contract.md now states this posture instead of the macOS gloss.
Watch 2 (every codex session pays, Bedrock or not): what a non-Bedrock codex session now gains is read access to one non-secret profile file, ~/.aws/config, and nothing else under ~/.aws. Scoping the re-exposure to Bedrock-configured sessions would need Crew to read the operator's ~/.codex/config.toml at spawn time; the residual exposure is a config file the read gate still fences for the agent's own tools, so that gate is not added here.
Suggestion (keep .aws excluded, re-mask credentials as a file leaf): not taken — it would still leave the SSO cache and every other file under ~/.aws readable and the tree writable. Hiding the directory and re-exposing exactly one file is strictly narrower and reuses a primitive both backends already carry.

@iamwhatever iamwhatever changed the title fix(codex): keep ~/.aws readable by the codex child so Bedrock auth works fix(codex): let a Bedrock-backed codex read ~/.aws/config inside its credential mask Sep 8, 2026
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever
iamwhatever force-pushed the fix/codex-mask-keep-aws-readable branch from 7ae80b6 to b47ad4e Compare September 8, 2026 03:35
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes
mechanism: link-proof expose pre-read in the Linux launcher (parent O_DIRECTORY|O_NOFOLLOW, leaf via dir_fd + O_NOFOLLOW, S_ISREG on the fd, both fds closed in finally)

  • Symlinked AWS config exposes another credential file — span=fa7ab943e399 — fixed in b47ad4e.

Taken as suggested, and one step further: the expose pre-read now opens the PARENT with O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY, opens the leaf by basename relative to that descriptor with O_NOFOLLOW, requires stat.S_ISREG on fstat(fd), and closes both descriptors on every path.
A symlink at the leaf (~/.aws/config -> ~/.aws/credentials) or at the parent (~/.aws -> ~/.docker) fails the open with ELOOP and degrades to "not exposed" with a stderr warning; the restore loop only writes entries that were read, so no link target's bytes ever reach the masked mount.
The same block serves cc mode's _CC_EXPOSE_FILES, so this also hardens the pre-existing .aws/config exposure for Claude Code.
Findings of the class "the expose pre-read follows a symlink at any path component" are covered by this ruling; pinned by four tests that execute the extracted launcher block against real leaf and parent symlinks, two with islink lying to prove the descriptor opens are the guard.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Static keys written into ~/.aws/config flow through the carve-out, unnamed (Watch + Suggestion) — Watch fixed in b47ad4e; Suggestion rebutted.

Watch: the residual is now recorded where the "clears when" asked — the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring and the agent-host-contract.md spec row both state that aws_access_key_id/aws_secret_access_key lines an operator writes INTO config are readable through the carve-out on both platforms, that this is the same residual cc's _CC_EXPOSE_FILES already carries, and that operators should keep static keys in credentials, which stays masked. The PR body says the same.
Suggestion (strip secret-bearing lines from the Linux copy): not taken. The Seatbelt carve-out exposes the real inode and cannot redact, so a Linux-only redaction would make the two platforms carry DIFFERENT residuals and leave the macOS one implicit — the opposite of what the Watch asks for. Keeping the copy byte-identical keeps the residual identical and documented once. The asymmetry and its reason are written into the docstring and the spec row.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • AWS config exposure leaks credentials — span=e6da0e9c22e0 — rebutted as not a defect (same ruling as on b47ad4e; the code this finding names is unchanged).

Not a defect. ~/.aws/config is, by the AWS CLI/SDK contract, the non-secret profile file; secrets belong in ~/.aws/credentials, which stays masked on both platforms along with the SSO cache. The child is handed the one file whose documented purpose is to describe HOW to obtain credentials — the same bytes the operator's own terminal codex reads with no Crew in the way.
Secret material reaches the child only when the operator has written aws_access_key_id / aws_secret_access_key INTO config: operator self-exposure, identical to the pre-existing residual cc mode's _CC_EXPOSE_FILES (sandbox.py) has carried for Claude Code since it shipped. This PR reuses that exact primitive and records the residual in the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring, the agent-host-contract.md spec row and the PR body.
The proposed sanitized copy is unavailable on both platforms — the Seatbelt carve-out exposes the real inode and cannot redact — so a Linux-only redaction would leave the platforms with different residuals and the macOS one implicit. A byte-identical exposure, documented once, is the honest shape.
Findings of the class "static keys an operator writes into ~/.aws/config are readable through the .aws/config carve-out" are covered by this ruling.

Maintainer override line for the current head, if you agree with the ruling:

/ai-review override gpt c6867daa34800f5ac4a97142f51dc17f455a2e33: ~/.aws/config is the AWS non-secret profile file; static keys written into it are operator self-exposure, identical to the pre-existing cc-mode .aws/config exposure, and are documented as an accepted residual while credentials and the SSO cache stay masked.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes
mechanism: expose pre-read resolves a symlinked source with realpath and admits it only when the opened file and every ancestor pass an inode-identity check against the mask (_masked_owners, _refuse_if_masked); the source's own named parent is exempt only while no second mask entry shares its inode

  • Watch: the symlink refusal silently regresses dotfile-managed ~/.aws/config setups, including existing cc-mode usersfixed in c6867da, by the second route the "clears when" named.

Taken: the guard now permits a symlink whose fd-resolved target lies outside every masked path. The launcher opens the named path link-proof; on ELOOP/ENOTDIR it resolves with realpath, opens the RESOLVED path with the same link-proof opens, and refuses only if the opened file or any directory above it IS a mask entry — by (st_dev, st_ino) identity, never a path string, so a case-insensitive home or a bind-mount alias cannot re-spell a masked path. A stow/chezmoi/home-manager ~/.aws/config -> ~/dotfiles/aws/config is exposed with its target's bytes; config -> credentials, ~/.aws -> ~/.docker, and a whole-directory ~/.aws link (whose target the bind-mount itself hides) are refused with a sandbox: WARNING, which the gateway already mirrors into its log as codex-acp stderr:.
cc mode's pre-existing exposure gets the same treatment, so Claude Code operators with a linked config keep working — the collateral the Watch named is gone rather than documented.
Pinned by ten tests executing the extracted launcher block against real files and links, two of them revert-verified against the string comparison and the direct-open bypass respectively. Findings of the class "a legitimate symlinked expose source is refused" are covered by this ruling.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

There is no user-facing codex/Bedrock setup page in docs/ to carry the line today; the posture is recorded at the two places that exist — the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring and the agent-host-contract.md spec row. The operator-facing sentence belongs with the failure-message work so the message and the doc say the same thing.
#9377 (label deferred-finding, assignee iamwhatever, Due: 2026-10-06) carries it as step 2 and now also names the credential_process-not-SSO wording from this round.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Watch: "hardens cc mode's existing exposure" undersells a behavior change — symlinked ~/.aws/config users previously authenticated, now fail with a stderr-only warningfixed in c6867da rather than confirmed as intended.

The premise the Watch questioned — that a symlink there is only ever hostile — was wrong, and the collateral is not accepted: a symlinked ~/.aws/config is now resolved and exposed when its target lies outside every masked path (by inode identity), so the cc-tier dotfiles layout authenticates exactly as it did before this PR. Only a link INTO masked material (config -> credentials, ~/.aws -> ~/.docker, a whole-directory ~/.aws link the bind-mount itself hides) is refused, and that class never worked as a safe exposure.
The spec row and the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring now state the admit-unless-masked rule instead of a blanket refusal; the PR body says the same in user words.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Subtraction: drop the os.path.islink(parent) or os.path.islink(src_path) pre-check branchfixed (removed) in c6867da.

Taken. The islink pre-check is gone; the descriptor opens (O_DIRECTORY|O_NOFOLLOW on the parent, O_NOFOLLOW via dir_fd on the leaf, S_ISREG on the fd) and the inode-identity comparison are the only guards, so the TOCTOU tests no longer have a second code path to prove irrelevant. The two "islink lies" tests were retargeted to lie about realpath instead, proving the resolved open's O_NOFOLLOW is what holds.
The _EXPOSE_SLICE_LANDMARKS list no longer names the pre-check, so the extracted-block tests fail loudly if it is ever reintroduced.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever
iamwhatever force-pushed the fix/codex-mask-keep-aws-readable branch from c6867da to f62de32 Compare September 8, 2026 06:49
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes
mechanism: none added — a retrospective ran this round and REMOVED the hardened expose pre-read (link-proof opens, realpath fallback, inode-identity mask check); the launcher's EXPOSE_FILES pre-read is again base's plain open(src_path, "rb")

  • Hard-linked config bypasses the credential mask — span=fa7ab943e399 — fixed by subtraction in f62de32; the residual it names is ruled not a defect of this PR.

A retrospective ran on this round (3rd judged head; span fa7ab943e399 had drawn a finding in every round it existed). Its verdict: the entire hardened pre-read was loop growth the round-0 intent never required. The intent is to give codex the same ~/.aws/config exposure the cc tier already gives Claude Code, and that tier has always read the file with a plain link-following open(). This PR does not introduce that exposure; it extends the same one file to a second adapter through the same primitive.
The pre-read is now byte-identical to origin/main. The symlink / hardlink / bind-alias class — a link the OPERATOR plants at ~/.aws/config pointing into their own masked store — is therefore the pre-existing residual cc mode has carried since _CC_EXPOSE_FILES shipped, not a boundary this PR opens: the child cannot plant such a link (~/.aws is bind-masked in its session and the agent's file tools are fenced from it), so only the trusted operator can expose the operator's own credentials to the operator's own agent. Recorded as such in the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring, the agent-host-contract.md spec row and the PR body.
This ruling retires the whole class on this PR: the round-1 symlink finding, the round-3 stow regression, the case-alias and bind-alias findings, and this hardlink finding all lived inside the removed mechanism. Findings that a link at the expose source carries another file's bytes past the mask are covered by this ruling as pre-existing cc-mode behaviour; hardening that shared primitive for both adapters is a separate change with its own threat model, not this fix.

Maintainer override line for the current head, if you agree with the ruling (also covers the unchanged static-keys residual, span e6da0e9c22e0, ×3):

/ai-review override gpt f62de325f95294b22b823c7bb714bc203eca93d1: ~/.aws/config is exposed to codex through the identical plain-open() primitive cc mode already uses for Claude Code; static keys written into config and operator-planted links at that path are pre-existing cc-mode residuals of the operator's own store, not boundaries the child can cross, and are documented as accepted while credentials and the SSO cache stay masked.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Watch: whole-directory ~/.aws symlinks that worked before are now refused, breaking existing dotfile-managed cc-mode setupsfixed in f62de32 by removing the refusal entirely.

A retrospective ran this round and removed the hardened pre-read the refusal lived in. The launcher reads ~/.aws/config with the same plain open() base uses, so a whole-directory ~/.aws -> ~/dotfiles/aws link, a leaf link, and every other layout that authenticated on base authenticates identically now — for cc mode and for codex. There is no longer a "regression to describe": the behaviour is base's behaviour.
The first "clears when" branch (show the ENOTDIR exemption unsafe) is moot with the mechanism gone; the class it guarded against is the pre-existing cc residual the spec row now names plainly. Findings of the class "the expose pre-read refuses a layout that worked on base" are covered by this ruling.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

No user-facing codex/Bedrock setup page exists in docs/ today; the posture lives in the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring and the agent-host-contract.md spec row. #9377 (label deferred-finding, assignee iamwhatever, Due: 2026-10-06) carries the operator-facing sentence together with the mask-naming auth-failure message, so message and doc say the same thing once.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Watch item 5: a whole-directory ~/.aws symlink layout that authenticated on base is now refused, framed as "pre-existing behaviour"fixed in f62de32; the mis-framing you caught was correct and is gone with the mechanism.

You were right that the deleted open(src_path, "rb") followed that link on base, so the test docstring's "pre-existing behaviour" claim was false. Rather than confirm the tightening or exempt the resolved parent, a retrospective this round removed the hardened pre-read altogether: the launcher is back to base's plain open(), the whole-dir link is followed exactly as before, and the test that made the claim no longer exists.
The class the hardening guarded against (an operator-planted link at ~/.aws/config into the operator's own masked store) is now stated in the docstring and spec row as the pre-existing cc-mode residual it always was, plantable only by the trusted operator, not by the fenced child.

@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Watch item 4: the hardening gates every cc-mode spawn's config exposure through ~130 new launcher lines; a false refusal is an auth outage for Claude Codefixed in f62de32 by removing those lines.

The retrospective reached the same conclusion from the intent side: the ~130 lines were not required to give codex the cc tier's .aws/config exposure, and every cc-mode spawn was paying for hardening the intent never asked for. They are gone; the launcher pre-read is byte-identical to origin/main, so cc mode's config exposure runs through exactly the code it ran through before this PR and carries no new refusal path. The "clears when" (ten identity tests green on Linux CI) is moot — those tests were removed with the mechanism, and the remaining tests pin only the plumbing, the dedupe and the Seatbelt carve-out.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Symlinked AWS config exposes masked credentials — span=fa7ab943e399 — rebutted as not a defect of this PR (same ruling recorded on c6867da after the retrospective; the code this finding names is base's, unchanged).

Not a defect introduced here. The line named is base's own plain open() pre-read for cc mode's _CC_EXPOSE_FILES — this PR does not add, move or alter it; it hands the SAME primitive one more adapter's entry for the SAME file. A finding against it is a finding against origin/main's cc-mode exposure, which is the tier this PR exists to match.
Your own adjudication reached the same conclusion and FLAGged it: the confined agent cannot plant the link — ~/.aws is bind-masked with an ephemeral tmpfs in its session and the pre-read runs against the host inode before that mask, so only a host-write actor who already holds the credentials can point ~/.aws/config at them. That is the operator exposing the operator's own store to the operator's own agent: self-exposure, not a boundary the untrusted child crosses.
A retrospective on this PR removed a hardened pre-read (link-proof opens, realpath, inode identity) that earlier rounds had grown to answer exactly this class; it was intent-excess that broke dotfile-managed layouts and drew a new finding every round. Hardening the shared cc/codex primitive is a separate change with its own threat model. Recorded in the ADAPTER_EXPOSED_CREDENTIAL_LEAVES docstring, the agent-host-contract.md spec row and the PR body.
Findings of the class "a symlink, hardlink or alias the operator plants at ~/.aws/config carries another file's bytes through the expose pre-read" are covered by this ruling on every head of this PR.

Maintainer override line (also covers the static-keys residual, span e6da0e9c22e0):

/ai-review override gpt f62de325f95294b22b823c7bb714bc203eca93d1: ~/.aws/config is exposed to codex through the identical plain-open() primitive cc mode already uses for Claude Code; static keys written into config and operator-planted links at that path are pre-existing cc-mode residuals of the operator's own store, not boundaries the child can cross, and are documented as accepted while credentials and the SSO cache stay masked.

@bolichen97

Copy link
Copy Markdown
Collaborator

@iamwhatever thanks for this. Two notes from a cross-PR audit of the open pull requests. The audit read this branch at c1c6974; the head has since moved to f62de32 and now also touches src/kiro_crew/sandbox.py, src/kiro_crew/acp/client.py and test/test_sandbox_cc_mode.py, so please say if the first point is already handled there.

Scope. At the audited sha the change excluded the whole .aws leaf from the codex credential mask (ADAPTER_OWN_CREDENTIAL_LEAVES in src/kiro_crew/acp_tool_gate.py), which also hands the ungated codex child ~/.aws/credentials and the SSO cache, not just the config file a Bedrock backed harness needs. main already owns a narrower per-file mechanism: _CC_EXPOSE_FILES in src/kiro_crew/sandbox.py re-exposes .aws/config read-only while the rest of the leaf stays hidden, currently wired to the cc tier only. Reusing that file-leaf path instead of a directory-wide exclusion is what the two blocking reviews are asking for, and it also means the body's claim that a config-only carve-out is not expressible does not hold.

Conflict to expect. #9013 edits the same "Extra hidden paths" row in docs/system-specs/modules/agent-host-contract.md, appending an OpenCode column to it. Whichever of the two lands second has to re-apply its own cell by hand, so plan on a rebase there.

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.
@iamwhatever
iamwhatever force-pushed the fix/codex-mask-keep-aws-readable branch from f62de32 to 53d93d8 Compare September 8, 2026 16:03
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
@iamwhatever
iamwhatever merged commit 54b3607 into main Sep 8, 2026
65 checks passed
@iamwhatever
iamwhatever deleted the fix/codex-mask-keep-aws-readable branch September 8, 2026 18:35
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 8, 2026
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.

3 participants