8000 T42 — Start every box's entrypoint from its spec through one launcher by minimal-foundry[bot] · Pull Request #2841 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
mitodrummer merged 10 commits into
mainfrom
agent/2399-start-every-box-s-entrypoint-from-its-spec-throu
Nov 2, 2026
Merged

mitodrummer merged 10 commits into
mainfrom
agent/2399-start-every-box-s-entrypoint-from-its-spec-throu

Conversation

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

T42 — Start every box's entrypoint from its spec through one launcher

slice: S2
kind: enablement
covers: none
size: L
est: 380 code, 300 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: daemon log names each launch's entrypoint
observability: one span per launch
layers:
- spec:
- crates/mfile/src/spec.rs: entrypoint, lifetime, terminal flag
test: cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag
- runtime:
- crates/minimald/src/box_runtime/launcher.rs: one launcher from the spec
- crates/minimald/src/box_runtime.rs: runs what the launcher starts
- crates/minimald/src/box_runtime/mock.rs: mock launches from spec
- crates/minimald/src/box_actor.rs: hands spec over, keeps hold
- crates/minimald/src/box_actor/host.rs: launch_host passes the spec
- crates/minimald/src/box_actor/launcher.rs: session_launcher variants take the spec
- crates/minimald/src/box_actor/tests.rs: tests
- crates/minimald/src/box_runtime/tests.rs: tests
test: cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec
test: cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold
produces: none
consumes:
- crates/mfile/src/spec.rs
proof: A session starts the same shell, launched from its spec.
depends on:
- blocked by #2388
- blocked by #23 8000 93
- blocked by #2395

Proof

A session starts the same shell as before, and the launch now reads it from the box's stored spec: the spec's entrypoint is the login shell, and LaunchSpec::entrypoint is the one decision the real and mock launchers both start from. The daemon log shows a box.launch span and the line "launching the box's entrypoint from its spec" with entrypoint=login shell for each session launch.

Notes from the agent: The spec gains an [execution] table: lifetime (until_complete or persistent) and, for an argv entrypoint, entrypoint. A shell entrypoint leaves the key out. The terminal flag is the existing io.pty_enabled. A v0 spec stored before [execution] existed reads with the entrypoint its type implies (session: shell, task: min task run ) and until_complete. BOX_SPEC_VERSION stays 0. A task's entrypoint is min task run <name>, the in-box command its exec runs today. Behaviour choice to keep tasks working: a launch that ensure_host mints for an exec (for_exec = true) starts the shell, because the TaskRun exec still runs the task itself. If the launcher started the task argv there, the task would run twice. Consequence: today no production launch starts a task's argv right after it is created. A non-interactive task box launched by an attach, or a hook launch that an attach later releases, starts /usr/bin/min task run <name> in place of bash. To make the task's launch the run itself, exec.rs and the client must change (outside these layers). Lifetime is carried and stored but nothing enforces it yet (BOX-037/BOX-038 are other tasks). Interactive tasks still get no spec, so they start the shell as before. An unreadable stored spec on a plain launch logs a warning and starts the shell. A resume still refuses such a spec as before. just clippy-strict was not run.

Tests

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-097c... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-00f8... 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 stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-b19f... 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.

Verify

min task run agent-verify passed ([tasks.verify]).

Closes #2399

Residuals

