Skip to content

feat(itmux): .env credential loading for itmux run (fixes the stale-token 401)#254

Open
NeuralEmpowerment wants to merge 9 commits into
feat/itmux-run-contractfrom
feat/itmux-env-credentials
Open

feat(itmux): .env credential loading for itmux run (fixes the stale-token 401)#254
NeuralEmpowerment wants to merge 9 commits into
feat/itmux-run-contractfrom
feat/itmux-env-credentials

Conversation

@NeuralEmpowerment

Copy link
Copy Markdown
Contributor

Populates AgentRunSpec.credentials from a .env/process env so itmux run injects subscription (OAuth / auth.json) or API-key credentials via the secure DooD path - instead of credentials: Default::default() silently falling back to the stale host $HOME/.claude file (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)

CLAUDE_CODE_OAUTH_TOKEN=...   # preferred (1yr from `claude setup-token`)
# ANTHROPIC_API_KEY=...       # fallback
CODEX_AUTH_FILE=~/.codex/auth.json   # preferred (contents materialized in-container)
# OPENAI_API_KEY=...          # fallback

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 OAuth injected as the container env var CLAUDE_CODE_OAUTH_TOKEN, NOT a synthesized .credentials.json.
  • Per-agent 0600 env-file staged over the existing base64-over-stdin docker exec transfer; pane launched via set -a; . '/home/agent/.itmux-secret-env-<agent>'; set +a; exec <harness> (only the file PATH + harness name on the command line).
  • No docker exec -e VAR=value, no tmux set-environment (both leak to argv).
  • claude_omit_credentials: in OAuth mode, stage ONLY the .claude.json trust marker, never a .credentials.json - so a stale host file can't re-cause the 401.

Hardening

  • Redaction test (tests/secret_redaction.rs, load-bearing): 3 sentinels asserted absent from every docker argv, the launch wrapper, all error strings, and serialized AgentRunEvents; positively confirms the two sanctioned carriers (rendered env-file body + base64 stdin, decoded in-test).
  • Fail-fast default with actionable error; --allow-host-auth-fallback re-enables the legacy path and warns with the source PATH only (never contents).
  • secret_env: BTreeMap (defaulted, serde-stable, allowlisted names); legacy claude.oauth_token/codex.auth_json kept (compat, R8); schema regenerated.
  • Thin Python client gains --env-file passthrough.

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.

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
@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Dual-review gate: PASS

  • Claude adversarial (security) review: APPROVE - traced every secret path; the value-never-leaks invariant (argv/logs/errors/events) holds; single_quote escaping verified safe (quote/backtick/$/newline); tests/secret_redaction.rs confirmed genuinely load-bearing.
  • codex cross-model review: CHANGES REQUESTED (2 create-with-default-umask windows: host secret env file briefly 0644, throwaway dir not 0700) -> fixed atomically (write_private_file O_CREAT+0600, create_private_dir mkdir+0700) at ALL 4 secret-bearing sites (+ codex auth.json, fresh_cred_dir), + a #[cfg(unix)] mode-assertion test + immediate host-file unlink post-transfer -> re-review APPROVE.

171 tests, clippy -D warnings clean, fmt clean. Permanently fixes the stale-token 401.

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.

1 participant