Skip to content

fix(itmux): DooD credential transfer (docker exec stdin, not -v mounts)#231

Closed
NeuralEmpowerment wants to merge 1 commit into
mainfrom
fix/itmux-dood-creds
Closed

fix(itmux): DooD credential transfer (docker exec stdin, not -v mounts)#231
NeuralEmpowerment wants to merge 1 commit into
mainfrom
fix/itmux-dood-creds

Conversation

@NeuralEmpowerment

Copy link
Copy Markdown
Contributor

What & why

itmux (Rust driver) delivered credentials via docker run -v host:container bind mounts. In Syntropic137's docker-out-of-docker (DooD) topology the driver runs inside a container, so a sibling -v resolves the source on the OUTER daemon and mounts empty/nonexistent paths - every agent starts unauthenticated. The canonical Python driver abandoned -v for exactly this reason.

This ports the Python driver's model (parity source driver/interactive_tmux.py):

  • Bare docker run ... sleep infinity (no -v cred flags).
  • Push each credential file into the running container via docker exec -i with base64 over stdin (ExecStep.stdin) - credentials never appear in argv (PY:1506-1517).
  • Directories transferred per-file (matching PY:1540-1563).
  • Secure in-container: chown -R 1000:1000 + find -type f -exec chmod 600 so the non-root agent (uid 1000) can read them (PY:1566-1583).

Part of Plan 1a (itmux -> production parity), Tasks 1+2. Prerequisite for deleting the Python driver (Plan 1b). Tracks okrs-51p.6 / okrs-o78.

Tests & gates (local - Rust not yet in CI; wired in Plan 1a Task 7)

  • cargo test: 54 passed (new cred_transfer.rs: 7 tests asserting no -v cred flags, stdin-only payload, chown/chmod plan - all without a docker daemon).
  • cargo clippy --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.
  • No new dependencies (std-only base64 encoder).

Reviewer notes

  • Security-sensitive credential path. Matched the real Python source over illustrative examples (per-file writes not tar; dir mode left untouched as Python does).
  • Residual risk: the live end-to-end path (bare run -> stage -> tmux bootstrap) was not exercised against a real Docker daemon (sandboxed impl env). A live DooD smoke is gated in Plan 1a Task 7 before the Python driver is deleted.

…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`).
Copilot AI review requested due to automatic review settings July 5, 2026 23:26

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 updates the Rust itmux interactive-tmux workspace driver to work correctly in docker-out-of-docker (DooD) setups by removing credential -v bind mounts from docker run and instead transferring credential files into the running container via docker exec -i over stdin (base64), then locking down ownership and permissions in-container to match the canonical Python driver.

Changes:

  • Provision containers with a bare docker run ... sleep infinity argv (no credential -v flags) and then push staged credentials in via docker exec stdin.
  • Introduce a testable “exec plan” model for credential transfer + in-container chmod/chown, plus new parity tests.
  • Add a docker_exec_with_stdin helper in the tmux module for stdin-fed exec calls.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
providers/workspaces/interactive-tmux/driver-rs/tests/pane_tail.rs Minor formatting-only change in an existing test.
providers/workspaces/interactive-tmux/driver-rs/tests/cred_transfer.rs Adds DooD credential-transfer parity tests asserting no -v and stdin-only payload transfer/secure plan.
providers/workspaces/interactive-tmux/driver-rs/src/workspace.rs Switches provisioning to a bare docker run + post-start credential push via auth::stage_into_container.
providers/workspaces/interactive-tmux/driver-rs/src/tmux.rs Adds docker_exec_with_stdin for stdin-fed docker exec -i calls.
providers/workspaces/interactive-tmux/driver-rs/src/auth.rs Replaces mount-based auth with staged-path + exec-transfer plan (base64 stdin), plus in-container secure step.

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

Comment on lines +83 to +91
let out = child.wait_with_output()?;
writer
.join()
.map_err(|_| Error::other("stdin-writer thread panicked"))??;
check_output(
out,
&format!("docker exec -i {container} {}", redact_args(args)),
)
}
Comment on lines +506 to +513
pub fn stage_into_container(container: &str, prepared: &PreparedAuth) -> Result<()> {
for staged in prepared.iter() {
for step in plan_for_staged_path(staged)? {
run_exec_step(container, &step)?;
}
}
Ok(())
}
@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Review status: APPROVED (2 independent passes) - merge-ready, held for maintainer merge

Orchestrator review: PASS. No -v cred mounts; creds stdin-only; injection-safe path quoting; chown 1000:1000 + chmod 600 parity with _secure_container_path.

Independent adversarial security review: VERDICT: APPROVE. Attacked all 7 vectors (cred leakage, shell injection, base64 correctness, securing completeness, writer-thread deadlock, panics, parity drift) - no BLOCKER/MAJOR. Highlights:

  • base64 encoder byte-exact on 1/2/3-byte tails; chunks(3) cannot panic.
  • shell_quote safe-char set is a strict subset of Python's -> never under-quotes.
  • Writer-thread drops stdin -> EOF so base64 -d terminates; wait_with_output drains concurrently -> no large-payload deadlock.
  • Decoded creds redirect to file (>> path), never returned via captured stdout.

MINOR (non-blocking), tracked:

  1. No exec timeout on docker_exec/docker_exec_with_stdin (availability parity w/ DEFAULT_EXEC_TIMEOUT_S). -> Fixed by the stacked Task 3 PR (fix/itmux-exec-timeouts, bounded 15s/30s).
  2. Error label includes the sh -c command shape (Python redacts it). No credential leak (payload is stdin-only; container paths are not sensitive). -> folded into a later cleanup.

NITs (EPIPE masking, to_string_lossy on filenames, brief pre-chmod window matching Python's write-then-secure order) noted, none exploitable.

Note: codex review was unavailable (headless codex ended its turn without emitting a verdict); used an independent adversarial Claude review as the cross-check. Recommend a live DooD smoke (gated in Plan 1a Task 7) before the Python driver is deleted (Plan 1b).

@NeuralEmpowerment

Copy link
Copy Markdown
Contributor Author

Consolidated into a single itmux feature PR: #243. This work is included there (same commits); closing to reduce PR sprawl. Review/merge #243 instead.

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