Current behaviour (supersedes the round-1 notes above)

  • The lifetime is box.lifetime: until_complete (the default for a v0 spec stored without it) or until_stopped. persistent is gone (arch C1).
  • An argv entrypoint serializes as execution.exec, and a shell entrypoint leaves the key out (arch C2).
  • run_by_exec is an interim until T43 (T43 — Run a task as a task box that returns its exit code #2405) moves a task's run onto the box's entrypoint (arch C3, listed under Residuals).
  • A launch whose stored spec can't be read, or whose store read fails, is refused with a message naming min session destroy. It doesn't log and start the shell. A box filed with no spec still starts the shell (arch C4).
  • crates/minimald/src/box_manager/tests.rs (outside the layers, accepted by the maintainer) now stores a real spec in stopped_box_record_readable_until_reaped instead of a placeholder.

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.
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.
@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: b22208e8-5e3c-4ae4-bdaa-45be9dd96c13

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

Approved: no findings at f92ca7ef68d2.

Nothing to do.

Clobber: none seen.

@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/box_actor/host.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 launcher shape conforms: one decision from the stored spec feeds both launchers. The stored spec keys do not match the design's box.toml. The design keeps lifetime in [box] with until_stopped, and names the entrypoint [execution] exec (architecture.md lines 837–842 and 1095–1096). C1 and C2 rename them before these bytes persist. A launch that cannot read the stored spec must refuse, not start the shell (C3). The exec-minted shell is an interim and needs a filed issue (C4).

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

Comment thread crates/mfile/src/spec.rs Resolved
Comment thread crates/mfile/src/spec.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
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.
@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 — 1 finding, 1 inline.

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

Comment thread crates/minimald/src/box_actor/host.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 spec still writes lifetime under [execution] as persistent. The design keeps it at [box] lifetime with until_stopped (lines 837-842, 1186). Make the change in C1. The entrypoint key is entrypoint, but the design key is [execution] exec (line 1096). Make the change in C2. The new run_by_exec: true on every launch stops a task from running twice, but it is an interim with no issue (C3). An unreadable stored spec starts the shell, so refuse the launch (C4, line 19).

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

Comment thread crates/mfile/src/spec.rs Resolved
Comment thread crates/mfile/src/spec.rs Resolved
Comment thread crates/mfile/src/spec.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
Comment thread crates/minimald/src/box_actor/host.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-c24c... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-652e... 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 stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-3044... 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.

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_actor/host.rs:808: Replaced the per-launch for_exec flag with LaunchSpec::run_by_exec, which launch_host sets on every launch (hook, attach and exec). min task run carries the task over the exec channel, so every launch of a task box now starts the shell. A gated hook launch or an attach no longer runs the task a seco
  • 👎 invalid — PRRT_kwDOPdfcxM6yiqYn: The argv command gets the same composed env_vars as the shell, and those include the connection env layered at launch. --rcfile ATTACH_ENV_BASH_RC is a bash flag: it is a prompt hook that only an interactive bash reads, so no arbitrary program can take it. The code says on purpose that SHELL/PS1 sta (not posted on the thread: not an open thread on the pull request)

A session box starts the same login shell as before. That shell now comes from the box's stored spec, through LaunchSpec::entrypoint, the one launcher decision. The daemon log has a box.launch span for each launch, and the span names the entrypoint. A task box also keeps its shell on every launch, because its run still arrives over the exec channel, so the task runs one time only.

Notes from the agent: Revise round 1. Thread 1 is valid. The per-launch for_exec flag became LaunchSpec::run_by_exec, and launch_host sets it on every launch. The reason is that min task run sends the task over the exec channel (crates/minimal/src/task.rs). The consequence is that no production launch starts an argv entrypoint today. The Argv branch in the launcher and the mock stays, and the launcher test covers it with run_by_exec false. A later task can clear run_by_exec when a task's run moves from the exec channel to the entrypoint. cargo clippy -p minimald --all-targets is clean. I did not run just ci, just clippy-strict or the full minimald suite.

@mitodrummer
Copy link
Copy Markdown
Member

/revise arch at f39db70, all in the layers.

C1: crates/mfile/src/spec.rs: rename Lifetime::Persistent to Lifetime::UntilStopped (serializes until_stopped). Move lifetime out of DocExecution into DocBox (serializes box.lifetime), with #[serde(default)] to until_complete for a v0 spec stored without it. Update the module doc (~lines 8-10), spec_carries_entrypoint_lifetime_and_terminal_flag (read value["box"]["lifetime"], expect until_stopped), and actor_hands_stored_spec_to_launcher_and_keeps_hold in crates/minimald/src/box_actor/tests.rs (Lifetime::UntilStopped).

C2: spec.rs DocExecution.entrypoint: serialize the key as exec (execution.exec). The v0 value is an argv array, not the box.toml string, and a shell entrypoint leaves the key out. Add a bullet for that to the module doc's list of v0 exceptions that #2410 ends. Stop calling execution.entrypoint a box.toml table at ~lines 8-10. Update the assertions that read value["execution"]["entrypoint"].

C3: mark run_by_exec: true in crates/minimald/src/box_actor/host.rs launch_host (~line 807) as an interim. Its flip condition: T43 #2405 moves a task's run from the exec channel to the box's entrypoint and removes the constant. Say so in the comment above that line and in the LaunchSpec::run_by_exec doc comment in crates/minimald/src/box_runtime/launcher.rs. Replace the stale note that a hook or attach launch starts /usr/bin/min task run <name>. The PR body already lists the residual.

C4: held_spec() in host.rs: when the stored spec bytes do not parse, or the store read fails, return an error that launch_host maps to AttachError, so the launch is refused (as the resume path does with stored_spec(...).map_err(unsatisfiable)?). Do not log and start the shell. Keep Ok(None) (a box filed with no spec) on the shell. Add a test that a box with unreadable stored spec bytes does not launch.

The tests failure (reap_hands_name_to_retained_holder) is a race on main that T59 #2840 fixes. It is not this PR's. Touch nothing else.

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.
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.
@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 — 1 finding, 1 inline.

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

Comment thread crates/minimald/src/box_actor/host.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 · the full ruling

This head resolves the earlier conditions. The lifetime is box.lifetime with until_complete and until_stopped, as section Box Spec lines 837-842 say. The entrypoint is execution.exec, and the argv shape is a listed v0 exception under #2410. A stored spec that the daemon cannot read now refuses the launch, and the launch does not fall back to the shell. The run_by_exec interim states its flip condition and #2405. Before T43 flips that constant, make the argv launch honour pty_enabled and remove the arguments from the box.launch span. The summary gives the design lines.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-31a7... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-4111... 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 stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-07ae... 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.

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.

 PASS [   0.021s] ( 375/1388) minimald box_manager::tests::native_create_ignores_a_client_supplied_box_id
        PASS [   0.020s] ( 376/1388) minimald box_manager::tests::needed_packages_skips_sessions_that_arent_active
        PASS [   0.460s] ( 377/1388) minimald box_manager::tests::guest_create_ignores_a_client_supplied_box_id_and_replies_when_no_door_answers
        PASS [   3.247s] ( 378/1388) minimald box_manager::tests::delete_during_resume_teardown_runs_the_destroy_hooks_once
        PASS [   0.615s] ( 379/1388) minimald box_manager::tests::list_returns_every_retained_record_with_state
        PASS [   0.228s] ( 380/1388) minimald box_manager::tests::pending_session_activates_on_approving_verdict
        PASS [   0.204s] ( 381/1388) minimald box_manager::tests::project_composable_contribution_reaches_composer
        PASS [   0.208s] ( 382/1388) minimald box_manager::tests::reading_a_composition_does_not_start_the_session
        PASS [   1.124s] ( 383/1388) minimald box_manager::tests::needed_packages_unions_across_sessions
        PASS [   1.445s] ( 384/1388) minimald box_manager::tests::no_automatic_reaper_runs
        PASS [   0.527s] ( 385/1388) minimald box_manager::tests::reap_resuming_box_refuses_and_forced_waits
        PASS [   0.031s] ( 386/1388) minimald box_manager::tests::record_carries_the_root_certificate_to_the_launch
        PASS [   0.247s] ( 387/1388) minimald box_manager::tests::reap_running_box_unforced_refuses
        FAIL [   1.147s] ( 388/1388) 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; 1272 filtered out; finished in 1.13s

  stderr ───

    thread 'box_manager::tests::reap_hands_name_to_retained_holder' (2815) panicked at crates/minimald/src/box_manager/tests.rs:4738:5:
    assertion `left == right` failed
      left: Some(BoxId(01a19ad7-3fad-7ec1-be32-ba8cc17f1450))
     right: Some(BoxId(01a19ad7-3eb3-718f-be3f-718330c64fd2))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   1.424s] ( 389/1388) minimald box_manager::tests::prune_selectors_reap_and_print
        PASS [   0.398s] ( 390/1388) minimald box_manager::tests::rename_of_unroutable_session_leaves_other_routes_alone
        PASS [   0.412s] ( 391/1388) minimald box_manager::tests::restart_keeps_running_records_and_reaps_pending_ones
