feat(itmux): .env credential loading for itmux run (fixes the stale-token 401)#254
Open
NeuralEmpowerment wants to merge 9 commits into
Open
feat(itmux): .env credential loading for itmux run (fixes the stale-token 401)#254NeuralEmpowerment wants to merge 9 commits into
NeuralEmpowerment wants to merge 9 commits into
Conversation
R2/R7/R8: add a defaulted, serde-stable generic secret_env map to AgentRunCredentials for harness-neutral env-var secrets, keeping the existing claude/codex fields for compat. Regenerate the schemars schema and add round-trip + default-empty tests. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
R4 narrow .env parser (KEY=VALUE, # comments, quotes; no expansion, multiline, or export; file:line errors) + load_credentials with precedence (--env-file > process env). R2 per-harness routing (resolve_agent_secrets) with confirmed preferred/fallback + R8 compat precedence. R3 fail-fast message. Renders the 0600 env-file body. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
R1/R6: stage a per-agent 0600 env file over the existing base64-over-stdin transfer, then launch the harness pane with 'set -a; . <file>; set +a; exec <cmd>' so the CLI inherits CLAUDE_CODE_OAUTH_TOKEN / API keys from the child env. No secret ever reaches argv (no docker exec -e, no tmux set-environment). StartOptions/Workspace carry per-agent secret_env and the staged file path; an agent is enabled by secret_env alone. R1/R8: claude OAuth mode stages .claude.json only (AuthContext.claude_omit_ credentials), never a synthesized .credentials.json. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
R3: replace the empty 'credentials: Default::default()' with loader output and route the recipe agent's secrets in the executor. Default is fail-fast (actionable error naming the missing var); --allow-host-auth- fallback re-enables the legacy $HOME/.<agent> path with a PATH-only warning. Kills the silent stale-file 401 fallback. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
Seed sentinel secrets and assert they never appear in any docker exec argv, tmux label, fail-fast/error string, or serialized AgentRunEvent - only in the in-memory spec and the base64 stdin payload. Includes a base64 decode to prove the stdin payload is the sole argv-free carrier. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
…rough Typed run_client forwarding --env-file to itmux run (or materializing an in-memory credential mapping to a private 0600 temp file whose PATH - never contents - is forwarded then removed). Tests assert forwarding and that a secret value never reaches argv or logs. mypy --strict clean. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
Document the recognized keys, precedence, CLAUDE_CODE_OAUTH_TOKEN preference, fail-fast + opt-in host fallback, the sourced-0600-env-file no-argv-leak mechanism, the stale-file 401 fix (Keychain-scrape workaround retired), and 'credentials live in the run/workspace layer, never in the recipe'. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
…view) Close the brief-world-readable window on host-side secret material: every file/dir holding secrets is now created with its restrictive mode at creation, not chmod'd afterward. Adds create_private_dir (mkdir 0700) and write_private_file (open O_CREAT|O_EXCL 0600). - Fix 1: secret env file - create_new 0600 instead of fs::write + chmod. - Fix 2: workspace throwaway dir - 0700 at creation. - Fix 3: codex auth.json temp file - create_new 0600 (same class, no carve-out); fresh_cred_dir now mkdir 0700 atomically too. - Fix 4: cfg(unix) mode-assertion tests driving the real creation helpers (dir 0700, file 0600, refuses to clobber). - Hygiene: unlink the host secret env file immediately after transfer; teardown remove_dir_all remains the fallback. Claude-Session: https://claude.ai/code/session_019TtekhJft4s2qBGRGgxToG
Contributor
Author
Dual-review gate: PASS
171 tests, clippy -D warnings clean, fmt clean. Permanently fixes the stale-token 401. |
…at/itmux-env-credentials
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Populates
AgentRunSpec.credentialsfrom a.env/process env soitmux runinjects subscription (OAuth / auth.json) or API-key credentials via the secure DooD path - instead ofcredentials: Default::default()silently falling back to the stale host$HOME/.claudefile (the recurring 401 root cause, confirmed live in EXP-08: the on-disk creds expire in hours; the live token is in the macOS Keychain). Codex plan-reviewed (8 revisions R1-R8 folded in). Stacked on #247.Secrets contract (consumer-supplied; NEVER in the recipe)
itmux run --env-file <path>(else process env). Precedence: env-file > process env > (opt-in)--allow-host-auth-fallback.Injection - no secret ever touches argv (R1/R5)
CLAUDE_CODE_OAUTH_TOKEN, NOT a synthesized.credentials.json.0600env-file staged over the existing base64-over-stdindocker exectransfer; pane launched viaset -a; . '/home/agent/.itmux-secret-env-<agent>'; set +a; exec <harness>(only the file PATH + harness name on the command line).docker exec -e VAR=value, notmux set-environment(both leak to argv).claude_omit_credentials: in OAuth mode, stage ONLY the.claude.jsontrust marker, never a.credentials.json- so a stale host file can't re-cause the 401.Hardening
tests/secret_redaction.rs, load-bearing): 3 sentinels asserted absent from every docker argv, the launch wrapper, all error strings, and serializedAgentRunEvents; positively confirms the two sanctioned carriers (rendered env-file body + base64 stdin, decoded in-test).--allow-host-auth-fallbackre-enables the legacy path and warns with the source PATH only (never contents).secret_env: BTreeMap(defaulted, serde-stable, allowlisted names); legacyclaude.oauth_token/codex.auth_jsonkept (compat, R8); schema regenerated.--env-filepassthrough.Gates: cargo test 169 pass, clippy -D warnings clean, fmt clean, R8 neutrality guard holds; python pytest 6 / ruff / mypy --strict clean. Fixes the stale-token 401 permanently; retires the Keychain-scrape workaround. Tracks okrs-51p.7.