Skip to content

fix(minimal): don't scaffold over an existing config on non-interactive activate - #758

Merged
norrietaylor merged 1 commit into
mainfrom
fix/activate-nontty-no-clobber
Jul 15, 2026
Merged

fix(minimal): don't scaffold over an existing config on non-interactive activate#758
norrietaylor merged 1 commit into
mainfrom
fix/activate-nontty-no-clobber

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 14, 2026

Copy link
Copy Markdown
Member

The bug

min activate . on a project whose config lives at .minimal/minimal.toml
would, in any non-interactive context (CI, piped/EOF stdin, an agent),
overwrite that config with a generated template — destroying the user's
tasks and package lists. It was also silently regenerating this repo's own
.minimal/minimal.toml on every native e2e run (scripts/session-e2e.sh runs
min activate . with non-tty stdin).

The fixes

Fix A — .minimal/-aware detection. Detection now goes through
mfile::File::from_dir — the same resolver the CLI loads config with. In the
activate path the config always carries a repo_dir override, so
ProjectSetup::for_init resolves via MFileSearchStrategy::Override
mfile::File::from_dir (crates/mctx/src/mfile_search_strategy.rs:21), which
understands both the root and .minimal/ layouts. Reusing it makes
detection match the exact path the init writer would target. Any outcome other
than mfile::Error::NotFound (including a present-but-malformed file) counts as
"exists", so we never scaffold over it.

Fix B — never default-YES a scaffold on non-interactive stdin. The prompt is
now gated on std::io::stdin().is_terminal(). On non-tty stdin we skip the
prompt entirely and bail! with the existing
"No {MFILE_NAME} found. Run 'minimal init' to create one." hint instead of
default-YES-ing a destructive create.

Where I gated the tty check: at the ensure_mfile_or_prompt call site, not
in the shared confirm(). confirm() has one other caller — the
run_init_flow "Continue?" prompt — which legitimately wants
empty-input-defaults-yes and is only reached once we're already committed to a
non-destructive init flow. Changing confirm() globally would be a wider blast
radius for no benefit, so gating only the destructive path is the surgical
choice.

Invariants after the fix

  • min activate . with non-tty stdin over a project that has a config
    (root or .minimal/) → proceeds, config untouched.
  • min activate . with non-tty stdin over a project with no config →
    errors telling the user to run minimal init; writes nothing.
  • Interactive behavior (real tty) is unchanged.

Test

Added focused, behavior-level tests in the minimal crate's existing tests
module:

  • project_has_mfile_detects_dot_minimal_layout — the core regression: a temp
    dir containing .minimal/minimal.toml is detected (so activate returns
    without prompting and never scaffolds over it). This is exactly what the old
    naive join(MFILE_NAME) check missed.
  • project_has_mfile_false_when_absent — a project with no config in either
    layout reports false, so the caller falls through to the (tty-gated) prompt.

Detection was factored into a small pure project_has_mfile helper to make it
directly testable; the stdin/tty branch is environment-dependent and left to
manual/CI coverage.