────────────
     Summary [  90.978s] 391/1388 tests run: 390 passed, 1 failed, 24 skipped
        FAIL [   1.147s] ( 388/1388) minimald box_manager::tests::reap_hands_name_to_retained_holder
warning: 997/1388 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 — crates/mfile/src/spec.rs: Renamed Lifetime::Persistent to UntilStopped and moved lifetime from DocExecution to DocBox (box.lifetime).
  • 👍 valid — crates/mfile/src/spec.rs:289: Serialized the entrypoint as execution.exec, kept the argv array, and listed it as a v0 exception that T16 — Fold the layers into one expanded spec, the same bytes every run #2410 ends in the module doc.
  • 👍 valid — crates/minimald/src/box_actor/host.rs: held_spec returns Err for unreadable bytes and a failed store read. launch_host refuses with AttachError::SpawnFailed, and only Ok(None) starts the shell.
  • 👍 valid — crates/minimald/src/box_runtime/launcher.rs: The run_by_exec doc on LaunchSpec and the comment in launch_host name the interim and its flip condition, T43 T43 — Run a task as a task box that returns its exit code #2405. LaunchSpec::for_exec no longer exists.
  • 👍 valid — crates/minimald/src/box_actor/host.rs:820: Kept the constant, and named the interim in the comment and the run_by_exec doc: every launch starts the shell until T43 T43 — Run a task as a task box that returns its exit code #2405 removes the constant.
  • 👍 valid — crates/mfile/src/spec.rs: Renamed the variant to Lifetime::UntilStopped, which serializes as until_stopped.
  • 👍 valid — crates/mfile/src/spec.rs: Moved lifetime to DocBox with #[serde(default)] to until_complete for a v0 spec stored without it. The test checks the default.
  • 👍 valid — crates/mfile/src/spec.rs: The key serializes as exec. The module doc lists the argv shape and the omitted shell key as a v0 exception that T16 — Fold the layers into one expanded spec, the same bytes every run #2410 ends.
  • 👍 valid — crates/minimald/src/box_actor/host.rs:819: The comment above run_by_exec and the LaunchSpec::run_by_exec doc name T43 T43 — Run a task as a task box that returns its exit code #2405 as the issue that removes the constant.
  • 👍 valid — crates/minimald/src/box_actor/host.rs: An unparsable stored spec now returns Err from held_spec and refuses the launch. New test launch_refuses_unreadable_stored_spec covers it.
  • 👍 valid — crates/minimald/src/box_actor/host.rs: A failed store read now returns Err, which launch_host maps to AttachError::SpawnFailed. The error is not held, so the next launch reads again.

