8000 T10 — Log a box's output from its start, out of the box's reach by minimal-foundry[bot] · Pull Request #2852 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
mitodrummer merged 13 commits into
mainfrom
agent/2402-log-a-box-s-output-from-its-start-out-of-the-box
Nov 3, 2026
Merged

mitodrummer merged 13 commits into
mainfrom
agent/2402-log-a-box-s-output-from-its-start-out-of-the-box

Conversation

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

T10 — Log a box's output from its start, out of the box's reach

slice: S2
kind: task
covers: BOX-016, BOX-155, BOX-164
size: L
est: 380 code, 300 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: daemon log names each refused write
observability: a detached event per client loss
layers:
- runtime:
- crates/minimald/src/box_runtime/launcher.rs: output logged from start
- crates/minimald/src/box_runtime.rs: client is a second reader
- crates/minimald/src/box_runtime/binding.rs: detached event
- crates/minimald/src/box_runtime/tests.rs: tests
test: cargo nextest run -p minimald pty_client_loss_records_detach
test: cargo nextest run -p minimald box_output_logged_from_start_complete_after_disconnect
- record:
- crates/boxes/src/store.rs: log beside record
- crates/minimald/src/rpc.rs: log read handler
test: cargo nextest run -p minimald in_box_root_cannot_write_record_or_log
- wire:
- crates/minimald-rpc/src/lib.rs: log read request
- crates/minimald-rpc/src/tests.rs: round-trip
test: cargo nextest run -p minimald-rpc read_log_request_round_trips
produces: none
consumes: none
proof: A box's output reads back whole after the client drops.
depends on:
- blocked by #2399

Proof

A box's entrypoint output goes to output.log beside its record from the start of each launch, with or without an attached client. A client that drops leaves a detached event, and the box runs on. The ReadLog subsystem (minimald-v1-ReadLog) then returns the whole log by box id, including output from before, during and after the attach. box_output_logged_from_start_complete_after_disconnect shows this.

Notes from the agent: Design: the store gets LOG_FILE (output.log) and BoxDir, which is the box's directory as code outside the store sees it. The host finds it from the workspace path (

/tree), and the rpc handler finds it by id through sessions/index.json, so box_manager.rs (not in layers) is unchanged. The host opens the log with O_NOFOLLOW and mode 0600 before the launch, and appends each PTY read before it sends the bytes to the binding. The binding now ends with a new ClientLost reason when its channel closes or its connection drops. Before this change, it stayed alive and forwarded to a dead channel. A ClientLost, a detach chord or a shed records a detached event through BoxDir::append_event. A ClientLost does not run on_detach hooks, for the same reason as a shed: nobody chose to leave. Diagnostics: a refused open of the log (for example a planted symlink) and a failed write each log a warn line with the box id, the log path and the error. After a failed write, the log stops. Limits: (1) BoxDir::append_event does not wake EVENTS_WRITTEN (box_manager, out of scope), so a follower of ReadEvents sees a detached event on its next 200 ms poll and not at once. (2) A followed ReadLog polls the file every 100 ms, because the host has no wake-up to send. (3) No min command reads the log yet. Only the wire subsystem exists. (4) The log appends across restarts of the same box and is capped at 8 MiB (boxes::store::LOG_CAP): past the cap it keeps a LOG_DROPPED line and the newest half, so a box that writes more than 8 MiB loses the start of its log. That departs from architecture.md line 473 until https://github.com/gominimal/arch/issues/159 decides the bound. (5) The in-box-root test is a placement test. It writes as the test user into the box's visible directories, and does not run uid 0 in a real sandbox. The e…[truncated]

Tests

7438
line exit tail
cargo nextest run -p minimald pty_client_loss_records_detach 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-7e17... 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 box_output_logged_from_start_complete_after_disconnect 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-0009... 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 in_box_root_cannot_write_record_or_log 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-67f7... 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 read_log_request_round_trips 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-5c36... 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 ([tasks.verify]).

Base

The base branch moved while the coder worked and was merged in before the tests ran. These files changed on BOTH sides since the branch point — read them first, because a rewrite from a stale reading can undo a merge without a conflict:

  • crates/boxes/src/store.rs

Closes #2402

