Skip to content

feat(itmux): production parity + claude/codex support#243

Open
NeuralEmpowerment wants to merge 10 commits into
mainfrom
feat/itmux-production-parity
Open

feat(itmux): production parity + claude/codex support#243
NeuralEmpowerment wants to merge 10 commits into
mainfrom
feat/itmux-production-parity

Conversation

@NeuralEmpowerment

Copy link
Copy Markdown
Contributor

Single consolidated feature PR for the Rust itmux driver work - the whole "make itmux the production driver for claude + codex" effort in one reviewable unit. Supersedes the stacked PRs #231-#236 + #241 (closed in favor of this).

This is one of the three layers of the Workspace Standard: itmux (Rust driver). The other two are #240 (Python contract) and APSS #88 (recipe standard).

What's in it (10 commits, oldest first)

Commit What
65b5ca8 DooD credential transfer - docker-exec base64-over-stdin (no -v mounts) + chown/chmod securing. Fixes unauthenticated agents under docker-out-of-docker.
492dbc5 Bounded timeouts (15s/30s) on every docker/tmux exec.
af5030b Await poll resilience + container_dead classification (daemon-outage not misclassified).
9c41610 >12KB paste-buffer path with bracketed-paste -p + named buffer (multiline prompts paste atomically).
dc3f7e5 (superseded by allowlist below) codex plugins skip.
ea55a05 Wire Rust into CI + justfile.
fc193f1 base64 known-vector tests (RFC 4648 + high-bit).
993e079 Codex fix - stage only codex auth surface via allowlist (root-caused a 785MB ~/.codex staging hang).
4e8ba53 Codex readiness fixture.
c1d5775 Follow symlinks when staging codex auth (dotfile-manager auth.json/config.toml).

Validation

  • Live-validated on real Docker: itmux start --agents claude,codex --strict-startup -> both ready (claude 639ms, codex 66ms), authenticated via the DooD cred transfer.
  • Dual-reviewed: whole-stack Claude adversarial review (APPROVE) + codex cross-model pass (APPROVE) over the full main...HEAD delta.
  • cargo test / clippy -D warnings / fmt all green.

Known follow-up (from the standalone eval)

The claude adapter has a send-race (first keystrokes after launch can drop before input-readiness); fix incoming as a claude-adapter input-readiness gate (per-harness, generalizable). Tracked; not a blocker for review of the above.

Tracks okrs-o78 / okrs-51p.6 / okrs-51p.7.

…in-container (DooD)

Syntropic137 runs this driver INSIDE a container (docker-out-of-docker), so
`docker run -v host:container` bind mounts for credentials are wrong: a
sibling `-v` is resolved by the OUTER daemon against its own filesystem,
which cannot see this driver's own staging dir. The Rust port previously
did exactly that (`auth::Mount` -> `-v` args in `Workspace::start`), so in
production every agent started unauthenticated.

Ports the Python driver's fix (driver/interactive_tmux.py PY:1493-1583,
PY:1850-1869) bit-for-bit:

- `auth::prepare` now returns `PreparedAuth` (staged host path -> in-
  container destination), not `-v` bind-mount args.
- `Workspace::start` runs a bare `docker run ... sleep infinity` with no
  credential mounts, then calls `auth::stage_into_container` to push each
  staged file/dir in over `docker exec` stdin: files as base64 through
  `base64 -d`, directories file-by-file (mirrors Python's `os.walk`
  transfer exactly, not a tar stream). Credentials never appear in argv
  (world-readable via `ps`/`/proc/<pid>/cmdline`) - only ever in stdin.
- After transfer, every staged path is chowned to 1000:1000 (the in-
  container `agent` user) and locked to 0600 for files (`chmod 600` via
  `find` for directory trees), for claude, codex, and gemini - not just
  the 2 claude files as before.
- `docker_exec_with_stdin` added to tmux.rs (feeds stdin from a dedicated
  thread to avoid a pipe-buffer deadlock against `wait_with_output`).
- The transfer/secure command plan is built by pure functions
  (`write_bytes_plan`, `transfer_dir_plan`, `secure_path_plan`,
  `plan_for_staged_path`) so it's assertable in tests without a docker
  daemon; `tests/cred_transfer.rs` covers the no-`-v` docker run argv, the
  staged destination shape, stdin-only payload delivery, and the
  chown/chmod plan for both files and directories.

No new dependencies: added a small std-only base64 encoder rather than
pulling in the `base64` crate for one encode call (decode happens
in-container via the coreutils `base64 -d`).
Every docker/tmux shell-out previously used Command::output() unbounded,
so a wedged docker exec could hang the whole workflow forever. Add
run_bounded/wait_bounded (std-only, spawn + mpsc::recv_timeout, kill on
timeout) and route docker_exec, docker_exec_with_stdin, and the docker
run/rm call sites through it, matching the Python driver's
DEFAULT_EXEC_TIMEOUT_S (15s, PY:86) and DEFAULT_RUN_TIMEOUT_S (30s, PY:87).
Wire the Rust itmux driver into repo-wide QA gates, matching the
existing Python justfile/qa.yml conventions:

- justfile: add rust-test, rust-lint, rust-fmt-check, rust-fmt
  recipes and fold them into fmt/fmt-check/lint/test so qa, ci, and
  check all cover the Rust crate.
- .github/workflows/rust.yml: new workflow mirroring qa.yml's
  trigger/concurrency shape, installs stable Rust via
  dtolnay/rust-toolchain@v1 (clippy + rustfmt components), caches
  via Swatinem/rust-cache@v2.9.1, then runs cargo fmt --check,
  cargo clippy --all-targets -D warnings, and cargo test.