A session starts the same shell, launched from its spec. The spec now writes box.lifetime (until_complete or until_stopped) and execution.exec. A box whose stored spec cannot be read is refused at launch, with a message that names min session destroy <name>, and does not start a shell.

Notes from the agent: Round 2 addresses C1 to C4 from the note. C1: Lifetime::Persistent is now Lifetime::UntilStopped, and lifetime moved to DocBox as box.lifetime with a serde default of until_complete. C2: the entrypoint key is execution.exec, and the module doc lists the argv shape and the omitted shell key as a v0 exception that #2410 ends. C3: the comment above run_by_exec in launch_host and the LaunchSpec::run_by_exec doc name the interim and its flip condition, T43 (#2405). The old comment about an exec that carries the run is replaced. I found no /usr/bin/min task run <name> note in the layer files, so the stale note is in the PR body only. C4: held_spec returns io::Result. An unreadable spec or a failed store read refuses the launch with AttachError::SpawnFailed, and the error is not held. Ok(None) still starts the shell. The refusal text reuses the resume path's remedy: Destroy it with min session destroy <name>, then start a new one with min session activate. SpawnFailed displays with the existing session spawn: prefix from attach.rs, which is outside the layers. I did not run the full minimald suite, just ci or clippy-strict. The known reap_hands_name_to_retained_holder race is untouched.

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.
@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 · the full ruling

