8000 Comparing main@{1day}...main · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
8000
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gominimal/minimal
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main@{1day}
Choose a base ref
...
head repository: gominimal/minimal
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 11 commits
  • 67 files changed
  • 6 contributors

Commits on Nov 5, 2026

  1. ci(release): scope and lock the signing keychain (#3066)

    * ci(release): scope and lock the signing keychain
    
    The signing jobs read KC_PW as a repository secret, passed it to
    security on the command line, and left the keychain unlocked when they
    ended.
    
    Both jobs now set `environment: release`, which holds the secret and
    admits only main and release/* branches. The password goes to security
    on standard input, with pipefail set so a refused unlock still fails
    the step. The last step of each job locks the keychain on any outcome.
    
    The docs add the release branch step for the signing runner group.
    
    * docs(release): keep the access fields in the runner group update
    norrietaylor authored Nov 5, 2026
    Configuration menu
    Copy the full SHA
    d7c85f6 View commit details
    Browse the repository at this point in the history
  2. T119 — Deliver cohort connections and redeem them for the named box (#…

    …3065)
    
    * feat(switch,minvmd): deliver host_ip cohort flows under a reserved id
    
    While a resident host_ip box declares a credentialed upstream, the node
    address holds a share of the proxy pool. Its deliveries carry the
    reserved cohort id, never a member's id (BEP-084).
    
    * feat(bep): redeem cohort deliveries for the box the value names
    
    A request on a host_ip cohort delivery is checked against the egress of
    the live host_ip box its sealed value names, and audited under that box.
    A value naming no such box is refused cohort_misattributed (BEP-084).
    
    * fix(bep): decide a cohort audit subject with its verdict
    
    The audit subject of a cohort delivery came from a second read of the
    cohort, after the verdict's own read. A box that ended or started between
    the two reads gave a record that named a different box than the verdict.
    redeem_attributed now gives the subject from the same facts as the verdict.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 5, 2026
    Configuration menu
    Copy the full SHA
    8bf14c5 View commit details
    Browse the repository at this point in the history

Commits on Nov 6, 2026

  1. T43 — Run a task as a task box that returns its exit code (#2999)

    * feat(minimald): run a task box's spec argv as its entrypoint
    
    A launch now starts the argv the box's spec names. Only a launch for
    hooks holds the shell, so the task does not run before its hooks.
    ensure_host replaces a hook-launched shell with the entrypoint.
    
    A task run on a task box relays the entrypoint's output from the box's
    log and returns the exit code the record keeps. The client leaving
    does not stop the box. The older sandboxed task path stays for one
    release, for a client that sends its arguments on the request.
    
    * feat(minimal): run min task run as a task box that keeps its record
    
    The task box's spec now carries the typed arguments on its entrypoint,
    min task run <task> <args>. The run asks the daemon for that
    entrypoint, owns no box, and exits with the code it relays. The box
    ends with its task and stays as an exited record, and the run prints
    the min box rm command that removes it. An interactive task, which
    has no spec, keeps the exec path.
    
    * test(minimald): prove a task run relays its box's entrypoint
    
    A task run on a task box returns the entrypoint's exit code 7 and its
    output, and the box stays as an exited record. A run whose client goes
    away leaves the entrypoint running, launched once. The record of an
    ended box is read from disk, because its actor stops with it. The
    owns_box request and end_run_box are marked as the older path, kept
    for one release.
    
    * test(e2e): expect min task run to keep its task box
    
    The task box now keeps its record when its entrypoint returns.
    The e2e checks the kept line, the listing, and min box rm.
    
    * test(minimal): pin task run tests to the task box entrypoint
    
    The run sends no --owns-box. The harness has no packages, so the
    stdin test pins the bridge's return, and the e2e pins the output.
    
    * fix(minimald): refuse a run on an ended box as ended, not activating
    
    ensure_host checked status != Active before it checked for a stopped or
    exited box, so a run on an exited task box read as still activating.
    The ended-box check now comes first.
    
    * fix(minimal): keep a running box for min task run --keep
    
    A task box ends when its task returns, and only a running box can run a
    command. A --keep run creates its box with no spec, as an interactive
    task does, so min session run and an attach can use it afterwards.
    
    * fix(minimald): start a task box's entrypoint in its inherit_cwd dir
    
    A task run that relays its box's entrypoint dropped the client's
    directory, so an inherit_cwd task started at /workbench. The exec now
    stages the directory with the task's variables, and the launch starts
    the argv entrypoint there, with PWD set for the in-box min task run.
    
    * fix(minimal): send the inherit_cwd dir on a task box run
    
    The entrypoint run request now carries the invocation directory for a
    task that declares inherit_cwd, as the older task run request did.
    
    * fix(e2e): read a run box's end from its state, not the listing
    
    A task box keeps its record when its command returns (BOX-038), so NET-131's end is the box leaving running. The wait now reads State and Exit code from min box show, passes on exited or stopped, and checks the exit code is the task's 0.
    
    * fix(minimald): log each task box's end and key its run on its spec
    
    The entrypoint watch now logs "task box ended" with the box, the task
    and the exit code, whether or not a client is connected. The e2e reads
    that record. A task run relays an entrypoint when the spec sets
    lifetime until_complete with an argv, not by the Box Type name.
    
    A run behind an own-address box's hook shell is refused at once. The
    entrypoint run clears ends_with_run once the entrypoint starts. The run
    waits for the host to end before it reads the exit code. The e2e
    removes the kept run box with min box rm.
    
    * fix(minimal): store the task box spec for min task run --keep
    
    A --keep run now creates the same task box, with its spec stored, and
    keeps it after the task returns. A run without --keep marks its box to
    end with the run until the daemon starts the entrypoint.
    
    * fix(e2e): run the session run proof in an activated session
    
    A task box ends when its task returns (BOX-038), so the box that
    min task run --keep keeps is exited, and min session run refuses it.
    The proof now starts a session with min session activate. The dirty
    gate check still uses the kept box, since an ended box has no host.
    
    * fix(minimal): remove min task run --keep, and a box the daemon refused
    
    Every task box keeps its record when its task returns, so --keep has no
    job left. The e2e dirty-gate check now runs on a session that
    min session activate starts, after a file is written in it.
    
    The daemon reports a code only once the box's record keeps it. A box
    still running after a reported code never started its task, so the
    client removes it and prints no kept-box line. The refusal now names
    the task and the box, without the spawn-failure prefix.
    
    * fix(minimal): pin the removed box in the stdin test, and say kept in help
    
    The held-open stdin test now asserts that no Kept box line prints and
    that the box is gone, since its entrypoint never starts. min task run
    --help says the box is named <task>-<n> and stays with its exit code.
    The kept line now prints when an entrypoint ended by a signal too.
    
    * fix(minimal): pass task arguments after --, and say stdin is unread
    
    The in-box run parsed a typed --path as its own flag. The help text
    claimed a piped stdin reached the task. The client span task_run now
    carries box_name.
    
    * fix(minimald): map timeout and OOM task ends to 124 and 137
    
    A task box that ran past its timeout or was OOM-killed made the run
    exit 1. The exit log now names the box. The doc comments name the two
    known gaps that still need issues.
    
    * fix(minimald): wait for a closed host's exit before a queued exec
    
    The host loop closes its mailbox a moment before its exit reaches the
    actor. An exec queued in that gap found no live host and no exit, and
    launched the entrypoint again in a box that had ended.
    
    * style(minimald): format the exit settle wait
    
    * fix(minimald): run a task box's entrypoint on three pipes
    
    A box whose spec does not set io.pty_enabled now runs its argv entrypoint on
    stdin, stdout and stderr pipes, not on the terminal. The host reads both
    output pipes into the box log and relays them to the task run as separate
    streams. The run writes the client's stdin to the entrypoint and closes it
    at the client's EOF. The log relay and its CR stripping are gone.
    
    Also in this change:
    - A task run that a signal stopped returns 128 + signal, and a box stopped
      at daemon start returns 137.
    - An exec whose wait for a closed host's exit does not settle is refused
      with exit 2, and nothing launches.
    - Only a task run replaces a hook shell with the box's entrypoint.
    - The staged task env is cleared when ensure_host fails.
    - The two interim comments link issues 3042 and 3043.
    
    * test(minimald): pin task-run pipes, stop codes and settle refusal
    
    The task-run test now pipes stdin to the entrypoint and asserts stdout
    and stderr come back as separate streams. New tests pin 143 for a stop
    by SIGTERM, refuse an exec whose host exit does not settle with no
    second launch, and keep a hook shell when a plain exec reaches it. A
    test seam holds a box's host exit so the settle wait can time out.
    
    * docs(minimal): say min task run pipes stdin, stdout and stderr
    
    The help for min task run now says a piped stdin reaches the task and
    that its stdout and stderr are the command's own, with a pipeline
    example. The held-open stdin test asserts again: the run fails when
    the entrypoint never starts, and stdout stays the task's alone.
    
    * fix(minimald): start a task's pipes entrypoint without a controlling tty
    
    hakoniwa's NewSession runs setsid and then TIOCSCTTY on stdin. On the three-pipe path stdin is
    a pipe, so the ioctl failed with ENOTTY and every real `min task run` exited 125 before its
    command ran.
    
    The sandbox's pre-exec closure now calls setsid itself when the launch asks for a session
    without a terminal, and the launcher asks for that on pipes. The entrypoint still leads its own
    session and process group, so it shares neither with the daemon and cannot open the daemon's
    terminal. A terminal launch keeps NewSession. A stop signals the box by its PID namespace and
    exec injection resolves the supervisor's child, so neither relies on a controlling terminal.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimald): keep a fast task's output for the run that takes it late
    
    The host loop reaped an entrypoint that ended before the run sent TakeEntrypointIo, drained its
    output to the log only, and dropped its mailbox. A task like `echo hi` then gave the run no
    output and the false error that its box runs on a terminal.
    
    After the reap, the host now keeps answering its mailbox while it holds the output, until a run
    takes the streams, a kill arrives or the hold's deadline passes. The reap's own mailbox drain
    answers a take that is already queued instead of dropping it. A host that ended before it
    answered now gets its own error, which says the box ended before the run connected to the
    task's input and output.
    
    Tests: a mock-host test reaps an entrypoint that exits at once, takes its streams afterwards,
    and gets its stdout, stderr and exit code; a daemon test runs such a task through the exec
    channel and gets the same.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * docs(minimal): say min task run does not read stdin yet
    
    The help said a piped or redirected stdin goes to the task. It does not: the task box's
    entrypoint is the in-box `min task run` helper, which sends its run request over the box's
    minenv socket and forwards no stdin, so the task reads none. Forwarding it is a task of its own.
    
    The help now says the task gets no input from stdin yet, that its stdout is this command's
    stdout, and that this command prints its own lines on stderr beside the task's.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimald): drop a task's ignored variables on the entrypoint path
    
    On the task box path the run did not apply the names the client's `[vars] ignore` matched
    (`TaskExec::drop_env`). The in-box `min task run` reads the task's declarations again from the
    uploaded minimal.toml, so a variable the user's policy rejected still reached the task.
    
    The run now stages the drop list beside the variables. The launch leaves dropped names out of
    the entrypoint's variables and records the list for the box. The daemon's in-box run removes each
    name from the task's variables before it fills any `inherit` one, so a dropped name never
    reaches the task, whatever minimal.toml declares for it. A test pins it for a name with a value
    and for an `inherit` name the helper's caller still has.
    
    #3043 stays open only for moving the values onto EnsureHost.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): keep a task box whose run lost its channel
    
    The client removed a task box whenever `bridge_exec` returned Ok and the record still read not
    started or running. `bridge_exec` also returns Ok(None) when the channel closes with no exit
    status, as a daemon or transport drop mid-run does, so the client destroyed a box whose
    entrypoint still ran. That broke BOX-015.
    
    The client now removes the box only for a reported exit code with a record that never left
    running, the daemon's refusal to start the entrypoint. A run with no exit status keeps the box,
    and does not read the record. A test pins both cases.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * docs(minimal): say min task run does not pass its stdin to the task yet
    
    Reword the help sentence, and fix the task run stdin test's comment, which said the client's
    stdin reaches the entrypoint. The entrypoint is the in-box `min task run`, which does not pass
    its stdin to the task yet.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): stop a task box on Ctrl-C instead of destroying it
    
    The run's Ctrl-C handler stayed armed on the entrypoint path, so a Ctrl-C destroyed the task box
    and its record while the task ran ("Interrupted; destroying session ..."). Ctrl-C on
    `min task run` must stop the box (BOX-013), and the box keeps its record (BOX-038).
    
    On the entrypoint path the first Ctrl-C now sends the graceful StopBox that `min box stop` sends
    (SIGTERM, the grace period, then SIGKILL), prints "Interrupted. Stopping box <name>...", and
    exits 130. The run waits for that exit instead of exiting with the stop's code. A task declared
    `interactive = true` has no spec and still owns its box, so its Ctrl-C still destroys it.
    The --help text says so.
    
    Tests: a unit test pins both interrupt lines. A session e2e step interrupts a running task, and
    checks that the run exits 130, that the box lists as stopped, and that `min box rm` removes it.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(e2e): leave the interrupt step's wait loop variable unnamed
    
    The task run interrupt step named a loop variable it never read, which shellcheck flags as
    SC2034 and the shell lint test refuses.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimald): record a task run's dropped names only when it launches
    
    `entrypoint_env::take` recorded the run's `[vars] ignore` names on every
    `ensure_host`, so a run that joined an entrypoint already running replaced
    the names the real launch recorded, and the in-box run read the wrong set.
    `take` now hands the names back, and `ensure_host` records them only on the
    path that launches the host.
    
    The recorded names also outlived the box: nothing removed them. A destroy
    (and a reap, which destroys through the actor) now forgets the box's staged
    and recorded entries. A stop keeps them, as the box keeps its record.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): keep a kept task box's name hold and egress until its reap
    
    A task run released its box's name hold, and with it the box's host_ip
    egress on the VM host daemon, whenever the run returned. A box the run
    keeps may still run (a run that lost its channel never ends it), or it
    ended and keeps its record until it is reaped. Its egress was cut all
    the same: on minvmd, `min task run long-job | head -1` lost the box's
    network while the task ran on.
    
    The run now releases only when it removed the box itself, and an
    interrupt releases only when it destroys the box. A stopped or kept box
    keeps both until its reap releases them. `release_task_name_hold` loses
    its `keep` parameter, which no caller set any more.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvep9ZaSf5pJMB2hQA
    
    * fix(minimald): refuse a second task run while the first relays the task
    
    A second `min task run` that joined a running task box's entrypoint took
    its streams over: the host replaced the first run's relay, so the first
    run got the exit code and no output, and the second got the output.
    
    The host now hands the entrypoint's streams to one run at a time. While
    an earlier run still reads either stream, a later take is refused, and
    that run exits 1 with:
    
      Task <task> is already running in box <box>, and another min task run
      shows its output. Only one run at a time can show a task's output.
    
    A run whose client went away closed its streams, so a later run still
    takes them in its place.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvep9ZaSf5pJMB2hQA
    
    * fix(minimald): give a task run's variables to its entrypoint alone
    
    A task run's resolved variables, which can hold values from the
    caller's shell and secrets among them, reached the launch that starts
    the box's entrypoint as connection variables. The host kept those for
    the box, so every later `min exec` in the box saw them, and the box's
    environment held them too.
    
    They now ride a launch field of their own, which the launcher sets on
    the argv entrypoint's process only. Neither the box's environment nor
    the host's connection layer holds them, so no later exec, attach or
    hook in the box sees them. The start directory rides there as well.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvep9ZaSf5pJMB2hQA
    
    * test(minimald): check the stream-take test's host ends after its kill
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvep9ZaSf5pJMB2hQA
    
    * fix(minimald): exit 2 when a second task run is refused (arch C1)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvep9ZaSf5pJMB2hQA
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    Co-authored-by: Karl Godard <karl@minimal.dev>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
    3 people authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    cd262ac View commit details
    Browse the repository at this point in the history
  2. T63 — Preview a prune with the disk each box holds, and report a stop…

    …ped box's disk (#3078)
    
    * feat(boxes): measure the disk a reap of a box directory frees
    
    BoxDir::disk_bytes counts the allocated blocks under the box directory, once per inode, and leaves out files with a link outside it.
    
    * feat(minimald-rpc): add PreviewPrune and a disk figure on ShowBox
    
    PreviewPrune lists each box a prune matches with the disk it holds. ShowBoxResponse carries disk_bytes, absent and never zero when unknown. The min tests that build a ShowBoxResponse set the new field.
    
    * feat(minimald): preview a prune with the disk each box holds
    
    Prune and the new PreviewPrune share one candidate step, so a preview lists what a prune reaps. ShowBox gives the disk of a stopped or exited box. The daemon measures off the manager loop and logs a box it cannot measure.
    
    * docs(specs): add the prune preview and the disk figure to BOX
    
    BOX-021 gains a preview sub-bullet and BOX-159 a disk figure for a stopped or exited box. Design reasoning says why the figure counts allocated blocks.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    a00f3d2 View commit details
    Browse the repository at this point in the history
  3. test(minimald): prove a task box exits after its client drops (#3085)

    A task run's client goes away mid-task. The task box ends by itself as
    exited with the task's code 3, keeps its exited event, and a new client
    reads its whole output over ReadLog. The daemon log names the box's end
    and its code.
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    1e23607 View commit details
    Browse the repository at this point in the history
  4. T15 — Read the type tables as box entries (#3091)

    * feat(mfile): read type-noun tables as box entries and refuse bad names
    
    [sessions|tasks|agents|services.<name>] read as [boxes.<name>] with the
    matching type (BOX-046). An entry name that is self, not DNS-safe, or
    not unique in the file fails with exit 3 naming the entry (BOX-007).
    
    * fix(mfile): refuse a non-string type and a mismatched sugar type
    
    A type-noun table sets its noun's type. A different type, or a type
    that is not a string, now fails with exit 3 and names the entry.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    f0a3f01 View commit details
    Browse the repository at this point in the history
  5. T121 — Steer a host-address grant box's lookups and admit dns on a VM…

    … host (#3079)
    
    * feat(minimal): admit dns steering for host_ip boxes on a VM host
    
    On a VM host a host_ip box resolves through a steering answerer of its
    own (BEP-086), so the host fact that BEP-017 reads is true for own_ip and
    host_ip there (BEP-081). A host with no VM keeps the refusal.
    
    * feat(minimald): steer a host_ip grant box's lookups on a VM host
    
    A host_ip box whose own spec declares a credentialed upstream with dns
    or both steering resolves through a steering answerer of its own, at a
    loopback address in 127.53.0.0/16 (BEP-086). It answers the box's steered
    names with the proxy's address, NODATA for AAAA, HTTPS and SVCB, SERVFAIL
    while the listener is down, and forwards every other name to the node's
    resolver. Attach fences the answerer to the box's own cgroup leaf with an
    nft chain of its own. Every other host_ip box keeps the gateway.
    
    * fix(minimald): fail closed when a steering answerer cannot be fenced
    
    The plan closes the answerer to every leaf as soon as it binds, and
    attach admits the box's own leaf. A refused fence at either step stops
    the answerer and fails the box. A leaf path with a character an nft
    script cannot carry is refused. A datagram the answerer cannot read is
    dropped, not forwarded to the node's resolver.
    
    * fix(minimald): serve tcp at the steering answerer and free it on drop
    
    A steered box's only resolver is its steering answerer. A truncated
    forwarded reply makes the box retry over TCP, and the answerer bound UDP
    only, so the lookup failed. The answerer now listens on TCP at the same
    address and port, answers steered names with the same verdicts, and
    forwards every other name to the node's resolver over TCP.
    
    The answerer holds the only strong reference to each socket, and its
    tasks hold weak ones, so dropping it frees the address and port at once.
    Drop also cancels in-flight forwards and TCP connections.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    e04514e View commit details
    Browse the repository at this point in the history
  6. Configuration menu
    Copy the full SHA
    98bb3e3 View commit details
    Browse the repository at this point in the history
  7. T16 — Fold the layers into one expanded spec, the same bytes every run (

    #3093)
    
    * feat(boxes): give the loadouts' packages as a spec layer
    
    UserComposer::loadout_layer returns the packages the selected
    loadouts ask for, as the loadout layer of the box spec expansion
    (BOX-064).
    
    * feat(mfile): fold an entry's layers into one expanded spec
    
    expand::expand folds upstream, type defaults, [defaults],
    [defaults.<type>], the entry, loadouts and command-line overrides in
    that order (BOX-048, BOX-064). Lists union, other values replace.
    The spec writes in key order, so one input gives the same bytes on
    every run (BOX-065). A report beside the spec names the layer of each
    value (BOX-161). A bad key exits 3 and names its layer (BOX-066).
    
    * fix(mfile): replace exec and cmdcmd whole, never union them
    
    An argument vector keeps its order and its repeats, so a more specific
    layer replaces it item for item.
    
    * fix(mfile): accept every Box Spec key and ignore client sections
    
    Add the box.toml sections and the type-default keys to SPEC_KEYS, and
    link the issues that replace the interim table. Leave a project's
    package-file-rules, loadout-file-rules and secret-store-rules out of the
    spec and list them in Expansion::ignored_sections for the caller to warn.
    Name the upstream layer as [upstream] in minimal.toml.
    
    * fix(mfile): fold [upstream] as the registry pin, not box settings
    
    Layers::upstream is minimal.toml's [upstream] table. expand folds it as
    the one spec key upstream and refuses a key inside it other than
    repo_url, branch, commit and build_cache_url.
    
    * fix(mfile): accept vars_lenient as a box setting in expand
    
    Session accepts vars_lenient, so the expanded spec must not refuse it
    with exit 3. The valid-keys test now sets it in an entry.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    d9915d4 View commit details
    Browse the repository at this point in the history
  8. T122 — Host-address boxes reach GitHub, end to end (#3095)

    * test(e2e): prove host_ip grant boxes reach GitHub via the proxy
    
    Three session e2e cases for BEP-087 on a VM host with the Box Egress
    Proxy running. A host_ip box with a GitHub grant clones, pushes and runs
    gh api against E2E_BEP_PRIVATE_REPO under the default dns steering with
    no proxy variables, and under proxy_env steering. A host_ip box with no
    grant resolves github.com to a real address. A third case shows the
    grant box's id in min box show, connects it to the proxy's listener,
    and checks that a box with no grant cannot connect after it ends.
    
    Each case prints PASS, or NOT RUN with its gate. On a failure each case
    prints min box audit and the proxy's log lines for each box it started.
    
    * test(e2e): clean up the pushed branch and print (none) in evidence
    
    Retry the branch delete in the host_ip case cleanup and name a branch
    tha
    8000
    t stays. Print (none) when the proxy log has no line for a box. Name
    #2500 as the owner of the macOS lane that runs these cases.
    
    * test(e2e): probe socat and check the refusal text for the no-grant box
    
    The no-grant leg of bep_host_ip_grant_box_reaches_the_proxy_lane passed on any
    socat failure. It now probes /usr/bin/socat first and requires a connect
    failure or a timeout in the error text.
    
    * test(e2e): leave no branch or box behind and check env in host_ip cases
    
    Record the pushed branch before the push runs, so cleanup deletes a
    branch that a failed push wrote. Cleanup skips the delete when
    git ls-remote finds no branch.
    
    Record each box name before the activate, so cleanup destroys a box
    that a failed activate made.
    
    Fail a case when env in the box fails, so an empty list of proxy
    variables is evidence.
    
    ---------
    
    Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
    minimal-foundry[bot] authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    601d4d1 View commit details
    Browse the repository at this point in the history
  9. fix(minimald): answer a create over the re-read fact and place the da…

    …emon through a socket unit (#3026)
    
    * fix(minimald): answer a host-address create's advisory over the create's re-read
    
    The create reply read the classifier fact before the manager's create
    re-read it. A daemon whose start-up probe ran before the placement unit
    moved it into its leaf holds a ProbeUnreadable fact, so a deny-all
    host_ip create was told the box is refused even when the re-read had
    just found the host decides per box. The reply now reads the fact after
    the create, so it carries the re-read's state. A host that still cannot
    read the table keeps the advisory, and the launch's own refusal is
    unchanged.
    
    Refs gominimal/inbox#1205
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(classifier): place the daemon through a socket unit, not a watch on its socket
    
    minimald-place.path watched the daemon's ssh.sock with inotify. On an
    SELinux-enforcing host the policy denies systemd a watch on a sock_file
    under the account's home, so after the first start no daemon start was
    placed, and the path unit still read active.
    
    min finalize-install now installs minimald-place.socket, a socket systemd
    holds at /run/minimald-place.sock for the delegated account, and the
    template minimald-place@.service, which runs the step's --place-listener
    for each connection. The daemon connects once its listener is bound and
    waits up to five seconds for the placer to close the connection, so its
    start-up reading is taken after the placement. The placer still finds
    the daemon from the holders of its listening socket, never from the
    connection. The install removes the earlier path pair, an install that
    still carries it is not done, and --undo and the uninstall advice
    disable only the units a host carries.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * feat(classifier): report a daemon that is not placed in its leaf
    
    A daemon outside its classifier leaf read only as a probe that could not
    read the table, and min finalize-install --show reported the step done.
    
    min finalize-install --show now names a running daemon that is not in
    its leaf, with its pid, and its step places that daemon with the step's
    --pid. The daemon's start line records daemon_placed, and a native
    daemon outside its leaf on a host whose tree is installed logs its own
    warning that names min finalize-install.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * test(e2e): name what still holds the classifier tree when teardown fails
    
    The setup teardown returned non-zero with no word on what kept
    /sys/fs/cgroup/minimald.slice alive. It now prints each remaining cgroup
    under the tree with its members and their command lines.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * docs(minimald): say who owns the placement request socket
    
    The comment called the socket root-owned. The unit sets SocketUser to
    the delegated account with mode 0600.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(classifier): sweep the probe's leftover leaves on uninstall
    
    A daemon stopped in the middle of its classifier effect probe leaves its
    empty boxes/deny/filter-probe-<pid> leaf behind, and the uninstall then
    refused, as if a box still held the tree. The start-up reading now runs
    after the placement, while the daemon is placed, so the probe makes its
    leaf on every start and the window shows up in the native e2e teardown.
    The uninstall now removes empty probe leaves with rmdir. A box's own
    leaf still stops it.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): name the unplaced daemon by the lock's holder, not the file
    
    min finalize-install --show took the daemon's pid from the instance lock
    file's contents, which the daemon writes only as a debug aid and the
    account can edit. A stale or edited pid could name the wrong process in
    the --pid step. The pid is now the lock's holder as /proc/locks lists it
    for the lock file's inode.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): match the lock's device as well as its inode
    
    lock_holder matched a /proc/locks entry by inode alone, so a lock on
    another filesystem with the same inode could name its holder. It now
    matches the major:minor device too.
    
    Refs gominimal/inbox#1206
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(classifier): keep the installed copy when the finalize step refuses
    
    The classifier block moved the new install-host-classifier.sh over the
    root-owned copy before the step ran. A refused run (a connection-mark
    conflict, say) stopped the block before the units, so the old boot and
    placement units stayed while the copy they run was already the refusing
    version, and the next reboot did not restore enforcement.
    
    The block now keeps the previous copy beside it, moves the new one in,
    and runs it. On success the kept copy goes. On failure the previous copy
    goes back (or the new one goes away on a first install) and the block
    exits with the step's status, so the copy and the units stay as they
    were. The step still runs from its installed path, so a refusal's hint
    names a file that exists.
    
    a_refused_classifier_step_keeps_the_installed_copy_and_units runs the
    block under a scratch root with the step stubbed and pins both outcomes.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): find the daemon's lock holder by inode on btrfs
    
    /proc/locks prints the superblock's device, and on a btrfs subvolume
    stat gives the subvolume's anonymous st_dev, so the holder never matched
    there. Match the listing on the inode alone and confirm the holder by one
    of its /proc/<pid>/fd entries statting to the lock file.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(classifier): order the placement service after the boot unit, not the socket
    
    The socket unit ordered after minimald-classifier.service, and the boot
    unit before the socket, closed a cycle at boot: the socket is before
    sockets.target, basic.target comes after sockets.target, and the boot
    unit comes after basic.target. Put After=minimald-classifier.service on
    minimald-place@.service and drop the order from the socket and the boot
    unit.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(classifier): remove the placement socket file when its unit stops
    
    systemd leaves a socket unit's file node in place on stop unless the
    unit sets RemoveOnStop=yes. Set it on minimald-place.socket, so --undo's
    systemctl disable --now leaves no /run/minimald-place.sock, as the
    install test's comment says.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): say the placement unit needs the daemon's socket path
    
    No unit watches the daemon's socket since the placement moved to a
    socket unit. The classifier item's cause and the comments that still
    said a unit watches it now say the placement unit needs the path to
    find the daemon.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * refactor(paths): declare the placement request socket once
    
    minimald and min each declared /run/minimald-place.sock. Move it to
    paths::PLACE_REQUEST_SOCKET beside MINIMALD_LOCK_FILE and use it from
    both crates. resolver.rs no longer carries the path, so the install
    test's exclusion for it goes too.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * test(minimal): run the refused classifier block test through run_confined
    
    The test ran the classifier block with sh after redirecting its paths by
    hand, and the daemon socket it named was still a host path. Render the
    socket under the scratch root too and run the block through #3056's
    run_confined, so the guard refuses the run if the block names any host
    path outside the root.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
    
    * fix(minimal): report a stopped placement socket, and start it in the remedy
    
    `--show` read the classifier item as done once the unit files and their
    wants links were there, so a placement socket that was installed but not
    listening (a boot that dropped its job, or `systemctl stop`) read as done,
    and an unplaced daemon's remedy was `--pid` alone: the next daemon start
    was unplaced again (Bryan's review on #3026).
    
    The item now reads whether the socket listens at PLACE_REQUEST_SOCKET; the
    unit sets RemoveOnStop=yes, so the file is there exactly while it listens.
    A stopped socket is its own state, remedied by `systemctl start
    minimald-place.socket`; with an unplaced daemon as well, the block starts
    the socket and then places the daemon with --pid. A host without
    systemctl is owed no placer.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTxBZ7SjPrDqmzAv4No
    
    * fix(minimald): log a placement request that closed without placing as a warning
    
    systemd also closes a connection it will not serve (over the socket's
    MaxConnections=), and a placer can exit without finding this daemon, so a
    closed connection was logged as "the placement unit answered" while the
    daemon stayed outside its leaf. Only the leaf says whether the request
    placed it: info when it did, warn when it did not.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTxBZ7SjPrDqmzAv4No
    
    * fix(minimal): place an unplaced daemon by its socket, not a pid read earlier
    
    Norrie's re-review of #3026:
    
    - The remedy for a running daemon outside its leaf now runs the step's
      --place-listener for the account's socket instead of --pid <pid>. It
      finds the holder when the block runs, so a reused pid is never moved,
      and it keeps the guards --pid lacks: a holder of another uid, or one
      already under the slice, is left alone.
    - Detection matches it: a lock holder whose cgroup is anywhere under the
      tree (a box leaf among them), or cannot be read, is not reported as
      unplaced (stands_in_tree).
    - With both causes present, `--show` lists them on two lines instead of
      one 47-word sentence; the JSON keeps them on one line.
    - The create reply's comment and its test no longer say the launch
      refuses a deny-all box an unplaced daemon starts; that box runs
      unenforced.
    - The placement-request doc states why the daemon connects to a socket
      in /run, without narrating the path unit it replaced.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTxBZ7SjPrDqmzAv4No
    
    * fix(minimald): state the host fact the create's own gate read
    
    The foundry review of #3026: the create reply read the process-global
    host-address fact after the manager's create returned, so another create
    or launch that refreshed it in between could make the reply state a fact
    its own create never saw. The manager's create now returns the fact its
    gate read (create_host_ip_fact), the CreateBox message carries it back
    on a oneshot, and the RPC handler states that fact through the new
    create_box_reporting_fact. The existing create_box_with_spec and
    create_fresh_box_with_spec keep their signatures.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTxBZ7SjPrDqmzAv4No
    
    * fix(minimal): check the names the unplaced-daemon remedy spells
    
    Norrie's third review of #3026 (F1): the --place-listener remedy for a
    running daemon outside its leaf put `operator` and `socket` into the
    root block on a branch that returns before the character check the
    install path applies. A --minimal-dir with an apostrophe broke the
    script handed to `sudo sh`, a crafted one could run a command as root,
    and one with a space rendered a remedy that never matched the socket.
    
    The check moves into unsafe_name_cause, which both the install arm and
    the unplaced-daemon arms use. An unsafe name with an unplaced daemon is
    now the same Cannot item with the same causes and no block; a healthy
    install read under such a name stays done, and starting a stopped
    socket names neither. New rows cover the crafted path, an apostrophe, a
    space and an empty name, with and without a stopped socket.
    
    Also from the review:
    - N1: the create reply's comments say it states the fact its own
      create returned, and when that read falls back to the stored fact.
    - N4: the block's printed comments use plain words for the cgroup and
      the socket owner, drop the inotify narration, and name the placement
      socket consistently.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_016CYvTxBZ7SjPrDqmzAv4No
    
    ---------
    
    Co-authored-by: Karl Godard <karl@minimal.dev>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
    Co-authored-by: agent-137 <281830107+agent-137@users.noreply.github.com>
    4 people authored Nov 6, 2026
    Configuration menu
    Copy the full SHA
    fd6679a View commit details
    Browse the repository at this point in the history
Loading
0