Residuals

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.
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.
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.
…s-output-from-its-start-out-of-the-box

# Conflicts:
#	crates/boxes/src/store.rs
@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: 690d5139-ab82-4a23-8b40-7b3949e17f2b

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

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

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

Reviewed: 1 finding, none blocking at 3196def4fcbc.

Address the comments on the diff.

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

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

Comment thread crates/minimald/src/rpc.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 · 2 conditions · the full ruling

The capture, the detach event and the placement conform to the design. Two paths fail open. A refused open of the log lets the box run with no log, and ReadLog then answers Done on an empty log (line 473). Make Host::build refuse the launch, as C1 says. BoxDir::read_log_from follows a symlink that the writer refuses, so a planted link becomes the box's captured output (line 21). Open the log with O_NOFOLLOW, as C2 says. A write failure in a running box needs a design decision, so that question is an escalation.

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

Comment thread crates/minimald/src/box_runtime.rs Resolved
Comment thread crates/boxes/src/store.rs Resolved
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.
@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).

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

The log path, the detached event and the placement outside the box conform to lines 21, 473 and 499. One gap remains. When the output log cannot be opened, BoxOutputLog::open logs a warning and the box starts with no log. A read then returns Done on an empty log, so the log looks complete when it is not. Line 473 says the log is complete from the box's start. Apply C1: Host::build refuses the launch when a store box's log cannot be opened.

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

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

/revise Four items at fa36e24, all in the layers.

  1. arch C1 (log fails open): BoxOutputLog::open in crates/minimald/src/box_runtime/launcher.rs returns io::Result<BoxOutputLog>. When BoxDir::for_tree(&paths.working) is Some and open_path fails, it returns Err. Keep the existing warn! (box_id, log_path, error) but drop "the box runs with no log". A workspace outside the store (for_tree is None) still returns Ok with no file. Host::build in crates/minimald/src/box_runtime.rs propagates that error with ? before launch, so a store box whose log cannot be opened does not start. Update in_box_root_cannot_write_record_or_log in crates/minimald/src/rpc.rs: assert that BoxOutputLog::open(linked, &linked_paths) is Err and that the symlink target stays empty. Keep the daemon-log assertion.

  2. arch C2 (unless already fixed at this head, in which case say so): the log reader in crates/boxes/src/store.rs (~532) opens with File::open, which follows a symlink the writer refuses with O_NOFOLLOW. Open with O_NOFOLLOW and refuse a file that is not regular, as open_path does.

  3. minimal-review PRRT_kwDOPdfcxM6y5fLV (medium, security): BoxOutputLog::write has no size cap, so a box can fill the daemon's state disk outside its own limits. Cap the log the way the exec transcripts are capped on main (TRANSCRIPT_CAP, 8 MiB, in crates/minimald/src/exec/bridge.rs): past the cap keep a drop mark and the newest half, keep the box running, and keep reads consistent (an offset-based ReadLog must not return torn or duplicated bytes across a rotation; document what a reader sees). Use a documented constant. Add a test that a box writing past the cap leaves a log no larger than the cap holding the newest output.

  4. minimal-review PRRT_kwDOPdfcxM6y5fLb (low): in crates/minimald/src/box_runtime/binding.rs Binding::run (~862), a lost client records a detached event but skips control.detached(), so on_detach hooks never run. Run the detach hooks for ClientLost as for Detach, so the event and the hooks agree. Add a test.

Touch nothing else.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2402. 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 1 of 10 on the runtime's review findings, pushed as fa36e2400954.

line exit tail
cargo nextest run -p minimald pty_client_loss_records_detach 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0b4a... 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 box_output_logged_from_start_complete_after_disconnect 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-8b45... 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 in_box_root_cannot_write_record_or_log 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-baa1... 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 read_log_request_round_trips 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-0bdf... 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 — PRRT_kwDOPdfcxM6y5Rp3: read_log_stream now runs BoxDir::find, read_log_from and is_retained through spawn_blocking. The PTY log append in launcher.rs stays synchronous: it must land before the client gets the bytes. (not posted on the thread: not an open thread on the pull request)

A box's output reads back whole after the client drops. The daemon's log read now does its file reads on the blocking pool, so a follower does not hold a runtime worker.

