Skip to content

Gate 5: a modeled microvm_slot_start that starts the shakedown slot's controller once and reads that invocation's receipt - #12178

Merged
gunbai-bot[bot] merged 63 commits into
mainfrom
session/quiet-koi-746-slot-start
Sep 24, 2026
Merged

gunbai-bot[bot] merged 63 commits into
mainfrom
session/quiet-koi-746-slot-start

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Gate 5 of the microVM real-run plan: nothing issued systemctl start for the slot unit. microvm_controller_install writes the locus and the gunbc-microvm-slot@ template and starts no instance, so every real-run receipt (5a job, 5b second attempt, 5c cancel, 5d 28 GiB floor) had no first step.

What changes

  • New module gunbc.runner_microvm_slot_start (microvm_slot_start_ci_wet):
    1. Checks the job's host binding (gunbc.runner_microvm_host_ready microvm_host_binding_wet, the one the boot probe reads) against the shakedown slot's host. A dispatch pinned elsewhere refuses by name.
    2. Starts the unit derived from gunbc.runner_slot_allocation srv1_microvm_shakedown_slot. The instance is never a dispatch input. The start goes through the modeled privileged operation SystemdStartUnit, whose argv the sudoers match is derived from. It starts a root-owned unit that microvm_controller_install wrote, so the job chooses no byte the root process evaluates.
    3. Captures the unit's systemd InvocationID and waits for inactive. The bound is the slot unit's own microvm_slot_unit_timeout_stop(): the VMM deadline plus every teardown bound, the same sum systemd enforces.
    4. Reads the controller receipt at the path that invocation names (gunbc.runner_microvm_slot_controller slot_controller_receipt_path), so an earlier start's receipt cannot stand for this one.
  • The step succeeds only when this invocation's receipt was read, whatever it says. A controller that refused or quarantined is a finding, and the receipt is the evidence. Every other arm fails the step, because no receipt of this attempt exists: wrong host, refused start, unread invocation, still active at the bound, or unreadable receipt.
  • Fleet-converge mode microvm_slot_start. It is a step in the existing fleet-converge job, alongside microvm_boot_probe, so no new job is added. Its timeout is derived from the same stop bound. A receipt-upload step is added. fleet-converge.yml is regenerated.
  • The start command is gunbc.executor_privileged_apply executor_privileged_operation_command, the existing typed builder over the same executor_privileged_operation_elevated derivation the sudoers match reads. Nothing new is admitted on argv_command.

What running it will show today

The controller refuses at the JIT mint: #12119 makes it pass RunnerGroupRestrictionUnobserved until bright-dove-353's #12155, #12172 and PR-C land. It may refuse earlier on the slot-network receipt (conntrack-tools is missing on srv1). Either way the controller receipt names the stage, which is the "exercised to the refusal arm" evidence the brief asks for before the network lands.

Evidence (fresh claim_batch)

  • New runner_microvm_slot_start_witness_test: 2/2 PASS.
    • Admission: only a completed start with this invocation's receipt read admits. The reds are the other five arms, including completed but receipt unreadable.
    • The started unit is the shakedown slot's own instance.
  • workflow_dispatch_input_witness_test (enumerates the modes): 30/30 PASS.

🤖 Generated with Claude Code

Brian Searls and others added 30 commits September 21, 2026 20:54
…nPID, the 28 GiB shakedown slot row, the reservation-authorized planner arm, and the controller's release locus

Deliverable 1 of the microVM slot end-to-end item: gunbc-microvm-slot@.service
(Type=exec, root, ExitType=main, KillMode=control-group) execs
gunbc.runner_microvm_slot_controller microvm_slot_controller_main, which composes
the readiness store, the host CAS reservation re-read, the cell binding, the
JIT mint, the per-slot guest shape, the reservation-authorized launch plan and
the durable network receipt into one call of run_controller. The
controller_main_pid_consumer_frontier row is dissolved per its trigger.

The unit and the controller's release locus (gunbc + sources + Firecracker +
jailer, root-owned) are installed through the live_deploy release locus,
parameterized by owner rather than mirrored, as a new fleet-converge mode
microvm_controller_install (a new HostEffect arm, an apply wet entry with
readback).

Deliverable 2: the srv1 shakedown slot (srv1-13) carries a 28 GiB MemoryMax,
MemoryHigh at the ceiling and MemorySwapMax=0 through
gunbc_runner_slot_desired_for, consumed per slot by the cell digest, the slice
directives, the sudoers grants and the guest shape; the fleet row does not
move and the expecting-red production-shape witness stays red. The width cost
is derived in memory_admitted_width_of_envelope and the change is declared as
gunbc.rung_drop microvm_shakedown_slot_row_above_fleet_row.

