From 2ffdf61621843055d1eb7cd898648ba8c58ceece Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 11:41:15 +0000 Subject: [PATCH 1/2] Six ungated main reds re-derived; three entries admitted by importing LiveTreeDisposition (1)(2) live_deploy.emit sudoers claims: the needle is now the install's own node (gunbc.ci_deploy_sudoers deploy_sudoers_elevated over the fleet visudo row) rendered by the same serializer. Stale since #12602 (/usr/bin/sudo) and #12525 (bash builder quotes every word). The two probe negatives are deleted: unmatchable under quoting, and since #12168 those probes are emitted on purpose after the install. (3) twin claim: count equality replaced by an identity join on artifact kind, host singletons (fabric storage + #12747's approval broker front door) subtracted by the functions that decide them. (4) CPUQuota grant: sudoers side read through sudoers_argument_word (escape since #12563). (5) tasks verdict: bare `Absent ==` never named the ConvergeVerdict arm; typed match + a Drifted discriminating conjunct. (6) runner_lifecycle: fabric rows from srv3/srv4_fabric_first_slot, each controlled by the slot below it on its own host (srv4-06 is fabric since 2026-09-18). Admission: build_cache_endpoint_observe, ci_budget_tree_witness, host_allocation_conservation import v2.std.live_tree (the #12540 class #12819 fixed once); variant rows retired ImportsFixed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../build_cache_endpoint_observe_test.dag | 1 + .../claim/ci/ci_budget_tree_witness_test.dag | 1 + ...utor_privileged_operation_witness_test.dag | 12 +++- .../host_allocation_conservation_test.dag | 1 + .../host_converge_slice1_witness_test.dag | 24 ++++++- dag/test/claim/live_deploy/emit_test.dag | 66 +++++++++++++++---- .../runner/runner_lifecycle_witness_test.dag | 26 ++++++-- ...r_unimported_bare_provider_debt_roster.dag | 6 +- 8 files changed, 109 insertions(+), 28 deletions(-) diff --git a/dag/test/claim/build_cache_endpoint_observe_test.dag b/dag/test/claim/build_cache_endpoint_observe_test.dag index 5fbbac457e6..5a4cda781ae 100644 --- a/dag/test/claim/build_cache_endpoint_observe_test.dag +++ b/dag/test/claim/build_cache_endpoint_observe_test.dag @@ -1,5 +1,6 @@ module test.claim.build_cache_endpoint_observe +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import gunbc.build_cache_instance { ProcessIdentity } import extdeps.systemd.unit_file { systemd_unit_file_lines } import std.materialization_ladder { string_list_contains } diff --git a/dag/test/claim/ci/ci_budget_tree_witness_test.dag b/dag/test/claim/ci/ci_budget_tree_witness_test.dag index 5679ff2e3f5..7e5c13f0a34 100644 --- a/dag/test/claim/ci/ci_budget_tree_witness_test.dag +++ b/dag/test/claim/ci/ci_budget_tree_witness_test.dag @@ -1,5 +1,6 @@ module test.claim.ci_budget_tree_witness +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } import gunbc.ci_runner_placement { resolve_session_slice } import gunbc.ci_runner_target { FleetSelfHosted, ci_runner_target_ram_speed_budget } import gunbc.ci_budget_tree { diff --git a/dag/test/claim/executor_privileged_operation_witness_test.dag b/dag/test/claim/executor_privileged_operation_witness_test.dag index 4f470f5edcc..81a0def4927 100644 --- a/dag/test/claim/executor_privileged_operation_witness_test.dag +++ b/dag/test/claim/executor_privileged_operation_witness_test.dag @@ -23,6 +23,7 @@ import gunbc.fabric_cell_effect { fabric_cell_slice_desired_directives } import extdeps.systemd.unit_file { slice_cpu_quota, slice_cpu_quota_unbounded, systemd_slice_directive_line } import gunbc.fleet_intent_network { operator_host_srv2, operator_host_srv4 } import gunbc.runner_host_deploy { admitted_deploy_for, RunnerHostDeploy } +import gunbc.executor_privileged_operation { sudoers_argument_word } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -179,19 +180,23 @@ test fn witness_population_tracks_the_slot_roster() -> Bool { // throttle the ruling forbids. That negative is a LINE-TERMINAL `%`: a percentage grant is the // only sudoers line that ends in one, whereas a `%group` principal line begins with it, so the // clause names the CPU grant and not every future row that happens to carry the character -// (review 68479). +// (review 68479). THE SUDOERS SIDE IS READ IN SUDOERS' OWN SPELLING: since #12563 every grant word +// passes through gunbc.executor_privileged_operation sudoers_argument_word, which escapes `=` as +// sudoers(5) documents, so the file carries `CPUQuota\=` and the bare directive line can never +// appear in it. The claim renders the assignment through that same escape rather than transcribing +// the escaped literal, so the two sides stay one word under the grammar that joins them. test fn witness_fabric_slice_cpu_quota_grant_is_the_declared_directive() -> Bool { let ds = fabric_cell_slice_desired_directives() let desired_lines = join(map(ds, d => systemd_slice_directive_line(directive: d)), "\n") match gunbc_runner_slot_cpu_quota() { CpuQuotaResolved { threads: t } => { let assignment = systemd_slice_directive_line(directive: slice_cpu_quota(threads: t)) - string_contains(s: srv3_runner_host_sudoers(), pattern: assignment) + string_contains(s: srv3_runner_host_sudoers(), pattern: sudoers_argument_word(w: assignment)) && string_contains(s: desired_lines, pattern: assignment) } SlotCpuQuotaUnbounded => { let assignment = systemd_slice_directive_line(directive: slice_cpu_quota_unbounded()) - string_contains(s: srv3_runner_host_sudoers(), pattern: join([assignment, "\n"], "")) + string_contains(s: srv3_runner_host_sudoers(), pattern: join([sudoers_argument_word(w: assignment), "\n"], "")) && string_contains(s: desired_lines, pattern: assignment) && !string_contains(s: srv3_runner_host_sudoers(), pattern: join(["%", "\n"], "")) } @@ -326,3 +331,4 @@ fn wt_srv3_deploy() -> RunnerHostDeploy { Absent => wt_missing_deploy() } } + diff --git a/dag/test/claim/host/host_allocation_conservation_test.dag b/dag/test/claim/host/host_allocation_conservation_test.dag index 10a40972aef..4dd6375c6b5 100644 --- a/dag/test/claim/host/host_allocation_conservation_test.dag +++ b/dag/test/claim/host/host_allocation_conservation_test.dag @@ -6,6 +6,7 @@ module test.claim.host_allocation_conservation // the only way to execute a row was a whole-tree floor run. The imports below are what it was // already using; making them explicit costs nothing and buys a witness you can execute while you // are writing it, which is the difference between finding a defect now and finding it in CI. +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.types { String, Bool, Int, List } import std.measure { ByteSize, byte_size, hardware_thread_count_value } import extdeps.cpu.ampere { altra_max_m12830_catalog } diff --git a/dag/test/claim/host/host_converge_slice1_witness_test.dag b/dag/test/claim/host/host_converge_slice1_witness_test.dag index f82140c4f3c..9504420bf8b 100644 --- a/dag/test/claim/host/host_converge_slice1_witness_test.dag +++ b/dag/test/claim/host/host_converge_slice1_witness_test.dag @@ -4,7 +4,7 @@ import std.types { Bool, NonEmptyStr, List, String } import gunbc.host_effect { LocalShell } import gunbc.runner_host_deploy { runner_index_seq } import extdeps.systemd { systemd_memory_max_property, MemoryMax, systemd_tasks_max_property, parse_systemd_task_limit_show } -import gunbc.host_converge { Drifted, PerSlotMemoryCap, VerdictConverged } +import gunbc.host_converge { ConvergeVerdict, Drifted, PerSlotMemoryCap, VerdictConverged } import gunbc.fleet_intent_network { operator_host_srv1 } import gunbc.runner_unit_live_read { gunbc_srv1_runner_unit_live_read_fixture, @@ -249,11 +249,28 @@ test fn witness_tasks_verdict_desired_count_converges_and_offbyone_drifts() -> B } } +// THE VERDICT IS READ BY A MATCH OVER ITS OWN TYPE, NOT COMPARED TO A BARE `Absent`. `Absent` names +// two constructors here -- the Optional arm this claim matches on one line up, and +// gunbc.host_converge ConvergeVerdict's arm, which this module never imported -- and an `==` operand +// carries no expected type to choose between them. Executed: the subject returns the verdict arm +// for "" and the comparison was false, so the claim could not go green whatever the subject did. +// A match on a ConvergeVerdict scrutinee is typed by the scrutinee, enumerates the other two arms, +// and the "512" conjunct is the discriminating red: a verdict function that answered Absent for +// everything satisfies the first two conjuncts and fails this one. +fn tasks_verdict_is_absent(v: ConvergeVerdict) -> Bool { + match v { + VerdictConverged => false + Drifted => false + Absent => true + } +} + test fn witness_tasks_parse_empty_or_junk_is_absent_verdict() -> Bool { match host_converge_slice1_tasks_knob(host: operator_host_srv1) { Present { value: knob } => - host_converge_slice1_tasks_verdict(limit: parse_systemd_task_limit_show(raw: ""), knob: knob) == Absent - && host_converge_slice1_tasks_verdict(limit: parse_systemd_task_limit_show(raw: "not-a-count"), knob: knob) == Absent + tasks_verdict_is_absent(v: host_converge_slice1_tasks_verdict(limit: parse_systemd_task_limit_show(raw: ""), knob: knob)) + && tasks_verdict_is_absent(v: host_converge_slice1_tasks_verdict(limit: parse_systemd_task_limit_show(raw: "not-a-count"), knob: knob)) + && !tasks_verdict_is_absent(v: host_converge_slice1_tasks_verdict(limit: parse_systemd_task_limit_show(raw: "512"), knob: knob)) Absent => false } } @@ -312,3 +329,4 @@ test fn witness_tasks_guard_boundary_equality_refuses() -> Bool { && host_converge_slice1_tasks_headroom_refuses(current: 16385, target: 16384) && !host_converge_slice1_tasks_headroom_refuses(current: 16383, target: 16384) } + diff --git a/dag/test/claim/live_deploy/emit_test.dag b/dag/test/claim/live_deploy/emit_test.dag index 5dadb87e9f8..f8d169863b1 100644 --- a/dag/test/claim/live_deploy/emit_test.dag +++ b/dag/test/claim/live_deploy/emit_test.dag @@ -24,6 +24,7 @@ import gunbc.live_deploy.spec { DeploymentDependencyStep, ensured_subject_identity, deployment_fabric_storage_steps, + deployment_approval_broker_front_door_steps, deployment_fabric_storage_serve_endpoint, EnsuredPackage, EnsuredManagedHostDirectory, @@ -86,7 +87,8 @@ import extdeps.tailscale.serve { } import extdeps.exec.command { shell_quote } import extdeps.posix.shell_command_language { posix_single_quote } -import gunbc.shell_command_text { shell_privileged_command_text_of_argv } +import gunbc.shell_command_text { shell_privileged_command_text_of_argv, shell_command_text_of_node } +import gunbc.ci_deploy_sudoers { deploy_sudoers_elevated } import gunbc.live_deploy.operations { install_directory_command, rm_force_command, @@ -151,7 +153,7 @@ import gunbc.ci_deploy_access { deploy_access_job_principal_refused_emit_poison, ci_deploy_srv1_access_or_refusal, DeployAccessReady, DeployAccessJobPrincipalRefused, } -import gunbc.fleet_posix_accounts { DeployRunnerPrincipal } +import gunbc.fleet_posix_accounts { DeployRunnerPrincipal, ci_runner_sudo_binary_path, SudoVisudo } import gunbc.host_axis_caps { deploy_memory_cap_apply_script_for_host, deploy_memory_cap_shadow_revert_script, @@ -347,14 +349,31 @@ test fn witness_dependency_ensure_is_idempotent() -> Bool { && string_contains(s: tmux, pattern: deploy_effect_command(inv: apt_install_unelevated_effect(package: package_tmux))) } +// THE STAGED-FILE VALIDATION, AS THE INSTALL ITSELF BUILDS IT. The install step is a bash command +// node built by gunbc.ci_deploy_sudoers deploy_sudoers_elevated over the sudo elevation argv and the +// fleet's visudo row; this is that same node with the same argv, rendered by the same serializer, so +// the needle is the producer's own words rather than a hand spelling of them. The hand spelling +// `sudo -n /usr/sbin/visudo -cf` went stale twice without the step changing meaning: #12602 spelled +// sudo by its absolute path (extdeps.sudo sudo_program), and #12525 moved the install onto the bash +// builder, which quotes every literal word. The stage path is a variable word the claim does not name. +fn witness_sudoers_stage_visudo_check_text() -> String { + shell_command_text_of_node(stmt: deploy_sudoers_elevated(command: [ci_runner_sudo_binary_path(b: SudoVisudo) as String, "-cf"], tail: [])) +} + +// THE TWO PROBE NEGATIVES THIS CLAIM CARRIED ARE DELETED, NOT RE-SPELLED. They banned +// `sudo -n /usr/sbin/visudo -V` and `sudo -n -l /usr/bin/tailscale` -- the bootstrap preflight's +// probes. Under the quoting serializer neither spelling can occur, so both were permanently true and +// carried nothing. Re-spelling them in the quoted form would be WRONG rather than merely redundant: +// since #12168 those exact probes are emitted on purpose AFTER the drop-in lands +// (gunbc.ci_deploy_sudoers deploy_sudoers_post_install_roster_probe_stmts). Their ORDER is the +// safety fact, and test.claim.deploy_mutation_gate_witness owns it; what this claim keeps is that +// no embedded preflight block exists, by its markers. test fn witness_apply_emits_no_embedded_preflight_and_only_sudoers() -> Bool { let sh = witness_apply_script() !string_contains(s: sh, pattern: "deploy-access-bootstrap-preflight") && !string_contains(s: sh, pattern: "whoami") - && !string_contains(s: sh, pattern: "sudo -n -l /usr/bin/tailscale >/dev/null 2>&1") - && !string_contains(s: sh, pattern: "sudo -n /usr/sbin/visudo -V >/dev/null") && !string_contains(s: sh, pattern: "deploy-access-preflight: PASS") - && string_contains(s: sh, pattern: "sudo -n /usr/sbin/visudo -cf") + && string_contains(s: sh, pattern: witness_sudoers_stage_visudo_check_text()) && string_contains(s: sh, pattern: "/etc/sudoers.d/gunbc-deploy") && !string_contains(s: sh, pattern: "gunbc-deploy-privileged") } @@ -366,18 +385,19 @@ test fn witness_apply_emits_no_embedded_preflight_and_only_sudoers() -> Bool { // per-claim ceiling of 72,300 (required floor run 34927164902), and the program carries nothing else // this claim names. // -// THE PREAMBLE IS LITERALLY SHARED, which is what makes the two absence conjuncts hold at this grain +// THE PREAMBLE IS LITERALLY SHARED, which is what makes the absence conjunct hold at this grain // rather than merely cost less: `live_deploy_retract_intent` concatenates -// `deploy_apply_preamble_steps` -- the same steps the apply fold uses -- so a bootstrap preflight or -// a tailscale sudo probe creeping back in would appear in exactly the text read here. The retract's -// own remaining steps are artifact teardowns, which carry neither. +// `deploy_apply_preamble_steps` -- the same steps the apply fold uses -- so a bootstrap preflight +// creeping back in would appear in exactly the text read here. The retract's own remaining steps are +// artifact teardowns, which carry none. (The tailscale sudo-probe negative is gone for the reason +// witness_apply_emits_no_embedded_preflight_and_only_sudoers states: that probe is now emitted on +// purpose after the install.) test fn witness_retract_preamble_acknowledges_shared_sudoers_install() -> Bool { let frame = pipeline_steps_text(steps: deploy_retract_frame_comments(spec: deployment_spec_srv1())) let preamble = witness_apply_preamble_text() string_contains(s: frame, pattern: "first retract-after-flip may be the dropin install") && !string_contains(s: preamble, pattern: "deploy-access-bootstrap-preflight") - && !string_contains(s: preamble, pattern: "sudo -n -l /usr/bin/tailscale >/dev/null 2>&1") - && string_contains(s: preamble, pattern: "sudo -n /usr/sbin/visudo -cf") + && string_contains(s: preamble, pattern: witness_sudoers_stage_visudo_check_text()) } // THE PREAMBLE, READ SEPARATELY, SO AN ABSENCE CLAIM DOES NOT LOSE HALF ITS SUBJECT. @@ -816,19 +836,38 @@ fn spec_owned_paths(spec: DeploymentSpec) -> List { } +// THE TWIN MIRRORS PRODUCTION AS AN IDENTITY JOIN ON ARTIFACT KIND, NOT A COUNT. The host-singleton +// members -- the fabric storage placement's two and, since #12747, the approval broker's front door -- +// are named by the functions that decide them and subtracted from production by path; every other +// production member must have a twin member of the same kind and vice versa, and the twin must own +// no host singleton at all. The count form this replaces (`twin + fabric == live`) went red the day +// #12747 added a second host-scoped member, though nothing about the twin was wrong. +fn live_host_singleton_paths(live: DeploymentSpec) -> List { + concat( + deployment_fabric_storage_steps(instance: srv1_live_dashboard_instance(), names: live.names), + deployment_approval_broker_front_door_steps(instance: srv1_live_dashboard_instance()), + ) |> map(s => s.path as String) +} + test fn a_twin_deployment_on_one_host_collides_with_nothing() -> Bool { let live = deployment_spec_srv1() let twin = srv1_twin_spec() let live_paths = spec_owned_paths(spec: live) let twin_paths = spec_owned_paths(spec: twin) + let singletons = live_host_singleton_paths(live: live) + let live_instance_scoped = filter(deployment_owned_steps_retract_order(spec: live), l => !any(singletons, h => h == (l.path as String))) + let twin_steps = deployment_owned_steps_retract_order(spec: twin) deployment_plan_host_identity(spec: live) == deployment_plan_host_identity(spec: twin) && !(deployment_plan_listen_port(spec: live) == deployment_plan_listen_port(spec: twin)) && !(live.service.unit_name == twin.service.unit_name) && !(live.service.repo_root == twin.service.repo_root) && !(live.service.serve_binary == twin.service.serve_binary) - && count(live_paths) > 0 - && count(twin_paths) + count(deployment_fabric_storage_steps(instance: srv1_live_dashboard_instance(), names: live.names)) == count(live_paths) + && count(live_instance_scoped) > 0 + && all(singletons, h => any(live_paths, l => l == h)) + && all(live_instance_scoped, l => any(twin_steps, t => t.kind == l.kind)) + && all(twin_steps, t => any(live_instance_scoped, l => l.kind == t.kind)) && count(deployment_fabric_storage_steps(instance: srv1_lab_dashboard_instance(), names: twin.names)) == 0 + && count(deployment_approval_broker_front_door_steps(instance: srv1_lab_dashboard_instance())) == 0 && all(twin_paths, t => !any(live_paths, l => l == t)) } @@ -1775,3 +1814,4 @@ test fn the_fabric_storage_unit_binds_its_door_socket_and_its_route_proxies_to_i && (route.backend as String) == join(["unix:", fabric_storage_door_socket() as String], "") && (match route.mount { ServeRootMount => true ServeSetPath { path: _ } => false }) } + diff --git a/dag/test/claim/runner/runner_lifecycle_witness_test.dag b/dag/test/claim/runner/runner_lifecycle_witness_test.dag index 0f3079993e4..df4bf6e4b61 100644 --- a/dag/test/claim/runner/runner_lifecycle_witness_test.dag +++ b/dag/test/claim/runner/runner_lifecycle_witness_test.dag @@ -4,6 +4,7 @@ import std.logic { Bool } import std.types { list_length, NonEmptyStr } import std.measure { byte_size, byte_size_count } import gunbc.runner_unit { runner_slot_unit_name } +import gunbc.runner_slot_allocation { srv3_fabric_first_slot, srv4_fabric_first_slot } import gunbc.runner_lifecycle { runner_slot_add_sequence_for, RunnerSlotAddSequence, @@ -171,13 +172,25 @@ fn sequence_registers_with_github(seq: RunnerSlotAddSequence) -> Bool { // them registrable. // // BOTH DIRECTIONS ARE ASSERTED BECAUSE ONLY ONE OF THEM IS THE SAFETY CLAIM AND ONLY THE OTHER -// PROVES THE GATE IS NOT VACUOUS. A gate that refused everything would satisfy the fabric row -// alone; srv4-06 is the control -- the same slot index on a host of the same committed width -- -// and it must still receive the full twelve-step GitHub sequence. +// PROVES THE GATE IS NOT VACUOUS. A gate that refused everything would satisfy the fabric rows +// alone, so each fabric member is paired with a control on ITS OWN HOST: the slot one below it. +// +// BOTH SIDES ARE READ FROM THE MEMBERSHIP AUTHORITY, NOT TYPED AS SLOT LITERALS. The previous +// control was srv4-06, chosen as "the same index on a host of the same width" when srv4 carried +// no fabric member; gunbc.runner_slot_allocation srv4_fabric_first_slot made srv4-06 a fabric +// slot on 2026-09-18, the control became a second fabric row, and the claim went red for a reason +// it had nothing to say about. Deriving the subjects from srv3_fabric_first_slot and +// srv4_fabric_first_slot means a moved fabric member moves the claim with it; the control below +// each is a GitHub member by construction of the carve (the second fabric member sits ABOVE the +// first, gunbc.runner_slot_allocation fabric_second_slot_index). +fn fabric_member_refuses_and_its_neighbour_registers(fabric: RunnerSlotIdentity) -> Bool { + !sequence_registers_with_github(seq: runner_slot_add_sequence_for(host: fabric.host, slot_index: fabric.slot_index)) + && sequence_registers_with_github(seq: runner_slot_add_sequence_for(host: fabric.host, slot_index: fabric.slot_index - 1)) +} + test fn the_fabric_slot_cannot_be_registered_and_the_gate_is_not_vacuous() -> Bool { - !sequence_registers_with_github(seq: runner_slot_add_sequence_for(host: operator_host_srv3, slot_index: 6)) - && sequence_registers_with_github(seq: runner_slot_add_sequence_for(host: operator_host_srv4, slot_index: 6)) - && sequence_registers_with_github(seq: runner_slot_add_sequence_for(host: operator_host_srv3, slot_index: 5)) + fabric_member_refuses_and_its_neighbour_registers(fabric: srv3_fabric_first_slot()) + && fabric_member_refuses_and_its_neighbour_registers(fabric: srv4_fabric_first_slot()) } // THE COLLISION ROW. Before the suffix fork was dissolved, runner_incarnation enumerated indices 1 @@ -211,3 +224,4 @@ test fn the_suffix_is_unchanged_across_the_committed_range() -> Bool { && runner_slot_index_suffix(index: 5) == "05" && runner_slot_index_suffix(index: 6) == "06" } + diff --git a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag index 1b6391e0194..b91b3bf0128 100644 --- a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag +++ b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag @@ -1013,7 +1013,7 @@ data unimported_bare_provider_dispositions: List Date: Thu, 1 Oct 2026 13:18:51 +0000 Subject: [PATCH 2/2] Split the two cost-debt-rostered sudoers claims out of this PR The floor's changed cost-debt edit judgment lexes the whole 125 KB emit_test.dag at base and at head per identity in the interpreter; with these two identities changed, site projection ran past the 90-minute cap (run 36856989404). Their fix moves to its own PR, held on that floor defect. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/test/claim/live_deploy/emit_test.dag | 41 +++++++----------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/dag/test/claim/live_deploy/emit_test.dag b/dag/test/claim/live_deploy/emit_test.dag index f8d169863b1..02e3cf83b37 100644 --- a/dag/test/claim/live_deploy/emit_test.dag +++ b/dag/test/claim/live_deploy/emit_test.dag @@ -87,8 +87,7 @@ import extdeps.tailscale.serve { } import extdeps.exec.command { shell_quote } import extdeps.posix.shell_command_language { posix_single_quote } -import gunbc.shell_command_text { shell_privileged_command_text_of_argv, shell_command_text_of_node } -import gunbc.ci_deploy_sudoers { deploy_sudoers_elevated } +import gunbc.shell_command_text { shell_privileged_command_text_of_argv } import gunbc.live_deploy.operations { install_directory_command, rm_force_command, @@ -153,7 +152,7 @@ import gunbc.ci_deploy_access { deploy_access_job_principal_refused_emit_poison, ci_deploy_srv1_access_or_refusal, DeployAccessReady, DeployAccessJobPrincipalRefused, } -import gunbc.fleet_posix_accounts { DeployRunnerPrincipal, ci_runner_sudo_binary_path, SudoVisudo } +import gunbc.fleet_posix_accounts { DeployRunnerPrincipal } import gunbc.host_axis_caps { deploy_memory_cap_apply_script_for_host, deploy_memory_cap_shadow_revert_script, @@ -349,31 +348,14 @@ test fn witness_dependency_ensure_is_idempotent() -> Bool { && string_contains(s: tmux, pattern: deploy_effect_command(inv: apt_install_unelevated_effect(package: package_tmux))) } -// THE STAGED-FILE VALIDATION, AS THE INSTALL ITSELF BUILDS IT. The install step is a bash command -// node built by gunbc.ci_deploy_sudoers deploy_sudoers_elevated over the sudo elevation argv and the -// fleet's visudo row; this is that same node with the same argv, rendered by the same serializer, so -// the needle is the producer's own words rather than a hand spelling of them. The hand spelling -// `sudo -n /usr/sbin/visudo -cf` went stale twice without the step changing meaning: #12602 spelled -// sudo by its absolute path (extdeps.sudo sudo_program), and #12525 moved the install onto the bash -// builder, which quotes every literal word. The stage path is a variable word the claim does not name. -fn witness_sudoers_stage_visudo_check_text() -> String { - shell_command_text_of_node(stmt: deploy_sudoers_elevated(command: [ci_runner_sudo_binary_path(b: SudoVisudo) as String, "-cf"], tail: [])) -} - -// THE TWO PROBE NEGATIVES THIS CLAIM CARRIED ARE DELETED, NOT RE-SPELLED. They banned -// `sudo -n /usr/sbin/visudo -V` and `sudo -n -l /usr/bin/tailscale` -- the bootstrap preflight's -// probes. Under the quoting serializer neither spelling can occur, so both were permanently true and -// carried nothing. Re-spelling them in the quoted form would be WRONG rather than merely redundant: -// since #12168 those exact probes are emitted on purpose AFTER the drop-in lands -// (gunbc.ci_deploy_sudoers deploy_sudoers_post_install_roster_probe_stmts). Their ORDER is the -// safety fact, and test.claim.deploy_mutation_gate_witness owns it; what this claim keeps is that -// no embedded preflight block exists, by its markers. test fn witness_apply_emits_no_embedded_preflight_and_only_sudoers() -> Bool { let sh = witness_apply_script() !string_contains(s: sh, pattern: "deploy-access-bootstrap-preflight") && !string_contains(s: sh, pattern: "whoami") + && !string_contains(s: sh, pattern: "sudo -n -l /usr/bin/tailscale >/dev/null 2>&1") + && !string_contains(s: sh, pattern: "sudo -n /usr/sbin/visudo -V >/dev/null") && !string_contains(s: sh, pattern: "deploy-access-preflight: PASS") - && string_contains(s: sh, pattern: witness_sudoers_stage_visudo_check_text()) + && string_contains(s: sh, pattern: "sudo -n /usr/sbin/visudo -cf") && string_contains(s: sh, pattern: "/etc/sudoers.d/gunbc-deploy") && !string_contains(s: sh, pattern: "gunbc-deploy-privileged") } @@ -385,19 +367,18 @@ test fn witness_apply_emits_no_embedded_preflight_and_only_sudoers() -> Bool { // per-claim ceiling of 72,300 (required floor run 34927164902), and the program carries nothing else // this claim names. // -// THE PREAMBLE IS LITERALLY SHARED, which is what makes the absence conjunct hold at this grain +// THE PREAMBLE IS LITERALLY SHARED, which is what makes the two absence conjuncts hold at this grain // rather than merely cost less: `live_deploy_retract_intent` concatenates -// `deploy_apply_preamble_steps` -- the same steps the apply fold uses -- so a bootstrap preflight -// creeping back in would appear in exactly the text read here. The retract's own remaining steps are -// artifact teardowns, which carry none. (The tailscale sudo-probe negative is gone for the reason -// witness_apply_emits_no_embedded_preflight_and_only_sudoers states: that probe is now emitted on -// purpose after the install.) +// `deploy_apply_preamble_steps` -- the same steps the apply fold uses -- so a bootstrap preflight or +// a tailscale sudo probe creeping back in would appear in exactly the text read here. The retract's +// own remaining steps are artifact teardowns, which carry neither. test fn witness_retract_preamble_acknowledges_shared_sudoers_install() -> Bool { let frame = pipeline_steps_text(steps: deploy_retract_frame_comments(spec: deployment_spec_srv1())) let preamble = witness_apply_preamble_text() string_contains(s: frame, pattern: "first retract-after-flip may be the dropin install") && !string_contains(s: preamble, pattern: "deploy-access-bootstrap-preflight") - && string_contains(s: preamble, pattern: witness_sudoers_stage_visudo_check_text()) + && !string_contains(s: preamble, pattern: "sudo -n -l /usr/bin/tailscale >/dev/null 2>&1") + && string_contains(s: preamble, pattern: "sudo -n /usr/sbin/visudo -cf") } // THE PREAMBLE, READ SEPARATELY, SO AN ABSENCE CLAIM DOES NOT LOSE HALF ITS SUBJECT.