Notes from the agent: Revise round 1. The read path in read_log_stream now calls BoxDir::find, read_log_from and is_retained through crate::traced::spawn_blocking. The PTY write path in launcher.rs (BoxOutputLog::write) stays synchronous on purpose. The host must append each chunk to the log before it sends the same bytes to the client, so a client never sees output the log lacks. That write is a small page-cache append. A move to the blocking pool adds a task hop per PTY read and does not change this order. The clippy-strict gate was not run.

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

The compare makes the log open fail closed, refuses a linked log on read, and records a lost client as detached. These conform to architecture.md lines 21 and 499. The new 8 MiB LOG_CAP drops the start of a large log, and line 473 says the log is complete from the box's start. The design gives no bound, so the question goes to an arch issue. Keep the cap. Do C1: remove the BOX-155 tag from the cap, and update limit (4) in the PR body to state the cap and link the arch issue.

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

Comment thread crates/boxes/src/store.rs Resolved
Comment thread crates/boxes/src/store.rs Resolved
Comment thread crates/minimald-rpc/src/lib.rs Resolved
@mitodrummer
Copy link
Copy Markdown
Member

/revise arch C1 at 2e1000b: document the log cap as a bound the design has not stated yet. Do not change the cap or its code. The arch issue that decides the bound is https://github.com/gominimal/arch/issues/159.

  1. crates/boxes/src/store.rs: in the doc comment of LogWriter (~596-597), remove the (BOX-155) tag from "never more than [LOG_CAP] bytes at rest". Say that LOG_CAP drops the start of the output past 8 MiB, that this departs from architecture.md line 473 ("complete from the box's start"), and that https://github.com/gominimal/arch/issues/159 decides the bound. Do the same in the doc comment of LOG_CAP (~544-548), and of READ_LOG_SUBSYSTEM in crates/minimald-rpc/src/lib.rs (~3666).
  2. In the PR body, replace limit (4) ("has no size limit") with the 8 MiB drop-oldest cap and its effect: a box that writes more than 8 MiB loses the start of its log. Add a Residuals line linking https://github.com/gominimal/arch/issues/159.

Touch nothing else.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. A coder is already running on #2402. 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 2e1000ba8607.

line exit tail
cargo nextest run -p minimald pty_client_loss_records_detach 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-ef8d... 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 box_output_logged_from_start_complete_after_disconnect 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-fb8b... 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 in_box_root_cannot_write_record_or_log 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-fd84... 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 read_log_request_round_trips 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3768... 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/box_runtime.rs: Host::build now propagates BoxOutputLog::open's error with ? before launch, so a store box with a refused log does not start.
  • 👍 valid — crates/minimald/src/box_runtime/launcher.rs: BoxOutputLog::open returns io::Result. It returns Err on the refused open, and the warn line no longer says the box runs with no log.
  • 👍 valid — crates/boxes/src/store.rs: The store reader lstats the path, refuses a non-regular file, and refuses an open file whose dev/inode differs (boxes has no libc for O_NOFOLLOW). A test covers a link and a directory.
  • 👍 valid — PRRT_kwDOPdfcxM6y5fLV: Log capped at boxes::store::LOG_CAP (8 MiB). Past it the file keeps the dropped-output line and the newest half. ReadLog uses one LogReader per stream, so a drop sends no byte twice. Tests cover both. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — crates/minimald/src/box_runtime/binding.rs: ClientLost now runs control.detached() as Detach does. pty_client_loss_records_detach_and_runs_detach_hooks fails without the change and passes with it.
  • 👍 valid — crates/minimald/src/box_runtime/launcher.rs:391: The refused-open arm now returns Err(e) for a store box, and the warn! line stays.
  • 👍 valid — crates/minimald/src/box_runtime.rs: Host::build calls BoxOutputLog::open(...)? before launch, so ReadLog never reports an unopened log as complete.
  • 👍 valid — crates/minimald/src/rpc.rs: The test asserts BoxOutputLog::open(linked, ..) is Err and the symlink target stays empty, and keeps the daemon-log assertion.