Validation

  • cargo fmt — clean (no reformatting of the change).
  • cargo test -p minimal / cargo clippy -p minimal --all-targets -- -D warnings
    can't run natively on the macOS dev host: the minimal crate transitively
    depends on procfs (via minvmd/switch), which is Linux-only. A Linux
    cross build compiled the full dependency tree and reached the minimal
    package, failing only in crates/minimal/build.rs (a git rev-parse for the
    version string returns empty inside the container because this is a git
    worktree whose gitdir isn't mounted) — a pre-existing, environment-specific
    build-script issue unrelated to this change. Definitive cargo test -p minimal
    • clippy run on the Linux CI lanes. The change is confined to CLI logic +
      tests in one crate with no platform-specific surface.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved activation behavior when projects use either supported configuration layout.
    • Prevented automatic configuration prompts in non-interactive environments.
    • Added clearer guidance to run minimal init when configuration is missing.

…ve activate

`min activate .` on a project whose config lives at
`.minimal/minimal.toml` would silently overwrite that config with a
generated template in any non-interactive context (CI, piped/EOF stdin,
an agent), destroying the user's tasks and package lists.

Two independent flaws combined:

- Detection used a naive `project_path.join(MFILE_NAME).exists()` check
  that only understands the root `<project>/minimal.toml` layout, so on
  a `.minimal/` project it missed and fell through to the prompt.
- `confirm()` returns `true` on empty/EOF input, so non-tty stdin
  default-YES'd the "create one?" prompt, and the init writer resolved
  the real `.minimal/minimal.toml` path and clobbered it.

Fix A: detect via `mfile::File::from_dir` — the same resolver the CLI
loads config with (root OR `.minimal/`) — so an existing config is
recognized and activation proceeds untouched.

Fix B: gate the scaffold prompt on `std::io::stdin().is_terminal()`.
On non-interactive stdin we bail with the existing "run 'minimal init'"
hint instead of default-YES-ing a destructive create. Gated at the
call site (not in shared `confirm()`) so the other, non-destructive
`confirm()` caller keeps its empty-defaults-yes behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2ca3bdd-3bbb-4ad2-ad9b-430a5587ec5b

📥 Commits

Reviewing files that changed from the base of the PR and between 89bc29b and ed90dbb.

📒 Files selected for processing (1)
  • crates/minimal/src/lib.rs

📝 Walkthrough

Walkthrough

minimal activate now recognizes both configuration layouts and only prompts to create missing configuration when stdin is interactive. Non-interactive execution exits with the existing initialization hint, with tests covering detection and absence.

Changes

Configuration prompting

Layer / File(s) Summary
Terminal-aware configuration flow
crates/minimal/src/lib.rs
Configuration detection uses mfile::File::from_dir for both supported layouts; prompting is gated on terminal stdin, and tests cover existing .minimal/ and absent configurations.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Suggested reviewers: evanspearman, 0chroma

Poem

I’m a rabbit who checks every burrow with care,
Root or hidden, the config is there.
No terminal? I won’t build things anew,
I’ll leave a clear hint for the next thing to do.
Hop, hop—safe prompts come through!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: preventing scaffolding over existing config during non-interactive activate.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@norrietaylor
norrietaylor merged commit f4c77b9 into main Jul 15, 2026
27 checks passed
@norrietaylor
norrietaylor deleted the fix/activate-nontty-no-clobber branch July 15, 2026 01:52
norrietaylor added a commit that referenced this pull request Jul 15, 2026
…it prompt

When a session shell exits, the daemon shows an `async_dialog::Select`
prompt (Detach / Delete) and blocks on the SSH channel for the answer. A
non-interactive attach — a scripted client, or one whose connection
dropped — has no one to answer, so the session host wedges on the prompt
forever (the reader never sees a keypress, nor an EOF, since a forced-pty
client delivers stdin close as an in-band Ctrl-D rather than a channel
EOF).

Bound the prompt with a short grace period, defaulting to the safe Detach
(identical to the existing At(0)/Cancelled arms) on timeout. This mirrors
the non-tty guard in #758: no interactive prompt may block a
non-interactive caller.

A gate on "no PTY requested" cannot serve here: `shell_request` only ever
grants a session shell with a PTY (it rejects a shell otherwise), so at
the exit prompt a PTY is always present — the pty-absence signal is
unreachable. A bounded wait is the reachable equivalent, and it also
covers a dropped or idle interactive client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
… min add)

The shared session e2e only proved the lifecycle (activate -> ls ->
destroy -> stop); it never forked a session sandbox, so it could not
catch a break in the real in-sandbox user path. Deepen it, on the native
lane (DM2), to attach an INTERACTIVE PTY shell — which forks a real
hakoniwa sandbox — and prove the in-sandbox `min` helper:

  - self-seed a throwaway project (mktemp -d) with the repo's own pinned
    [upstream] and a light shell stack, and activate THAT, not the repo
    root (a non-interactive activate over the repo root scaffolds over
    .minimal/minimal.toml; fixed crate-side in #758).
  - pipe an interactive `min attach <sid>` a command stream that records a
    non-baseline tool (tar) as absent, `min add`s it into the session,
    then runs it. Its version banner round-tripping proves `min add`
    reached the daemon over the /run/minenv_sock relay and hardlinked the
    package into the live rootfs. On `exit` the session-exit prompt
    auto-detaches this non-interactive client (the minimald fix in this
    same PR), so the attach returns exit 0.

The proof is gated off the VM lanes (E2E_VM): there the sandbox forks
inside the guest, whose compose/cache and bridge-driven attach this
harness cannot validate; those stay proven in Rust. VM lanes run the
lifecycle proof only — no faked coverage. The existing structure
(set -uo pipefail, mnl(), fail()/teardown(), ::group::/::error::,
millisecond timing) is preserved; shellcheck is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
…it prompt

When a session shell exits, the daemon shows an `async_dialog::Select`
prompt (Detach / Delete) and blocks on the SSH channel for the answer. A
non-interactive attach — a scripted client, or one whose connection
dropped — has no one to answer, so the session host wedges on the prompt
forever (the reader never sees a keypress, nor an EOF, since a forced-pty
client delivers stdin close as an in-band Ctrl-D rather than a channel
EOF).

Bound the prompt with a short grace period, defaulting to the safe Detach
(identical to the existing At(0)/Cancelled arms) on timeout. This mirrors
the non-tty guard in #758: no interactive prompt may block a
non-interactive caller.

A gate on "no PTY requested" cannot serve here: `shell_request` only ever
grants a session shell with a PTY (it rejects a shell otherwise), so at
the exit prompt a PTY is always present — the pty-absence signal is
unreachable. A bounded wait is the reachable equivalent, and it also
covers a dropped or idle interactive client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
… min add)

