8000 T35 — Attach to a terminal exec in a box by minimal-foundry[bot] · Pull Request #3044 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T35 — Attach to a terminal exec in a box - #3044

Merged
agent-137 merged 7 commits into
mainfrom
agent/2911-attach-to-a-terminal-exec-in-a-box
Nov 5, 2026
Merged

agent-137 merged 7 commits into
mainfrom
agent/2911-attach-to-a-terminal-exec-in-a-box

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T35 — Attach to a terminal exec in a box

slice: S3
kind: task
covers: BCLI-068
size: M
est: 100 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: for an exec id the box never ran, min box attach <box> --exec <id> prints " has no exec ." and exits 4; for an exec that ended, it prints "Exec has ended. Read its output with min box logs --exec , or its exit code with min box wait --exec ." and exits 4, pinned by box_attach_exec_ended_exit4_names_logs_and_wait
observability: the command 8000 's existing tracing span records the verb, the box id it resolved and the outcome
layers:
- client:
- crates/minimal/src/cli.rs: the verb's arguments and help in the one command model
- crates/minimal/src/cmd/boxes/attach.rs: --exec <id> reattaches a live terminal exec, with the unknown, ended, no-terminal and no-box refusals in that order
- crates/minimal/src/main.rs: failure_exit_code maps this verb's refusals to their exit codes
- crates/minimal/src/tests.rs: the in-process minimald test harness gains this layer's tests
test: cargo nextest run -p minimal box_attach_exec_reattaches_live_pty_exec
test: cargo nextest run -p minimal box_attach_exec_unknown_id_exit4_names_id
test: cargo nextest run -p minimal box_attach_exec_non_pty_exit2_names_logs
test: cargo nextest run -p minimal box_attach_exec_ended_exit4_names_logs_and_wait
test: cargo nextest run -p minimal box_attach_exec_without_box_exit2
produces: none
consumes:
- crates/minimal/src/cmd/boxes/address.rs
proof: min box attach <box> --exec <id> reattaches a terminal exec, and refuses an exec that ended or has no terminal.
depends on:
- blocked by #2904
- blocked by #2895

Proof

A person can run min box attach <box> --exec <id> to reattach their terminal to a live terminal exec in the box. The command refuses an exec id the box never ran (exit 4), an exec that ended (exit 4, naming min box logs and min box wait with --exec), an exec with no terminal (exit 2, naming min box logs --exec), and --exec with no box (exit 2).