A box's output reads back whole after its client drops. A box in the store whose log cannot be opened does not start. Past 8 MiB the log keeps a dropped-output line and the newest output, and a ReadLog stream sends no byte twice across a drop. A lost client now runs the on_detach hooks as well as recording the detached event.

Notes from the agent: Revise round 2. Item 1 (C1): BoxOutputLog::open returns io::Result. A store box whose log open fails gets Err, the warn line stays without 'the box runs with no log', and Host::build propagates it with ? before launch. in_box_root_cannot_write_record_or_log asserts the open is Err, the symlink target stays empty, and the daemon log names the path. Item 2 (C2): the boxes crate has no libc dependency and its Cargo.toml is outside the layers, so the reader cannot pass O_NOFOLLOW. It lstats the path, refuses anything that is not a regular file, opens it, and refuses the file if its device and inode differ from the lstat (a link or a swap). Residual: a swap to a FIFO in the window between the lstat and the open can block that one read. Only the daemon's user can write the box directory. Item 3: boxes::store gets LOG_CAP (8 MiB), LOG_DROPPED (the same text as the exec transcript mark), LogWriter and LogReader. The writer appends, and past the cap it renames a new file (mark + newest bytes, half the cap) over output.log. ReadLog keeps one LogReader for the whole stream. On a replacement it reads the old file to its end. If the new file carries the bytes the old one ended with, it goes on after them. Otherwise it reads the new file from byte 0, so the mark shows the gap. Documented on READ_LOG_SUBSYSTEM and ReadLogRequest.offset. Known limits: an Output offset can go backward after a drop. A fresh request that resumes from an offset taken before a drop does not name the same byte (documented). A reader that missed two or more drops of identical repeated output can match by content and skip the mark. The cap test drive…[truncated]

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.
@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: 12 unresolved threads — minimal-arch[bot] crates/minimald/src/box_runtime.rs, minimal-arch[bot] crates/minimald/src/box_runtime/launcher.rs, minimal-arch[bot] crates/boxes/src/store.rs, minimal-foundry[bot] crates/minimald/src/box_runtime/binding.rs, minimal-arch[bot] crates/minimald/src/box_runtime/launcher.rs:391, …. 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: CONDITIONS · 1 condition · the full ruling

The log of a box keeps at most 8 MiB and drops its oldest bytes. Line 473 of architecture.md says that the log is complete from the box's start. You link gominimal/arch#159 for the bound, but only in code comments. The spec decision at line 572 still says that the log is complete, and the PR body still says "no size limit". Make the change in C1 to record the cap as an interim with its flip condition. The detached event (BOX-016) and the placement of the log (BOX-164) conform.

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

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 7d7313c6b1ea.

line exit tail
cargo nextest run -p minimald pty_client_loss_records_detach 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-b802... 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 box_output_logged_from_start_complete_after_disconnect 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-0463... 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 in_box_root_cannot_write_record_or_log 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-cafb... 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 read_log_request_round_trips 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-aadb... 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:

  • 👎 invalid — crates/minimald/src/box_runtime.rs:1891: The write is synchronous on the host loop, as the finding says. The stall is an accepted tradeoff for now: a normal write is a local append no larger than one PTY read. The copy of about 4 MiB happens once per 4 MiB of output. This round's /revise note limits changes to doc comments, so the write st
  • 👎 invalid — crates/minimald/src/box_runtime/binding.rs:724: The race exists: the select is not biased, so a Close that arrives with the exit message can win. Then the box gets a detached event and its on_detach hooks, and the shell-exit prompt does not run for a client that is already gone. This round's /revise note forbids changes outside the doc comments,
  • 👍 valid — crates/boxes/src/store.rs: Removed the (BOX-155) tag from the LogWriter doc. It now says LOG_CAP drops the start past 8 MiB, departs from architecture.md line 473, and links gominimal/arch#159.
  • 👍 valid — crates/boxes/src/store.rs:553: The LOG_CAP doc now says a box that writes more than 8 MiB loses the start of its log, and that gominimal/arch#159 decides the bound. The cap is unchanged.
  • 👍 valid — crates/minimald-rpc/src/lib.rs:3666: The READ_LOG_SUBSYSTEM doc now says the design does not state the cap yet, and that gominimal/arch#159 decides the bound.