This head changes one test fixture since the PASS on 3b5a63f. The launch now refuses an unreadable stored spec, so the test stores a real BoxSpec. The test still proves that a stopped box keeps its record and spec until the reap (architecture.md line 21). The change conforms. Also, correct the PR body: it still says that an unreadable spec starts the shell.

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

@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 — 1 finding, 1 inline.

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

Comment thread crates/mfile/src/spec.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a failed check suite, pushed as a532982d234d.

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-76bf... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-2750... 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 stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-61b6... 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.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/box_manager/tests.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👎 invalid — PRRT_kwDOPdfcxM6yjHxq: The refusal is deliberate. Review threads C3 and C4 (PRRT_kwDOPdfcxM6yiq3T, PRRT_kwDOPdfcxM6yi08P, PRRT_kwDOPdfcxM6yi08U) asked for every launch to refuse a stored spec that cannot be read or a failed store read, and only Ok(None) to start the shell. A spec that does not parse cannot specify the box (not posted on the thread: not an open thread on the pull request)

A session starts the same shell, launched from its spec. A box filed with no spec also starts the shell. A box whose stored spec cannot be read is refused with a message that names min session destroy <name>.

Notes from the agent: The failed tests job came from box_manager::tests::stopped_box_record_readable_until_reaped. That test stored the placeholder bytes {"type":"session"} as the box spec. Round 2 made a launch refuse a stored spec that does not parse, so the test's ensure_host failed. The fix stores a real session spec, serialized with serde_json_lenient. That file, crates/minimald/src/box_manager/tests.rs, is outside the task's layers. I changed it because it is the only way to make the crate's suite pass with the refusal that the review asked for. ci-linux-native-success is the aggregator, and it failed because tests failed. I ran the full minimald suite once before the fix. After the fix, I ran only the failing test, the three test: lines and clippy. I did not run the other workspace crates' suites. Their code does not read the spec keys that this pull request renamed. The pull request body is stale: it says that a plain launch with an unreadable spec logs a warning and starts the shell. The runner writes that body, and I cannot edit it. The code refuses the launch, as review threads C3 and C4 asked.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 2, 2026 06:37
@mitodrummer
Copy link
Copy Markdown
Member

/revise The maintainer accepts one test file outside the layers: crates/minimald/src/box_manager/tests.rs.

CI tests fails minimald box_manager::tests::stopped_box_record_readable_until_reaped (tests.rs ~2997-3008). The test stores the fake spec {"type":"session"}. Under C4 the launch now correctly refuses a stored spec it cannot parse ("unknown field type"). Change that fixture to store a real serialized v0 spec (for example mfile::spec::BoxSpec::for_session(...) serialized the way a create writes it), so the box launches. Grep the same file for any other test that stores a hand-written fake spec through store_spec, and fix it the same way. Do not weaken C4 in production code. Touch nothing else.

@mitodrummer
mitodrummer marked this pull request as ready for review November 2, 2026 14:10
A box ends when its entrypoint returns, whatever box.lifetime says.
@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 · the full ruling

