8000 T30 — Run a command in a box with a terminal or detached, under an exec id by minimal-foundry[bot] · Pull Request #2843 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T30 — Run a command in a box with a terminal or detached, under an exec id - #2843

Merged
mitodrummer merged 9 commits into
mainfrom
agent/2404-run-a-command-in-a-box-with-a-terminal-or-detach
Nov 2, 2026
Merged

mitodrummer merged 9 commits into
mainfrom
agent/2404-run-a-command-in-a-box-with-a-terminal-or-detach

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor

T30 — Run a command in a box with a terminal or detached, under an exec id

slice: S7
kind: task
covers: BOX-149
size: L
est: 460 code, 420 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: daemon log names each exec's id and mode
observability: one span per exec
layers:
- exec:
- crates/minimald/src/exec.rs: argv unshelled; exec ids
- crates/minimald/src/exec/process.rs: terminal, detached, timeout
- crates/minimald/src/exec/bridge.rs: transcripts by exec id
- crates/minimald/src/exec/tests.rs: tests
test: cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup
test: cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state
test: cargo nextest run -p minimald every_exec_gets_uuidv7_id
test: cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript
test: cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code
test: cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running
- wire:
- crates/minimald-rpc/src/exec.rs: mode, timeout, exec id
- crates/boxes/src/store.rs: transcripts beside record
test: cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout
produces: none
consumes: none
proof: A detached command returns an id whose output reads back.
depends on:
- blocked by #2394
- blocked by #2398

Proof

A client sends min://exec with mode detached for a running box. It gets a UUIDv7 exec id on stdout at once. min://exec/output then returns what the command wrote, and min://exec/wait returns its exit code. The transcript is kept at /execs/.log until the box is reaped. exec_detach_closes_stdin_captures_logs_and_wait_returns_code shows this end to end on the daemon's exec path.

Notes from the agent: Wire: the new min://exec form carries {argv, mode: attached|tty|detached, timeout_secs}. New forms min://exec/attach, min://exec/output and min://exec/wait take an exec id. The old min://shell and min://argv forms are unchanged. They now also get an exec id and the not-running refusal. No client (min CLI, minimal-client) sends the new forms yet, because those crates are outside this task's layers. A follow-up task must add the CLI flags.

Limits a reviewer must know:

  1. The test harness has no box sandbox. So exec_runs_argv_unshelled_in_box_namespaces_and_cgroup proves the join this way: it builds the box host's nsenter Injection from the exec's argv and checks the words after -- with --pidfd and --classifier-leaf. It runs the argv directly on the host for the stdio and exit-code half. It does not execute inside a real box. The VM or KVM lanes (just e2e) still owe that evidence.
  2. The daemon allocates the tty exec's PTY with openpty, as the interactive session path does. The PTY is the process's stdin, stdout and stderr, so isatty is true. It is not the process's controlling terminal, because the nsenter shim (outside the layers) calls setpgid, not setsid. So Ctrl-C through the terminal does not signal the exec, and an attach does not pass window-size changes on.
  3. Exit codes and tty terminals for wait and attach live in an in-memory registry. Transcripts are on disk. After a daemon restart, output reads still work, and wait and attach refuse with a message. Each entry is …[truncated]

Tests

line exit tail
cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0557... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6668... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald every_exec_gets_uuidv7_id 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-13f5... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-5777... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-9746... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-eb20... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-d820... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling common v0.0.1 (/workbench

Verify

min task run agent-verify exited 100 under rustc 1.99.0, which the repository does not pin (1.99.1 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

d_and_retries_with_one_token
        PASS [   0.024s] ( 901/2004) minimald box_manager::tests::in_box_root_cannot_append_events
        PASS [   0.222s] ( 902/2004) minimald box_manager::tests::idle_session_does_not_block_unforced_shutdown
        PASS [   0.026s] ( 903/2004) minimald box_manager::tests::list_scoped_to_project_reads_project_root
        PASS [   0.023s] ( 904/2004) minimald box_manager::tests::native_create_ignores_a_client_supplied_box_id
        PASS [   0.022s] ( 905/2004) minimald box_manager::tests::needed_packages_skips_sessions_that_arent_active
        PASS [   0.459s] ( 906/2004) minimald box_manager::tests::guest_create_ignores_a_client_supplied_box_id_and_replies_when_no_door_answers
        PASS [   3.277s] ( 907/2004) minimald box_manager::tests::delete_during_resume_teardown_runs_the_destroy_hooks_once
        PASS [   0.240s] ( 908/2004) minimald box_manager::tests::pending_session_activates_on_approving_verdict
        PASS [   0.670s] ( 909/2004) minimald box_manager::tests::list_returns_every_retained_record_with_state
        PASS [   0.264s] ( 910/2004) minimald box_manager::tests::project_composable_contribution_reaches_composer
        PASS [   0.257s] ( 911/2004) minimald box_manager::tests::reading_a_composition_does_not_start_the_session
        PASS [   1.261s] ( 912/2004) minimald box_manager::tests::needed_packages_unions_across_sessions
        PASS [   1.491s] ( 913/2004) minimald box_manager::tests::no_automatic_reaper_runs
        FAIL [   0.785s] ( 914/2004) minimald box_manager::tests::reap_hands_name_to_retained_holder
  stdout ───

    running 1 test
    test box_manager::tests::reap_hands_name_to_retained_holder ... FAILED

    failures:

    failures:
        box_manager::tests::reap_hands_name_to_retained_holder

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1276 filtered out; finished in 0.77s

  stderr ───

    thread 'box_manager::tests::reap_hands_name_to_retained_holder' (3990) panicked at crates/minimald/src/box_manager/tests.rs:4721:5:
    assertion `left == right` failed
      left: Some(BoxId(01a19aff-4e3b-7c7e-80ce-d567d8730d72))
     right: Some(BoxId(01a19aff-4c57-7437-a80d-96f4deea66f7))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   0.264s] ( 915/2004) minimald box_manager::tests::reap_running_box_unforced_refuses
        PASS [   0.555s] ( 916/2004) minimald box_manager::tests::reap_resuming_box_refuses_and_forced_waits
        PASS [   1.554s] ( 917/2004) minimald box_manager::tests::prune_selectors_reap_and_print
────────────
     Summary [  94.137s] 917/2004 tests run: 916 passed, 1 failed, 24 skipped
        FAIL [   0.785s] ( 914/2004) minimald box_manager::tests::reap_hands_name_to_retained_holder
warning: 1087/2004 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)
error: test run failed