Whole-stack review flagged the credential path rides on this hand-rolled
encoder being byte-exact while the container-side base64 -d only runs under
real docker. Locks it with RFC 4648 vectors + high-bit/padding cases.
codex-cli 0.139.0's ~/.codex mixes a tiny auth/config surface (auth.json,
config.toml) with hundreds of megabytes of operational state under dirs the
old {tmp,log,logs,plugins} denylist never matched (.tmp/, sessions/,
logs_2.sqlite*, computer-use/, cache/, ...). Staging copied all of it
file-by-file over docker exec base64, turning start_workspace into a
multi-minute crawl that never completed - so itmux start --agents codex never
reached ready (claude, whose surface is one small file, works fine). The
--startup-timeout bound covers only the post-launch readiness wait, not
credential staging, so the symptom was a hang, not a timeout.

Switch codex staging from a fragile denylist to an allowlist of the actual
auth surface (auth.json, config.toml, config.json, AGENTS.md). An allowlist
cannot regress when codex adds new state directories. Empirically validated
that auth.json + config.toml is sufficient for codex to reach its ready
composer. Kept byte-for-byte parity between the Rust and Python drivers.
Capture the real codex-cli 0.139.0 first-ready pane after
itmux start --agents codex (trust banner accepted, hooks modal dismissed,
composer visible) as a fixture and assert codex_is_ready / codex_is_started
match it. Guards the start-path predicate now that the staging fix lets
start_workspace reach this pane.
Rust DirEntry::file_type() stats the link itself, not its target, so an
allowlisted auth.json/config.toml that is a symlink (common with dotfile
managers like stow/chezmoi) was silently dropped - the exact files the
allowlist exists to preserve. Use fs::metadata (follows symlinks) for the
file-type decision so a symlink-to-regular-file is staged by-value. Restores
parity with the Python driver, whose Path.is_file() already follows symlinks.

Also assert the full declared allowlist (config.json, AGENTS.md) in the
allowlist test, and add a unix symlink test proving symlinked auth files are
staged with their target's bytes.

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.

Pull request overview

This PR consolidates the Rust itmux driver work to reach production parity for Claude + Codex workspace startup, focusing on reliable credential staging in Docker-out-of-Docker environments, bounded shell-outs, and readiness/await resilience. It also adds CI + just integration and expands the Rust test suite with regression fixtures to lock in the parity behavior.

Changes:

  • Replace credential bind mounts with post-docker run credential transfer via docker exec stdin (base64), plus in-container permission lockdown.
  • Add bounded timeouts for docker/tmux shell-outs, resilient polling classification (retry vs container-dead), and a large-payload tmux send path using load-buffer/paste-buffer with bracketed paste.
  • Wire Rust into repo automation (Just + GitHub Actions) and add regression/fixture tests for Codex readiness, poll resilience, timeouts, and send-literal planning.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
providers/workspaces/interactive-tmux/driver/interactive_tmux.py Switch Codex staging from a denylist copy to a small allowlist of auth/config files.
providers/workspaces/interactive-tmux/driver-rs/src/auth.rs Implement staged-path model + docker exec stdin transfer plans, Codex auth allowlist, base64 encoder, and secure-in-container steps.
providers/workspaces/interactive-tmux/driver-rs/src/tmux.rs Add bounded subprocess execution, stdin-fed docker exec, and send-literal paste-buffer fallback for large payloads.
providers/workspaces/interactive-tmux/driver-rs/src/workspace.rs Provision bare container (no -v), stage credentials via exec, and add poll classification + container-dead detection.
providers/workspaces/interactive-tmux/driver-rs/src/result.rs Add AwaitResult::container_dead classification to surface target-death vs transient capture failures.
providers/workspaces/interactive-tmux/driver-rs/tests/cred_transfer.rs Add parity tests asserting exec-stdin credential transfer planning and secure steps.
providers/workspaces/interactive-tmux/driver-rs/tests/exec_timeout.rs Add tests validating bounded execution kills/returns promptly on timeouts.
providers/workspaces/interactive-tmux/driver-rs/tests/poll_resilience.rs Add tests for retry-vs-dead poll classification (including daemon-outage resilience).
providers/workspaces/interactive-tmux/driver-rs/tests/send_literal.rs Add tests for send-literal planning and the paste-buffer/bracketed-paste path over threshold.
providers/workspaces/interactive-tmux/driver-rs/tests/readiness.rs Add a live-captured Codex fresh-launch fixture and readiness regression test.
providers/workspaces/interactive-tmux/driver-rs/tests/pane_tail.rs Formatting-only change in an existing test.
providers/workspaces/interactive-tmux/driver-rs/tests/fixtures/codex/ready_fresh_launch.txt Add fresh-launch Codex pane capture fixture for readiness predicate regression coverage.
justfile Add Rust fmt/lint/test tasks and include them in combined targets.
.github/workflows/rust.yml Add a dedicated Rust CI workflow for fmt/clippy/test in the itmux crate directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +72
// Best-effort: ask the process (and, since docker/tmux
// children are frequently themselves a fork/exec wrapper,
// just its own pid - not a process group) to die. We do not
// have a `Child` handle here anymore (it was moved into the
// waiter thread so that thread can drain stdout/stderr without
// deadlocking on a full pipe buffer), so killing by pid via
// the `kill` utility is the only std-only, `unsafe`-free way
// to reach it from this side.
let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
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