The shared session e2e only proved the lifecycle (activate -> ls ->
destroy -> stop); it never forked a session sandbox, so it could not
catch a break in the real in-sandbox user path. Deepen it, on EVERY lane
(native DM2, KVM DM3, macOS DM1), to attach an INTERACTIVE PTY shell —
which forks a real hakoniwa sandbox (on a VM lane, inside the guest, over
the vsock bridge) — and prove the in-sandbox `min` helper:

  - guarantee the activate target carries a host-side `minimal.toml`: the
    native lane self-seeds a throwaway `mktemp -d` (repo's pinned
    [upstream] + a light shell stack), never the repo root; when the
    caller sets E2E_PROJECT_DIR (VM lanes pass /tmp) we seed one there if
    missing. Since #758 a non-interactive `min activate` over a
    config-less dir bails, so this is the harness-side complement.
  - pipe an interactive `min attach <sid>` a command stream that records a
    non-baseline tool (tar) as absent, `min add`s it into the session,
    then runs it. Its version banner round-tripping proves `min add`
    reached the daemon over the /run/minenv_sock relay and hardlinked the
    package into the live rootfs. On `exit` the session-exit prompt
    auto-detaches this non-interactive client (the minimald fix in this
    same PR), so the attach returns exit 0.

The proof is un-gated: the `min` CLI abstracts where the daemon lives, so
the identical attach+add+run runs on all three lanes (the script's design
premise), and the VM lanes exercise the more valuable bridge-exec path.
Structure preserved (set -uo pipefail, mnl(), fail()/teardown(),
::group::/::error::, millisecond timing); shellcheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
When a session shell exits, the daemon shows a Detach/Delete prompt and
blocks on the SSH channel for the answer. A non-interactive attach — a
scripted or piped client — has no one to answer, so the session host
wedges on the prompt forever (a forced-pty client delivers stdin close as
an in-band Ctrl-D, never a channel EOF the reader could detect).

Have the `min` client report whether its own context is a terminal (both
stdin and stdout a tty) via a MINIMAL_INTERACTIVE env var, independent of
the forced `-tt`. The session host consults it at the exit prompt: an
interactive client is prompted as before; a non-interactive one skips the
unanswerable prompt and deletes the session on exit, so an ephemeral
attach cleans up after itself instead of orphaning a session. Mirrors the
non-tty bail in #758: no interactive prompt may block a non-interactive
caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox, so it could not catch a break in the real in-sandbox user
path. Deepen it, on every lane (native, KVM, macOS), to attach a PTY shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
non-baseline tool (tar) as absent, `min add` it into the session, then run
it; its version banner round-tripping proves `min add` reached the daemon
over the /run/minenv_sock relay and hardlinked the package into the live
rootfs.