This push adds two doc comments in crates/mfile/src/spec.rs and changes no code. The comments say that no launch reads box.lifetime yet. That is true at this head. The a532982d ruling stands. The run_by_exec interim stays filed as #2405.

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

@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 — 1 finding, 1 inline.

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

Comment thread crates/minimald/src/box_runtime.rs Resolved
@mitodrummer
Copy link
Copy Markdown
Member

/revise One item, minimal-review PRRT_kwDOPdfcxM6ytdHa: the box.launch span and its info event in crates/minimald/src/box_runtime.rs (~line 1217) print entrypoint, whose Display joins the program and every argument. Once T43 starts argv entrypoints, any secret in a task argument would land in the daemon log at info level. Log only the kind (shell, or the program name without its arguments) and the argument count. Keep the full argv out of every span and event. Add a test that a launch with an argv entrypoint carrying a secret-looking argument does not log that argument. Touch nothing else. (The cargo-deny failure is RUSTSEC-2026-0433, fixed by #2846, not this PR.)

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2399. 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 4 of 10 on a /revise note, pushed as 4350b29b3bad.

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-2e39... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-725f... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/ 8 crates/a 10BC8 rgs) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-82ae... 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.

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/box_manager/tests.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — PRRT_kwDOPdfcxM6yjgNS: Only a test reads lifetime. The Lifetime and BoxSpec::lifetime doc comments now say no launch reads it yet, so UntilStopped has no effect today. (not posted on the thread: not an open thread on the pull request)

A session starts the same shell, launched from its spec. A launch whose stored spec cannot be read is refused, and the stopped-box record test now stores a real session spec and passes.

Notes from the agent: The fixture fix that the note asks for was already at the branch head in a532982. It stores BoxSpec::for_session serialized with serde_json_lenient::to_vec, the same way a create writes it. No other test in box_manager/tests.rs calls store_spec. That test now passes. No production code changed for C4. This round adds only a doc comment in crates/mfile/src/spec.rs, a file in the layers, to answer the open Lifetime thread.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 2, 2026 14:55
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.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as b4b1f7592ebe.

line exit tail
cargo nextest run -p mfile spec_carries_entrypoint_lifetime_and_terminal_flag 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-9c40... 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 client-config v0.0.1 (/wo
cargo nextest run -p minimald launcher_starts_session_and_task_entrypoints_from_spec 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-d08a... 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 stdlib v0.0.22 (/workbenc
cargo nextest run -p minimald actor_hands_stored_spec_to_launcher_and_keeps_hold 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-85a5... 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.

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.027s] ( 372/1389) minimald box_manager::tests::in_box_root_cannot_append_events
        PASS [   0.262s] ( 373/1389) minimald box_manager::tests::idle_session_does_not_block_unforced_shutdown
        PASS [   0.031s] ( 374/1389) minimald box_manager::tests::list_scoped_to_project_reads_project_root
        PASS [   0.028s] ( 375/1389) minimald box_manager::tests::native_create_ignores_a_client_supplied_box_id
        PASS [   0.463s] ( 376/1389) minimald box_manager::tests::guest_create_ignores_a_client_supplied_box_id_and_replies_when_no_door_answers
        PASS [   0.028s] ( 377/1389) minimald box_manager::tests::needed_packages_skips_sessions_that_arent_active
        PASS [   3.292s] ( 378/1389) minimald box_manager::tests::delete_during_resume_teardown_runs_the_destroy_hooks_once
        PASS [   0.312s] ( 379/1389) minimald box_manager::tests::pending_session_activates_on_approving_verdict
        PASS [   0.829s] ( 380/1389) minimald box_manager::tests::list_returns_every_retained_record_with_state
        PASS [   0.278s] ( 381/1389) minimald box_manager::tests::project_composable_contribution_reaches_composer
        PASS [   0.270s] ( 382/1389) minimald box_manager::tests::reading_a_composition_does_not_start_the_session
        PASS [   1.411s] ( 383/1389) minimald box_manager::tests::needed_packages_unions_across_sessions
        PASS [   1.603s] ( 384/1389) minimald box_manager::tests::no_automatic_reaper_runs
        FAIL [   0.827s] ( 385/1389) 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; 1273 filtered out; finished in 0.81s

  stderr ───

    thread 'box_manager::tests::reap_hands_name_to_retained_holder' (2861) panicked at crates/minimald/src/box_manager/tests.rs:4725:5:
    assertion `left == right` failed
      left: Some(BoxId(01a19cd2-9f39-71be-891e-d4497faa973f))
     right: Some(BoxId(01a19cd2-9d23-7814-9262-5d74b555d82b))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   0.258s] ( 386/1389) minimald box_manager::tests::reap_running_box_unforced_refuses
        PASS [   0.562s] ( 387/1389) minimald box_manager::tests::reap_resuming_box_refuses_and_forced_waits
        PASS [   1.655s] ( 388/1389) minimald box_manager::tests::prune_selectors_reap_and_print