A box's output reads back whole after the client drops, up to the 8 MiB cap. The doc comments of LOG_CAP, LogWriter and READ_LOG_SUBSYSTEM now say that a box that writes more than 8 MiB loses the start of its log, that this departs from architecture.md line 473, and that gominimal/arch#159 decides the bound.

Notes from the agent: Revise round 3 changes doc comments only, as the /revise note asks. The cap and its code are unchanged. Item 2 of the note asks for a PR body edit: replace limit (4) with the 8 MiB drop-oldest cap (a box that writes more than 8 MiB loses the start of its log) and add a Residuals line that links https://github.com/gominimal/arch/issues/159. I cannot edit the PR body, so the runner must carry it. The two minimal-foundry threads are judged against the code but get no change this round, because the note says to touch nothing else. Both are candidates for a follow-up.

@mitodrummer
Copy link
Copy Markdown
Member

/revise arch C1 at 7d7313c. The maintainer accepts one file outside the layers for it: docs/specs/29-spec-box-local-first/29-spec-box-local-first.md. In the decision "A box's output is logged from its start." (~line 572), add one sentence for each fact:

  • until https://github.com/gominimal/arch/issues/159 decides a bound, the daemon keeps at most 8 MiB of a box's log (boxes::store::LOG_CAP);
  • past the cap, the log keeps the LOG_DROPPED line and the newest half;
  • the flip is the arch#159 decision: remove LOG_CAP if the design keeps the log complete, or change BOX-155 to the bound the design states.
    Do not change BOX-155 itself or anything else in that file. The PR body already records the cap and the residual. Touch nothing else.

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.
@github-actions
github-actions Bot commented Nov 3, 2026
Copy link
Copy Markdown
Contributor

Spec lint

docs/specs/29-spec-box-local-first/29-spec-box-local-first.md — 40 finding(s)

docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:98: P004 BOX-007 names test 'self_as_name_is_exit3', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:142: P004 BOX-015 names test 'daemon_never_stops_for_idle_or_client_loss', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:190: P004 BOX-028 names test 'hooks_on_resume_reruns_on_activate', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:201: P004 BOX-037 names test 'until_complete_timeout_terms_then_kills_box_and_execs_reason_timeout', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:205: P004 BOX-038 names test 'until_complete_ends_on_entrypoint_return_without_client', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:250: P004 BOX-046 names test 'type_noun_tables_are_boxes_sugar', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:257: P004 BOX-048 names test 'defaults_merge_lists_union_scalars_replace', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:262: P004 BOX-049 names test 'flat_per_entry_keys_and_legacy_keys_keep_meaning', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:266: P004 BOX-050 names test 'legacy_session_table_reads_as_defaults_session_with_hint', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:273: P004 BOX-165 names test 'no_session_entry_expands_implicit_default_session', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:280: P004 BOX-051 names test 'legacy_defaults_task_keys_with_hint', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:284: P004 BOX-052 names test 'legacy_params_reads_as_args_with_hint', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:288: P004 BOX-054 names test 'interactive_task_exit3_names_session', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:292: P004 BOX-059 names test 'box_type_resolves_to_builtin_root', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:301: P004 BOX-061 names test 'expanded_spec_violating_constraint_exit3_names_type_and_layer', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:306: P004 BOX-156 names test 'derived_type_loosening_constraint_exit3_names_type_key_parent', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:311: P004 BOX-160 names test 'admission_refuses_spec_breaching_builtin_root_constraint_exit3', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:315: P004 BOX-062 names test 'constraint_never_adds_package_port_or_egress', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:320: P004 BOX-063 names test 'session_and_task_entries_expand_and_validate', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:327: P004 BOX-064 names test 'expansion_order_and_merge_rules', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:332: P004 BOX-065 names test 'expansion_is_byte_identical_across_runs', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:337: P004 BOX-066 names test 'invalid_key_exit3_names_key_and_layer', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:341: P004 BOX-161 names test 'expansion_reports_layer_and_type_per_value_outside_spec_bytes', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:352: P004 BOX-071 names test 'projection_is_the_6_3_2_field_set', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:366: P004 BOX-072 names test 'projection_digest_mismatch_refuses_create', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:371: P004 BOX-073 names test 'unenrolled_evaluates_no_policy', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:375: P004 BOX-074 names test 'get_version_reports_expander_version', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:382: P004 BOX-145 names test 'example_expansion_matches_golden_vectors', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:386: P004 BOX-076 names test 'sections_follow_box_toml_with_nested_egress', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:408: P004 BOX-079 names test 'network_mode_legacy_spelling_hint_and_canonical_value', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:412: P004 BOX-157 names test 'flat_egress_keys_hint_and_canonical_nested_keys', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:416: P004 BOX-080 names test 'network_bep_secrets_passed_unchanged', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:423: P004 BOX-081 names test 'broker_secret_unenrolled_exit5', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:427: P004 BOX-085 names test 'params_accepted_and_rendered', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:467: P004 BOX-151 names test 'stop_exec_ends_process_group', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:472: P004 BOX-152 names test 'exec_events_carry_principal_argv_pty_code', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:477: P004 BOX-153 names test 'exec_disabled_refuses_exit5', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:484: P004 BOX-140 names test 'unenrolled_operations_over_file_mode_trust', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:488: P004 BOX-141 names test 'store_secret_passed_through_unenrolled', which was not found in the repository
docs/specs/29-spec-box-local-first/29-spec-box-local-first.md:492: P004 BOX-142 names test 'enrolled_and_unenrolled_expansion_identical', which was not found in the repository