The piped attach is non-interactive, so on `exit` the daemon deletes the
session (the minimald change in this branch); the script verifies the
delete-on-exit rather than issuing a separate `min destroy`. A host-side
minimal.toml is seeded so the client pre-flight neither prompts nor bails
(#758); every seed is removed on teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
When a session shell exits, the daemon shows a Detach/Delete prompt and
blocks on the SSH channel for the answer. A non-interactive attach — a
scripted or piped client — has no one to answer, so the session host
wedges on the prompt forever (a forced-pty client delivers stdin close as
an in-band Ctrl-D, never a channel EOF the reader could detect).

Have the `min` client report whether its own context is a terminal (both
stdin and stdout a tty) via a MINIMAL_INTERACTIVE env var, independent of
the forced `-tt`. The session host consults it at the exit prompt: an
interactive client is prompted as before; a non-interactive one skips the
unanswerable prompt and deletes the session on exit, so an ephemeral
attach cleans up after itself instead of orphaning a session. Mirrors the
non-tty bail in #758: no interactive prompt may block a non-interactive
caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox, so it could not catch a break in the real in-sandbox user
path. Deepen it, on every lane (native, KVM, macOS), to attach a PTY shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
non-baseline tool (tar) as absent, `min add` it into the session, then run
it; its version banner round-tripping proves `min add` reached the daemon
over the /run/minenv_sock relay and hardlinked the package into the live
rootfs.

The absence marker is built at runtime (printf "TOOL_%s_BEFORE") so the
PTY echo of the command stream cannot satisfy the assertion — the literal
appears only as executed output. The piped attach is non-interactive, so
on `exit` the daemon deletes the session (the minimald change in this
branch); the script verifies the delete-on-exit rather than issuing a
separate `min destroy`. A host-side minimal.toml is seeded so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
When a session shell exits, the daemon shows a Detach/Delete prompt and
blocks on the SSH channel for the answer. A non-interactive attach — a
scripted or piped client — has no one to answer, so the session host
wedges on the prompt forever (a forced-pty client delivers stdin close as
an in-band Ctrl-D, never a channel EOF the reader could detect).

Have the `min` client report whether its own context is a terminal (both
stdin and stdout a tty) via a MINIMAL_INTERACTIVE env var, independent of
the forced `-tt`. The session host consults it at the exit prompt: an
interactive client is prompted as before; a non-interactive one skips the
unanswerable prompt and deletes the session on exit, so an ephemeral
attach cleans up after itself instead of orphaning a session. Mirrors the
non-tty bail in #758: no interactive prompt may block a non-interactive
caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox, so it could not catch a break in the real in-sandbox user
path. Deepen it, on every lane (native, KVM, macOS), to attach a PTY shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it into the session, then
run it; its version banner round-tripping proves `min add` reached the
daemon over the /run/minenv_sock relay and hardlinked the package into the
live rootfs.

The absence marker is built at runtime (printf "TOOL_%s_BEFORE") so the
PTY echo of the command stream cannot satisfy the assertion — the literal
appears only as executed output. The piped attach is non-interactive, so
on `exit` the daemon deletes the session (the minimald change in this
branch); the script verifies the delete-on-exit rather than issuing a
separate `min destroy`. A host-side minimal.toml is seeded so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
When a session shell exits, the daemon shows a Detach/Delete prompt and
blocks on the SSH channel for the answer. A non-interactive attach — a
scripted or piped client — has no one to answer, so the session host
wedges on the prompt forever (a forced-pty client delivers stdin close as
an in-band Ctrl-D, never a channel EOF the reader could detect).

Have the `min` client report whether its own context is a terminal (both
stdin and stdout a tty) via a MINIMAL_INTERACTIVE env var, independent of
the forced `-tt`. The session host consults it at the exit prompt: an
interactive client is prompted as before; a non-interactive one skips the
unanswerable prompt and deletes the session on exit, so an ephemeral
attach cleans up after itself instead of orphaning a session. Mirrors the
non-tty bail in #758: no interactive prompt may block a non-interactive
caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox, so it could not catch a break in the real in-sandbox user
path. Deepen it, on every lane (native, KVM, macOS), to attach a PTY shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it into the session, then
run it; its version banner round-tripping proves `min add` reached the
daemon over the /run/minenv_sock relay and hardlinked the package into the
live rootfs.

The absence marker is built at runtime (printf "TOOL_%s_BEFORE") so the
PTY echo of the command stream cannot satisfy the assertion — the literal
appears only as executed output. The piped attach is non-interactive, so
on `exit` the daemon deletes the session (the minimald change in this
branch); the script verifies the delete-on-exit rather than issuing a
separate `min destroy`. A host-side minimal.toml is seeded so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox, so it could not catch a break in the real in-sandbox user
path. Deepen it, on every lane (native, KVM, macOS), to attach a PTY shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it into the session, then
run it; its version banner round-tripping proves `min add` reached the
daemon over the /run/minenv_sock relay and hardlinked the package into the
live rootfs.

The absence marker is built at runtime (printf "TOOL_%s_BEFORE") so the
PTY echo of the command stream cannot satisfy the assertion — the literal
appears only as executed output. The piped attach is non-interactive, so
on `exit` the daemon deletes the session (the minimald change in this
branch); the script verifies the delete-on-exit rather than issuing a
separate `min destroy`. A host-side minimal.toml is seeded so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox. Deepen it, on every lane, to attach an interactive shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it, then run it; its version
banner round-tripping proves `min add` reached the daemon over the
/run/minenv_sock relay and hardlinked the package into the live rootfs.

A session is interactive by design, so the proof drives it like a real
user through a REAL pty (scripts/e2e-attach-pty.py): pump the command
stream, then answer the session-exit Detach/Delete prompt with keystrokes
(Down + Enter => Delete). A pipe is not a tty and cannot answer that
prompt. Selecting Delete tears the session down, so it must be delisted.
Output is matched with a bash glob, not `printf | grep -q`, so `min add`'s
progress-bar flood cannot SIGPIPE a pipefail false-negative. A host-side
minimal.toml is seeded (uploaded to the session since #748) so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox. Deepen it, on every lane, to attach an interactive shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it, then run it; its version
banner round-tripping proves `min add` reached the daemon over the
/run/minenv_sock relay and hardlinked the package into the live rootfs.

A session is interactive by design, so the proof drives it like a real
user through a REAL pty (scripts/e2e-attach-pty.py): pump the command
stream, then answer the session-exit Detach/Delete prompt with keystrokes
(Down + Enter => Delete). A pipe is not a tty and cannot answer that
prompt. Selecting Delete tears the session down, so it must be delisted.
Output is matched with a bash glob, not `printf | grep -q`, so `min add`'s
progress-bar flood cannot SIGPIPE a pipefail false-negative. A host-side
minimal.toml is seeded (uploaded to the session since #748) so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
The shared session e2e only proved the lifecycle; it never forked a
session sandbox. Deepen it, on every lane, to attach an interactive shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it, then run it; its version
banner round-tripping proves `min add` reached the daemon over the
/run/minenv_sock relay and hardlinked the package into the live rootfs.

A session is interactive by design, so the proof drives it like a real
user through a REAL pty (scripts/e2e-attach-pty.py): pump the command
stream, then answer the session-exit Detach/Delete prompt with keystrokes
(Down + Enter => Delete). A pipe is not a tty and cannot answer that
prompt. Selecting Delete tears the session down, so it must be delisted.
Output is matched with a bash glob, not `printf | grep -q`, so `min add`'s
progress-bar flood cannot SIGPIPE a pipefail false-negative. A host-side
minimal.toml is seeded (uploaded to the session since #748) so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jul 15, 2026
… every lane (#760)

* fix(checkouts): report exit status on git command failure

A failed git command surfaced only its stderr. When git is killed by a
signal (e.g. OOM mid-fetch) stderr holds only whatever it wrote before
dying — which can be a misleadingly benign line such as a "templates not
found" warning, masking that the process was killed rather than failing
cleanly.

Include the rendered ExitStatus (exit code or signal) in GitCommandFailed
and its mctx wrapper, so a signal kill is distinguishable from a
git-reported error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: exercise the session sandbox via interactive attach

The shared session e2e only proved the lifecycle; it never forked a
session sandbox. Deepen it, on every lane, to attach an interactive shell
— which forks a real hakoniwa sandbox (on a VM lane, inside the guest over
the vsock bridge) — and prove the in-sandbox `min` helper: record a
genuinely-absent tool (jq — the shell stack composes base + curl, and jq
is pulled in by neither) as absent, `min add` it, then run it; its version
banner round-tripping proves `min add` reached the daemon over the
/run/minenv_sock relay and hardlinked the package into the live rootfs.

A session is interactive by design, so the proof drives it like a real
user through a REAL pty (scripts/e2e-attach-pty.py): pump the command
stream, then answer the session-exit Detach/Delete prompt with keystrokes
(Down + Enter => Delete). A pipe is not a tty and cannot answer that
prompt. Selecting Delete tears the session down, so it must be delisted.
Output is matched with a bash glob, not `printf | grep -q`, so `min add`'s
progress-bar flood cannot SIGPIPE a pipefail false-negative. A host-side
minimal.toml is seeded (uploaded to the session since #748) so the client
pre-flight neither prompts nor bails (#758); every seed is removed on
teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: provision gvproxy for the KVM/macOS session-e2e lanes

The KVM and macOS session-e2e lanes boot a microVM whose guest daemon
clones the upstream `pkgs` repo during session mint. That clone needs the
guest's egress — NAT + DNS — which minvmd provides by spawning gvproxy as
the per-host-VM switch. Neither lane provisioned the gvproxy binary, and
its resolution is best-effort (never errors), so minvmd booted the VM
switchless: the guest's overlay had no other end and every egress —
including the clone — died with "Could not resolve host: github.com".

Fetch the pinned gvproxy (scripts/fetch-gvproxy.sh, already used by the
native minimald-root-integration job) and point minvmd at it via
MINVMD_GVPROXY_BIN on each lane's session-e2e step. The macOS lane's
fetch auto-selects the gvproxy-darwin asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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.

3 participants