Notes from the agent: 1. The spec says to resolve the exec id against the live exec ids from BOX-159. The daemon's ShowBox still returns an empty live_execs list on this branch (box_manager.rs, issue #2404). So the client reads the box's kept events (exec_started, exec_exited and their pty field), and also counts any id in live_execs as live. An exec of a box that does not run counts as ended. The two blockers (#2904, #2895) are not visible in git log.
2. The harness daemon never starts a sandbox, so it runs no exec. The tests write the exec events that the daemon writes into the box's events file. Then they drive box_attach_exec_on_hosts with a fake attach. The real ssh -tt attach path (attach_exec_by_id) is not exercised by these tests. It mirrors the attach_exec_terminal function in exec.rs, which is outside the layers, so attach.rs has its own copy.
3. main.rs needed no change: failure_exit_code already maps every BoxAttachRefusal through exit_code(), and the new variants live on that type.
4. Printed text: the unknown-id and ended-exec messages copy the task's exact wording. The ended message and the no-terminal message name min box logs <box> --exec <id> and min box wait <box> --exec <id>, as the spec requires. These flags do not exist on this branch yet, so the instruction is not runnable as written until the tasks for BCLI-056 and BCLI-057 land. The no-terminal wording is mine: "Exec in has no terminal to attach to. Run min box logs <box> --exec <id> to see its output."
5. A completed attach exits with the exec's own code, as min box exec -t does. ssh's 255 maps to the existing exit-7…[truncated]

Tests

line exit tail
cargo nextest run -p minimal box_attach_exec_reattaches_live_pty_exec 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-12b4... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimal box_attach_exec_unknown_id_exit4_names_id 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-28ae... 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 minimal box_attach_exec_non_pty_exit2_names_logs 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-ae16... 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 minimal box_attach_exec_ended_exit4_names_logs_and_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-5afb... 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 minimal box_attach_exec_without_box_exit2 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-1ef9... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minimal-clie

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.

dSpan {
            name: "session.message",
            fields: {
                "kind": "GetNetSwitch",
                "parent_span_id": "ddb39f97b8fa2aa8",
                "span_id": "cb384e377856a88a",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "ddb39f97b8fa2aa8",
                "span_id": "891e8e38c4a69e9a",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "ddb39f97b8fa2aa8",
                "span_id": "4d8f8e8cf7c732a2",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "ddb39f97b8fa2aa8",
                "queued_behind": "0",
                "span_id": "63a7ff13dd02815c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "ddb39f97b8fa2aa8",
                "queued_behind": "0",
                "span_id": "32f2357515e53c17",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "32f2357515e53c17",
                "span_id": "c43210518cae9ebe",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "HandOverName",
                "parent_span_id": "32f2357515e53c17",
                "queued_behind": "0",
                "span_id": "fbdc4f58fdc69b8c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 
────────────
     Summary [  54.719s] 931 tests run: 930 passed, 1 failed, 1 skipped
        FAIL [   2.947s] (931/931) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
error: test run failed

Closes #2911

`min box attach <box> --exec <id>` reattaches this terminal to a live
terminal exec in the box. It reads the box's kept events for the exec,
and refuses an unknown id (exit 4), an exec that ended (exit 4), and an
exec with no terminal (exit 2), in that order. clap refuses `--exec`
without a box with exit 2.
@coderabbitai
coderabbitai Bot commented Nov 5, 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: 73f9ccd8-8adb-4b2d-811f-0b3b91668272

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 5, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: every review thread resolved at 60273dc9fd99.

@agent-137 resolved the last thread, which releases the approval this review had withheld.

Nothing to do.

@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/minimal/src/cmd/boxes/attach.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 · 3 conditions · the full ruling

The --exec refusals and their order conform to architecture.md line 491. The exec attach does not end as the design says. Line 491 and line 376 say that a stop, a removal or a takeover ends an attach with 254 after one stderr line. The --exec path returns the raw ssh code with no notice. C1 fixes this. The events fallback for live execs is an interim, and C2 labels it with #2404. C3 files the residual for the logs --exec and wait --exec verbs that the messages name. One escalation asks which code an exec's own exit gives.

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

Comment thread crates/minimal/src/cmd/boxes/attach.rs Resolved
Comment thread crates/minimal/src/cmd/boxes/attach.rs Resolved
Comment thread crates/minimal/src/cmd/boxes/attach.rs Resolved
A start event with neither pty nor mode no longer refuses a live exec
as having no terminal.
@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: 4 unresolved threads — minimal-foundry[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs:1060, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs:786, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs:865. 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 · 2 conditions · the full ruling

The --exec attach finds the box and the exec and refuses in the order that line 491 gives. The compare correctly leaves an unknown PTY flag to the host, and the host refuses a non-TTY exec. Two points stay open. A box stop or removal under an exec attach must end with 254 after one line, as lines 491 and 376 say. Make that change in C1. The client derives liveness from events because live_execs is empty until #2404. Mark that as an interim with its flip in C2. The exit code for an exec that ends by itself goes to a person as an escalation.

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

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

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

line exit tail
cargo nextest run -p minimal box_attach_exec_reattaches_live_pty_exec 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-ecb5... 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 minimal box_attach_exec_unknown_id_exit4_names_id 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-fca0... 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 minimal box_attach_exec_non_pty_exit2_names_logs 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-15ea... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling de 8000 code v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimal box_attach_exec_ended_exit4_names_logs_and_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-05da... 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 minimal box_attach_exec_without_box_exit2 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-1649... 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.

dSpan {
            name: "session.message",
            fields: {
                "kind": "GetNetSwitch",
                "parent_span_id": "fbf2ee1779bf0564",
                "span_id": "7b1de4f153918a87",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "fbf2ee1779bf0564",
                "span_id": "93b5f124fd18cd4c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "fbf2ee1779bf0564",
                "span_id": "ea336abf88e3c8c5",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "fbf2ee1779bf0564",
                "queued_behind": "0",
                "span_id": "98ffa00bc4784399",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "fbf2ee1779bf0564",
                "queued_behind": "0",
                "span_id": "270f6e0a602283c4",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "270f6e0a602283c4",
                "span_id": "122f2e74333d1a04",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "HandOverName",
                "parent_span_id": "270f6e0a602283c4",
                "queued_behind": "0",
                "span_id": "ccdb46a2798eaa6b",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 
────────────
     Summary [  54.292s] 931 tests run: 930 passed, 1 failed, 1 skipped
        FAIL [   2.929s] (931/931) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
error: test run failed

Review threads the coder judged:

  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs: exec_found treats a start event with neither pty nor mode as unknown and leaves the terminal check to the host. box_attach_exec_non_pty_exit2_names_logs pins this.

min box attach <box> --exec <id> reattaches a terminal exec, and refuses an exec that ended or has no terminal. A live exec whose start event does not record a terminal is now no longer refused by the client. The host makes that check.

Notes from the agent: Revise round 1: the one review thread is valid. exec_found now refuses with NoTerminal only when the start event records pty=false or a mode that is not tty. The non-pty test asserts that a start event with neither field classifies as Terminal. cargo clippy -p minimal --all-targets is clean.

@agent-137
Copy link
Copy Markdown
Member

/revise Arch C1 and C2 at head b1502c5, plus the maintainer's ruling on the open question.

Ruling: an exec that ends by itself exits 0. architecture.md line 376: min box attach reports how the attach ended, and "a detach or the entrypoint's own exit is 0". min box attach --exec follows the same table. Its own end is 0, and the exec's code stays readable through min box wait <box> --exec <id> (#2913).

C1 (threads at attach.rs ~786 and ~1060). box_attach_exec_on_hosts returns the raw ssh code, and ~786 hands it straight to Handoff::Exit, so the --exec attach skips #3018's ending table. When someone runs min box stop <box> while the user is attached, the exec dies by signal, exit_code is null, and ExecExit::status() gives code.unwrap_or(1). So the command exits 1 with no line. A removed box does the same.

  • After the attach ends, read the box with box_after_attach_settled (the same bounds as the bare path: ENTRYPOINT_END_SETTLE after 0, DAEMON_END_SETTLE after 254), and return an AttachEnding through the same ending table.
  • That gives 254 with one line for a stop ("Detached: was stopped."), a removal, or another client's takeover, and 0 for a detach or the exec's own end, per the ruling.
  • Print ending.notice once.
  • Add box_attach_exec_box_stopped_exit254_names_stop, plus a test that an exec ending with code 7 makes min box attach --exec exit 0.

C2 (threads at attach.rs ~842 and ~865). exec_found decides liveness from events (exec_started with no exec_exited in a running box) because ShowBox.live_execs stays empty until #2404. Mark this // Interim: in its doc comment, naming #2404 and the flip: live_execs alone decides once #2404 fills it.

Change nothing else.

A stop, a removal or another terminal's attach now ends the exec attach
with 254 and one line, and a detach or the exec's own end with 0. Mark
the event-derived liveness in exec_found as interim until #2404.
@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 compare fixes the open conditions from b1502c53. min box attach <box> --exec <id> now ends through the same table as the bare attach. A stop or a removal is 254 with one line on stderr. A detach or the exec's own end is 0. Lines 376 and 491 say this. The doc comment on exec_found marks the event-derived liveness as an interim until #2404, with its flip condition. Nothing remains to change in this PR.

Ruled at 09fa8a1 against gominimal/arch@7a86ec7bb3c0 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: 6 unresolved threads — minimal-foundry[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs:874, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, …. No findings.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a /revise note, pushed as 09fa8a1a4f04.

line exit tail
cargo nextest run -p minimal box_attach_exec_reattaches_live_pty_exec 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-5b20... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimal box_attach_exec_unknown_id_exit4_names_id 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-dbce... 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 minimal box_attach_exec_non_pty_exit2_names_logs 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-8844... 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 minimal box_attach_exec_ended_exit4_names_logs_and_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-4017... 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 minimal box_attach_exec_without_box_exit2 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-e6f0... 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.

dSpan {
            name: "session.message",
            fields: {
                "kind": "GetNetSwitch",
                "parent_span_id": "49441659b38764ff",
                "span_id": "9753bca9580d2104",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "49441659b38764ff",
                "span_id": "a8844e1d7324f527",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "49441659b38764ff",
                "span_id": "b41fa025c03a6753",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "49441659b38764ff",
                "queued_behind": "0",
                "span_id": "275e6254d151e585",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "49441659b38764ff",
                "queued_behind": "0",
                "span_id": "39afc6a07343e5d1",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "39afc6a07343e5d1",
                "span_id": "e9d8e7b778b902a5",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "HandOverName",
                "parent_span_id": "39afc6a07343e5d1",
                "queued_behind": "0",
                "span_id": "c15a05600985eeeb",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 
────────────
     Summary [  68.298s] 933 tests run: 932 passed, 1 failed, 1 skipped
        FAIL [   3.580s] (933/933) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
error: test run failed

Review threads the coder judged:

  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs: box_attach_exec_on_hosts now reads the box after the attach and returns an AttachEnding from exec_attach_ending, which delegates to attach_ending. A stop or removal gives 254 and one line.
  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs: The --exec branch of cmd_box_attach prints ending.notice on stderr once before Handoff::Exit(ending.code), the same as the bare path.
  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs:874: exec_found's doc comment marks the event-derived liveness as Interim, names T30 — Run a command in a box with a terminal or detached, under an exec id #2404, and says live_execs alone decides once T30 — Run a command in a box with a terminal or detached, under an exec id #2404 fills it.
  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs: A signal-ended exec (code 1) in a box that a stop recorded as stopped now ends with 254 and "Detached: was stopped.", pinned by box_attach_exec_box_stopped_exit254_names_stop.
  • 👍 valid — crates/minimal/src/cmd/boxes/attach.rs:851: Added the Interim note to exec_found's doc comment with the T30 — Run a command in a box with a terminal or detached, under an exec id #2404 link and the flip: once live_execs fills, only live_execs decides liveness.

min box attach <box> --exec <id> reattaches a terminal exec, and refuses an exec that ended or has no terminal. A stop or a removal during the attach ends the command with 254 and one line, such as "Detached: web was stopped.". A detach or the exec's own end exits 0.

Notes from the agent: C1: box_attach_exec_on_hosts now reads the box after the attach with box_after_attach_settled and returns an AttachEnding through the new exec_attach_ending. That function delegates to attach_ending. The ssh code 254 and 255 map as on the bare path. A gone box gives 254 with the removal line. A stopped box gives 254 with the stop line. Every other ending gives 0, per the ruling. cmd_box_attach prints ending.notice once before the exit. Settle bound: DAEMON_END_SETTLE after 254, and ENTRYPOINT_END_SETTLE after every other code. A signal-ended exec reports 1, not 0, so the box is read after every code, not only after 0. Limit: a stop that waits on a process past 500 ms after the exec died by signal will read as running, and the command exits 0. A longer bound for non-zero codes would make every exec that exits by itself with a non-zero code wait up to 12 s. C2: exec_found's doc comment marks the event-derived liveness as Interim, links #2404, and states the flip. The existing reattach test now expects exit 0 in place of the exec's code 3. just clippy-strict was not run.

@agent-137
Copy link
Copy Markdown
Member

/revise One case is left open from C1 (thread PRRT_kwDOPdfcxM6z_kDZ), at head 09fa8a1. Arch passes. CI is green.

A min box stop of a box whose entrypoint survives SIGTERM (a plain shell, for example) exits min box attach --exec with 0 and no line. stop_box_processes (crates/minimald/src/box_actor/host.rs ~1619-1700) signals every process at once, but writes the stopped record only after everything has ended, or after the 10 s STOP_GRACE. So the attached exec dies at once with status 1 (ExecExit::status, exec/bridge.rs ~1702), and the box still reads running past the client's 500 ms ENTRYPOINT_END_SETTLE. exec_attach_ending then gives 0. The test box_attach_exec_box_stopped_exit254_names_stop does not catch it, because its fake stop lands 100 ms after the attach.

Fix it on the daemon side, so the client does not have to wait 12 s after every failing exec. When the box's stop (or removal) ends an attached exec, serve_exec_attach (exec/bridge.rs ~2294) ends the attach with 254 instead of the exec's status. The client then settles with DAEMON_END_SETTLE and prints "Detached: was stopped." through the ending table it already has. The maintainer accepts the exec/bridge.rs edit outside the layers. Keep the exec's own end as its own exit code to the daemon's records. Only the attach's end changes.

Tests:

  • A daemon-side test: stopping a box ends an attached exec's channel with 254.
  • A client test where the stopped record lands later than 500 ms after a 254, and the attach still exits 254 with the stop line.

Also make the unreachable SSH_CONNECTION_FAILED arm in exec_attach_ending name --exec <id> in its retry command, or remove it. Change nothing else.

A stop takes the box's host before it signals the box, and a removal
marks the box first. An attached tty exec that ends after either now
ends its attach channel with 254, so min box attach --exec reads the
box and prints the stop line. The exec's records keep its own status.
exec_attach_ending now builds the retry command with --exec <id>.
A new test pins a stopped record that lands well after the daemon's
254: the attach still ends with 254 and the stop line.
@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: 6 unresolved threads — minimal-foundry[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs:874, minimal-arch[bot] crates/minimal/src/cmd/boxes/attach.rs, …. No findings.

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

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

arch-review: PASS · the full ruling

The compare conforms to architecture.md line 491 and line 376. When a stop or a removal ends an attached terminal exec, the daemon now ends the attach with 254. The client then prints the stop or removal line. A lost connection gives 7, and the retry names --exec <id>. The exec's own records keep its exit code. The minimald edit is outside the task's layers, but line 491 makes the daemon the owner of the 254.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p minimal box_attach_exec_reattaches_live_pty_exec 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-e174... 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 minimal box_attach_exec_unknown_id_exit4_names_id 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-49e2... 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 minimal box_attach_exec_non_pty_exit2_names_logs 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-c53b... 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 minimal box_attach_exec_ended_exit4_names_logs_and_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-da0a... 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 minimal box_attach_exec_without_box_exit2 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-83f0... 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.

9c3",
                "span_id": "cb30290086b57e94",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "23bc1e6d8b2ad9c3",
                "span_id": "95a5850c43cd556c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "23bc1e6d8b2ad9c3",
                "queued_behind": "0",
                "span_id": "70efac9f84de9df5",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "23bc1e6d8b2ad9c3",
                "queued_behind": "0",
                "span_id": "6b4f6514261531e2",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "6b4f6514261531e2",
                "span_id": "65779e66aeadc7ea",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "HandOverName",
                "parent_span_id": "6b4f6514261531e2",
                "queued_behind": "0",
                "span_id": "6dd9c3b016f5cc24",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   2.742s] ( 963/2323) minimald box_actor::tests::a_box_nobody_handed_an_address_publishes_at_the_answerers_grant
        PASS [   2.581s] ( 964/2323) minimald box_actor::tests::a_contended_walk_never_replaces_a_present_verdict
        PASS [   3.012s] ( 965/2323) minimald box_actor::tests::a_box_re_registered_inside_the_landing_window_keeps_its_grant
────────────
     Summary [  86.890s] 965/2323 tests run: 964 passed, 1 failed, 26 skipped
        FAIL [   4.468s] ( 962/2323) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 1358/2323 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_actor/handle.rs
  • crates/minimald/src/exec.rs
  • crates/minimald/src/exec/bridge.rs
  • crates/minimald/src/exec/tests.rs

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

min box attach <box> --exec <id> reattaches a terminal exec, and refuses an exec that ended or has no terminal. A min box stop that ends the attached exec now ends the command with 254 and "Detached: was stopped.", even when the box's entrypoint outlives SIGTERM and the stopped record lands seconds later.

Notes from the agent: Revise round 3. The daemon side: serve_exec_attach now takes the box's handle. When an attached tty exec ends, the attach channel gets 254 if the box is ending, and the exec's own status otherwise. A box is ending when a removal started (is_destroying) or when the actor no longer holds a host. A stop takes the host out of the slot before it signals any process, so the exec's death always comes after it. The exec's own exit stays in its records and in min box wait. Files outside the layers: crates/minimald/src/exec/bridge.rs (accepted by the maintainer), crates/minimald/src/exec.rs (one call site passes the handle), crates/minimald/src/box_actor/handle.rs (a new BoxHandle::has_host reader, plus two cfg(test) helpers that empty the host slot and mark a removal), and crates/minimald/src/exec/tests.rs (the daemon-side test). One side effect: the end of the entrypoint also takes the host, so an exec that the box's own exit ends now gives 254. The client then reads the box as exited and ends with 0 and the existing "Detached: exited with code N." line, where before it ended with 0 and no line. The SSH_CONNECTION_FAILED arm in exec_attach_ending now names --exec <id> in its retry command. To do that, exec_attach_ending takes the exec id. The daemon test models the stop and the removal by emptying the host slot and by setting the destroy flag on a stand-in handle. It does not run a full actor stop. just clippy-strict and the whole-crate suites were not run.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 5, 2026 17:42
@agent-137
agent-137 marked this pull request as ready for review November 5, 2026 17:57
@agent-137 agent-137 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 60273dc against #2911 (BCLI-068 --exec). min box attach --exec resolves a live PTY exec in the named box and ends through #3018's ending table. 5a4c3f5 makes the daemon end an exec attach with 254 when the box's end (stop, removal, entrypoint end) ended the exec, keeping the exec's own status in its records, so a stop of a box whose entrypoint survives SIGTERM still reads 'Detached: was stopped.' (daemon and late-record client tests). The exec's own end exits 0 per the maintainer's ruling (architecture.md line 376). 9c83fb5 names --exec in the lost-connection retry.

@agent-137
Copy link
Copy Markdown
Member

/review

@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; @agent-137 closed the last one. The review of 60273dc9fd99 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 5, 2026
@agent-137
agent-137 added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 8efecfc Nov 5, 2026
36 checks passed
@agent-137
agent-137 deleted the agent/2911-attach-to-a-terminal-exec-in-a-box branch November 5, 2026 18:43
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.

T35 — Attach to a terminal exec in a box 8

1 participant

0