────────────
     Summary [  95.069s] 388/1389 tests run: 387 passed, 1 failed, 24 skipped
        FAIL [   0.827s] ( 385/1389) minimald box_manager::tests::reap_hands_name_to_retained_holder
warning: 1001/1389 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

Outside the task's layers::

  • crates/minimald/src/box_manager/tests.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/box_runtime.rs:1232: Removed Display on LaunchEntrypoint. The box.launch span and its info event now log only the program (or login shell) and args=<count>. A new test checks that a secret-looking argument does not reach the log.

A session starts the same shell, launched from its spec. The daemon log names each launch by its program (or the login shell) and its argument count, and never prints an argument.

Notes from the agent: Revise round 5: the Display impl on LaunchEntrypoint is gone, so no format string can print the argv by accident. LaunchEntrypoint::log_name and arg_count replace it. The span and the info event moved into a new box_runtime::launch_span helper, so the test can capture them directly. The new test name extends the T42 test-line filter, so the existing test line runs it. cargo clippy -p minimald --all-targets is clean. The cargo-deny failure (RUSTSEC-2026-0433) is out of scope, as the note says.

@mitodrummer
mitodrummer marked this pull request as ready for review November 2, 2026 15:25
@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 · the full ruling

The new commit keeps entrypoint arguments out of the launch log. The log names the program and the argument count only. The design says that secret values are never written into logs (architecture.md line 79). The earlier PASS stands for the rest of the change. The run_by_exec interim stays a residual in issue #2405.

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

@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: 1 unresolved thread — minimal-foundry[bot] crates/minimald/src/box_runtime.rs:1232. No findings.

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

@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 — approved

Verdict APPROVE — every review thread is resolved; @mitodrummer closed the last one. The review of b4b1f7592ebe 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 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 b4b1f75. Specs already on disk still read: an absent execution is the type's implied entrypoint and an absent box.lifetime is until_complete. Every launch goes through launch_host with the held spec, and a shell box starts exactly as before. The only new refusal is an unreadable stored spec (arch C4). The launch log carries only the program and the argument count. Follow-ups are in the walk ledger: serde error text in the spec-read warning can echo a value, and create should validate the spec as strictly as launch does.

@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 · the full ruling

This head merges main into the branch and changes none of the branch's own content. The merge brings in #2842 and #2846. They do not touch the spec or the launch path. The ruling on b4b1f759 stands. The run_by_exec interim stays a residual on #2405.

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

@mitodrummer
mitodrummer added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit ec2f3ec Nov 2, 2026
36 checks passed
@mitodrummer
mitodrummer deleted the agent/2399-start-every-box-s-entrypoint-from-its-spec-throu branch November 2, 2026 20:58
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 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

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T42 — Start every box's entrypoint from its spec through one launcher

1 participant

0