Repository navigation
Repair the test -e absence probe in runner_host_file_converge (metadata failure read as absent) - #11845
Conversation
gunbc.runner_host_file_converge observed a desired host file with ["test", "-e", path] and mapped exit 1 to HostFileAbsent. GNU test -e is stat(path) == 0, so ENOENT, EACCES on an ancestor directory, ELOOP, ENOTDIR and EIO all answer 1 with an empty stderr: "the file is not there" and "I could not look" arrived as one value, and HostFileAbsent is the arm that plans a write into a file this module exists to keep root-owned and non-injectable. The probe is now extdeps.tools.stat's own stat_file_type_command, whose annotation states this defect as the reason the operation exists, built through the sealed argv_command mint rather than spelled here. The read has two honest outcomes -- read, or not read -- and absence is settled by gunbc.live_deploy.member_observe entry_presence, the landed authority for establishing absence from a parent listing over a transport, rather than by the probe's silence. A metadata read that failed beside a parent that holds the entry reaches HostFileUnobservable carrying the cause and can no longer reach HostFileAbsent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 69169: the parent listing was executed on every observation and discarded on the common path where stat answered. entry_presence is a remote leg -- ls -1 -A of the parent over fleet ssh, recursing up the ancestor chain when a listing refuses -- so that was several discarded round trips per desired file per host (DESIGN section 2, and section 6's bare minimum cost). The in-diff justification it replaced was wrong: section 3c asks who consumes a declaration and by what executing route, and computing the parent standing inside the unread arm keeps HostFilePathAbsent and HostFilePathUnreadable inhabited by exactly the production route that can reach them. classify_host_file_path stays total and pure, and the witnesses over it are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed The finding is correct and my in-diff justification was wrong. DESIGN §3c asks who consumes a declaration and by what executing route; computing the parent standing inside the
Also in this push: a merge of main (7f9e624), and the PR body replaced — it was still the auto-opened TODO template because my The discriminating RED is being executed on a throwaway branch (#11868, draft, not for merge): it collapses the unreadable arm back into — sent from bold-moth-41 |
…ng cleanup, ancestors-before-mutation) and #11845 (parent-listing absence) into the custody roster Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ence chain, not the deleted classify_host_file_presence gunbc.microvm_controller_app_key_converge imported and called classify_host_file_presence from gunbc.runner_host_file_converge. The function was real (#11063) and this consumer bound it in #11679; #11845 deleted it because its exit-1 arm minted HostFileAbsent for every stat failure (GNU `test -e` is stat(path) == 0), and touched only the sibling module, so this consumer went dangling. Restoring the name would restore the false-absence arm on the one file the module keeps root:root 0400. observe_key_content now runs the sibling's stat `%F` probe over this module's elevated leg and consumes classify_host_file_metadata, classify_host_file_path and runner_host_file_settled_standing; the parent listing that decides absence when the read failed runs elevated (the same stated departure from member_observe's entry_presence that clear_staging already carries, because the parent is root:root 0700), with the argv and membership read still member_observe's. Evidence: per-entry compile of the module (primary-precedence pool) on #11941's head with the old file reports exactly the three errors (name not found in module at the import, function not found in scope at the call, effect summary incomplete); with this file it reports none, both runs resolving the same 1563-source closure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflicts, and the second was load-bearing rather than mechanical. .github/workflows/fleet-converge.yml is a generated projection and the driver refused it rather than answering, since both sides changed it since the merge base. Regenerated from the merged authorities instead of merged by hand; the result carries this branch's host_credential_custody_converge mode AND main's new modes (mtcollins1_census_image_publish, r2_bucket_ensure, app_key_version_verify, r2_bucket_admin_mint), with no microvm_controller_app_key surviving. A second gate run wrote nothing. dag/gunbc/runner/microvm_controller_app_key_converge.dag was a modify/delete: this branch deletes it as part of the rename to gunbc.host_credential_custody_converge, while #11946 edited it on main. Taking the delete is right, but the edit could not simply be discarded -- it relocated executor_bootstrap_principal from gunbc.runner_host_grants to gunbc.fleet_bootstrap_principal, and the replacement module still imported it from the old home. Resolving delete-vs-modify without reading main's edit would have produced a branch that merges cleanly and does not resolve. The import is repointed, and a census over the branch confirms no other importer takes executor_bootstrap_principal from runner_host_grants. The prose citation at the privilege law is left alone: it names bootstrap_principal_is_not_the_job_user, which did not move. Measured rather than assumed: compiling the replacement module's closure at this head reports 3 blocking errors, all 'shell' transport emission refusals in extdeps.gunbc, and all present in the same closure family on main. Compiling the deleted module ON main reports its own resolution failure (classify_host_file_presence, deleted by #11845) plus annotation-grain errors -- so main currently carries a module that does not resolve, and this merge removes it rather than inheriting it. 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>
The defect
gunbc.runner_host_file_convergeobserve_runner_host_file_contentobserved a desired host file with["test", "-e", <path>]over fleet ssh, andclassify_host_file_presencemappedexit_code == 1toHostFileAbsent.GNU
test -eisstat(path) == 0. ENOENT, EACCES on an ancestor directory, ELOOP, ENOTDIR and EIO all answer exit 1 with an empty stderr, so "the file is not there" and "I could not look" arrived as one value — and they license opposite actions.HostFileAbsentis the arm that walks into write-admission and the comparison path for a file this module exists to keep root-owned and non-injectable.The repair — inhabiting an existing model
extdeps.tools.statstat_file_type_command, built through the sealedargv_commandmint rather than spelled at the call site. That operation's own annotation states this exact defect as the reason it exists, andgunbc.fabric.fabric_cell_acquirealready consumes it in production.classify_host_file_metadatayieldsHostFileMetadataRead { file_type }orHostFileMetadataUnread { cause }. An exit-zero read that printed nothing is unread, not a nameless file type — exit success is a fact about the call, never about whether the answer arrived.classify_host_file_pathjoins the reading withgunbc.live_deploy.member_observeentry_presence— the landed authority for establishing absence from a parent listing over a transport, which also returnsEntryPresenceIndeterminatewhen the parent itself cannot be listed. Consumed rather than restated, so no second absence vocabulary is minted beside the containment rule ofabsence_proof_whose_premise_the_success_destroys(gunbc#11823).HostFilePathPresent/HostFilePathAbsent/HostFilePathUnreadable.runner_host_file_settled_standingmaps them to the standing grain: unreadable reachesHostFileUnobservablecarrying the cause and can no longer reachHostFileAbsent; present settles nothing and continues into the existing byte comparison.observe_runner_host_file_path), because that is the only outcome whose meaning the parent decides.entry_presenceis a remote leg that recurses up the ancestor chain on a refusal, so asking it beside a read that already answered buys a standing the classifier discards.classify_host_file_presenceis deleted, not left standing beside its replacement.The REDs
In
test.claim.runner_host_file_converge_witness_test:an_unread_path_reaches_unobservable_and_never_absent— the control that is red for this reason. One unread cause, held constant, must reachHostFileUnobservablebeside a parent that holds the entry andHostFileAbsentonly beside a parent that lacks it. Collapsing them again — which is exactly whattest -edid — reds on the first row. A control that merely separated exit 0 from exit 1 would pass every row here, which is why the cause is held constant across all three.a_metadata_read_that_failed_is_absent_only_when_the_parent_lacks_the_entry— the same discrimination at the path-standing grain, including the indeterminate parent.a_metadata_read_that_answered_is_present_whatever_the_parent_said— the parent cannot turn a read file unreadable.a_stat_that_exited_zero_and_printed_nothing_is_unread— exit success is not an answer.the_production_probe_sends_the_cited_stat_metadata_words— inhabitance (DESIGN §3): the words the observer sends, over the path a production desired file produces, equalargv_words(stat_file_type_command(...))and contain no boolean test. The remote leg is not hermetically executable (gunbc.rung_droprunner_host_file_apply_route_hermetic), so this is the deepest live route to the new probe.Test plan
CI run 35513652407 on head
e8aef59a447: clippy, compiler, floor, witnesses all pass. The floor job shows the claims executed rather than skipped —CI is re-running at the current head, which carries
review 69169's fix and a merge of main.Out of scope, deliberately untouched
gunbc.typed_remote_file_writeexit 1 (cmp reporting a genuine byte difference),extdeps.shellexecExitZeroOrOneSucceeds(a declared policy), and therunner_host_path_ancestorsownership guard.🤖 Generated with Claude Code