Parent decision 2026-09-21: the planner gains a sealed, trust-domain-selected
reservation-authorized arm (no production route constructs an ExecutionGrant),
with the grant producer declared as its frontier. Two further frontiers are
declared where owned: the App control-plane observation the mint needs, and
the admin-edge ConvergedSlotNetwork receipt at
runner_microvm_converged_receipt_path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-install job

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nted mover (run 35655377521 refused rsync as the job user)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stall names its source relative to the job cwd

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ROOT), and both release-locus units set it from one row

Parent ruling 2026-09-21: cli_run's workspace_root_from scaffold named its own
dissolution -- release bins receive checkout-root at spawn -- and the first start of
gunbc-microvm-slot@srv1-13 on srv1 (exit 101, 'not inside a git checkout') is the
population that needs it; the approval broker's serve unit is dead on srv1 for the same
reason. One env name, read at one site (workspace_root_resolve, consumed by
workspace_root and process_workspace_root); when set it is the authority and the walk
is not consulted, refusing typed unless the path holds dag/ and the locus's tree
receipt; when unset the walk is exactly as before. Two executed controls in
workspace_root_discovery_tests. v1 admission (gunbc.v1_maintenance_standing): this
serves the emitted-binary door the seed already scaffolded, not v1 growth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…controller; one Filesystem authority; dissolve markers on the two piped-install strings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… cell (the seed refuses an unlimited cgroup: HostBudgetUnreadable on srv1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s installed and the write is checked, the stop slack is a declared row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed at 8 GiB on srv1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ced slice (the seed filled each bound and was killed at it on srv1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ric_execution_slot_identities, not a count

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w plus cache headroom (24 GiB); the planning-budget request is dropped as inert (RSS filled 8 and 12 GiB bounds alike on srv1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es a body annotation)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lice (parent ruling 2026-09-22); the controller figure is homed in the width authority and cites the attribution lane's frontier rather than being tuned to the symptom

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t discharged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…an answer for UnmeasuredRoot

Review 69931 (DESIGN §5, fabricated default): whole_corpus_compile_public_root_demand_peak
matched the demand row and answered byte_size(0) on the UnmeasuredRoot arm. That arm is
unreachable only while the roster row stays MeasuredForRoot, and this change made the figure
load-bearing twice over -- it sizes the controller slice's MemoryMax and it is a term in
gunbc_microvm_shakedown_slot_charge_bytes, which host_memory_admitted_width derives srv1's
admitted slot count from. A zero there is not a stop: 0 + 8 GiB is a plausible cgroup bound
(measured fatal -- OOM-killed at 7.9G under an 8 GiB bound) and a 16 GiB drop in the charge
hands srv1 two extra admitted slots.

There is no non-fabricated answer for that arm, so the fix is to have no arm. The measured
figure is hoisted to whole_corpus_compile_public_root_peak, the demand row is BUILT from it,
and the one process-sizing consumer reads the row directly. If this root ever becomes
UnmeasuredRoot the row is deleted with the arm and every consumer refuses to compile rather
than reading a default.

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag --function
the_controller_unit_joins_its_own_bounded_no_swap_slice_outside_the_cell -- the closure
typechecks and the claim evaluates true (the host's trailing refusal is the ProcessExit
wrapper convention, not the claim).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion in the roster

The floor refused this PR's changed-witness set with an inhabitance error at two call sites in
test.claim.typed_argv_exec_realization_witness_test: `privileged_commands.skip(n: 4).first()`
produces Optional<PrivilegedCommand> and was handed to a `cmd: PrivilegedCommand` parameter.

The defect is latent on main, not written here; what this change did was reach it. The roster is
derived (grounded_principal_privileged_commands maps the principal's sudo_grants), so altering
fleet membership and the sudoers projection pulls this witness into the floor's changed-witness
set. Checked before attributing it to the wall: gunbc#12046's floor is SUCCESS on the same wall
and gunbc#12045 refuses it in that PR's own changed file, so the wall is sound and this site is
simply now within reach.

Index 4 was the deeper fault and the type error was its symptom. A positional citation into a
derived list re-points silently whenever the roster moves, and it never said WHICH grant was
meant -- both claims assert the answer is the tailscale one. The selector now asks for it by
command_path against fleet_tailscale_binary_path, the same authority the roster derives the path
from, and each claim matches Present/Absent with Absent returning false: a roster that no longer
carries the grant fails the claim rather than skipping it.

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/typed_argv_exec_realization_witness_test.dag --function
witness_tailscale_roster_selects_grant_list_not_execute_probe -- the closure resolves with no
inhabitance error and the claim evaluates true, so the selector finds the grant rather than
passing through the Absent arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he seed, so the seed stops transcribing them

Review 69990 (DESIGN §3/§5): the spawn-root env name and the tree-receipt name each had two
authors -- the .dag rows the emitter renders into the release-locus unit, and a `const` of the
same spelling hand-written in cli_run.rs -- and the only thing standing over them was
`gunbc_workspace_root_env_name == "GUNBC_WORKSPACE_ROOT"`, a row compared to a copy of its own
value. That check stays green when the row is edited, which is precisely §5's tell. If either
side moved alone every release-locus unit would exit 101 in workspace_root at start, the failure
the spawn arm exists to repair.

NOT the prescribed remedy, and the substitution is deliberate. The review asked for a witness
reading cli_run.rs, on the precedent of the hand-Rust equivalence witnesses. Two reasons against:
a check over two independently-authored literals is a drift DETECTOR that fires after the fork
already exists, and a new tree-reading witness is a new WET witness, which
v2.workflow.floor_changed_witness blocks for any identity the PR touches (RouteGapBeforeVerdict
and DeclinedDiscoveryExcluded both map to PlannedWithoutTerminalVerdict) -- so the prescribed
form likely cannot reach a verdict on this PR at all.

So the fork is removed instead of watched. gunbc.release_locus_seed_constants_emit projects both
rows into release_locus_seed_constants_generated.rs, registered in the generated-artifact roster
and its drift wall, and cli_run.rs consumes them rather than declaring its own. The join is now
the Rust compiler plus the drift gate: a .dag spelling moves the seed at the next regen, and a
hand edit of the generated file is refused. Construction over validation, and the same argument
gunbc.evaluation_budget_consequence_emit already records for refusing a runtime read of the .dag.

The surviving claim asserts the projection CARRIES each row's value. The emitter never spells
either literal, so it goes red if a row moves and the projection does not.

Receipts, both executed:
- ctrl-build --remote -- bash -lc 'ls -la src/v1/stage0/src/release_locus_seed_constants_generated.rs
  && cargo check -p v1-compiler --lib' -- the file is present (500 bytes) and the seed compiles
  clean against the generated consts. The ls is a control: an earlier run reported Finished over
  an UNTRACKED generated file that the mirror could not have carried, and that green proved
  nothing.
- gunbc run --entry dag/test/claim/cli_run_workspace_root_hand_rust_witness_test.dag --function
  workspace_root_spawn_arm_names_its_env_receipt_and_controls -- true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arms the new artifact variant owed

TWO THINGS, the second a repair of the first commit in this pair.

1. workflow_dispatch_choice_input_projects_options_list discriminates at ONE interface --
workflow_trigger_entry turning a WorkflowDispatch with a choice input into a yaml mapping whose
`mode` carries an `options` sequence. It reached that interface by forcing
fleet_converge_workflow and taking its first trigger, which constructs every DispatchInput's
description (one is a multi-thousand-character string) for a fact about none of them: 528572 eval
steps against a 72300 budget, rising with every input any lane adds. It now supplies the trigger,
which is the construction two sibling claims in the same file already use.

The predicate was ALSO fusing two subjects, which is why supplying alone did not work: it
hard-wired fleet_converge_mode_options, so it asserted the PROJECTION and the CONTENT at once.
Only the first is a fact about workflow_trigger_entry, and the second is true by construction --
the workflow literal writes `options: fleet_converge_mode_options`, so comparing against that row
restates what the source already carries (DESIGN §5), and the roster's own coherence is
established by every_dispatch_option_is_a_wire_value_of_the_vocabulary. The expected list is now
a parameter, so the discrimination is intact: all options reach the yaml as strings, none dropped
and none added.

The pairing obligation is named on the carrier: the real producer is still run end to end by
fleet_converge_workflow_has_build_job_needs_release_bins and the rendered-yaml claims, so
deleting them would leave this supplied input unpaired.

2. ReleaseLocusSeedConstantsGeneratedRsArtifact left two GeneratedArtifact matches non-exhaustive
-- gunbc.ledger_row_coherence and gunbc.instruments.docs_projection_gate -- so the previous commit
does not resolve. The exhaustive-match wall is what caught it; nothing I ran before pushing
reached either module, which is the defect in the receipt rather than in the wall. Both arms are
added.

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/workflow_dispatch_input_witness_test.dag --function
workflow_dispatch_choice_input_projects_options_list -- the closure resolves and the claim
evaluates true. The discriminating red is recorded in the same pair of runs: with the predicate
still hard-wired to the fleet roster the supplied two-option trigger returned FALSE, so the
parameter is load-bearing rather than decorative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…authorable red, and the third non-exhaustive match

Review 70028, both findings, plus the resolution break that was the actual CI blocker.

1. THE CENSUS ROW WAS FALSE. cli_run_workspace_root_discovery_loc_delta_net binds
`v1_compiler.cli_run workspace_root_from` at WholeDeclaration and stood at 12 -- the
.git-ancestor kernel alone -- while this change grows exactly that declaration. A row left at 12
is the parallel ledger DESIGN §6 refuses: it states a figure the tree contradicts, and its only
consumer (`... > 0`) cannot see it move. Re-derived to 70, and the INSTRUMENT is named on the
carrier rather than the count alone (§6): added non-blank lines of this scaffold's hand-Rust in
cli_run.rs outside its #[test] module, 35 + 20 + 3 = 58, with the 42 test lines excluded on the
same basis the original 12 excluded its own. 12 + 58 = 70.

2. TWO CONJUNCTS WERE DECORATION INSIDE AN OTHERWISE REAL CLAIM, and the review is right that
this is the same objection the claim's own note raises one conjunct earlier.
`string_contains(s: <row>, pattern: "walk_is_not_consulted")` compares a row to a slice of its own
value and stays green after the seed test it names is renamed or deleted. Deleting them outright
would have left both discriminator rows with no consumer at all (§3c), so the claim now asserts
the scaffold names FOUR DISTINCT, non-empty controls. That has an authorable red and it was RUN:
setting the refusal row to the authority row's value -- the copy-paste that makes an unwritten
control look covered -- returns false; restored, it returns true.

What the replacement does NOT establish is stated on the carrier: that the seed declares those
names. No claim here can, because a witness reading cli_run.rs is a new WET witness and
v2.workflow.floor_changed_witness blocks any such identity its own PR touches.

Also corrected: this change's own de-fork had made the scaffold's prose false -- it still said
"the seed transcribes both spellings" after the seed had stopped transcribing them.

3. THE FLOOR BLOCKER WAS A THIRD NON-EXHAUSTIVE MATCH, generated_artifact_emit
artifact_extra_valid, a SECOND match over GeneratedArtifact in a file I had already edited once.
My earlier search grepped a sibling variant while excluding the files I had touched, which is why
it found two sites and not three. The five sites are now enumerated by arm count;
generated_workflow_provenance carries `_ => none` and needs nothing. The cost rows this run was
meant to re-measure were never reached, so those figures are still owed.

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/cli_run_workspace_root_hand_rust_witness_test.dag --function
workspace_root_spawn_arm_names_its_env_receipt_and_controls -- true, and false under the planted
duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fold that cannot says why

Review 70050. The sharpest form of the finding is the one worth answering: this change MINTS a
ByteSize authority (whole_corpus_compile_public_root_peak) and then unwraps it on the next line to
add a bare Int, which gunbc.runner_microvm_slot_unit re-wraps one module later. std.measure
measure_add is the existing surface for adding two measures of one quantity and scale, so reaching
past it is DESIGN §2's re-invention of a concept that already has a home. That the sibling
gunbc_runner_slot_memory_max_bytes sits on main in the same file is prevalence, not precedent.

SUMMED IN THE CARRIER, where the carrier is this change's to move:
- microvm_controller_slice_memory_max() returns ByteSize via measure_add over the peak row and
  microvm_controller_slice_cache_headroom (now a ByteSize row). The pass-through in
  runner_microvm_slot_unit that re-wrapped the Int is DELETED rather than retyped -- two names for
  one fact is the §3 fork, and the module imports the authority directly now.
- gunbc_microvm_shakedown_slot_memory_max is a ByteSize row; the two slice directives that wrapped
  it with byte_size(...) read it directly.
- gunbc_microvm_shakedown_slot_charge() returns ByteSize via measure_add.
- microvm_slot_unit_timeout_stop() returns Second via measure_add over the five bound rows. Second
  is Measure<Time, One, Nat>, so the same surface applies verbatim; the projection to a scalar moved
  to microvm_slot_unit_timeout_stop_sec, which is the one place the rendered directive needs it.

TAGGED RATHER THAN CONVERTED, once, with the reason on the carrier: memory_admitted_width_of_envelope
divides by gunbc_runner_slot_memory_max_bytes, an Int row standing on main and not this change's to
move. Converting the fold alone would take a ByteSize, unwrap it to divide by that row, and re-wrap
-- the same carrier drop relocated rather than removed. It carries
`🟡 dissolve-on: feature:fleet-slot-row-on-a-measure-carrier`, naming measure_fit_count_floor as the
surface that owns the quotient and its zero-divisor guard once the fleet row carries ByteSize. This
is the second arm the review itself offers.

Two annotations that cited the renamed symbols are corrected in the same change; a citation that no
longer resolves is a §3 defect, not a cosmetic.

Receipt: all four claims over the changed arithmetic execute true --
the_controller_unit_joins_its_own_bounded_no_swap_slice_outside_the_cell,
the_unit_stop_timeout_covers_the_controllers_declared_bounds,
the_shakedown_slot_carries_28_gib_no_swap_and_no_throttle_line_while_the_fleet_row_stands,
the_shakedown_slot_is_charged_its_cell_plus_its_controller_before_the_fleet_row_divides. Running all
four rather than only the one naming a renamed symbol is deliberate: twice in this lane a receipt
whose closure did not reach the consumers missed a break that the floor then found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ledger-Repair-Judged: docs/design-rung-drops.md
Heal-Candidate-Run: 35699456339
…with the new generated artifact

THE LAST COST BLOCKER. fleet_converge_workflow_has_build_job_needs_release_bins is about the job
partition and its needs edges, and it read them off fleet_converge_workflow -- which forces the
WHOLE row, every job's steps AND every DispatchInput description under `on`, one of which runs to
thousands of characters, to establish seven ids: 521845 eval steps against a 72300 budget,
identical across two separate runs (so the claim's own deterministic work, not contention), and
rising with every job or input any lane adds.

The seven jobs were written inline in the Workflow literal, which made the workflow the only route
to the fact. They are now `fleet_converge_jobs()` in the owning module and the workflow reads it.
That is a single authority with the workflow as one consumer, not a test affordance bolted on: the
roster is where the jobs are declared, and there is no second list to drift.

THE COMPANION FIX IS SECOND-ORDER AND IS WHY THE GATE WAS RUN AT ALL. Registering
ReleaseLocusSeedConstantsGeneratedRsArtifact and emitting its .rs was not sufficient: .gitattributes
is ITSELF derived from the artifact roster, so a new artifact moves it too, and the committed file
was stale by exactly the one line that binds the new path to the refusing merge driver. Regenerated
through main_wet on the gate rather than hand-edited, which is what the finding itself instructs.

Receipts:
- gunbc run --entry dag/test/claim/workflow_dispatch_input_witness_test.dag --function
  fleet_converge_workflow_has_build_job_needs_release_bins -- true.
- gunbc run --entry dag/gunbc/instruments/generated_artifact_gate.dag --function main -- the only
  finding was .gitattributes. fleet-converge.yml is ABSENT from it, which is the receipt that
  matters for the roster move: lifting the jobs into a function is emission-neutral, so the
  committed workflow yaml is unchanged.
- The eval-step figure for the roster-reading form comes from CI; this change does not claim it
  clears 72300 until that run reports it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he one its shape suggests

Review 70080. Three annotations -- gunbc.live_deploy.emit release_locus_tree_copy_steps,
gunbc.runner_microvm_slot_unit, gunbc.runner_microvm_slot_controller -- justified the root-owned
locus as buying "a tree root execs that the job user cannot write" / "root must not exec a binary
the job user can write". On srv1 that protection does not hold, and the tree already says so:
ghrunner holds NOPASSWD over a PATH-UNRESTRICTED /usr/bin/install on every host carrying
/etc/sudoers.d/gunbc-deploy, install(1) is the very mover the install step elevates with, and the
class is rostered at gunbc.rung_drop the_job_user_holds_a_path_unrestricted_root_grant and
gunbc.recurring_failure_mode the_grant_a_frontier_refuses_is_already_held_from_another_authority.

That is DESIGN §4b(1) inflation in this change's own prose: a class's rung is the MINIMUM across
its in-scope paths, and these sentences cited the strongest while the granted-mover path stayed
silent. It was also inconsistent with this same diff twelve lines away, where the guest-image note
states the shakedown makes no isolation claim rather than implying one.

Each carrier now separates the two facts. WHAT THE LOCUS BUYS: the unprivileged-by-default write
path is removed -- nothing lands bytes there without a granted privileged mover, so an ordinary job
step, a stray tool or a mistaken relative path cannot -- and the unit execs this tree's VMM rather
than the job user's HOME copy. WHAT IT DOES NOT BUY: a locus the job user cannot write, because the
same principal reaches it through the same grant. The standing drop is named at each site, with the
statement that this change adds no grant and removes none, so the stronger claim becomes true when
that drop retires rather than when this locus lands.

Comments only; no declaration, value or emitted byte moves.

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag --function
the_slot_unit_execs_the_controller_entry_as_main_pid_with_the_declared_teardown -- true, and the
annotation-grain sweep over the three files reports no body-grain block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…racted against the measurement

MEASURED, AND THE HYPOTHESIS IS FALSIFIED. The job roster was extracted to bring
test.claim.workflow_dispatch_input_witness fleet_converge_workflow_has_build_job_needs_release_bins
under its floor budget, on the reading that `on` -- whose DispatchInput descriptions run to
thousands of characters each -- dominated the row. Two required-floor runs:

  whole Workflow row : 521845 eval steps
  roster             : 521510 eval steps
  budget             :  72300

335 steps, 0.06%. The cost is constructing the seven Job values themselves, step bodies included;
a List<Job> projection forces every one, so reading the roster cannot avoid it. The suspect was
wrong.

The extraction is KEPT, on the ground that survives: the jobs were inline in the Workflow literal,
which made the workflow the only route to the job partition, and the roster is now that authority
with the workflow as one consumer. What is retracted is the cost claim -- both annotations now
carry the two figures and state the refutation, because an annotation asserting a saving this lane
measured away is the same overclaim review 70080 flagged in this change's prose one commit ago.

What would actually be cheap is named rather than implied: a roster of ids and needs edges ALONE,
consumed by the seven job functions for those two fields. That restructures all seven and is not
attempted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, consumed by the seven builders

Parent ruling, item 2. Each of the seven job builders hardcoded its own `id` and `needs`, so the
graph -- which jobs exist, and which must wait for the release binaries -- was authored in seven
places and owned nowhere. FleetConvergeJobEdge is that graph, and all seven builders now take their
needs from fleet_converge_job_needs(id:); no literal needs list survives in a builder.

THE LOOKUP REFUSES RATHER THAN INVENTS. An absent id would otherwise fall to an empty needs list,
which is the one wrong answer that still emits: a host-touching job with no edge to `build`
installs whatever binaries happened to be on the host. FleetConvergeJobEdgeAbsent carries the id
and the needs projection renders a REFUSED line instead of a dependency.

WHY THIS IS NOT THE PREVIOUS ATTEMPT REPEATED. Lifting the JOBS into their own list bought 335
eval steps of 521845, because forcing a List<Job> forces every Job including every step body --
the projection WAS the construction, now filed as gunbc.recurring_failure_mode
extracting_a_collection_whose_elements_are_the_cost. These edges are ids and lists of strings built
independently of any Job, so a consumer reading the graph forces no job body. That is a difference
in mechanism, stated before the measurement rather than hoped for after it.

The claim reads the edges; its inhabitance obligation stays with the rendered-yaml claims in the
same file, which run the real workflow end to end -- so what is established is the graph the
workflow EMITS, not the graph a builder was told.

Receipts:
- gunbc run --entry dag/test/claim/workflow_dispatch_input_witness_test.dag --function
  fleet_converge_workflow_has_build_job_needs_release_bins -- true.
- RED, run: dropping the microvm-controller-install edge row fails the claim. Reported honestly --
  it fails as an evaluation error on the now-absent index rather than a clean false, so the count
  conjunct is carrying part of that red.
- gunbc run --entry dag/gunbc/instruments/generated_artifact_gate.dag --function main -- ZERO
  findings, so fleet-converge.yml is byte-identical with every needs list now computed through the
  lookup. That is the receipt that matters: the refactor is emission-neutral.
- The eval-step figure comes from CI. This change does not claim it clears 72300 until that run
  reports it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 70098. outcome_is_success was an exact re-mint of gunbc.command_runner
process_outcome_admitted -- same three arms, same verdicts -- in a module that already imports from
that very module, so the canonical name was one import member away. DESIGN §3: a second name for
one concept, and §2's test that net concepts must not grow by re-invention. The canonical function
even documents this exact caller shape (a `test -f` whose product is the exit status and whose
stdout nobody wants), so the re-mint did not even carry a different rationale.

Deleted, with both call sites -- path_is_executable and path_exists -- now asking
process_outcome_admitted.

The extdeps.shell.exec import went with it: ProcessOutcome and its three variants were reachable
from this module ONLY through the deleted function, so keeping the line would trade a §3 fork for
an unused import. (The neighbouring bare `import extdeps.shell` is pre-existing and untouched.)

Receipt: gunbc run --source-root dag --source-root src/v2 --entry
dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag --function
the_controller_selects_only_a_fabric_member_out_of_its_instance_name -- the closure resolves with
the import removed and the claim evaluates true.

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

Parent ruling: a control that DIES is weaker evidence than one that ANSWERS false, because an
evaluation error is also what a typo, a renamed field or a broken fixture produces -- so the red
stops discriminating the thing it was built for.

THE PRESCRIBED REMEDY DOES NOT APPLY HERE, and the evidence was already in hand: the count
conjunct was ALREADY first (`edges.length() == 7 && edges[0].id == ...`) when the control died on
the absent index. `&&` in this evaluator does not spare its later operands, so no reordering turns
that into a false.

So the claim is TOTAL instead -- it reads no index at all. The projected id list is compared
against the expected spelling (count, membership and order in one structural comparison) and the
edge property is folded with `all`: build needs [], every other row needs [build]. Same content as
the indexed form, nothing weakened.

Receipts, both run:
- green: the claim returns true.
- red: dropping the microvm-controller-install edge row returns FALSE -- a clean answer, where the
  indexed form produced an evaluation error under the same edit. An eval error from this claim now
  means something else genuinely broke, which is the separation the ruling asked for.
Roster restored to seven rows after the control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tionID, and property reads are typed (review 70683)

systemctl start on an active unit is a no-op that exits zero, so the step could have
read an EARLIER start's receipt as this dispatch's. The unit must now be inactive
before the start (SlotStartAlreadyActive names the running invocation) and the
InvocationID after the start must differ from the one read before it
(SlotStartInvocationUnchanged). ShowProperty failures are UnitPropertyUnread with
their detail instead of an empty string, carried into exec_main_status and into
SlotStartInvocationUnread. Witness reds gain both new arms.

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

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Review 70683: both findings held and are fixed at d1d6b2a.

  1. An earlier start's receipt could stand for this one. Now refused by construction. microvm_slot_start_on_its_host refuses SlotStartAlreadyActive if the unit is active before the start, naming the running invocation, and nothing is started. After the start, the InvocationID must differ from the one read before it (empty if the unit never ran). If it didn't change, the result is SlotStartInvocationUnchanged. Only a new invocation reaches the wait and the receipt read, so the receipt path is always this start's.
  2. Failed property reads looked like empty values. show_property now returns UnitPropertyRead { value } | UnitPropertyUnread { property, detail }. exec_main_status carries that reading, rendered as UNREAD(...) rather than a blank. SlotStartInvocationUnread carries the failure's detail and whether it was before or after the start. An empty InvocationID after the start is also refused explicitly.

The witness red set now includes both new arms. 2/2 PASS, typechecks.

— sent from quiet-koi-746

… projected only when rendered (review 70696)

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

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Review 70696: fixed at 32a178a. SlotStartStillActiveAtBound now carries bound: Second, filled with microvm_slot_unit_timeout_stop() directly. second_count is called only in microvm_slot_start_text when the bound is printed. The witness supplies second(count: 10), and both claims PASS. The step's timeout_minutes stays an Int because that is the GitHub Actions field, derived from the same Second at that edge.

On the previous head's floor red (PureProducerShareRowModuleUnframeable in test.claim.namespace_xl2_rehearsal_census, AmbiguousBareNameRead sites=2): this PR does not touch or import that module, and all 7 of its claims PASS on this branch with a fresh claim_batch. This push reruns the floor; if it recurs I'll trace which bare name it reports as ambiguous.

— sent from quiet-koi-746

…operation_command instead of a duplicate of it

The previous heads re-authored executor_privileged_operation_command, same name and
same body, in gunbc.executor_privileged_operation and admitted it on argv_command. Two
declarations of one name is a second authority for one fact (DESIGN 3), and the floor
refused on it (AmbiguousBareNameRead, surfaced as PureProducerShareRowModuleUnframeable
in namespace_xl2_rehearsal_census). Both files are restored to main's bytes and the slot
start imports the existing builder.

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

gunbai-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Root cause of the floor red on the last two heads, fixed at 1919c8a: my own duplicate. I had re-authored executor_privileged_operation_command (same name, same body) in gunbc.executor_privileged_operation and admitted it on argv_command, when gunbc.executor_privileged_apply already owns it. Two declarations of one name is the second authority DESIGN 3 forbids. The floor refused on it: the ambiguous bare read surfaced as PureProducerShareRowModuleUnframeable in namespace_xl2_rehearsal_census. I confirmed it was this PR's by the same refusal being absent from five concurrent main-queue and branch runs. Both files are now back to main's bytes, the slot start imports the existing builder, and the PR body is corrected.

— sent from quiet-koi-746

…sed unit-name witness is deleted and the step timeout rounds up (review 70720)

fleet_converge_workflow imported gunbc.runner_microvm_slot_start for one string, which
pulled 17 modules (the controller, lifecycle, guest image, launch planner and more) into
the workflow emitter's closure; the floor refused on that head. The receipt path now
lives with the unit's on-host layout in gunbc.runner_microvm_slot_unit, which main's
workflow closure already contains; the workflow and ci_spec read it there (ci_spec no
longer spells it a second time). The fleet workflow's closure now adds nothing over
main's. Review 70720: the_started_unit_is_the_shakedown_slots_own_instance compared a
function to its own body and is deleted; the step timeout uses ceiling division as its
annotation says.

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

gunbai-bot Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Review 70720 is fixed at 4a9d139. The same commit makes a second attempt at the floor red.

  • The witness that could never fail is deleted. the_started_unit_is_the_shakedown_slots_own_instance compared microvm_slot_start_unit() with its own body. A hand-copied literal would only be a change detector (DESIGN 5's oracle rule). The admission witness, with its seven red arms, is the claim this module owns.
  • Timeout rounding. It is now (second_count(...) + 59) / 60, i.e. ceiling division, as the annotation says.

Floor. The duplicate builder in 1919c8a was a real defect, but it was not the only one: the refusal persisted. The workflow emitter imported gunbc.runner_microvm_slot_start for one string, the receipt path. That import pulled 17 modules into its closure that main's does not carry: the controller, lifecycle, guest image, launch planner and more. The path now lives with the unit's on-host layout in gunbc.runner_microvm_slot_unit, which main's workflow closure already contains. The workflow and ci_spec read it from there, so ci_spec no longer spells it separately. Measured with claim_batch's --print-entry-closure: the fleet workflow's closure now adds nothing over main's. This head's floor will say whether that was the cause.

— sent from quiet-koi-746

…iguousBareNameRead; the boot-probe comment returns to its own function (review 70740)

The floor's refusal named its sites on the lines after the refusal: gunbc.runner_guest_image
and gunbc.runner_microvm_host_ready read Filesystem bare, and the slot-start module brings
both declaring modules (extdeps.filesystem.filesystem_io's service and std.resources) into
one scope, so the bare read would be resolved by scope precedence. Both call
Filesystem.Write, the extdeps service, and now import it from there, as the refusal's
remedy directs. Review 70740: the new step had been inserted between the boot-probe
upload step and its leading comment; the comment is back above its function, and the new
upload step states its own always() rationale.

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

gunbai-bot Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Fixed at b4ec93b: review 70740, plus the floor red's actual cause.

  • Review 70740. I had inserted the new step between fleet_converge_microvm_boot_probe_receipt_upload_step and its leading comment, so that rationale attached to the wrong function. It is back above the boot-probe upload step. The new upload step now carries its own one-line always() rationale.
  • The floor refusal, found this time rather than guessed. The refusal prints its sites on the lines after the headline, which is where I should have read first. It says Filesystem is read bare by gunbc.runner_guest_image and gunbc.runner_microvm_host_ready, and is declared by both extdeps.filesystem.filesystem_io (service) and std.resources. This PR's module brings both declaring modules into one scope, so those bare reads would have been resolved by scope precedence. Both modules call Filesystem.Write, the extdeps service, and now import it from there, as the refusal's remedy says. My two earlier attempts fixed real defects (a duplicate builder, and an oversized workflow closure), but neither was this.

— sent from quiet-koi-746

@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 24, 2026
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 24, 2026
…bels' into session/quiet-koi-746-slot-start
@gunbai-bot
gunbai-bot Bot removed this pull request from the merge queue due to a manual request Sep 24, 2026
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 24, 2026
Brian Searls and others added 3 commits September 24, 2026 11:41
…s YamlEmitRefused arm (main's #11730 replaced serialize_yaml)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bels' into session/quiet-koi-746-slot-start

# Conflicts:
#	.github/workflows/fleet-converge.yml
@gunbai-bot
gunbai-bot Bot removed this pull request from the merge queue due to a manual request Sep 24, 2026
Brian Searls and others added 3 commits September 24, 2026 12:41
…-slot-start

# Conflicts:
#	.github/workflows/fleet-converge.yml
#	dag/gunbc/fleet/fleet_converge_workflow.dag
#	docs/design-rung-drops.md
… per-file claim (no refusals, zero uses)

main's census requires every executed workflow file to carry a roster row and a
per-file claim; the new shakedown workflow had neither, so the floor's roster join
failed. Its claim asserts zero uses rather than census_holds's uses > 0: the job is
action-free by design, an unreadable file refuses rather than reading as empty, and
a uses: added later reds it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bels' into session/quiet-koi-746-slot-start
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 59aaecb Sep 24, 2026
5 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/quiet-koi-746-slot-start branch September 24, 2026 15:50
gunbai-bot Bot pushed a commit that referenced this pull request Sep 24, 2026
…diff, applied three-way

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 24, 2026
Regenerate fleet-converge.yml and design-rung-drops.md from the merged authorities. The shakedown job
from #12178 carries Job.environment (none), which this branch adds to extdeps.github.actions Job.

Co-Authored-By: Claude Opus 5.5 (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.

0 participants