Closes #2404

Residuals

min://exec carries an argv with its mode (attached, tty or detached) and
an optional timeout. min://exec/attach, min://exec/output and
min://exec/wait name an exec by its UUIDv7 id. The box store places
each exec's transcript beside the record and appends events by path.
Every command for a box gets a UUIDv7 exec id, and a box that is not
running refuses it with exit 2 naming its state. min://exec runs an
argv with no shell: attached, on a daemon terminal, or detached with
stdin closed. Tty and detached execs answer with their id at once,
capture their output beside the box record, and keep their exit for
min://exec/wait. An exec's own timeout stops it and records an
exec_exited event with reason timeout, and the box keeps running.
@coderabbitai
coderabbitai Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7099f685-dd6d-4372-a72a-81be8e764fc0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@minimal-foundry
minimal-foundry Bot commented Nov 2, 2026 •
Copy link
Copy Markdown
Contributor Author

Reviewed: 2 findings, none blocking at 9fa80fd169d9.

Address the comments on the diff.

Clobber: none seen. Earlier threads on the unbounded wait after a timeout, EXECS growth and exit 124 are fixed in this diff.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/bridge.rs Resolved
@minimal-arch minimal-arch Bot 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.

arch-review: CONDITIONS · 5 conditions · the full ruling

This change runs an exec under a UUIDv7 id in three modes, and most of BOX-149 conforms. An exec that its own timeout ends returns 1. Lines 477 and 497 of the design say it returns 124. C1 fixes this. A wait reads the exit code from memory only, so a daemon restart loses it. Line 478 says the exit code is retained. C4 reads it from the stored exec_exited event. C2 adds principal and argv to exec_started, C3 makes the attached path refuse when it cannot write events, and C5 files the three limits as issues.

Ruled at 8ff9a9d against gominimal/arch@8640f85e193d by claude-opus-5-5.

Comment thread crates/minimald/src/exec/bridge.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec.rs Resolved
Comment thread crates/minimald/src/exec/bridge.rs Resolved
A command that survives the stop at its timeout no longer holds its
wait open: the wait after the stop is bounded, then SIGKILLed and
recorded as a timeout. Ended execs of a reaped box leave the exec map.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/bridge.rs Resolved
@minimal-arch minimal-arch Bot 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.

arch-review: CONDITIONS · 4 conditions · the full ruling

The exec path conforms on argv, state gate, ids, transcripts and the timeout sequence. Four gaps remain. A timed-out exec does not return 124, which line 497 requires (C1). A wait after a daemon restart refuses, but the exec_exited event on disk holds the exit code (C2, lines 23 and 501). The exec_started event has no principal or argv (C3, line 497). The PTY and CLI gaps outside the layers have no filed issue (C4).

Ruled at ce91b19 against gominimal/arch@8640f85e193d by claude-opus-5-5.

Comment thread crates/minimald/src/exec/bridge.rs Resolved
Comment thread crates/minimald/src/exec/bridge.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as ce91b198cf55.

