diff --git a/dag/extdeps/exec/program.dag b/dag/extdeps/exec/program.dag index 320bdf8aa76..d7384a029d0 100644 --- a/dag/extdeps/exec/program.dag +++ b/dag/extdeps/exec/program.dag @@ -132,6 +132,7 @@ fn uncataloged_program(invocation: NonEmptyStr) -> ProgramIdentity decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mkdir_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mv_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "sort_program"), + decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "tee_program"), decl_ref(module_path: "extdeps.tools.ldd", decl_name: "ldd_program"), decl_ref(module_path: "extdeps.tools.dpkg_deb", decl_name: "dpkg_deb_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "printf_program"), @@ -140,7 +141,6 @@ fn uncataloged_program(invocation: NonEmptyStr) -> ProgramIdentity decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "rmdir_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "dd_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "timeout_program"), - decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "tee_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "true_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "whoami_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "pwd_program"), diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index 40ca0b403d4..cdda4ad0ad0 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -377,6 +377,10 @@ data excl_install_media_reason: String = "real host-effect execution witness (sh data excl_local_repo_wet_reason: String = "real host-effect execution witness over a THROWAWAY LOCAL REPOSITORY -- refused by the hermetic envelope, so excluded from the discovery corpus, and executed instead by the required floor's local-repo wet lane (v2.workflow.local_repo_wet_terminal local_repo_wet_schedule). This row is NOT the dark bin-wet class: every function in the named file is on that schedule, the floor decodes it, runs each member wet, and joins the observed terminals back to the roster at identity grain, so a member that stops executing refuses rather than reading as covered." +data excl_local_repo_wet_browser_ready_label_reason: String = "real host-effect execution witness whose effect is ARGV EXECUTION THAT WRITES INSIDE A THROWAWAY TEMP DIRECTORY -- the readiness-label converge (gunbc.runner_browser_toolchain browser_ready_labels_converge_wet) driven over LocalShell with the unprivileged posture parameterization against a mktemp scratch root: dd writes the scratch drop-in, cat reads it back, rm -f removes it, the scratch is removed. Refused by the hermetic envelope (shell.Exec over argv and shell.Mktemp.Dir have no mock_response), so excluded from the discovery corpus and executed by the required floor's local-repo wet lane. IT IS A DISTINCT ADMITTED EFFECT FROM ITS NEIGHBOURS AND IS NAMED RATHER THAN FOLDED IN: unlike excl_local_repo_wet_tempdir_write_reason the writes go through EXECUTED ARGV (dd/cat/rm over the host-effect transport), not a Filesystem.Write call; unlike excl_local_repo_wet_bash_materialized_reason a file IS written and read; unlike excl_local_repo_wet_host_probe_reason the probe writes. What admits it to the same lane is the shared negative half: no network, no cargo, no remote host, no install media, no sudo (the unprivileged posture skips daemon-reload with a typed cause), and nothing outside a directory the witness created and removes. The production FLEET route is a different row's subject: the declared stall is rostered as gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall." + +data excl_local_repo_wet_browser_ready_label_dissolve: DissolutionCondition = unbound_dissolution(description: "mock_response coverage lands for the argv-over-host-effect-transport legs and shell.Mktemp.Dir, sufficient for the converge's write-then-proceed and remove-then-proceed folds to be asserted under fixed mocks at the scratch destination; then these fns re-enroll as ordinary hermetic discovery rows, drop off local_repo_wet_schedule, and this row deletes. The FleetSsh production route's own stall (gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall) is NOT this row's trigger: the receipts keep executing over LocalShell regardless of when the shared transport admits that arm.") + data excl_local_repo_wet_tempdir_write_reason: String = "real host-effect execution witness whose effect is a FILESYSTEM WRITE INTO A THROWAWAY TEMP DIRECTORY -- refused by the hermetic envelope (Filesystem.Write and shell.Mktemp.Dir have no mock_response), so excluded from the discovery corpus and executed by the required floor's local-repo wet lane. THIS IS A DISTINCT ADMITTED EFFECT FROM BOTH NEIGHBOURS AND IS NAMED RATHER THAN FOLDED INTO EITHER: it is not excl_local_repo_wet_reason's throwaway REPOSITORY property -- no repository is built and no git operation runs -- and it is not excl_local_repo_wet_host_probe_reason, which admits read-only probes that write NOTHING. What admits it to the same lane is the negative half those rows share: no network, no cargo, no remote host, no install media, and nothing outside a directory the witness created and removes. IT IS ENROLLED IN THE IDENTITY-GRAIN LANE DELIBERATELY rather than in the dark bin-wet class, because the whole purpose of the witness is that a wall which stops executing must REFUSE rather than read as covered -- enrolling a witness about silent absence somewhere it could be silently absent would reproduce its own subject." // Capture verification hashes the supplied bytes through stdin. The capture @@ -1243,6 +1247,12 @@ data witness_exclusion_frontier: List = [ classification: LocalRepoWetLane, reason: excl_local_repo_wet_reason, dissolution: excl_local_repo_wet_dissolve}, + WitnessExclusionRow { + pattern: "browser_ready_label_real_path_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_browser_ready_label_reason, + dissolution: excl_local_repo_wet_browser_ready_label_dissolve, + }, WitnessExclusionRow { pattern: "roadmap_belt_base_advance_wet_witness_test.dag", classification: LocalRepoWetLane, diff --git a/dag/gunbc/guarantee_stall/browser_ready_labels_fleet_route_stall.dag b/dag/gunbc/guarantee_stall/browser_ready_labels_fleet_route_stall.dag new file mode 100644 index 00000000000..505ae94c08a --- /dev/null +++ b/dag/gunbc/guarantee_stall/browser_ready_labels_fleet_route_stall.dag @@ -0,0 +1,31 @@ +module gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall + +import gunbc.guarantee_rung { MechanicallyPreventable, StructurallyGuaranteed } +import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, BoundedPopulation, climbs_when } + +// THE READINESS-LABEL CONVERGE'S PRODUCTION FLEET ROUTE EXECUTES NOWHERE — A PRE-EXISTING STALL, +// NOT AN EVENT (review round on gunbc#13401; the side chat's ruling: a gap that never held is a +// GuaranteeStall awaiting one grounding, not a RungDrop, because nothing that held stopped +// holding). In a real bootstrap the converge's transport is always FleetSsh +// (browser_toolchain_admin_transport), and BOTH shared host-effect runners -- the argv runner and +// the argv+stdin runner -- return RunArgvTransportRefused for FleetSsh before any leg runs; the +// arm's own fail-closed note says admitting that capability "is its own change with its own +// analysis". So grant, revoke and daemon-reload never execute over fleet, on CI or in a bootstrap, +// and the route's outcome is always BrowserReadyLabelsFailed there. The deleted shell path was +// carried by the same posture, so this change newly breaks nothing -- it declares what the route +// already was. The steps DO execute for real in the changed-witness receipts +// (test.claim.runner.browser_ready_label_real_path_witness): the production converge over +// LocalShell, the unprivileged posture, against a mktemp scratch destination, with the unprivileged +// reload honestly skipped as a typed posture arm. +data browser_ready_labels_fleet_route_stall: GuaranteeStall = GuaranteeStall { + subject: "The runner readiness-label converge over its production fleet route: every decision is typed and the step pairing is executed for real in the LocalRepoWet receipts over LocalShell against a scratch destination, but the production route itself -- whose transport is always FleetSsh -- is refused by both shared host-effect runners before any leg, so the converge executes nowhere over fleet and its outcome there is always BrowserReadyLabelsFailed.", + current: MechanicallyPreventable, + ceiling: StructurallyGuaranteed, + blocker: AwaitsOneGrounding { + grounding: "the shared host-effect FleetSsh argv and argv+stdin realization, plus a safe daemon-reload realization", + }, + population: BoundedPopulation { members: [ + "gunbc.runner_browser_toolchain::browser_ready_labels_converge_wet over browser_ready_labels_production_target", + ] }, + next_rung_trigger: climbs_when(capability: "the shared fleet_subsumption route admits FleetSsh argv and argv+stdin, safely realizes reload, and an executing fleet receipt traverses the route."), +} diff --git a/dag/gunbc/guarantee_stall/roster.dag b/dag/gunbc/guarantee_stall/roster.dag index 45f1ec36561..24d395a518d 100644 --- a/dag/gunbc/guarantee_stall/roster.dag +++ b/dag/gunbc/guarantee_stall/roster.dag @@ -15,6 +15,7 @@ import gunbc.guarantee_stall { GuaranteeStall } import gunbc.guarantee_stall.pci_address_rendering_equivalence_stall { pci_address_rendering_equivalence_stall } import gunbc.guarantee_stall.fabric_purpose_handover_registration_unobserved_stall { fabric_purpose_handover_registration_unobserved_stall } import gunbc.guarantee_stall.emission_provenance_missing_consumer { emission_provenance_missing_consumer } +import gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall { browser_ready_labels_fleet_route_stall } import gunbc.guarantee_stall.emit_returns_medium_not_contribution_stall { emit_returns_medium_not_contribution_stall } import gunbc.guarantee_stall.emission_apply_generic_r_uninstantiated_stall { emission_apply_generic_r_uninstantiated_stall } import gunbc.guarantee_stall.pre_resolve_entry_closure_module_identity_stall { pre_resolve_entry_closure_module_identity_stall } @@ -243,6 +244,7 @@ data all_guarantee_stalls: List = [ emitted_inline_crate_path_edges_unreturned_stall, test_reference_ledger_typed_census_stall, compile_pool_provision_wired_but_unreachable_stall, + browser_ready_labels_fleet_route_stall, accumulator_copy_untyped_combiner_refusals_stall, keyed_apply_patch_rebuilds_rows_per_hunk_stall, ] diff --git a/dag/gunbc/runner/runner_browser_toolchain.dag b/dag/gunbc/runner/runner_browser_toolchain.dag index d5ab4dd239e..cabdb26adf0 100644 --- a/dag/gunbc/runner/runner_browser_toolchain.dag +++ b/dag/gunbc/runner/runner_browser_toolchain.dag @@ -76,11 +76,14 @@ import gunbc.clock_read { clock_now_probed_at, probed_at_word } import v2.std.node { Node } import gunbc.runner_unit { runner_unit_dropin_path } import gunbc.runner_unit_file { runner_host_browser_ready_labels_dropin_text, RunnerUnitText, RunnerUnitTextRefused } -import gunbc.shell_command_text { shell_command_intent, shell_privileged_command_intent, shell_command_text_of_stmts } -import gunbc.retained_shell_script { retained_srvn } -import gunbc.host_effect_realize { run_shell_transport, ShellExecObserved } -import extdeps.systemd.systemctl { systemctl_daemon_reload_argv } -import v2.extdeps.languages.bash_build { bash_build_pipe, bash_build_with_redir_to_file, bash_build_word_lit } +import gunbc.host_effect_realize { + run_argv_over_host_effect_transport, run_argv_stdin_over_host_effect_transport, + run_argv_transport_succeeded, RunArgvTransportOutcome, +} +import extdeps.exec.command { ArgvCommand, argv_words } +import extdeps.sudo.elevation { sudo_elevate_command } +import extdeps.systemd.systemctl { systemctl_daemon_reload_command } +import extdeps.tools.gnu_coreutils { dd_overwrite_path_command, rm_force_command } // THE DECLARED HEADLESS-BROWSER TOOLCHAIN OF A FLEET RUNNER, ITS CONVERGE, AND THE ONE PLACE A // CONSUMER READS IT FROM. gunbc.machine_intake_megarac_kvm_still drives the MegaRAC KVM viewer @@ -1225,38 +1228,81 @@ fn superseded_trees_text(r: SupersededTrees) -> String { // runner whose toolchain is absent can no longer take the floor's real-Chromium wet claims. data browser_ready_labels_dropin_path: NonEmptyStr = runner_unit_dropin_path(file_name: "85-browser-ready-labels.conf" as NonEmptyStr) +// THE ELEVATION POSTURE IS A PARAMETER, NOT A GLOBAL: SudoElevated is the production posture -- +// the legs wrap in sudo -n through sudo_elevate_command, the carrier the deleted privileged +// intents rendered; Unprivileged is the receipt posture -- the SAME program and arguments, bare, +// so a scratch-root receipt executes the same legs without touching the host's configuration. +// The reload leg answers the posture: an unprivileged receipt SKIPS daemon-reload with a typed +// cause rather than fabricating a reload it did not run. +type BrowserReadyLabelsPosture + = ReadyLabelsSudoElevated + | ReadyLabelsUnprivileged + +type BrowserReadyLabelsReload + = ReadyLabelsReloadRun { command: ArgvCommand } + | ReadyLabelsReloadPostureSkipped { cause: String } + +// THE DESTINATION IS A PARAMETER TOO. The production target names the privileged system constant; +// a receipt names a path inside a mktemp scratch root, so the converge's legs execute for real +// against a directory the receipt created and removes. +type BrowserReadyLabelsTarget { + dropin: NonEmptyStr + posture: BrowserReadyLabelsPosture +} + +data browser_ready_labels_production_target: BrowserReadyLabelsTarget = BrowserReadyLabelsTarget { + dropin: browser_ready_labels_dropin_path, + posture: ReadyLabelsSudoElevated, +} + type BrowserReadyLabelsPlan - = BrowserReadyLabelsPlanned { script: String } + = BrowserReadyLabelsGrantReady { write: ArgvCommand, content: String, reload: BrowserReadyLabelsReload } + | BrowserReadyLabelsRevokeReady { remove: ArgvCommand, reload: BrowserReadyLabelsReload } | BrowserReadyLabelsUnrenderable { cause: String } -// Both plans are one modeled program under set -e: a failed write stops it, and the verdict sees it. -fn browser_ready_labels_grant_plan(host: HostIdentity) -> BrowserReadyLabelsPlan { +// THE PLANS ARE TYPED ARGV STEPS, NOT SCRIPT TEXT. The deleted plan rendered intents to a script +// string and shipped it through the retained-shell wall; the steps were already modeled, so the +// text was a lossy projection of something the type system already held. The grant is two steps: +// the drop-in bytes reach dd of=PATH status=none on STDIN (the same overwrite the deleted +// `printf | sudo tee >/dev/null` performed -- dd over tee because tee echoes the payload to +// stdout, which would carry the file contents into the command log; status=none carries the old +// >/dev/null suppression, and the bytes cross as data, never as script text, so a unit body +// carrying quotes or metacharacters cannot change the command's meaning), then an elevated +// daemon-reload. The revoke is the elevated rm -f, then the +// same reload. The deleted plan's leading `set -e` (stop at the first failed step) is modeled by +// the converge's own fold: each leg runs only if the previous succeeded, and a failed leg is the +// failed outcome. Elevation keeps the sudo -n posture through sudo_elevate_command, the same +// carrier the deleted privileged intents rendered. +fn browser_ready_labels_posture_argv(posture: BrowserReadyLabelsPosture, command: ArgvCommand) -> ArgvCommand { + match posture { + ReadyLabelsSudoElevated => sudo_elevate_command(command: argv_words(command: command)) + ReadyLabelsUnprivileged => command + } +} + +fn browser_ready_labels_reload_leg(posture: BrowserReadyLabelsPosture) -> BrowserReadyLabelsReload { + match posture { + ReadyLabelsSudoElevated => ReadyLabelsReloadRun { command: systemctl_daemon_reload_command() } + ReadyLabelsUnprivileged => ReadyLabelsReloadPostureSkipped { cause: "unprivileged posture: daemon-reload requires root and is skipped, not fabricated" as String } + } +} + +fn browser_ready_labels_grant_plan(host: HostIdentity, target: BrowserReadyLabelsTarget) -> BrowserReadyLabelsPlan { match runner_host_browser_ready_labels_dropin_text(host: host) { RunnerUnitTextRefused { reason: r } => BrowserReadyLabelsUnrenderable { cause: r as String } RunnerUnitText { text: content } => - BrowserReadyLabelsPlanned { - script: shell_command_text_of_stmts(stmts: [ - shell_command_intent(words: ["set", "-e"]), - bash_build_pipe( - left: shell_command_intent(words: ["printf", "%s", content]), - right: bash_build_with_redir_to_file( - inner: shell_privileged_command_intent(argv: ["tee", browser_ready_labels_dropin_path as String]), - path: bash_build_word_lit(text: "/dev/null"), - ), - ), - shell_privileged_command_intent(argv: systemctl_daemon_reload_argv()), - ]), + BrowserReadyLabelsGrantReady { + write: browser_ready_labels_posture_argv(posture: target.posture, command: dd_overwrite_path_command(path: target.dropin as String)), + content: content, + reload: browser_ready_labels_reload_leg(posture: target.posture), } } } -fn browser_ready_labels_revoke_plan() -> BrowserReadyLabelsPlan { - BrowserReadyLabelsPlanned { - script: shell_command_text_of_stmts(stmts: [ - shell_command_intent(words: ["set", "-e"]), - shell_privileged_command_intent(argv: ["rm", "-f", browser_ready_labels_dropin_path as String]), - shell_privileged_command_intent(argv: systemctl_daemon_reload_argv()), - ]), +fn browser_ready_labels_revoke_plan(target: BrowserReadyLabelsTarget) -> BrowserReadyLabelsPlan { + BrowserReadyLabelsRevokeReady { + remove: browser_ready_labels_posture_argv(posture: target.posture, command: rm_force_command(path: target.dropin as String)), + reload: browser_ready_labels_reload_leg(posture: target.posture), } } @@ -1284,22 +1330,53 @@ fn browser_ready_labels_outcome_holds(o: BrowserReadyLabelsOutcome, ready: Bool) } } -fn browser_ready_labels_converge_wet(host: NonEmptyStr, ready: Bool, transport: HostEffectTransport) -> BrowserReadyLabelsOutcome { - let plan = if ready { browser_ready_labels_grant_plan(host: (host as String) as HostIdentity) } else { browser_ready_labels_revoke_plan() } + +fn browser_ready_labels_reload_then(success: BrowserReadyLabelsOutcome, reload: BrowserReadyLabelsReload, transport: HostEffectTransport) -> BrowserReadyLabelsOutcome { + match reload { + ReadyLabelsReloadRun { command: rc } => { + let reloaded = run_argv_over_host_effect_transport(command: rc, transport: transport) + if run_argv_transport_succeeded(outcome: reloaded) { + success + } else { + browser_ready_labels_step_failed(step: "daemon-reload", outcome: reloaded) + } + } + ReadyLabelsReloadPostureSkipped { cause: _ } => success + } +} + +fn browser_ready_labels_converge_wet(host: NonEmptyStr, ready: Bool, transport: HostEffectTransport, target: BrowserReadyLabelsTarget) -> BrowserReadyLabelsOutcome { + let plan = if ready { browser_ready_labels_grant_plan(host: (host as String) as HostIdentity, target: target) } else { browser_ready_labels_revoke_plan(target: target) } match plan { BrowserReadyLabelsUnrenderable { cause: c } => BrowserReadyLabelsFailed { cause: concat("the readiness labels drop-in does not render: ", c) } - BrowserReadyLabelsPlanned { script: body } => { - let observed = run_shell_transport( - script: retained_srvn(body: body, reason: "browser readiness label grant/revoke: template drop-in write and daemon-reload over the administrator edge" as NonEmptyStr), - transport: transport, - ) - if observed.success { - if ready { BrowserReadyLabelsGranted } else { BrowserReadyLabelsRevoked } + BrowserReadyLabelsGrantReady { write: w, content: payload, reload: r } => { + let written = run_argv_stdin_over_host_effect_transport(command: w, stdin_payload: payload, transport: transport) + if run_argv_transport_succeeded(outcome: written) { + browser_ready_labels_reload_then(success: BrowserReadyLabelsGranted, reload: r, transport: transport) } else { - BrowserReadyLabelsFailed { cause: join(["exit=", to_string(observed.exit_code), " ", observed.stderr], "") } + browser_ready_labels_step_failed(step: "the readiness labels drop-in write", outcome: written) } } + BrowserReadyLabelsRevokeReady { remove: rm, reload: r } => { + let removed = run_argv_over_host_effect_transport(command: rm, transport: transport) + if run_argv_transport_succeeded(outcome: removed) { + browser_ready_labels_reload_then(success: BrowserReadyLabelsRevoked, reload: r, transport: transport) + } else { + browser_ready_labels_step_failed(step: "the readiness labels drop-in removal", outcome: removed) + } + } + } +} + +// ONE FAILURE SHAPE FOR EVERY LEG: the refused arm carries the wall's own admission text (the +// broken wall, not a connection error), the observed arm carries the real exit status and stderr. +fn browser_ready_labels_step_failed(step: String, outcome: RunArgvTransportOutcome) -> BrowserReadyLabelsOutcome { + match outcome { + RunArgvTransportRefused { reason: why } => + BrowserReadyLabelsFailed { cause: join([step, ": the transport refused before the leg ran: ", why], "") } + RunArgvTransportObserved { observed: o } => + BrowserReadyLabelsFailed { cause: join([step, ": exit=", to_string(o.exit_code), " ", o.stderr], "") } } } @@ -1348,7 +1425,7 @@ fn runner_browser_toolchain_converge_ci_wet() -> ProcessExit none } let ready = match standing { Present { value: st } => runner_browser_toolchain_ready(s: st) Absent => false } - let labels = browser_ready_labels_converge_wet(host: host, ready: ready, transport: browser_toolchain_admin_transport(host: host, context: context)) + let labels = browser_ready_labels_converge_wet(host: host, ready: ready, transport: browser_toolchain_admin_transport(host: host, context: context), target: browser_ready_labels_production_target) let retirement_text = superseded_trees_text(r: observe_superseded_trees_wet(location: location)) let readback_text = match standing { Present { value: st } => runner_browser_toolchain_standing_text(s: st) diff --git a/dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag b/dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag new file mode 100644 index 00000000000..faa23b81937 --- /dev/null +++ b/dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag @@ -0,0 +1,92 @@ +module test.claim.runner.browser_ready_label_real_path_witness + +import std.types { String, NonEmptyStr, Bool, Int, List } +import extdeps.shell +import gunbc.fleet_host_identity { operator_host_srv3 } +import gunbc.runner_browser_toolchain { + BrowserReadyLabelsOutcome, BrowserReadyLabelsGranted, BrowserReadyLabelsRevoked, BrowserReadyLabelsFailed, + BrowserReadyLabelsTarget, ReadyLabelsUnprivileged, + browser_ready_labels_converge_wet, browser_ready_labels_outcome_holds, +} +import gunbc.host_effect_realize { + run_argv_over_host_effect_transport, RunArgvTransportOutcome, RunArgvTransportObserved, RunArgvTransportRefused, +} +import gunbc.host_effect { HostEffectTransport, LocalShell } +import extdeps.tools.gnu_coreutils { cat_command } + +// THE READINESS-LABEL CONVERGE EXECUTES FOR REAL HERE. This file is enrolled in three channels, +// as for chunk_30's precedent: the two WetScheduledClaim rows in v2.workflow.local_repo_wet_terminal +// local_repo_wet_schedule, the gunbc.ci_layer_roots LocalRepoWetLane row for +// browser_ready_label_real_path_witness_test.dag, and the floor_route_gap chunk expectation. What +// the receipts execute is the PRODUCTION converge (gunbc.runner_browser_toolchain +// browser_ready_labels_converge_wet) over LocalShell with the UNPRIVILEGED posture parameterization +// against a mktemp scratch root: the same plan functions, the same write-then-proceed and +// remove-then-proceed folds, the bytes read back from the scratch drop-in. The reload leg is the +// typed posture skip -- daemon-reload requires root and is skipped, not fabricated. The production +// fleet route itself stays the declared STALL (gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall): +// no CI lane, and no bootstrap, executes the converge over FleetSsh until the shared host-effect +// transport admits that arm. +fn fresh_root() -> String { + shell.Mktemp.DirWithTemplate(template: "/tmp/gunbc_browser_ready_label.XXXXXX").path +} + +fn scratch_target(scratch: String) -> BrowserReadyLabelsTarget { + BrowserReadyLabelsTarget { + dropin: (concat(scratch, "/85-browser-ready-labels.conf")) as NonEmptyStr, + posture: ReadyLabelsUnprivileged, + } +} + +fn read_dropin_bytes(dropin: NonEmptyStr) -> RunArgvTransportOutcome { + run_argv_over_host_effect_transport(command: cat_command(path: dropin as String), transport: LocalShell) +} + +fn observed_stdout_of(outcome: RunArgvTransportOutcome) -> String { + match outcome { + RunArgvTransportObserved { observed: o } => o.stdout + RunArgvTransportRefused { reason: _ } => "" + } +} + +fn observed_exit_zero(outcome: RunArgvTransportOutcome) -> Bool { + match outcome { + RunArgvTransportObserved { observed: o } => o.exit_code == 0 + RunArgvTransportRefused { reason: _ } => false + } +} + +fn scratch_removed(scratch: String) -> Bool { + shell.Remove.RecursiveForce(path: scratch as NonEmptyStr).success +} + +// Discriminating: a write that never reached the file (empty read-back), or a converge that did +// not return Granted (a refusal, a failed leg), reds. The second converge run re-renders the same +// label and overwrites the same bytes -- granting twice is a no-op, not a doubled append. +test fn grant_executes_the_dropin_write_for_real_on_a_scratch_root() -> Bool { + let scratch = fresh_root() + let target = scratch_target(scratch: scratch) + let first = browser_ready_labels_converge_wet(host: (operator_host_srv3 as String) as NonEmptyStr, ready: true, transport: LocalShell, target: target) + let second = browser_ready_labels_converge_wet(host: (operator_host_srv3 as String) as NonEmptyStr, ready: true, transport: LocalShell, target: target) + let bytes = read_dropin_bytes(dropin: target.dropin) + let removed = scratch_removed(scratch: scratch) + removed + && browser_ready_labels_outcome_holds(o: first, ready: true) + && browser_ready_labels_outcome_holds(o: second, ready: true) + && observed_exit_zero(outcome: bytes) + && string_contains(s: observed_stdout_of(outcome: bytes), pattern: "gunbc-browser-ready") +} + +// Discriminating: a grant whose bytes survive the revoke, a removal that never ran (the read-back +// still succeeds), or a converge that returned Failed instead of Revoked, reds. +test fn revoke_executes_the_dropin_removal_for_real_on_a_scratch_root() -> Bool { + let scratch = fresh_root() + let target = scratch_target(scratch: scratch) + let granted = browser_ready_labels_converge_wet(host: (operator_host_srv3 as String) as NonEmptyStr, ready: true, transport: LocalShell, target: target) + let revoked = browser_ready_labels_converge_wet(host: (operator_host_srv3 as String) as NonEmptyStr, ready: false, transport: LocalShell, target: target) + let bytes = read_dropin_bytes(dropin: target.dropin) + let removed = scratch_removed(scratch: scratch) + removed + && browser_ready_labels_outcome_holds(o: granted, ready: true) + && browser_ready_labels_outcome_holds(o: revoked, ready: false) + && if observed_exit_zero(outcome: bytes) { false } else { true } +} diff --git a/dag/test/claim/runner/runner_browser_ready_label_witness_test.dag b/dag/test/claim/runner/runner_browser_ready_label_witness_test.dag index c7a9cef5449..b03fa8497c3 100644 --- a/dag/test/claim/runner/runner_browser_ready_label_witness_test.dag +++ b/dag/test/claim/runner/runner_browser_ready_label_witness_test.dag @@ -11,37 +11,133 @@ import gunbc.runner_registration_labels { gunbc_browser_ready_label, } import gunbc.runner_browser_toolchain { - BrowserReadyLabelsPlan, BrowserReadyLabelsPlanned, BrowserReadyLabelsUnrenderable, + BrowserReadyLabelsPlan, BrowserReadyLabelsGrantReady, BrowserReadyLabelsRevokeReady, + BrowserReadyLabelsUnrenderable, BrowserReadyLabelsReload, ReadyLabelsReloadRun, ReadyLabelsReloadPostureSkipped, + BrowserReadyLabelsPosture, ReadyLabelsSudoElevated, ReadyLabelsUnprivileged, BrowserReadyLabelsTarget, browser_ready_labels_grant_plan, browser_ready_labels_revoke_plan, browser_ready_labels_outcome_holds, + browser_ready_labels_production_target, BrowserReadyLabelsGranted, BrowserReadyLabelsRevoked, BrowserReadyLabelsFailed, } +import extdeps.exec.command { argv_words } +import gunbc.filesystem_model { concat_lists } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // THE gunbc-browser-ready LABEL IS GRANTED BY A READY BROWSER-TOOLCHAIN CONVERGE AND NOTHING ELSE. -// Each claim supplies its subject at one interface: the plan text, the conformance relation, the -// converge verdict. The wet write is the converge's own and is not re-run here. +// Each claim supplies its subject at one interface: the plan's typed steps, the conformance +// relation, the converge verdict. The wet write is the converge's own and is not re-run here. +// EXECUTION STANDING: the converge's REAL executions are the wet receipts +// (test.claim.runner.browser_ready_label_real_path_witness) -- the production converge over +// LocalShell, the unprivileged posture, against a mktemp scratch destination. The production FLEET +// route is the declared stall gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall: both shared host-effect runners refuse FleetSsh before any leg, +// so the converge executes nowhere over fleet until that arm is admitted by its own change. +// The plan is typed argv steps (the retained-shell text is gone), so these claims read the STEPS: +// the elevated write's words, the drop-in bytes on stdin, the reload, the removal -- pure over the +// rendered plan, no transport. The revoke's no-write property is now TYPED: a RevokeReady carries +// no write leg at all, so a revoke that rewrites rather than removes cannot even be constructed. -fn plan_script(p: BrowserReadyLabelsPlan) -> String { +fn plan_write_words(p: BrowserReadyLabelsPlan) -> List { match p { - BrowserReadyLabelsPlanned { script: s } => s + BrowserReadyLabelsGrantReady { write: w, content: _, reload: _ } => argv_words(command: w) + BrowserReadyLabelsRevokeReady { remove: _, reload: _ } => [] + BrowserReadyLabelsUnrenderable { cause: _ } => [] + } +} + +fn plan_payload(p: BrowserReadyLabelsPlan) -> String { + match p { + BrowserReadyLabelsGrantReady { write: _, content: c, reload: _ } => c + BrowserReadyLabelsRevokeReady { remove: _, reload: _ } => "" BrowserReadyLabelsUnrenderable { cause: _ } => "" } } -// GRANT writes the template drop-in carrying the label and reloads; REVOKE removes the same path. -// Discriminating: a grant without the label, or a revoke that rewrote rather than removed, reds. +fn reload_words(r: BrowserReadyLabelsReload) -> List { + match r { + ReadyLabelsReloadRun { command: c } => argv_words(command: c) + ReadyLabelsReloadPostureSkipped { cause: _ } => [] + } +} + +fn plan_reload_words(p: BrowserReadyLabelsPlan) -> List { + match p { + BrowserReadyLabelsGrantReady { write: _, content: _, reload: r } => reload_words(r: r) + BrowserReadyLabelsRevokeReady { remove: _, reload: r } => reload_words(r: r) + BrowserReadyLabelsUnrenderable { cause: _ } => [] + } +} + +// THE POSTURE IS PART OF THE PLAN'S PAIRING, NOT A FLAG THE CALLER FORGETS: the production target +// elevates every leg and runs the reload; the unprivileged target runs the SAME programs bare and +// SKIPS the reload with a typed cause -- skipping is an arm, not a fabricated success. The +// scratch path here is an inert string: the plans are pure, no host effect is reached. +fn browser_ready_labels_unprivileged_target() -> BrowserReadyLabelsTarget { + BrowserReadyLabelsTarget { dropin: "/tmp/hermetic-scratch/85-browser-ready-labels.conf" as NonEmptyStr, posture: ReadyLabelsUnprivileged } +} + +fn words_carry_no_sudo(words: List) -> Bool { + match words { + Empty {} => true + Cons { head: "sudo", tail: _ } => false + Cons { head: _, tail: rest } => words_carry_no_sudo(words: rest) + } +} + +fn grant_reload_skipped(r: BrowserReadyLabelsReload) -> Bool { + match r { + ReadyLabelsReloadPostureSkipped { cause: c } => string_contains(s: c, pattern: "unprivileged posture") + ReadyLabelsReloadRun { command: _ } => false + } +} + +fn grant_reload_is_posture_skipped(p: BrowserReadyLabelsPlan) -> Bool { + match p { + BrowserReadyLabelsGrantReady { write: _, content: _, reload: r } => grant_reload_skipped(r: r) + BrowserReadyLabelsRevokeReady { remove: _, reload: _ } => false + BrowserReadyLabelsUnrenderable { cause: _ } => false + } +} + +// Discriminating: a sudo word under the unprivileged posture, or a reload command where the +// posture says skip, reds. +test fn unprivileged_posture_runs_bare_and_skips_the_reload() -> Bool { + let grant = browser_ready_labels_grant_plan(host: operator_host_srv3, target: browser_ready_labels_unprivileged_target()) + let revoke = browser_ready_labels_revoke_plan(target: browser_ready_labels_unprivileged_target()) + words_carry_no_sudo(words: concat_lists(a: plan_write_words(p: grant), b: plan_remove_words(p: revoke))) + && grant_reload_is_posture_skipped(p: grant) +} + +fn plan_remove_words(p: BrowserReadyLabelsPlan) -> List { + match p { + BrowserReadyLabelsGrantReady { write: _, content: _, reload: _ } => [] + BrowserReadyLabelsRevokeReady { remove: rm, reload: _ } => argv_words(command: rm) + BrowserReadyLabelsUnrenderable { cause: _ } => [] + } +} + +// GRANT writes the template drop-in carrying the label (bytes on stdin to dd of=PATH status=none +// at the drop-in path — dd over tee because tee would echo the payload into the command log) and +// reloads; REVOKE removes the same path and carries no write step. +// Discriminating: a grant without the label, a revoke whose removal misses the path, or a revoke +// that carries a write leg, reds. test fn grant_writes_the_label_and_revoke_removes_it() -> Bool { - let grant = plan_script(p: browser_ready_labels_grant_plan(host: operator_host_srv3)) - let revoke = plan_script(p: browser_ready_labels_revoke_plan()) - string_contains(s: grant, pattern: "actions-runner@.service.d/85-browser-ready-labels.conf") - && string_contains(s: grant, pattern: "gunbc-browser-ready") - && string_contains(s: grant, pattern: "srv3") - && string_contains(s: grant, pattern: "daemon-reload") - && string_contains(s: revoke, pattern: "actions-runner@.service.d/85-browser-ready-labels.conf") - && string_contains(s: revoke, pattern: "'rm' '-f'") - && !string_contains(s: revoke, pattern: "gunbc-browser-ready") + let grant = browser_ready_labels_grant_plan(host: operator_host_srv3, target: browser_ready_labels_production_target) + let revoke = browser_ready_labels_revoke_plan(target: browser_ready_labels_production_target) + let write = join(plan_write_words(p: grant), " ") + let payload = plan_payload(p: grant) + let reload = join(plan_reload_words(p: grant), " ") + let remove = join(plan_remove_words(p: revoke), " ") + string_contains(s: write, pattern: "dd of=") + && string_contains(s: write, pattern: "status=none") + && string_contains(s: write, pattern: "actions-runner@.service.d/85-browser-ready-labels.conf") + && string_contains(s: payload, pattern: "gunbc-browser-ready") + && string_contains(s: payload, pattern: "srv3") + && string_contains(s: reload, pattern: "daemon-reload") + && string_contains(s: remove, pattern: "rm -f") + && string_contains(s: remove, pattern: "actions-runner@.service.d/85-browser-ready-labels.conf") + && plan_payload(p: revoke) == "" + && join(plan_write_words(p: revoke), " ") == "" } // THE CONVERGE VERDICT: only a grant on a ready toolchain leaves the host floor-ready; a revoke is diff --git a/src/v2/workflow/floor_route_gap.dag b/src/v2/workflow/floor_route_gap.dag index 086965bfbe7..f8e696b5b80 100644 --- a/src/v2/workflow/floor_route_gap.dag +++ b/src/v2/workflow/floor_route_gap.dag @@ -2020,6 +2020,24 @@ fn floor_route_gap_expectation_chunk_40() -> List { } } +// The test.claim.runner.browser_ready_label_real_path_witness identities: the readiness-label +// converge driven for real over LocalShell against a mktemp scratch root (the unprivileged posture +// parameterization; gunbc#13401's review round). First effect without a hermetic arm is +// shell.Mktemp.DirWithTemplate (no mock_response). Triple enrollment as for chunk_30: these +// expectations, WetScheduledClaim rows in local_repo_wet_schedule, and the gunbc.ci_layer_roots +// LocalRepoWetLane row for browser_ready_label_real_path_witness_test.dag. The production fleet +// route's own stall is rostered as gunbc.guarantee_stall.browser_ready_labels_fleet_route_stall, +// not here. +fn floor_route_gap_expectation_chunk_41() -> List { + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.runner.browser_ready_label_real_path_witness.grant_executes_the_dropin_write_for_real_on_a_scratch_root", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.runner.browser_ready_label_real_path_witness.revoke_executes_the_dropin_removal_for_real_on_a_scratch_root", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Empty {} + } + } +} + // The test.claim.scm_repository_save_real_execution_witness identity: the guarded save's one real // write over the committed v7 fixture. First effect without a hermetic arm is Filesystem.Write (no // mock_response). Triple enrollment as for chunk_30: this expectation, a WetScheduledClaim row in @@ -2241,7 +2259,7 @@ fn floor_route_gap_expectation_chunk_37() -> List { } fn floor_route_gap_expectation_chunks() -> List> { - Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Cons { head: floor_route_gap_expectation_chunk_20(), tail: Cons { head: floor_route_gap_expectation_chunk_21(), tail: Cons { head: floor_route_gap_expectation_chunk_22(), tail: Cons { head: floor_route_gap_expectation_chunk_23(), tail: Cons { head: floor_route_gap_expectation_chunk_24(), tail: Cons { head: floor_route_gap_expectation_chunk_25(), tail: Cons { head: floor_route_gap_expectation_chunk_26(), tail: Cons { head: floor_route_gap_expectation_chunk_27(), tail: Cons { head: floor_route_gap_expectation_chunk_28(), tail: Cons { head: floor_route_gap_expectation_chunk_29(), tail: Cons { head: floor_route_gap_expectation_chunk_30(), tail: Cons { head: floor_route_gap_expectation_chunk_31(), tail: Cons { head: floor_route_gap_expectation_chunk_32(), tail: Cons { head: floor_route_gap_expectation_chunk_33(), tail: Cons { head: floor_route_gap_expectation_chunk_34(), tail: Cons { head: floor_route_gap_expectation_chunk_35(), tail: Cons { head: floor_route_gap_expectation_chunk_36(), tail: Cons { head: floor_route_gap_expectation_chunk_37(), tail: Cons { head: floor_route_gap_expectation_chunk_38(), tail: Cons { head: floor_route_gap_expectation_chunk_39(), tail: Cons { head: floor_route_gap_expectation_chunk_40(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }} + Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Cons { head: floor_route_gap_expectation_chunk_20(), tail: Cons { head: floor_route_gap_expectation_chunk_21(), tail: Cons { head: floor_route_gap_expectation_chunk_22(), tail: Cons { head: floor_route_gap_expectation_chunk_23(), tail: Cons { head: floor_route_gap_expectation_chunk_24(), tail: Cons { head: floor_route_gap_expectation_chunk_25(), tail: Cons { head: floor_route_gap_expectation_chunk_26(), tail: Cons { head: floor_route_gap_expectation_chunk_27(), tail: Cons { head: floor_route_gap_expectation_chunk_28(), tail: Cons { head: floor_route_gap_expectation_chunk_29(), tail: Cons { head: floor_route_gap_expectation_chunk_30(), tail: Cons { head: floor_route_gap_expectation_chunk_31(), tail: Cons { head: floor_route_gap_expectation_chunk_32(), tail: Cons { head: floor_route_gap_expectation_chunk_33(), tail: Cons { head: floor_route_gap_expectation_chunk_34(), tail: Cons { head: floor_route_gap_expectation_chunk_35(), tail: Cons { head: floor_route_gap_expectation_chunk_36(), tail: Cons { head: floor_route_gap_expectation_chunk_37(), tail: Cons { head: floor_route_gap_expectation_chunk_38(), tail: Cons { head: floor_route_gap_expectation_chunk_39(), tail: Cons { head: floor_route_gap_expectation_chunk_40(), tail: Cons { head: floor_route_gap_expectation_chunk_41(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }} } diff --git a/src/v2/workflow/local_repo_wet_terminal.dag b/src/v2/workflow/local_repo_wet_terminal.dag index 9b5d26a25a3..1a30d941913 100644 --- a/src/v2/workflow/local_repo_wet_terminal.dag +++ b/src/v2/workflow/local_repo_wet_terminal.dag @@ -2940,6 +2940,18 @@ fn local_repo_wet_schedule() -> List { entry: "dag/test/claim/roadmap/allocation_client_execution_wet_witness_test.dag", function: "client_typed_unavailable_disables_create", expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.runner.browser_ready_label_real_path_witness", function: "grant_executes_the_dropin_write_for_real_on_a_scratch_root" }, + entry: "dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag", + function: "grant_executes_the_dropin_write_for_real_on_a_scratch_root", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.runner.browser_ready_label_real_path_witness", function: "revoke_executes_the_dropin_removal_for_real_on_a_scratch_root" }, + entry: "dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag", + function: "revoke_executes_the_dropin_removal_for_real_on_a_scratch_root", + expectation: ExpectedToHold {} } ] }