Advisory. None of this blocks the merge; the review decides.

Advisory, and never a merge gate. Linter: gominimal/foundry@fb5932b8, spec format: How Minimal Writes Specs.

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

The code is the same as on the last head. BOX-016 and BOX-164 conform. The 8 MiB cap on the box log still departs from architecture.md line 473, which says the log is complete from the box's start. The spec now states that cap as an interim, with a flip and gominimal/arch#159. That makes the interim honest, so the change can merge with C1 open. C1 is the flip that the arch#159 decision triggers.

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

Comment thread crates/boxes/src/store.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 — 1 finding, 1 inline.

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

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

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

line exit tail
cargo nextest run -p minimald pty_client_loss_records_detach 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-81b3... 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 box_output_logged_from_start_complete_after_disconnect 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-928c... 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 in_box_root_cannot_write_record_or_log 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-b7b5... 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 read_log_request_round_trips 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-e14f... 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.

Outside the task's layers::

  • docs/specs/29-spec-box-local-first/29-spec-box-local-first.md

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

Review threads the coder judged:

  • 👍 valid — crates/boxes/src/store.rs:553: Added the 8 MiB LOG_CAP, the LOG_DROPPED plus newest-half behaviour, and the arch#159 flip condition to the spec decision at line 572.

A box's output reads back whole after the client drops, up to the 8 MiB cap. The BOX spec decision now records that cap, what the log keeps past it, and the arch#159 decision that removes or changes it.

Notes from the agent: This round changed only docs/specs/29-spec-box-local-first/29-spec-box-local-first.md, the one file the maintainer accepted outside the layers. Three sentences were added to the decision 'A box's output is logged from its start.' BOX-155 and the rest of the file are unchanged. just lint-prose did not run: vale is not installed on this box. The thread also asks to correct 'limit (4)' in the PR body. The runner writes the PR body, so that edit is not mine to make.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 3, 2026 00:46
@mitodrummer
mitodrummer marked this pull request as ready for review November 3, 2026 00:55
@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 3196def. The log is written from the box's start, whether or not a client is attached, outside the box's mounts. Writer and reader both refuse links and non-regular files. A store box whose log can't open does not start. The 8 MiB drop-oldest cap holds at rest, and a reader that keeps up gets each byte once across a rotation. A lost PTY client records detached and runs on_detach (BOX-016). ReadLog is additive on the wire, and the log goes with the box on reap. Low follow-ups are in the walk ledger.

@mitodrummer
mitodrummer added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 2894ffc Nov 3, 2026
37 checks passed
@mitodrummer
mitodrummer deleted the agent/2402-log-a-box-s-output-from-its-start-out-of-the-box branch November 3, 2026 01:42
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
mirr
E4A5
or_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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T10 — Log a box's output from its start, out of the box's reach

1 participant

0