line exit tail
cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-e9cc... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-b01d... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling graph v0
cargo nextest run -p minimald every_exec_gets_uuidv7_id 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-8c9f... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-42ac... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-8d56... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-1de4... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-ed5a... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling common v0.0.1 (/workbench

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/exec/process.rs: The wait after the stop at the timeout is now bounded (wait_after_stop). On the bound it sends SIGKILL and records a Timeout end with no code. This covers wait_with_timeout and TimedProcess::wait. Test: exec_timeout_ends_even_when_the_stop_fails.
  • 👍 valid — crates/minimald/src/exec/bridge.rs:1863: register_exec now drops ended entries whose box directory is gone, and find_exec skips them (ExecEntry::is_reaped). The reap path is outside this task's files, so the map prunes itself. Test: ended_exec_of_a_reaped_box_is_not_found.

A person can start a detached command in a running box, get its exec id back at once, and read its output back by that id. Now a wait on that id also returns when the command survives the stop at its timeout.

Notes from the agent: Revise round 1. The wait after a stop at the timeout now has a bound: SHIM_ESCALATION plus 5 s. When the bound elapses, the daemon sends a last SIGKILL and records the exec as ended by its timeout, with no exit code. This applies to wait_with_timeout and to TimedProcess::wait. The EXECS map now drops an ended exec when its box directory is gone. register_exec prunes these entries and find_exec skips them. The box reap code is outside this task's layers, so the map prunes itself and the reap code does not call into it. just clippy-strict was not run.

@mitodrummer
Copy link
Copy Markdown
Member

/revise arch C1 and C2 at ce91b19, in the layers.

C1: return 124 for an exec that its own timeout ended. In crates/minimald/src/exec/bridge.rs, ExecExit::status() returns 124 when reason is ExecExitReason::Timeout, before it falls back to code. In crates/minimald/src/exec/process.rs, TimedProcess::wait returns Ok(Some(124)) once timed_out is set (the wait_after_stop error case included), so the attached bridge sends 124 on the channel. exec_own_timeout_ends_exec_reason_timeout_box_keeps_running asserts exit.status() == 124. Add an assertion that a timed-out attached exec's channel status is 124.

C2: keep a detached exec's exit code across a daemon restart. In bridge.rs, serve_exec_wait takes the BoxHandle instead of the SessionId. When find_exec returns None, it reads the box's events file (ExecBox::dir joined with boxes::events::EVENTS_FILE) for the exec_exited event whose data.exec_id is the id, and answers with that exit (124 for reason timeout). It answers unknown_exit_message only when no such event exists. Put the lookup in stored_exec_exit(box_dir, id) -> Option<ExecExit>, and test it against an events file that holds an exec_exited event for an id that EXECS does not hold.

C3 and C4 are owned elsewhere (T31 #2406, #2845, T32 #2408) and listed under Residuals in the PR body. Do not do them here. Touch nothing else.

An exec that its own timeout ends now exits 124, attached or detached.
A wait on an exec that this daemon holds no entry for reads the box's
stored exec_exited event, so a detached exec's exit code outlives a
daemon restart.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
@minimal-arch minimal-arch Bot 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.

arch-review: PASS_WITH_CONDITIONS · 4 conditions · the full ruling

The compare fixes the two gaps that blocked. A timed-out exec now returns 124, and a wait reads the stored exit after a daemon restart, as line 497 and line 501 require. One deviation stays: the tty PTY is not the exec's controlling terminal, and line 497 says the PTY works like a PTY box (C1, #2406). The namespace and cgroup proof does not run in a real box, so that leg stays open until just e2e covers it (C2, #2408). C3 and C4 move the event fields and the CLI to their filed tasks. The change can merge with these four open.

Ruled at 56306bd against gominimal/arch@8640f85e193d by claude-opus-5-5.

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/tests.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec.rs Resolved
Comment thread crates/minimald-rpc/src/exec.rs Resolved
@mitodrummer
Copy link
Copy Markdown
Member

/revise One item, minimal-review PRRT_kwDOPdfcxM6ytbAm and PRRT_kwDOPdfcxM6ytbAx: the exec transcript (execs/<id>.log in the box state directory, written from crates/minimald/src/exec/process.rs start_managed_exec) has no size cap. A detached or tty command that writes without end (for example yes) fills the host disk outside any box quota. serve_exec_output and the tty attach replay in crates/minimald/src/exec/bridge.rs then load the whole file with std::fs::read. Cap the transcript: keep at most a fixed tail (pick a constant such as 8 MiB, documented), dropping the oldest bytes (rotate or truncate-and-mark), and keep the command running when the cap is reached. Make the output read and the attach replay stream with a bounded buffer instead of reading the whole file. Add a test that a command writing past the cap leaves a transcript no larger than the cap, holding the newest output. Touch nothing else. (The cargo-deny failure is RUSTSEC-2026-0433, fixed by #2846.)

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2404. Your note is kept and goes into the next round here, which starts when that coder finishes.

Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a /revise note, pushed as 56306bdfb9dd.

line exit tail
cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-d6f9... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-9660... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald every_exec_gets_uuidv7_id 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-2575... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-71a6... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-4d91... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-a8cb... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-4f1e... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling common v0.0.1 (/workbench

Verify min task run agent-verify exited 100 under rustc 1.99.0, which the repository does not pin (1.99.1 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

d_and_retries_with_one_token
        PASS [   0.024s] ( 901/2008) minimald box_manager::tests::in_box_root_cannot_append_events
        PASS [   0.230s] ( 902/2008) minimald box_manager::tests::idle_session_does_not_block_unforced_shutdown
        PASS [   0.027s] ( 903/2008) minimald box_manager::tests::list_scoped_to_project_reads_project_root
        PASS [   0.025s] ( 904/2008) minimald box_manager::tests::native_create_ignores_a_client_supplied_box_id
        PASS [   0.024s] ( 905/2008) minimald box_manager::tests::needed_packages_skips_sessions_that_arent_active
        PASS [   0.467s] ( 906/2008) minimald box_manager::tests::guest_create_ignores_a_client_supplied_box_id_and_replies_when_no_door_answers
        PASS [   3.292s] ( 907/2008) minimald box_manager::tests::delete_during_resume_teardown_runs_the_destroy_hooks_once
        PASS [   0.284s] ( 908/2008) minimald box_manager::tests::pending_session_activates_on_approving_verdict
        PASS [   0.768s] ( 909/2008) minimald box_manager::tests::list_returns_every_retained_record_with_state
        PASS [   0.231s] ( 910/2008) minimald box_manager::tests::project_composable_contribution_reaches_composer
        PASS [   0.250s] ( 911/2008) minimald box_manager::tests::reading_a_composition_does_not_start_the_session
        PASS [   1.494s] ( 912/2008) minimald box_manager::tests::no_automatic_reaper_runs
        PASS [   1.583s] ( 913/2008) minimald box_manager::tests::needed_packages_unions_across_sessions
        FAIL [   0.826s] ( 914/2008) minimald box_manager::tests::reap_hands_name_to_retained_holder
  stdout ───

    running 1 test
    test box_manager::tests::reap_hands_name_to_retained_holder ... FAILED

    failures:

    failures:
        box_manager::tests::reap_hands_name_to_retained_holder

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1280 filtered out; finished in 0.81s

  stderr ───

    thread 'box_manager::tests::reap_hands_name_to_retained_holder' (3877) panicked at crates/minimald/src/box_manager/tests.rs:4721:5:
    assertion `left == right` failed
      left: Some(BoxId(01a19cb7-c104-79a1-80e5-9a3e1580dc83))
     right: Some(BoxId(01a19cb7-bf03-7609-abc5-3983bf8a084e))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   0.291s] ( 915/2008) minimald box_manager::tests::reap_running_box_unforced_refuses
        PASS [   0.601s] ( 916/2008) minimald box_manager::tests::reap_resuming_box_refuses_and_forced_waits
        PASS [   1.612s] ( 917/2008) minimald box_manager::tests::prune_selectors_reap_and_print
────────────
     Summary [  95.802s] 917/2008 tests run: 916 passed, 1 failed, 24 skipped
        FAIL [   0.826s] ( 914/2008) minimald box_manager::tests::reap_hands_name_to_retained_holder
warning: 1091/2008 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)
error: test run failed

Review threads the coder judged:

  • 👍 valid — PRRT_kwDOPdfcxM6yj2BD: ExecExit::status() now returns 124 when reason is Timeout, before it falls back to the code. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6yj2BI: TimedProcess::wait returns Ok(Some(124)) after a deadline kill, also when the bounded wait fails. Tested by attached_exec_timeout_sends_status_124. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6yj2BM: Not changed in this PR. The revise note gives the exec_started principal and argv fields to other tasks (T31 T31 — Keep terminal and detached execs past a client loss, stop one exec, and honour exec_enabled #2406, min box exec verbs over min://exec, attach, output and wait (BCLI) #2845, T32 T32 — Exec into a box, end to end #2408), and the PR body lists them under Residuals. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6yj2BO: Not changed in this PR. The revise note gives this work to other tasks (T31 T31 — Keep terminal and detached execs past a client loss, stop one exec, and honour exec_enabled #2406, min box exec verbs over min://exec, attach, output and wait (BCLI) #2845, T32 T32 — Exec into a box, end to end #2408), and the PR body lists it under Residuals. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6yj2BQ: When find_exec misses, serve_exec_wait reads the box's stored exec_exited event through stored_exec_exit and answers with that exit. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6yj_2o: The fix is setsid and TIOCSCTTY in the nsenter.rs shim. That file is outside this task's layers. The revise note gives it to another task, under Residuals. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6yj_2t: Not changed in this round, because the revise note limits it to C1 and C2. The gap is real: a lagging client loses chunks that the transcript keeps. A follow-up must replay them or tell the client. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6ykBX2: status() returns 124 for reason Timeout, and TimedProcess::wait gives 124 on the attached path. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6ykBX8: Added stored_exec_exit(box_dir, id). The wait uses it when find_exec returns None. Tested by stored_exec_exit_reads_the_exit_after_a_restart. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6ykBX-: Not changed in this PR. The revise note gives C3 (principal and argv on exec_started) to T31 T31 — Keep terminal and detached execs past a client loss, stop one exec, and honour exec_enabled #2406, min box exec verbs over min://exec, attach, output and wait (BCLI) #2845, T32 T32 — Exec into a box, end to end #2408, under Residuals. (not posted on the thread: not an open thread on the pull request)
  • 👎 invalid — PRRT_kwDOPdfcxM6ykBYA: Not changed in this PR. The fix is in the nsenter shim, outside the layers. The revise note gives C4 to another task, under Residuals. (not posted on the thread: not an open thread on the pull request)

A detached command returns an id. Its output reads back by that id, and a wait on that id returns its exit code, also after a daemon restart. An exec that its own timeout ends exits 124.

Notes from the agent: Revise round 2, C1 and C2 only, as the note asks. C1: ExecExit::status() returns TIMEOUT_STATUS (124) for reason Timeout. TimedProcess::wait returns Ok(Some(124)) once timed_out is set, and the wait_after_stop error case is logged and also gives 124. C2: serve_exec_wait takes the BoxHandle. When find_exec misses, it calls stored_exec_exit(box_dir, id), which reads the last exec_exited event for the id from events.jsonl. A timeout exit prints the same timeout line as a live wait. C3 and C4 are not done here (owned by T31 #2406, #2845, T32 #2408). The ExecStarted principal/argv, the exec.rs refusal, the PTY controlling terminal and the attach-lag findings are not changed this round. just clippy-strict was not run.

A detached exec now writes through a pipe and a pump, as a tty exec
does, so the daemon can cap its transcript. Past the cap the transcript
keeps a mark and the newest half of the cap, and the command keeps
running. The output read and the attach replay copy the transcript in
64 KiB chunks, up to its length at the open, and the attach takes the
terminal lock only to open the file and subscribe.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — approval withheld

Verdict COMMENT — APPROVE withheld: 2 unresolved threads — minimal-foundry[bot] crates/minimald/src/exec/process.rs, minimal-foundry[bot] crates/minimald/src/exec/process.rs. No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-arch minimal-arch Bot 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.

arch-review: PASS_WITH_CONDITIONS · 4 conditions · the full ruling

The compare adds an 8 MiB cap to each exec transcript, and that conforms. The design gives no size bound for an exec's transcript. Line 473 makes only the entrypoint's logs complete from the box's start. The four conditions from the last ruling stay open as C1–C4, each with a filed issue. C1 adds principal and argv to exec_started, as line 497 requires. Update the exec_transcript_path doc in crates/boxes/src/store.rs to say the file keeps the newest output after a cap.

Ruled at ecd002b against gominimal/arch@8640f85e193d by claude-opus-5-5.

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald-rpc/src/exec.rs Resolved
Comment thread crates/minimald/src/exec/tests.rs Resolved
Comment thread crates/boxes/src/store.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a /revise note, pushed as ecd002ba3fbe.

line exit tail
cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-6543... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-9bef... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald every_exec_gets_uuidv7_id 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-011b... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-44fe... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-4a41... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-4079... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-cd55... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling common v0.0.1 (/workbench

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/exec/process.rs: Capped the transcript at 8 MiB. Past the cap it keeps the newest 4 MiB and a mark, and the command keeps running. Output read and attach replay stream 64 KiB chunks, and the replay reads after it releases the terminal lock.
  • 👍 valid — crates/minimald/src/exec/process.rs: Detached execs now write through a pipe and a pump into a transcript capped at 8 MiB. read_exec_output returns an open file that is streamed in chunks, not read whole. New test: exec_transcript_past_cap_keeps_newest_output_within_cap.

A detached command returns an exec id, and min://exec/output for that id reads its output back. The transcript now holds at most 8 MiB, the newest output, so a command such as yes keeps running and does not fill the host disk.

Notes from the agent: Revise round 3: transcript cap. TRANSCRIPT_CAP = 8 MiB, in bridge.rs. A write that takes the transcript past the cap replaces the file with one mark line and the newest 4 MiB. The new file is <id>.log.next, renamed over <id>.log, so a reader that has the old file open still reads a whole transcript. The command keeps running. A detached exec now writes through a pipe and a pump (pump_output), as a tty exec does, because the daemon cannot cap a file that the child writes directly. After the exit, the supervisor waits up to 1 s for a detached pump, but it does not abort it, so output that a descendant writes later still reaches the transcript. A tty pump is aborted as before. The output read (serve_exec_output) and the attach replay (relay_terminal) now stream the file in 64 KiB chunks, up to its length at the open. The attach takes the terminal lock only to open the file and subscribe, and it reads after it releases the lock. New printed text: the mark line at the top of a capped transcript, [older output was dropped. Only the newest output is kept.]. The 8 MiB cap is a fixed constant and nothing configures it. The cut at the cap can split a line or a terminal escape sequence. exec_request_scrubs_credentials_from_log took 134 s. It runs an attached curl to https://x/ and waits on the network, and it does not go through the changed code. I did not run just clippy-strict. The cargo-deny failure (RUSTSEC-2026-0433) is outside this task and #2846 fixes it.

@minimal-foundry minimal-foundry Bot left a comment
8 Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — approved

Verdict APPROVE — every review thread is resolved; @mitodrummer closed the last one. The review of ecd002ba3fbe withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Nov 2, 2026
@mitodrummer
Copy link
Copy Markdown
Member

/revise Three items from my review at ecd002b, in the layers.

  1. (medium) Bound tty attach input. ExecTerminal::new (crates/minimald/src/exec/bridge.rs ~1710, ~1730) uses mpsc::unbounded_channel, and relay_terminal (~2096) pushes every ChannelMsg::Data into it, while feed_terminal (process.rs) blocks on the PTY write. If the program never reads stdin (for example sleep 1e9), the queue and daemon memory grow without limit, outside the box's cgroup. The existing attached path caps unread input with HeldInput (EXEC_INPUT_HELD_MAX 256 MiB per exec, EXEC_INPUT_HELD_DAEMON_MAX 512 MiB). Route tty exec input through HeldInput with the same bounds, or use a bounded channel with .send().await so the SSH window pushes back. Add a test: a tty exec whose program never reads stdin, fed more than the bound, does not hold more than the bound.

  2. (low) A huge timeout_secs panics the attached path. TimedBoxExec::exec (process.rs) computes tokio::time::Instant::now() + t, which overflows for values such as u64::MAX after the process has already been spawned. Use checked_add and fall back to no deadline (or cap timeout_secs in ExecRequest::parse, crates/minimald-rpc/src/exec.rs, with a clear refusal). Add a test.

  3. (test) Prove isolation between boxes: start an exec in box A, and assert that attach, output and wait for that exec id all refuse from box B.

Touch nothing else.

A tty exec's terminal input went through an unbounded channel, so a
program that never reads stdin let the daemon queue grow without
limit. The terminal now holds typed input in a HeldInput with the
attached path's bounds (EXEC_INPUT_HELD_MAX per exec,
EXEC_INPUT_HELD_DAEMON_MAX for the daemon). Past either bound the
chunk is dropped and the log says so once per exec. The exec's end
releases what it held.

An attached exec's timeout too large for the clock panicked after the
spawn. timeout_deadline now uses checked_add and gives no deadline.

Tests: unread tty input stays within the cap, a timeout of u64::MAX
seconds runs the exec to its end, and an exec id from one box finds no
exec, output or exit in another.
The tty feed wrote to the terminal from a blocking thread. When the
program never read its input, that write never returned, and each such
exec left a blocking thread behind for good. The terminal and the
detached pipe are now non-blocking and watched through AsyncFd, so the
supervisor can stop the pump and the feed at the exec's end. Both stop
on a closed state too, because tokio never clears it and a wait for it
returns at once.
@minimal-arch minimal-arch Bot 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.

arch-review: PASS_WITH_CONDITIONS · 4 conditions · the full ruling

The two new commits bound the input that a tty exec holds and stop the feed when the exec ends. They also stop a panic on a very large timeout. No design line changes status with them. The four open conditions from the last ruling carry forward unchanged, and each has a filed issue. C1 and C2 close the gaps against line 497: principal and argv on exec_started, and a PTY that is the controlling terminal.

Ruled at 4b4838f against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/minimald/src/exec/bridge.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
@minimal-foundry minimal-foundry Bot removed the review:approved minimal-review approved the current head label Nov 2, 2026
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on a /revise note, pushed as 4b4838f1dae2.

line exit tail
cargo nextest run -p minimald exec_runs_argv_unshelled_in_box_namespaces_and_cgroup 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-3c0a... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald exec_into_non_running_box_refused_exit2_names_state 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-8730... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald every_exec_gets_uuidv7_id 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-2758... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_tty_returns_reattachable_id_and_keeps_transcript 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-f56f... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_detach_closes_stdin_captures_logs_and_wait_returns_code 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-44f0... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald exec_own_timeout_ends_exec_reason_timeout_box_keeps_running 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-fdae... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimald-rpc exec_request_carries_mode_and_timeout 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-624a... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling paths v0.0.1 (/workbench/crates/paths) Compiling common v0.0.1 (/workbench

Verify min task run agent-verify passed.

A detached command returns an exec id, and its output reads back by that id. A tty exec whose program never reads its input now holds no more of what a client types than the exec's cap. An exec id from one box finds no exec, output or exit in another box. A timeout too large for the clock no longer panics the daemon.

Notes from the agent: Revise round 4. Item 1: tty input now goes into a HeldInput with the daemon's bounds (EXEC_INPUT_HELD_MAX per exec, EXEC_INPUT_HELD_DAEMON_MAX for the daemon). I did not use a bounded channel with send().await. russh gives no backpressure, so a parked relay would park the whole session loop. A tty has no stall-and-end path like the attached exec, so a chunk past a bound is dropped, and the daemon log warns once per exec. The exec's end clears the held input. The new test exposed an older defect that was present before this round. The tty feed wrote to the terminal from a blocking thread, and that write never returned when the program did not read. So each such exec leaked a blocking thread, and the test runtime hung at shutdown. The fix is in process.rs: the terminal and the detached pipe are now O_NONBLOCK and watched through AsyncFd, and the supervisor aborts the feed at the exec's end. The pump and the feed also stop on a closed readiness state. Tokio never clears that state, so without the check the feed spun inside one poll after the program died. This calls crate::net::switch::set_nonblocking, which already exists, and does not change that file. Item 2: the deadline now comes from timeout_deadline, which uses checked_add and gives no deadline when the add overflows. The box's own ceilings still apply. The managed path already used tokio::time::timeout, and that saturates. Item 3: the isolation test checks the lookups that the attach, output and wait handlers make (find_exec, read_exec_output in the box's directory, stored_exec_exit), from box B, while the exec runs and after it ends. The serve_exec_* functions ta…[truncated]

@mitodrummer mitodrummer left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at 4b4838f. The new exec forms pass the same session gate as existing execs, every exec-id lookup is scoped to its own box (pinned by exec_id_from_one_box_is_refused_in_another), and ids parse as UUIDv7 only. A timeout exits 124 on both paths with a bounded stop. A detached exit survives a daemon restart through the stored exec_exited event. Transcripts are capped at 8 MiB with atomic rotation and streamed reads, and tty input is held within the daemon's existing input bounds. Remaining items are residuals in T31 #2406, #2845 and T32 #2408.

@minimal-arch minimal-arch Bot 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.

arch-review: PASS_WITH_CONDITIONS · 4 conditions · the full ruling

The exec code did not change since the last ruling on 4b4838f1. This head only merges main (#2842, #2846), and the design lines for exec did not change in arch 7a86ec7b. BOX-149 conforms to architecture.md lines 23, 477 and 497. Four conditions stay open as filed residuals. C1 adds principal and argv to exec_started. C2 makes the tty PTY the controlling terminal. C3 adds the client forms. C4 proves the join in a real box with just e2e.

Ruled at 9fa80fd against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/exec/process.rs Resolved
Comment thread crates/minimald/src/exec/process.rs Resolved
@mitodrummer
mitodrummer added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 3a458b7 Nov 2, 2026
36 checks passed
@mitodrummer
mitodrummer deleted the agent/2404-run-a-command-in-a-box-with-a-terminal-or-detach branch November 2, 2026 21:00
norrietaylor added a commit that referenced this pull request Nov 3, 2026
* fix(deps): bump pulldown-cmark to 0.13.6 for RUSTSEC-2026-0433 (#2846)

Claude-Session: https://claude.ai/code/session_01NSAwDroXL6KqsSxUzdbZGw

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(minimald): hand a reaped name over in one manager step (#2842)

A reap read the records and then sent the handover as a second message.
A removal that began between the two left the name with no row while
another retained box still had it. The manager now releases the reaped
box's row, picks the successor among boxes not being removed, and
registers it in one message. The reap and the prune return after both.

The daemon log names the release (name-released) before the handover
(name-handed-over), both with box ids.

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T126 — A host-address box's port that collides on the host is shown at session start and in `min ls` (#2830)

* feat(minvmd): answer a node's collided mirror ports over control

The host control socket answers a read-only mirror_collisions verb
with the node's mirrored ports that collided with a holder on the
host. The node mirror keeps the time each port first collided.

* feat(minimal): show collided mirror ports in min ls and activate

min ls prints a MIRROR PORTS row per VM whose host_ip box ports are
held by another program on the host, and --json carries
mirror_collisions per VM. min session activate of a host_ip box on a
VM host prints the same sentence per port when it does not attach.

* fix(minimal): show an unread VM's mirror ports as null in min ls

Read each VM's collided mirror ports concurrently, and keep a failed
read apart from an empty one: --json writes null for a VM with
EE4D
 no
answer and [] only for an answered empty read.

* fix(minimal): name a VM host whose mirror ports were not read

min ls printed nothing for a running VM host that gave no answer, so
it looked the same as one with no collisions. It now prints one
MIRROR PORTS row that says the VM did not report its ports.

* fix(minimal): skip the mirror-port read on a native host

A native host has no VM host daemon. The fallback listing's control
socket there is minimald's, which does not serve the collided-port
read, so min ls printed a false MIRROR PORTS row on every native host.

* fix(minimal): name an unread VM host at activate, enter the span

min session activate now prints the unread sentence that min ls prints
when the VM host daemon gives no answer. The ListSessions span wraps the
collided-port reads, and a native host's --json carries no
mirror_collisions field.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T42 — Start every box's entrypoint from its spec through one launcher (#2841)

* feat(mfile): carry entrypoint and lifetime in the box spec

The spec gains an [execution] table with the box's lifetime and, for a
box that does not run its login shell, its entrypoint argv. A v0 spec
stored before the table existed reads with the entrypoint its type
implies.

* feat(minimald): start each box's entrypoint from its spec

One launcher decision, LaunchSpec::entrypoint, reads the first process
from the box's stored spec: the login shell, or the spec's argv. The
actor reads the stored spec once, keeps its hold, and hands it to every
launcher. A launch minted for an exec keeps the shell, because the exec
runs the box's command. Each launch logs its entrypoint in a
box.launch span.

* fix(minimald): keep the shell on every launch of an exec-run box

A task's run arrives over the exec channel. A hook launch or an attach
can come before or after that exec, and a hook launch's gated first
process starts at the next attach. Only the exec-minted launch held
the shell, so a task box could run its task twice, and an attach got
the task, not a shell. Every launch now says an exec carries the run,
and the launcher starts the shell for it.

* refactor(mfile): write box.lifetime and execution.exec in the spec

Rename Lifetime::Persistent to UntilStopped, move the lifetime to
[box] with a until_complete default for v0 specs, and serialize the
entrypoint as execution.exec. List the argv shape as a v0 exception.

* fix(minimald): refuse a launch whose stored spec cannot be read

held_spec returns an error for unreadable spec bytes or a failed store
read, and launch_host refuses with SpawnFailed instead of starting the
shell. Mark run_by_exec as an interim that T43 (#2405) removes.

* test(minimald): store a readable spec in the stopped-box record test

A launch now refuses a stored spec it cannot read, so the test's
placeholder bytes failed its ensure_host. Store a session spec instead.

* docs(mfile): say that no launch reads the spec's lifetime yet

A box ends when its entrypoint returns, whatever box.lifetime says.

* fix(minimald): keep entrypoint arguments out of the launch log

The box.launch span and its info event printed every argument of an
argv entrypoint. An argument can carry a secret, so they now name the
program (or the login shell) and the argument count only. A test pins
that a secret-looking argument does not reach the log.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Karl Godard <karl@minimal.dev>

* T30 — Run a command in a box with a terminal or detached, under an exec id (#2843)

* feat(minimald-rpc): carry exec mode, timeout and exec id on the wire

min://exec carries an argv with its mode (attached, tty or detached) and
an optional timeout. min://exec/attach, min://exec/output and
min://exec/wait name an exec by its UUIDv7 id. The box store places
each exec's transcript beside the record and appends events by path.

* feat(minimald): run box execs under an id, with a terminal or detached

Every command for a box gets a UUIDv7 exec id, and a box that is not
running refuses it with exit 2 naming its state. min://exec runs an
argv with no shell: attached, on a daemon terminal, or detached with
stdin closed. Tty and detached execs answer with their id at once,
capture their output beside the box record, and keep their exit for
min://exec/wait. An exec's own timeout stops it and records an
exec_exited event with reason timeout, and the box keeps running.

* style(minimald-rpc): format the exec id parse and its tests

* fix(minimald): bound the wait after an exec timeout; drop reaped execs

A command that survives the stop at its timeout no longer holds its
wait open: the wait after the stop is bounded, then SIGKILLed and
recorded as a timeout. Ended execs of a reaped box leave the exec map.

* fix(minimald): exit 124 on exec timeout; wait reads stored exit

An exec that its own timeout ends now exits 124, attached or detached.
A wait on an exec that this daemon holds no entry for reads the box's
stored exec_exited event, so a detached exec's exit code outlives a
daemon restart.

* fix(minimald): cap exec transcripts at 8 MiB and stream reads

A detached exec now writes through a pipe and a pump, as a tty exec
does, so the daemon can cap its transcript. Past the cap the transcript
keeps a mark and the newest half of the cap, and the command keeps
running. The output read and the attach replay copy the transcript in
64 KiB chunks, up to its length at the open, and the attach takes the
terminal lock only to open the file and subscribe.

* fix(minimald): bound tty exec input; no deadline past the clock

A tty exec's terminal input went through an unbounded channel, so a
program that never reads stdin let the daemon queue grow without
limit. The terminal now holds typed input in a HeldInput with the
attached path's bounds (EXEC_INPUT_HELD_MAX per exec,
EXEC_INPUT_HELD_DAEMON_MAX for the daemon). Past either bound the
chunk is dropped and the log says so once per exec. The exec's end
releases what it held.

An attached exec's timeout too large for the clock panicked after the
spawn. timeout_deadline now uses checked_add and gives no deadline.

Tests: unread tty input stays within the cap, a timeout of u64::MAX
seconds runs the exec to its end, and an exec id from one box finds no
exec, output or exit in another.

* fix(minimald): wait on exec terminals in the runtime, not a thread

The tty feed wrote to the terminal from a blocking thread. When the
program never read its input, that write never returned, and each such
exec left a blocking thread behind for good. The terminal and the
detached pipe are now non-blocking and watched through AsyncFd, so the
supervisor can stop the pump and the feed at the exec's end. Both stop
on a closed state too, because tokio never clears it and a wait for it
returns at once.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Karl Godard <karl@minimal.dev>

* fix(minimald): hand a destroyed box's name to the next holder (#2849)

A destroy removed the box that held a name's row and left the name
with no row, while another retained box still had that name. The
destroy now runs the same handover as a reap: the row goes to the
running box with the name, else to the most recent creation. The
destroy answers after the handover.

The daemon log names the destroyed box and the successor, and the
destroy span records the successor id or "none".

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T114 — `min ls` and the box commands tell the truth, end to end (#2839)

* test(e2e): prove min ls and the box commands across two VMs

Adds the ls_and_box_commands_tell_the_truth case: a stopped VM's boxes
in min ls, the ADDRESS and EGRESS cells against min session policy,
policy and forward without --vm, the unknown-box and stopped-VM
refusals, both proxy ports after the preferred port frees, and the
--public and out-of-range expose refusals.

* test(e2e): record ls_and_box gates as NOT RUN, check declared rules

The four gates of ls_and_box_commands_tell_the_truth now call not_run,
so the lane summary names the case. A 'declared rules' EGRESS cell is
now checked against the subnets rule row that min session policy
prints, not only against an egress heading.

* test(e2e): read the expose address past a trailing carriage return

The ls_and_box case read the box's address from the in-box expose reply
with an end-of-line anchor. A reply over the exec channel can end in a
carriage return, which left the address empty.

* test(e2e): brace lt_port before a bracket class in the expose read

shellcheck reads $lt_port[[:space:]] as an array index (SC1087).
The braces keep the same match and make the script lint-clean again.

* fix(minimal): poll the owning VM in min net forward

A forward to a box on another VM polled the selected VM for its
session. That VM does not hold the box, so the forward closed on the
first tick. Reconnect the poll to the owning VM, version-gated.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Co-authored-by: Norrie Taylor <91171431+norrietaylor@users.noreply.github.com>

* docs(spec): specify dns steering and the host_ip lane for spec 24 (#2851)

* docs(spec): specify dns steering and the host_ip lane for spec 24

Add BEP-080 to BEP-087 so the default `dns` steering works for own_ip
and host_ip boxes on a VM host, as architecture D6 and networking §5.4
require.

- BEP-080, BEP-081: direct TLS to the proxy on port 443, routed by
  SNI, and admitting dns and both on a VM host (#2805).
- BEP-082 to BEP-087: the host_ip lane: a host-side box id, the cohort
  proxy lane, cohort-attributed delivery with the named box's egress
  and audit subject, the in-effect rule, per-box steering, and the
  live proof.
- BEP-015 gains a host_ip proxy_env case.
- Security considerations states the cohort lane and per-box steering.

* docs(spec): keep the first slice paragraph unchanged and move the host_ip e2e checks to BEP-087

* docs(spec): address review on the host_ip lane requirements

- BEP-083: the cohort lane also admits TCP 443 at the proxy's address,
  the direct TLS path dns steering uses.
- BEP-084: refuse and audit a cohort-attributed value that names no
  live host_ip box of this host.
- BEP-086: cite NET-003's VM-backed resolver requirement, not NET-079.
- BEP-087: the proxy_env case is its own sub-requirement.
- Security considerations: BEP-083 and BEP-084 join the redemption
  invariant, and BEP-084 the egress invariant.

* T10 — Log a box's output from its start, out of the box's reach (#2852)

* feat(boxes): name a box's output log beside its record

Adds LOG_FILE and BoxDir, the box's directory as code outside the store
sees it: the log path, a lookup by id through the index, a log read by
offset, and an event append for a writer that holds no DiskBox.

* feat(minimald-rpc): add a streamed read of a box's output log

* feat(minimald): log a box's output from its start

The host appends every read from the box's terminal to its output log
before it hands the bytes to an attached client, so the client is a
second reader. A binding whose client closes its channel, drops its
connection or is shed records a detached event, and the box runs on.

* feat(minimald): serve a box's output log by its id

ReadLog streams the log from a byte offset, and with follow sends new
output until the box is reaped. A test shows that files written into
the box's own directories reach neither the log nor the record.

* fix(minimald): read a box's log on the blocking pool

The followed log read polled the store with blocking std::fs calls on an
async task. The find, the chunk reads and the retained check now run
through spawn_blocking, so a slow disk does not hold a runtime worker.

* fix(boxes): cap a box's output log and read it across a drop

The log keeps a drop mark and the newest half past 8 MiB. A reader
keeps its file open and moves to the replaced file with no byte sent
twice. The read refuses a link or a file that is not regular.

* fix(minimald): start no store box without its output log

BoxOutputLog::open returns the open's error for a box in the store, and
Host::build stops before the launch. The log writes through the capped
store writer, and ReadLog keeps one reader for the whole stream.

* fix(minimald): run detach hooks when a PTY client is lost

A lost client already records a detached event. Its on_detach hooks now
run too, so the event and the hooks agree. A test drives a box past the
log cap.

* style: format the log cap tests

* docs(boxes): mark the 8 MiB log cap as not yet in the design

The cap drops the start of a box's output past 8 MiB. That departs from
architecture.md line 473. gominimal/arch#159 decides the bound.

* docs(specs): record the interim 8 MiB box log cap in BOX

The decision that logs a box's output from its start now states the
8 MiB cap, what the log keeps past it, and the arch#159 decision that
removes the cap or changes BOX-155.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* T9 — End a box when its entrypoint returns, never for idleness or client loss (#2850)

* feat(minimald): end an until_complete box when its entrypoint returns

The host loop now says whether a kill ended it. The actor watches each
launch's loop and, when the entrypoint returned on its own and the spec
sets lifetime = "until_complete", sets the record to exited with the
exit code and records an exited event with reason "exit". The daemon
does this whether or not a client is attached. Such a box raises no
shell-exit prompt (BOX-038). A client that goes away never stops a box
(BOX-015).

* test(minimald): pin box ends on entrypoint return, not client loss

Adds the actor tests for BOX-038 and BOX-015. The shell round trip now
expects no prompt and an exited record. Tests about a box kept after its
shell exits now create an until_stopped box. The rpc shutdown test no
longer answers a prompt.

* fix(minimald): retry the exited-record write when an entrypoint ends

The host and the watch are gone by the time the write runs, so a
single failed write left the box running with nothing in it. Retry it
with a short backoff, and pin the retry with a test seam.

* test(e2e): expect an exited box, not a prompt, when a shell exits

The session-exit prompt is gone: exiting the shell ends the box, which
stays listed as exited. The pty driver no longer answers a prompt. Each
leg that exited a shell now checks the box is exited with code 0, and
destroys it where it used to answer Delete.

* docs(minimald): put the launch-record seam doc back on its module

* test(e2e): resume the exited kept box before the exec into it

An exec runs only in a running box. After the shell exits and the row is
withdrawn, the leg now resumes the box with a pty 'min session attach',
checks that it runs under the same id and that its row is back, and then
runs the exec as before.

* test(minimald): fail when the channel stays open after shell exit

The wait loop ended on a timeout as well as on close, so the test
passed even if the channel never closed. Panic on the timeout.

* fix(minimald): end an exited box before a queued attach or exec

A message queued beside a returned entrypoint ran first and launched
a new host, so the entrypoint ran again and its exit was lost. Now
only a stop or a destroy goes ahead of the exit, and ensure_host
refuses a stopped or exited box.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>

* build(deps): bump sysinfo to 0.40

sysinfo 0.40 makes System construction fallible (System::new returns
Result<System, sysinfo::Error>). The three callers keep their 0.39
behaviour on an unsupported platform: minvmd's metrics sample and
minimald's resource-pool probe return None, and minvmd's host capacity
probe reports zero memory.

Unblocks the dependabot cargo group (#2844), which bumped sysinfo
without migrating the callers.

---------

Co-authored-by: Karl Godard <karl@minimal.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
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.

T30 — Run a command in a box with a terminal or detached, under an exec id

1 participant

0