Skip to content

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

Open
NeuralEmpowerment wants to merge 10 commits into
mainfrom
feat/itmux-env-credentials
Open

feat(itmux): .env credential loading for itmux run (fixes the stale-token 401)#254
NeuralEmpowerment wants to merge 10 commits into
mainfrom
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.

NeuralEmpowerment added a commit that referenced this pull request Jul 7, 2026
…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.

@NeuralEmpowerment
NeuralEmpowerment force-pushed the feat/itmux-run-contract branch from 7fde8f0 to a060abb Compare July 15, 2026 19:45
@NeuralEmpowerment
NeuralEmpowerment changed the base branch from feat/itmux-run-contract to feat/itmux-python-client July 15, 2026 20:01
@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Retargeted to #250 and rebased. Reconciled the earlier env-file client with #250’s reviewed streaming/process-group client; credential values remain out of argv and are materialized only to a 0600 temporary env file. Fixed one post-rebase AuthContext fixture. Local Rust fmt/test/clippy plus Python Ruff and 22 focused tests pass. Fresh CI pending.

NeuralEmpowerment added a commit that referenced this pull request Jul 15, 2026
…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
NeuralEmpowerment force-pushed the feat/itmux-env-credentials branch from 4c0211f to bd7cde2 Compare July 15, 2026 20:04
@NeuralEmpowerment
NeuralEmpowerment force-pushed the feat/itmux-python-client branch from d6b048f to e790602 Compare July 15, 2026 22:17
NeuralEmpowerment added a commit that referenced this pull request Jul 15, 2026
…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

Rebased onto refreshed #250 head (current dependency base) and force-with-lease pushed. Validation: Rust format + compile; (27 passed); F401 [*] os imported but unused
--> lib/python/agentic_memory/tests/test_doctor.py:10:8
|
9 | import json
10 | import os
| ^^
11 | import stat
12 | from pathlib import Path
|
help: Remove unused import: os

F401 [*] pathlib.Path imported but unused
--> lib/python/agentic_memory/tests/test_doctor.py:12:21
|
10 | import os
11 | import stat
12 | from pathlib import Path
| ^^^^
13 |
14 | import pytest
|
help: Remove unused import: pathlib.Path

F401 [*] pytest imported but unused
--> lib/python/agentic_memory/tests/test_doctor.py:14:8
|
12 | from pathlib import Path
13 |
14 | import pytest
| ^^^^^^
15 |
16 | from agentic_memory.contract import MemoryContract
|
help: Remove unused import: pytest

F401 [*] pytest imported but unused
--> scripts/tests/test_capture_recording.py:8:8
|
6 | from pathlib import Path
7 |
8 | import pytest
| ^^^^^^
9 |
10 | # Add scripts to path
|
help: Remove unused import: pytest

Found 4 errors.
[*] 4 fixable with the --fix option. (passed). This remains stacked behind #250 until #250 merges, then it will be retargeted to main.

@NeuralEmpowerment
NeuralEmpowerment force-pushed the feat/itmux-env-credentials branch from bd7cde2 to 603640d Compare July 15, 2026 22:24
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
NeuralEmpowerment force-pushed the feat/itmux-env-credentials branch from 603640d to 8853659 Compare July 15, 2026 22:31
@NeuralEmpowerment
NeuralEmpowerment changed the base branch from feat/itmux-python-client to main July 15, 2026 22:31
@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Dependency update: #250 is now merged. Rebased #254 directly onto current main, force-with-lease pushed, and retargeted this PR to main. Revalidated Rust format/compile plus 27 Python itmux-client tests and Ruff.

@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Fixed the required Rust clippy gate: handle_run now has the targeted too_many_arguments allowance used for CLI boundary functions. Verified locally with cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test.

Copilot AI review requested due to automatic review settings July 16, 2026 00:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants