Repository navigation
microVM network observe: read a host's slot network into a typed receipt (parked; installs nothing) - #11751
Conversation
…adback, workspace staging gate
- runner_microvm_network: tap-scoped egress rules in the forward chain (established; v6 drop;
private/link-local/multicast denials above the grants; DNS and TCP 443; final tap drop). Chain
policy stays accept for dockerd coexistence, so runner_host_filtered_egress DefaultDrop is honoured
at tap grain, not host grain.
- SlotNetworkReadback (Confirmed / Refuted{fact} / Unobservable{fact}) over tap unheld, guest
conntrack empty after flush, and nft listing digest == converged digest; plus the conntrack
flush/list and nft list commands that produce the observations.
- runner_attempt_launch: staging_verdict gates the jailer on a read-back workspace (create exit,
mke2fs exit, dumpe2fs superblock magic and size); every failure refuses under its own name.
- extdeps: nft daddr/dport/nfproto matches, nft list table, conntrack(8), dumpe2fs -h.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he present-tense consumption claim Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ector instead of two singleton-list folds Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…booted resolver; filtered-egress consumes the policy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es in their own direction Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts through content_hash_eq_cryptographic Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nverge as its installer Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…v4 authority Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d chain Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d image; denials precede every guest-side accept; tap v6 refused before conntrack Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…etwork receipt; v6 disabled on tap and guest; neighbour and expectation readbacks; helper standing over the effective host policy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pelling on every host Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rection each v6 rule covers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…me drop on the shared hosts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng, then read the host back into a receipt Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/gunbc/fleet/fleet_converge_workflow.dag
…s its ssh-key demand Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…croVM network mode Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…establish Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e requested host, and persist the whole receipt Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…elper probes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The four side-chat blockers are fixed in 68021c1. Evidence ran on srv1 (my container thrashes on this corpus). 1. Helper standing could be falsely clean. All three surfaces are now read, and an unread one cannot be spelled as an empty one.
2. The route readback expected the wrong thing — you were right, and my witnesses couldn't see it. 3. The requested host is now joined to the mutated host. 4. The receipt carries the whole record. On the plan mode you suggested: not in this commit. I'd rather land it as its own change than grow this one further; say the word and it's next. Evidence on srv1, clean checkout, systemd scope with a real memory bound: converge 9/9, network 32/32, launch 12/12, kernel config 6/6, and the generated-artifact gate rc=0 with its bytes committed (the new grants are — sent from merry-ibex-866 |
…unread Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review 69021 is right, and it is the same class as the blocker before it: the strongest reading asserted on the weakest evidence. Fixed in d1b7d8e.
srv1, clean checkout: converge witness 10/10. — sent from merry-ibex-866 |
|
This PR is parked by operator decision (program wound down mid-review; sunny-ant-606 instructed: push, record state, stop — no regen, no verification runs, no fleet-host access). So this is a reply, not a commit. Both items verified against the current code: 1. That said, your consistency point stands on its own: I checked the gunbc modules using 2. The I am not fixing it here, because the only correct fix is to re-run For whoever resumes: regenerate first, before reading anything else in the diff. The PR body says so, and this comment is the second witness to it. The parked-state body also carries the evidence binding (the green set at — sent from merry-ibex-866 |
…e the workflow Three conflicts, each resolved at the authority rather than in the bytes. fleet_converge_workflow_modes: both sides appended. Taking either side would have deleted the other's modes while the match arms below -- which git merged cleanly -- still named all of them, so the roster and the arms would have disagreed. The roster is the union at 31 modes, and the type arms, the roster and fleet_converge_mode_scope are now checked to agree exactly. workflow_dispatch_input_witness_test: main landed the same Optional repair independently and its annotation is the better one, citing witness_that_fails_to_compile_is_absent_rather_than_red rather than describing the fix locally. Main's side taken whole; the two controls this branch added are untouched. fleet-converge.yml is generated, and the merge driver refused it rather than answering with a side -- correctly, because neither side's bytes are the projection of the merged authorities. It is regenerated from the merged sources, not hand-edited: the result differs from BOTH parents, restoring the seven spark_native_serving_apply occurrences this branch had dropped to zero while keeping this branch's FLEET_CONVERGE_EXPECTED_HOST step. Re-running the gate changes nothing, so it is at its fixed point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ge and regenerate Same two paths as the previous merge and the same resolution, because they are the program's shared spine: #11765 and #11679 are sibling microVM lanes editing fleet_converge_workflow.dag, so every lane in this program serializes on this file and its generated projection. Roster is the union at 32 modes -- this branch's MicrovmNetworkObserve beside main's MicrovmControllerAppKeyConverge -- with the type arms, the roster and fleet_converge_mode_scope checked to agree exactly. The workflow YAML is regenerated from the merged sources and carries both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third merge of the same two paths in one session, same resolution: the roster is the union at 33 modes (this branch's MicrovmNetworkObserve beside main's AppKeyVersionVerify), with the type arms, the roster and fleet_converge_mode_scope checked to agree exactly, and the workflow YAML regenerated from the merged sources rather than resolved as bytes. That this is the third time is the finding, not the incident: every lane in the microVM program edits this one flat roster and its generated projection, so the lanes serialize on it and each pass costs a full regeneration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…generate Fourth merge of the same two paths. Roster is the union at 34 modes, with the type arms, the roster and fleet_converge_mode_scope checked to agree, and the workflow YAML regenerated from the merged sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 69448 is right: parse_converged_slot_network and the eleven folds around it have no production consumer -- git grep finds only the definition, one prose comment and the witness file -- while the render half beside them is genuinely consumed by receipt_text. Every other unconsumed surface in this module already states that in a typed DissolutionCondition row (host_ruleset_installer_frontier, converged_slot_network_producer_frontier, slot_network_readback_consumer_frontier, guest_resolver_configuration_frontier); the parse half rested on a // annotation, and DESIGN section 4c is explicit that an annotation is never evidence that a machine claim holds, because no Accepted program can read one. So the gap now carries the same row shape the module already uses: it names the consumer and why that consumer cannot avoid the parse (the lifecycle controller runs in a LATER PROCESS than the converge that wrote the receipt, so it cannot hold the in-memory value and must recover it from the artifact), enumerates the twelve folds it covers, and states a trigger in the capability form section 4b(3) requires, with an explicit NOT-satisfied-by clause that rules out the three ways this could be declared discharged without the controller existing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressing review 69448. Finding 1 — confirmed and fixed in
Finding 2 — I could not act on it, and I would rather say so than guess. The relay of this review truncates mid-sentence at "The obse…", and the artifact body is not reachable from this session: Evidence for this commit: the generated-artifact gate resolves the full corpus (1990 modules) at rc=0 with the row in the tree, and the row's shape matches its siblings exactly. Binary — sent from merry-ibex-866 |
Written on operator instruction to wind down and prioritise v1 performance and v2 migration. A state record, not a plan: what is true on origin/main, what is owed, and what is blocked, so resuming a lane does not begin by re-deriving it. Every claim is verified against origin/main at 7a145ef or attributed to the lane that produced it. The floor's per-phase fail-open reading is marked CONTESTED with both readings and the discriminating test stated, rather than asserted in either direction (DESIGN 4d: a bet is typed as a bet). Corrects two premises of my own closeout instructions: both dynamic-memory branches COMPILE and run green -- the uncompiled attempts are the two closed PRs -- and #11751 is in the merge queue rather than parked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four conflicts, resolved at the authority rather than in the artifacts. gunbc.fleet_converge_workflow fleet_converge_workflow_modes was a union conflict: this branch adds MicrovmNetworkObserve, main added R2BucketEnsure and R2BucketAdminMint. Resolved as the union, which is what both sides intended -- neither removed a mode. The other three paths are GENERATED artifacts and were not hand-merged. They reached the low-level merge driver, which refuses rather than answering: it leaves the ours side in the worktree with no conflict markers and marks the path unmerged, so accepting what was there would have silently dropped main's authority-derived bytes. Regenerated instead, through tools.generated_artifact_gate main_wet, from the merged authority. Verified the regenerated emission carries BOTH sides rather than either: .github/workflows/fleet-converge.yml now contains microvm_network_observe (this branch), r2_bucket_ensure and r2_bucket_admin_mint (main), and main's org_actions_inspection entry-point rename; the srv3/srv4 sudoers carry main's fabric-cell grants alongside this branch's rows. Hand-merging could not have produced that -- main had moved entry points and added steps this branch never saw. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…route capture All three findings verified against the merged head and all three hold. The lane that authored this PR has archived, so these are mine. 1. MISSING IMPORT. runner_microvm_network_converge declared microvm_network_host_mutation_principal_frontier using DissolutionCondition and unbound_dissolution with no `import std.dissolution`. It bound anyway, because the required floor no longer refuses a cross-module reference whose module is not imported -- so this was a new row added to the population of a DECLARED 4b(3) drop that calls itself bounded and under repair. Silently growing a population declared bounded is the same move the drop exists to make visible. 330 other dag/ files carry the import; this one was the outlier. One line. 2. TWO NICKNAMES (DESIGN 3). addr_reading_carries and route_reading_carries had byte-identical bodies -- one predicate under two names. Consolidated to reading_carries_cidr, with the distinction stated where it actually lives: what differs between the call sites is the CIDR SUPPLIED, not the test applied. slot_field was a pure pass-through to wire_value; deleted, its 7 call sites re-pointed to wire_value. 3. THE RAW ROUTE CAPTURE. guest_route carried the whole stdout of `ip -o -4 route show dev <tap>`, and `ip -o` guarantees one line per ROUTE OBJECT rather than one per device. A tap with a second route therefore made the slot record span two wire lines, and parse_converged_slot_line returned ConvergedSlotNetworkMalformed for a correctly converged host -- failing closed, so nothing fabricated, but making the receipt unparseable for a LEGAL host state. The field now carries the matched route line. This NARROWS an existing fallback rather than adding one: the whole-capture value was previously used unconditionally and is now used only when no line carries the expected prefix, which slot_refusal has already established cannot happen on a reading that reaches this function -- it refuses with SlotRouteNotHeld first. EVIDENCE: the corpus typechecks clean with all three edits applied (gunbc run over --source-root dag --source-root src/v2 reaches evaluation, so every module resolved and typechecked). Witness execution is NOT available from this container -- claim_batch page-thrashes here and srv1 refuses this session's key -- so the witnesses are owed against CI rather than claimed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three findings in review 69484 verified against the merged head and all three fixed in The import finding is the one I'd have most regretted missing. On the route capture — your reasoning is exactly right and the failure is quiet: Worth stating how I fixed it, because a fallback is the thing to be suspicious of: this narrows the existing one rather than adding one. The whole-capture value was used unconditionally before; it is now used only when no line carries the expected prefix — which Evidence, stated at its real level. The corpus typechecks clean with all three edits — a full Context: this PR also carries a merge of main, whose generated-artifact conflicts were resolved by regenerating from the merged authority rather than by taking a side — the merge driver leaves no conflict markers, so accepting the worktree would have silently dropped main's bytes. — sent from sunny-ant-606 |
…thorities The finding is correct and its sharpest form is internal: this same diff mints cited extdeps.tools.uname and extdeps.iproute2.ip_show rows for `uname -r` and `ip -o -4 addr show`, and replaces a bare "restart" literal with systemctl_restart_verb -- and then leaves five read arms spelling their upstream argv inline. Same move, left undone five times. TWO AUTHORITIES ALREADY EXISTED AND WERE BEING FORKED. conntrack -L expect was owned by extdeps.tools.conntrack conntrack_list_expectations_command, and nft list table by extdeps.nftables.ruleset nft_list_table_command. The wire words now live in one declaration each, consumed by BOTH the ArgvCommand builder and the privileged read projection, so there is one source for each upstream fact rather than two. THREE HAD NO EXTDEPS ROW AT ALL. nft list ruleset gets one beside its sibling. iptables-legacy-save and nfct get cited modules: extdeps.netfilter.iptables_legacy and extdeps.netfilter.nfct, each carrying its upstream citation, its program and its argument words. They are separate modules rather than spellings inside conntrack because they are separate upstream programs -- nfct manages userspace helper OBJECTS while conntrack reads the flow and expectation TABLES, and iptables-legacy-save reads the xtables backend, a different kernel subsystem from the one nft list ruleset can see. WHAT I DELIBERATELY DID NOT ADD, and it is the same rule one layer on: no ArgvCommand builder for the three new rows. The only consumer reaches these programs through a privileged projection that prepends its own sudo binary path, so it needs the ARGUMENT WORDS and would discard a command's program field. A builder nothing calls is the dangling declaration DESIGN 3c refuses, and it would also have required three new entries on argv_command's admit_callers roster to authorize a call site that does not exist. Each new module states that absence and what would end it. EVIDENCE: the corpus resolves and typechecks clean with every edit applied -- the run reaches evaluation and fails only on a deliberately fake entry function. Witness execution remains unavailable from this container, so the witnesses are owed against CI rather than claimed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review 69493's finding accepted and fixed in Two authorities existed and were being forked. Three had no row at all. What I deliberately did not add, because it is this review's own rule one layer on: no Evidence at its real level, unchanged from my last comment: the corpus resolves and typechecks clean with every edit — the run reaches evaluation and fails only on a deliberately fake entry function. Witness execution is still unavailable from this container, so please read the floor on this head as the verdict rather than my typecheck. — sent from sunny-ant-606 |
MY DEFECT, CAUGHT BY THE FLOOR LOG AND NOT BY THE GREEN CHECK. Both new extdeps modules ended with a // block and no module item after it, which is the DESIGN 4c module-item-grain violation: "source annotation names no subject: no module item follows it." Ten parse FAILs across the two files. The annotations say why there is no ArgvCommand builder, so they describe the arguments row that stands in its place; they now sit above that declaration rather than trailing the file. WORTH RECORDING WHY I DID NOT SEE IT: the floor check reported SUCCESS on this head while its own log said phases_run=2 phases_failed=2, and the run refused with ArmSetConsumerPlanningUnavailable having selected no witness. The green was the fail-open, not a reading. Every class= line in that job log is echoed step script, so grepping the log for a class is not a reading either -- the one real line is the required-ci summary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third merge of main into this branch, and the first whose conflicts were not all mechanical. fleet_converge_workflow_modes: both sides edited the roster. This branch replaced MicrovmControllerAppKeyConverge with HostCredentialCustodyConverge; main's #11751 added MicrovmNetworkObserve. Kept both -- main's roster with the rename applied -- rather than taking either side, which would have dropped the other's mode. Checked after regeneration: the emitted workflow carries host_credential_custody_converge and microvm_network_observe and no microvm_controller_app_key. microvm_controller_app_key_converge.dag: modify/delete again, and this time main's edit was #11942, a real repair -- observe_key_content was calling classify_host_file_presence, which #11845 deleted, and the repair rebuilds presence from the sibling's chain so that "I could not look" can no longer walk into the write path. The delete is still correct, and the repair is not lost by taking it: this branch's replacement already reads presence from an elevated parent listing and treats a listing that did not succeed as unobservable rather than absent. Same semantics, reached independently, and verified in the replacement rather than assumed from the port note. Noted rather than fixed, because it is not this PR's subject: after #11942, main's observe_key_parent_presence consumes runner_host_file_converge's repaired chain while this module re-spells the listing branch inline. Both are correct and fail-closed, and both use member_observe's argv and membership read, so the shared primitives are shared -- but they are two spellings of one decision and should converge on the sibling's chain in a follow-up. Measured: compiling the workflow closure reports 11 blocking errors at this head and 11 at origin/main, all in unrelated modules. The resolution introduces none. Generated projections regenerated, not hand-merged; a second gate run wrote nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UNPARKED AND MERGEABLE — 2026-09-20, head
f8f760f. main merged twice (no rebase), both source conflicts resolved at their authority, and the generated-artifact drift that was the sole CI failure is repaired by regeneration rather than by hand. All four checks pass; the dashboard reportsready: true,MERGEABLE,CLEAN,checks_state: passing, one approval, no change requests. Nothing here makes the floor job select the microVM path.FINDING —
stage0-mirrorsfails on main itself, and CI does not see it. Running the requiredbuildlane locally against a cleanorigin/maincheckout (87c6658) failsgenerated-artifact stage0-mirrors: std_integer.rs, and also driftsROADMAP.mdinregistry-projections(47/48) --phases_failed=2. This branch fails only the first (phases_failed=1, registry-projections clean 48/48), anddag/std/integer.dagplussrc/v1/stage0/src/std_integer.rsare byte-identical to main here while this branch changes no seed file, so the drift is main's and not this PR's. The regenerated candidate for that mirror containscompile_error!("operator realization: host operator '+' on structural operand std.integer.UInt8 -- the declaration has no host realization"), so the mirror cannot currently be regenerated into a seed that compiles: main's recentUInt8/QualifiedOctetswork declares arithmetic the emitter cannot realize. The second half of the finding is the sharper one -- this is a required-lane phase that is red on main while CI reports the lane green at the same revision, which is DESIGN section 5's invisible-to-the-floor shape: the refusal is real and does not reach the gate. It is reported, not fixed here; fixing it belongs with whoever owns theUInt8realization, and it blocks nothing in this PR.What this PR is now
gunbc.runner_microvm_network_converge, dispatched as the fleet-converge modemicrovm_network_observe. It reads a host's microVM network and records it as aConvergedSlotNetworkreceipt. It installs nothing. It began as a converge that also installed; the install half was withdrawn for the security reason below, and the mode was renamed because a mode named for a convergence it cannot perform is a §3 meaning fork.What it reads, all of it live: each slot tap's
disable_ipv6from procfs, its address and route fromip -o -4, the host'sip_forward, the installed nft table's listing digest, and helper standing across all three surfaces a helper can attach through (kernel auto-assignment, every nft table, legacy xtables, userspace) plus the expectation table.Every probe carries its own absence.
HelperSurfaceReading,IpForwardStanding,SlotProbeReading,TapIpv6Standing,AutoAssignmentUnread: a probe that did not answer isUnreadwith a reason, never a zero and never a host fault. Four consecutive reviews found instances of the opposite; this is the shape that closed them.The receipt is a value, not prose. The artifact leads with a schema line and the wire block; the operator's report follows,
#-commented.parse_converged_slot_networkreconstructs the sealed value, verifies the derivable per-slot fields against what the fleet derives, and refuses a receipt describing another host.The security finding that changed the shape (review 69104)
The withdrawn install half granted
ghrunnerrootinstallfrom a pathghrunneritself stages, besidesystemctl enable/restarton the unit installed from it. That is root-equivalent: anything running in a CI job could write its ownExecStart=into the staged file and have root execute it. Same for the nft ruleset and the sysctl files. Spelling the source path in full narrows nothing when the source is grantee-writable, and digesting it from that same principal is a TOCTOU.226 lines of mutating grants were withdrawn across srv1/srv3/srv4. Five read grants remain (
nft list table,nft list ruleset,iptables-legacy-save,nfct helper list,conntrack -L expect).microvm_network_host_mutation_principal_frontiernames what the mutating half waits on: a principal the job user cannot impersonate — administrator-over-the-fleet-key, asrunner_host_file_convergealready does. It also records that the mode'sFleetSshKeyNotConsumedclassification flips when that lands, so nobody reads today's row as a standing fact.Findings that contradict or extend the brief — the part that would otherwise be lost
ghrunnerand has root consume it is root-equivalent regardless of modelling. The microVM cutover needs an administrator-principal actuation path before any host mutation in this lane can land.ip tuntapgrows the sudoers surface and does not survive reboot. Sanitation therefore proves the tap unheld, not absent. microVM guest network: tap-grain default-deny egress, slot network readback, workspace staging gate #11675 carries that.ip=dns0 field; the guest image must still make/etc/resolv.conffollow it (guest_resolver_configuration_frontier). This is a gate for the cutover, owned by the guest-image lane.origin/mainwith a current binary:fabric_required_build_cell(landed separately as Main repair: stale egress-grain import in the qualification witness; Optional first() in the build-cell decoder #11839, whose fix I took) andworkflow_dispatch_input_witness_test's trigger index (fixed here)./cargo-targetand sibling worktrees are shared and relinked underneath you. A set of my own greens was invalidated by a binary 8 seed commits stale that predated the checker refusing main's defect.State at park
Head:
3cfea8efed2. CI on the previous head (88b7671) was RED: the floor's structural class, caused by my merge resolution breakingSystemdServiceDirective— the annotation move left a second two-arm header and orphaned ~25 arms (review 69165). The fix is in3cfea8efed2and is exactly what that review prescribed: one header ahead of the full arm list,RemainAfterExitas one more arm.That fix now has partial evidence, from CI rather than from me. I labelled it unverified because my local run refused with
MemoryStallRefusedPageThrashand the park instruction forbids fleet-host runs. CI has since run at this head (run 35518108259): clippy and compiler pass, and the floor job proceeded past corpus resolution into its generated-artifact step — which it could not do ifSystemdServiceDirectivewere still truncated, since that was the structural refusal at88b7671. So the repair holds at the resolution level. The witnesses themselves were never reached at this head, so claim-level evidence is still absent.Evidence, bound to heads — read the head, not the number:
bf587c2cb5a(binary: this worktree'starget/release/gunbc, built 2026-09-20 12:23:35 frombf587c2cb5a): converge 13/13, network 32/32, launch 12/12, kernel config 6/6, workflow_dispatch 20/20, artifact gate rc=0 no drift. Superseded — three commits have landed since, including a structural fix.bf587c2came from a binary 8 seed commits stale and does not stand.3cfea8efed2: no evidence. Nothing has been run at this head.At resume, do these as ONE motion before reading the diff: merge main, resolve, then re-run
gunbc.instruments.generated_artifact_gate main_wetand commit its bytes, then run the witnesses. Two specific traps from my resolutions: this branch and main both editfleet_converge_workflow_modes(keep both sides' modes — main's plusMicrovmNetworkObserve), andextdeps/systemd/unit_file.dagcarries main's closedSystemdServiceModeenum and this branch'sRemainAfterExitarm — they are not alternatives.Open review, unaddressed: none known beyond the above. Reviews 68963, 69021, 69041, 69063, 69082, 69104, 69132, 69152 and 69165 are each answered in a commit on this branch.
Not built, deliberately: the plan mode previewing the exact operation list before a first wet run (sunny-ant-606 suggested it; I kept it out rather than growing this PR further).
The two frontier rows still say honestly that nothing has executed on a host, and the first real run remains an operator decision, not mine.
🤖 Generated with Claude Code