From 63c9500bc4409fc82df49b6ffec0ed2d075b87b3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 4 Oct 2026 04:03:15 +0000 Subject: [PATCH 01/44] mtcollins1 runner: JIT register/deregister over a managed-host unit binding (leg 2) One JIT mint over a slot sum (microVM cell | transient systemd unit on a gunbc.managed_host host); ensure-style deregistration with org-listing readback and typed refusal on every exit; teardown inventory generalized to a host-unit arm; census row for deregistration (pre-approved, ruling 2026-10-03); route legs registration/deregistration bound. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../github/actions_self_hosted_runners.dag | 121 +++++++++++++ dag/extdeps/github/effect.dag | 5 + dag/gunbc/auth/privileged_effect_census.dag | 17 +- dag/gunbc/extdeps_scope_frontier.dag | 1 + dag/gunbc/github_effect_perform.dag | 30 +++- dag/gunbc/runner/runner_attempt_launch.dag | 8 +- dag/gunbc/runner/runner_host_unit_slot.dag | 82 +++++++++ .../runner/runner_jit_deregistration.dag | 165 ++++++++++++++++++ dag/gunbc/runner/runner_jit_mint.dag | 83 +++++++-- dag/gunbc/runner/runner_jit_perform.dag | 22 ++- .../runner/runner_microvm_slot_controller.dag | 7 +- dag/gunbc/runner/runner_teardown_receipt.dag | 77 +++++++- .../runner_throughput_qualification_route.dag | 28 +-- ...ization_pattern_selection_witness_test.dag | 12 ++ .../github_app_registry_witness_test.dag | 92 ++++++++-- .../github_effect_perform_witness_test.dag | 2 +- .../runner_attempt_launch_witness_test.dag | 5 +- .../runner_canary_receipt_witness_test.dag | 85 ++++++++- ...runner_jit_deregistration_witness_test.dag | 94 ++++++++++ ...r_microvm_slot_controller_witness_test.dag | 3 +- ..._throughput_qualification_witness_test.dag | 25 ++- 21 files changed, 882 insertions(+), 82 deletions(-) create mode 100644 dag/extdeps/github/actions_self_hosted_runners.dag create mode 100644 dag/gunbc/runner/runner_host_unit_slot.dag create mode 100644 dag/gunbc/runner/runner_jit_deregistration.dag create mode 100644 dag/test/claim/runner/runner_jit_deregistration_witness_test.dag diff --git a/dag/extdeps/github/actions_self_hosted_runners.dag b/dag/extdeps/github/actions_self_hosted_runners.dag new file mode 100644 index 00000000000..5d303c3b399 --- /dev/null +++ b/dag/extdeps/github/actions_self_hosted_runners.dag @@ -0,0 +1,121 @@ +module extdeps.github.actions_self_hosted_runners + +import std.types { String, NonEmptyStr, Int, List, Secret } +import extdeps.github.github { default_api_base } +import extdeps.github.errors { GitHubErrorShape } +import extdeps.transports.rest { RestOutcome } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.uri { Uri, Https } +import std.resources { Network } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "docs.github.com/en/rest/actions/self-hosted-runners#list-self-hosted-runners-for-an-organization", + } +} + +data delete_organization_runner_citation: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "docs.github.com/en/rest/actions/self-hosted-runners#delete-a-self-hosted-runner-from-an-organization", + } +} + +// THE ORGANIZATION'S RUNNER LISTING, FILTERED BY NAME, IS THE READBACK FOR WHETHER A REGISTRATION +// EXISTS. Upstream's list operation takes a `name` query parameter and answers `total_count` beside +// the `runners` page (REST API version 2022-11-28). A name is the identity this fleet minted the +// registration under (extdeps.github.actions_jit_runner JitRunnerConfigRequest name), so a lookup by +// name answers for a registration whose runner id was never received -- a mint whose answer was +// lost still left a runner under that name, and a readback keyed by id could not see it. +// +// Only the two fields a consumer reads are modeled; the rest of upstream's runner schema (os, +// status, busy, labels) has no reader here and would be a declaration nothing consumes. +type OrganizationRunnerWire { + id: Int + name: String +} + +type OrganizationRunnerListWire { + total_count: Int + runners: List +} + +// THE DELETE ANSWERS 204 WITH NO BODY. It is the same organization self-hosted-runners write +// permission as the JIT mint (extdeps.github.effect DeleteOrganizationRunner). +type OrganizationRunnerDeletedBody {} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.github.actions_self_hosted_runners", + decl_name: "OrganizationRunnerListWire", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [delete_organization_runner_citation] +} + +data organization_runners_max_per_page: Int = 100 + +service github.ActionsSelfHostedRunners { + config { + endpoint: default_api_base + auth: Bearer + auth_input: installation_token + } + + operation ListOrganizationRunnersByName { + requires Network + input { + installation_token: Secret + org: NonEmptyStr + name: NonEmptyStr + per_page: Int = organization_runners_max_per_page + } + output { + result: OrganizationRunnerListWire + outcome: RestOutcome + } + readonly + transport rest { + method: GET, + path: "/orgs/\{org\}/actions/runners", + query: { name: name, per_page: per_page } + } + response { + 200 => OrganizationRunnerListWire + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } + + operation DeleteOrganizationRunner { + requires Network + input { + installation_token: Secret + org: NonEmptyStr + runner_id: Int + } + output { + result: OrganizationRunnerDeletedBody + outcome: RestOutcome + } + transport rest { + method: DELETE, + path: "/orgs/\{org\}/actions/runners/\{runner_id\}" + } + response { + 204 => OrganizationRunnerDeletedBody + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } +} diff --git a/dag/extdeps/github/effect.dag b/dag/extdeps/github/effect.dag index d56ed8f75ea..921d64f886b 100644 --- a/dag/extdeps/github/effect.dag +++ b/dag/extdeps/github/effect.dag @@ -87,6 +87,9 @@ type GitHubEffect | GenerateOrganizationJitConfig { organization: GitHubOrganizationRef } + | DeleteOrganizationRunner { + organization: GitHubOrganizationRef + } data workflow_directory_prefix: NonEmptyStr = ".github/workflows/" @@ -158,6 +161,8 @@ fn github_effect_required_permissions(effect: GitHubEffect) -> List [organization_self_hosted_runners_write] + DeleteOrganizationRunner { organization: _ } => + [organization_self_hosted_runners_write] } } diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index db8742b465c..a2f3a010c09 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -523,7 +523,22 @@ data privileged_effect_census: List = [ PrivilegedEffectSite { site: site(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), effect: PrivilegedEffect { - subject: "mint a GitHub App installation token from the App key fetched over WIF, then generate a JIT runner config" as NonEmptyStr, + subject: "mint a GitHub App installation token from the App key fetched over WIF, then generate a JIT runner config for one slot -- a microVM attempt or a transient unit on a managed host -- pre-approved under the dispatching run's authorization (operator ruling 2026-10-03)" as NonEmptyStr, + frequency: EveryProvision, + reversibility: ReversibleByReapply, + surface: ApiSurface, + workload_identity: convergence_principal(), + minted_reach: MintsResourceScopedCredential, + billing: NoBillingConsequence, + witness_discharge: NoWitnessDischarge, + }, + realized: RealizedFederatedGrant, + divergence_reason: none, + }, + PrivilegedEffectSite { + site: site(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), + effect: PrivilegedEffect { + subject: "on every slot exit, delete the slot's JIT runner registration from the organization under the controller's installation token and confirm it absent from the organization's runner listing, pre-approved under the dispatching run's authorization (operator ruling 2026-10-03)" as NonEmptyStr, frequency: EveryProvision, reversibility: ReversibleByReapply, surface: ApiSurface, diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index d09bf274fce..07c15cf9019 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -231,6 +231,7 @@ data scope_carrier_paths: List = [ "dag/extdeps/github/action/rust_cache.dag", "dag/extdeps/github/action/setup_rust_toolchain.dag", "dag/extdeps/github/action/upload_artifact.dag", + "dag/extdeps/github/actions_self_hosted_runners.dag", "dag/extdeps/network/dell_0r887v.dag", "dag/extdeps/network/ibm_01ft753.dag", "dag/extdeps/network/mcx4121a_acat.dag", diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 001b19b5fad..3f47dbfec0d 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -20,12 +20,14 @@ import extdeps.github.actions_jit_runner { JitConfigCreatedBody, jit_config_mint_outcome, jit_config_created_status, jit_runner_label_wire, } import extdeps.transports.rest { - RestOutcome, RestMutationExchange, classify_rest_outcome, RestExchangePerformance, + RestOutcome, RestMutationExchange, RestReadExchange, classify_rest_outcome, RestExchangePerformance, RestExchangeSucceeded, RestExchangeStatusRefused, } import gunbc.fleet.org_actions_inspection { RunnerGroupListDecode, read_runner_groups } import extdeps.github.app import extdeps.github.actions_jit_runner +import extdeps.github.actions_self_hosted_runners +import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } import gunbc.auth.github_apps { DeclaredGitHubApp, gunbai_ci_declared } import extdeps.github.app { GitHubAppInstallationId } import gunbc.runner.runner_jit_perform { @@ -227,7 +229,7 @@ fn jit_mint_generate_gate(dispatch: JitMintDispatch) -> JitMintGenerateGate { match dispatch { JitMintDispatchRefused { refusal: r } => JitMintDecidedBeforeGenerate { performance: JitMintReceived { delivery: JitCredentialNotDispatched { refusal: r } } } - JitMintDispatchAuthorized { attempt: _, app: app, installation: i, organization: o, request: req } => + JitMintDispatchAuthorized { slot: _, app: app, installation: i, organization: o, request: req } => if app != lifecycle_controller_app { JitMintDecidedBeforeGenerate { performance: JitMintAppKeyNotCustodied { app: app } } } else { @@ -266,3 +268,27 @@ fn perform_organization_jit_mint(dispatch: JitMintDispatch, token: ControllerIns fn read_runner_groups_with(token: ControllerInstallationToken, organization: NonEmptyStr) -> RunnerGroupListDecode uses net: Network { read_runner_groups(token: token.token, organization: organization) } + +// THE DEREGISTRATION'S TWO EXCHANGES, under the same controller installation token that minted the +// registration (gunbc.runner.runner_jit_deregistration ensure_jit_runner_deregistered). The token's +// Secret is shed here and nowhere else, as for the mint; the answers are classified at their own +// grain -- the listing a read, the delete a mutation -- and the decision over them is the +// deregistration module's, taken without I/O. +type OrganizationRunnerListRead { + performance: RestExchangePerformance + body: OrganizationRunnerListWire +} + +fn list_runners_by_name_with(token: ControllerInstallationToken, organization: NonEmptyStr, name: NonEmptyStr) -> OrganizationRunnerListRead + uses net: Network +{ + let listed = github.ActionsSelfHostedRunners.ListOrganizationRunnersByName(installation_token: token.token, org: organization, name: name) + OrganizationRunnerListRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: listed.outcome }), body: listed.result } +} + +fn delete_runner_with(token: ControllerInstallationToken, organization: NonEmptyStr, runner_id: Int) -> RestExchangePerformance + uses net: Network +{ + let deleted = github.ActionsSelfHostedRunners.DeleteOrganizationRunner(installation_token: token.token, org: organization, runner_id: runner_id) + classify_rest_outcome(standing: RestMutationExchange { rest: deleted.outcome }) +} diff --git a/dag/gunbc/runner/runner_attempt_launch.dag b/dag/gunbc/runner/runner_attempt_launch.dag index a0e4284587f..3d5860738a8 100644 --- a/dag/gunbc/runner/runner_attempt_launch.dag +++ b/dag/gunbc/runner/runner_attempt_launch.dag @@ -42,7 +42,7 @@ import gunbc.github_effect_perform { JitMintPerformance, JitMintReceived } import gunbc.runner.runner_jit_perform { JitCredentialDelivery, JitCredentialBoundToAttempt, } -import gunbc.runner_jit_mint { jit_runner_name_for } +import gunbc.runner_jit_mint { jit_runner_name_for, JitSlot, MicroVmJitSlot } import gunbc.runner.runner_jit_admission { SignatureAxis, ExpiryAxis, jit_admission_axis_standing, AxisRequired, HostMintAdmission, } @@ -160,7 +160,7 @@ type AttemptGround type JitDeviceRefusal = JitMintNotReceived { performance: JitMintPerformance } | JitCredentialNotDelivered { delivery: JitCredentialDelivery } - | JitCredentialBoundToDifferentAttempt { bound_to: MicroVmAttempt } + | JitCredentialBoundToDifferentAttempt { bound_to: JitSlot } | JitCredentialBelowFloor { bytes: ByteSize } | JitCredentialAboveCeiling { bytes: ByteSize } | JitMintAdmissionAxisNotRequired @@ -207,10 +207,10 @@ fn admit_jit_credential(attempt: MicroVmAttempt, mint: JitMintPerformance) -> Ji match mint { JitMintReceived { delivery: delivery } => match delivery { - JitCredentialBoundToAttempt { attempt: bound, credential: c, runner_id: id } => + JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id } => if !host_mint_admission_is_required() { JitCredentialRefused { refusal: JitMintAdmissionAxisNotRequired } - } else if bound != attempt { + } else if bound != MicroVmJitSlot { attempt: attempt } { JitCredentialRefused { refusal: JitCredentialBoundToDifferentAttempt { bound_to: bound } } } else if !measure_le(a: jit_config_byte_floor, b: jit_credential_bytes(credential: c)) { JitCredentialRefused { refusal: JitCredentialBelowFloor { bytes: jit_credential_bytes(credential: c) } } diff --git a/dag/gunbc/runner/runner_host_unit_slot.dag b/dag/gunbc/runner/runner_host_unit_slot.dag new file mode 100644 index 00000000000..95f14225dc6 --- /dev/null +++ b/dag/gunbc/runner/runner_host_unit_slot.dag @@ -0,0 +1,82 @@ +module gunbc.runner.runner_host_unit_slot + +import std.types { List, NonEmptyStr, String } +import extdeps.toolchain.types { Aarch64 } +import extdeps.github.actions_runner { + ActionsRunnerRelease, ActionsRunnerBinaryArtifact, actions_runner_binary_artifact_for_arch, + actions_runner_release_2_337_0, actions_runner_base_dir, actions_runner_listener_relative_path, + actions_runner_listener_run_verb, +} +import gunbc.managed_host { ManagedHostBinding, ManagedHostBindingStanding, ManagedHostBound } +import gunbc.runner_slot_desired { gunbc_runner_slot_desired } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_argv, ephemeral_slot_labels } + +// THE SECOND BINDING OF ONE JIT MINT: A TRANSIENT systemd UNIT ON A LIVE MANAGED HOST. +// +// gunbc.runner_jit_mint plans a JIT registration over a slot; a microVM cell is one realization of +// a slot and this is the other. The mint is not forked per binding (DESIGN §3: transport is +// realization) -- this module only decides whether a host-unit slot EXISTS, and the mint decides +// everything else over the sum both bindings inhabit. +// +// The host is admitted only through the ManagedHost seam (gunbc.managed_host): a host whose binding +// standing is anything but ManagedHostBound has no slot, and the refusal carries that standing +// unchanged rather than a re-coined cause. No mtcollins1_* module is named here; mtcollins1 reaches +// this fold as the gunbc.managed_host managed_hosts row and nothing else. +// +// The unit, its systemd-run argv and its labels are the qualification route's own derivations +// (gunbc.runner_throughput_qualification_route ephemeral_slot_unit / ephemeral_slot_argv / +// ephemeral_slot_labels), so the slot the mint registers is by construction the slot the route +// stages -- one spelling of the attempt label, not two. +// +// THE ARGV CARRIES NO CREDENTIAL. The listener reads its JIT config from the environment +// (extdeps.github.actions_runner actions_runner_jitconfig_env_name) and deletes it once read, so the +// command is the bare listener run verb; an argv is readable by every process on the host +// (gunbc.credential_argv_exposure), and putting the encoded config there would re-open that class. +// Placing the credential into the unit's environment is the launch's act, not the registration's. +type HostUnitSlot sole_constructor { + host: ManagedHostBinding + attempt: NonEmptyStr + unit: NonEmptyStr + labels: List + argv: List + runner: ActionsRunnerBinaryArtifact +} + +type HostUnitSlotRefusal + = HostUnitSlotHostNotBound { standing: ManagedHostBindingStanding } + | HostUnitSlotRunnerBinaryUnpublished { binary_release: ActionsRunnerRelease } + +type HostUnitSlotStanding + = HostUnitSlotBound { slot: HostUnitSlot } + | HostUnitSlotRefused { refusal: HostUnitSlotRefusal } + +// THE SAME RELEASE THE microVM GUEST RUNS (gunbc.runner_microvm), so the two bindings of one mint +// run one listener whose environment contract is the one cited in extdeps.github.actions_runner. +data host_unit_slot_runner_release: ActionsRunnerRelease = actions_runner_release_2_337_0 + +fn host_unit_slot_listener_argv() -> List { + [concat(actions_runner_base_dir, actions_runner_listener_relative_path), actions_runner_listener_run_verb] +} + +fn host_unit_slot_for(host: ManagedHostBindingStanding, attempt: NonEmptyStr) -> HostUnitSlotStanding { + match host { + ManagedHostBound { binding: b } => + match actions_runner_binary_artifact_for_arch(binary_release: host_unit_slot_runner_release, arch: Aarch64) { + Absent => HostUnitSlotRefused { refusal: HostUnitSlotRunnerBinaryUnpublished { binary_release: host_unit_slot_runner_release } } + Present { value: artifact } => { + let unit = ephemeral_slot_unit(host: b.host.host, attempt: attempt) + HostUnitSlotBound { + slot: HostUnitSlot { + host: b, + attempt: attempt, + unit: unit, + labels: ephemeral_slot_labels(host: b.host.host, attempt: attempt), + argv: ephemeral_slot_argv(unit: unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()), + runner: artifact, + }, + } + } + } + other => HostUnitSlotRefused { refusal: HostUnitSlotHostNotBound { standing: other } } + } +} diff --git a/dag/gunbc/runner/runner_jit_deregistration.dag b/dag/gunbc/runner/runner_jit_deregistration.dag new file mode 100644 index 00000000000..8cb6cc57285 --- /dev/null +++ b/dag/gunbc/runner/runner_jit_deregistration.dag @@ -0,0 +1,165 @@ +module gunbc.runner.runner_jit_deregistration + +import std.types { Bool, Int, List, NonEmptyStr, String } +import std.resources { Network } +import std.upsert_decision { UpsertDecision, Noop, Apply, Refuse } +import extdeps.transports.rest { RestExchangePerformance, RestExchangeSucceeded } +import extdeps.github.effect { GitHubOrganizationRef, DeleteOrganizationRunner } +import extdeps.github.actions_jit_runner { JitRunnerName } +import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } +import gunbc.auth.github_apps { GitHubAppControlPlaneState } +import gunbc.auth.github_credential { + GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, +} +import gunbc.runner_jit_mint { JitSlot } +import gunbc.runner.runner_jit_perform { JitMintDispatch, JitMintDispatchAuthorized, JitMintDispatchRefused } +import gunbc.github_effect_perform { + ControllerInstallationToken, OrganizationRunnerListRead, list_runners_by_name_with, delete_runner_with, +} + +// DEREGISTRATION IS AN ENSURE, NOT A STEP AFTER SUCCESS. +// +// A JIT runner is ephemeral by construction -- GitHub removes it when its one job ends -- but "its +// one job ended" is exactly the condition that does NOT hold on the failure paths: a unit that never +// started, a job that was never dispatched, a listener killed mid-job. On each of those the +// registration outlives the slot and sits in the organization answering to the attempt label. So +// the deregistration is total over how the slot exited (JitSlotExit), records that exit in its +// receipt, and decides only from what GitHub lists now. +// +// THE REGISTRATION IS IDENTIFIED BY THE NAME IT WAS MINTED UNDER, which an authorized dispatch fixes +// BEFORE GitHub answers (gunbc.runner_jit_mint jit_slot_runner_name). So a mint whose answer was lost +// -- the one case where a runner may exist under an id nobody received -- still yields a +// registration this fold can find and remove. A refused dispatch minted nothing and yields none. +// +// SUCCESS IS A READBACK, NEVER A DELETE STATUS. The organization's listing filtered by the name +// (extdeps.github.actions_self_hosted_runners ListOrganizationRunnersByName) is read before and, +// when anything was removed, after; JitRunnerDeregistered is constructed only from a listing that +// does not contain the name. A 204 with the runner still listed, an ambiguous delete, or a listing +// that could not be read are each a typed refusal -- the line stops (DESIGN §5), it does not +// report the slot clean. +// +// AUTHORIZATION IS THE DISPATCHING RUN'S (operator ruling 2026-10-03): registration and +// deregistration are pre-approved under the fleet-converge run's own federated identity, with no +// per-registration approval. That is recorded where the corpus records authorization patterns -- +// gunbc.auth.privileged_effect_census rows this site through +// gunbc.auth.authorization_pattern_selection select_authorization_pattern -- and the credential's +// own capability is still checked here, against the same effect algebra the mint uses. +type JitSlotExit + = SlotJobCompleted + | SlotJobFailed { cause: NonEmptyStr } + | SlotNeverLaunched { cause: NonEmptyStr } + +type JitRegistration { + organization: GitHubOrganizationRef + name: JitRunnerName + slot: JitSlot +} + +fn jit_registration_of(dispatch: JitMintDispatch) -> JitRegistration? { + match dispatch { + JitMintDispatchRefused { refusal: _ } => none + JitMintDispatchAuthorized { slot: s, app: _, installation: _, organization: o, request: r } => + Present { value: JitRegistration { organization: o, name: r.name, slot: s } } + } +} + +type JitRunnerListing + = JitRunnerListed { ids: List } + | JitRunnerNotListed + | JitRunnerListingUnreadable { performance: RestExchangePerformance, cause: NonEmptyStr } + +// A PAGE SHORTER THAN total_count IS NOT A LISTING. The name filter makes more than one page +// implausible, but a runner on an unread page would read as absent, which is the one wrong answer +// this fold exists to refuse. +fn jit_runner_listing_of(name: JitRunnerName, read: OrganizationRunnerListRead) -> JitRunnerListing { + match read.performance { + RestExchangeSucceeded => + if count(read.body.runners) != read.body.total_count { + JitRunnerListingUnreadable { + performance: read.performance, + cause: join(["runner listing returned ", to_string(count(read.body.runners)), " of total_count ", to_string(read.body.total_count)], "") as NonEmptyStr, + } + } else { + let ids = read.body.runners |> filter(r => r.name == (name.value as String)) |> map(r => r.id) + if count(ids) == 0 { JitRunnerNotListed } else { JitRunnerListed { ids: ids } } + } + other => JitRunnerListingUnreadable { performance: other, cause: "the organization runner listing did not succeed" as NonEmptyStr } + } +} + +fn plan_jit_deregistration(listing: JitRunnerListing) -> UpsertDecision> { + match listing { + JitRunnerNotListed => Noop + JitRunnerListed { ids: ids } => Apply { plan: ids } + JitRunnerListingUnreadable { performance: _, cause: c } => Refuse { reason: c } + } +} + +type JitRunnerDeleteAttempt { + runner_id: Int + performance: RestExchangePerformance +} + +type JitDeregistrationRefusal + = DeregistrationCredentialRefused { unsatisfied: List } + | DeregistrationListingUnreadable { performance: RestExchangePerformance, cause: NonEmptyStr } + | DeregistrationRunnerStillListed { ids: List, deletes: List } + +type JitDeregistration + = JitRunnerDeregistered { registration: JitRegistration, exit: JitSlotExit, deletes: List } + | JitDeregistrationRefused { registration: JitRegistration, exit: JitSlotExit, refusal: JitDeregistrationRefusal } + +// THE CONCLUSION READS THE LISTING AND NOTHING ELSE. The delete attempts are carried into the +// receipt as evidence of what was tried, but no delete status can mint the deregistered arm. +fn conclude_jit_deregistration( + registration: JitRegistration, + exit: JitSlotExit, + deletes: List, + readback: JitRunnerListing, +) -> JitDeregistration { + match readback { + JitRunnerNotListed => JitRunnerDeregistered { registration: registration, exit: exit, deletes: deletes } + JitRunnerListed { ids: ids } => + JitDeregistrationRefused { registration: registration, exit: exit, refusal: DeregistrationRunnerStillListed { ids: ids, deletes: deletes } } + JitRunnerListingUnreadable { performance: p, cause: c } => + JitDeregistrationRefused { registration: registration, exit: exit, refusal: DeregistrationListingUnreadable { performance: p, cause: c } } + } +} + +fn jit_runner_deregistered(result: JitDeregistration) -> Bool { + match result { + JitRunnerDeregistered { registration: _, exit: _, deletes: _ } => true + JitDeregistrationRefused { registration: _, exit: _, refusal: _ } => false + } +} + +fn ensure_jit_runner_deregistered( + token: ControllerInstallationToken, + authority: GitHubCredentialAuthority, + control_plane: GitHubAppControlPlaneState, + registration: JitRegistration, + exit: JitSlotExit, +) -> JitDeregistration uses net: Network { + let effect = DeleteOrganizationRunner { organization: registration.organization } + if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { + JitDeregistrationRefused { + registration: registration, + exit: exit, + refusal: DeregistrationCredentialRefused { + unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect), + }, + } + } else { + let org = registration.organization.login + let before = jit_runner_listing_of(name: registration.name, read: list_runners_by_name_with(token: token, organization: org, name: registration.name.value)) + match plan_jit_deregistration(listing: before) { + Noop => conclude_jit_deregistration(registration: registration, exit: exit, deletes: [], readback: before) + Refuse { reason: _ } => conclude_jit_deregistration(registration: registration, exit: exit, deletes: [], readback: before) + Apply { plan: ids } => { + let deletes = ids |> map(id => JitRunnerDeleteAttempt { runner_id: id, performance: delete_runner_with(token: token, organization: org, runner_id: id) }) + let after = jit_runner_listing_of(name: registration.name, read: list_runners_by_name_with(token: token, organization: org, name: registration.name.value)) + conclude_jit_deregistration(registration: registration, exit: exit, deletes: deletes, readback: after) + } + } + } +} diff --git a/dag/gunbc/runner/runner_jit_mint.dag b/dag/gunbc/runner/runner_jit_mint.dag index 19ec92e4996..5233ae40ddb 100644 --- a/dag/gunbc/runner/runner_jit_mint.dag +++ b/dag/gunbc/runner/runner_jit_mint.dag @@ -23,6 +23,9 @@ import gunbc.runner_microvm_attempt { MicroVmAttempt, MicroVmCellBinding, VmBoundToOwnedCell, VmUnboundCellNotOwned, runner_microvm_attempt_unit_name, } +import gunbc.runner.runner_host_unit_slot { + HostUnitSlot, HostUnitSlotStanding, HostUnitSlotBound, HostUnitSlotRefused, HostUnitSlotRefusal, +} data runner_jit_mint_disposition: ConstructionMechanism = SingleAuthority @@ -38,12 +41,27 @@ data runner_jit_mint_disposition: ConstructionMechanism = SingleAuthority // caller cannot mint first and check ownership after, because the only value that names the attempt // a credential belongs to is produced by the arm that already proved both. +// ONE MINT, TWO BINDINGS. A JIT registration is minted for a SLOT, and a slot is realized either +// as a microVM attempt in an owned cell or as a transient systemd unit on a managed host +// (gunbc.runner.runner_host_unit_slot). The plan below is one fold over this sum, not one fold per +// binding (DESIGN §3: transport is realization) -- the credential, attempt-binding and runner-group +// gates are the same gates whichever arm the slot is, and only the slot's existence proof and the +// name and labels it registers under are the binding's own. +type JitSlotBinding + = MicroVmSlotBinding { binding: MicroVmCellBinding } + | HostUnitSlotBinding { standing: HostUnitSlotStanding } + +type JitSlot + = MicroVmJitSlot { attempt: MicroVmAttempt } + | HostUnitJitSlot { slot: HostUnitSlot } + type JitMintPlan = JitMintAuthorized { - attempt: MicroVmAttempt + slot: JitSlot request: JitRunnerConfigRequest } | JitMintRefusedCellNotOwned { cause: String } + | JitMintRefusedHostUnitSlot { refusal: HostUnitSlotRefusal } | JitMintRefusedCredential { organization: NonEmptyStr unsatisfied: List @@ -92,39 +110,72 @@ fn microvm_attempt_registration_labels(attempt: MicroVmAttempt) -> List // MicrovmRunnerGroupStanding, whose authorizing arm carries a sole_constructor record only that // module's observer can write; every other reading is RunnerGroupRestrictionRefused with a named // cause, which the refusal below carries to the controller unchanged. +fn jit_slot_runner_name(slot: JitSlot) -> JitRunnerName { + match slot { + MicroVmJitSlot { attempt: a } => jit_runner_name_for(attempt: a) + HostUnitJitSlot { slot: h } => JitRunnerName { value: h.unit } + } +} + +fn jit_slot_registration_labels(slot: JitSlot) -> List { + match slot { + MicroVmJitSlot { attempt: a } => microvm_attempt_registration_labels(attempt: a) + HostUnitJitSlot { slot: h } => h.labels + } +} + data microvm_jit_runner_group_restriction_frontier: DissolutionCondition = unbound_dissolution( description: "NAMED CONSUMER: plan_jit_mint, which refuses every RunnerGroupRestrictionRefused (JitMintRefusedRunnerGroupUnrestricted). The controller now passes the sealed observer's result over a live read under its installation token (gunbc.runner_microvm_slot_controller mint_attempt_credential), so the code half of this row's trigger has fired; the remaining half is a fact about the organization, and until it holds every real attempt refuses at the mint with RunnerGroupAbsent. TRIGGER: the organization holds the group microvm_shakedown_runner_group_name restricted to exactly microvm_shakedown_selected_workflow, created by one run of fleet-converge mode microvm_runner_group_ensure (gunbc.runner_group_restriction_ensure_run) under one operator approval, with its readback receipt recorded. SUFFICIENT FOR deleting this row: a JIT registration that received the OS and architecture defaults still cannot take a witnesses.yml, heal.yml or fleet-converge job. Until then every real attempt refuses at the mint." as NonEmptyStr, ) fn jit_config_request_for( - attempt: MicroVmAttempt, + slot: JitSlot, runner_group_id: JitRunnerGroupId, ) -> JitRunnerConfigRequest { JitRunnerConfigRequest { - name: jit_runner_name_for(attempt: attempt), + name: jit_slot_runner_name(slot: slot), runner_group_id: runner_group_id, - labels: map(microvm_attempt_registration_labels(attempt: attempt), l => JitRunnerLabel { value: l as NonEmptyStr }), + labels: map(jit_slot_registration_labels(slot: slot), l => JitRunnerLabel { value: l as NonEmptyStr }), work_folder: jit_guest_work_folder, } } +type JitSlotResolution + = JitSlotResolved { slot: JitSlot } + | JitSlotUnresolved { refusal: JitMintPlan } + +fn resolve_jit_slot(binding: JitSlotBinding) -> JitSlotResolution { + match binding { + MicroVmSlotBinding { binding: b } => + match b { + VmUnboundCellNotOwned { cause: c } => JitSlotUnresolved { refusal: JitMintRefusedCellNotOwned { cause: c } } + VmBoundToOwnedCell { + attempt: attempt, + rootfs_path: _, + vm_config_path: _, + unit_name: _, + slice_property: _, + } => JitSlotResolved { slot: MicroVmJitSlot { attempt: attempt } } + } + HostUnitSlotBinding { standing: st } => + match st { + HostUnitSlotRefused { refusal: r } => JitSlotUnresolved { refusal: JitMintRefusedHostUnitSlot { refusal: r } } + HostUnitSlotBound { slot: h } => JitSlotResolved { slot: HostUnitJitSlot { slot: h } } + } + } +} + fn plan_jit_mint( - binding: MicroVmCellBinding, + binding: JitSlotBinding, authority: GitHubCredentialAuthority, control_plane: GitHubAppControlPlaneState, organization: GitHubOrganizationRef, runner_group: MicrovmRunnerGroupStanding, attempt_binding: JitAdmissionAxisStanding ) -> JitMintPlan { - match binding { - VmUnboundCellNotOwned { cause: c } => JitMintRefusedCellNotOwned { cause: c } - VmBoundToOwnedCell { - attempt: attempt, - rootfs_path: _, - vm_config_path: _, - unit_name: _, - slice_property: _, - } => + match resolve_jit_slot(binding: binding) { + JitSlotUnresolved { refusal: r } => r + JitSlotResolved { slot: slot } => if credential_authorizes( authority: authority, control_plane: control_plane, @@ -136,8 +187,8 @@ fn plan_jit_mint( JitMintRefusedRunnerGroupUnrestricted { group: n, cause: c } RunnerGroupRestrictedToWorkflow { restriction: r } => JitMintAuthorized { - attempt: attempt, - request: jit_config_request_for(attempt: attempt, runner_group_id: r.group), + slot: slot, + request: jit_config_request_for(slot: slot, runner_group_id: r.group), } } } else { diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 9375bb9e60a..350cfecec4d 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -17,9 +17,10 @@ import gunbc.auth.github_credential { CredentialCapability, } import gunbc.runner.runner_jit_admission { JitAdmissionAxisStanding } -import gunbc.runner_microvm_attempt { MicroVmAttempt, MicroVmCellBinding } +import gunbc.runner.runner_host_unit_slot { HostUnitSlotRefusal } import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, RunnerGroupRestrictionRefusal } import gunbc.runner_jit_mint { + JitSlotBinding, JitSlot, JitMintRefusedHostUnitSlot, JitMintPlan, JitMintAuthorized, JitMintRefusedCellNotOwned, JitMintRefusedCredential, JitMintRefusedAttemptUnbound, JitMintRefusedRunnerGroupUnrestricted, plan_jit_mint, } @@ -68,6 +69,7 @@ data runner_jit_perform_disposition: ConstructionMechanism = SingleAuthority // a default that silently reports the wrong cause. type JitDispatchRefusal = DispatchRefusedCellNotOwned { cause: String } + | DispatchRefusedHostUnitSlot { refusal: HostUnitSlotRefusal } | DispatchRefusedCredential { organization: NonEmptyStr unsatisfied: List @@ -94,7 +96,7 @@ type JitDispatchRefusal // would be cited as coverage. It is recorded here as a totality arm and counted as none. type JitMintDispatch = JitMintDispatchAuthorized { - attempt: MicroVmAttempt + slot: JitSlot app: DeclaredGitHubApp installation: GitHubAppInstallationId organization: GitHubOrganizationRef @@ -116,7 +118,7 @@ fn authority_installation(authority: GitHubCredentialAuthority) -> AuthorityInst } fn dispatch_jit_mint( - binding: MicroVmCellBinding, + binding: JitSlotBinding, authority: GitHubCredentialAuthority, control_plane: GitHubAppControlPlaneState, organization: GitHubOrganizationRef, @@ -133,6 +135,8 @@ fn dispatch_jit_mint( ) { JitMintRefusedCellNotOwned { cause: c } => JitMintDispatchRefused { refusal: DispatchRefusedCellNotOwned { cause: c } } + JitMintRefusedHostUnitSlot { refusal: r } => + JitMintDispatchRefused { refusal: DispatchRefusedHostUnitSlot { refusal: r } } JitMintRefusedAttemptUnbound { cause: c } => JitMintDispatchRefused { refusal: DispatchRefusedAttemptUnbound { cause: c } } JitMintRefusedRunnerGroupUnrestricted { group: g, cause: c } => @@ -142,7 +146,7 @@ fn dispatch_jit_mint( refusal: DispatchRefusedCredential { organization: org, unsatisfied: u }, } JitMintAuthorized { - attempt: attempt, + slot: slot, request: request, } => match authority_installation(authority: authority) { @@ -154,7 +158,7 @@ fn dispatch_jit_mint( } Present { value: held } => JitMintDispatchAuthorized { - attempt: attempt, + slot: slot, app: held.app, installation: held.installation, organization: organization, @@ -181,7 +185,7 @@ fn dispatch_jit_mint( // state no fold writes for a fork that drifts. type JitCredentialDelivery = JitCredentialBoundToAttempt { - attempt: MicroVmAttempt + slot: JitSlot credential: EncodedJitConfig runner_id: Int } @@ -195,7 +199,7 @@ fn receive_jit_mint( match dispatch { JitMintDispatchRefused { refusal: r } => JitCredentialNotDispatched { refusal: r } JitMintDispatchAuthorized { - attempt: attempt, + slot: slot, app: _, installation: _, organization: _, @@ -204,7 +208,7 @@ fn receive_jit_mint( match outcome { JitConfigMinted { config: config, runner_id: runner_id } => JitCredentialBoundToAttempt { - attempt: attempt, + slot: slot, credential: config, runner_id: runner_id, } @@ -227,7 +231,7 @@ fn receive_jit_mint( fn jit_credential_delivered(delivery: JitCredentialDelivery) -> Bool { match delivery { JitCredentialBoundToAttempt { - attempt: _, + slot: _, credential: _, runner_id: _, } => true diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index b839b466bfe..bbdfe99d528 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -70,6 +70,7 @@ import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, RunnerGroupRestrictionNotYetObserved, microvm_shakedown_runner_group_name, runner_group_restriction_not_yet_observed, observe_shakedown_runner_group_restriction, } +import gunbc.runner_jit_mint { MicroVmSlotBinding } import gunbc.runner.runner_jit_perform { dispatch_jit_mint, JitMintDispatch, JitMintDispatchRefused, JitMintDispatchAuthorized, DispatchRefusedRunnerGroupUnrestricted, @@ -264,7 +265,7 @@ fn controller_credential_authority() -> GitHubCredentialAuthority { fn attempt_dispatch(binding: MicroVmCellBinding, state: GitHubAppControlPlaneState, runner_group: MicrovmRunnerGroupStanding) -> JitMintDispatch { dispatch_jit_mint( - binding: binding, + binding: MicroVmSlotBinding { binding: binding }, authority: controller_credential_authority(), control_plane: state, organization: github_organization_ref(), @@ -309,7 +310,7 @@ fn mint_attempt_credential(binding: MicroVmCellBinding, state: GitHubAppControlP mint_under_observed_group(binding: binding, state: state) other => JitMintReceived { delivery: JitCredentialNotDispatched { refusal: other } } } - JitMintDispatchAuthorized { attempt: _, app: _, installation: _, organization: _, request: _ } => + JitMintDispatchAuthorized { slot: _, app: _, installation: _, organization: _, request: _ } => mint_under_observed_group(binding: binding, state: state) } } @@ -439,7 +440,7 @@ fn minted_registration(mint: JitMintPerformance) -> Int? { match mint { JitMintReceived { delivery: d } => match d { - JitCredentialBoundToAttempt { attempt: _, credential: _, runner_id: id } => Present { value: id } + JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id } => Present { value: id } JitCredentialNotDispatched { refusal: _ } => none JitCredentialMintNotCreated { outcome: _ } => none } diff --git a/dag/gunbc/runner/runner_teardown_receipt.dag b/dag/gunbc/runner/runner_teardown_receipt.dag index b82954fb60b..4e87cb3657c 100644 --- a/dag/gunbc/runner/runner_teardown_receipt.dag +++ b/dag/gunbc/runner/runner_teardown_receipt.dag @@ -11,12 +11,12 @@ import gunbc.runner.runner_usage_receipt { } // Expected resources come from the launch allocation, not the observer's discovered population. -// The product requires all eight declared inventory fields at construction. Coverage of the +// The microVM product requires all eight declared inventory fields at construction. Coverage of the // teardown fold is checked separately by the independent resource fixtures; adding a resource // class requires extending both the inventory/readback and that coverage contract. The VMM // identity includes boot and start time; names and exit status cannot prove its absence. // The cgroup covers its descendants. -type RunnerEnvironmentInventory { +type MicroVmEnvironmentInventory { vmm: ProcessIdentity cgroup: NonEmptyStr jail: NonEmptyStr @@ -27,8 +27,36 @@ type RunnerEnvironmentInventory { firewall_table: NonEmptyStr } +// A SLOT IS REALIZED AS A microVM ATTEMPT OR AS A TRANSIENT systemd UNIT ON A MANAGED HOST +// (gunbc.runner_jit_mint JitSlot), and the teardown is one fold over both. The host-unit arm owns +// no jail, jail mount, JIT drive, tap or firewall table -- the unit is the host's own process tree +// in its own cgroup -- so its inventory names only what its launch allocates, rather than filling +// five fields with paths that never existed. Which classes a teardown positive-controls is derived +// from the inventory's arm (runner_resource_classes_for), so a host-unit teardown is not refused +// for a jail it never had, and a microVM teardown still requires all eight. +type HostUnitEnvironmentInventory { + unit_process: ProcessIdentity + unit: NonEmptyStr + cgroup: NonEmptyStr + workspace: NonEmptyStr +} + +type RunnerEnvironmentInventory + = MicroVmInventory { inventory: MicroVmEnvironmentInventory } + | HostUnitInventory { inventory: HostUnitEnvironmentInventory } + +// THE PROCESS WHOSE ABSENCE PROVES THE ENVIRONMENT GONE: the VMM for a microVM, the unit's main +// process for a host unit. Both carry boot and start time, so a reused pid cannot stand in. +fn runner_environment_process(inventory: RunnerEnvironmentInventory) -> ProcessIdentity { + match inventory { + MicroVmInventory { inventory: m } => m.vmm + HostUnitInventory { inventory: h } => h.unit_process + } +} + type RunnerResourceClass = RunnerVmm + | RunnerTransientUnit | RunnerCgroup | RunnerJail | RunnerJailMount @@ -51,6 +79,15 @@ data runner_resource_classes: List = [ RunnerVmm, RunnerCgroup, RunnerJail, RunnerJailMount, RunnerJitDrive, RunnerWorkspace, RunnerTap, RunnerFirewallTable, ] +data host_unit_resource_classes: List = [RunnerTransientUnit, RunnerCgroup, RunnerWorkspace] + +fn runner_resource_classes_for(inventory: RunnerEnvironmentInventory) -> List { + match inventory { + MicroVmInventory { inventory: _ } => runner_resource_classes + HostUnitInventory { inventory: _ } => host_unit_resource_classes + } +} + type RunnerResourceReading = RunnerResourcePresent | RunnerResourceAbsent @@ -58,6 +95,7 @@ type RunnerResourceReading type RunnerEnvironmentReadback { vmm: RunnerResourceReading + unit: RunnerResourceReading cgroup: RunnerResourceReading jail: RunnerResourceReading jail_mount: RunnerResourceReading @@ -115,6 +153,24 @@ type RunnerTeardownResult

| RunnerTeardownRefused { cause: RunnerTeardownRefusal } fn runner_environment_inventory_eq(a: RunnerEnvironmentInventory, b: RunnerEnvironmentInventory) -> Bool { + match a { + MicroVmInventory { inventory: x } => + match b { + MicroVmInventory { inventory: y } => micro_vm_environment_inventory_eq(a: x, b: y) + HostUnitInventory { inventory: _ } => false + } + HostUnitInventory { inventory: x } => + match b { + HostUnitInventory { inventory: y } => + process_identity_eq(a: x.unit_process, b: y.unit_process) && + (x.unit as String) == (y.unit as String) && (x.cgroup as String) == (y.cgroup as String) && + (x.workspace as String) == (y.workspace as String) + MicroVmInventory { inventory: _ } => false + } + } +} + +fn micro_vm_environment_inventory_eq(a: MicroVmEnvironmentInventory, b: MicroVmEnvironmentInventory) -> Bool { process_identity_eq(a: a.vmm, b: b.vmm) && (a.cgroup as String) == (b.cgroup as String) && (a.jail as String) == (b.jail as String) && (a.jail_mount as String) == (b.jail_mount as String) && @@ -133,6 +189,7 @@ type RunnerResourceTransition { fn readback_reading(class: RunnerResourceClass, readback: RunnerEnvironmentReadback) -> RunnerResourceReading { match class { RunnerVmm => readback.vmm + RunnerTransientUnit => readback.unit RunnerCgroup => readback.cgroup RunnerJail => readback.jail RunnerJailMount => readback.jail_mount @@ -143,8 +200,12 @@ fn readback_reading(class: RunnerResourceClass, readback: RunnerEnvironmentReadb } } -fn runner_resource_transitions(before: RunnerEnvironmentReadback, after: RunnerEnvironmentReadback) -> List { - runner_resource_classes |> map(class => RunnerResourceTransition { +fn runner_resource_transitions( + classes: List, + before: RunnerEnvironmentReadback, + after: RunnerEnvironmentReadback, +) -> List { + classes |> map(class => RunnerResourceTransition { resource: class, before: readback_reading(class: class, readback: before), after: readback_reading(class: class, readback: after), @@ -179,14 +240,14 @@ fn runner_teardown_from_observations

( } else if process_identity_eq(a: request.destroyer, b: before.observer) || process_identity_eq(a: request.destroyer, b: after.observer) { RunnerTeardownRefused { cause: TeardownSelfObservation } - } else if process_identity_eq(a: request.inventory.vmm, b: before.observer) || - process_identity_eq(a: request.inventory.vmm, b: after.observer) { + } else if process_identity_eq(a: runner_environment_process(inventory: request.inventory), b: before.observer) || + process_identity_eq(a: runner_environment_process(inventory: request.inventory), b: after.observer) { RunnerTeardownRefused { cause: TeardownObserverInsideEnvironment } } else if !declaration_ref_eq(a: before.evidence.producer, b: after.evidence.producer) { RunnerTeardownRefused { cause: TeardownInstrumentChanged } } else if !process_identity_eq(a: before.observer, b: after.observer) { RunnerTeardownRefused { cause: TeardownObserverChanged } - } else if (request.inventory.vmm.boot_id as String) != (request.subject.boot_id as String) || + } else if (runner_environment_process(inventory: request.inventory).boot_id as String) != (request.subject.boot_id as String) || (request.destroyer.boot_id as String) != (request.subject.boot_id as String) || (after.observer.boot_id as String) != (request.subject.boot_id as String) { RunnerTeardownRefused { cause: TeardownBootMismatch } @@ -197,7 +258,7 @@ fn runner_teardown_from_observations

( ]) { Present { value: d } => RunnerTeardownRefused { cause: TeardownObservationOrderRefused { diagnostic: d } } Absent => { - let refusal = runner_resource_transitions(before: before.readback, after: after.readback) + let refusal = runner_resource_transitions(classes: runner_resource_classes_for(inventory: request.inventory), before: before.readback, after: after.readback) |> fold(init: Absent, f: fn(found, row) { match found { Present { value: _ } => found diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index 916d8ec12e0..b520e896640 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -33,10 +33,8 @@ import gunbc.runner_throughput_qualification { // derivation takes its payload as a parameter. The boot leg is not, and not for want of a landing: // the boot run on main (`gunbc.machine_intake_mtcollins1_boot_run`) boots exactly two media, the // census image and the stock installer, and its terminal is the census program's, so it cannot boot a -// runner and observe it ready. The JIT registration authority -// (`gunbc.runner.runner_jit_perform dispatch_jit_mint`) is shaped for a MicroVmCellBinding, and -// `extdeps.github.workflows CreateDispatch` has no production caller and a heal-specific input -// contract. Each unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR +// runner and observe it ready. `extdeps.github.workflows CreateDispatch` has no production caller +// and a heal-specific input contract. The registration and deregistration legs are bound. Each unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR // (section 4b(3)), never a pull request, because a pull request can merge while the capability stays // missing -- which is exactly what happened to the boot leg. @@ -219,12 +217,17 @@ data mtcollins1_image_leg: RouteLegStanding = LegBoundToAuthority { authority: decl_ref(module_path: "extdeps.provisioning.ubuntu_seeded_install_media", decl_name: "UbuntuSeededInstallMediaBuildInput"), } -// THE THREE GITHUB CONTROL-PLANE LEGS. Registration and deregistration are one JIT contract -- -// an ephemeral JIT runner is removed by GitHub when its one job ends -- so both name the same -// authority; dispatch names the cited REST operation that has no production caller yet. -data mtcollins1_registration_leg: RouteLegStanding = LegAwaitingAuthority { - lands_with: "gunbc.runner.runner_jit_perform dispatch_jit_mint generalized over a slot binding that is a transient unit on a live host rather than a MicroVmCellBinding, minting the JIT config whose labels are ephemeral_slot_labels" as NonEmptyStr, - sufficient_for: "registering exactly one ephemeral runner for one attempt label with the gunbc runner-slot shape, refused before any launch on an inadmissible envelope" as NonEmptyStr, +// THE THREE GITHUB CONTROL-PLANE LEGS. Registration is the one JIT mint, taken over the slot sum +// whose host-unit arm (gunbc.runner.runner_host_unit_slot host_unit_slot_for) is this route's own +// ephemeral_slot_unit / ephemeral_slot_argv / ephemeral_slot_labels on a gunbc.managed_host host. +// Deregistration is NOT left to GitHub's ephemeral removal, which holds only when the one job ran: +// it is an ensure over every slot exit whose success is the runner absent from the organization's +// listing, with the host-side teardown carried by gunbc.runner.runner_teardown_receipt +// RunnerTeardownRequest over a host-unit inventory. Both are pre-approved under the dispatching +// run's authorization (operator ruling 2026-10-03), rowed in gunbc.auth.privileged_effect_census. +// Dispatch names the cited REST operation that has no production caller yet. +data mtcollins1_registration_leg: RouteLegStanding = LegBoundToAuthority { + authority: decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), } data mtcollins1_dispatch_leg: RouteLegStanding = LegAwaitingAuthority { @@ -232,9 +235,8 @@ data mtcollins1_dispatch_leg: RouteLegStanding = LegAwaitingAuthority { sufficient_for: "dispatching the floor to the one registration whose runs-on names the attempt label, so the run is unservicable by any other runner" as NonEmptyStr, } -data mtcollins1_deregistration_leg: RouteLegStanding = LegAwaitingAuthority { - lands_with: "the same JIT contract as mtcollins1_registration_leg: GitHub removes the ephemeral runner when its one job ends, and gunbc.runner.runner_teardown_receipt RunnerTeardownRequest carries the host-side teardown" as NonEmptyStr, - sufficient_for: "the registration being gone from the organization and the unit torn down after the one job, observed rather than assumed" as NonEmptyStr, +data mtcollins1_deregistration_leg: RouteLegStanding = LegBoundToAuthority { + authority: decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), } // THE FLEET-CONVERGE MODE THIS ROUTE BECOMES IS NOT NAMED HERE. A mode row belongs to diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 4e2780c4a02..9ff8df0845f 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -388,3 +388,15 @@ test fn every_root_of_the_standing_ruling_is_a_conforming_census_row() -> Bool { && match v.conformance { Conforms => true _ => false }) && any(privileged_effect_interlocks, i => declaration_ref_display_key(ref: i.site) == "gunbc.host_reset_return_run::host_reset_return_wet") } + +// THE 2026-10-03 PRE-APPROVAL IS A SELECTION, NOT A NEW VOCABULARY. JIT deregistration is a +// recurring automated effect under the dispatching run's federated identity, so the selection must +// answer the federated scoped grant and the row must conform; rowing it as an approval capability or +// a per-registration ntfy approval turns this red. +test fn the_jit_registration_and_deregistration_sites_select_the_federated_grant_and_conform() -> Bool { + ["dispatch_jit_mint", "ensure_jit_runner_deregistered"] |> all(name => + any(census_verdicts(), v => + (v.site.decl_name as String) == name + && selected_federated(s: v.selection) + && match v.conformance { Conforms => true _ => false })) +} diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index 699980188db..fe4b150ca85 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -33,6 +33,7 @@ import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis, AxisDeferred, } import gunbc.runner_jit_mint { + JitSlotBinding, MicroVmSlotBinding, MicroVmJitSlot, HostUnitSlotBinding, HostUnitJitSlot, JitMintRefusedHostUnitSlot, JitMintPlan, JitMintAuthorized, JitMintRefusedCellNotOwned, JitMintRefusedCredential, JitMintRefusedAttemptUnbound, JitMintRefusedRunnerGroupUnrestricted, plan_jit_mint, @@ -56,6 +57,15 @@ import gunbc.runner.runner_jit_perform { import extdeps.uri { Uri, uri_https } import extdeps.auth.jws { JwsSigningKeyRef, HostKeyFile } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.managed_host { managed_hosts, managed_host_binding, ManagedHostAccessUnobserved } +import extdeps.bmc.endpoint { BmcControllerEndpoint } +import extdeps.github.actions_runner { ActionsRunnerLinuxArm64 } +import gunbc.runner_slot_desired { gunbc_runner_slot_desired } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_labels, ephemeral_slot_argv } +import gunbc.runner.runner_host_unit_slot { + HostUnitSlotStanding, HostUnitSlotRefused, HostUnitSlotHostNotBound, host_unit_slot_for, host_unit_slot_listener_argv, +} data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -1005,13 +1015,15 @@ data jit_test_slot: RunnerSlotIdentity = RunnerSlotIdentity { // attempt to bind, so it answers with the binding's own refusal, carrying a cause that does NOT say // "not owned". If this fixture's id ever stopped being an acceptable segment, the positive control // goes RED rather than the fixture quietly becoming a refusal the negative arms would still accept. -fn jit_binding(owned: Bool) -> MicroVmCellBinding { - match microvm_attempt(slot: jit_test_slot, attempt_id: "w-11") { - MicroVmAttemptAccepted { attempt: a } => bind_owned_attempt(attempt: a, owned: owned) - MicroVmAttemptRefused { refusal: _ } => - VmUnboundCellNotOwned { - cause: "the witness fixture's attempt id was refused, so this binding is not the subject under test", - } +fn jit_binding(owned: Bool) -> JitSlotBinding { + MicroVmSlotBinding { + binding: match microvm_attempt(slot: jit_test_slot, attempt_id: "w-11") { + MicroVmAttemptAccepted { attempt: a } => bind_owned_attempt(attempt: a, owned: owned) + MicroVmAttemptRefused { refusal: _ } => + VmUnboundCellNotOwned { + cause: "the witness fixture's attempt id was refused, so this binding is not the subject under test", + } + }, } } @@ -1074,7 +1086,7 @@ test fn witness_jit_mint_refuses_a_credential_missing_the_runner_permission() -> test fn witness_jit_mint_authorizes_an_owned_cell_with_the_runner_permission() -> Bool { match planned(owned: true, perms: runner_grant) { JitMintAuthorized { - attempt: a, + slot: MicroVmJitSlot { attempt: a }, request: req, } => (req.name.value as String) == (runner_microvm_attempt_unit_name(attempt: a) as String) @@ -1093,7 +1105,7 @@ test fn witness_jit_mint_authorizes_an_owned_cell_with_the_runner_permission() - // only non-provenance label is the attempt's own name. test fn witness_jit_mint_carries_no_host_production_label() -> Bool { match planned(owned: true, perms: runner_grant) { - JitMintAuthorized { attempt: _, request: req } => + JitMintAuthorized { slot: _, request: req } => let wire = map(req.labels, l => l.value as String) count(filter(wire, w => w == "srv2" || w == "gunbc-heavy" || w == "linux" || w == "arm64")) == 0 && count(wire) == 2 @@ -1224,7 +1236,7 @@ test fn witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred() -> B test fn witness_jit_dispatch_authorizes_and_names_both_steps_of_the_app_path() -> Bool { match dispatched(owned: true, perms: runner_grant) { JitMintDispatchAuthorized { - attempt: a, + slot: MicroVmJitSlot { attempt: a }, app: app, installation: installation, organization: org, @@ -1283,12 +1295,12 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool jit_credential_delivered(delivery: delivery) && match delivery { JitCredentialBoundToAttempt { - attempt: a, + slot: a, credential: c, runner_id: id, } => match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { attempt: d, app: _, installation: _, organization: _, request: _ } => a == d + JitMintDispatchAuthorized { slot: d, app: _, installation: _, organization: _, request: _ } => a == d _ => false } && (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" @@ -1296,3 +1308,59 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool _ => false } } + +// ── THE HOST-UNIT ARM OF THE SAME MINT ──────────────────────────────────────────────────────── +// +// One plan over the slot sum (gunbc.runner_jit_mint JitSlotBinding), so these claims reuse the +// microVM claims' credential, control-plane and group fixtures unchanged: a fork of the mint per +// binding would need fixtures of its own, and that is what these claims would stop compiling on. +fn host_unit_planned(standing: HostUnitSlotStanding, perms: List) -> JitMintPlan { + plan_jit_mint( + binding: HostUnitSlotBinding { standing: standing }, + authority: app_credential, + control_plane: observed_grant(perms: perms), + organization: gunbai_org_ref, + runner_group: restricted_group_1(), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) +} + +// INHABITANCE, AND IT ASSERTS THE ROUTE: the real managed-host roster (gunbc.managed_host +// managed_hosts, the mtcollins1 row) binds, the slot is derived from that binding, and the mint +// registers under the route's own unit name and labels, launched by the route's own systemd-run +// argv over the arm64 runner artifact. Removing the managed-host row, or registering under a name or +// label set the route does not stage, turns this red. +test fn witness_jit_mint_authorizes_a_managed_host_unit_slot_under_the_routes_own_spelling() -> Bool { + let hosts = managed_hosts() + count(hosts) > 0 && hosts |> all(h => + match host_unit_planned(standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1"), perms: runner_grant) { + JitMintAuthorized { slot: HostUnitJitSlot { slot: u }, request: req } => + (req.name.value as String) == (ephemeral_slot_unit(host: h.host, attempt: "q-1") as String) + && runner_label_sets_equal(a: map(req.labels, l => l.value as String), b: ephemeral_slot_labels(host: h.host, attempt: "q-1")) + && u.argv == ephemeral_slot_argv(unit: u.unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()) + && match u.runner.arch { ActionsRunnerLinuxArm64 => true _ => false } + _ => false + }) +} + +// RED: A HOST THE MANAGED-HOST SEAM DOES NOT BIND HAS NO SLOT, and the refusal carries the seam's own +// standing rather than a re-coined cause -- even with a fully sufficient credential. +test fn witness_jit_mint_refuses_a_host_the_managed_host_seam_does_not_bind() -> Bool { + let unbound = ManagedHostAccessUnobserved { endpoint: BmcControllerEndpoint { host: "fixture-bmc" } } + match host_unit_planned(standing: host_unit_slot_for(host: unbound, attempt: "q-1"), perms: runner_grant) { + JitMintRefusedHostUnitSlot { refusal: HostUnitSlotHostNotBound { standing: ManagedHostAccessUnobserved { endpoint: e } } } => + (e.host as String) == "fixture-bmc" + _ => false + } +} + +// THE GATES ARE THE MINT'S, NOT THE BINDING'S: a bound host-unit slot under a credential without +// organization self-hosted-runners write refuses exactly as an owned microVM cell does. +test fn witness_a_host_unit_slot_passes_through_the_same_credential_gate() -> Bool { + managed_hosts() |> all(h => + match host_unit_planned(standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1"), perms: full_workflow_grant) { + JitMintRefusedCredential { organization: org, unsatisfied: u } => (org as String) == "gunb-ai" && count(u) > 0 + _ => false + }) +} + diff --git a/dag/test/claim/github_effect_perform_witness_test.dag b/dag/test/claim/github_effect_perform_witness_test.dag index b99a4b89a1e..1b399fdf1f3 100644 --- a/dag/test/claim/github_effect_perform_witness_test.dag +++ b/dag/test/claim/github_effect_perform_witness_test.dag @@ -102,7 +102,7 @@ data created_body: JitConfigCreatedBody = JitConfigCreatedBody { test fn witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered() -> Bool { let d = dispatched(owned: true, perms: runner_grant) let delivered = match jit_mint_performance_of_generate(dispatch: d, outcome: RestOk, created: created_body) { - JitMintReceived { delivery: JitCredentialBoundToAttempt { attempt: _, credential: c, runner_id: id } } => + JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id } } => (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" && id == 4242 _ => false } diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index d19ee0c2925..6d59f19e10f 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -60,6 +60,7 @@ import extdeps.github.actions_jit_runner { EncodedJitConfig, JitConfigMinted, jit_config_mint_outcome, } import extdeps.transports.rest { RestExchangeCommitAmbiguous } +import gunbc.runner_jit_mint { MicroVmJitSlot } import gunbc.runner.runner_jit_perform { JitCredentialBoundToAttempt, JitCredentialMintNotCreated, } @@ -136,7 +137,7 @@ fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance match minted_credential(encoded: encoded) { Cons { head: c, tail: _ } => JitMintReceived { - delivery: JitCredentialBoundToAttempt { attempt: attempt, credential: c, runner_id: registration_fixture_id }, + delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id }, } Empty {} => JitMintAppJwtUnsigned { detail: "fixture: credential fixture did not mint" } } @@ -275,7 +276,7 @@ test fn a_credential_bound_to_another_attempt_stages_no_device() -> Bool { let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: b), ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { - Cons { head: JitCredentialBoundToDifferentAttempt { bound_to: other }, tail: _ } => other == b + Cons { head: JitCredentialBoundToDifferentAttempt { bound_to: other }, tail: _ } => other == MicroVmJitSlot { attempt: b } _ => false } } diff --git a/dag/test/claim/runner/runner_canary_receipt_witness_test.dag b/dag/test/claim/runner/runner_canary_receipt_witness_test.dag index de75bdcb924..f6b907642dd 100644 --- a/dag/test/claim/runner/runner_canary_receipt_witness_test.dag +++ b/dag/test/claim/runner/runner_canary_receipt_witness_test.dag @@ -27,7 +27,8 @@ import gunbc.runner.runner_teardown_receipt { RunnerResourceClass, RunnerVmm, RunnerCgroup, RunnerJail, RunnerJailMount, RunnerJitDrive, RunnerWorkspace, RunnerTap, RunnerFirewallTable, TeardownObserverInsideEnvironment, TeardownInstrumentChanged, - RunnerEnvironmentInventory, RunnerResourceReading, RunnerResourcePresent, + RunnerEnvironmentInventory, MicroVmEnvironmentInventory, MicroVmInventory, HostUnitEnvironmentInventory, + HostUnitInventory, RunnerTransientUnit, RunnerResourceReading, RunnerResourcePresent, RunnerResourceAbsent, RunnerResourceUnreadable, RunnerEnvironmentReadback, RunnerEnvironmentObservation, RunnerEnvironmentObserved, RunnerEnvironmentUnobserved, RunnerTeardownRequest, RunnerTeardownResult, RunnerTeardownProduced, RunnerTeardownRefused, @@ -226,17 +227,21 @@ fn fixture_process(pid: Nat) -> ProcessIdentity { ProcessIdentity { boot_id: "fixture-boot", pid: pid, start_time: "100" } } -fn fixture_inventory(tap: NonEmptyStr) -> RunnerEnvironmentInventory { - RunnerEnvironmentInventory { +fn fixture_microvm_inventory(tap: NonEmptyStr) -> MicroVmEnvironmentInventory { + MicroVmEnvironmentInventory { vmm: fixture_process(pid: 10), cgroup: "/attempt/cgroup", jail: "/attempt/jail", jail_mount: "/attempt/jail", jit_drive: "/attempt/jit", workspace: "/attempt/work", tap: tap, firewall_table: "attempt-table", } } +fn fixture_inventory(tap: NonEmptyStr) -> RunnerEnvironmentInventory { + MicroVmInventory { inventory: fixture_microvm_inventory(tap: tap) } +} + fn fixture_readback(value: RunnerResourceReading) -> RunnerEnvironmentReadback { RunnerEnvironmentReadback { - vmm: value, cgroup: value, jail: value, jail_mount: value, + vmm: value, unit: value, cgroup: value, jail: value, jail_mount: value, jit_drive: value, workspace: value, tap: value, firewall_table: value, } } @@ -311,14 +316,14 @@ test fn changing_observation_instrument_refuses() -> Bool { test fn cross_boot_processes_refuse() -> Bool { let subject = fixture_subject(principal: "tenant-a", attempt: "attempt-a") - let original = fixture_inventory(tap: "tap-a") + let original = fixture_microvm_inventory(tap: "tap-a") ["vmm", "destroyer", "observer"] |> all(axis => { - let inventory = RunnerEnvironmentInventory { + let inventory = MicroVmInventory { inventory: MicroVmEnvironmentInventory { vmm: ProcessIdentity { boot_id: if axis == "vmm" { "other-boot" } else { "fixture-boot" }, pid: 10, start_time: "100" }, cgroup: original.cgroup, jail: original.jail, jail_mount: original.jail_mount, jit_drive: original.jit_drive, workspace: original.workspace, tap: original.tap, firewall_table: original.firewall_table, - } + } } let destroyer = ProcessIdentity { boot_id: if axis == "destroyer" { "other-boot" } else { "fixture-boot" }, pid: 20, start_time: "100", } @@ -351,6 +356,7 @@ test fn an_always_absent_instrument_refuses() -> Bool { fn fixture_single_residue(resource: RunnerResourceClass, value: RunnerResourceReading) -> RunnerEnvironmentReadback { RunnerEnvironmentReadback { vmm: if resource == RunnerVmm { value } else { RunnerResourceAbsent }, + unit: if resource == RunnerTransientUnit { value } else { RunnerResourceAbsent }, cgroup: if resource == RunnerCgroup { value } else { RunnerResourceAbsent }, jail: if resource == RunnerJail { value } else { RunnerResourceAbsent }, jail_mount: if resource == RunnerJailMount { value } else { RunnerResourceAbsent }, @@ -381,6 +387,71 @@ test fn every_residue_and_unreadable_resource_refuses() -> Bool { }) } +// THE HOST-UNIT ARM OF THE SAME FOLD. A transient unit on a managed host owns no jail, jail mount, +// JIT drive, tap or firewall table, so its positive control is the three resources its launch +// allocates. The readback below reads every microVM-only class Absent BEFORE as well as after: a +// microVM inventory would refuse that as TeardownPositiveControlMissing, and this inventory must not. +fn fixture_host_unit_inventory(unit: NonEmptyStr) -> RunnerEnvironmentInventory { + HostUnitInventory { inventory: HostUnitEnvironmentInventory { + unit_process: fixture_process(pid: 10), unit: unit, cgroup: "/system.slice/attempt.service", workspace: "/attempt/work", + } } +} + +fn fixture_host_unit_readback(unit: RunnerResourceReading, cgroup: RunnerResourceReading, workspace: RunnerResourceReading) -> RunnerEnvironmentReadback { + RunnerEnvironmentReadback { + vmm: RunnerResourceAbsent, unit: unit, cgroup: cgroup, jail: RunnerResourceAbsent, jail_mount: RunnerResourceAbsent, + jit_drive: RunnerResourceAbsent, workspace: workspace, tap: RunnerResourceAbsent, firewall_table: RunnerResourceAbsent, + } +} + +fn fixture_host_unit_teardown(inventory: RunnerEnvironmentInventory, after: RunnerEnvironmentReadback) -> RunnerTeardownResult { + let subject = fixture_subject(principal: "tenant-a", attempt: "attempt-a") + let unit_inventory = fixture_host_unit_inventory(unit: "attempt.service") + produce_runner_teardown_receipt( + request: RunnerTeardownRequest { subject: subject, inventory: unit_inventory, destroyer: fixture_process(pid: 20), issued_at: fixture_at(ns: 20) }, + before: RunnerEnvironmentObserved { observation: fixture_environment(subject: subject, observer: fixture_process(pid: 30), + inventory: unit_inventory, at: fixture_at(ns: 10), + readback: fixture_host_unit_readback(unit: RunnerResourcePresent, cgroup: RunnerResourcePresent, workspace: RunnerResourcePresent)) }, + after: RunnerEnvironmentObserved { observation: fixture_environment(subject: subject, observer: fixture_process(pid: 30), + inventory: inventory, at: fixture_at(ns: 30), readback: after) }) +} + +test fn a_host_unit_teardown_is_accepted_without_microvm_resources() -> Bool { + match fixture_host_unit_teardown( + inventory: fixture_host_unit_inventory(unit: "attempt.service"), + after: fixture_host_unit_readback(unit: RunnerResourceAbsent, cgroup: RunnerResourceAbsent, workspace: RunnerResourceAbsent), + ) { + RunnerTeardownProduced { receipt: _ } => true + _ => false + } +} + +test fn every_surviving_host_unit_resource_refuses() -> Bool { + [RunnerTransientUnit, RunnerCgroup, RunnerWorkspace] |> all(resource => + match fixture_host_unit_teardown( + inventory: fixture_host_unit_inventory(unit: "attempt.service"), + after: fixture_host_unit_readback( + unit: if resource == RunnerTransientUnit { RunnerResourcePresent } else { RunnerResourceAbsent }, + cgroup: if resource == RunnerCgroup { RunnerResourcePresent } else { RunnerResourceAbsent }, + workspace: if resource == RunnerWorkspace { RunnerResourcePresent } else { RunnerResourceAbsent }, + ), + ) { + RunnerTeardownRefused { cause: TeardownResourceSurvived { resource: r } } => r == resource + _ => false + } + ) +} + +test fn a_host_unit_observed_as_another_unit_or_a_microvm_refuses() -> Bool { + let clean = fixture_host_unit_readback(unit: RunnerResourceAbsent, cgroup: RunnerResourceAbsent, workspace: RunnerResourceAbsent) + [fixture_host_unit_inventory(unit: "other.service"), fixture_inventory(tap: "tap-a")] |> all(inventory => + match fixture_host_unit_teardown(inventory: inventory, after: clean) { + RunnerTeardownRefused { cause: TeardownInventoryMismatch } => true + _ => false + } + ) +} + test fn missing_observer_receipt_is_detectable() -> Bool { let subject = fixture_subject(principal: "tenant-a", attempt: "attempt-a") let inventory = fixture_inventory(tap: "tap-a") diff --git a/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag b/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag new file mode 100644 index 00000000000..5c29610fc0c --- /dev/null +++ b/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag @@ -0,0 +1,94 @@ +module test.claim.runner_jit_deregistration_witness + +import std.types { Bool, Int, List, NonEmptyStr, String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.upsert_decision { Noop, Apply, Refuse } +import extdeps.transports.rest { RestExchangeSucceeded, RestExchangeStatusRefused, RestExchangeCommitAmbiguous, RestExchangeUnreached } +import extdeps.github.effect { GitHubOrganizationRef } +import extdeps.github.actions_jit_runner { JitRunnerName } +import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire, OrganizationRunnerWire } +import gunbc.github_effect_perform { OrganizationRunnerListRead } +import gunbc.managed_host { managed_hosts, managed_host_binding } +import gunbc.runner.runner_host_unit_slot { host_unit_slot_for, HostUnitSlotBound } +import gunbc.runner_jit_mint { HostUnitJitSlot } +import gunbc.runner.runner_jit_deregistration { + JitRegistration, JitSlotExit, SlotJobCompleted, SlotJobFailed, SlotNeverLaunched, + JitRunnerListing, JitRunnerListed, JitRunnerNotListed, JitRunnerListingUnreadable, + JitRunnerDeleteAttempt, JitDeregistration, JitRunnerDeregistered, JitDeregistrationRefused, + DeregistrationRunnerStillListed, DeregistrationListingUnreadable, + jit_runner_listing_of, plan_jit_deregistration, conclude_jit_deregistration, jit_runner_deregistered, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data runner_name: JitRunnerName = JitRunnerName { value: "gunbc-qualification-mtcollins1-q-1.service" } + +fn listing_read(runners: List, total: Int) -> OrganizationRunnerListRead { + OrganizationRunnerListRead { performance: RestExchangeSucceeded, body: OrganizationRunnerListWire { total_count: total, runners: runners } } +} + +fn registrations() -> List { + managed_hosts() |> flat_map(h => + match host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1") { + HostUnitSlotBound { slot: s } => + [JitRegistration { organization: GitHubOrganizationRef { login: "gunb-ai" }, name: runner_name, slot: HostUnitJitSlot { slot: s } }] + _ => [] + }) +} + +data every_exit: List = [ + SlotJobCompleted, + SlotJobFailed { cause: "the floor job failed" }, + SlotNeverLaunched { cause: "the unit never started" }, +] + +// THE LISTING IS AN IDENTITY JOIN ON THE MINTED NAME: a different runner in the page is not this +// registration, and a page shorter than total_count is unreadable rather than absent. +test fn the_listing_finds_the_registration_by_name_and_refuses_a_short_page() -> Bool { + let other = OrganizationRunnerWire { id: 7, name: "some-other-runner" } + let mine = OrganizationRunnerWire { id: 42, name: runner_name.value as String } + (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other], total: 1)) { JitRunnerNotListed => true _ => false }) + && (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other, mine], total: 2)) { + JitRunnerListed { ids: ids } => ids == [42] + _ => false + }) + && (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other], total: 2)) { + JitRunnerListingUnreadable { performance: _, cause: _ } => true + _ => false + }) + && (match jit_runner_listing_of(name: runner_name, read: OrganizationRunnerListRead { + performance: RestExchangeUnreached { cause: "fixture: connection refused" }, + body: OrganizationRunnerListWire { total_count: 0, runners: [] }, + }) { + JitRunnerListingUnreadable { performance: _, cause: _ } => true + _ => false + }) +} + +test fn the_plan_is_an_ensure_over_the_listing() -> Bool { + (match plan_jit_deregistration(listing: JitRunnerNotListed) { Noop => true _ => false }) + && (match plan_jit_deregistration(listing: JitRunnerListed { ids: [42] }) { Apply { plan: ids } => ids == [42] _ => false }) + && (match plan_jit_deregistration(listing: JitRunnerListingUnreadable { performance: RestExchangeSucceeded, cause: "fixture" }) { + Refuse { reason: _ } => true + _ => false + }) +} + +// SUCCESS IS THE READBACK AND ONLY THE READBACK, ON EVERY EXIT. A 204 with the runner still listed +// refuses; an ambiguous delete whose readback shows it gone succeeds; an unreadable readback refuses; +// and none of that depends on how the slot exited. +test fn deregistration_succeeds_only_on_an_absent_readback_on_every_exit() -> Bool { + let deleted_ok = [JitRunnerDeleteAttempt { runner_id: 42, performance: RestExchangeSucceeded }] + let deleted_ambiguous = [JitRunnerDeleteAttempt { runner_id: 42, performance: RestExchangeCommitAmbiguous { detail: "fixture: 502 after the write" } }] + count(registrations()) > 0 && registrations() |> all(reg => every_exit |> all(exit => + jit_runner_deregistered(result: conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ambiguous, readback: JitRunnerNotListed)) + && (match conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ok, readback: JitRunnerListed { ids: [42] }) { + JitDeregistrationRefused { registration: _, exit: _, refusal: DeregistrationRunnerStillListed { ids: ids, deletes: _ } } => ids == [42] + _ => false + }) + && (match conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ok, + readback: JitRunnerListingUnreadable { performance: RestExchangeStatusRefused { status: 403, body: "fixture" }, cause: "fixture" }) { + JitDeregistrationRefused { registration: _, exit: _, refusal: DeregistrationListingUnreadable { performance: _, cause: _ } } => true + _ => false + }))) +} diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 0aff5b7eb4d..18b4994dd80 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -44,6 +44,7 @@ import gunbc.runner_microvm_slot_controller { slot_of_instance, minted_registrat import gunbc.fabric_required_build_cell { ReservationUnreadable, ReservationPayloadUndecodable } import std.durable_compare_and_set { CasUnreadableReadRefused } import gunbc.github_effect_perform { JitMintReceived, JitMintClockUnread } +import gunbc.runner_jit_mint { MicroVmJitSlot } import gunbc.runner.runner_jit_perform { JitCredentialBoundToAttempt, JitCredentialMintNotCreated, JitCredentialNotDispatched, DispatchRefusedCellNotOwned } import extdeps.github.actions_jit_runner { jit_config_mint_outcome, JitConfigMinted } import gunbc.live_deploy.emit { microvm_slot_unit_file_with_workspace_root } @@ -292,7 +293,7 @@ fn delivered_mint() -> Bool { Present { value: a } => match jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: "") { JitConfigMinted { config: c, runner_id: id } => - minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { attempt: a, credential: c, runner_id: id } }) == Present { value: 4242 } + minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id } }) == Present { value: 4242 } _ => false } } diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index 0f3cb2b6a65..e20d3e49c61 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -58,7 +58,7 @@ import gunbc.runner_throughput_qualification { SlotWidthDerived, SlotWidthUnderivable, ThroughputRefusal, SubjectJoinDidNotHold, SamplingDidNotHold, RunnerThroughputAssessment, assess_runner_throughput, assessment_holds, assessment_wire, - qualification_run_producer_frontier, leg_is_bound, + qualification_run_producer_frontier, leg_is_bound, RouteLegStanding, LegBoundToAuthority, AssessedFact, InterpreterRateAgainstPin, SlotMemoryEnvelope, PhaseWallsAgainstSuppliedCeilings, ConcurrentCohortAtWidth, RunnerCohortEvidence, NoCohortObserved, CohortObserved, run_key, } @@ -747,8 +747,8 @@ test fn the_route_classifies_every_mutation_at_its_grain() -> Bool { )) } -// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: every effectful leg is a declared -// frontier. This flips to the regression control that the bindings stay when they land. +// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the boot and dispatch legs are still +// declared frontiers. This flips to the regression control that the bindings stay when they land. test fn the_route_is_not_executable_while_any_effectful_leg_is_unbound() -> Bool { !route_is_executable(route: route()) && !leg_is_bound(leg: qualification_run_producer_frontier) } @@ -759,6 +759,25 @@ test fn the_image_leg_is_bound_and_the_boot_leg_awaits_a_runner_medium() -> Bool leg_is_bound(leg: mtcollins1_route_legs.image) && !leg_is_bound(leg: mtcollins1_route_legs.boot) } +// REGISTRATION AND DEREGISTRATION ARE BOUND, AND TO THE AUTHORITIES THAT PERFORM THEM -- by +// declaration identity, so pointing either leg at some other declaration turns this red -- while the +// route stays unexecutable: boot and dispatch are still awaiting authority, and the claim above +// keeps route_is_executable false until they bind. +fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> Bool { + match leg { + LegBoundToAuthority { authority: a } => (a.module_path as String) == module_path && (a.decl_name as String) == decl_name + _ => false + } +} + +test fn the_registration_and_deregistration_legs_are_bound_and_the_route_still_is_not() -> Bool { + leg_names(leg: mtcollins1_route_legs.registration, module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint") + && leg_names(leg: mtcollins1_route_legs.deregistration, module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered") + && !leg_is_bound(leg: mtcollins1_route_legs.boot) + && !leg_is_bound(leg: mtcollins1_route_legs.dispatch) + && !route_is_executable(route: route()) +} + test fn the_route_registers_one_slot_deregisters_it_and_dispatches_to_the_attempt() -> Bool { route_registers_one_slot(route: route()) && route_deregisters_what_it_registered(route: route()) From 64232065e23d7719615a57f7be51e10b385b37c6 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 04:10:37 +0000 Subject: [PATCH 02/44] mtcollins1 runner: bind the dispatch leg to CreateDispatch with the attempt label as input; collect the run back (leg 3) Generalizes extdeps.github.workflows CreateDispatch from the heal-only expected_healed_sha key to the upstream's own inputs object, and dissolves create_dispatch_unconsumed_frontier_rows with a production caller. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/workflow_runs.dag | 1 + dag/extdeps/github/workflows.dag | 32 ++- dag/gunbc/auth/privileged_effect_census.dag | 15 ++ dag/gunbc/census_closure_frontier.dag | 4 +- .../runner/runner_qualification_dispatch.dag | 202 ++++++++++++++++++ .../runner_throughput_qualification_route.dag | 12 +- ...ization_pattern_selection_witness_test.dag | 7 +- ...er_qualification_dispatch_witness_test.dag | 150 +++++++++++++ ..._throughput_qualification_witness_test.dag | 16 +- 9 files changed, 400 insertions(+), 39 deletions(-) create mode 100644 dag/gunbc/runner/runner_qualification_dispatch.dag create mode 100644 dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 13ad9b70602..98df2230815 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -310,6 +310,7 @@ service github.WorkflowRuns { } output { run: WorkflowRun + outcome: RestOutcome } readonly transport rest { diff --git a/dag/extdeps/github/workflows.dag b/dag/extdeps/github/workflows.dag index 8a78952fb9f..b1f0d3fb243 100644 --- a/dag/extdeps/github/workflows.dag +++ b/dag/extdeps/github/workflows.dag @@ -3,12 +3,10 @@ module extdeps.github.workflows import extdeps.github.github { default_api_base } import extdeps.github.errors { GitHubErrorShape } import std.credentials { EnvVar } -import std.types { CommitSha, List, NonEmptyStr } +import std.types { NonEmptyStr, String } +import extdeps.transports.rest { RestOutcome } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } -import std.roster_frontier { FrontierRow, frontier_row_decl } -import std.dissolution { unbound_dissolution } -import std.decl_ref { decl_ref } import std.resources { Network } import extdeps.ietf.http_semantics { POST } @@ -40,20 +38,13 @@ type WorkflowDispatchReceipt { // WorkflowDispatchReceipt would fabricate a run that was refused. The owning workflow may be // retriggered after diagnosis; this operation never assumes a git push created a run. // -// FROZEN UPSTREAM SHAPE: tools.ci_heal_dispatch, the only production caller, is deleted. Restoring -// heal auto-dispatch does not consume this operation. `expected_healed_sha` is the `inputs` -// key GitHub accepts for this repository's workflow_dispatch contract; it is not a second heal -// policy and must not grow more heal-specific keys here. Disposition: -// create_dispatch_unconsumed_frontier_rows. - -data create_dispatch_unconsumed_frontier_rows: List = [ - frontier_row_decl( - ref: decl_ref(module_path: "extdeps.github.workflows", decl_name: "WorkflowDispatchReceipt"), - reason: "github.Workflows CreateDispatch and WorkflowDispatchReceipt are the cited REST shape with no production caller after tools.ci_heal_dispatch was deleted; frozen: no new heal-specific keys or auto-invoke rows", - dissolution: unbound_dissolution(description: "a production fold that is not heal auto-dispatch calls github.Workflows CreateDispatch, or CreateDispatch and WorkflowDispatchReceipt are deleted. NOT satisfied by restoring tools.ci_heal_dispatch, nor by a witness constructing WorkflowDispatchReceipt"), - ) -] - +// `inputs` IS THE UPSTREAM'S OWN SHAPE: an object of input name to value, at most 25 properties, +// each key a workflow_dispatch input the target workflow declares (extdeps.github.actions +// DispatchInput). It once carried exactly one key, expected_healed_sha, because the deleted +// tools.ci_heal_dispatch was its only caller -- a consumer's policy fused into the interface +// (DESIGN section 3). Which keys a dispatch carries is the dispatching workflow's fact, so the +// caller supplies the object and this module names no key. GitHub answers an undeclared key with +// 422, which is the refusal arm below, never a silently dropped input. service github.Workflows { config { endpoint: default_api_base @@ -68,12 +59,13 @@ service github.Workflows { repo: String workflow_id: String ref: String - expected_healed_sha: CommitSha + inputs: Map } output { workflow_run_id: Int run_url: NonEmptyStr html_url: NonEmptyStr + outcome: RestOutcome } transport rest { method: POST, @@ -81,7 +73,7 @@ service github.Workflows { headers: { "X-GitHub-Api-Version": workflows_api_version }, body: { ref: ref, - inputs: { expected_healed_sha: expected_healed_sha } + inputs: inputs } } response { diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index a2f3a010c09..392d99b174c 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -550,6 +550,21 @@ data privileged_effect_census: List = [ realized: RealizedFederatedGrant, divergence_reason: none, }, + PrivilegedEffectSite { + site: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + effect: PrivilegedEffect { + subject: "dispatch the qualification floor workflow with the slot's attempt label as its input, so only the one JIT registration carrying that label can serve the job, under the dispatching run's own GITHUB_TOKEN, pre-approved under the dispatching run's authorization (operator ruling 2026-10-03)" as NonEmptyStr, + frequency: EveryProvision, + reversibility: ReversibleByReapply, + surface: ApiSurface, + workload_identity: convergence_principal(), + minted_reach: MintsNoCredential, + billing: NoBillingConsequence, + witness_discharge: NoWitnessDischarge, + }, + realized: RealizedFederatedGrant, + divergence_reason: none, + }, PrivilegedEffectSite { site: site(module_path: "gunbc.runner_group_restriction_ensure_run", decl_name: "microvm_runner_group_after_gate"), effect: PrivilegedEffect { diff --git a/dag/gunbc/census_closure_frontier.dag b/dag/gunbc/census_closure_frontier.dag index 5e8b9f7bbec..827651a2659 100644 --- a/dag/gunbc/census_closure_frontier.dag +++ b/dag/gunbc/census_closure_frontier.dag @@ -21,11 +21,11 @@ import extdeps.docker.container_stats { } import extdeps.docker.endpoint { endpoint_string_type_frontier_rows } import extdeps.github.merge_state { merge_state_graphql_transport_frontier_rows } +import gunbc.runner.runner_qualification_dispatch { qualification_instrument_extraction_frontier_rows } import extdeps.github.workflow_runs { conclusion_authority_consolidation_frontier_rows, } import extdeps.github.app { modeled_webhook_event_vocabulary_frontier_rows } -import extdeps.github.workflows { create_dispatch_unconsumed_frontier_rows } import gunbc.public_workload_census { public_workload_census_replay_consumer_frontier_rows, public_workload_census_run_attempt_consumer_frontier_rows, @@ -109,7 +109,7 @@ fn census_closure_frontier_row_groups() -> List> { endpoint_string_type_frontier_rows, merge_state_graphql_transport_frontier_rows, conclusion_authority_consolidation_frontier_rows, - create_dispatch_unconsumed_frontier_rows, + qualification_instrument_extraction_frontier_rows, modeled_webhook_event_vocabulary_frontier_rows, census_app_installation_route_frontier_rows, census_app_installation_population_frontier_rows, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag new file mode 100644 index 00000000000..970b47f2f9b --- /dev/null +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -0,0 +1,202 @@ +module gunbc.runner.runner_qualification_dispatch + +import std.types { Bool, Int, List, NonEmptyStr, String } +import std.resources { Network } +import std.roster_frontier { FrontierRow, frontier_row_decl } +import std.dissolution { unbound_dissolution } +import std.decl_ref { decl_ref } +import extdeps.transports.rest { RestOutcome, RestOk } +import extdeps.github.actions { DispatchInput, InputString } +import extdeps.github.workflows { WorkflowDispatchReceipt } +import extdeps.github.workflow_runs { + WorkflowRun, WorkflowRunStatus, Completed, WorkflowRunConclusion, WorkflowJobRun, WorkflowJobRunList, +} +import gunbc.runner_throughput_qualification_route { QualificationInstrument, qualification_instruments } + +// THE DISPATCH LEG OF THE QUALIFICATION ROUTE, AND THE COLLECT STAGE THAT READS ITS RUN BACK. +// +// The route registers one ephemeral slot whose label set carries a per-boot attempt label +// (gunbc.runner_throughput_qualification_route ephemeral_slot_labels). Dispatching the floor to that +// slot is a workflow_dispatch whose input IS the attempt label, so the target workflow's runs-on can +// name it and no other registration in the organization can serve the job. This module is the +// production caller of extdeps.github.workflows github.Workflows CreateDispatch; its landing is what +// dissolved that module's unconsumed-operation frontier. +// +// WHICH WORKFLOW IS A PARAMETER. The fleet-converge mode that becomes this route belongs to +// gunbc.fleet_converge_workflow and lands with the boot leg; naming a workflow file here before that +// mode exists would be a second spelling of a fact whose home is not yet written. What this module +// does own is the input the target workflow must declare: qualification_attempt_dispatch_input is +// the DispatchInput row the workflow side consumes, and the dispatch map is keyed by its name, so +// the key the dispatch sends and the key the workflow declares are one declaration. +// +// AUTHORIZATION IS THE DISPATCHING RUN'S (operator ruling 2026-10-03): CreateDispatch authenticates +// with the running job's GITHUB_TOKEN (the service config's auth_source), with no per-dispatch +// approval. gunbc.auth.privileged_effect_census rows dispatch_qualification_floor through +// gunbc.auth.authorization_pattern_selection. + +data qualification_attempt_dispatch_input: DispatchInput = DispatchInput { + name: "runner_attempt_label", + description: Present { value: "the per-boot attempt label the ephemeral qualification slot registered with; runs-on names it" }, + required: true, + default: none, + type: InputString, +} + +type QualificationWorkflowTarget { + owner: NonEmptyStr + repo: NonEmptyStr + workflow_id: NonEmptyStr + ref: NonEmptyStr +} + +fn qualification_dispatch_inputs(attempt: NonEmptyStr) -> Map { + map_insert(m: empty_map(), key: qualification_attempt_dispatch_input.name, value: attempt as String) +} + +// A DISPATCH IS ONE ATTEMPT AND ITS ANSWER IS THE RECEIPT OR THE REFUSAL, never a run assumed to +// exist. The run id comes from GitHub's 2026-03-10 response (WorkflowDispatchReceipt); a refused or +// unreached request carries its RestOutcome and mints no run. +type QualificationDispatch + = QualificationDispatched { attempt: NonEmptyStr, receipt: WorkflowDispatchReceipt } + | QualificationDispatchRefused { attempt: NonEmptyStr, outcome: RestOutcome } + +fn dispatch_qualification_floor(target: QualificationWorkflowTarget, attempt: NonEmptyStr) -> QualificationDispatch uses net: Network { + let answered = github.Workflows.CreateDispatch( + owner: target.owner as String, + repo: target.repo as String, + workflow_id: target.workflow_id as String, + ref: target.ref as String, + inputs: qualification_dispatch_inputs(attempt: attempt), + ) + match answered.outcome { + RestOk => + QualificationDispatched { + attempt: attempt, + receipt: WorkflowDispatchReceipt { + workflow_run_id: answered.workflow_run_id, + run_url: answered.run_url, + html_url: answered.html_url, + }, + } + other => QualificationDispatchRefused { attempt: attempt, outcome: other } + } +} + +// ── COLLECT: THE RUN READ BACK, ENSURE-STYLE ────────────────────────────────────────────────── +// +// A DISPATCH RECEIPT SAYS A RUN EXISTS, NOT THAT THE ATTEMPT'S RUNNER SERVED IT. The collection +// therefore decides only from what GitHub reports about the run now: the run must be Completed with +// a conclusion, and at least one of its jobs must carry the attempt label and have started -- that +// is, been taken by a runner registered under the attempt. A run that finished without that is the +// typed refusal RunNeverReachedAttemptRunner, carrying every job's labels so the cause is located; +// it is never collected as a measurement of a host that did not run it. +// +// THE CONCLUSION IS READ BACK AND CARRIED, NOT FILTERED: a failed floor on the attempt's runner is +// still a run of this host and the receipt says how it ended. + +type QualificationRunRefusal + = RunReadUnreadable { outcome: RestOutcome } + | RunNotCompleted { status: WorkflowRunStatus } + | RunCompletedWithoutConclusion + | RunJobsUnreadable { outcome: RestOutcome } + | RunJobsTruncated { returned: Int, total: Int } + | RunNeverReachedAttemptRunner { job_labels: List> } + +// INSTRUMENT EXTRACTION IS A DECLARED FRONTIER, carried on the collected arm as the instruments the +// run was read for and not yet extracted from (qualification_instrument_extraction_frontier_rows). +type QualificationRunCollection + = QualificationRunCollected { + attempt: NonEmptyStr + run_id: Int + conclusion: WorkflowRunConclusion + attempt_jobs: List + instruments_unextracted: List + } + | QualificationRunRefused { attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal } + +fn job_served_by_attempt(job: WorkflowJobRun, attempt: NonEmptyStr) -> Bool { + (job.labels |> any(l => l == (attempt as String))) + && (match job.started_at { Present { value: _ } => true Absent => false }) +} + +fn refuse_run(attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal) -> QualificationRunCollection { + QualificationRunRefused { attempt: attempt, run_id: run_id, refusal: refusal } +} + +fn conclude_qualification_run( + attempt: NonEmptyStr, + run_id: Int, + run_outcome: RestOutcome, + run: WorkflowRun, + jobs_outcome: RestOutcome, + jobs: WorkflowJobRunList, +) -> QualificationRunCollection { + match run_outcome { + RestOk => + match run.status { + Completed => + match run.conclusion { + Absent => refuse_run(attempt: attempt, run_id: run_id, refusal: RunCompletedWithoutConclusion) + Present { value: conclusion } => + match jobs_outcome { + RestOk => + if count(jobs.jobs) != jobs.total_count { + refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsTruncated { returned: count(jobs.jobs), total: jobs.total_count }) + } else { + let served = jobs.jobs |> filter(j => job_served_by_attempt(job: j, attempt: attempt)) + if count(served) == 0 { + refuse_run(attempt: attempt, run_id: run_id, refusal: RunNeverReachedAttemptRunner { job_labels: jobs.jobs |> map(j => j.labels) }) + } else { + QualificationRunCollected { + attempt: attempt, + run_id: run_id, + conclusion: conclusion, + attempt_jobs: served, + instruments_unextracted: qualification_instruments, + } + } + } + other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsUnreadable { outcome: other }) + } + } + other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunNotCompleted { status: other }) + } + other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunReadUnreadable { outcome: other }) + } +} + +fn qualification_run_collected(c: QualificationRunCollection) -> Bool { + match c { + QualificationRunCollected { attempt: _, run_id: _, conclusion: _, attempt_jobs: _, instruments_unextracted: _ } => true + QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false + } +} + +fn collect_qualification_run( + target: QualificationWorkflowTarget, + dispatched: WorkflowDispatchReceipt, + attempt: NonEmptyStr, +) -> QualificationRunCollection uses net: Network { + let run_id = dispatched.workflow_run_id as String + let read = github.WorkflowRuns.GetRun(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) + let jobs = github.WorkflowRuns.ListJobs(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) + conclude_qualification_run( + attempt: attempt, + run_id: dispatched.workflow_run_id, + run_outcome: read.outcome, + run: read.run, + jobs_outcome: jobs.outcome, + jobs: jobs.result, + ) +} + +// THE EXTRACTION FRONTIER, NAMED AT THE CAPABILITY GRAIN. Four instruments are run outputs and are +// reached through the run's artifacts and job logs; two are not run outputs at all and their sources +// are named here so the extraction leg does not go looking for them in the run. +data qualification_instrument_extraction_frontier_rows: List = [ + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "QualificationRunCollection"), + reason: "a collected qualification run carries every qualification_instrument as unextracted: extdeps.github has no Actions artifacts or job-logs read operation, no path that follows the logs redirect or opens the artifact archive, and no parser for the [floor-phase] and [floor-cgroup] log rows. Two instruments are not run outputs: GuestIdleMeminfoAndNproc comes from the boot leg's runner-host-up readback, and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort", + dissolution: unbound_dissolution(description: "the Actions artifacts and job-logs read path (list and download a run's artifacts, download a job's log text) plus the floor row parsers are SUFFICIENT to populate FloorPhaseRows, FloorCgroupRows, ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from one collected run through the producer each instrument names, with GuestIdleMeminfoAndNproc read from the boot leg's runner-host-up readback and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort, so QualificationRunCollected carries readings for every qualification_instrument and no unextracted list. NOT satisfied by an artifacts operation with no parser, nor by a parser exercised only on a fixture"), + ) +] diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index b520e896640..7697d114de3 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -33,8 +33,8 @@ import gunbc.runner_throughput_qualification { // derivation takes its payload as a parameter. The boot leg is not, and not for want of a landing: // the boot run on main (`gunbc.machine_intake_mtcollins1_boot_run`) boots exactly two media, the // census image and the stock installer, and its terminal is the census program's, so it cannot boot a -// runner and observe it ready. `extdeps.github.workflows CreateDispatch` has no production caller -// and a heal-specific input contract. The registration and deregistration legs are bound. Each unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR +// runner and observe it ready. The registration, dispatch and deregistration legs are bound. Each +// unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR // (section 4b(3)), never a pull request, because a pull request can merge while the capability stays // missing -- which is exactly what happened to the boot leg. @@ -225,14 +225,14 @@ data mtcollins1_image_leg: RouteLegStanding = LegBoundToAuthority { // listing, with the host-side teardown carried by gunbc.runner.runner_teardown_receipt // RunnerTeardownRequest over a host-unit inventory. Both are pre-approved under the dispatching // run's authorization (operator ruling 2026-10-03), rowed in gunbc.auth.privileged_effect_census. -// Dispatch names the cited REST operation that has no production caller yet. +// Dispatch is gunbc.runner.runner_qualification_dispatch dispatch_qualification_floor: CreateDispatch +// with the attempt label as the workflow input, under the same run's authorization. data mtcollins1_registration_leg: RouteLegStanding = LegBoundToAuthority { authority: decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), } -data mtcollins1_dispatch_leg: RouteLegStanding = LegAwaitingAuthority { - lands_with: "a production fold calling extdeps.github.workflows github.Workflows CreateDispatch for the qualification workflow with the attempt label as its input -- which also dissolves that module's create_dispatch_unconsumed_frontier_rows" as NonEmptyStr, - sufficient_for: "dispatching the floor to the one registration whose runs-on names the attempt label, so the run is unservicable by any other runner" as NonEmptyStr, +data mtcollins1_dispatch_leg: RouteLegStanding = LegBoundToAuthority { + authority: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), } data mtcollins1_deregistration_leg: RouteLegStanding = LegBoundToAuthority { diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 9ff8df0845f..1f4497659d0 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -389,12 +389,13 @@ test fn every_root_of_the_standing_ruling_is_a_conforming_census_row() -> Bool { && any(privileged_effect_interlocks, i => declaration_ref_display_key(ref: i.site) == "gunbc.host_reset_return_run::host_reset_return_wet") } -// THE 2026-10-03 PRE-APPROVAL IS A SELECTION, NOT A NEW VOCABULARY. JIT deregistration is a +// THE 2026-10-03 PRE-APPROVAL IS A SELECTION, NOT A NEW VOCABULARY. JIT registration, +// deregistration and the qualification dispatch are each a // recurring automated effect under the dispatching run's federated identity, so the selection must // answer the federated scoped grant and the row must conform; rowing it as an approval capability or // a per-registration ntfy approval turns this red. -test fn the_jit_registration_and_deregistration_sites_select_the_federated_grant_and_conform() -> Bool { - ["dispatch_jit_mint", "ensure_jit_runner_deregistered"] |> all(name => +test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federated_grant_and_conform() -> Bool { + ["dispatch_jit_mint", "ensure_jit_runner_deregistered", "dispatch_qualification_floor"] |> all(name => any(census_verdicts(), v => (v.site.decl_name as String) == name && selected_federated(s: v.selection) diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag new file mode 100644 index 00000000000..8a3854c5dff --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -0,0 +1,150 @@ +module test.claim.runner_qualification_dispatch_witness + +import std.types { Bool, Int, List, NonEmptyStr, String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.transports.rest { RestOk, RestStatusRefused, RestTransportRefused } +import extdeps.github.workflow_runs { + WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, +} +import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, qualification_instruments } +import gunbc.fleet_intent_network { operator_host_mtcollins1 } +import gunbc.runner.runner_qualification_dispatch { + qualification_attempt_dispatch_input, qualification_dispatch_inputs, + QualificationRunCollection, QualificationRunCollected, QualificationRunRefused, + RunReadUnreadable, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, + RunJobsTruncated, RunNeverReachedAttemptRunner, + conclude_qualification_run, qualification_run_collected, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data attempt: NonEmptyStr = "q-20261004-1" + +// THE DISPATCH KEY IS THE DECLARED INPUT'S NAME AND ITS VALUE IS THE ATTEMPT -- the same label the +// slot registers under (ephemeral_slot_labels), so runs-on fed from this input selects exactly that +// registration. Renaming the input row, or sending any other value, turns this red. +test fn the_dispatch_carries_the_attempt_label_under_the_declared_input() -> Bool { + let inputs = qualification_dispatch_inputs(attempt: attempt) + count(map_keys(inputs)) == 1 + && match map_get(inputs, qualification_attempt_dispatch_input.name) { + Present { value: v } => v == (attempt as String) + && (ephemeral_slot_labels(host: operator_host_mtcollins1, attempt: attempt) |> any(l => l == v)) + Absent => false + } + && qualification_attempt_dispatch_input.required +} + +fn run(status_completed: Bool, concluded: Bool) -> WorkflowRun { + WorkflowRun { + id: 77, + name: none, + path: none, + head_sha: "abc123", + head_branch: none, + event: "workflow_dispatch", + status: if status_completed { Completed } else { InProgress }, + conclusion: if concluded { Present { value: Failure } } else { none }, + workflow_id: 9, + run_number: 1, + run_attempt: 1, + actor: none, + triggering_actor: none, + html_url: "https://github.com/gunb-ai/gunbc/actions/runs/77", + created_at: "2026-10-04T00:00:00Z", + updated_at: "2026-10-04T01:00:00Z", + pull_requests: [], + } +} + +fn job(labels: List, started: Bool) -> WorkflowJobRun { + WorkflowJobRun { + id: 500, + run_id: 77, + run_attempt: 1, + name: "floor", + status: Completed, + conclusion: Present { value: Success }, + labels: labels, + created_at: Present { value: "2026-10-04T00:00:10Z" }, + started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, + completed_at: Present { value: "2026-10-04T00:50:00Z" }, + } +} + +fn jobs(js: List) -> WorkflowJobRunList { + WorkflowJobRunList { total_count: count(js), jobs: js } +} + +fn conclude(run_completed: Bool, concluded: Bool, js: WorkflowJobRunList) -> QualificationRunCollection { + conclude_qualification_run( + attempt: attempt, run_id: 77, + run_outcome: RestOk, run: run(status_completed: run_completed, concluded: concluded), + jobs_outcome: RestOk, jobs: js, + ) +} + +// THE POSITIVE CONTROL: a completed run whose job was taken under the attempt label is collected, +// its conclusion is the one read back (a FAILED floor is still collected and says so), and every +// qualification_instrument is carried as unextracted -- the declared extraction frontier, not a +// silent empty reading. +test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion() -> Bool { + match conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { + QualificationRunCollected { attempt: a, run_id: r, conclusion: c, attempt_jobs: served, instruments_unextracted: u } => + a == attempt && r == 77 && count(served) == 1 + && (match c { Failure => true _ => false }) + && count(u) == count(qualification_instruments) + QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false + } +} + +// THE BRIEF'S REFUSAL: the run completed but no job carried the attempt label, or the one that did +// never started (no runner under the attempt took it). Both refuse, and carry the labels observed. +test fn a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw() -> Bool { + let other = conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", "srv1"], started: true)])) + let unstarted = conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: [attempt as String], started: false)])) + (match other { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunNeverReachedAttemptRunner { job_labels: ls } } => + count(ls) == 1 && (ls |> all(l => l |> any(x => x == "srv1"))) + _ => false + }) + && !qualification_run_collected(c: unstarted) + && (match unstarted { QualificationRunRefused { attempt: _, run_id: _, refusal: RunNeverReachedAttemptRunner { job_labels: _ } } => true _ => false }) +} + +test fn an_unfinished_or_unconcluded_run_refuses_rather_than_collecting() -> Bool { + let served = jobs(js: [job(labels: [attempt as String], started: true)]) + (match conclude(run_completed: false, concluded: false, js: served) { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunNotCompleted { status: InProgress } } => true + _ => false + }) + && (match conclude(run_completed: true, concluded: false, js: served) { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunCompletedWithoutConclusion } => true + _ => false + }) +} + +// A PAGE SHORTER THAN total_count IS NOT A JOB LISTING: the attempt's job could sit on the unread +// page, so the fold refuses rather than reporting the runner never reached. +test fn a_truncated_or_unreadable_readback_refuses() -> Bool { + let truncated = WorkflowJobRunList { total_count: 2, jobs: [job(labels: [attempt as String], started: true)] } + (match conclude(run_completed: true, concluded: true, js: truncated) { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunJobsTruncated { returned: 1, total: 2 } } => true + _ => false + }) + && (match conclude_qualification_run( + attempt: attempt, run_id: 77, + run_outcome: RestStatusRefused { status: 404, body: "Not Found" }, run: run(status_completed: true, concluded: true), + jobs_outcome: RestOk, jobs: jobs(js: []), + ) { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunReadUnreadable { outcome: _ } } => true + _ => false + }) + && (match conclude_qualification_run( + attempt: attempt, run_id: 77, + run_outcome: RestOk, run: run(status_completed: true, concluded: true), + jobs_outcome: RestTransportRefused { cause: "unreached" }, jobs: jobs(js: []), + ) { + QualificationRunRefused { attempt: _, run_id: _, refusal: RunJobsUnreadable { outcome: _ } } => true + _ => false + }) +} diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index e20d3e49c61..1701d8a8172 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -747,8 +747,8 @@ test fn the_route_classifies_every_mutation_at_its_grain() -> Bool { )) } -// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the boot and dispatch legs are still -// declared frontiers. This flips to the regression control that the bindings stay when they land. +// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the boot leg is still a declared +// frontier. This flips to the regression control that the bindings stay when they land. test fn the_route_is_not_executable_while_any_effectful_leg_is_unbound() -> Bool { !route_is_executable(route: route()) && !leg_is_bound(leg: qualification_run_producer_frontier) } @@ -759,10 +759,10 @@ test fn the_image_leg_is_bound_and_the_boot_leg_awaits_a_runner_medium() -> Bool leg_is_bound(leg: mtcollins1_route_legs.image) && !leg_is_bound(leg: mtcollins1_route_legs.boot) } -// REGISTRATION AND DEREGISTRATION ARE BOUND, AND TO THE AUTHORITIES THAT PERFORM THEM -- by -// declaration identity, so pointing either leg at some other declaration turns this red -- while the -// route stays unexecutable: boot and dispatch are still awaiting authority, and the claim above -// keeps route_is_executable false until they bind. +// REGISTRATION, DISPATCH AND DEREGISTRATION ARE BOUND, AND TO THE AUTHORITIES THAT PERFORM THEM -- +// by declaration identity, so pointing any of them at some other declaration turns this red -- while +// the route stays unexecutable: the boot leg is still awaiting authority, and the claim above keeps +// route_is_executable false until it binds. fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> Bool { match leg { LegBoundToAuthority { authority: a } => (a.module_path as String) == module_path && (a.decl_name as String) == decl_name @@ -770,11 +770,11 @@ fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> B } } -test fn the_registration_and_deregistration_legs_are_bound_and_the_route_still_is_not() -> Bool { +test fn the_registration_dispatch_and_deregistration_legs_are_bound_and_the_route_still_is_not() -> Bool { leg_names(leg: mtcollins1_route_legs.registration, module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint") + && leg_names(leg: mtcollins1_route_legs.dispatch, module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor") && leg_names(leg: mtcollins1_route_legs.deregistration, module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered") && !leg_is_bound(leg: mtcollins1_route_legs.boot) - && !leg_is_bound(leg: mtcollins1_route_legs.dispatch) && !route_is_executable(route: route()) } From f33d26589c7005b08fc5bf6748b9ee642e14c772 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 04:37:21 +0000 Subject: [PATCH 03/44] leg 3: seal the dispatched run and the collected arm; check the readback's subject is the dispatched run Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 99 +++++++++++++------ ...er_qualification_dispatch_witness_test.dag | 29 +++++- 2 files changed, 95 insertions(+), 33 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 970b47f2f9b..a61614768a8 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -56,8 +56,21 @@ fn qualification_dispatch_inputs(attempt: NonEmptyStr) -> Map { // A DISPATCH IS ONE ATTEMPT AND ITS ANSWER IS THE RECEIPT OR THE REFUSAL, never a run assumed to // exist. The run id comes from GitHub's 2026-03-10 response (WorkflowDispatchReceipt); a refused or // unreached request carries its RestOutcome and mints no run. +// +// THE DISPATCHED RUN IS SEALED, AND THAT IS WHAT PUTS COLLECTION CAUSALLY AFTER DISPATCH. +// WorkflowDispatchReceipt is a plain upstream record anyone can write, so a collect fold taking one +// would read back whatever run id a caller named. DispatchedQualificationRun is sole_constructor: +// only this file can write it, and in this file only dispatch_qualification_floor does, from a +// RestOk answer. collect_qualification_run takes that value, so no run is collected that this +// module did not dispatch, for this target and this attempt. +type DispatchedQualificationRun sole_constructor { + target: QualificationWorkflowTarget + attempt: NonEmptyStr + receipt: WorkflowDispatchReceipt +} + type QualificationDispatch - = QualificationDispatched { attempt: NonEmptyStr, receipt: WorkflowDispatchReceipt } + = QualificationDispatched { run: DispatchedQualificationRun } | QualificationDispatchRefused { attempt: NonEmptyStr, outcome: RestOutcome } fn dispatch_qualification_floor(target: QualificationWorkflowTarget, attempt: NonEmptyStr) -> QualificationDispatch uses net: Network { @@ -71,11 +84,14 @@ fn dispatch_qualification_floor(target: QualificationWorkflowTarget, attempt: No match answered.outcome { RestOk => QualificationDispatched { - attempt: attempt, - receipt: WorkflowDispatchReceipt { - workflow_run_id: answered.workflow_run_id, - run_url: answered.run_url, - html_url: answered.html_url, + run: DispatchedQualificationRun { + target: target, + attempt: attempt, + receipt: WorkflowDispatchReceipt { + workflow_run_id: answered.workflow_run_id, + run_url: answered.run_url, + html_url: answered.html_url, + }, }, } other => QualificationDispatchRefused { attempt: attempt, outcome: other } @@ -84,6 +100,10 @@ fn dispatch_qualification_floor(target: QualificationWorkflowTarget, attempt: No // ── COLLECT: THE RUN READ BACK, ENSURE-STYLE ────────────────────────────────────────────────── // +// THE SUBJECT IS THE DISPATCHED RUN, CHECKED ON THE READBACK: the run read must carry the id the +// dispatch returned and the workflow_dispatch event, and every job listed must belong to it; +// anything else is RunSubjectMismatch, never a collection of some other run. +// // A DISPATCH RECEIPT SAYS A RUN EXISTS, NOT THAT THE ATTEMPT'S RUNNER SERVED IT. The collection // therefore decides only from what GitHub reports about the run now: the run must be Completed with // a conclusion, and at least one of its jobs must carry the attempt label and have started -- that @@ -101,17 +121,25 @@ type QualificationRunRefusal | RunJobsUnreadable { outcome: RestOutcome } | RunJobsTruncated { returned: Int, total: Int } | RunNeverReachedAttemptRunner { job_labels: List> } + | RunSubjectMismatch { observed_run_id: Int, observed_event: String, foreign_job_run_ids: List } // INSTRUMENT EXTRACTION IS A DECLARED FRONTIER, carried on the collected arm as the instruments the // run was read for and not yet extracted from (qualification_instrument_extraction_frontier_rows). +// +// THE SUCCESS PAYLOAD IS SEALED: CollectedQualificationRun is sole_constructor, written only by +// conclude_qualification_run, whose callers are admitted by name -- the one production reader, +// collect_qualification_run, and the witness declarations that supply readbacks at that interface. +// A module elsewhere cannot mint "this host ran the floor" without a readback. +type CollectedQualificationRun sole_constructor { + attempt: NonEmptyStr + run_id: Int + conclusion: WorkflowRunConclusion + attempt_jobs: List + instruments_unextracted: List +} + type QualificationRunCollection - = QualificationRunCollected { - attempt: NonEmptyStr - run_id: Int - conclusion: WorkflowRunConclusion - attempt_jobs: List - instruments_unextracted: List - } + = QualificationRunCollected { collected: CollectedQualificationRun } | QualificationRunRefused { attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal } fn job_served_by_attempt(job: WorkflowJobRun, attempt: NonEmptyStr) -> Bool { @@ -130,9 +158,19 @@ fn conclude_qualification_run( run: WorkflowRun, jobs_outcome: RestOutcome, jobs: WorkflowJobRunList, -) -> QualificationRunCollection { +) -> QualificationRunCollection + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_run"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "conclude"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_truncated_or_unreadable_readback_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_readback_of_another_run_refuses_on_the_subject"), + ] +{ match run_outcome { RestOk => + if run.id != run_id || run.event != "workflow_dispatch" { + refuse_run(attempt: attempt, run_id: run_id, refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, foreign_job_run_ids: [] }) + } else { match run.status { Completed => match run.conclusion { @@ -143,46 +181,51 @@ fn conclude_qualification_run( if count(jobs.jobs) != jobs.total_count { refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsTruncated { returned: count(jobs.jobs), total: jobs.total_count }) } else { + let foreign = jobs.jobs |> filter(j => j.run_id != run_id) |> map(j => j.run_id) + if count(foreign) != 0 { + refuse_run(attempt: attempt, run_id: run_id, refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, foreign_job_run_ids: foreign }) + } else { let served = jobs.jobs |> filter(j => job_served_by_attempt(job: j, attempt: attempt)) if count(served) == 0 { refuse_run(attempt: attempt, run_id: run_id, refusal: RunNeverReachedAttemptRunner { job_labels: jobs.jobs |> map(j => j.labels) }) } else { QualificationRunCollected { - attempt: attempt, - run_id: run_id, - conclusion: conclusion, - attempt_jobs: served, - instruments_unextracted: qualification_instruments, + collected: CollectedQualificationRun { + attempt: attempt, + run_id: run_id, + conclusion: conclusion, + attempt_jobs: served, + instruments_unextracted: qualification_instruments, + }, } } + } } other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsUnreadable { outcome: other }) } } other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunNotCompleted { status: other }) } + } other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunReadUnreadable { outcome: other }) } } fn qualification_run_collected(c: QualificationRunCollection) -> Bool { match c { - QualificationRunCollected { attempt: _, run_id: _, conclusion: _, attempt_jobs: _, instruments_unextracted: _ } => true + QualificationRunCollected { collected: _ } => true QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false } } -fn collect_qualification_run( - target: QualificationWorkflowTarget, - dispatched: WorkflowDispatchReceipt, - attempt: NonEmptyStr, -) -> QualificationRunCollection uses net: Network { - let run_id = dispatched.workflow_run_id as String +fn collect_qualification_run(dispatched: DispatchedQualificationRun) -> QualificationRunCollection uses net: Network { + let target = dispatched.target + let run_id = dispatched.receipt.workflow_run_id as String let read = github.WorkflowRuns.GetRun(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) let jobs = github.WorkflowRuns.ListJobs(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) conclude_qualification_run( - attempt: attempt, - run_id: dispatched.workflow_run_id, + attempt: dispatched.attempt, + run_id: dispatched.receipt.workflow_run_id, run_outcome: read.outcome, run: read.run, jobs_outcome: jobs.outcome, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 8a3854c5dff..638aae51eed 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -12,7 +12,7 @@ import gunbc.runner.runner_qualification_dispatch { qualification_attempt_dispatch_input, qualification_dispatch_inputs, QualificationRunCollection, QualificationRunCollected, QualificationRunRefused, RunReadUnreadable, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, - RunJobsTruncated, RunNeverReachedAttemptRunner, + RunJobsTruncated, RunNeverReachedAttemptRunner, RunSubjectMismatch, conclude_qualification_run, qualification_run_collected, } @@ -89,10 +89,10 @@ fn conclude(run_completed: Bool, concluded: Bool, js: WorkflowJobRunList) -> Qua // silent empty reading. test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion() -> Bool { match conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { - QualificationRunCollected { attempt: a, run_id: r, conclusion: c, attempt_jobs: served, instruments_unextracted: u } => - a == attempt && r == 77 && count(served) == 1 - && (match c { Failure => true _ => false }) - && count(u) == count(qualification_instruments) + QualificationRunCollected { collected: c } => + c.attempt == attempt && c.run_id == 77 && count(c.attempt_jobs) == 1 + && (match c.conclusion { Failure => true _ => false }) + && count(c.instruments_unextracted) == count(qualification_instruments) QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false } } @@ -148,3 +148,22 @@ test fn a_truncated_or_unreadable_readback_refuses() -> Bool { _ => false }) } + +// THE SUBJECT IS THE DISPATCHED RUN: a readback naming another run id, or listing a job of another +// run, refuses as RunSubjectMismatch even when everything else would collect -- the served job +// below carries the attempt label and started, so only the subject check can turn it red. +test fn a_readback_of_another_run_refuses_on_the_subject() -> Bool { + let served = jobs(js: [job(labels: [attempt as String], started: true)]) + let other_run = conclude_qualification_run( + attempt: attempt, run_id: 78, + run_outcome: RestOk, run: run(status_completed: true, concluded: true), + jobs_outcome: RestOk, jobs: served, + ) + let foreign_job = conclude(run_completed: true, concluded: true, js: jobs(js: [WorkflowJobRun { + id: 501, run_id: 99, run_attempt: 1, name: "floor", status: Completed, + conclusion: Present { value: Success }, labels: [attempt as String], + created_at: none, started_at: Present { value: "2026-10-04T00:01:00Z" }, completed_at: none, + }])) + (match other_run { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: 77, observed_event: _, foreign_job_run_ids: _ } } => true _ => false }) + && (match foreign_job { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: _, observed_event: _, foreign_job_run_ids: f } } => count(f) == 1 && (f |> all(x => x == 99)) _ => false }) +} From 303651b9b27d1295c969a9aae396686482f99ee0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 04:48:11 +0000 Subject: [PATCH 04/44] leg 3: admit only Bool-returning claims to the sealed conclude; delete the carrier-returning helper (constructor proxy) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 10 +++++++-- ...er_qualification_dispatch_witness_test.dag | 21 +++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index a61614768a8..f9a2c1c83ff 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -128,7 +128,11 @@ type QualificationRunRefusal // // THE SUCCESS PAYLOAD IS SEALED: CollectedQualificationRun is sole_constructor, written only by // conclude_qualification_run, whose callers are admitted by name -- the one production reader, -// collect_qualification_run, and the witness declarations that supply readbacks at that interface. +// collect_qualification_run, and the witness claims that supply readbacks at that interface. An +// admit list constrains the call edge, not where the result goes, so every admitted witness entry +// is a `test fn` returning Bool that consumes the collection internally; a helper returning the +// collection would be a public constructor proxy, and none is admitted. collect_qualification_run +// returns the collection, but only from a sealed DispatchedQualificationRun. // A module elsewhere cannot mint "this host ran the floor" without a readback. type CollectedQualificationRun sole_constructor { attempt: NonEmptyStr @@ -161,7 +165,9 @@ fn conclude_qualification_run( ) -> QualificationRunCollection admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_run"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "conclude"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_or_unconcluded_run_refuses_rather_than_collecting"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_truncated_or_unreadable_readback_refuses"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_readback_of_another_run_refuses_on_the_subject"), ] diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 638aae51eed..4744874c42b 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -75,20 +75,13 @@ fn jobs(js: List) -> WorkflowJobRunList { WorkflowJobRunList { total_count: count(js), jobs: js } } -fn conclude(run_completed: Bool, concluded: Bool, js: WorkflowJobRunList) -> QualificationRunCollection { - conclude_qualification_run( - attempt: attempt, run_id: 77, - run_outcome: RestOk, run: run(status_completed: run_completed, concluded: concluded), - jobs_outcome: RestOk, jobs: js, - ) -} // THE POSITIVE CONTROL: a completed run whose job was taken under the attempt label is collected, // its conclusion is the one read back (a FAILED floor is still collected and says so), and every // qualification_instrument is carried as unextracted -- the declared extraction frontier, not a // silent empty reading. test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion() -> Bool { - match conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { + match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { QualificationRunCollected { collected: c } => c.attempt == attempt && c.run_id == 77 && count(c.attempt_jobs) == 1 && (match c.conclusion { Failure => true _ => false }) @@ -100,8 +93,8 @@ test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_concl // THE BRIEF'S REFUSAL: the run completed but no job carried the attempt label, or the one that did // never started (no runner under the attempt took it). Both refuse, and carry the labels observed. test fn a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw() -> Bool { - let other = conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", "srv1"], started: true)])) - let unstarted = conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: [attempt as String], started: false)])) + let other = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: ["self-hosted", "srv1"], started: true)])) + let unstarted = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: [attempt as String], started: false)])) (match other { QualificationRunRefused { attempt: _, run_id: _, refusal: RunNeverReachedAttemptRunner { job_labels: ls } } => count(ls) == 1 && (ls |> all(l => l |> any(x => x == "srv1"))) @@ -113,11 +106,11 @@ test fn a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw() - test fn an_unfinished_or_unconcluded_run_refuses_rather_than_collecting() -> Bool { let served = jobs(js: [job(labels: [attempt as String], started: true)]) - (match conclude(run_completed: false, concluded: false, js: served) { + (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: false, concluded: false), jobs_outcome: RestOk, jobs: served) { QualificationRunRefused { attempt: _, run_id: _, refusal: RunNotCompleted { status: InProgress } } => true _ => false }) - && (match conclude(run_completed: true, concluded: false, js: served) { + && (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: false), jobs_outcome: RestOk, jobs: served) { QualificationRunRefused { attempt: _, run_id: _, refusal: RunCompletedWithoutConclusion } => true _ => false }) @@ -127,7 +120,7 @@ test fn an_unfinished_or_unconcluded_run_refuses_rather_than_collecting() -> Boo // page, so the fold refuses rather than reporting the runner never reached. test fn a_truncated_or_unreadable_readback_refuses() -> Bool { let truncated = WorkflowJobRunList { total_count: 2, jobs: [job(labels: [attempt as String], started: true)] } - (match conclude(run_completed: true, concluded: true, js: truncated) { + (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: truncated) { QualificationRunRefused { attempt: _, run_id: _, refusal: RunJobsTruncated { returned: 1, total: 2 } } => true _ => false }) @@ -159,7 +152,7 @@ test fn a_readback_of_another_run_refuses_on_the_subject() -> Bool { run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served, ) - let foreign_job = conclude(run_completed: true, concluded: true, js: jobs(js: [WorkflowJobRun { + let foreign_job = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [WorkflowJobRun { id: 501, run_id: 99, run_attempt: 1, name: "floor", status: Completed, conclusion: Present { value: Success }, labels: [attempt as String], created_at: none, started_at: Present { value: "2026-10-04T00:01:00Z" }, completed_at: none, From 4e8adcadedbc66c0b8de98f4b72437ecf2a7ed41 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 04:55:05 +0000 Subject: [PATCH 05/44] mtcollins1 runner: extract qualification instruments from the run's job log; claim-cost read refuses typed (leg 3b) Model the Actions artifacts (List workflow run artifacts, Download an artifact) and job-logs (Download job logs for a workflow run) REST operations in extdeps.github with upstream names; the 302 is followed by the bound REST handler. Parse [floor-phase] rows in gunbc.required_ci_phase_roster and [floor-cgroup] level rows in gunbc.host_budget_source, decoding values through extdeps.linux.cgroup_v2_memory. The collect stage reads each served job's log into typed readings or a typed refusal and attempts the claim-cost artifact read, carrying its typed refusal. The single extraction frontier row becomes three rows, one per missing capability. Supplied-input witnesses are paired with an inhabitance claim over a recorded required-floor log's verbatim rows. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/actions_artifacts.dag | 124 ++++++++ dag/extdeps/github/workflow_runs.dag | 37 +++ dag/gunbc/host/host_budget_source.dag | 71 +++++ dag/gunbc/required_ci_phase_roster.dag | 97 +++++- .../runner/runner_qualification_dispatch.dag | 297 +++++++++++++++++- ...er_qualification_dispatch_witness_test.dag | 183 ++++++++++- .../recorded_required_floor_log_excerpt.dag | 22 ++ 7 files changed, 810 insertions(+), 21 deletions(-) create mode 100644 dag/extdeps/github/actions_artifacts.dag create mode 100644 dag/test/fixture/recorded_required_floor_log_excerpt.dag diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag new file mode 100644 index 00000000000..bcfa8e607df --- /dev/null +++ b/dag/extdeps/github/actions_artifacts.dag @@ -0,0 +1,124 @@ +module extdeps.github.actions_artifacts + +import extdeps.github.github { default_api_base, max_per_page } +import extdeps.github.errors { GitHubErrorShape } +import extdeps.transports.rest { RestOutcome } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import std.credentials { EnvVar } +import std.types { Bool, Bytes, Int, List, String, Timestamp } +import std.resources { Network } + +// THE GITHUB ACTIONS ARTIFACTS REST API, as the upstream names it: "List workflow run artifacts" +// (operationId actions/list-workflow-run-artifacts) and "Download an artifact" +// (actions/download-artifact), docs.github.com/en/rest/actions/artifacts, read 2026-10-04. +// +// THE DOWNLOAD IS A REDIRECT TO A ZIP, AND BOTH HALVES ARE UPSTREAM FACTS. GitHub answers +// `GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}` with `302 Found` and a +// `Location` header naming a short-lived storage URL (about one minute), and `410 Gone` once the +// artifact has expired; `archive_format` admits exactly `zip`, and the body at the Location is the +// artifact as a zip archive -- upload-artifact always archives, even a single file. Following the +// redirect is the bound REST realization's work, not this interface's: the seed handler +// (v1_interpreter observe_rest_exchange, over ureq's redirect policy) follows it and decides on the +// final response, so the response block below names the answer at the end of the redirect. +// +// THE ARCHIVE IS Bytes AND NO BOUND REALIZATION CAN YET CARRY IT. The REST handler reads every body +// as UTF-8 text (RestBodyObservation carries body: String), so a zip arrives as a body that could not +// be read and the call answers RestBodyUndecodable with the status it came under. That refusal is the +// honest answer today and it is typed; reading a member out of the archive needs a binary body +// carrier on the REST transport and an inflate reader, which is +// the trigger of gunbc.runner.runner_qualification_dispatch qualification_instrument_extraction_frontier_rows' +// parse_claim_cost_tsv row. Its production caller is that module's read_claim_cost_artifact. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "docs.github.com/en/rest/actions/artifacts" + } +} + +// The fields a reader of a run's artifacts asks of one: which artifact (id, name), how big, and +// whether it can still be downloaded. `expired` is upstream's own flag; a download of an expired +// artifact is answered 410, and a reader that checks the flag first refuses without the round trip. +type ActionsArtifact { + id: Int + name: String + size_in_bytes: Int + archive_download_url: String + expired: Bool + created_at: Timestamp? + expires_at: Timestamp? +} + +type ActionsArtifactList { + total_count: Int + artifacts: List +} + +// `archive_format` IS A PATH SEGMENT WHOSE UPSTREAM VOCABULARY HAS ONE MEMBER, `zip`, so the path +// carries it as the literal upstream spelling rather than as an input. An input would make every +// other spelling representable, and a coproduct input interpolated into a path is rendered by the +// seed with its AUTHORED variant name (the regression extdeps.github.workflow_runs +// WorkflowJobAttemptFilter records), so neither would be more faithful than the literal. +service github.Artifacts { + config { + endpoint: default_api_base + auth: Bearer + auth_source: EnvVar { name: "GITHUB_TOKEN" } + } + + operation ListWorkflowRunArtifacts { + requires Network + input { + auth_token: Secret + owner: String + repo: String + run_id: String + per_page: Int = max_per_page + } + output { + result: ActionsArtifactList + outcome: RestOutcome + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/runs/\{run_id\}/artifacts", + query: { per_page: per_page } + } + response { + 200 => ActionsArtifactList + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 500 => GitHubErrorShape + } + } + + operation DownloadArtifact { + requires Network + input { + auth_token: Secret + owner: String + repo: String + artifact_id: String + } + output { + archive: Bytes + outcome: RestOutcome + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/artifacts/\{artifact_id\}/zip", + response_format: "Text" + } + response { + 200 => Bytes + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 410 => GitHubErrorShape + 500 => GitHubErrorShape + } + } +} diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 98df2230815..4f821e2eaf3 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -287,6 +287,15 @@ data structural_coverage_gap_workflow_run_codec_hand_rolled: List = [ // operation is not a module item, so per-operation rationale is not expressible; it belongs here // or nowhere. // +// DOWNLOADJOBLOGSFORWORKFLOWRUN IS UPSTREAM'S "Download job logs for a workflow run" +// (actions/download-job-logs-for-workflow-run, docs.github.com/en/rest/actions/workflow-jobs, read +// 2026-10-04): `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs` answers `302 Found` with a +// `Location` naming a storage URL that expires after about a minute, and the body there is the job's +// log as plain text, one line per row behind an RFC 3339 timestamp. The redirect is followed by the +// bound REST realization (the seed's ureq handler), so the response block names the text at the end +// of it. A body the handler cannot read -- not UTF-8, or past its read cap -- is RestBodyUndecodable +// with its status, never a truncated log reported as the whole one. +// // THE FILTER IS A CALLER'S CHOICE AND WAS A LITERAL. "latest" returns only the most recent attempt // of each job, which is the right answer for "what is the current state" and the wrong one for any // census of consumed capacity: a re-run mutates a job's record in place, so the attempts "latest" @@ -417,6 +426,34 @@ service github.WorkflowRuns { 500 => GitHubErrorShape } } + operation DownloadJobLogsForWorkflowRun { + requires Network + input { + auth_token: Secret + owner: String + repo: String + job_id: String + } + output { + log: String + outcome: RestOutcome + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/jobs/\{job_id\}/logs", + response_format: "Text" + } + response { + 200 => String + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 410 => GitHubErrorShape + 500 => GitHubErrorShape + } + } + operation ListJobs { requires Network input { diff --git a/dag/gunbc/host/host_budget_source.dag b/dag/gunbc/host/host_budget_source.dag index 2cb18c62a96..83e78ac966a 100644 --- a/dag/gunbc/host/host_budget_source.dag +++ b/dag/gunbc/host/host_budget_source.dag @@ -4,6 +4,11 @@ import std.types { NonEmptyStr, String, Bool, List } import std.measure { ByteSize, byte_size_count } import std.os.types { KernelFamily, Linux, Darwin, WindowsNt, LinuxGuestOnWindows } import extdeps.linux.cgroup_v2 { kernel_provides_cgroup_v2 } +import extdeps.linux.cgroup_v2_memory { + CgroupMemoryLimitValue, CgroupMemoryLimited, CgroupMemoryUnlimited, CgroupMemoryLimitUnparseable, + CgroupMemoryCount, CgroupMemoryCountUnparseable, parse_cgroup_memory_limit, parse_cgroup_memory_count, +} +import gunbc.required_ci_phase_roster { floor_log_row_fields, floor_log_field_value } import std.disposition { Disposition, Terminal, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.linux.procfs { @@ -353,3 +358,69 @@ data host_budget_source_seed_mirror_disposition: Disposition = Scaffold { data host_budget_source_seed_mirror_note: String = "THIS MODEL IS THE AUTHORITY AND v1 STILL HAND-REALIZES IT, which is a mirror of exactly the kind that just went stale elsewhere in this change — so it is declared rather than left implicit. memory_governor::read_host_budget_bytes composes its own labels and cli_run::typed_module_cache_cap_derivation computes its own degraded flag, and neither reads these rows.\n\nWhy the mirror cannot simply be deleted today: one consumer runs strictly before any .dag value could exist. cli_run's enforce_typed_cache_entry_cap derives the typed-module-cache eviction bound from this same budget WHILE MODULES ARE BEING RESOLVED — it is the thing bounding the memory used to resolve the corpus, so it cannot be a value obtained by resolving the corpus. The governor's own arm in claim_executor is different and could in principle read a resolved value (it happens after the plan resolve), so that half is ordinary seed-shrink debt rather than a bootstrap constraint. The two must not be conflated when the emit lands: the first needs the value baked into the seed, the second only needs the seed to stop duplicating it.\n\nTHAT CHECK NOW EXISTS BUT IS NOT RUN BY CI, and the distinction is the whole point of this row: test.claim.seed_mirror_constant_lens_witness_test reads the seed's constants against their authority rows and covers the union of what this note and gunbc.typed_module_cache_capacity's seed_mirror_note each asked for separately — DECLARED_RUNNER_SLOT_MEMORY_HIGH_BYTES, DECLARED_FLOOR_MINIMUM_VIABLE_ARMED_BUDGET_BYTES, and the three typed-module-cache constants. Two notes naming different overlapping subsets, neither of which was the union, was itself two partial specifications of one mechanism. THIS NOTE'S OWN WARNING CAME TRUE AND IS WORTH KEEPING FOR IT. It cited DECLARED_RUNNER_SLOT_MEMORY_HIGH_BYTES as having drifted once already; it then drifted again and sat eleven days at 13958643712 against an authority of 16106127360 (gunbc.runner_slot_desired gunbc_runner_slot_desired, moved by #8388 on 2026-08-17). Knowing about a gap and naming its fix is not the same as closing it, and the interval is the receipt. Note also that the sibling witness could not have caught it: test.claim.host_budget_source_witness built its expectation from the mirror value retyped, so it greened whichever way the mirror went — a second inert-by-construction surface, since corrected to derive from the row." + +// ── THE FLOOR'S [floor-cgroup] LEVEL ROWS, READ BACK FROM A RUN'S LOG ────────────────────────── +// +// The floor prints, at entry and on every heartbeat, one `[floor-cgroup] when= level=

` +// row per cgroup level from its own cgroup upward, carrying that level's memory.max, memory.high, +// memory.current and memory.peak as the kernel spelled them (plus events and pressure, which this +// reading does not take). The budget this module resolves IS the memory.high of the slot level, so +// the bound and the peak a qualification reads are denominated in this module's vocabulary -- which is +// why gunbc.runner_throughput_qualification_route names HostBudgetResolution as their producer. The +// values are decoded by extdeps.linux.cgroup_v2_memory, the kernel interface's own authority, never +// re-parsed here: `max` is CgroupMemoryUnlimited, never a large number. +// +// The `when= path=` row the floor prints beside them is a different row (the process's +// own cgroup path, no readings) and is not a level row: it reads as none, like any other line. +type FloorCgroupLevelRow { + when: NonEmptyStr + level: NonEmptyStr + high: CgroupMemoryLimitValue + peak: ByteSize +} + +type FloorCgroupRowReading + = FloorCgroupLevelRead { row: FloorCgroupLevelRow } + | FloorCgroupRowGarbled { line: String, cause: NonEmptyStr } + +data floor_cgroup_row_tag: String = "[floor-cgroup]" + +fn floor_cgroup_garbled(line: String, cause: String) -> FloorCgroupRowReading? { + Present { value: FloorCgroupRowGarbled { line: line, cause: cause as NonEmptyStr } } +} + +fn read_floor_cgroup_level_row(line: String) -> FloorCgroupRowReading? { + match floor_log_row_fields(line: line, tag: floor_cgroup_row_tag) { + Absent => none + Present { value: fields } => + match floor_log_field_value(fields: fields, key: "level") { + Absent => none + Present { value: level } => + match floor_log_field_value(fields: fields, key: "when") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no when= field") + Present { value: when } => + if when == "" || level == "" { + floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries an empty when= or level= field") + } else { + match floor_log_field_value(fields: fields, key: "high") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no high= field") + Present { value: raw_high } => + match parse_cgroup_memory_limit(body: raw_high) { + CgroupMemoryLimitUnparseable { body: _ } => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] high= is neither a byte count nor max") + high => + match floor_log_field_value(fields: fields, key: "peak") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no peak= field") + Present { value: raw_peak } => + match parse_cgroup_memory_count(body: raw_peak) { + CgroupMemoryCountUnparseable { body: _ } => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] peak= is not a byte count") + CgroupMemoryCount { bytes: peak } => + Present { value: FloorCgroupLevelRead { row: FloorCgroupLevelRow { when: when as NonEmptyStr, level: level as NonEmptyStr, high: high, peak: peak } } } + } + } + } + } + } + } + } + } +} diff --git a/dag/gunbc/required_ci_phase_roster.dag b/dag/gunbc/required_ci_phase_roster.dag index 8115d058ac4..7e3b9246e22 100644 --- a/dag/gunbc/required_ci_phase_roster.dag +++ b/dag/gunbc/required_ci_phase_roster.dag @@ -1,6 +1,7 @@ module gunbc.required_ci_phase_roster -import std.types { String, Bool, List } +import std.types { String, Bool, Int, List, NonEmptyStr } +import v2.std.optional { Present, Absent } // THE SUBSTRATE AUTHORITY FOR WHICH PHASES REQUIRED CI RUNS, AND WHICH LANE OWNS EACH. // gunbc.required_ci_host_verdict_census recorded the gap this module closes: "Phase existence and @@ -192,3 +193,97 @@ fn required_ci_lane_phase_rows() -> List { phase: required_ci_phase_name(p: p) }) } + +// ── THE FLOOR PHASE'S OWN PROGRESS ROWS, READ BACK FROM A RUN'S LOG ───────────────────────────── +// +// FloorPhase is one RequiredCiPhase; inside it the host prints `[floor-phase] phase= ...` rows +// for its own preparation stages (claim_executor, cli_run). Those stage WORDS are not this roster's +// vocabulary -- they are an open host vocabulary with no substrate authority, so a reading carries +// the word as the host spelled it and never maps it onto RequiredCiPhase. What this module owns is +// the reading's denominator: these rows are the timings OF required_ci_phases' FloorPhase, which is +// why gunbc.runner_throughput_qualification_route names this module as their producer. +// +// THE LINE ENCODING IS `[tag] key=value key=value`, one row per line, possibly behind the timestamp +// GitHub prefixes to each job-log line. floor_log_row_fields is the one reader of that encoding; the +// [floor-cgroup] reader (gunbc.host_budget_source) uses it rather than tokenizing a second time. A +// value may itself contain `=` (the cgroup pressure field does), so only the FIRST `=` splits. +type FloorLogField { + key: String + value: String +} + +fn floor_log_field(token: String) -> FloorLogField? { + let parts = split(s: token, delimiter: "=") + match parts.first() { + Absent => none + Present { value: k } => + if count(parts) < 2 { + none + } else { + let v = substring(s: token, start: string_length(s: k) + 1, end: string_length(s: token)) + Present { value: FloorLogField { key: k, value: v } } + } + } +} + +// none: the line is not a row of this tag. A row of the tag yields its fields, possibly empty. +fn floor_log_row_fields(line: String, tag: String) -> List? { + let halves = split(s: line, delimiter: concat(tag, " ")) + match halves.skip(n: 1).first() { + Absent => none + Present { value: payload } => + if count(halves) != 2 { + none + } else { + let tokens = split(s: payload, delimiter: " ") + let fields = tokens |> flat_map(t => match floor_log_field(token: t) { Absent => [] Present { value: f } => [f] }) + Present { value: fields } + } + } +} + +fn floor_log_field_value(fields: List, key: String) -> String? { + match fields |> filter(f => f.key == key) |> first { + Absent => none + Present { value: f } => Present { value: f.value } + } +} + +data floor_phase_row_tag: String = "[floor-phase]" + +// A STAGE ROW EITHER CARRIES ITS WALL OR DOES NOT, and the two are not one row with a zero. The host +// prints `state=started` rows and classification rows with no wall_ms; reporting those as 0 ms would +// fabricate a timing. A row with no phase word, or a wall_ms that is not a count, is garbled and is +// carried with its line, never skipped. +type FloorPhaseRow + = FloorPhaseTimed { phase: NonEmptyStr, wall_ms: Int } + | FloorPhaseUntimed { phase: NonEmptyStr } + +type FloorPhaseRowReading + = FloorPhaseRowRead { row: FloorPhaseRow } + | FloorPhaseRowGarbled { line: String, cause: NonEmptyStr } + +fn read_floor_phase_row(line: String) -> FloorPhaseRowReading? { + match floor_log_row_fields(line: line, tag: floor_phase_row_tag) { + Absent => none + Present { value: fields } => + match floor_log_field_value(fields: fields, key: "phase") { + Absent => Present { value: FloorPhaseRowGarbled { line: line, cause: "[floor-phase] row carries no phase= field" as NonEmptyStr } } + Present { value: "" } => Present { value: FloorPhaseRowGarbled { line: line, cause: "[floor-phase] row carries an empty phase= field" as NonEmptyStr } } + Present { value: phase } => + match floor_log_field_value(fields: fields, key: "wall_ms") { + Absent => Present { value: FloorPhaseRowRead { row: FloorPhaseUntimed { phase: phase as NonEmptyStr } } } + Present { value: raw } => + match parse_int(s: raw) { + Absent => Present { value: FloorPhaseRowGarbled { line: line, cause: "[floor-phase] wall_ms is not a count" as NonEmptyStr } } + Present { value: ms } => + if ms < 0 { + Present { value: FloorPhaseRowGarbled { line: line, cause: "[floor-phase] wall_ms is negative" as NonEmptyStr } } + } else { + Present { value: FloorPhaseRowRead { row: FloorPhaseTimed { phase: phase as NonEmptyStr, wall_ms: ms } } } + } + } + } + } + } +} diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 970b47f2f9b..656e4863781 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -11,7 +11,21 @@ import extdeps.github.workflows { WorkflowDispatchReceipt } import extdeps.github.workflow_runs { WorkflowRun, WorkflowRunStatus, Completed, WorkflowRunConclusion, WorkflowJobRun, WorkflowJobRunList, } -import gunbc.runner_throughput_qualification_route { QualificationInstrument, qualification_instruments } +import gunbc.runner_throughput_qualification_route { + QualificationInstrument, qualification_instruments, + FloorPhaseRows, FloorCgroupRows, ClaimCostArtifactEvalSteps, CalibrationSpecimenWitnessLine, + GuestIdleMeminfoAndNproc, ConcurrentCohortSeatWindows, +} +import gunbc.required_ci_phase_roster { + FloorPhaseRow, FloorPhaseTimed, FloorPhaseUntimed, FloorPhaseRowReading, FloorPhaseRowRead, FloorPhaseRowGarbled, + read_floor_phase_row, +} +import gunbc.witness_floor_workflow { required_floor_claim_cost_artifact_name } +import extdeps.github.actions_artifacts { ActionsArtifact, ActionsArtifactList } +import gunbc.host_budget_source { + FloorCgroupLevelRow, FloorCgroupRowReading, FloorCgroupLevelRead, FloorCgroupRowGarbled, + read_floor_cgroup_level_row, +} // THE DISPATCH LEG OF THE QUALIFICATION ROUTE, AND THE COLLECT STAGE THAT READS ITS RUN BACK. // @@ -102,15 +116,15 @@ type QualificationRunRefusal | RunJobsTruncated { returned: Int, total: Int } | RunNeverReachedAttemptRunner { job_labels: List> } -// INSTRUMENT EXTRACTION IS A DECLARED FRONTIER, carried on the collected arm as the instruments the -// run was read for and not yet extracted from (qualification_instrument_extraction_frontier_rows). +// A COLLECTED RUN IS A RUN THE ATTEMPT'S RUNNER SERVED, AND NOTHING YET ABOUT WHAT IT MEASURED. The +// instruments are read from it afterwards (extract_qualification_instruments), because reading them +// costs one log download per served job and that is only owed once the run is known to be this host's. type QualificationRunCollection = QualificationRunCollected { attempt: NonEmptyStr run_id: Int conclusion: WorkflowRunConclusion attempt_jobs: List - instruments_unextracted: List } | QualificationRunRefused { attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal } @@ -152,7 +166,6 @@ fn conclude_qualification_run( run_id: run_id, conclusion: conclusion, attempt_jobs: served, - instruments_unextracted: qualification_instruments, } } } @@ -167,7 +180,7 @@ fn conclude_qualification_run( fn qualification_run_collected(c: QualificationRunCollection) -> Bool { match c { - QualificationRunCollected { attempt: _, run_id: _, conclusion: _, attempt_jobs: _, instruments_unextracted: _ } => true + QualificationRunCollected { attempt: _, run_id: _, conclusion: _, attempt_jobs: _ } => true QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false } } @@ -190,13 +203,271 @@ fn collect_qualification_run( ) } -// THE EXTRACTION FRONTIER, NAMED AT THE CAPABILITY GRAIN. Four instruments are run outputs and are -// reached through the run's artifacts and job logs; two are not run outputs at all and their sources -// are named here so the extraction leg does not go looking for them in the run. + +// ── EXTRACT: THE INSTRUMENTS READ FROM THE COLLECTED RUN ────────────────────────────────────── +// +// WHERE EACH INSTRUMENT IS READ FROM IS A TOTAL MATCH, so adding an instrument to +// gunbc.runner_throughput_qualification_route QualificationInstrument refuses to compile here until it +// says where its reading comes from. Two are this run's job log, read now. Two are this run's +// required-floor-claim-cost artifact, whose read is attempted on every collection and today ends in a +// typed refusal (read_claim_cost_artifact); two are not outputs of this run at all. Each of those four +// is carried by a row below with the capability that would read it. +type QualificationInstrumentSource + = ReadFromAttemptJobLog + | ReadFromClaimCostArtifact + | ReadFromBootLegReadback + | ReadFromCohortObservation + +fn qualification_instrument_source(i: QualificationInstrument) -> QualificationInstrumentSource { + match i { + FloorPhaseRows => ReadFromAttemptJobLog + FloorCgroupRows => ReadFromAttemptJobLog + ClaimCostArtifactEvalSteps => ReadFromClaimCostArtifact + CalibrationSpecimenWitnessLine => ReadFromClaimCostArtifact + GuestIdleMeminfoAndNproc => ReadFromBootLegReadback + ConcurrentCohortSeatWindows => ReadFromCohortObservation + } +} + +fn qualification_instrument_read_from_job_log(i: QualificationInstrument) -> Bool { + match qualification_instrument_source(i: i) { + ReadFromAttemptJobLog => true + _ => false + } +} + +// ONE SERVED JOB'S LOG, AS GITHUB ANSWERED IT. An unreadable log carries the RestOutcome it came with +// -- a 410 for a log past retention, RestBodyUndecodable for a body the handler could not read whole -- +// and is never read as an empty log. +type AttemptJobLog + = AttemptJobLogRead { job_id: Int, log: String } + | AttemptJobLogUnreadable { job_id: Int, outcome: RestOutcome } + +// THE READINGS. Phase rows are every [floor-phase] row the served jobs printed, timed and untimed, in +// log order; the cgroup rows are only the [floor-cgroup] rows at the SLOT's own level, because the +// levels above it (the runner slice, the system slice) are other subjects' memory and a qualification +// of this slot must not take their peak for its own. +type QualificationLogReadings { + floor_phase_rows: List + slot_cgroup_rows: List +} + +type InstrumentReadRefusal + = JobLogUnreadable { job_id: Int, outcome: RestOutcome } + | FloorPhaseRowGarbledInLog { job_id: Int, line: String, cause: NonEmptyStr } + | FloorCgroupRowGarbledInLog { job_id: Int, line: String, cause: NonEmptyStr } + | FloorPhaseRowsAbsent + | SlotCgroupLevelAbsent { slot_unit: NonEmptyStr, levels_seen: List } + +type QualificationInstrumentExtraction + = QualificationInstrumentsRead { + attempt: NonEmptyStr + run_id: Int + conclusion: WorkflowRunConclusion + readings: QualificationLogReadings + claim_cost: ClaimCostArtifactRead + } + | QualificationInstrumentsRefused { attempt: NonEmptyStr, run_id: Int, refusal: InstrumentReadRefusal } + | QualificationRunNotCollected { collection: QualificationRunCollection } + +// One log's rows, or the first row that could not be read. A garbled row stops the read: a reading +// assembled around a row it skipped would report a phase or a level as absent that the run printed. +type JobLogRowsReading + = JobLogRowsRead { phases: List, levels: List } + | JobLogRowsRefused { refusal: InstrumentReadRefusal } + +fn read_job_log_line(acc: JobLogRowsReading, job_id: Int, line: String) -> JobLogRowsReading { + match acc { + JobLogRowsRefused { refusal: _ } => acc + JobLogRowsRead { phases: ps, levels: ls } => + match read_floor_phase_row(line: line) { + Present { value: FloorPhaseRowGarbled { line: l, cause: c } } => + JobLogRowsRefused { refusal: FloorPhaseRowGarbledInLog { job_id: job_id, line: l, cause: c } } + Present { value: FloorPhaseRowRead { row: r } } => JobLogRowsRead { phases: list_push(ps, r), levels: ls } + Absent => + match read_floor_cgroup_level_row(line: line) { + Present { value: FloorCgroupRowGarbled { line: l, cause: c } } => + JobLogRowsRefused { refusal: FloorCgroupRowGarbledInLog { job_id: job_id, line: l, cause: c } } + Present { value: FloorCgroupLevelRead { row: r } } => JobLogRowsRead { phases: ps, levels: list_push(ls, r) } + Absent => acc + } + } + } +} + +fn read_job_log_rows(acc: JobLogRowsReading, log: AttemptJobLog) -> JobLogRowsReading { + match acc { + JobLogRowsRefused { refusal: _ } => acc + JobLogRowsRead { phases: _, levels: _ } => + match log { + AttemptJobLogUnreadable { job_id: j, outcome: o } => JobLogRowsRefused { refusal: JobLogUnreadable { job_id: j, outcome: o } } + AttemptJobLogRead { job_id: j, log: text } => + fold(split(s: text, delimiter: "\n"), init: acc, f: (a, line) => read_job_log_line(acc: a, job_id: j, line: line)) + } + } +} + +fn level_is_slot(row: FloorCgroupLevelRow, slot_unit: NonEmptyStr) -> Bool { + ends_with(s: row.level as String, suffix: concat("/", slot_unit as String)) +} + +// THE SUPPLIED-INPUT BOUNDARY. Everything the extraction decides is decided here from the run's +// collection and the logs as read; collect_qualification_instruments below is the only caller that +// reaches the network, and it does nothing but read the logs and hand them in. +fn extract_qualification_instruments( + collection: QualificationRunCollection, + logs: List, + claim_cost: ClaimCostArtifactRead, + slot_unit: NonEmptyStr, +) -> QualificationInstrumentExtraction { + match collection { + QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => QualificationRunNotCollected { collection: collection } + QualificationRunCollected { attempt: attempt, run_id: run_id, conclusion: conclusion, attempt_jobs: _ } => + match fold(logs, init: JobLogRowsRead { phases: [], levels: [] }, f: (acc, l) => read_job_log_rows(acc: acc, log: l)) { + JobLogRowsRefused { refusal: r } => QualificationInstrumentsRefused { attempt: attempt, run_id: run_id, refusal: r } + JobLogRowsRead { phases: phases, levels: levels } => { + let slot_levels = levels |> filter(l => level_is_slot(row: l, slot_unit: slot_unit)) + let timed = phases |> filter(p => match p { FloorPhaseTimed { phase: _, wall_ms: _ } => true FloorPhaseUntimed { phase: _ } => false }) + if count(timed) == 0 { + QualificationInstrumentsRefused { attempt: attempt, run_id: run_id, refusal: FloorPhaseRowsAbsent } + } else if count(slot_levels) == 0 { + QualificationInstrumentsRefused { + attempt: attempt, + run_id: run_id, + refusal: SlotCgroupLevelAbsent { slot_unit: slot_unit, levels_seen: levels |> map(l => l.level as String) }, + } + } else { + QualificationInstrumentsRead { + attempt: attempt, + run_id: run_id, + conclusion: conclusion, + readings: QualificationLogReadings { floor_phase_rows: phases, slot_cgroup_rows: slot_levels }, + claim_cost: claim_cost, + } + } + } + } + } +} + +fn read_attempt_job_log(target: QualificationWorkflowTarget, job: WorkflowJobRun) -> AttemptJobLog uses net: Network { + let read = github.WorkflowRuns.DownloadJobLogsForWorkflowRun( + auth_token: "", + owner: target.owner as String, + repo: target.repo as String, + job_id: job.id as String, + ) + match read.outcome { + RestOk => AttemptJobLogRead { job_id: job.id, log: read.log } + other => AttemptJobLogUnreadable { job_id: job.id, outcome: other } + } +} + +// THE CLAIM-COST ARTIFACT, READ AS FAR AS ANY BOUND REALIZATION CAN READ IT. It is looked up by the +// name the floor workflow uploads it under (gunbc.witness_floor_workflow +// required_floor_claim_cost_artifact_name), downloaded through GitHub's redirect, and every way that +// can fail is its own arm. An archive that DID arrive is still not a reading: it is a zip, and no +// inflate reader exists to take the TSV out of it, so that arm says so instead of passing bytes on +// as if they were the TSV. There is deliberately no parsed arm yet; it lands with the capability the +// parse_claim_cost_tsv row of qualification_instrument_extraction_frontier_rows names, at which point +// this read hands the member to gunbc.floor_cost_distribution parse_claim_cost_tsv. +type ClaimCostArtifactRead + = ClaimCostArtifactListUnreadable { outcome: RestOutcome } + | ClaimCostArtifactListTruncated { returned: Int, total: Int } + | ClaimCostArtifactAbsent { names_seen: List } + | ClaimCostArtifactExpired { artifact_id: Int } + | ClaimCostArchiveUnreadable { artifact_id: Int, outcome: RestOutcome } + | ClaimCostArchiveNotInflatable { artifact_id: Int } + +fn claim_cost_artifact_listed(outcome: RestOutcome, listed: ActionsArtifactList) -> ActionsArtifact? { + match outcome { + RestOk => listed.artifacts |> filter(a => a.name == required_floor_claim_cost_artifact_name) |> first + _ => none + } +} + +fn conclude_claim_cost_listing(outcome: RestOutcome, listed: ActionsArtifactList) -> ClaimCostArtifactRead? { + match outcome { + RestOk => + if count(listed.artifacts) != listed.total_count { + Present { value: ClaimCostArtifactListTruncated { returned: count(listed.artifacts), total: listed.total_count } } + } else { + match claim_cost_artifact_listed(outcome: outcome, listed: listed) { + Absent => Present { value: ClaimCostArtifactAbsent { names_seen: listed.artifacts |> map(a => a.name) } } + Present { value: a } => + if a.expired { Present { value: ClaimCostArtifactExpired { artifact_id: a.id } } } else { none } + } + } + other => Present { value: ClaimCostArtifactListUnreadable { outcome: other } } + } +} + +fn conclude_claim_cost_download(artifact_id: Int, outcome: RestOutcome) -> ClaimCostArtifactRead { + match outcome { + RestOk => ClaimCostArchiveNotInflatable { artifact_id: artifact_id } + other => ClaimCostArchiveUnreadable { artifact_id: artifact_id, outcome: other } + } +} + +fn read_claim_cost_artifact(target: QualificationWorkflowTarget, run_id: Int) -> ClaimCostArtifactRead uses net: Network { + let listed = github.Artifacts.ListWorkflowRunArtifacts( + auth_token: "", + owner: target.owner as String, + repo: target.repo as String, + run_id: run_id as String, + ) + match conclude_claim_cost_listing(outcome: listed.outcome, listed: listed.result) { + Present { value: refused } => refused + Absent => + match claim_cost_artifact_listed(outcome: listed.outcome, listed: listed.result) { + Absent => ClaimCostArtifactAbsent { names_seen: [] } + Present { value: a } => { + let archive = github.Artifacts.DownloadArtifact( + auth_token: "", + owner: target.owner as String, + repo: target.repo as String, + artifact_id: a.id as String, + ) + conclude_claim_cost_download(artifact_id: a.id, outcome: archive.outcome) + } + } + } +} + +fn collect_qualification_instruments( + target: QualificationWorkflowTarget, + collection: QualificationRunCollection, + slot_unit: NonEmptyStr, +) -> QualificationInstrumentExtraction uses net: Network { + match collection { + QualificationRunCollected { attempt: _, run_id: run_id, conclusion: _, attempt_jobs: served } => { + let logs = served |> map(j => read_attempt_job_log(target: target, job: j)) + let claim_cost = read_claim_cost_artifact(target: target, run_id: run_id) + extract_qualification_instruments(collection: collection, logs: logs, claim_cost: claim_cost, slot_unit: slot_unit) + } + QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => QualificationRunNotCollected { collection: collection } + } +} + +// ── THE INSTRUMENTS THIS RUN'S LOG DOES NOT CARRY, EACH WITH THE CAPABILITY THAT WOULD READ IT ── +// +// The extraction above reads every instrument qualification_instrument_source places on the job +// log. The remaining four are three facts, so three rows: one per missing capability, so that each +// row is retired by its own trigger and by nothing else. data qualification_instrument_extraction_frontier_rows: List = [ frontier_row_decl( - ref: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "QualificationRunCollection"), - reason: "a collected qualification run carries every qualification_instrument as unextracted: extdeps.github has no Actions artifacts or job-logs read operation, no path that follows the logs redirect or opens the artifact archive, and no parser for the [floor-phase] and [floor-cgroup] log rows. Two instruments are not run outputs: GuestIdleMeminfoAndNproc comes from the boot leg's runner-host-up readback, and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort", - dissolution: unbound_dissolution(description: "the Actions artifacts and job-logs read path (list and download a run's artifacts, download a job's log text) plus the floor row parsers are SUFFICIENT to populate FloorPhaseRows, FloorCgroupRows, ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from one collected run through the producer each instrument names, with GuestIdleMeminfoAndNproc read from the boot leg's runner-host-up readback and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort, so QualificationRunCollected carries readings for every qualification_instrument and no unextracted list. NOT satisfied by an artifacts operation with no parser, nor by a parser exercised only on a fixture"), - ) + ref: decl_ref(module_path: "gunbc.floor_cost_distribution", decl_name: "parse_claim_cost_tsv"), + reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding), and neither reaches the job log in complete form: measured on a recorded required-floor log, eval_steps appear only on the 25-row [over-cost] preview and the per-claim [floor-shared-fill] rows, and the calibration specimen's [witness] line does not appear at all. Two capabilities are missing. The seed still writes the TSV (cli_run write_required_floor_claim_cost_tsv) but no workflow uploads it as an artifact today -- recent required runs publish only required-ci-measurement-receipt and compiler-pair-candidate. And an uploaded artifact is always a zip, while the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so extdeps.github.actions_artifacts github.Artifacts DownloadArtifact answers RestBodyUndecodable. read_claim_cost_artifact attempts the read on every collection and carries that as a typed ClaimCostArtifactRead refusal (ClaimCostArtifactAbsent while nothing uploads it)", + dissolution: unbound_dissolution(description: "the workflow the qualification dispatch targets uploads required-floor-claim-cost (gunbc.witness_floor_workflow carries the upload step under required_floor_claim_cost_artifact_name), AND the REST transport carries a binary body with an inflate reader SUFFICIENT FOR reading one member out of one upload-artifact zip, so extract_qualification_instruments reads ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from the same collected run through parse_claim_cost_tsv and qualification_instrument_source places both on that read. NOT satisfied by a new print of eval_steps into the log, by a 25-row preview, by an artifact downloaded outside this route, or by the zip read alone while no workflow uploads the artifact"), + ), + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.runner_throughput_qualification_route", decl_name: "mtcollins1_boot_leg"), + reason: "GuestIdleMeminfoAndNproc is not a run output: the guest's idle MemTotal, MemAvailable (extdeps.linux.proc_meminfo), nproc and uname -m are read before the job starts, by the boot leg's runner-host-up readback, and that leg is gunbc.runner_throughput_qualification_route mtcollins1_boot_leg, still LegAwaitingAuthority", + dissolution: unbound_dissolution(description: "the boot leg's terminal is the runner host observed up and its readback carries the guest's idle meminfo, nproc and machine word, SUFFICIENT FOR this route to hand that readback to the qualification receipt beside this run's extraction. NOT satisfied by a meminfo read inside the floor job, which is a loaded guest rather than an idle one"), + ), + frontier_row_decl( + ref: decl_ref(module_path: "product.capacity.cohort_observation", decl_name: "seal_cohort"), + reason: "ConcurrentCohortSeatWindows is sealed by product.capacity.cohort_observation seal_cohort over a roster of seats released at one barrier, each with ready, start and end instants and a settlement. This route registers ONE ephemeral slot and dispatches one floor, so there is no cohort and no barrier to read; one job's started_at and completed_at are not a seat window", + dissolution: unbound_dissolution(description: "a qualification route that registers a roster of concurrent seats on the host, releases them at a barrier, and reads each seat's ready, start and end back, SUFFICIENT FOR seal_cohort to seal that reading for this host. NOT satisfied by a cohort of one, nor by job timestamps standing in for seat readiness"), + ), ] diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 8a3854c5dff..e42792242d1 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -2,11 +2,12 @@ module test.claim.runner_qualification_dispatch_witness import std.types { Bool, Int, List, NonEmptyStr, String } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import extdeps.transports.rest { RestOk, RestStatusRefused, RestTransportRefused } +import extdeps.transports.rest { RestOk, RestStatusRefused, RestTransportRefused, RestBodyUndecodable } +import extdeps.github.actions_artifacts { ActionsArtifact, ActionsArtifactList } import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, } -import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, qualification_instruments } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, qualification_instruments, FloorPhaseRows, FloorCgroupRows } import gunbc.fleet_intent_network { operator_host_mtcollins1 } import gunbc.runner.runner_qualification_dispatch { qualification_attempt_dispatch_input, qualification_dispatch_inputs, @@ -14,6 +15,21 @@ import gunbc.runner.runner_qualification_dispatch { RunReadUnreadable, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunNeverReachedAttemptRunner, conclude_qualification_run, qualification_run_collected, + AttemptJobLog, AttemptJobLogRead, AttemptJobLogUnreadable, + QualificationInstrumentExtraction, QualificationInstrumentsRead, QualificationInstrumentsRefused, QualificationRunNotCollected, + QualificationLogReadings, JobLogUnreadable, FloorPhaseRowGarbledInLog, FloorCgroupRowGarbledInLog, + FloorPhaseRowsAbsent, SlotCgroupLevelAbsent, + extract_qualification_instruments, qualification_instrument_read_from_job_log, + ClaimCostArtifactRead, ClaimCostArtifactAbsent, ClaimCostArtifactExpired, ClaimCostArtifactListTruncated, + ClaimCostArchiveUnreadable, ClaimCostArchiveNotInflatable, + conclude_claim_cost_listing, conclude_claim_cost_download, + qualification_instrument_extraction_frontier_rows, +} +import gunbc.required_ci_phase_roster { FloorPhaseTimed, FloorPhaseUntimed } +import extdeps.linux.cgroup_v2_memory { CgroupMemoryLimited } +import std.measure { byte_size_count } +import test.fixture.recorded_required_floor_log_excerpt { + recorded_required_floor_log_excerpt, recorded_floor_log_job_id, recorded_floor_log_slot_unit, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -84,15 +100,12 @@ fn conclude(run_completed: Bool, concluded: Bool, js: WorkflowJobRunList) -> Qua } // THE POSITIVE CONTROL: a completed run whose job was taken under the attempt label is collected, -// its conclusion is the one read back (a FAILED floor is still collected and says so), and every -// qualification_instrument is carried as unextracted -- the declared extraction frontier, not a -// silent empty reading. +// and its conclusion is the one read back (a FAILED floor is still collected and says so). test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion() -> Bool { match conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { - QualificationRunCollected { attempt: a, run_id: r, conclusion: c, attempt_jobs: served, instruments_unextracted: u } => + QualificationRunCollected { attempt: a, run_id: r, conclusion: c, attempt_jobs: served } => a == attempt && r == 77 && count(served) == 1 && (match c { Failure => true _ => false }) - && count(u) == count(qualification_instruments) QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false } } @@ -148,3 +161,159 @@ test fn a_truncated_or_unreadable_readback_refuses() -> Bool { _ => false }) } + +// ── EXTRACTION, OVER SUPPLIED LOGS ───────────────────────────────────────────────────────────── +// +// These claims are about extract_qualification_instruments alone, so the collection and the logs are +// supplied values. The real producer's bytes are exercised once, by the inhabitance claim at the end. + +data no_claim_cost: ClaimCostArtifactRead = ClaimCostArtifactAbsent { names_seen: ["required-ci-measurement-receipt"] } + +data slot: NonEmptyStr = "gunbc-qualification-mtcollins1-q-20261004-1.service" + +fn served_collection() -> QualificationRunCollection { + conclude(run_completed: true, concluded: true, js: jobs(js: [job(labels: [attempt as String], started: true)])) +} + +data phase_line: String = "2026-10-04T00:10:00.0000000Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0" +data started_line: String = "2026-10-04T00:09:00.0000000Z [floor-phase] phase=strict-preparation state=started" +data slot_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/gunbc-qualification-mtcollins1-q-20261004-1.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0]" +data slice_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=1 peak=364675248128" + +fn log_of(lines: List) -> List { + [AttemptJobLogRead { job_id: 500, log: join(lines, "\n") }] +} + +fn extract(lines: List) -> QualificationInstrumentExtraction { + extract_qualification_instruments(collection: served_collection(), logs: log_of(lines: lines), claim_cost: no_claim_cost, slot_unit: slot) +} + +test fn the_job_log_instruments_are_the_two_the_extraction_reads() -> Bool { + qualification_instrument_read_from_job_log(i: FloorPhaseRows) + && qualification_instrument_read_from_job_log(i: FloorCgroupRows) + && count(qualification_instruments |> filter(i => qualification_instrument_read_from_job_log(i: i))) == 2 + && count(qualification_instrument_extraction_frontier_rows) == 3 +} + +// THE POSITIVE CONTROL: timed and untimed phase rows are both carried, and only the slot's own level +// is taken as the cgroup reading -- the slice above it has a larger peak and must not be read as the +// slot's. +test fn a_log_with_phase_rows_and_the_slot_level_is_read() -> Bool { + match extract(lines: ["noise before the floor", started_line, slot_level_line, slice_level_line, phase_line]) { + QualificationInstrumentsRead { attempt: a, run_id: r, conclusion: _, readings: rd, claim_cost: cc } => + a == attempt && r == 77 + && (match cc { ClaimCostArtifactAbsent { names_seen: _ } => true _ => false }) + && count(rd.floor_phase_rows) == 2 + && count(rd.slot_cgroup_rows) == 1 + && (rd.slot_cgroup_rows |> all(l => byte_size_count(b: l.peak) == 10444185600)) + _ => false + } +} + +test fn a_garbled_row_refuses_rather_than_being_skipped() -> Bool { + (match extract(lines: [slot_level_line, phase_line, "[floor-phase] phase=gate-closure wall_ms=twelve"]) { + QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: FloorPhaseRowGarbledInLog { job_id: 500, line: _, cause: _ } } => true + _ => false + }) + && (match extract(lines: [phase_line, "[floor-cgroup] when=beat-10 level=/sys/fs/cgroup/x.service high=lots peak=1"]) { + QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: FloorCgroupRowGarbledInLog { job_id: 500, line: _, cause: _ } } => true + _ => false + }) +} + +test fn a_log_missing_either_reading_refuses_with_what_it_saw() -> Bool { + (match extract(lines: [started_line, slot_level_line]) { + QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: FloorPhaseRowsAbsent } => true + _ => false + }) + && (match extract(lines: [phase_line, slice_level_line]) { + QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } } => + u == slot && count(seen) == 1 + _ => false + }) +} + +test fn an_unreadable_log_or_an_uncollected_run_reads_nothing() -> Bool { + (match extract_qualification_instruments( + collection: served_collection(), + logs: [AttemptJobLogUnreadable { job_id: 500, outcome: RestStatusRefused { status: 410, body: "Gone" } }], + claim_cost: no_claim_cost, + slot_unit: slot, + ) { + QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: JobLogUnreadable { job_id: 500, outcome: _ } } => true + _ => false + }) + && (match extract_qualification_instruments( + collection: conclude(run_completed: false, concluded: false, js: jobs(js: [])), + logs: log_of(lines: [phase_line, slot_level_line]), + claim_cost: no_claim_cost, + slot_unit: slot, + ) { + QualificationRunNotCollected { collection: _ } => true + _ => false + }) +} + +// ── INHABITANCE: THE REAL PRODUCER'S BYTES ───────────────────────────────────────────────────── +// +// The supplied lines above are designed against the row shapes; this claim is what keeps them +// readings. It hands extract_qualification_instruments the verbatim [floor-phase] and [floor-cgroup] +// lines claim_executor printed on a recorded required-floor run (test.fixture +// recorded_required_floor_log_excerpt), under that run's real slot unit, and asserts the ROUTE as well +// as the answer: every one of the 79 phase rows is read (none garbled, none dropped), the timed rows +// carry the gate-closure wall the log printed, and of the 12 level rows only the 4 at the slot's own +// level -- floor entry and three heartbeats -- are taken, with the slot's memory.high read as a byte +// bound and floor entry's memory.peak as printed. If the host's row encoding moves, this goes red +// while the designed fixtures above stay green. +test fn the_extraction_reads_a_recorded_required_floor_log() -> Bool { + match extract_qualification_instruments( + collection: served_collection(), + logs: [AttemptJobLogRead { job_id: recorded_floor_log_job_id, log: recorded_required_floor_log_excerpt }], + claim_cost: no_claim_cost, + slot_unit: recorded_floor_log_slot_unit as NonEmptyStr, + ) { + QualificationInstrumentsRead { attempt: _, run_id: _, conclusion: _, readings: rd, claim_cost: _ } => + count(rd.floor_phase_rows) == 79 + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: ph, wall_ms: ms } => (ph as String) == "gate-closure" && ms == 22194 _ => false })) + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseUntimed { phase: ph } => (ph as String) == "strict-preparation" _ => false })) + && count(rd.slot_cgroup_rows) == 4 + && (rd.slot_cgroup_rows |> all(l => match l.high { CgroupMemoryLimited { bytes: b } => byte_size_count(b: b) == 16106127360 _ => false })) + && (rd.slot_cgroup_rows |> any(l => (l.when as String) == "floor-entry" && byte_size_count(b: l.peak) == 10444185600)) + _ => false + } +} + +// ── THE CLAIM-COST READ'S REFUSALS ───────────────────────────────────────────────────────────── +// +// Every way the artifact read can end today is a typed arm, and none of them is a reading: the +// listing truncated, the artifact absent (with the names that were there), expired, the archive +// unreadable (the RestBodyUndecodable a zip body produces), or an archive that arrived with no +// inflate reader to open it. +fn artifact(name: String, expired: Bool) -> ActionsArtifact { + ActionsArtifact { id: 9001, name: name, size_in_bytes: 4096, archive_download_url: "https://api.github.com/x", expired: expired, created_at: none, expires_at: none } +} + +test fn every_claim_cost_read_ends_in_a_typed_refusal() -> Bool { + let present = ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-floor-claim-cost", expired: false)] } + (match conclude_claim_cost_listing(outcome: RestOk, listed: ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-ci-measurement-receipt", expired: false)] }) { + Present { value: ClaimCostArtifactAbsent { names_seen: ns } } => count(ns) == 1 + _ => false + }) + && (match conclude_claim_cost_listing(outcome: RestOk, listed: ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-floor-claim-cost", expired: true)] }) { + Present { value: ClaimCostArtifactExpired { artifact_id: 9001 } } => true + _ => false + }) + && (match conclude_claim_cost_listing(outcome: RestOk, listed: ActionsArtifactList { total_count: 3, artifacts: [] }) { + Present { value: ClaimCostArtifactListTruncated { returned: 0, total: 3 } } => true + _ => false + }) + && (match conclude_claim_cost_listing(outcome: RestOk, listed: present) { Absent => true _ => false }) + && (match conclude_claim_cost_download(artifact_id: 9001, outcome: RestBodyUndecodable { status: 200, cause: "stream did not contain valid UTF-8" }) { + ClaimCostArchiveUnreadable { artifact_id: 9001, outcome: _ } => true + _ => false + }) + && (match conclude_claim_cost_download(artifact_id: 9001, outcome: RestOk) { + ClaimCostArchiveNotInflatable { artifact_id: 9001 } => true + _ => false + }) +} diff --git a/dag/test/fixture/recorded_required_floor_log_excerpt.dag b/dag/test/fixture/recorded_required_floor_log_excerpt.dag new file mode 100644 index 00000000000..6c738127189 --- /dev/null +++ b/dag/test/fixture/recorded_required_floor_log_excerpt.dag @@ -0,0 +1,22 @@ +module test.fixture.recorded_required_floor_log_excerpt + +import std.types { Int, String } + +// REAL BYTES FROM ONE RECORDED REQUIRED-FLOOR JOB LOG, NOT A DESIGNED FIXTURE. The source is GitHub +// Actions run 35048059968, job 104642384772 (required-witnesses-floor, conclusion success, runner slot +// actions-runner@srv4-18.service), read through GitHub's "Download job logs for a workflow run" on +// 2026-10-04. The excerpt is every `[floor-phase]` and `[floor-cgroup]` line of that log, verbatim and +// in log order with GitHub's timestamp prefix kept, plus a handful of the log's untagged and +// differently tagged lines (an `[over-cost]` preview row among them) that a reader of the two tags +// must pass over. Nothing is edited inside a line; the .dag string escapes are the only change. +// +// IT IS AN EXCERPT BECAUSE THE WHOLE LOG IS 1.5 MB, and that size is itself a recorded fact the +// extraction relies on: it sits well under the seed REST handler's whole-body read, so the network +// read of a log like this one is not refused for size. +data recorded_floor_log_run_id: Int = 35048059968 + +data recorded_floor_log_job_id: Int = 104642384772 + +data recorded_floor_log_slot_unit: String = "actions-runner@srv4-18.service" + +data recorded_required_floor_log_excerpt: String = "2026-09-16T02:36:29.9676382Z [floor-cgroup] when=floor-entry path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:36:29.9692531Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.00,total=2,full,avg10=0.00,avg60=0.00,avg300=0.00,total=2]\n2026-09-16T02:36:29.9698651Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=196170059776 peak=364675248128 events=[low,0,high,53814358,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.02,total=13867330781,full,avg10=0.00,avg60=0.00,avg300=0.02,total=13729467014]\n2026-09-16T02:36:29.9703909Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=212249088000 peak=380169330688 events=[low,0,high,53814358,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.02,total=13841256370,full,avg10=0.00,avg60=0.00,avg300=0.02,total=13703013143]\n2026-09-16T02:36:29.9709147Z [floor-phase] phase=strict-preparation state=started\n2026-09-16T02:40:20.9989392Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0 seeds=1 closure=168 bare_pulled=36 outside_closure=5688 corpus=5856\n2026-09-16T02:40:32.0494666Z [floor-phase] phase=expr-var-classification expr_var_occurrences=13930 free_reference_edges=3081 bound_occurrences_suppressed=10836 qualified_chain_heads=13 classification_refusals=0\n2026-09-16T02:40:32.0501531Z [floor-phase] phase=expr-var-classification BoundByParameter=7869 BoundByLet=1153 BoundByMatchPattern=1814 DeclaredInThisModule=1964 DeclaredElsewhere=1117 QualifiedChainHead=13\n2026-09-16T02:40:32.0506107Z [floor-phase] phase=reference-closure-index state=completed wall_ms=63 modules=168 names=6680 subject=93ea90191edaf50d\n2026-09-16T02:40:32.7858463Z [floor-phase] phase=touched-entry-compile-subject seeds=5 modules=[\"extdeps.tmux\", \"gunbc.roadmap_dashboard_instance\", \"gunbc.roadmap_dashboard_instance_apply\", \"test.claim.roadmap_dashboard_instance_apply_witness\", \"test.claim.tmux_session_list_outcome_witness\"]\n2026-09-16T02:40:32.7866444Z [floor-phase] phase=arm-set-changed-consumers state=completed changed_declarations=0 consumers_added=0 flat_channel_consumers=0 outside_floor_roots=0 base=db2257679fb454fcf80ddb4b901d422e8afa5d51 head=84085164947f2c2116ecd8060368c2cf35d22c8c\n2026-09-16T02:41:38.2496524Z [floor-phase] phase=gate-closure state=completed wall_ms=65296 prefixes=32 seeds=1027 closure=3021 bare_pulled=71 outside_closure=2835 corpus=5856\n2026-09-16T02:46:29.9985719Z [floor-cgroup] when=beat-10 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:46:29.9992653Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16104755200 peak=16106577920 events=[low,0,high,3848,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,3848,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=5.62,avg60=3.64,avg300=1.66,total=8053481,full,avg10=5.62,avg60=3.64,avg300=1.66,total=8053205]\n2026-09-16T02:46:30.0042734Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=227828391936 peak=364675248128 events=[low,0,high,53837371,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=1.16,avg60=1.36,avg300=0.72,total=13872120203,full,avg10=1.16,avg60=1.36,avg300=0.72,total=13734253335]\n2026-09-16T02:46:30.0074968Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice max=max high=max current=243909996544 peak=380169330688 events=[low,0,high,53837371,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=1.15,avg60=1.39,avg300=0.72,total=13846043407,full,avg10=1.15,avg60=1.39,avg300=0.72,total=13707796653]\n2026-09-16T02:48:21.6671401Z [floor-phase] phase=module-path-index-warm state=completed cpu_ms=0 wall_ms=0 rss_growth_bytes=8192 modules=5856 provenance=built-by-preparation\n2026-09-16T02:48:21.6828893Z [floor-phase] phase=shared-index-warm state=completed cpu_ms=0 wall_ms=0 rss_growth_bytes=61440 modules=5856 provenance=built-by-preparation\n2026-09-16T02:48:22.6822520Z [floor-phase] phase=expr-var-classification expr_var_occurrences=178167 free_reference_edges=47772 bound_occurrences_suppressed=129176 qualified_chain_heads=1210 classification_refusals=0\n2026-09-16T02:48:22.6831385Z [floor-phase] phase=expr-var-classification BoundByParameter=84278 BoundByLet=17473 BoundByMatchPattern=27425 DeclaredInThisModule=21920 DeclaredElsewhere=25852 QualifiedChainHead=1210 NamesNothingKnown=9\n2026-09-16T02:48:22.7377365Z [floor-phase] phase=expr-var-classification unclassified=9 distinct=7 specimens=[\"extdeps.cloud.gcp.iam::access_token\", \"extdeps.cloud.gcp.secret_manager::access_token\", \"extdeps.cloudflare.account_api_tokens::auth_token\", \"extdeps.github.pulls::List_PullRequest\", \"extdeps.github.pulls::List_PullReview\", \"extdeps.github.pulls::List_ReviewComment\", \"std.resources::ReadWrite\"]\n2026-09-16T02:48:22.7385985Z [floor-phase] phase=reference-closure-index state=completed wall_ms=926 modules=3019 names=68559 subject=eb8fa8db5887fb3b\n2026-09-16T02:48:24.2494508Z [floor-phase] phase=pool-root-index-warm state=completed cpu_ms=524 wall_ms=777 rss_growth_bytes=7786496 producer=v2.lens.registry.completeness.lens_registry_completeness_live_facts provenance=built-by-preparation\n2026-09-16T02:48:25.1817029Z [floor-phase] phase=languages-consumer-census-warm state=completed cpu_ms=729 wall_ms=785 rss_growth_bytes=0 decl_rows=72 provenance=built-by-preparation\n2026-09-16T02:48:27.8950074Z [floor-phase] phase=prepared-effect-input-acquire state=completed acquisition=gunbc.roadmap_authority.roadmap_acceptance_event_history_load checkout_input=dag/gunbc/roadmap/roadmap_acceptance_event_history.jsonl content_digest=ac7a97b227a758c0 disposition=RoadmapAcceptanceEventHistoryLoaded cpu_ms=13 wall_ms=23 rss_growth_bytes=0\n2026-09-16T02:48:28.1831332Z [floor-phase] phase=prepared-effect-input-warm state=completed producer=gunbc.roadmap_authority.roadmap_authority_projection input=gunbc.roadmap_authority.roadmap_acceptance_event_history_load disposition=Stored cpu_ms=250 wall_ms=287 rss_growth_bytes=1867776\n2026-09-16T02:48:30.3776259Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.native_decl_selection.collision_resolved disposition=Stored cpu_ms=2150 wall_ms=2194 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:30.3818798Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.extdeps.languages.bash_command_fold.bash_fold_formal_productions disposition=Stored cpu_ms=4 wall_ms=4 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:30.3826228Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.extdeps.languages.bash_command_fold.bash_fold_lex disposition=Stored cpu_ms=0 wall_ms=0 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:30.4161518Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.compiler.program_assembly.dag_prepared_grammar disposition=Stored cpu_ms=32 wall_ms=32 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:31.1141726Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_produced_module_equals_eval.produced_add_module_source disposition=Stored cpu_ms=694 wall_ms=697 rss_growth_bytes=237568 provenance=built-by-preparation\n2026-09-16T02:48:31.6563401Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_classical_not_ingested_equals_eval.ingested_classical_not_arrow_with_body disposition=Stored cpu_ms=529 wall_ms=531 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:31.8729253Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_classical_not_ingested_equals_eval.ingested_classical_not_swapped_arrow_with_body disposition=Stored cpu_ms=224 wall_ms=225 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:32.2808714Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.compiler.self_host.direct_rust_door_fixture.direct_rust_door_specimen_resolved disposition=Stored cpu_ms=405 wall_ms=408 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:32.6503107Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.execution.emit_on_demand_match_loop_fold_family_witness.mlf_family_primary_emitted disposition=Stored cpu_ms=358 wall_ms=369 rss_growth_bytes=487424 provenance=built-by-preparation\n2026-09-16T02:48:33.0043494Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.execution.emit_on_demand_family_crate_witness.logic_family_primary_emitted disposition=Stored cpu_ms=341 wall_ms=353 rss_growth_bytes=221184 provenance=built-by-preparation\n2026-09-16T02:48:33.3686481Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.self_host.stage0_production_target.stage0_boundary_profile_emitted_source disposition=Stored cpu_ms=358 wall_ms=364 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:33.5046083Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.self_host.stage0_production_target.stage0_boundary_base_emitted_source disposition=Stored cpu_ms=11 wall_ms=11 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.1446122Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add disposition=Stored cpu_ms=744 wall_ms=764 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.3435250Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add2 disposition=Stored cpu_ms=180 wall_ms=198 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.6980018Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add_then_add2 disposition=Stored cpu_ms=342 wall_ms=354 rss_growth_bytes=114688 provenance=built-by-preparation\n2026-09-16T02:48:34.7349575Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declarationless_refuses disposition=Stored cpu_ms=35 wall_ms=36 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:34.7437180Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_unwired_two_declaration_refuses disposition=Stored cpu_ms=9 wall_ms=9 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:34.7516227Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declaration_count_add disposition=Stored cpu_ms=6 wall_ms=6 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.7582621Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declaration_count_add_then_add2 disposition=Stored cpu_ms=8 wall_ms=8 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.9354974Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_reproducer_parsed disposition=Stored cpu_ms=166 wall_ms=173 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:35.0535117Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_bare_parsed disposition=Stored cpu_ms=116 wall_ms=121 rss_growth_bytes=90112 provenance=built-by-preparation\n2026-09-16T02:48:35.1540672Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_control_parsed disposition=Stored cpu_ms=97 wall_ms=100 rss_growth_bytes=49152 provenance=built-by-preparation\n2026-09-16T02:48:35.3384595Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_cache_identity_where_parsed disposition=Stored cpu_ms=183 wall_ms=184 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:35.5009300Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.where_refinement_clause_parse.where_refinement_bare_parsed disposition=Stored cpu_ms=161 wall_ms=162 rss_growth_bytes=16384 provenance=built-by-preparation\n2026-09-16T02:48:35.6112723Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.expression_bodied_fn_parsed disposition=Stored cpu_ms=109 wall_ms=110 rss_growth_bytes=16384 provenance=built-by-preparation\n2026-09-16T02:48:35.6769819Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.expression_bodied_literal_fn_parsed disposition=Stored cpu_ms=65 wall_ms=65 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:35.7771869Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.braced_fn_control_parsed disposition=Stored cpu_ms=99 wall_ms=100 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:35.7781034Z [floor-phase] phase=pure-producer-share-scope state=completed rostered_modules_outside_subject=1 modules=[test.claim.live_deploy.emit]\n2026-09-16T02:48:55.2023750Z [floor-phase] phase=bare-reference-edge-index-warm state=completed roots=BareReferenceEdgeIndexBuild/source-roots cpu_ms=10124 wall_ms=17621 rss_growth_bytes=0 source_files=5856 bare_eligible=637 provenance=built-by-preparation\n2026-09-16T02:48:55.2034410Z [floor-phase] phase=bare-reference-edge-index-warm state=completed roots=BareReferenceEdgeIndexBuild/witness-layer-roots cpu_ms=6 wall_ms=6 rss_growth_bytes=0 source_files=5856 bare_eligible=637 provenance=already-warm-on-entry triggered_by=a-site-ahead-of-floor-preparation\n2026-09-16T02:48:55.2046185Z [floor-phase] phase=strict-preparation state=completed wall_ms=745230 modules_resolved=3019 modules_excluded=2 digest=eb8fa8db5887fb3b\n2026-09-16T02:48:55.4049682Z [floor-phase] phase=module-index state=completed wall_ms=202 modules=4460\n2026-09-16T02:48:55.7569513Z [floor-phase] phase=declarer-discovery state=completed wall_ms=350 declarers=18\n2026-09-16T02:48:56.1087890Z [floor-phase] phase=whole-tree-graph-facts state=completed wall_ms=353\n2026-09-16T02:48:56.1231903Z [floor-phase] phase=declarer-closure state=completed wall_ms=14 sources=19\n2026-09-16T02:48:56.2786170Z [floor-phase] phase=closure-strict-resolve state=completed wall_ms=155 sources=19\n2026-09-16T02:48:56.3076153Z [floor-phase] phase=published-mock-projection state=completed wall_ms=1105 keys=91\n2026-09-16T02:50:27.9817767Z [floor-phase] phase=discovery-authority state=completed wall_ms=90265 authority=v2.workflow.floor_discovery_source_authority sources=5856 rows=19891 entries=2194 full_inventory_release_rss_kb_before=15307380 full_inventory_release_trim_reclaimed_kb=731228 full_inventory_release_rss_kb_after=14577200\n2026-09-16T02:50:28.9533919Z [floor-claim-ceiling] grandfathered_identities=3779 grandfathered_budget_steps=361500 new_witness_budget_steps=72300\n2026-09-16T02:50:29.0124493Z [floor-phase] phase=site-projection state=completed wall_ms=91735 declared=19891 sites=4879 files=2194 claims=3971 declined_long=401 declined_fixture=0 declined_outside_gate=177 declined_gate_closure=15007 declined_discovery_excluded=5 declined_cost_debt=330\n2026-09-16T02:50:31.9575417Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9580122Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9626843Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=5 subject=c01458db5a196a34\n2026-09-16T02:50:31.9645327Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=0 bound_occurrences_suppressed=1 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9649696Z [floor-phase] phase=expr-var-classification BoundByParameter=1\n2026-09-16T02:50:31.9653062Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=92107e64fdc80bbc\n2026-09-16T02:50:31.9704793Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=1 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9707855Z [floor-phase] phase=expr-var-classification DeclaredElsewhere=1\n2026-09-16T02:50:31.9712213Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=4 names=7 subject=9b51427d706ae1d9\n2026-09-16T02:50:31.9898466Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=1 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9903147Z [floor-phase] phase=expr-var-classification DeclaredInThisModule=1\n2026-09-16T02:50:31.9905070Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=4 names=7 subject=3bd800efac0c7530\n2026-09-16T02:50:31.9923594Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9926724Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9930122Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:50:31.9993760Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9995823Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9999837Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:50:32.0033070Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:32.0037327Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:32.0039559Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:56:30.0047177Z [floor-cgroup] when=beat-20 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:56:30.0056010Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16078217216 peak=16106631168 events=[low,0,high,7254,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,7254,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=2.17,avg60=1.80,avg300=1.62,total=26620107,full,avg10=2.17,avg60=1.80,avg300=1.62,total=26619735]\n2026-09-16T02:56:30.0068529Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=159988195328 peak=364675248128 events=[low,0,high,53855074,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.24,avg60=0.28,avg300=0.47,total=13878228854,full,avg10=0.24,avg60=0.28,avg300=0.47,total=13740334938]\n2026-09-16T02:56:30.0079198Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice max=max high=max current=176071000064 peak=380169330688 events=[low,0,high,53855074,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.21,avg60=0.26,avg300=0.45,total=13852086330,full,avg10=0.21,avg60=0.26,avg300=0.45,total=13713813769]\n2026-09-16T02:59:58.3859791Z [floor-fold-time] scope_build=143.5s over 695 construction(s) (mean=206ms) frame_build=2.2s over 3971 construction(s) (mean=0.5ms)\n2026-09-16T02:59:58.3860774Z [floor-scope-cost] max=0.13GB at=v2.test.claim.staging (1596 modules) total_built=1.29GB over 695 construction(s)\n2026-09-16T03:00:30.6968920Z [floor-phase] phase=terminal-ledger-publish state=completed resolve_ms=31249 total_ms=32635 rows=3971\n2026-09-16T03:00:30.8700701Z [over-cost] v2.test.claim.enforcement.determinism_transitive_witness.determinism_live_intra_holds observed_wall_ms=325 observed_cpu_ms=317 eval_steps=165094 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8705737Z [over-cost] v2.test.lens_test_migration_debt.test_migration_debt_test.retained_rust_kernel_wall_holds_against_live_tree observed_wall_ms=295 observed_cpu_ms=292 eval_steps=3128 line_ms=100 outcome=fail\n2026-09-16T03:00:30.8711745Z [over-cost] v2.test.claim.body_lowering.arrow_body_form_witness.arrow_body_form_eval_value_vertical_holds observed_wall_ms=287 observed_cpu_ms=232 eval_steps=90747 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8717111Z [over-cost] v2.test.claim.enforcement.lens_module_gate_witness.question_zero_verdict_live_holds observed_wall_ms=274 observed_cpu_ms=212 eval_steps=63825 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8723183Z [over-cost] v2.test.emit.emit_module_contribution.emc_all_three_arms_are_constructed_by_the_emitter_holds observed_wall_ms=211 observed_cpu_ms=203 eval_steps=110579 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8724944Z [over-cost] v2.test.parse.d5_expression_grammar_parse.cause_i_match_infix_parses_holds observed_wall_ms=204 observed_cpu_ms=203 eval_steps=76436 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8726657Z [over-cost] v2.test.emit.emit_module_contribution.emc_origins_locate_the_unobserved_producer_holds observed_wall_ms=208 observed_cpu_ms=201 eval_steps=110307 line_ms=100 outcome=pass\n2026-09-16T03:06:30.0271620Z [floor-cgroup] when=beat-30 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T03:06:30.0274812Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16106008576 peak=16112607232 events=[low,0,high,10824,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,10824,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=9.69,avg60=6.15,avg300=2.73,total=39674404,full,avg10=9.69,avg60=6.15,avg300=2.73,total=39670316]\n2026-09-16T03:06:30.0279569Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=172097458176 peak=364675248128 events=[low,0,high,53882643,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.61,avg60=1.03,avg300=1.39,total=13890030741,full,avg10=0.51,avg60=0.99,avg300=1.38,total=13752007203]\n2026-09-16T03:06:30.0283751Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice max=max high=max current=188181905408 peak=380169330688 events=[low,0,high,53882645,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.61,avg60=1.00,avg300=1.35,total=13863806856,full,avg10=0.51,avg60=0.97,avg300=1.34,total=13725404851]" From b07b5751287e9a5256190ed2b9872fa48528bc82 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 4 Oct 2026 05:12:42 +0000 Subject: [PATCH 06/44] Address side-chat RC on #13206: causal readback, sealed subject/receipt, token check, workflow compare; split RouteLegStanding (1) post-delete listing consumes the delete receipts (readback_after_deletes) (2) JitRegistration sole_constructor, minted only by jit_registration_of (3) refuse a token or authority for another App/installation (4) JitDeregistrationReceipt sealed, built only from a GitHub listing (OrganizationRunnerListRead sealed); pure classifier kept (5) slots carry their workflow; mint refuses a group restricted to another RouteLegStanding = LegWired | LegAuthorityImplemented | LegAwaitingAuthority; route_is_executable requires LegWired; registration/deregistration are LegAuthorityImplemented with the wiring they owe. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/github_effect_perform.dag | 4 +- dag/gunbc/runner/runner_host_unit_slot.dag | 7 +- .../runner/runner_jit_deregistration.dag | 189 ++++++++++++------ dag/gunbc/runner/runner_jit_mint.dag | 30 ++- dag/gunbc/runner/runner_jit_perform.dag | 5 +- .../runner_throughput_qualification.dag | 22 +- .../runner_throughput_qualification_route.dag | 18 +- .../github_app_registry_witness_test.dag | 97 +++++++-- ...runner_jit_deregistration_witness_test.dag | 82 +++----- ..._throughput_qualification_witness_test.dag | 25 +-- 10 files changed, 326 insertions(+), 153 deletions(-) diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 3f47dbfec0d..1f7806e0fd3 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -274,7 +274,9 @@ fn read_runner_groups_with(token: ControllerInstallationToken, organization: Non // Secret is shed here and nowhere else, as for the mint; the answers are classified at their own // grain -- the listing a read, the delete a mutation -- and the decision over them is the // deregistration module's, taken without I/O. -type OrganizationRunnerListRead { +// SEALED: a listing reaches a deregistration receipt only as an answer this module received from +// GitHub, never as a value a caller assembled (gunbc.runner.runner_jit_deregistration). +type OrganizationRunnerListRead sole_constructor { performance: RestExchangePerformance body: OrganizationRunnerListWire } diff --git a/dag/gunbc/runner/runner_host_unit_slot.dag b/dag/gunbc/runner/runner_host_unit_slot.dag index 95f14225dc6..9cd2719b33b 100644 --- a/dag/gunbc/runner/runner_host_unit_slot.dag +++ b/dag/gunbc/runner/runner_host_unit_slot.dag @@ -36,6 +36,7 @@ import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, epheme type HostUnitSlot sole_constructor { host: ManagedHostBinding attempt: NonEmptyStr + workflow: NonEmptyStr unit: NonEmptyStr labels: List argv: List @@ -58,7 +59,10 @@ fn host_unit_slot_listener_argv() -> List { [concat(actions_runner_base_dir, actions_runner_listener_relative_path), actions_runner_listener_run_verb] } -fn host_unit_slot_for(host: ManagedHostBindingStanding, attempt: NonEmptyStr) -> HostUnitSlotStanding { +// THE WORKFLOW IS THE CALLER'S: the slot exists to run one workflow, and the mint refuses a runner +// group restricted to any other (gunbc.runner_jit_mint jit_slot_workflow). It is spelled the way +// GitHub identifies a selected workflow, `//@`. +fn host_unit_slot_for(host: ManagedHostBindingStanding, attempt: NonEmptyStr, workflow: NonEmptyStr) -> HostUnitSlotStanding { match host { ManagedHostBound { binding: b } => match actions_runner_binary_artifact_for_arch(binary_release: host_unit_slot_runner_release, arch: Aarch64) { @@ -69,6 +73,7 @@ fn host_unit_slot_for(host: ManagedHostBindingStanding, attempt: NonEmptyStr) -> slot: HostUnitSlot { host: b, attempt: attempt, + workflow: workflow, unit: unit, labels: ephemeral_slot_labels(host: b.host.host, attempt: attempt), argv: ephemeral_slot_argv(unit: unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()), diff --git a/dag/gunbc/runner/runner_jit_deregistration.dag b/dag/gunbc/runner/runner_jit_deregistration.dag index 8cb6cc57285..b4ebb6038e7 100644 --- a/dag/gunbc/runner/runner_jit_deregistration.dag +++ b/dag/gunbc/runner/runner_jit_deregistration.dag @@ -4,15 +4,18 @@ import std.types { Bool, Int, List, NonEmptyStr, String } import std.resources { Network } import std.upsert_decision { UpsertDecision, Noop, Apply, Refuse } import extdeps.transports.rest { RestExchangePerformance, RestExchangeSucceeded } +import extdeps.github.app { GitHubAppInstallationId } import extdeps.github.effect { GitHubOrganizationRef, DeleteOrganizationRunner } import extdeps.github.actions_jit_runner { JitRunnerName } import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } -import gunbc.auth.github_apps { GitHubAppControlPlaneState } +import gunbc.auth.github_apps { DeclaredGitHubApp, GitHubAppControlPlaneState } import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, } import gunbc.runner_jit_mint { JitSlot } -import gunbc.runner.runner_jit_perform { JitMintDispatch, JitMintDispatchAuthorized, JitMintDispatchRefused } +import gunbc.runner.runner_jit_perform { + JitMintDispatch, JitMintDispatchAuthorized, JitMintDispatchRefused, authority_installation, +} import gunbc.github_effect_perform { ControllerInstallationToken, OrganizationRunnerListRead, list_runners_by_name_with, delete_runner_with, } @@ -26,18 +29,6 @@ import gunbc.github_effect_perform { // the deregistration is total over how the slot exited (JitSlotExit), records that exit in its // receipt, and decides only from what GitHub lists now. // -// THE REGISTRATION IS IDENTIFIED BY THE NAME IT WAS MINTED UNDER, which an authorized dispatch fixes -// BEFORE GitHub answers (gunbc.runner_jit_mint jit_slot_runner_name). So a mint whose answer was lost -// -- the one case where a runner may exist under an id nobody received -- still yields a -// registration this fold can find and remove. A refused dispatch minted nothing and yields none. -// -// SUCCESS IS A READBACK, NEVER A DELETE STATUS. The organization's listing filtered by the name -// (extdeps.github.actions_self_hosted_runners ListOrganizationRunnersByName) is read before and, -// when anything was removed, after; JitRunnerDeregistered is constructed only from a listing that -// does not contain the name. A 204 with the runner still listed, an ambiguous delete, or a listing -// that could not be read are each a typed refusal -- the line stops (DESIGN §5), it does not -// report the slot clean. -// // AUTHORIZATION IS THE DISPATCHING RUN'S (operator ruling 2026-10-03): registration and // deregistration are pre-approved under the fleet-converge run's own federated identity, with no // per-registration approval. That is recorded where the corpus records authorization patterns -- @@ -49,17 +40,25 @@ type JitSlotExit | SlotJobFailed { cause: NonEmptyStr } | SlotNeverLaunched { cause: NonEmptyStr } -type JitRegistration { +// THE SUBJECT IS MINTED FROM THE AUTHORIZED DISPATCH AND NOWHERE ELSE. jit_registration_of is its +// only constructor, so the runner this fold may delete is one an authorized mint named -- a caller +// cannot point the delete at an arbitrary runner name. The name is fixed by the dispatch BEFORE +// GitHub answers (gunbc.runner_jit_mint jit_slot_runner_name), so a mint whose answer was lost -- +// the one case where a runner may exist under an id nobody received -- still yields a subject. The +// App and installation are the dispatch's, so the token that deletes must be the one that minted. +type JitRegistration sole_constructor { organization: GitHubOrganizationRef name: JitRunnerName slot: JitSlot + app: DeclaredGitHubApp + installation: GitHubAppInstallationId } fn jit_registration_of(dispatch: JitMintDispatch) -> JitRegistration? { match dispatch { JitMintDispatchRefused { refusal: _ } => none - JitMintDispatchAuthorized { slot: s, app: _, installation: _, organization: o, request: r } => - Present { value: JitRegistration { organization: o, name: r.name, slot: s } } + JitMintDispatchAuthorized { slot: s, app: a, installation: i, organization: o, request: r } => + Present { value: JitRegistration { organization: o, name: r.name, slot: s, app: a, installation: i } } } } @@ -68,25 +67,29 @@ type JitRunnerListing | JitRunnerNotListed | JitRunnerListingUnreadable { performance: RestExchangePerformance, cause: NonEmptyStr } -// A PAGE SHORTER THAN total_count IS NOT A LISTING. The name filter makes more than one page -// implausible, but a runner on an unread page would read as absent, which is the one wrong answer -// this fold exists to refuse. -fn jit_runner_listing_of(name: JitRunnerName, read: OrganizationRunnerListRead) -> JitRunnerListing { - match read.performance { +// THE PURE CLASSIFIER. A page shorter than total_count is not a listing: the name filter makes more +// than one page implausible, but a runner on an unread page would read as absent, which is the one +// wrong answer this fold exists to refuse. +fn jit_runner_listing_of(name: JitRunnerName, performance: RestExchangePerformance, body: OrganizationRunnerListWire) -> JitRunnerListing { + match performance { RestExchangeSucceeded => - if count(read.body.runners) != read.body.total_count { + if count(body.runners) != body.total_count { JitRunnerListingUnreadable { - performance: read.performance, - cause: join(["runner listing returned ", to_string(count(read.body.runners)), " of total_count ", to_string(read.body.total_count)], "") as NonEmptyStr, + performance: performance, + cause: join(["runner listing returned ", to_string(count(body.runners)), " of total_count ", to_string(body.total_count)], "") as NonEmptyStr, } } else { - let ids = read.body.runners |> filter(r => r.name == (name.value as String)) |> map(r => r.id) + let ids = body.runners |> filter(r => r.name == (name.value as String)) |> map(r => r.id) if count(ids) == 0 { JitRunnerNotListed } else { JitRunnerListed { ids: ids } } } other => JitRunnerListingUnreadable { performance: other, cause: "the organization runner listing did not succeed" as NonEmptyStr } } } +fn jit_runner_listing_of_read(name: JitRunnerName, read: OrganizationRunnerListRead) -> JitRunnerListing { + jit_runner_listing_of(name: name, performance: read.performance, body: read.body) +} + fn plan_jit_deregistration(listing: JitRunnerListing) -> UpsertDecision> { match listing { JitRunnerNotListed => Noop @@ -101,38 +104,111 @@ type JitRunnerDeleteAttempt { } type JitDeregistrationRefusal - = DeregistrationCredentialRefused { unsatisfied: List } + = DeregistrationTokenForAnotherInstallation { app: DeclaredGitHubApp, installation: GitHubAppInstallationId } + | DeregistrationAuthorityForAnotherInstallation + | DeregistrationCredentialRefused { unsatisfied: List } | DeregistrationListingUnreadable { performance: RestExchangePerformance, cause: NonEmptyStr } | DeregistrationRunnerStillListed { ids: List, deletes: List } +// THE VERDICT OVER A READBACK, decided without I/O and witnessable on supplied listings. It carries +// no success payload: absence is a reading here, not a receipt. +type JitDeregistrationVerdict + = ReadbackShowsAbsent + | ReadbackRefused { refusal: JitDeregistrationRefusal } + +fn classify_jit_deregistration(readback: JitRunnerListing, deletes: List) -> JitDeregistrationVerdict { + match readback { + JitRunnerNotListed => ReadbackShowsAbsent + JitRunnerListed { ids: ids } => ReadbackRefused { refusal: DeregistrationRunnerStillListed { ids: ids, deletes: deletes } } + JitRunnerListingUnreadable { performance: p, cause: c } => ReadbackRefused { refusal: DeregistrationListingUnreadable { performance: p, cause: c } } + } +} + +// THE SUCCESS RECEIPT IS SEALED. It binds the sealed subject to the listing GitHub returned AFTER +// the deletes (or the listing that already showed the name absent), and conclude_from_readback in +// this module is its only constructor -- reachable only from ensure_jit_runner_deregistered, with a +// readback only gunbc.github_effect_perform list_runners_by_name_with produces. No delete status can +// mint it. +type JitDeregistrationReceipt sole_constructor { + registration: JitRegistration + exit: JitSlotExit + deletes: List + readback: OrganizationRunnerListRead +} + type JitDeregistration - = JitRunnerDeregistered { registration: JitRegistration, exit: JitSlotExit, deletes: List } + = JitRunnerDeregistered { receipt: JitDeregistrationReceipt } | JitDeregistrationRefused { registration: JitRegistration, exit: JitSlotExit, refusal: JitDeregistrationRefusal } -// THE CONCLUSION READS THE LISTING AND NOTHING ELSE. The delete attempts are carried into the -// receipt as evidence of what was tried, but no delete status can mint the deregistered arm. -fn conclude_jit_deregistration( +fn conclude_from_readback( registration: JitRegistration, exit: JitSlotExit, deletes: List, - readback: JitRunnerListing, + readback: OrganizationRunnerListRead, ) -> JitDeregistration { - match readback { - JitRunnerNotListed => JitRunnerDeregistered { registration: registration, exit: exit, deletes: deletes } - JitRunnerListed { ids: ids } => - JitDeregistrationRefused { registration: registration, exit: exit, refusal: DeregistrationRunnerStillListed { ids: ids, deletes: deletes } } - JitRunnerListingUnreadable { performance: p, cause: c } => - JitDeregistrationRefused { registration: registration, exit: exit, refusal: DeregistrationListingUnreadable { performance: p, cause: c } } + match classify_jit_deregistration(readback: jit_runner_listing_of_read(name: registration.name, read: readback), deletes: deletes) { + ReadbackShowsAbsent => + JitRunnerDeregistered { receipt: JitDeregistrationReceipt { registration: registration, exit: exit, deletes: deletes, readback: readback } } + ReadbackRefused { refusal: r } => JitDeregistrationRefused { registration: registration, exit: exit, refusal: r } } } fn jit_runner_deregistered(result: JitDeregistration) -> Bool { match result { - JitRunnerDeregistered { registration: _, exit: _, deletes: _ } => true + JitRunnerDeregistered { receipt: _ } => true JitDeregistrationRefused { registration: _, exit: _, refusal: _ } => false } } +// THE READBACK CONSUMES THE COMPLETED DELETES. The listing is taken inside a function whose input +// is the delete receipts, so it is causally after them by data dependency rather than by the +// accident of two sibling lets being evaluated in source order. +type DeletesThenReadback { + deletes: List + readback: OrganizationRunnerListRead +} + +fn readback_after_deletes( + token: ControllerInstallationToken, + registration: JitRegistration, + deletes: List, +) -> DeletesThenReadback uses net: Network { + DeletesThenReadback { + deletes: deletes, + readback: list_runners_by_name_with(token: token, organization: registration.organization.login, name: registration.name.value), + } +} + +// THE TOKEN AND THE AUTHORITY MUST BE THE MINT'S. A token minted for another App or installation is +// refused before any request, as gunbc.github_effect_perform perform_organization_jit_mint refuses +// one for the mint; so is an authority naming another installation, because its capability check +// would then be about a credential that is not the one performing the delete. +fn deregistration_credential_refusal( + token: ControllerInstallationToken, + authority: GitHubCredentialAuthority, + control_plane: GitHubAppControlPlaneState, + registration: JitRegistration, +) -> JitDeregistrationRefusal? { + let effect = DeleteOrganizationRunner { organization: registration.organization } + if token.app != registration.app || token.installation != registration.installation { + Present { value: DeregistrationTokenForAnotherInstallation { app: token.app, installation: token.installation } } + } else { + match authority_installation(authority: authority) { + Absent => Present { value: DeregistrationAuthorityForAnotherInstallation } + Present { value: held } => + if held.app != registration.app || held.installation != registration.installation { + Present { value: DeregistrationAuthorityForAnotherInstallation } + } else if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { + Present { value: DeregistrationCredentialRefused { + unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect), + } } + } else { + none + } + } + } +} + fn ensure_jit_runner_deregistered( token: ControllerInstallationToken, authority: GitHubCredentialAuthority, @@ -140,25 +216,22 @@ fn ensure_jit_runner_deregistered( registration: JitRegistration, exit: JitSlotExit, ) -> JitDeregistration uses net: Network { - let effect = DeleteOrganizationRunner { organization: registration.organization } - if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { - JitDeregistrationRefused { - registration: registration, - exit: exit, - refusal: DeregistrationCredentialRefused { - unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect), - }, - } - } else { - let org = registration.organization.login - let before = jit_runner_listing_of(name: registration.name, read: list_runners_by_name_with(token: token, organization: org, name: registration.name.value)) - match plan_jit_deregistration(listing: before) { - Noop => conclude_jit_deregistration(registration: registration, exit: exit, deletes: [], readback: before) - Refuse { reason: _ } => conclude_jit_deregistration(registration: registration, exit: exit, deletes: [], readback: before) - Apply { plan: ids } => { - let deletes = ids |> map(id => JitRunnerDeleteAttempt { runner_id: id, performance: delete_runner_with(token: token, organization: org, runner_id: id) }) - let after = jit_runner_listing_of(name: registration.name, read: list_runners_by_name_with(token: token, organization: org, name: registration.name.value)) - conclude_jit_deregistration(registration: registration, exit: exit, deletes: deletes, readback: after) + match deregistration_credential_refusal(token: token, authority: authority, control_plane: control_plane, registration: registration) { + Present { value: r } => JitDeregistrationRefused { registration: registration, exit: exit, refusal: r } + Absent => { + let org = registration.organization.login + let before = list_runners_by_name_with(token: token, organization: org, name: registration.name.value) + match plan_jit_deregistration(listing: jit_runner_listing_of_read(name: registration.name, read: before)) { + Noop => conclude_from_readback(registration: registration, exit: exit, deletes: [], readback: before) + Refuse { reason: _ } => conclude_from_readback(registration: registration, exit: exit, deletes: [], readback: before) + Apply { plan: ids } => { + let after = readback_after_deletes( + token: token, + registration: registration, + deletes: ids |> map(id => JitRunnerDeleteAttempt { runner_id: id, performance: delete_runner_with(token: token, organization: org, runner_id: id) }), + ) + conclude_from_readback(registration: registration, exit: exit, deletes: after.deletes, readback: after.readback) + } } } } diff --git a/dag/gunbc/runner/runner_jit_mint.dag b/dag/gunbc/runner/runner_jit_mint.dag index 5233ae40ddb..c485c8ec8d5 100644 --- a/dag/gunbc/runner/runner_jit_mint.dag +++ b/dag/gunbc/runner/runner_jit_mint.dag @@ -9,7 +9,7 @@ import extdeps.github.actions_jit_runner { } import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, RunnerGroupRestrictedToWorkflow, RunnerGroupRestrictionRefused, - RunnerGroupRestrictionRefusal, + RunnerGroupRestrictionRefusal, microvm_shakedown_selected_workflow, } import gunbc.runner.runner_jit_admission { attempt_binding_admits_mint_name, JitAdmissionAxisStanding, @@ -68,6 +68,7 @@ type JitMintPlan } | JitMintRefusedAttemptUnbound { cause: String } | JitMintRefusedRunnerGroupUnrestricted { group: NonEmptyStr, cause: RunnerGroupRestrictionRefusal } + | JitMintRefusedRunnerGroupForAnotherWorkflow { group: NonEmptyStr, selected: NonEmptyStr, intended: NonEmptyStr } // THE RUNNER NAME IS THE UNIT NAME, not a fresh identifier minted beside it. A second naming scheme // for one attempt is a nickname (DESIGN §3), and it is the expensive kind: an orphaned GitHub runner @@ -117,6 +118,19 @@ fn jit_slot_runner_name(slot: JitSlot) -> JitRunnerName { } } +// THE WORKFLOW A SLOT EXISTS TO RUN. A restricted group is observed restricted to ONE workflow +// (gunbc.runner_group_restriction_observation RestrictedRunnerGroup selected_workflow), and a slot +// registered into a group restricted to a different workflow would take that workflow's jobs and +// never its own. So the slot carries its intended workflow -- the microVM slot's is the shakedown's, +// derived; a host-unit slot's is its caller's -- and the mint compares the two before it uses the +// group id at all. +fn jit_slot_workflow(slot: JitSlot) -> NonEmptyStr { + match slot { + MicroVmJitSlot { attempt: _ } => microvm_shakedown_selected_workflow() + HostUnitJitSlot { slot: h } => h.workflow + } +} + fn jit_slot_registration_labels(slot: JitSlot) -> List { match slot { MicroVmJitSlot { attempt: a } => microvm_attempt_registration_labels(attempt: a) @@ -186,9 +200,17 @@ fn plan_jit_mint( RunnerGroupRestrictionRefused { name: n, cause: c } => JitMintRefusedRunnerGroupUnrestricted { group: n, cause: c } RunnerGroupRestrictedToWorkflow { restriction: r } => - JitMintAuthorized { - slot: slot, - request: jit_config_request_for(slot: slot, runner_group_id: r.group), + if (r.selected_workflow as String) != (jit_slot_workflow(slot: slot) as String) { + JitMintRefusedRunnerGroupForAnotherWorkflow { + group: r.name, + selected: r.selected_workflow, + intended: jit_slot_workflow(slot: slot), + } + } else { + JitMintAuthorized { + slot: slot, + request: jit_config_request_for(slot: slot, runner_group_id: r.group), + } } } } else { diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 350cfecec4d..7d0d0f9b895 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -21,7 +21,7 @@ import gunbc.runner.runner_host_unit_slot { HostUnitSlotRefusal } import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, RunnerGroupRestrictionRefusal } import gunbc.runner_jit_mint { JitSlotBinding, JitSlot, JitMintRefusedHostUnitSlot, - JitMintPlan, JitMintAuthorized, JitMintRefusedCellNotOwned, JitMintRefusedCredential, + JitMintPlan, JitMintAuthorized, JitMintRefusedRunnerGroupForAnotherWorkflow, JitMintRefusedCellNotOwned, JitMintRefusedCredential, JitMintRefusedAttemptUnbound, JitMintRefusedRunnerGroupUnrestricted, plan_jit_mint, } @@ -76,6 +76,7 @@ type JitDispatchRefusal } | DispatchRefusedAttemptUnbound { cause: String } | DispatchRefusedRunnerGroupUnrestricted { group: NonEmptyStr, cause: RunnerGroupRestrictionRefusal } + | DispatchRefusedRunnerGroupForAnotherWorkflow { group: NonEmptyStr, selected: NonEmptyStr, intended: NonEmptyStr } | DispatchRefusedNoInstallationCredential { cause: String } // THE FOURTH REFUSAL IS A TOTALITY ARM AND IS DELIBERATELY NOT ENROLLED AS COVERAGE. @@ -141,6 +142,8 @@ fn dispatch_jit_mint( JitMintDispatchRefused { refusal: DispatchRefusedAttemptUnbound { cause: c } } JitMintRefusedRunnerGroupUnrestricted { group: g, cause: c } => JitMintDispatchRefused { refusal: DispatchRefusedRunnerGroupUnrestricted { group: g, cause: c } } + JitMintRefusedRunnerGroupForAnotherWorkflow { group: g, selected: s, intended: i } => + JitMintDispatchRefused { refusal: DispatchRefusedRunnerGroupForAnotherWorkflow { group: g, selected: s, intended: i } } JitMintRefusedCredential { organization: org, unsatisfied: u } => JitMintDispatchRefused { refusal: DispatchRefusedCredential { organization: org, unsatisfied: u }, diff --git a/dag/gunbc/runner/runner_throughput_qualification.dag b/dag/gunbc/runner/runner_throughput_qualification.dag index a7b22e07b44..1f3f46827a3 100644 --- a/dag/gunbc/runner/runner_throughput_qualification.dag +++ b/dag/gunbc/runner/runner_throughput_qualification.dag @@ -928,17 +928,33 @@ fn assessment_holds

(a: RunnerThroughputAssessment

) -> Bool { // route's collect stage parsing a real run's rows into `observed_run` -- is a frontier carried as // one (DESIGN section 3c). A DeclarationRef to its landing would stop the parse phase today, so the // leg names the capability the landing must be SUFFICIENT FOR (section 4b(3)). +// +// AN AUTHORITY EXISTING IS NOT A LEG BEING WIRED. A leg whose authority is implemented but has no +// production caller on the route performs nothing when the route runs, so it is its own arm: +// LegAuthorityImplemented names the authority AND the wiring still owed, at capability grain. Only +// LegWired counts toward route_is_executable; reading an implemented authority as a wired leg is +// the specification-without-execution DESIGN §5 forbids, one layer up. type RouteLegStanding - = LegBoundToAuthority { authority: DeclarationRef } + = LegWired { authority: DeclarationRef } + | LegAuthorityImplemented { authority: DeclarationRef, wiring_owed: NonEmptyStr } | LegAwaitingAuthority { lands_with: NonEmptyStr, sufficient_for: NonEmptyStr } -fn leg_is_bound(leg: RouteLegStanding) -> Bool { +fn leg_is_wired(leg: RouteLegStanding) -> Bool { match leg { - LegBoundToAuthority { authority: _ } => true + LegWired { authority: _ } => true + LegAuthorityImplemented { authority: _, wiring_owed: _ } => false LegAwaitingAuthority { lands_with: _, sufficient_for: _ } => false } } +fn leg_authority(leg: RouteLegStanding) -> DeclarationRef? { + match leg { + LegWired { authority: a } => Present { value: a } + LegAuthorityImplemented { authority: a, wiring_owed: _ } => Present { value: a } + LegAwaitingAuthority { lands_with: _, sufficient_for: _ } => none + } +} + data qualification_run_producer_frontier: RouteLegStanding = LegAwaitingAuthority { lands_with: "a collect stage on the qualification route that parses the run's [floor-phase], [floor-cgroup] and calibration [witness] rows, the unit's effective properties and the guest's idle meminfo/nproc into observed_run, with admit_callers on observed_run naming it, AND reads every seat's ready/start/end instants and settlement plus the barrier and host window into a product.capacity.cohort_observation CohortReading, seals it with seal_cohort and passes it to assess_runner_throughput as CohortObserved (gunbc.runner_throughput_qualification_route ConcurrentCohortSeatWindows); until then assess_runner_throughput has no production caller and a delivery claim refuses ConcurrencyUnobserved; it needs a runner to have run, so it follows gunbc.runner_throughput_qualification_route mtcollins1_boot_leg" as NonEmptyStr, sufficient_for: "minting ObservedRunnerQualificationRuns and a sealed concurrent cohort of them from a real run of the floor on mtcollins1, so assess_runner_throughput is fed by execution rather than by a fixture" as NonEmptyStr, diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index b520e896640..0cc063de7a4 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -13,7 +13,7 @@ import gunbc.runner_slot_allocation { runner_host_label, runner_label_set_union import gunbc.runner_slot_desired { RunnerSlotDesired, RunnerSlotCpuQuota, CpuQuotaResolved, SlotCpuQuotaUnbounded, gunbc_runner_slot_desired } import gunbc.runner_throughput_qualification { WorkspaceBacking, RamBackedTmpfs, FloorWorkloadPin, - RouteLegStanding, LegBoundToAuthority, LegAwaitingAuthority, leg_is_bound, + RouteLegStanding, LegWired, LegAuthorityImplemented, LegAwaitingAuthority, leg_is_wired, } // THE EXECUTION ROUTE IS A TYPED STAGE PLAN, AND EVERY STAGE SAYS WHAT IT MUTATES AND UNDER WHOM. @@ -34,7 +34,7 @@ import gunbc.runner_throughput_qualification { // the boot run on main (`gunbc.machine_intake_mtcollins1_boot_run`) boots exactly two media, the // census image and the stock installer, and its terminal is the census program's, so it cannot boot a // runner and observe it ready. `extdeps.github.workflows CreateDispatch` has no production caller -// and a heal-specific input contract. The registration and deregistration legs are bound. Each unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR +// and a heal-specific input contract. The registration and deregistration legs have implemented authorities that nothing on the route calls yet. Each unbound leg names the CAPABILITY missing now and what it must be SUFFICIENT FOR // (section 4b(3)), never a pull request, because a pull request can merge while the capability stays // missing -- which is exactly what happened to the boot leg. @@ -213,11 +213,13 @@ data mtcollins1_boot_leg: RouteLegStanding = LegAwaitingAuthority { sufficient_for: "booting mtcollins1 from the derived runner image under grant_from_store with the SOL collector attached before the BMC stimulus, no controller write on deny, expiry or an unfiled request, and a terminal that says the runner host is up" as NonEmptyStr, } -data mtcollins1_image_leg: RouteLegStanding = LegBoundToAuthority { +data mtcollins1_image_leg: RouteLegStanding = LegWired { authority: decl_ref(module_path: "extdeps.provisioning.ubuntu_seeded_install_media", decl_name: "UbuntuSeededInstallMediaBuildInput"), } -// THE THREE GITHUB CONTROL-PLANE LEGS. Registration is the one JIT mint, taken over the slot sum +// THE THREE GITHUB CONTROL-PLANE LEGS. Registration and deregistration have implemented +// authorities but no production caller on this route yet, so they are LegAuthorityImplemented and +// each names the wiring it still owes. Registration is the one JIT mint, taken over the slot sum // whose host-unit arm (gunbc.runner.runner_host_unit_slot host_unit_slot_for) is this route's own // ephemeral_slot_unit / ephemeral_slot_argv / ephemeral_slot_labels on a gunbc.managed_host host. // Deregistration is NOT left to GitHub's ephemeral removal, which holds only when the one job ran: @@ -226,8 +228,9 @@ data mtcollins1_image_leg: RouteLegStanding = LegBoundToAuthority { // RunnerTeardownRequest over a host-unit inventory. Both are pre-approved under the dispatching // run's authorization (operator ruling 2026-10-03), rowed in gunbc.auth.privileged_effect_census. // Dispatch names the cited REST operation that has no production caller yet. -data mtcollins1_registration_leg: RouteLegStanding = LegBoundToAuthority { +data mtcollins1_registration_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), + wiring_owed: "a production fold on this route that binds gunbc.runner.runner_host_unit_slot host_unit_slot_for to the booted managed host, mints under an observed runner group restricted to the qualification workflow and performs it through gunbc.github_effect_perform perform_organization_jit_mint; SUFFICIENT FOR one ephemeral registration per attempt label actually existing in the organization when the route runs" as NonEmptyStr, } data mtcollins1_dispatch_leg: RouteLegStanding = LegAwaitingAuthority { @@ -235,8 +238,9 @@ data mtcollins1_dispatch_leg: RouteLegStanding = LegAwaitingAuthority { sufficient_for: "dispatching the floor to the one registration whose runs-on names the attempt label, so the run is unservicable by any other runner" as NonEmptyStr, } -data mtcollins1_deregistration_leg: RouteLegStanding = LegBoundToAuthority { +data mtcollins1_deregistration_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), + wiring_owed: "the same route fold calling ensure_jit_runner_deregistered on every slot exit with the registration jit_registration_of minted from the registration leg's dispatch, joined to a host-unit gunbc.runner.runner_teardown_receipt RunnerTeardownResult; SUFFICIENT FOR the route reporting a slot retired only when the runner is absent from the organization listing and the unit's resources are observed gone" as NonEmptyStr, } // THE FLEET-CONVERGE MODE THIS ROUTE BECOMES IS NOT NAMED HERE. A mode row belongs to @@ -345,7 +349,7 @@ fn route_bmc_writes_are_gated(route: List) -> Bool { // THE ROUTE IS EXECUTABLE ONLY WHEN EVERY LEG UNDER EVERY EFFECTFUL STAGE IS BOUND. fn route_is_executable(route: List) -> Bool { - route |> all(s => stage_legs(stage: s) |> all(l => leg_is_bound(leg: l))) + route |> all(s => stage_legs(stage: s) |> all(l => leg_is_wired(leg: l))) } fn route_registers_one_slot(route: List) -> Bool { diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index fe4b150ca85..3e7b43286d8 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -33,7 +33,7 @@ import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis, AxisDeferred, } import gunbc.runner_jit_mint { - JitSlotBinding, MicroVmSlotBinding, MicroVmJitSlot, HostUnitSlotBinding, HostUnitJitSlot, JitMintRefusedHostUnitSlot, + JitSlotBinding, MicroVmSlotBinding, MicroVmJitSlot, HostUnitSlotBinding, HostUnitJitSlot, JitMintRefusedHostUnitSlot, JitMintRefusedRunnerGroupForAnotherWorkflow, JitMintPlan, JitMintAuthorized, JitMintRefusedCellNotOwned, JitMintRefusedCredential, JitMintRefusedAttemptUnbound, JitMintRefusedRunnerGroupUnrestricted, plan_jit_mint, @@ -43,7 +43,7 @@ import extdeps.transports.rest { RestOk } import extdeps.github.org_actions { OrgRunnerGroupWire, OrgRunnerGroupListWire } import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, RunnerGroupRestrictionNotYetObserved, - observe_shakedown_runner_group_restriction, runner_group_restriction_not_yet_observed, + observe_shakedown_runner_group_restriction, observe_runner_group_restriction, runner_group_restriction_not_yet_observed, microvm_shakedown_selected_workflow, microvm_shakedown_runner_group_name, } import gunbc.runner.runner_jit_perform { @@ -58,11 +58,12 @@ import gunbc.runner.runner_jit_perform { import extdeps.uri { Uri, uri_https } import extdeps.auth.jws { JwsSigningKeyRef, HostKeyFile } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import gunbc.managed_host { managed_hosts, managed_host_binding, ManagedHostAccessUnobserved } +import gunbc.managed_host { ManagedHost, managed_hosts, managed_host_binding, ManagedHostAccessUnobserved } import extdeps.bmc.endpoint { BmcControllerEndpoint } import extdeps.github.actions_runner { ActionsRunnerLinuxArm64 } import gunbc.runner_slot_desired { gunbc_runner_slot_desired } import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_labels, ephemeral_slot_argv } +import gunbc.runner.runner_jit_deregistration { jit_registration_of } import gunbc.runner.runner_host_unit_slot { HostUnitSlotStanding, HostUnitSlotRefused, HostUnitSlotHostNotBound, host_unit_slot_for, host_unit_slot_listener_argv, } @@ -1312,15 +1313,41 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool // ── THE HOST-UNIT ARM OF THE SAME MINT ──────────────────────────────────────────────────────── // // One plan over the slot sum (gunbc.runner_jit_mint JitSlotBinding), so these claims reuse the -// microVM claims' credential, control-plane and group fixtures unchanged: a fork of the mint per -// binding would need fixtures of its own, and that is what these claims would stop compiling on. -fn host_unit_planned(standing: HostUnitSlotStanding, perms: List) -> JitMintPlan { +// microVM claims' credential and control-plane fixtures unchanged: a fork of the mint per binding +// would need fixtures of its own, and that is what these claims would stop compiling on. +data qualification_workflow: NonEmptyStr = "gunb-ai/gunbc/.github/workflows/runner-qualification.yml@refs/heads/main" + +// The group the slot's workflow is observed restricted to, through the same sealed observer. +fn group_restricted_to(workflow: NonEmptyStr) -> MicrovmRunnerGroupStanding { + observe_runner_group_restriction( + name: "runner-qualification", + selected_workflow: workflow, + listed: runner_groups_of_rest_read(outcome: RestOk, body: OrgRunnerGroupListWire { + total_count: 1, + runner_groups: [OrgRunnerGroupWire { + id: 3, + name: "runner-qualification", + visibility: "selected", + allows_public_repositories: false, + restricted_to_workflows: true, + selected_workflows: [workflow as String], + workflow_restrictions_read_only: false, + }], + }), + ) +} + +fn host_unit_slot(h: ManagedHost) -> HostUnitSlotStanding { + host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1", workflow: qualification_workflow) +} + +fn host_unit_planned(standing: HostUnitSlotStanding, perms: List, group: MicrovmRunnerGroupStanding) -> JitMintPlan { plan_jit_mint( binding: HostUnitSlotBinding { standing: standing }, authority: app_credential, control_plane: observed_grant(perms: perms), organization: gunbai_org_ref, - runner_group: restricted_group_1(), + runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), ) } @@ -1328,14 +1355,14 @@ fn host_unit_planned(standing: HostUnitSlotStanding, perms: List Bool { let hosts = managed_hosts() count(hosts) > 0 && hosts |> all(h => - match host_unit_planned(standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1"), perms: runner_grant) { + match host_unit_planned(standing: host_unit_slot(h: h), perms: runner_grant, group: group_restricted_to(workflow: qualification_workflow)) { JitMintAuthorized { slot: HostUnitJitSlot { slot: u }, request: req } => (req.name.value as String) == (ephemeral_slot_unit(host: h.host, attempt: "q-1") as String) + && req.runner_group_id.value == 3 && runner_label_sets_equal(a: map(req.labels, l => l.value as String), b: ephemeral_slot_labels(host: h.host, attempt: "q-1")) && u.argv == ephemeral_slot_argv(unit: u.unit, desired: gunbc_runner_slot_desired(), command_argv: host_unit_slot_listener_argv()) && match u.runner.arch { ActionsRunnerLinuxArm64 => true _ => false } @@ -1343,11 +1370,40 @@ test fn witness_jit_mint_authorizes_a_managed_host_unit_slot_under_the_routes_ow }) } +// RED: A GROUP RESTRICTED TO ANOTHER WORKFLOW. The shakedown's group is a perfectly authorized +// restriction -- for the shakedown. A host-unit slot that exists to run the qualification workflow +// must not register into it, and a microVM slot (whose workflow IS the shakedown's) still does: the +// comparison is the slot's workflow against the group's, not a property of either alone. +test fn witness_jit_mint_refuses_a_group_restricted_to_another_workflow() -> Bool { + let host_refused = managed_hosts() |> all(h => + match host_unit_planned(standing: host_unit_slot(h: h), perms: runner_grant, group: restricted_group_1()) { + JitMintRefusedRunnerGroupForAnotherWorkflow { group: _, selected: s, intended: i } => + (s as String) == (microvm_shakedown_selected_workflow() as String) && (i as String) == (qualification_workflow as String) + _ => false + }) + let microvm_into_qualification_refused = match plan_jit_mint( + binding: jit_binding(owned: true), + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_workflow), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintRefusedRunnerGroupForAnotherWorkflow { group: _, selected: _, intended: _ } => true + _ => false + } + count(managed_hosts()) > 0 && host_refused && microvm_into_qualification_refused +} + // RED: A HOST THE MANAGED-HOST SEAM DOES NOT BIND HAS NO SLOT, and the refusal carries the seam's own // standing rather than a re-coined cause -- even with a fully sufficient credential. test fn witness_jit_mint_refuses_a_host_the_managed_host_seam_does_not_bind() -> Bool { let unbound = ManagedHostAccessUnobserved { endpoint: BmcControllerEndpoint { host: "fixture-bmc" } } - match host_unit_planned(standing: host_unit_slot_for(host: unbound, attempt: "q-1"), perms: runner_grant) { + match host_unit_planned( + standing: host_unit_slot_for(host: unbound, attempt: "q-1", workflow: qualification_workflow), + perms: runner_grant, + group: group_restricted_to(workflow: qualification_workflow), + ) { JitMintRefusedHostUnitSlot { refusal: HostUnitSlotHostNotBound { standing: ManagedHostAccessUnobserved { endpoint: e } } } => (e.host as String) == "fixture-bmc" _ => false @@ -1358,9 +1414,26 @@ test fn witness_jit_mint_refuses_a_host_the_managed_host_seam_does_not_bind() -> // organization self-hosted-runners write refuses exactly as an owned microVM cell does. test fn witness_a_host_unit_slot_passes_through_the_same_credential_gate() -> Bool { managed_hosts() |> all(h => - match host_unit_planned(standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1"), perms: full_workflow_grant) { + match host_unit_planned(standing: host_unit_slot(h: h), perms: full_workflow_grant, group: group_restricted_to(workflow: qualification_workflow)) { JitMintRefusedCredential { organization: org, unsatisfied: u } => (org as String) == "gunb-ai" && count(u) > 0 _ => false }) } +// THE DEREGISTRATION SUBJECT IS MINTED FROM THE AUTHORIZED DISPATCH ONLY: a refused dispatch yields +// no subject, and an authorized one yields exactly the name, App and installation it dispatched. +test fn witness_a_deregistration_subject_exists_only_for_an_authorized_dispatch() -> Bool { + let refused_none = match jit_registration_of(dispatch: dispatched(owned: false, perms: runner_grant)) { + Absent => true + Present { value: _ } => false + } + refused_none && match dispatched(owned: true, perms: runner_grant) { + JitMintDispatchAuthorized { slot: _, app: app, installation: i, organization: _, request: req } => + match jit_registration_of(dispatch: dispatched(owned: true, perms: runner_grant)) { + Present { value: reg } => + (reg.name.value as String) == (req.name.value as String) && reg.app == app && reg.installation == i + Absent => false + } + _ => false + } +} diff --git a/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag b/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag index 5c29610fc0c..e3e182054ba 100644 --- a/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag +++ b/dag/test/claim/runner/runner_jit_deregistration_witness_test.dag @@ -4,62 +4,34 @@ import std.types { Bool, Int, List, NonEmptyStr, String } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.upsert_decision { Noop, Apply, Refuse } import extdeps.transports.rest { RestExchangeSucceeded, RestExchangeStatusRefused, RestExchangeCommitAmbiguous, RestExchangeUnreached } -import extdeps.github.effect { GitHubOrganizationRef } import extdeps.github.actions_jit_runner { JitRunnerName } import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire, OrganizationRunnerWire } -import gunbc.github_effect_perform { OrganizationRunnerListRead } -import gunbc.managed_host { managed_hosts, managed_host_binding } -import gunbc.runner.runner_host_unit_slot { host_unit_slot_for, HostUnitSlotBound } -import gunbc.runner_jit_mint { HostUnitJitSlot } import gunbc.runner.runner_jit_deregistration { - JitRegistration, JitSlotExit, SlotJobCompleted, SlotJobFailed, SlotNeverLaunched, JitRunnerListing, JitRunnerListed, JitRunnerNotListed, JitRunnerListingUnreadable, - JitRunnerDeleteAttempt, JitDeregistration, JitRunnerDeregistered, JitDeregistrationRefused, + JitRunnerDeleteAttempt, ReadbackShowsAbsent, ReadbackRefused, DeregistrationRunnerStillListed, DeregistrationListingUnreadable, - jit_runner_listing_of, plan_jit_deregistration, conclude_jit_deregistration, jit_runner_deregistered, + jit_runner_listing_of, plan_jit_deregistration, classify_jit_deregistration, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly data runner_name: JitRunnerName = JitRunnerName { value: "gunbc-qualification-mtcollins1-q-1.service" } -fn listing_read(runners: List, total: Int) -> OrganizationRunnerListRead { - OrganizationRunnerListRead { performance: RestExchangeSucceeded, body: OrganizationRunnerListWire { total_count: total, runners: runners } } +fn listing(runners: List, total: Int) -> JitRunnerListing { + jit_runner_listing_of(name: runner_name, performance: RestExchangeSucceeded, body: OrganizationRunnerListWire { total_count: total, runners: runners }) } -fn registrations() -> List { - managed_hosts() |> flat_map(h => - match host_unit_slot_for(host: managed_host_binding(host: h), attempt: "q-1") { - HostUnitSlotBound { slot: s } => - [JitRegistration { organization: GitHubOrganizationRef { login: "gunb-ai" }, name: runner_name, slot: HostUnitJitSlot { slot: s } }] - _ => [] - }) -} - -data every_exit: List = [ - SlotJobCompleted, - SlotJobFailed { cause: "the floor job failed" }, - SlotNeverLaunched { cause: "the unit never started" }, -] - // THE LISTING IS AN IDENTITY JOIN ON THE MINTED NAME: a different runner in the page is not this -// registration, and a page shorter than total_count is unreadable rather than absent. +// registration, and a page shorter than total_count -- or a listing that did not succeed -- is +// unreadable rather than absent. test fn the_listing_finds_the_registration_by_name_and_refuses_a_short_page() -> Bool { let other = OrganizationRunnerWire { id: 7, name: "some-other-runner" } let mine = OrganizationRunnerWire { id: 42, name: runner_name.value as String } - (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other], total: 1)) { JitRunnerNotListed => true _ => false }) - && (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other, mine], total: 2)) { - JitRunnerListed { ids: ids } => ids == [42] - _ => false - }) - && (match jit_runner_listing_of(name: runner_name, read: listing_read(runners: [other], total: 2)) { - JitRunnerListingUnreadable { performance: _, cause: _ } => true - _ => false - }) - && (match jit_runner_listing_of(name: runner_name, read: OrganizationRunnerListRead { - performance: RestExchangeUnreached { cause: "fixture: connection refused" }, - body: OrganizationRunnerListWire { total_count: 0, runners: [] }, - }) { + (match listing(runners: [other], total: 1) { JitRunnerNotListed => true _ => false }) + && (match listing(runners: [other, mine], total: 2) { JitRunnerListed { ids: ids } => ids == [42] _ => false }) + && (match listing(runners: [other], total: 2) { JitRunnerListingUnreadable { performance: _, cause: _ } => true _ => false }) + && (match jit_runner_listing_of(name: runner_name, performance: RestExchangeUnreached { cause: "fixture: connection refused" }, + body: OrganizationRunnerListWire { total_count: 0, runners: [] }) { JitRunnerListingUnreadable { performance: _, cause: _ } => true _ => false }) @@ -74,21 +46,23 @@ test fn the_plan_is_an_ensure_over_the_listing() -> Bool { }) } -// SUCCESS IS THE READBACK AND ONLY THE READBACK, ON EVERY EXIT. A 204 with the runner still listed -// refuses; an ambiguous delete whose readback shows it gone succeeds; an unreadable readback refuses; -// and none of that depends on how the slot exited. -test fn deregistration_succeeds_only_on_an_absent_readback_on_every_exit() -> Bool { +// ABSENCE IS THE READBACK AND ONLY THE READBACK. A successful delete with the runner still listed +// refuses; an ambiguous delete whose readback shows the name gone reads absent; an unreadable +// readback refuses. The verdict carries no success payload -- the sealed receipt is minted only by +// the effectful ensure from a listing GitHub returned. +test fn the_verdict_reads_absent_only_from_an_absent_readback() -> Bool { let deleted_ok = [JitRunnerDeleteAttempt { runner_id: 42, performance: RestExchangeSucceeded }] let deleted_ambiguous = [JitRunnerDeleteAttempt { runner_id: 42, performance: RestExchangeCommitAmbiguous { detail: "fixture: 502 after the write" } }] - count(registrations()) > 0 && registrations() |> all(reg => every_exit |> all(exit => - jit_runner_deregistered(result: conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ambiguous, readback: JitRunnerNotListed)) - && (match conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ok, readback: JitRunnerListed { ids: [42] }) { - JitDeregistrationRefused { registration: _, exit: _, refusal: DeregistrationRunnerStillListed { ids: ids, deletes: _ } } => ids == [42] - _ => false - }) - && (match conclude_jit_deregistration(registration: reg, exit: exit, deletes: deleted_ok, - readback: JitRunnerListingUnreadable { performance: RestExchangeStatusRefused { status: 403, body: "fixture" }, cause: "fixture" }) { - JitDeregistrationRefused { registration: _, exit: _, refusal: DeregistrationListingUnreadable { performance: _, cause: _ } } => true - _ => false - }))) + (match classify_jit_deregistration(readback: JitRunnerNotListed, deletes: deleted_ambiguous) { ReadbackShowsAbsent => true _ => false }) + && (match classify_jit_deregistration(readback: JitRunnerListed { ids: [42] }, deletes: deleted_ok) { + ReadbackRefused { refusal: DeregistrationRunnerStillListed { ids: ids, deletes: _ } } => ids == [42] + _ => false + }) + && (match classify_jit_deregistration( + readback: JitRunnerListingUnreadable { performance: RestExchangeStatusRefused { status: 403, body: "fixture" }, cause: "fixture" }, + deletes: deleted_ok, + ) { + ReadbackRefused { refusal: DeregistrationListingUnreadable { performance: _, cause: _ } } => true + _ => false + }) } diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index e20d3e49c61..598c0825c13 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -58,7 +58,7 @@ import gunbc.runner_throughput_qualification { SlotWidthDerived, SlotWidthUnderivable, ThroughputRefusal, SubjectJoinDidNotHold, SamplingDidNotHold, RunnerThroughputAssessment, assess_runner_throughput, assessment_holds, assessment_wire, - qualification_run_producer_frontier, leg_is_bound, RouteLegStanding, LegBoundToAuthority, + qualification_run_producer_frontier, leg_is_wired, RouteLegStanding, LegAuthorityImplemented, AssessedFact, InterpreterRateAgainstPin, SlotMemoryEnvelope, PhaseWallsAgainstSuppliedCeilings, ConcurrentCohortAtWidth, RunnerCohortEvidence, NoCohortObserved, CohortObserved, run_key, } @@ -750,31 +750,32 @@ test fn the_route_classifies_every_mutation_at_its_grain() -> Bool { // THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the boot and dispatch legs are still // declared frontiers. This flips to the regression control that the bindings stay when they land. test fn the_route_is_not_executable_while_any_effectful_leg_is_unbound() -> Bool { - !route_is_executable(route: route()) && !leg_is_bound(leg: qualification_run_producer_frontier) + !route_is_executable(route: route()) && !leg_is_wired(leg: qualification_run_producer_frontier) } // THE IMAGE LEG IS BOUND AND THE BOOT LEG IS NOT, and the claim tells the two apart: binding the // boot leg to the census-only boot run, or unbinding the image leg, turns this red. test fn the_image_leg_is_bound_and_the_boot_leg_awaits_a_runner_medium() -> Bool { - leg_is_bound(leg: mtcollins1_route_legs.image) && !leg_is_bound(leg: mtcollins1_route_legs.boot) + leg_is_wired(leg: mtcollins1_route_legs.image) && !leg_is_wired(leg: mtcollins1_route_legs.boot) } -// REGISTRATION AND DEREGISTRATION ARE BOUND, AND TO THE AUTHORITIES THAT PERFORM THEM -- by -// declaration identity, so pointing either leg at some other declaration turns this red -- while the -// route stays unexecutable: boot and dispatch are still awaiting authority, and the claim above -// keeps route_is_executable false until they bind. +// REGISTRATION AND DEREGISTRATION HAVE IMPLEMENTED AUTHORITIES AND ARE NOT WIRED -- by declaration +// identity, so pointing either leg at some other declaration, or promoting either to LegWired while +// nothing on the route calls it, turns this red -- and the route stays unexecutable. fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> Bool { match leg { - LegBoundToAuthority { authority: a } => (a.module_path as String) == module_path && (a.decl_name as String) == decl_name + LegAuthorityImplemented { authority: a, wiring_owed: _ } => (a.module_path as String) == module_path && (a.decl_name as String) == decl_name _ => false } } -test fn the_registration_and_deregistration_legs_are_bound_and_the_route_still_is_not() -> Bool { +test fn the_registration_and_deregistration_legs_are_implemented_not_wired_and_the_route_is_not_executable() -> Bool { leg_names(leg: mtcollins1_route_legs.registration, module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint") && leg_names(leg: mtcollins1_route_legs.deregistration, module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered") - && !leg_is_bound(leg: mtcollins1_route_legs.boot) - && !leg_is_bound(leg: mtcollins1_route_legs.dispatch) + && !leg_is_wired(leg: mtcollins1_route_legs.registration) + && !leg_is_wired(leg: mtcollins1_route_legs.deregistration) + && !leg_is_wired(leg: mtcollins1_route_legs.boot) + && !leg_is_wired(leg: mtcollins1_route_legs.dispatch) && !route_is_executable(route: route()) } @@ -809,7 +810,7 @@ test fn only_the_runner_stages_open_the_network_and_only_default_drop() -> Bool // windows and cites product.capacity.cohort_observation seal_cohort as their producer, and the one // collect-stage frontier stays unbound until the stage that seals a real cohort lands. test fn the_collect_stage_owes_a_sealed_concurrent_cohort() -> Bool { - !leg_is_bound(leg: qualification_run_producer_frontier) + !leg_is_wired(leg: qualification_run_producer_frontier) && (route() |> any(s => match s { CollectInstruments { instruments: is } => From ccad694a9c936bb696125e5a9a2ce3b0cb6ef166 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 4 Oct 2026 05:17:45 +0000 Subject: [PATCH 07/44] Image leg is LegAuthorityImplemented: no production caller builds the derived runner image until the untangle 4a runner medium Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner_throughput_qualification_route.dag | 12 +++++++----- ...unner_throughput_qualification_witness_test.dag | 14 +++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index 0cc063de7a4..00d772b35cb 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -13,7 +13,7 @@ import gunbc.runner_slot_allocation { runner_host_label, runner_label_set_union import gunbc.runner_slot_desired { RunnerSlotDesired, RunnerSlotCpuQuota, CpuQuotaResolved, SlotCpuQuotaUnbounded, gunbc_runner_slot_desired } import gunbc.runner_throughput_qualification { WorkspaceBacking, RamBackedTmpfs, FloorWorkloadPin, - RouteLegStanding, LegWired, LegAuthorityImplemented, LegAwaitingAuthority, leg_is_wired, + RouteLegStanding, LegAuthorityImplemented, LegAwaitingAuthority, leg_is_wired, } // THE EXECUTION ROUTE IS A TYPED STAGE PLAN, AND EVERY STAGE SAYS WHAT IT MUTATES AND UNDER WHOM. @@ -28,9 +28,10 @@ import gunbc.runner_throughput_qualification { // stages as writing nothing, and a green witness certified it -- a false fact with coverage. // Only the collect stage is observation. // -// A ROUTE IS EXECUTABLE ONLY WHEN EVERY EFFECTFUL LEG IS BOUND. `route_is_executable` is false -// today and the witness asserts that it is false. The image leg IS bound: the seeded-media -// derivation takes its payload as a parameter. The boot leg is not, and not for want of a landing: +// A ROUTE IS EXECUTABLE ONLY WHEN EVERY EFFECTFUL LEG IS WIRED -- its authority has a production +// caller on the route. `route_is_executable` is false today and the witness asserts that it is +// false. No leg is wired yet: the image leg's authority exists (the seeded-media derivation takes its +// payload as a parameter) but nothing builds the derived runner image. The boot leg has no authority, and not for want of a landing: // the boot run on main (`gunbc.machine_intake_mtcollins1_boot_run`) boots exactly two media, the // census image and the stock installer, and its terminal is the census program's, so it cannot boot a // runner and observe it ready. `extdeps.github.workflows CreateDispatch` has no production caller @@ -213,8 +214,9 @@ data mtcollins1_boot_leg: RouteLegStanding = LegAwaitingAuthority { sufficient_for: "booting mtcollins1 from the derived runner image under grant_from_store with the SOL collector attached before the BMC stimulus, no controller write on deny, expiry or an unfiled request, and a terminal that says the runner host is up" as NonEmptyStr, } -data mtcollins1_image_leg: RouteLegStanding = LegWired { +data mtcollins1_image_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "extdeps.provisioning.ubuntu_seeded_install_media", decl_name: "UbuntuSeededInstallMediaBuildInput"), + wiring_owed: "the runner medium the boot leg lands with (untangle cut 4a): a production caller that builds the derived runner image from UbuntuSeededInstallMediaBuildInput with the runner payload and hands it to the boot run as its medium; SUFFICIENT FOR the image the boot leg boots being the one this route derived, not a fixture" as NonEmptyStr, } // THE THREE GITHUB CONTROL-PLANE LEGS. Registration and deregistration have implemented diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index 598c0825c13..d68fcc71705 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -58,7 +58,7 @@ import gunbc.runner_throughput_qualification { SlotWidthDerived, SlotWidthUnderivable, ThroughputRefusal, SubjectJoinDidNotHold, SamplingDidNotHold, RunnerThroughputAssessment, assess_runner_throughput, assessment_holds, assessment_wire, - qualification_run_producer_frontier, leg_is_wired, RouteLegStanding, LegAuthorityImplemented, + qualification_run_producer_frontier, leg_is_wired, RouteLegStanding, LegAuthorityImplemented, LegAwaitingAuthority, AssessedFact, InterpreterRateAgainstPin, SlotMemoryEnvelope, PhaseWallsAgainstSuppliedCeilings, ConcurrentCohortAtWidth, RunnerCohortEvidence, NoCohortObserved, CohortObserved, run_key, } @@ -753,10 +753,14 @@ test fn the_route_is_not_executable_while_any_effectful_leg_is_unbound() -> Bool !route_is_executable(route: route()) && !leg_is_wired(leg: qualification_run_producer_frontier) } -// THE IMAGE LEG IS BOUND AND THE BOOT LEG IS NOT, and the claim tells the two apart: binding the -// boot leg to the census-only boot run, or unbinding the image leg, turns this red. -test fn the_image_leg_is_bound_and_the_boot_leg_awaits_a_runner_medium() -> Bool { - leg_is_wired(leg: mtcollins1_route_legs.image) && !leg_is_wired(leg: mtcollins1_route_legs.boot) +// THE IMAGE LEG HAS AN IMPLEMENTED AUTHORITY AND IS NOT WIRED; THE BOOT LEG HAS NO AUTHORITY. The +// claim tells the two apart: wiring the image leg before a production caller builds the runner +// image, pointing it at another declaration, or binding the boot leg to the census-only boot run, +// turns this red. +test fn the_image_leg_is_implemented_not_wired_and_the_boot_leg_awaits_a_runner_medium() -> Bool { + leg_names(leg: mtcollins1_route_legs.image, module_path: "extdeps.provisioning.ubuntu_seeded_install_media", decl_name: "UbuntuSeededInstallMediaBuildInput") + && !leg_is_wired(leg: mtcollins1_route_legs.image) + && match mtcollins1_route_legs.boot { LegAwaitingAuthority { lands_with: _, sufficient_for: _ } => true _ => false } } // REGISTRATION AND DEREGISTRATION HAVE IMPLEMENTED AUTHORITIES AND ARE NOT WIRED -- by declaration From 9669ecced14d5c5b0e11137c7de0f287edb00deb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 05:18:54 +0000 Subject: [PATCH 08/44] leg 3b: claim-cost row names both missing capabilities and the calibration line's absence from the log, with the recorded run as receipt Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_qualification_dispatch.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index f2c1fdc6173..dda4c7b2bff 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -504,8 +504,8 @@ fn collect_qualification_instruments( data qualification_instrument_extraction_frontier_rows: List = [ frontier_row_decl( ref: decl_ref(module_path: "gunbc.floor_cost_distribution", decl_name: "parse_claim_cost_tsv"), - reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding), and neither reaches the job log in complete form: measured on a recorded required-floor log, eval_steps appear only on the 25-row [over-cost] preview and the per-claim [floor-shared-fill] rows, and the calibration specimen's [witness] line does not appear at all. Two capabilities are missing. The seed still writes the TSV (cli_run write_required_floor_claim_cost_tsv) but no workflow uploads it as an artifact today -- recent required runs publish only required-ci-measurement-receipt and compiler-pair-candidate. And an uploaded artifact is always a zip, while the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so extdeps.github.actions_artifacts github.Artifacts DownloadArtifact answers RestBodyUndecodable. read_claim_cost_artifact attempts the read on every collection and carries that as a typed ClaimCostArtifactRead refusal (ClaimCostArtifactAbsent while nothing uploads it)", - dissolution: unbound_dissolution(description: "the workflow the qualification dispatch targets uploads required-floor-claim-cost (gunbc.witness_floor_workflow carries the upload step under required_floor_claim_cost_artifact_name), AND the REST transport carries a binary body with an inflate reader SUFFICIENT FOR reading one member out of one upload-artifact zip, so extract_qualification_instruments reads ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from the same collected run through parse_claim_cost_tsv and qualification_instrument_source places both on that read. NOT satisfied by a new print of eval_steps into the log, by a 25-row preview, by an artifact downloaded outside this route, or by the zip read alone while no workflow uploads the artifact"), + reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are both read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding). Neither is on the job log: on the recorded required-floor log of run 35048059968 / job 104642384772 (test.fixture recorded_required_floor_log_excerpt), there are zero [witness] lines and zero occurrences of the specimen identity test.claim.floor_eval_step_calibration_specimen_test.calibration_specimen_performs_its_declared_work, and eval_steps appear only on the [over-cost] preview, a 25-row truncated view that is not the instrument and is not read as one. Two capabilities are missing. FIRST, the required workflow no longer uploads the artifact: cli_run write_required_floor_claim_cost_tsv still writes the TSV, but the emission of .github/workflows/witnesses.yml by gunbc.compiler_gate_workflow carries no upload step for it, and recent required runs publish only required-ci-measurement-receipt and compiler-pair-candidate. SECOND, an uploaded artifact is always a zip, and the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so extdeps.github.actions_artifacts github.Artifacts DownloadArtifact answers RestBodyUndecodable. read_claim_cost_artifact attempts the read on every collection and carries the result as a typed ClaimCostArtifactRead refusal (ClaimCostArtifactAbsent while nothing uploads it)", + dissolution: unbound_dissolution(description: "BOTH capabilities hold. (1) The required workflow uploads required-floor-claim-cost again, as an upload step in the existing floor job emitted by gunbc.compiler_gate_workflow under gunbc.witness_floor_workflow required_floor_claim_cost_artifact_name -- an emission change, not a new job. (2) The REST transport carries a binary body and an inflate reader can take one member out of one upload-artifact zip. Together these are SUFFICIENT FOR extract_qualification_instruments to read ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from the same collected run through parse_claim_cost_tsv, and for qualification_instrument_source to place both on that read. NOT satisfied by either capability alone, by the [over-cost] preview or any other truncated view, by a new print of eval_steps into the log, or by an artifact downloaded outside this route"), ), frontier_row_decl( ref: decl_ref(module_path: "gunbc.runner_throughput_qualification_route", decl_name: "mtcollins1_boot_leg"), From 24baafe00b082a22f71a22b40037c96eb663a911 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:06:29 +0000 Subject: [PATCH 09/44] leg 3b: make the two wildcard arms exhaustive (NonFoldResidueRosterDiverged on the floor) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_qualification_dispatch.dag | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index dda4c7b2bff..e81e5784f87 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -5,7 +5,7 @@ import std.resources { Network } import std.roster_frontier { FrontierRow, frontier_row_decl } import std.dissolution { unbound_dissolution } import std.decl_ref { decl_ref } -import extdeps.transports.rest { RestOutcome, RestOk } +import extdeps.transports.rest { RestOutcome, RestOk, RestStatusRefused, RestTransportRefused, RestBodyUndecodable } import extdeps.github.actions { DispatchInput, InputString } import extdeps.github.workflows { WorkflowDispatchReceipt } import extdeps.github.workflow_runs { @@ -285,7 +285,9 @@ fn qualification_instrument_source(i: QualificationInstrument) -> QualificationI fn qualification_instrument_read_from_job_log(i: QualificationInstrument) -> Bool { match qualification_instrument_source(i: i) { ReadFromAttemptJobLog => true - _ => false + ReadFromClaimCostArtifact => false + ReadFromBootLegReadback => false + ReadFromCohortObservation => false } } @@ -434,7 +436,9 @@ type ClaimCostArtifactRead fn claim_cost_artifact_listed(outcome: RestOutcome, listed: ActionsArtifactList) -> ActionsArtifact? { match outcome { RestOk => listed.artifacts |> filter(a => a.name == required_floor_claim_cost_artifact_name) |> first - _ => none + RestStatusRefused { status: _, body: _ } => none + RestTransportRefused { cause: _ } => none + RestBodyUndecodable { status: _, cause: _ } => none } } From be2c8448b192457203fe3dc2f9ca8d2e2a2c8e70 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:11:57 +0000 Subject: [PATCH 10/44] leg 3b: spell response_format as the WireFormat variant Text (the emitter's reading; the interpreter's after #13228) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/actions_artifacts.dag | 3 ++- dag/extdeps/github/workflow_runs.dag | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag index bcfa8e607df..eb27e917c71 100644 --- a/dag/extdeps/github/actions_artifacts.dag +++ b/dag/extdeps/github/actions_artifacts.dag @@ -8,6 +8,7 @@ import extdeps.uri { Uri, Https } import std.credentials { EnvVar } import std.types { Bool, Bytes, Int, List, String, Timestamp } import std.resources { Network } +import std.serialization { Text } // THE GITHUB ACTIONS ARTIFACTS REST API, as the upstream names it: "List workflow run artifacts" // (operationId actions/list-workflow-run-artifacts) and "Download an artifact" @@ -110,7 +111,7 @@ service github.Artifacts { transport rest { method: GET, path: "/repos/\{owner\}/\{repo\}/actions/artifacts/\{artifact_id\}/zip", - response_format: "Text" + response_format: Text } response { 200 => Bytes diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 4f821e2eaf3..290f3c1001f 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -16,6 +16,7 @@ import std.dissolution { unbound_dissolution } import std.decl_ref { decl_ref } import std.algebra { Cons, Empty } import std.resources { Network } +import std.serialization { Text } // WorkflowJobRun.labels are read by gunbc.public_workload_census (observed_job_labels) to // settle observed architecture and provider. run_attempt is modeled here and copied onto @@ -442,7 +443,7 @@ service github.WorkflowRuns { transport rest { method: GET, path: "/repos/\{owner\}/\{repo\}/actions/jobs/\{job_id\}/logs", - response_format: "Text" + response_format: Text } response { 200 => String From f6e820adff4301a70ec86df9f47a6915e5f21b00 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:26:30 +0000 Subject: [PATCH 11/44] leg 3: answer the operator review -- sealed dispatch subject, commit-ambiguous standing, App-token credential, runner-id floor join, attempt-1 binding, ruling+interpretation+interlock - QualificationDispatchSubject (sole_constructor) minted only from the route's JIT registration and its delivered credential, the slot's workflow == the generated fleet-converge workflow, and a floor job whose runs-on is exactly the declared attempt input; REDs for other workflows and labels - dispatch classified through RestMutationExchange: 4xx refused, transport/5xx/undecodable ambiguous - CreateDispatch takes its bearer as an input; dispatch runs under the gunbai-ci installation token, credential checked against DispatchWorkflow; takes the host-generic UnitHoldProof (interlock) - collection: attempt-scoped jobs, run_attempt == 1, revision pinned, workflow path, and the floor job's runner_id == the minted runner id; collected payload keeps the sealed dispatch + WorkflowRun - WorkflowJobRun gains runner_id/runner_name (upstream job resource fields) - census: IrreversibleEffect, discharged by the 2026-10-03 ruling (verbatim) through a separate scope interpretation (eager-gull-22, 2026-10-04); interlock rostered; narrowed-interpretation RED Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/workflow_runs.dag | 8 + dag/extdeps/github/workflows.dag | 11 +- dag/gunbc/auth/privileged_effect_census.dag | 60 +- dag/gunbc/github_effect_perform.dag | 55 ++ dag/gunbc/public_workload_census.dag | 16 + .../runner/runner_qualification_dispatch.dag | 594 +++++++++++++----- .../runner_throughput_qualification_route.dag | 2 +- ...ization_pattern_selection_witness_test.dag | 39 +- .../public_workload_census_witness_test.dag | 22 + ...er_qualification_dispatch_witness_test.dag | 440 +++++++++---- ...ded_run_starvation_census_witness_test.dag | 12 + 11 files changed, 978 insertions(+), 281 deletions(-) diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 98df2230815..97ad64e880b 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -194,6 +194,12 @@ fn workflow_job_attempt_filter_wire(f: WorkflowJobAttemptFilter) -> String { // rendered name. The parenthetical of combination VALUES in axis order is the rendering this // tree observed on those names; it is not a REST field. Standing is TranscribedUncited until // GitHub's Jobs docs state that encoding. +// runner_id AND runner_name ARE WHICH RUNNER TOOK THE JOB, a third fact beside the declared +// runs-on and the observed labels: two runners can carry identical labels, and only the runner +// identity joins a job to one registration (GitHub returns both null until a runner is assigned). +// The census rows transcribed before these fields existed carry none for both: the field was not +// read, which is the same absence a reader of an unassigned job sees, and no consumer of those rows +// asks it. type WorkflowJobRun { id: Int run_id: Int @@ -202,6 +208,8 @@ type WorkflowJobRun { status: WorkflowRunStatus conclusion: WorkflowRunConclusion? labels: List + runner_id: Int? + runner_name: String? created_at: Timestamp? started_at: Timestamp? completed_at: Timestamp? diff --git a/dag/extdeps/github/workflows.dag b/dag/extdeps/github/workflows.dag index b1f0d3fb243..36465157304 100644 --- a/dag/extdeps/github/workflows.dag +++ b/dag/extdeps/github/workflows.dag @@ -2,8 +2,7 @@ module extdeps.github.workflows import extdeps.github.github { default_api_base } import extdeps.github.errors { GitHubErrorShape } -import std.credentials { EnvVar } -import std.types { NonEmptyStr, String } +import std.types { NonEmptyStr, Secret, String } import extdeps.transports.rest { RestOutcome } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -45,16 +44,22 @@ type WorkflowDispatchReceipt { // (DESIGN section 3). Which keys a dispatch carries is the dispatching workflow's fact, so the // caller supplies the object and this module names no key. GitHub answers an undeclared key with // 422, which is the refusal arm below, never a silently dropped input. +// +// THE CREDENTIAL IS AN INPUT, NOT AN AMBIENT VARIABLE. It once read GITHUB_TOKEN from the +// environment, which fixed the executing credential to whichever job happened to run the call. +// Which credential performs a dispatch is the caller's authorization fact (gunbc.auth), so the +// operation takes the bearer as auth_token, as the self-hosted-runner and gist services do. service github.Workflows { config { endpoint: default_api_base auth: Bearer - auth_source: EnvVar { name: "GITHUB_TOKEN" } + auth_input: auth_token } operation CreateDispatch { requires Network input { + auth_token: Secret owner: String repo: String workflow_id: String diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index c829b2d2edf..b72c8edb069 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -343,6 +343,13 @@ data mtcollins1_hold_owning_roots: List = [ ] data privileged_effect_interlocks: List = [ + PrivilegedEffectInterlock { + site: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + hold: runner_lifecycle_operator_ruling.interlock, + refusal: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + premise: "the dispatch takes the slot host's unit hold proof and refuses DispatchHoldForAnotherHost unless the proof's admitted host is the slot's own; the root that acquires it is the route fold the dispatch leg owes (LegAuthorityImplemented wiring_owed), so no production root is rostered yet" as NonEmptyStr, + hold_owning_roots: [] as List, + }, PrivilegedEffectInterlock { site: site(module_path: "gunbc.spark.pair_serving_d0_door", decl_name: "d0_standing_grant_for"), hold: group_a_dev_standing_grant.ruling.interlock, @@ -498,6 +505,50 @@ fn present_trigger_declarations() -> List { // committed standing grant covers is admitted by that grant, never filed to the broker. The selection // decides the pattern from the effect's parameters with the ruling as the witness discharge, conditional on // its interlock (D0's durable claim, rostered in privileged_effect_interlocks); the census does not assert it. +// THE OPERATOR'S 2026-10-03 RUNNER-LIFECYCLE RULING, VERBATIM, AND ITS SCOPE INTERPRETATION AS A +// SEPARATE CITED FACT. The ruling text names registration and deregistration. Whether it also +// covers dispatching the qualification floor is not in its words; that coverage is an +// interpretation made by eager-gull-22 on 2026-10-04 and reported to the operator, who may narrow +// it. The two are kept apart so narrowing the interpretation is an edit to one row that changes the +// discharge of exactly the sites it names, and the ruling text is never rewritten to say more than +// the operator said. The interlock is the host-generic unit hold (gunbc.managed_host_unit_hold +// UnitHoldProof): the dispatch takes the proof and refuses unless it is the slot's own host's. +data runner_lifecycle_operator_ruling: StandingDestructiveAuthorization = StandingDestructiveAuthorization { + ruling_text: "Runner registration and deregistration are PRE-APPROVED. Approval moves up to the workflow dispatcher (the fleet-converge run's own authorization). There is no per-registration approval step. Classify this through gunbc.auth.authorization_pattern_selection, recording that ruling rather than a per-effect ntfy approval. (operator decision, 2026-10-03)" as NonEmptyStr, + effect_subject: "ephemeral JIT runner lifecycle: registration, deregistration, and the dispatch of the qualification floor to the attempt's own runner" as NonEmptyStr, + interlock: decl_ref(module_path: "gunbc.managed_host_unit_hold", decl_name: "UnitHoldProof"), +} + +type RulingScopeInterpretation { + ruling: StandingDestructiveAuthorization + covers: List + interpreted_by: NonEmptyStr + interpreted_on: NonEmptyStr + standing: NonEmptyStr +} + +data runner_lifecycle_ruling_scope_interpretation: RulingScopeInterpretation = RulingScopeInterpretation { + ruling: runner_lifecycle_operator_ruling, + covers: [ + decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), + decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + ], + interpreted_by: "eager-gull-22" as NonEmptyStr, + interpreted_on: "2026-10-04" as NonEmptyStr, + standing: "covers register, deregister and the qualification dispatch; reported to the operator, who may narrow it" as NonEmptyStr, +} + +// A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE INTERPRETATION NAMES IT. Removing a site from +// covers turns its discharge into NoWitnessDischarge, and the selection then decides it afresh. +fn ruling_discharge_for(interpretation: RulingScopeInterpretation, site_ref: DeclarationRef) -> WitnessDischarge { + if declaration_ref_in_list(ref: site_ref, list: interpretation.covers) { + StandingRulingUnderInterlock { ruling: interpretation.ruling } + } else { + NoWitnessDischarge + } +} + data privileged_effect_census: List = [ PrivilegedEffectSite { site: site(module_path: "gunbc.live_deploy.emit", decl_name: "approval_broker_helper_grant_steps"), @@ -553,14 +604,17 @@ data privileged_effect_census: List = [ PrivilegedEffectSite { site: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), effect: PrivilegedEffect { - subject: "dispatch the qualification floor workflow with the slot's attempt label as its input, so only the one JIT registration carrying that label can serve the job, under the dispatching run's own GITHUB_TOKEN, pre-approved under the dispatching run's authorization (operator ruling 2026-10-03)" as NonEmptyStr, + subject: "dispatch the generated fleet-converge workflow with the slot's attempt label as the input its floor job's runs-on reads, so only the JIT registration minted for that attempt can serve the job, under the gunbai-ci installation token whose App key is read over WIF, checked for actions: write" as NonEmptyStr, frequency: EveryProvision, - reversibility: ReversibleByReapply, + reversibility: IrreversibleEffect { what_is_lost: "a dispatched run is durable: a second dispatch creates another run and does not undo the first, and a cancel stops it without unmaking it" as NonEmptyStr }, surface: ApiSurface, workload_identity: convergence_principal(), minted_reach: MintsNoCredential, billing: NoBillingConsequence, - witness_discharge: NoWitnessDischarge, + witness_discharge: ruling_discharge_for( + interpretation: runner_lifecycle_ruling_scope_interpretation, + site_ref: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + ), }, realized: RealizedFederatedGrant, divergence_reason: none, diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 1f7806e0fd3..2519447a4d5 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -28,6 +28,10 @@ import extdeps.github.app import extdeps.github.actions_jit_runner import extdeps.github.actions_self_hosted_runners import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } +import extdeps.github.workflows +import extdeps.github.workflows { WorkflowDispatchReceipt } +import extdeps.github.workflow_runs +import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRunList } import gunbc.auth.github_apps { DeclaredGitHubApp, gunbai_ci_declared } import extdeps.github.app { GitHubAppInstallationId } import gunbc.runner.runner_jit_perform { @@ -294,3 +298,54 @@ fn delete_runner_with(token: ControllerInstallationToken, organization: NonEmpty let deleted = github.ActionsSelfHostedRunners.DeleteOrganizationRunner(installation_token: token.token, org: organization, runner_id: runner_id) classify_rest_outcome(standing: RestMutationExchange { rest: deleted.outcome }) } + +// THE QUALIFICATION DISPATCH AND ITS READBACK, under the same controller installation token +// (gunbc.runner.runner_qualification_dispatch). The dispatch is a mutation and is classified as one, +// so a 5xx, a lost response or an undecodable 200 is commit-ambiguous rather than refused; the two +// reads are reads. Each answer is sealed so the decision module receives only what GitHub returned. +type WorkflowDispatchAnswer sole_constructor { + performance: RestExchangePerformance + receipt: WorkflowDispatchReceipt +} + +fn dispatch_workflow_with( + token: ControllerInstallationToken, + owner: NonEmptyStr, + repo: NonEmptyStr, + workflow_id: NonEmptyStr, + git_ref: NonEmptyStr, + inputs: Map, +) -> WorkflowDispatchAnswer uses net: Network { + let answered = github.Workflows.CreateDispatch( + auth_token: token.token, + owner: owner as String, + repo: repo as String, + workflow_id: workflow_id as String, + ref: git_ref as String, + inputs: inputs, + ) + WorkflowDispatchAnswer { + performance: classify_rest_outcome(standing: RestMutationExchange { rest: answered.outcome }), + receipt: WorkflowDispatchReceipt { workflow_run_id: answered.workflow_run_id, run_url: answered.run_url, html_url: answered.html_url }, + } +} + +type WorkflowRunRead sole_constructor { + performance: RestExchangePerformance + run: WorkflowRun +} + +fn read_run_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> WorkflowRunRead uses net: Network { + let read = github.WorkflowRuns.GetRun(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String) + WorkflowRunRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: read.outcome }), run: read.run } +} + +type WorkflowAttemptJobsRead sole_constructor { + performance: RestExchangePerformance + jobs: WorkflowJobRunList +} + +fn read_attempt_jobs_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int, attempt: Int) -> WorkflowAttemptJobsRead uses net: Network { + let read = github.WorkflowRuns.ListAttemptJobs(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String, attempt_number: attempt) + WorkflowAttemptJobsRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: read.outcome }), jobs: read.result } +} diff --git a/dag/gunbc/public_workload_census.dag b/dag/gunbc/public_workload_census.dag index 19d698ca403..ec11c28ce47 100644 --- a/dag/gunbc/public_workload_census.dag +++ b/dag/gunbc/public_workload_census.dag @@ -1020,6 +1020,8 @@ data candidate_biome_test: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -1082,6 +1084,8 @@ data candidate_pdns_dnsdist_arm: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -1149,6 +1153,8 @@ data candidate_openobserve_arm_build: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:21Z" }, completed_at: Present { value: "2026-09-01T10:27:44Z" }, @@ -1218,6 +1224,8 @@ data candidate_openobserve_amd64: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-8"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:15Z" }, completed_at: Absent, @@ -1271,6 +1279,8 @@ data candidate_openobserve_manifest: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:28:12Z" }, completed_at: Absent, @@ -1324,6 +1334,8 @@ data candidate_biome_test_windows: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-windows-2022-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:31Z" }, completed_at: Present { value: "2026-09-04T16:13:50Z" }, @@ -1371,6 +1383,8 @@ data candidate_biome_test_macos: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-macos-latest"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:08:06Z" }, completed_at: Absent, @@ -1416,6 +1430,8 @@ data candidate_openobserve_summary: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-latest"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:29:11Z" }, completed_at: Absent, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index f9a2c1c83ff..73f7c6c0d4b 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -1,242 +1,508 @@ module gunbc.runner.runner_qualification_dispatch -import std.types { Bool, Int, List, NonEmptyStr, String } +import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } import std.resources { Network } import std.roster_frontier { FrontierRow, frontier_row_decl } import std.dissolution { unbound_dissolution } import std.decl_ref { decl_ref } -import extdeps.transports.rest { RestOutcome, RestOk } -import extdeps.github.actions { DispatchInput, InputString } +import product.placement_supply { HostIdentity } +import extdeps.transports.rest { + RestExchangePerformance, RestExchangeSucceeded, RestExchangeStatusRefused, + RestExchangeCommitAmbiguous, RestExchangeUnreached, RestExchangeUndecodable, +} +import extdeps.github.effect { GitHubEffect, DispatchWorkflow, GitHubWorkflowRef } +import extdeps.github.actions { + Workflow, Job, DispatchInput, InputString, WorkflowDispatch, RunnerSpec, RunsOnExpression, +} +import extdeps.github.expressions { ingest_template, Interpolation, ContextAccess, Inputs } import extdeps.github.workflows { WorkflowDispatchReceipt } import extdeps.github.workflow_runs { WorkflowRun, WorkflowRunStatus, Completed, WorkflowRunConclusion, WorkflowJobRun, WorkflowJobRunList, } +import gunbc.repository { gunbc_repository } +import gunbc.generated_artifact { artifact_path, FleetConvergeYamlArtifact } +import gunbc.fleet_converge_workflow { fleet_converge_workflow } +import gunbc.auth.github_apps { GitHubAppControlPlaneState } +import gunbc.auth.github_credential { + GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, +} +import gunbc.runner_jit_mint { HostUnitJitSlot, MicroVmJitSlot } +import gunbc.runner.runner_jit_perform { JitCredentialDelivery, JitCredentialBoundToAttempt } +import gunbc.runner.runner_jit_deregistration { JitRegistration } +import gunbc.managed_host_unit_hold { UnitHoldProof } +import gunbc.github_effect_perform { + ControllerInstallationToken, dispatch_workflow_with, read_run_with, read_attempt_jobs_with, +} import gunbc.runner_throughput_qualification_route { QualificationInstrument, qualification_instruments } // THE DISPATCH LEG OF THE QUALIFICATION ROUTE, AND THE COLLECT STAGE THAT READS ITS RUN BACK. // -// The route registers one ephemeral slot whose label set carries a per-boot attempt label -// (gunbc.runner_throughput_qualification_route ephemeral_slot_labels). Dispatching the floor to that -// slot is a workflow_dispatch whose input IS the attempt label, so the target workflow's runs-on can -// name it and no other registration in the organization can serve the job. This module is the -// production caller of extdeps.github.workflows github.Workflows CreateDispatch; its landing is what -// dissolved that module's unconsumed-operation frontier. +// The route registers one ephemeral JIT runner whose label set carries a per-boot attempt label +// (gunbc.runner_throughput_qualification_route ephemeral_slot_labels). Dispatching the floor to it is +// a workflow_dispatch whose input IS that attempt label, read by the floor job's runs-on, so no other +// registration can serve the job. This module is the production caller of extdeps.github.workflows +// github.Workflows CreateDispatch. // -// WHICH WORKFLOW IS A PARAMETER. The fleet-converge mode that becomes this route belongs to -// gunbc.fleet_converge_workflow and lands with the boot leg; naming a workflow file here before that -// mode exists would be a second spelling of a fact whose home is not yet written. What this module -// does own is the input the target workflow must declare: qualification_attempt_dispatch_input is -// the DispatchInput row the workflow side consumes, and the dispatch map is keyed by its name, so -// the key the dispatch sends and the key the workflow declares are one declaration. +// THE SUBJECT IS SEALED AND ITS SCOPE IS FIXED BY CONSTRUCTION. QualificationDispatchSubject is +// minted only by qualification_dispatch_subject_over, and only from: +// - the JIT registration the route minted and the credential delivered for that same slot -- so +// the attempt is the slot's own and the runner id is the one GitHub returned for it; +// - the qualification workflow, which is not a parameter of the request: its path is the generated +// fleet-converge artifact (gunbc.generated_artifact FleetConvergeYamlArtifact) and production +// supplies its model as gunbc.fleet_converge_workflow fleet_converge_workflow, where the route's +// mode lands with the boot leg. A slot restricted to any other workflow refuses; +// - a floor job in that workflow whose runs-on is exactly the declared attempt input, so the job +// can be served by the attempt's runner and nothing else. A literal label, another input, or a +// composed expression refuses. +// The repository is gunbc_repository; a caller cannot name another. // -// AUTHORIZATION IS THE DISPATCHING RUN'S (operator ruling 2026-10-03): CreateDispatch authenticates -// with the running job's GITHUB_TOKEN (the service config's auth_source), with no per-dispatch -// approval. gunbc.auth.privileged_effect_census rows dispatch_qualification_floor through -// gunbc.auth.authorization_pattern_selection. +// AUTHORIZATION. The dispatch runs under the gunbai-ci installation token the registration was +// minted with (gunbc.github_effect_perform ControllerInstallationToken, App key read over WIF); the +// credential is checked against the DispatchWorkflow effect (actions: write) before any request, and +// a token for another App or installation refuses. The operator's 2026-10-03 ruling, as interpreted +// to cover this dispatch (gunbc.auth.privileged_effect_census +// runner_lifecycle_ruling_scope_interpretation), stands only beside its interlock: +// dispatch_qualification_floor takes the host-generic unit hold proof +// (gunbc.managed_host_unit_hold UnitHoldProof) and refuses unless it is the slot's own host's. data qualification_attempt_dispatch_input: DispatchInput = DispatchInput { name: "runner_attempt_label", - description: Present { value: "the per-boot attempt label the ephemeral qualification slot registered with; runs-on names it" }, + description: Present { value: "the per-boot attempt label the ephemeral qualification slot registered with; the floor job's runs-on reads it" }, required: true, default: none, type: InputString, } -type QualificationWorkflowTarget { - owner: NonEmptyStr - repo: NonEmptyStr - workflow_id: NonEmptyStr - ref: NonEmptyStr +fn qualification_workflow_path() -> NonEmptyStr { + artifact_path(a: FleetConvergeYamlArtifact) as NonEmptyStr +} + +fn qualification_workflow_file_name() -> NonEmptyStr { + match list_last(split(qualification_workflow_path() as String, "/")) { + Present { value: f } => f as NonEmptyStr + Absent => qualification_workflow_path() + } +} + +// The selected-workflow spelling a restricted runner group carries (owner/repo/path@ref), the same +// form gunbc.runner_group_restriction_observation microvm_shakedown_selected_workflow derives. +fn qualification_selected_workflow(branch: NonEmptyStr) -> NonEmptyStr { + join([gunbc_repository.full_name, "/", qualification_workflow_path() as String, "@refs/heads/", branch as String], "") as NonEmptyStr } fn qualification_dispatch_inputs(attempt: NonEmptyStr) -> Map { map_insert(m: empty_map(), key: qualification_attempt_dispatch_input.name, value: attempt as String) } -// A DISPATCH IS ONE ATTEMPT AND ITS ANSWER IS THE RECEIPT OR THE REFUSAL, never a run assumed to -// exist. The run id comes from GitHub's 2026-03-10 response (WorkflowDispatchReceipt); a refused or -// unreached request carries its RestOutcome and mints no run. +// ── THE SUBJECT ─────────────────────────────────────────────────────────────────────────────── + +type QualificationDispatchSubject sole_constructor { + registration: JitRegistration + host: HostIdentity + attempt: NonEmptyStr + runner_id: Int + branch: NonEmptyStr + revision: CommitSha + floor_job_name: NonEmptyStr +} + +type QualificationSubjectRefusal + = SubjectCredentialNotDelivered { delivery: JitCredentialDelivery } + | SubjectDeliveryForAnotherSlot + | SubjectSlotNotHostUnit + | SubjectSlotForAnotherWorkflow { slot_workflow: NonEmptyStr, qualification_workflow: NonEmptyStr } + | SubjectAttemptInputUndeclared { input: String } + | SubjectFloorJobAbsent { job_id: NonEmptyStr } + | SubjectFloorRunsOnNotTheAttemptInput { job_id: NonEmptyStr, runs_on: String } + +type QualificationSubjectStanding + = QualificationSubjectAdmitted { subject: QualificationDispatchSubject } + | QualificationSubjectRefused { refusal: QualificationSubjectRefusal } + +fn workflow_declares_dispatch_input(workflow: Workflow, input: DispatchInput) -> Bool { + workflow.on |> any(t => + match t { + WorkflowDispatch { inputs: ins } => ins |> any(i => i.name == input.name) + _ => false + }) +} + +// EXACTLY `${{ inputs. }}` and nothing else: one interpolation segment reading the input. +fn runs_on_is_exactly_the_input(runner: RunnerSpec, input: DispatchInput) -> Bool { + match runner { + RunsOnExpression { expression: e } => + match ingest_template(src: e) { + Present { value: t } => + count(t.segments) == 1 && (t.segments |> all(s => + match s { + Interpolation { expression: ContextAccess { context: Inputs, path: p } } => + count(p) == 1 && (p |> all(x => x == input.name)) + _ => false + })) + Absent => false + } + _ => false + } +} + +fn runner_spec_text(runner: RunnerSpec) -> String { + match runner { + RunsOnExpression { expression: e } => e + _ => "a runs-on that is not an expression" + } +} + +fn job_display_name(job: Job) -> NonEmptyStr { + match job.name { + Present { value: n } => n as NonEmptyStr + Absent => job.id as NonEmptyStr + } +} + +// THE PURE JOIN, OVER A SUPPLIED WORKFLOW MODEL. Production reaches it only through +// qualification_dispatch_subject, which supplies fleet_converge_workflow; the witness claims supply a +// model carrying the floor job the boot leg's mode will add. An admit list constrains the call edge, +// not where the result goes, so every admitted witness entry is a `test fn` returning Bool. +fn qualification_dispatch_subject_over( + registration: JitRegistration, + delivery: JitCredentialDelivery, + workflow: Workflow, + floor_job_id: NonEmptyStr, + branch: NonEmptyStr, + revision: CommitSha, +) -> QualificationSubjectStanding + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "no_other_workflow_or_label_can_be_dispatched_through_the_ruling"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_unpinned_or_unreadable_run_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_dispatch_has_three_standings_and_an_interlock"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "slot_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "runs_on_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "undeclared_input_refuses"), + ] +{ + match delivery { + JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: runner_id } => + if delivered_slot != registration.slot { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherSlot } + } else { + match registration.slot { + MicroVmJitSlot { attempt: _ } => QualificationSubjectRefused { refusal: SubjectSlotNotHostUnit } + HostUnitJitSlot { slot: h } => + if (h.workflow as String) != (qualification_selected_workflow(branch: branch) as String) { + QualificationSubjectRefused { + refusal: SubjectSlotForAnotherWorkflow { slot_workflow: h.workflow, qualification_workflow: qualification_selected_workflow(branch: branch) }, + } + } else if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_attempt_dispatch_input) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } + } else { + match list_find(workflow.jobs, j => j.id == (floor_job_id as String)) { + Absent => QualificationSubjectRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } + Present { value: job } => + if !runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { + QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: floor_job_id, runs_on: runner_spec_text(runner: job.runner) } } + } else { + QualificationSubjectAdmitted { + subject: QualificationDispatchSubject { + registration: registration, + host: h.host.host.host, + attempt: h.attempt, + runner_id: runner_id, + branch: branch, + revision: revision, + floor_job_name: job_display_name(job: job), + }, + } + } + } + } + } + } + other => QualificationSubjectRefused { refusal: SubjectCredentialNotDelivered { delivery: other } } + } +} + +// THE PRODUCTION MINT: the qualification workflow is the generated fleet-converge model. Until the +// boot leg lands the route's mode there, it has no floor job reading the attempt input and this +// refuses -- the wiring the route leg owes, refused rather than faked. +fn qualification_dispatch_subject( + registration: JitRegistration, + delivery: JitCredentialDelivery, + floor_job_id: NonEmptyStr, + branch: NonEmptyStr, + revision: CommitSha, +) -> QualificationSubjectStanding { + qualification_dispatch_subject_over( + registration: registration, + delivery: delivery, + workflow: fleet_converge_workflow, + floor_job_id: floor_job_id, + branch: branch, + revision: revision, + ) +} + +fn qualification_dispatch_effect(subject: QualificationDispatchSubject) -> GitHubEffect { + DispatchWorkflow { + repository: gunbc_repository, + workflow: GitHubWorkflowRef { file_name: qualification_workflow_file_name() }, + subject: subject.revision, + } +} + +// ── THE DISPATCH ────────────────────────────────────────────────────────────────────────────── // -// THE DISPATCHED RUN IS SEALED, AND THAT IS WHAT PUTS COLLECTION CAUSALLY AFTER DISPATCH. -// WorkflowDispatchReceipt is a plain upstream record anyone can write, so a collect fold taking one -// would read back whatever run id a caller named. DispatchedQualificationRun is sole_constructor: -// only this file can write it, and in this file only dispatch_qualification_floor does, from a -// RestOk answer. collect_qualification_run takes that value, so no run is collected that this -// module did not dispatch, for this target and this attempt. +// A DISPATCH IS A MUTATION AND HAS THREE STANDINGS, NOT TWO. GitHub's 2026-03-10 answer (200 with +// the run id) is the only one that mints a DispatchedQualificationRun. A decided 4xx is refused: no +// run exists. A 5xx, a request that got no response, or a 200 this client could not decode is +// commit-ambiguous (extdeps.transports.rest classify_rest_outcome over RestMutationExchange): a run +// may exist with no receipt here, so the standing keeps the subject for reconciliation and is not a +// refusal a caller may retry as if nothing were created. +// +// THE DISPATCHED RUN IS SEALED, AND THAT IS WHAT PUTS COLLECTION CAUSALLY AFTER DISPATCH: only +// dispatch_qualification_floor writes one, from a succeeded exchange, carrying the sealed subject. type DispatchedQualificationRun sole_constructor { - target: QualificationWorkflowTarget - attempt: NonEmptyStr + subject: QualificationDispatchSubject receipt: WorkflowDispatchReceipt } +type QualificationDispatchRefusal + = DispatchCredentialRefused { unsatisfied: List } + | DispatchTokenForAnotherInstallation + | DispatchHoldForAnotherHost { held: HostIdentity, slot: HostIdentity } + | DispatchStatusRefused { status: Int, body: String } + +type QualificationDispatchStanding + = DispatchStandingSucceeded + | DispatchStandingRefused { status: Int, body: String } + | DispatchStandingCommitAmbiguous { detail: NonEmptyStr } + type QualificationDispatch = QualificationDispatched { run: DispatchedQualificationRun } - | QualificationDispatchRefused { attempt: NonEmptyStr, outcome: RestOutcome } - -fn dispatch_qualification_floor(target: QualificationWorkflowTarget, attempt: NonEmptyStr) -> QualificationDispatch uses net: Network { - let answered = github.Workflows.CreateDispatch( - owner: target.owner as String, - repo: target.repo as String, - workflow_id: target.workflow_id as String, - ref: target.ref as String, - inputs: qualification_dispatch_inputs(attempt: attempt), - ) - match answered.outcome { - RestOk => - QualificationDispatched { - run: DispatchedQualificationRun { - target: target, - attempt: attempt, - receipt: WorkflowDispatchReceipt { - workflow_run_id: answered.workflow_run_id, - run_url: answered.run_url, - html_url: answered.html_url, - }, - }, - } - other => QualificationDispatchRefused { attempt: attempt, outcome: other } + | QualificationDispatchRefused { subject: QualificationDispatchSubject, refusal: QualificationDispatchRefusal } + | QualificationDispatchCommitAmbiguous { subject: QualificationDispatchSubject, detail: NonEmptyStr } + +// The standing of one dispatch exchange, decided from the mutation classifier and nothing else. An +// Unreached or Undecodable performance is not produced for a mutation; were one ever passed here, it +// is the ambiguous side, never a refusal. +fn qualification_dispatch_standing(performance: RestExchangePerformance) -> QualificationDispatchStanding { + match performance { + RestExchangeSucceeded => DispatchStandingSucceeded + RestExchangeStatusRefused { status: s, body: b } => DispatchStandingRefused { status: s as Int, body: b } + RestExchangeCommitAmbiguous { detail: d } => DispatchStandingCommitAmbiguous { detail: d } + RestExchangeUnreached { cause: c } => DispatchStandingCommitAmbiguous { detail: c } + RestExchangeUndecodable { status: _, cause: c } => DispatchStandingCommitAmbiguous { detail: c } + } +} + +// THE INTERLOCK, as a pure comparison: the proof's admitted host against the slot's host. +fn hold_covers_subject(held: HostIdentity, subject: QualificationDispatchSubject) -> Bool { + (held as String) == (subject.host as String) +} + +fn dispatch_qualification_floor( + subject: QualificationDispatchSubject, + hold: UnitHoldProof, + token: ControllerInstallationToken, + authority: GitHubCredentialAuthority, + control_plane: GitHubAppControlPlaneState, +) -> QualificationDispatch uses net: Network { + let effect = qualification_dispatch_effect(subject: subject) + if !hold_covers_subject(held: hold.subject.host.host, subject: subject) { + QualificationDispatchRefused { subject: subject, refusal: DispatchHoldForAnotherHost { held: hold.subject.host.host, slot: subject.host } } + } else if token.app != subject.registration.app || token.installation != subject.registration.installation { + QualificationDispatchRefused { subject: subject, refusal: DispatchTokenForAnotherInstallation } + } else if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { + QualificationDispatchRefused { + subject: subject, + refusal: DispatchCredentialRefused { unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect) }, + } + } else { + let answer = dispatch_workflow_with( + token: token, + owner: gunbc_repository.owner as NonEmptyStr, + repo: gunbc_repository.name as NonEmptyStr, + workflow_id: qualification_workflow_file_name(), + git_ref: subject.branch, + inputs: qualification_dispatch_inputs(attempt: subject.attempt), + ) + match qualification_dispatch_standing(performance: answer.performance) { + DispatchStandingSucceeded => QualificationDispatched { run: DispatchedQualificationRun { subject: subject, receipt: answer.receipt } } + DispatchStandingRefused { status: s, body: b } => QualificationDispatchRefused { subject: subject, refusal: DispatchStatusRefused { status: s, body: b } } + DispatchStandingCommitAmbiguous { detail: d } => QualificationDispatchCommitAmbiguous { subject: subject, detail: d } + } } } // ── COLLECT: THE RUN READ BACK, ENSURE-STYLE ────────────────────────────────────────────────── // -// THE SUBJECT IS THE DISPATCHED RUN, CHECKED ON THE READBACK: the run read must carry the id the -// dispatch returned and the workflow_dispatch event, and every job listed must belong to it; -// anything else is RunSubjectMismatch, never a collection of some other run. +// THE SUBJECT IS THE DISPATCHED RUN AT ITS FIRST ATTEMPT. A dispatch creates attempt 1; a re-run +// reuses the run id and mints attempt 2 with new jobs and a new conclusion. So the run read must +// carry the dispatched id, the workflow_dispatch event, the qualification workflow's path, the pinned +// revision and run_attempt 1; the jobs are read through the attempt-scoped endpoint for attempt 1 +// and every one must carry that run id and attempt. A run that has advanced past attempt 1 refuses: +// its conclusion is no longer the dispatch's. // -// A DISPATCH RECEIPT SAYS A RUN EXISTS, NOT THAT THE ATTEMPT'S RUNNER SERVED IT. The collection -// therefore decides only from what GitHub reports about the run now: the run must be Completed with -// a conclusion, and at least one of its jobs must carry the attempt label and have started -- that -// is, been taken by a runner registered under the attempt. A run that finished without that is the -// typed refusal RunNeverReachedAttemptRunner, carrying every job's labels so the cause is located; -// it is never collected as a measurement of a host that did not run it. +// THE RUNNER IS JOINED BY IDENTITY, NOT BY LABEL. Two runners can carry the same labels, and a +// helper job on the attempt's runner says nothing about where the floor ran. So the collection finds +// THE floor job by the name the subject fixed and requires its runner_id to be the one GitHub +// returned when the registration was minted, and that it started. Anything else is +// RunNeverReachedAttemptRunner, carrying what was observed. // // THE CONCLUSION IS READ BACK AND CARRIED, NOT FILTERED: a failed floor on the attempt's runner is -// still a run of this host and the receipt says how it ended. +// still a run of this host. The collected payload keeps the sealed dispatch and the whole observed +// WorkflowRun, so extraction and assessment read the revision, workflow, event and attempt from +// here rather than re-reading GitHub. type QualificationRunRefusal - = RunReadUnreadable { outcome: RestOutcome } + = RunReadUnreadable { performance: RestExchangePerformance } + | RunSubjectMismatch { observed_run_id: Int, observed_event: String, observed_path: String?, foreign_jobs: List } + | RunAdvancedPastDispatchedAttempt { observed_attempt: Int } + | RunRevisionNotPinned { pinned: CommitSha, observed: CommitSha } | RunNotCompleted { status: WorkflowRunStatus } | RunCompletedWithoutConclusion - | RunJobsUnreadable { outcome: RestOutcome } + | RunJobsUnreadable { performance: RestExchangePerformance } | RunJobsTruncated { returned: Int, total: Int } - | RunNeverReachedAttemptRunner { job_labels: List> } - | RunSubjectMismatch { observed_run_id: Int, observed_event: String, foreign_job_run_ids: List } + | RunFloorJobNotUnique { floor_job_name: NonEmptyStr, matching: Int } + | RunNeverReachedAttemptRunner { minted_runner_id: Int, observed_runner_id: Int?, observed_runner_name: String?, started: Bool } // INSTRUMENT EXTRACTION IS A DECLARED FRONTIER, carried on the collected arm as the instruments the // run was read for and not yet extracted from (qualification_instrument_extraction_frontier_rows). -// -// THE SUCCESS PAYLOAD IS SEALED: CollectedQualificationRun is sole_constructor, written only by -// conclude_qualification_run, whose callers are admitted by name -- the one production reader, -// collect_qualification_run, and the witness claims that supply readbacks at that interface. An -// admit list constrains the call edge, not where the result goes, so every admitted witness entry -// is a `test fn` returning Bool that consumes the collection internally; a helper returning the -// collection would be a public constructor proxy, and none is admitted. collect_qualification_run -// returns the collection, but only from a sealed DispatchedQualificationRun. -// A module elsewhere cannot mint "this host ran the floor" without a readback. type CollectedQualificationRun sole_constructor { - attempt: NonEmptyStr - run_id: Int + dispatched: DispatchedQualificationRun + run: WorkflowRun + floor_job: WorkflowJobRun conclusion: WorkflowRunConclusion - attempt_jobs: List instruments_unextracted: List } type QualificationRunCollection = QualificationRunCollected { collected: CollectedQualificationRun } - | QualificationRunRefused { attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal } + | QualificationRunRefused { run_id: Int, refusal: QualificationRunRefusal } + +fn qualification_run_collected(c: QualificationRunCollection) -> Bool { + match c { + QualificationRunCollected { collected: _ } => true + QualificationRunRefused { run_id: _, refusal: _ } => false + } +} + +data dispatched_run_attempt: Int = 1 + +fn optional_text_is(o: String?, expected: String) -> Bool { + match o { Present { value: v } => v == expected Absent => false } +} + +fn optional_int_is(o: Int?, expected: Int) -> Bool { + match o { Present { value: v } => v == expected Absent => false } +} -fn job_served_by_attempt(job: WorkflowJobRun, attempt: NonEmptyStr) -> Bool { - (job.labels |> any(l => l == (attempt as String))) - && (match job.started_at { Present { value: _ } => true Absent => false }) +fn job_started(job: WorkflowJobRun) -> Bool { + match job.started_at { Present { value: _ } => true Absent => false } } -fn refuse_run(attempt: NonEmptyStr, run_id: Int, refusal: QualificationRunRefusal) -> QualificationRunCollection { - QualificationRunRefused { attempt: attempt, run_id: run_id, refusal: refusal } +// THE DECISION, WITHOUT I/O, over what was read. Its verdict carries the evidence and no sealed +// payload: minting CollectedQualificationRun is collect_qualification_run's alone, so this fold is +// open to supplied readbacks and has no success constructor to proxy. +type QualificationRunVerdict + = RunVerdictCollected { run: WorkflowRun, floor_job: WorkflowJobRun, conclusion: WorkflowRunConclusion } + | RunVerdictRefused { refusal: QualificationRunRefusal } + +fn decide_floor_job(subject: QualificationDispatchSubject, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { + let floors = jobs |> filter(j => j.name == (subject.floor_job_name as String)) + match list_head(floors) { + Absent => RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: 0 } } + Present { value: floor } => + if count(floors) != 1 { + RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: count(floors) } } + } else if optional_int_is(o: floor.runner_id, expected: subject.runner_id) && job_started(job: floor) { + RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } + } else { + RunVerdictRefused { + refusal: RunNeverReachedAttemptRunner { + minted_runner_id: subject.runner_id, + observed_runner_id: floor.runner_id, + observed_runner_name: floor.runner_name, + started: job_started(job: floor), + }, + } + } + } } -fn conclude_qualification_run( - attempt: NonEmptyStr, +fn decide_qualification_run( + subject: QualificationDispatchSubject, run_id: Int, - run_outcome: RestOutcome, + run_performance: RestExchangePerformance, run: WorkflowRun, - jobs_outcome: RestOutcome, + jobs_performance: RestExchangePerformance, jobs: WorkflowJobRunList, -) -> QualificationRunCollection - admit_callers: [ - decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_run"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_or_unconcluded_run_refuses_rather_than_collecting"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_truncated_or_unreadable_readback_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_readback_of_another_run_refuses_on_the_subject"), - ] -{ - match run_outcome { - RestOk => - if run.id != run_id || run.event != "workflow_dispatch" { - refuse_run(attempt: attempt, run_id: run_id, refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, foreign_job_run_ids: [] }) +) -> QualificationRunVerdict { + match run_performance { + RestExchangeSucceeded => + if run.id != run_id || run.event != "workflow_dispatch" || !optional_text_is(o: run.path, expected: qualification_workflow_path() as String) { + RunVerdictRefused { refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, observed_path: run.path, foreign_jobs: [] } } + } else if run.run_attempt != dispatched_run_attempt { + RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: run.run_attempt } } + } else if (run.head_sha as String) != (subject.revision as String) { + RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: subject.revision, observed: run.head_sha } } } else { - match run.status { - Completed => - match run.conclusion { - Absent => refuse_run(attempt: attempt, run_id: run_id, refusal: RunCompletedWithoutConclusion) - Present { value: conclusion } => - match jobs_outcome { - RestOk => - if count(jobs.jobs) != jobs.total_count { - refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsTruncated { returned: count(jobs.jobs), total: jobs.total_count }) - } else { - let foreign = jobs.jobs |> filter(j => j.run_id != run_id) |> map(j => j.run_id) - if count(foreign) != 0 { - refuse_run(attempt: attempt, run_id: run_id, refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, foreign_job_run_ids: foreign }) + match run.status { + Completed => + match run.conclusion { + Absent => RunVerdictRefused { refusal: RunCompletedWithoutConclusion } + Present { value: conclusion } => + match jobs_performance { + RestExchangeSucceeded => + if count(jobs.jobs) != jobs.total_count { + RunVerdictRefused { refusal: RunJobsTruncated { returned: count(jobs.jobs), total: jobs.total_count } } } else { - let served = jobs.jobs |> filter(j => job_served_by_attempt(job: j, attempt: attempt)) - if count(served) == 0 { - refuse_run(attempt: attempt, run_id: run_id, refusal: RunNeverReachedAttemptRunner { job_labels: jobs.jobs |> map(j => j.labels) }) - } else { - QualificationRunCollected { - collected: CollectedQualificationRun { - attempt: attempt, - run_id: run_id, - conclusion: conclusion, - attempt_jobs: served, - instruments_unextracted: qualification_instruments, - }, + let foreign = jobs.jobs |> filter(j => j.run_id != run_id || j.run_attempt != dispatched_run_attempt) |> map(j => j.id) + if count(foreign) != 0 { + RunVerdictRefused { refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, observed_path: run.path, foreign_jobs: foreign } } + } else { + decide_floor_job(subject: subject, run: run, conclusion: conclusion, jobs: jobs.jobs) } } - } - } - other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunJobsUnreadable { outcome: other }) - } - } - other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunNotCompleted { status: other }) - } + other => RunVerdictRefused { refusal: RunJobsUnreadable { performance: other } } + } + } + other => RunVerdictRefused { refusal: RunNotCompleted { status: other } } + } } - other => refuse_run(attempt: attempt, run_id: run_id, refusal: RunReadUnreadable { outcome: other }) - } -} - -fn qualification_run_collected(c: QualificationRunCollection) -> Bool { - match c { - QualificationRunCollected { collected: _ } => true - QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false + other => RunVerdictRefused { refusal: RunReadUnreadable { performance: other } } } } -fn collect_qualification_run(dispatched: DispatchedQualificationRun) -> QualificationRunCollection uses net: Network { - let target = dispatched.target - let run_id = dispatched.receipt.workflow_run_id as String - let read = github.WorkflowRuns.GetRun(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) - let jobs = github.WorkflowRuns.ListJobs(auth_token: "", owner: target.owner as String, repo: target.repo as String, run_id: run_id) - conclude_qualification_run( - attempt: dispatched.attempt, - run_id: dispatched.receipt.workflow_run_id, - run_outcome: read.outcome, +fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { + let run_id = dispatched.receipt.workflow_run_id + let owner = gunbc_repository.owner as NonEmptyStr + let repo = gunbc_repository.name as NonEmptyStr + let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) + let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: dispatched_run_attempt) + match decide_qualification_run( + subject: dispatched.subject, + run_id: run_id, + run_performance: read.performance, run: read.run, - jobs_outcome: jobs.outcome, - jobs: jobs.result, - ) + jobs_performance: jobs.performance, + jobs: jobs.jobs, + ) { + RunVerdictRefused { refusal: r } => QualificationRunRefused { run_id: run_id, refusal: r } + RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } => + QualificationRunCollected { + collected: CollectedQualificationRun { + dispatched: dispatched, + run: run, + floor_job: floor, + conclusion: conclusion, + instruments_unextracted: qualification_instruments, + }, + } + } } // THE EXTRACTION FRONTIER, NAMED AT THE CAPABILITY GRAIN. Four instruments are run outputs and are @@ -244,8 +510,8 @@ fn collect_qualification_run(dispatched: DispatchedQualificationRun) -> Qualific // are named here so the extraction leg does not go looking for them in the run. data qualification_instrument_extraction_frontier_rows: List = [ frontier_row_decl( - ref: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "QualificationRunCollection"), + ref: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "CollectedQualificationRun"), reason: "a collected qualification run carries every qualification_instrument as unextracted: extdeps.github has no Actions artifacts or job-logs read operation, no path that follows the logs redirect or opens the artifact archive, and no parser for the [floor-phase] and [floor-cgroup] log rows. Two instruments are not run outputs: GuestIdleMeminfoAndNproc comes from the boot leg's runner-host-up readback, and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort", - dissolution: unbound_dissolution(description: "the Actions artifacts and job-logs read path (list and download a run's artifacts, download a job's log text) plus the floor row parsers are SUFFICIENT to populate FloorPhaseRows, FloorCgroupRows, ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from one collected run through the producer each instrument names, with GuestIdleMeminfoAndNproc read from the boot leg's runner-host-up readback and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort, so QualificationRunCollected carries readings for every qualification_instrument and no unextracted list. NOT satisfied by an artifacts operation with no parser, nor by a parser exercised only on a fixture"), + dissolution: unbound_dissolution(description: "the Actions artifacts and job-logs read path (list and download a run's artifacts, download a job's log text) plus the floor row parsers are SUFFICIENT to populate FloorPhaseRows, FloorCgroupRows, ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from one collected run's floor job through the producer each instrument names, with GuestIdleMeminfoAndNproc read from the boot leg's runner-host-up readback and ConcurrentCohortSeatWindows from product.capacity.cohort_observation seal_cohort, so CollectedQualificationRun carries readings for every qualification_instrument and no unextracted list. NOT satisfied by an artifacts operation with no parser, nor by a parser exercised only on a fixture"), ) ] diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index f9fdc7d96a7..9f708ff0556 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -239,7 +239,7 @@ data mtcollins1_registration_leg: RouteLegStanding = LegAuthorityImplemented { data mtcollins1_dispatch_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), - wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a route fold calling dispatch_qualification_floor with a QualificationWorkflowTarget naming the qualification workflow, whose workflow_dispatch declares qualification_attempt_dispatch_input and whose runs-on reads it, then collect_qualification_run over the DispatchedQualificationRun it returns; SUFFICIENT FOR the floor run of each attempt being served only by that attempt's registration and read back as collected or as a typed refusal" as NonEmptyStr, + wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a floor job in fleet_converge_workflow whose runs-on is exactly the qualification_attempt_dispatch_input it declares, and a route fold that takes the slot host's unit hold, mints the registration, builds qualification_dispatch_subject from it, calls dispatch_qualification_floor and then collect_qualification_run over the DispatchedQualificationRun it returns, reconciling a commit-ambiguous dispatch before any retry; SUFFICIENT FOR the floor run of each attempt being served only by the runner minted for that attempt and read back as collected or as a typed refusal" as NonEmptyStr, } data mtcollins1_deregistration_leg: RouteLegStanding = LegAuthorityImplemented { diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 36f6a720954..d659b3bbdf5 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -24,12 +24,13 @@ import gunbc.auth.privileged_effect_census { site_verdict, Conforms, DivergesWithReason, Diverges, SiteNotDecidable, mtcollins1_boot_effect, privileged_effect_interlocks, interlocked_sites_missing_from_census, rulings_without_a_rostered_interlock, ruling_roots_missing_from_census, census_sites_outside_ruling_roots, + runner_lifecycle_ruling_scope_interpretation, runner_lifecycle_operator_ruling, RulingScopeInterpretation, ruling_discharge_for, } import std.human_intervention { HumanIntervention, HumanInterventionIdentity, FleetOnce, DeviceOnce, EveryProvision, BreakGlassOnly, NoDischargeSiteModeled, } -import std.decl_ref { declaration_ref_eq, declaration_ref_display_key } +import std.decl_ref { decl_ref, declaration_ref_eq, declaration_ref_display_key } import std.types { NonEmptyStr, String, Int, List } import std.logic { Bool } @@ -401,3 +402,39 @@ test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federa && selected_federated(s: v.selection) && match v.conformance { Conforms => true _ => false })) } + +// THE DISPATCH'S COVERAGE IS THE INTERPRETATION'S, NOT THE RULING TEXT'S. The census row's +// discharge reads runner_lifecycle_ruling_scope_interpretation; narrowed to registration and +// deregistration -- the ruling's own words -- the same irreversible dispatch effect is no longer +// discharged and the selection stops answering the federated grant. The ruling text is unchanged +// either way, and its interlock is the host-generic unit hold. +test fn the_dispatch_is_federated_only_while_the_interpretation_names_it() -> Bool { + let row = list_find(privileged_effect_census, r => (r.site.decl_name as String) == "dispatch_qualification_floor") + let narrowed = RulingScopeInterpretation { + ruling: runner_lifecycle_operator_ruling, + covers: [ + decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), + decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), + ], + interpreted_by: runner_lifecycle_ruling_scope_interpretation.interpreted_by, + interpreted_on: runner_lifecycle_ruling_scope_interpretation.interpreted_on, + standing: "narrowed to the ruling's own words" as NonEmptyStr, + } + (runner_lifecycle_operator_ruling.interlock.decl_name as String) == "UnitHoldProof" + && (runner_lifecycle_operator_ruling.interlock.module_path as String) == "gunbc.managed_host_unit_hold" + && match row { + Present { value: r } => + selected_federated(s: select_authorization_pattern(e: r.effect)) + && !selected_federated(s: select_authorization_pattern(e: PrivilegedEffect { + subject: r.effect.subject, + frequency: r.effect.frequency, + reversibility: r.effect.reversibility, + surface: r.effect.surface, + workload_identity: r.effect.workload_identity, + minted_reach: r.effect.minted_reach, + billing: r.effect.billing, + witness_discharge: ruling_discharge_for(interpretation: narrowed, site_ref: r.site), + })) + Absent => false + } +} diff --git a/dag/test/claim/public_workload_census_witness_test.dag b/dag/test/claim/public_workload_census_witness_test.dag index 45efa581a32..b7488718758 100644 --- a/dag/test/claim/public_workload_census_witness_test.dag +++ b/dag/test/claim/public_workload_census_witness_test.dag @@ -74,6 +74,8 @@ data candidate_biome_blob_mismatch_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -123,6 +125,8 @@ data candidate_biome_matrix_selector_mismatch_control: WorkloadCandidate = Workl status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -178,6 +182,8 @@ data candidate_pdns_axis_name_mismatch_control: WorkloadCandidate = WorkloadCand status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -238,6 +244,8 @@ data candidate_pdns_axis_name_swap_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -291,6 +299,8 @@ data candidate_biome_job_key_mismatch_control: WorkloadCandidate = WorkloadCandi status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -339,6 +349,8 @@ data candidate_biome_job_key_titlecase_uninspected_control: WorkloadCandidate = status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -387,6 +399,8 @@ data candidate_biome_multi_label_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["self-hosted", "linux", "arm64"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -435,6 +449,8 @@ data candidate_empty_labels_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -483,6 +499,8 @@ data candidate_absent_conclusion_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Absent, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -531,6 +549,8 @@ data candidate_run_attempt_zero_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -579,6 +599,8 @@ data candidate_empty_matrix_axes_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 4744874c42b..7249fab5a8a 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -1,52 +1,243 @@ module test.claim.runner_qualification_dispatch_witness -import std.types { Bool, Int, List, NonEmptyStr, String } +import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import extdeps.transports.rest { RestOk, RestStatusRefused, RestTransportRefused } +import product.placement_supply { HostIdentity } +import extdeps.transports.rest { + RestExchangeSucceeded, RestExchangeStatusRefused, RestExchangeUnreached, + RestOutcome, RestOk, RestTransportRefused, RestStatusRefused, RestBodyUndecodable, RestMutationExchange, classify_rest_outcome, +} +import extdeps.github.actions { Workflow, Job, WorkflowDispatch, RunsOnExpression, SelfHosted, RunnerSpec } +import extdeps.github.effect { DispatchWorkflow } import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, } -import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, qualification_instruments } -import gunbc.fleet_intent_network { operator_host_mtcollins1 } +import gunbc.managed_host { ManagedHost, managed_hosts, managed_host_binding } +import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } +import gunbc.runner_jit_mint { HostUnitSlotBinding } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import gunbc.runner.runner_jit_perform { JitMintDispatch, JitCredentialDelivery, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } import gunbc.runner.runner_qualification_dispatch { - qualification_attempt_dispatch_input, qualification_dispatch_inputs, - QualificationRunCollection, QualificationRunCollected, QualificationRunRefused, - RunReadUnreadable, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, - RunJobsTruncated, RunNeverReachedAttemptRunner, RunSubjectMismatch, - conclude_qualification_run, qualification_run_collected, + qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_selected_workflow, + qualification_workflow_path, qualification_dispatch_effect, + QualificationDispatchSubject, QualificationSubjectAdmitted, QualificationSubjectRefused, + SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorRunsOnNotTheAttemptInput, + qualification_dispatch_subject, qualification_dispatch_subject_over, + qualification_dispatch_standing, QualificationDispatchStanding, + DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, + hold_covers_subject, + decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, + RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunNotCompleted, + RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, RunNeverReachedAttemptRunner, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -data attempt: NonEmptyStr = "q-20261004-1" - -// THE DISPATCH KEY IS THE DECLARED INPUT'S NAME AND ITS VALUE IS THE ATTEMPT -- the same label the -// slot registers under (ephemeral_slot_labels), so runs-on fed from this input selects exactly that -// registration. Renaming the input row, or sending any other value, turns this red. -test fn the_dispatch_carries_the_attempt_label_under_the_declared_input() -> Bool { - let inputs = qualification_dispatch_inputs(attempt: attempt) - count(map_keys(inputs)) == 1 - && match map_get(inputs, qualification_attempt_dispatch_input.name) { - Present { value: v } => v == (attempt as String) - && (ephemeral_slot_labels(host: operator_host_mtcollins1, attempt: attempt) |> any(l => l == v)) - Absent => false +data attempt: NonEmptyStr = "q-1" +data branch: NonEmptyStr = "main" +data revision: CommitSha = "1111111111111111111111111111111111111111" +data minted_runner: Int = 4242 +data floor_job_id: NonEmptyStr = "qualification-floor" + +// ── THE SUBJECT, THROUGH THE REAL MINT ────────────────────────────────────────────────────────── +// The registration and the delivered credential come from the route's own JIT mint over the real +// managed-host roster: dispatch_jit_mint, jit_registration_of, and receive_jit_mint over a 201 +// decoded by upstream's own outcome fold (runner id 4242). Nothing below constructs a registration, +// and every caller of the sealed subject mint is a claim returning Bool (no constructor proxy). + +fn host() -> ManagedHost { + match list_head(managed_hosts()) { + Present { value: h } => h + Absent => panic("managed_hosts is empty") + } +} + +fn minted(slot_workflow: NonEmptyStr) -> JitMintDispatch { + dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: host()), attempt: attempt, workflow: slot_workflow) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: slot_workflow), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) +} + +fn registration(slot_workflow: NonEmptyStr) -> JitRegistration { + match jit_registration_of(dispatch: minted(slot_workflow: slot_workflow)) { + Present { value: r } => r + Absent => panic("the fixture mint was refused") + } +} + +fn delivery(slot_workflow: NonEmptyStr) -> JitCredentialDelivery { + receive_jit_mint(dispatch: minted(slot_workflow: slot_workflow), outcome: minted_outcome) +} + +fn qualification_slot() -> NonEmptyStr { + qualification_selected_workflow(branch: branch) +} + +fn floor_job(runs_on: RunnerSpec) -> Job { + Job { + id: floor_job_id as String, + name: Present { value: "qualification floor" }, + runner: runs_on, + environment: none, + steps: [], + needs: [], + env: none, + outputs: none, + if_condition: none, + timeout_minutes: none, + continue_on_error: none, + concurrency: none, + permissions: none, + } +} + +data attempt_runs_on: RunnerSpec = RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}" } + +fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: if declares_input { [qualification_attempt_dispatch_input] } else { [] } }], + concurrency: none, + jobs: [floor_job(runs_on: runs_on)], + env: none, + permissions: none, + } +} + +// POSITIVE CONTROL AND THE ROUTE: the subject carries the slot's own attempt and host, the runner id +// GitHub returned for that registration, and the floor job's name; the dispatch sends exactly the +// declared input carrying that attempt, which is a label the slot registered; the effect names the +// generated fleet-converge workflow file in gunbc_repository at the pinned revision. +test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => + s.attempt == attempt && s.runner_id == minted_runner + && (s.floor_job_name as String) == "qualification floor" + && (s.host as String) == (host().host as String) + && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt), qualification_attempt_dispatch_input.name) { + Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: host().host, attempt: attempt) |> any(l => l == v)) + Absent => false + }) + && (match qualification_dispatch_effect(subject: s) { + DispatchWorkflow { repository: r, workflow: w, subject: sha } => + r.full_name == "gunb-ai/gunbc" && (w.file_name as String) == "fleet-converge.yml" && (sha as String) == (revision as String) + _ => false + }) + QualificationSubjectRefused { refusal: _ } => false + } +} + +fn slot_refuses(slot_workflow: NonEmptyStr) -> Bool { + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: slot_workflow), delivery: delivery(slot_workflow: slot_workflow), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) + _ => false + } +} + +fn runs_on_refuses(runs_on: RunnerSpec) -> Bool { + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true + _ => false + } +} + +fn undeclared_input_refuses() -> Bool { + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true + _ => false + } +} + +// THE RULING'S SCOPE IS CONSTRUCTION, AND THIS IS ITS RED. Through the one mint, no other workflow +// and no other label can become a dispatch subject: +// - a slot registered for another workflow file, or for the qualification path on another branch, +// refuses before the workflow model is read; +// - a floor job whose runs-on is a literal label, another input, the attempt input composed with +// anything else, or a bare string, refuses; +// - a workflow that does not declare the input refuses; +// - the production mint, over today's generated fleet-converge model (no qualification mode yet), +// refuses rather than admitting anything. +test fn no_other_workflow_or_label_can_be_dispatched_through_the_ruling() -> Bool { + slot_refuses(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") + && slot_refuses(slot_workflow: qualification_selected_workflow(branch: "feature")) + && runs_on_refuses(runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refuses(runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refuses(runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refuses(runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && undeclared_input_refuses() + && (match qualification_dispatch_subject( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: _ } => false + QualificationSubjectRefused { refusal: _ } => true + }) +} + +fn mutation_standing(rest: RestOutcome) -> QualificationDispatchStanding { + qualification_dispatch_standing(performance: classify_rest_outcome(standing: RestMutationExchange { rest: rest })) +} + +fn ambiguous(s: QualificationDispatchStanding) -> Bool { + match s { DispatchStandingCommitAmbiguous { detail: _ } => true _ => false } +} + +// THREE STANDINGS, FROM THE MUTATION CLASSIFIER: a decided 422 is refused; no response, a 503, and +// an undecodable 200 are commit-ambiguous; only a succeeded exchange dispatches. And the interlock: +// the hold covers the slot's own host and no other. +test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { + (match mutation_standing(rest: RestStatusRefused { status: 422, body: "Unexpected inputs provided" }) { DispatchStandingRefused { status: st, body: _ } => st == 422 _ => false }) + && ambiguous(s: mutation_standing(rest: RestTransportRefused { cause: "fixture: connection reset after send" })) + && ambiguous(s: mutation_standing(rest: RestStatusRefused { status: 503, body: "fixture" })) + && ambiguous(s: mutation_standing(rest: RestBodyUndecodable { status: 200, cause: "fixture: truncated body" })) + && (match mutation_standing(rest: RestOk) { DispatchStandingSucceeded => true _ => false }) + && match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => + hold_covers_subject(held: host().host, subject: s) && !hold_covers_subject(held: "fixture-other-host" as HostIdentity, subject: s) + QualificationSubjectRefused { refusal: _ } => false } - && qualification_attempt_dispatch_input.required } -fn run(status_completed: Bool, concluded: Bool) -> WorkflowRun { +// ── THE COLLECTION ───────────────────────────────────────────────────────────────────────────── + +fn run(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool) -> WorkflowRun { WorkflowRun { - id: 77, + id: id, name: none, - path: none, - head_sha: "abc123", - head_branch: none, - event: "workflow_dispatch", - status: if status_completed { Completed } else { InProgress }, + path: Present { value: path }, + head_sha: sha, + head_branch: Present { value: branch as String }, + event: event, + status: if completed { Completed } else { InProgress }, conclusion: if concluded { Present { value: Failure } } else { none }, workflow_id: 9, run_number: 1, - run_attempt: 1, + run_attempt: attempt_n, actor: none, triggering_actor: none, html_url: "https://github.com/gunb-ai/gunbc/actions/runs/77", @@ -56,107 +247,138 @@ fn run(status_completed: Bool, concluded: Bool) -> WorkflowRun { } } -fn job(labels: List, started: Bool) -> WorkflowJobRun { +fn dispatched_run(attempt_n: Int) -> WorkflowRun { + run(id: 77, attempt_n: attempt_n, event: "workflow_dispatch", path: qualification_workflow_path() as String, sha: revision, completed: true, concluded: true) +} + +fn job(id: Int, name: String, runner_id: Int, started: Bool, attempt_n: Int) -> WorkflowJobRun { WorkflowJobRun { - id: 500, + id: id, run_id: 77, - run_attempt: 1, - name: "floor", + run_attempt: attempt_n, + name: name, status: Completed, conclusion: Present { value: Success }, - labels: labels, + labels: [attempt as String], + runner_id: Present { value: runner_id }, + runner_name: Present { value: "fixture-runner" }, created_at: Present { value: "2026-10-04T00:00:10Z" }, started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, completed_at: Present { value: "2026-10-04T00:50:00Z" }, } } +fn floor_on(runner_id: Int) -> WorkflowJobRun { + job(id: 1, name: "qualification floor", runner_id: runner_id, started: true, attempt_n: 1) +} + fn jobs(js: List) -> WorkflowJobRunList { WorkflowJobRunList { total_count: count(js), jobs: js } } - -// THE POSITIVE CONTROL: a completed run whose job was taken under the attempt label is collected, -// its conclusion is the one read back (a FAILED floor is still collected and says so), and every -// qualification_instrument is carried as unextracted -- the declared extraction frontier, not a -// silent empty reading. -test fn a_completed_run_served_by_the_attempt_runner_is_collected_with_its_conclusion() -> Bool { - match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: ["self-hosted", attempt as String], started: true)])) { - QualificationRunCollected { collected: c } => - c.attempt == attempt && c.run_id == 77 && count(c.attempt_jobs) == 1 - && (match c.conclusion { Failure => true _ => false }) - && count(c.instruments_unextracted) == count(qualification_instruments) - QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => false +fn never_reached(v: QualificationRunVerdict) -> Bool { + match v { + RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { minted_runner_id: m, observed_runner_id: _, observed_runner_name: _, started: _ } } => m == minted_runner + _ => false } } -// THE BRIEF'S REFUSAL: the run completed but no job carried the attempt label, or the one that did -// never started (no runner under the attempt took it). Both refuse, and carry the labels observed. -test fn a_run_the_attempt_runner_never_served_refuses_with_the_labels_it_saw() -> Bool { - let other = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: ["self-hosted", "srv1"], started: true)])) - let unstarted = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job(labels: [attempt as String], started: false)])) - (match other { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunNeverReachedAttemptRunner { job_labels: ls } } => - count(ls) == 1 && (ls |> all(l => l |> any(x => x == "srv1"))) +fn mismatch(v: QualificationRunVerdict) -> Bool { + match v { + RunVerdictRefused { refusal: RunSubjectMismatch { observed_run_id: _, observed_event: _, observed_path: _, foreign_jobs: _ } } => true _ => false - }) - && !qualification_run_collected(c: unstarted) - && (match unstarted { QualificationRunRefused { attempt: _, run_id: _, refusal: RunNeverReachedAttemptRunner { job_labels: _ } } => true _ => false }) + } } -test fn an_unfinished_or_unconcluded_run_refuses_rather_than_collecting() -> Bool { - let served = jobs(js: [job(labels: [attempt as String], started: true)]) - (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: false, concluded: false), jobs_outcome: RestOk, jobs: served) { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunNotCompleted { status: InProgress } } => true - _ => false - }) - && (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: false), jobs_outcome: RestOk, jobs: served) { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunCompletedWithoutConclusion } => true - _ => false - }) +// THE RUNNER IS JOINED BY IDENTITY. Positive control: the named floor job on the minted runner id is +// collected with the run's own conclusion (a Failure is still this host's run). REDs: the same +// labels on another runner; a helper job on the minted runner while the floor ran elsewhere; a floor +// on the minted runner that never started; two jobs answering to the floor's name. +test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => { + let decide = (js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: js) + (match decide(jobs(js: [floor_on(runner_id: minted_runner)])) { + RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) + _ => false + }) + && never_reached(v: decide(jobs(js: [floor_on(runner_id: 99)]))) + && never_reached(v: decide(jobs(js: [ + job(id: 2, name: "helper", runner_id: minted_runner, started: true, attempt_n: 1), + floor_on(runner_id: 99), + ]))) + && never_reached(v: decide(jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: false, attempt_n: 1)]))) + && (match decide(jobs(js: [floor_on(runner_id: minted_runner), job(id: 2, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 + _ => false + }) + } + QualificationSubjectRefused { refusal: _ } => false + } } -// A PAGE SHORTER THAN total_count IS NOT A JOB LISTING: the attempt's job could sit on the unread -// page, so the fold refuses rather than reporting the runner never reached. -test fn a_truncated_or_unreadable_readback_refuses() -> Bool { - let truncated = WorkflowJobRunList { total_count: 2, jobs: [job(labels: [attempt as String], started: true)] } - (match conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: truncated) { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunJobsTruncated { returned: 1, total: 2 } } => true - _ => false - }) - && (match conclude_qualification_run( - attempt: attempt, run_id: 77, - run_outcome: RestStatusRefused { status: 404, body: "Not Found" }, run: run(status_completed: true, concluded: true), - jobs_outcome: RestOk, jobs: jobs(js: []), - ) { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunReadUnreadable { outcome: _ } } => true - _ => false - }) - && (match conclude_qualification_run( - attempt: attempt, run_id: 77, - run_outcome: RestOk, run: run(status_completed: true, concluded: true), - jobs_outcome: RestTransportRefused { cause: "unreached" }, jobs: jobs(js: []), - ) { - QualificationRunRefused { attempt: _, run_id: _, refusal: RunJobsUnreadable { outcome: _ } } => true - _ => false - }) +// THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT. REDs: the run re-run to attempt 2 (whose floor +// job carries the same labels and runner); a job of attempt 2 in the listing; another run id; +// another event; another workflow path. +test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { + let floor = jobs(js: [floor_on(runner_id: minted_runner)]) + let path = qualification_workflow_path() as String + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => { + let decide = (r: WorkflowRun, js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) + (match decide(dispatched_run(attempt_n: 2), floor) { + RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 + _ => false + }) + && mismatch(v: decide(dispatched_run(attempt_n: 1), jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 2)]))) + && mismatch(v: decide(run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), floor)) + && mismatch(v: decide(run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), floor)) + && mismatch(v: decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), floor)) + } + QualificationSubjectRefused { refusal: _ } => false + } } -// THE SUBJECT IS THE DISPATCHED RUN: a readback naming another run id, or listing a job of another -// run, refuses as RunSubjectMismatch even when everything else would collect -- the served job -// below carries the attempt label and started, so only the subject check can turn it red. -test fn a_readback_of_another_run_refuses_on_the_subject() -> Bool { - let served = jobs(js: [job(labels: [attempt as String], started: true)]) - let other_run = conclude_qualification_run( - attempt: attempt, run_id: 78, - run_outcome: RestOk, run: run(status_completed: true, concluded: true), - jobs_outcome: RestOk, jobs: served, - ) - let foreign_job = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [WorkflowJobRun { - id: 501, run_id: 99, run_attempt: 1, name: "floor", status: Completed, - conclusion: Present { value: Success }, labels: [attempt as String], - created_at: none, started_at: Present { value: "2026-10-04T00:01:00Z" }, completed_at: none, - }])) - (match other_run { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: 77, observed_event: _, foreign_job_run_ids: _ } } => true _ => false }) - && (match foreign_job { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: _, observed_event: _, foreign_job_run_ids: f } } => count(f) == 1 && (f |> all(x => x == 99)) _ => false }) +test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { + let floor = jobs(js: [floor_on(runner_id: minted_runner)]) + let path = qualification_workflow_path() as String + match qualification_dispatch_subject_over( + registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => { + let decide = (r: WorkflowRun, js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) + (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), floor) { + RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true + _ => false + }) + && (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), floor) { + RunVerdictRefused { refusal: RunCompletedWithoutConclusion } => true + _ => false + }) + && (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), floor) { + RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: _, observed: _ } } => true + _ => false + }) + && (match decide(dispatched_run(attempt_n: 1), WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { + RunVerdictRefused { refusal: RunJobsTruncated { returned: rt, total: t } } => rt == 1 && t == 2 + _ => false + }) + && (match decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeUnreached { cause: "fixture" }, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: floor) { + RunVerdictRefused { refusal: RunReadUnreadable { performance: _ } } => true + _ => false + }) + && (match decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeStatusRefused { status: 404, body: "fixture" }, jobs: floor) { + RunVerdictRefused { refusal: RunJobsUnreadable { performance: _ } } => true + _ => false + }) + } + QualificationSubjectRefused { refusal: _ } => false + } } diff --git a/dag/test/claim/superseded_run_starvation_census_witness_test.dag b/dag/test/claim/superseded_run_starvation_census_witness_test.dag index e5da6157f55..96091a15fd6 100644 --- a/dag/test/claim/superseded_run_starvation_census_witness_test.dag +++ b/dag/test/claim/superseded_run_starvation_census_witness_test.dag @@ -213,6 +213,8 @@ data job_cancelled: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Cancelled }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:01:00Z" }, started_at: Present { value: "2026-09-01T00:10:00Z" }, completed_at: Present { value: "2026-09-01T00:20:00Z" }, @@ -226,6 +228,8 @@ data job_failed: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Failure }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:00:00Z" }, started_at: Present { value: "2026-09-01T00:00:00Z" }, completed_at: Present { value: "2026-09-01T00:15:00Z" }, @@ -239,6 +243,8 @@ data job_success: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:02:00Z" }, started_at: Present { value: "2026-09-01T00:05:00Z" }, completed_at: Present { value: "2026-09-01T00:10:00Z" }, @@ -252,6 +258,8 @@ data job_queued: WorkflowJobRun = WorkflowJobRun { status: Queued, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:03:00Z" }, started_at: none, completed_at: none, @@ -265,6 +273,8 @@ data job_in_progress: WorkflowJobRun = WorkflowJobRun { status: InProgress, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:04:00Z" }, started_at: Present { value: "2026-09-01T00:30:00Z" }, completed_at: none, @@ -339,6 +349,8 @@ test fn earliest_created_at_returns_none_when_no_created_at() -> Bool { status: InProgress, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: none, started_at: none, completed_at: none, From d2021c086ba9611bc083551df12720a9728e7bbf Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:40:54 +0000 Subject: [PATCH 12/44] leg 3b: address review on 3be84a5ee3 -- sealed extraction subject, one designated job, sealed receipt with evidence, per-source standing, collect leg not wired 1. The collect stage carries a RouteLegStanding (mtcollins1_collect_leg, LegAuthorityImplemented) that counts toward route_is_executable; the log instruments keep a frontier row until the route invokes collect_qualification_instruments. 2. collect_qualification_instruments takes the sealed DispatchedQualificationRun, the collection and the authorized JitRegistration -- no target or unit argument. QualificationExtractionSubject (sole_constructor) is minted only when the run, attempt and host-unit slot agree. 3. QualificationJobLogReceipt (sole_constructor) is minted only by job_log_standing (admit_callers: collect_qualification_instruments) after the network read, and carries the subject, the log's code-point length and content digest, and the rows. The decoder stays open and identity-free. 4. The floor job is the one served job whose runner_name is the registration's name; zero or two refuse, and only that job's log is read. WorkflowJobRun regains runner_name with this consumer. 5. Per-source standing (QualificationJobLogInstrumentsRead / claim_cost) replaces the instruments-read arm; wall is std.measure Millisecond; new operations use 5xx; a download 410 is ClaimCostArtifactExpired. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/actions_artifacts.dag | 4 +- dag/extdeps/github/workflow_runs.dag | 10 +- dag/gunbc/public_workload_census.dag | 8 + dag/gunbc/required_ci_phase_roster.dag | 6 +- .../runner/runner_qualification_dispatch.dag | 362 ++++++++++++++---- .../runner_throughput_qualification_route.dag | 29 +- .../public_workload_census_witness_test.dag | 11 + ...er_qualification_dispatch_witness_test.dag | 241 ++++++++---- ..._throughput_qualification_witness_test.dag | 12 +- ...ded_run_starvation_census_witness_test.dag | 6 + 10 files changed, 506 insertions(+), 183 deletions(-) diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag index eb27e917c71..4fc524f6671 100644 --- a/dag/extdeps/github/actions_artifacts.dag +++ b/dag/extdeps/github/actions_artifacts.dag @@ -91,7 +91,7 @@ service github.Artifacts { 401 => GitHubErrorShape 403 => GitHubErrorShape 404 => GitHubErrorShape - 500 => GitHubErrorShape + 5xx => GitHubErrorShape } } @@ -119,7 +119,7 @@ service github.Artifacts { 403 => GitHubErrorShape 404 => GitHubErrorShape 410 => GitHubErrorShape - 500 => GitHubErrorShape + 5xx => GitHubErrorShape } } } diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 290f3c1001f..85cabba90b0 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -177,6 +177,13 @@ fn workflow_job_attempt_filter_wire(f: WorkflowJobAttemptFilter) -> String { // original ground, which was right both times. DESIGN section 4c is the rule: an annotation is // never evidence that a machine claim holds. // +// RETURNED 2026-10-04 WITH ITS CONSUMER IN THE SAME CHANGE, exactly as the trigger below required: +// gunbc.runner.runner_qualification_dispatch designated_floor_job reads it to find the one job a +// JIT-registered runner ran, by the name the authorized mint fixed (JitRegistration name). Upstream +// publishes runner_name as nullable -- null until a runner takes the job -- so it is optional here, +// and every record in this tree written before the field returned carries none rather than a +// guessed name. +// // The example survives the deletion because it is the part that was worth keeping. // "depot-1mw2k81zm2" is neither GitHub-hosted nor self-hosted but a third party's fleet, so any // provenance reading with two arms folds a paying provider customer in with a team running its own @@ -203,6 +210,7 @@ type WorkflowJobRun { status: WorkflowRunStatus conclusion: WorkflowRunConclusion? labels: List + runner_name: String? created_at: Timestamp? started_at: Timestamp? completed_at: Timestamp? @@ -451,7 +459,7 @@ service github.WorkflowRuns { 403 => GitHubErrorShape 404 => GitHubErrorShape 410 => GitHubErrorShape - 500 => GitHubErrorShape + 5xx => GitHubErrorShape } } diff --git a/dag/gunbc/public_workload_census.dag b/dag/gunbc/public_workload_census.dag index 19d698ca403..56db757bf0d 100644 --- a/dag/gunbc/public_workload_census.dag +++ b/dag/gunbc/public_workload_census.dag @@ -1020,6 +1020,7 @@ data candidate_biome_test: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -1082,6 +1083,7 @@ data candidate_pdns_dnsdist_arm: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -1149,6 +1151,7 @@ data candidate_openobserve_arm_build: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:21Z" }, completed_at: Present { value: "2026-09-01T10:27:44Z" }, @@ -1218,6 +1221,7 @@ data candidate_openobserve_amd64: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-8"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:15Z" }, completed_at: Absent, @@ -1271,6 +1275,7 @@ data candidate_openobserve_manifest: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:28:12Z" }, completed_at: Absent, @@ -1324,6 +1329,7 @@ data candidate_biome_test_windows: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-windows-2022-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:31Z" }, completed_at: Present { value: "2026-09-04T16:13:50Z" }, @@ -1371,6 +1377,7 @@ data candidate_biome_test_macos: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-macos-latest"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:08:06Z" }, completed_at: Absent, @@ -1416,6 +1423,7 @@ data candidate_openobserve_summary: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-latest"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:29:11Z" }, completed_at: Absent, diff --git a/dag/gunbc/required_ci_phase_roster.dag b/dag/gunbc/required_ci_phase_roster.dag index 7e3b9246e22..27a162b0b4f 100644 --- a/dag/gunbc/required_ci_phase_roster.dag +++ b/dag/gunbc/required_ci_phase_roster.dag @@ -1,6 +1,8 @@ module gunbc.required_ci_phase_roster import std.types { String, Bool, Int, List, NonEmptyStr } +import std.measure { Millisecond, millisecond } +import std.checked_arithmetic { nat_magnitude } import v2.std.optional { Present, Absent } // THE SUBSTRATE AUTHORITY FOR WHICH PHASES REQUIRED CI RUNS, AND WHICH LANE OWNS EACH. @@ -256,7 +258,7 @@ data floor_phase_row_tag: String = "[floor-phase]" // fabricate a timing. A row with no phase word, or a wall_ms that is not a count, is garbled and is // carried with its line, never skipped. type FloorPhaseRow - = FloorPhaseTimed { phase: NonEmptyStr, wall_ms: Int } + = FloorPhaseTimed { phase: NonEmptyStr, wall: Millisecond } | FloorPhaseUntimed { phase: NonEmptyStr } type FloorPhaseRowReading @@ -280,7 +282,7 @@ fn read_floor_phase_row(line: String) -> FloorPhaseRowReading? { if ms < 0 { Present { value: FloorPhaseRowGarbled { line: line, cause: "[floor-phase] wall_ms is negative" as NonEmptyStr } } } else { - Present { value: FloorPhaseRowRead { row: FloorPhaseTimed { phase: phase as NonEmptyStr, wall_ms: ms } } } + Present { value: FloorPhaseRowRead { row: FloorPhaseTimed { phase: phase as NonEmptyStr, wall: millisecond(count: nat_magnitude(a: ms)) } } } } } } diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index e81e5784f87..5d0108de0b9 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -21,6 +21,10 @@ import gunbc.required_ci_phase_roster { read_floor_phase_row, } import gunbc.witness_floor_workflow { required_floor_claim_cost_artifact_name } +import gunbc.runner.runner_jit_deregistration { JitRegistration } +import gunbc.runner.runner_host_unit_slot { HostUnitSlot } +import gunbc.runner_jit_mint { MicroVmJitSlot, HostUnitJitSlot } +import std.content_hash { Fnv1a64Structural, content_hash_atom } import extdeps.github.actions_artifacts { ActionsArtifact, ActionsArtifactList } import gunbc.host_budget_source { FloorCgroupLevelRow, FloorCgroupRowReading, FloorCgroupLevelRead, FloorCgroupRowGarbled, @@ -184,11 +188,10 @@ fn conclude_qualification_run( decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_or_unconcluded_run_refuses_rather_than_collecting"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_truncated_or_unreadable_readback_refuses"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_readback_of_another_run_refuses_on_the_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_log_with_phase_rows_and_the_slot_level_is_read"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_garbled_row_refuses_rather_than_being_skipped"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_log_missing_either_reading_refuses_with_what_it_saw"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unreadable_log_reads_nothing"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_extraction_reads_a_recorded_required_floor_log"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_floor_job_is_the_one_the_registered_runner_ran"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_dispatch_of_another_run_refuses_the_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_of_another_attempt_refuses_the_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "zero_or_two_jobs_on_the_registered_runner_refuse"), ] { match run_outcome { @@ -261,10 +264,11 @@ fn collect_qualification_run(dispatched: DispatchedQualificationRun) -> Qualific // // WHERE EACH INSTRUMENT IS READ FROM IS A TOTAL MATCH, so adding an instrument to // gunbc.runner_throughput_qualification_route QualificationInstrument refuses to compile here until it -// says where its reading comes from. Two are this run's job log, read now. Two are this run's +// says where its reading comes from. Two are the designated floor job's log; two are this run's // required-floor-claim-cost artifact, whose read is attempted on every collection and today ends in a -// typed refusal (read_claim_cost_artifact); two are not outputs of this run at all. Each of those four -// is carried by a row below with the capability that would read it. +// typed refusal (read_claim_cost_artifact); two are not outputs of this run at all. Each of the +// four is carried by a row below with the capability that would read it, and the two log +// instruments carry a row too until the route invokes collect_qualification_instruments. type QualificationInstrumentSource = ReadFromAttemptJobLog | ReadFromClaimCostArtifact @@ -291,57 +295,156 @@ fn qualification_instrument_read_from_job_log(i: QualificationInstrument) -> Boo } } -// ONE SERVED JOB'S LOG, AS GITHUB ANSWERED IT. An unreadable log carries the RestOutcome it came with -// -- a 410 for a log past retention, RestBodyUndecodable for a body the handler could not read whole -- -// and is never read as an empty log. -type AttemptJobLog - = AttemptJobLogRead { job_id: Int, log: String } - | AttemptJobLogUnreadable { job_id: Int, outcome: RestOutcome } +// ── THE SUBJECT: ONE RUN, ONE REGISTERED RUNNER, ONE JOB ────────────────────────────────────── +// +// EVERY IDENTITY THE READS USE COMES FROM ONE SEALED VALUE, never from caller-supplied strings. The +// repository and workflow are the dispatch's (DispatchedQualificationRun target), the run is the one +// that dispatch returned and conclude_qualification_run collected, and the slot is the authorized +// JIT registration's own host-unit slot (gunbc.runner.runner_jit_deregistration JitRegistration, +// minted only from an authorized mint), so the cgroup unit the rows are filtered to is that slot's +// unit and no other. A target from run B beside a collection of run A, or a slot minted for +// another attempt, refuses here, before any network read. +// +// THE FLOOR JOB IS THE ONE JOB THE REGISTERED RUNNER RAN. The collection's served jobs carry the +// attempt label and started; among them, the designated job is the one whose runner_name is the +// name the authorized mint fixed. Zero such jobs, or more than one, refuses: reading every served +// job and pooling their rows would let phase rows from one job and a cgroup row from another +// stand as one measurement. +type QualificationExtractionSubject sole_constructor { + dispatched: DispatchedQualificationRun + collected: CollectedQualificationRun + registration: JitRegistration + slot: HostUnitSlot + floor_job: WorkflowJobRun +} + +type ExtractionSubjectRefusal + = SubjectRunNotCollected { refusal: QualificationRunRefusal } + | SubjectRunMismatch { dispatched_run_id: Int, collected_run_id: Int } + | SubjectAttemptMismatch { dispatched_attempt: NonEmptyStr, other_attempt: NonEmptyStr } + | SubjectSlotNotAHostUnit + | NoJobOnRegisteredRunner { runner_name: NonEmptyStr, runner_names_seen: List } + | SeveralJobsOnRegisteredRunner { runner_name: NonEmptyStr, job_ids: List } + +type DesignatedFloorJob + = FloorJobDesignated { job: WorkflowJobRun } + | FloorJobRefused { refusal: ExtractionSubjectRefusal } + +fn job_ran_on(job: WorkflowJobRun, runner_name: NonEmptyStr) -> Bool { + match job.runner_name { + Present { value: n } => n == (runner_name as String) + Absent => false + } +} + +fn job_runner_name_seen(job: WorkflowJobRun) -> String { + match job.runner_name { + Present { value: n } => n + Absent => "" + } +} + +// THE PURE JOIN, over the identities alone, so each refusal is witnessable without a network read. +fn designated_floor_job( + dispatched_run_id: Int, + dispatched_attempt: NonEmptyStr, + collected: CollectedQualificationRun, + slot_attempt: NonEmptyStr, + runner_name: NonEmptyStr, +) -> DesignatedFloorJob { + if collected.run_id != dispatched_run_id { + FloorJobRefused { refusal: SubjectRunMismatch { dispatched_run_id: dispatched_run_id, collected_run_id: collected.run_id } } + } else if collected.attempt != dispatched_attempt { + FloorJobRefused { refusal: SubjectAttemptMismatch { dispatched_attempt: dispatched_attempt, other_attempt: collected.attempt } } + } else if slot_attempt != dispatched_attempt { + FloorJobRefused { refusal: SubjectAttemptMismatch { dispatched_attempt: dispatched_attempt, other_attempt: slot_attempt } } + } else { + let on_runner = collected.attempt_jobs |> filter(j => job_ran_on(job: j, runner_name: runner_name)) + if count(on_runner) == 0 { + FloorJobRefused { refusal: NoJobOnRegisteredRunner { runner_name: runner_name, runner_names_seen: collected.attempt_jobs |> map(j => job_runner_name_seen(job: j)) } } + } else if count(on_runner) > 1 { + FloorJobRefused { refusal: SeveralJobsOnRegisteredRunner { runner_name: runner_name, job_ids: on_runner |> map(j => j.id) } } + } else { + match on_runner |> first { + Present { value: j } => FloorJobDesignated { job: j } + Absent => FloorJobRefused { refusal: NoJobOnRegisteredRunner { runner_name: runner_name, runner_names_seen: [] } } + } + } + } +} -// THE READINGS. Phase rows are every [floor-phase] row the served jobs printed, timed and untimed, in -// log order; the cgroup rows are only the [floor-cgroup] rows at the SLOT's own level, because the -// levels above it (the runner slice, the system slice) are other subjects' memory and a qualification -// of this slot must not take their peak for its own. +type ExtractionSubjectStanding + = ExtractionSubjectMinted { subject: QualificationExtractionSubject } + | ExtractionSubjectRefused { refusal: ExtractionSubjectRefusal } + +fn qualification_extraction_subject( + dispatched: DispatchedQualificationRun, + collection: QualificationRunCollection, + registration: JitRegistration, +) -> ExtractionSubjectStanding { + match collection { + QualificationRunRefused { attempt: _, run_id: _, refusal: r } => ExtractionSubjectRefused { refusal: SubjectRunNotCollected { refusal: r } } + QualificationRunCollected { collected: collected } => + match registration.slot { + MicroVmJitSlot { attempt: _ } => ExtractionSubjectRefused { refusal: SubjectSlotNotAHostUnit } + HostUnitJitSlot { slot: slot } => + match designated_floor_job( + dispatched_run_id: dispatched.receipt.workflow_run_id, + dispatched_attempt: dispatched.attempt, + collected: collected, + slot_attempt: slot.attempt, + runner_name: registration.name.value, + ) { + FloorJobRefused { refusal: r } => ExtractionSubjectRefused { refusal: r } + FloorJobDesignated { job: j } => + ExtractionSubjectMinted { + subject: QualificationExtractionSubject { dispatched: dispatched, collected: collected, registration: registration, slot: slot, floor_job: j }, + } + } + } + } +} + +// ── THE PURE DECODER ────────────────────────────────────────────────────────────────────────── +// +// It decides what one log's text says and nothing about whose log it is; identity is the subject's +// and the receipt's. It is open so the row shapes are witnessable on supplied and recorded text. +// The phase rows are every [floor-phase] row the log printed, timed and untimed, in log order; the +// cgroup rows are only the [floor-cgroup] rows at the slot's own level, because the levels above it +// are other subjects' memory and a qualification of this slot must not take their peak for its own. type QualificationLogReadings { floor_phase_rows: List slot_cgroup_rows: List } type InstrumentReadRefusal - = JobLogUnreadable { job_id: Int, outcome: RestOutcome } - | FloorPhaseRowGarbledInLog { job_id: Int, line: String, cause: NonEmptyStr } - | FloorCgroupRowGarbledInLog { job_id: Int, line: String, cause: NonEmptyStr } + = FloorPhaseRowGarbledInLog { line: String, cause: NonEmptyStr } + | FloorCgroupRowGarbledInLog { line: String, cause: NonEmptyStr } | FloorPhaseRowsAbsent | SlotCgroupLevelAbsent { slot_unit: NonEmptyStr, levels_seen: List } -type QualificationInstrumentExtraction - = QualificationInstrumentsRead { - attempt: NonEmptyStr - run_id: Int - conclusion: WorkflowRunConclusion - readings: QualificationLogReadings - claim_cost: ClaimCostArtifactRead - } - | QualificationInstrumentsRefused { attempt: NonEmptyStr, run_id: Int, refusal: InstrumentReadRefusal } +type JobLogInstrumentDecode + = JobLogInstrumentsDecoded { readings: QualificationLogReadings } + | JobLogInstrumentsUndecodable { refusal: InstrumentReadRefusal } -// One log's rows, or the first row that could not be read. A garbled row stops the read: a reading -// assembled around a row it skipped would report a phase or a level as absent that the run printed. +// A garbled row stops the read: a reading assembled around a row it skipped would report a phase +// or a level as absent that the run printed. type JobLogRowsReading = JobLogRowsRead { phases: List, levels: List } | JobLogRowsRefused { refusal: InstrumentReadRefusal } -fn read_job_log_line(acc: JobLogRowsReading, job_id: Int, line: String) -> JobLogRowsReading { +fn read_job_log_line(acc: JobLogRowsReading, line: String) -> JobLogRowsReading { match acc { JobLogRowsRefused { refusal: _ } => acc JobLogRowsRead { phases: ps, levels: ls } => match read_floor_phase_row(line: line) { Present { value: FloorPhaseRowGarbled { line: l, cause: c } } => - JobLogRowsRefused { refusal: FloorPhaseRowGarbledInLog { job_id: job_id, line: l, cause: c } } + JobLogRowsRefused { refusal: FloorPhaseRowGarbledInLog { line: l, cause: c } } Present { value: FloorPhaseRowRead { row: r } } => JobLogRowsRead { phases: list_push(ps, r), levels: ls } Absent => match read_floor_cgroup_level_row(line: line) { Present { value: FloorCgroupRowGarbled { line: l, cause: c } } => - JobLogRowsRefused { refusal: FloorCgroupRowGarbledInLog { job_id: job_id, line: l, cause: c } } + JobLogRowsRefused { refusal: FloorCgroupRowGarbledInLog { line: l, cause: c } } Present { value: FloorCgroupLevelRead { row: r } } => JobLogRowsRead { phases: ps, levels: list_push(ls, r) } Absent => acc } @@ -349,61 +452,100 @@ fn read_job_log_line(acc: JobLogRowsReading, job_id: Int, line: String) -> JobLo } } -fn read_job_log_rows(acc: JobLogRowsReading, log: AttemptJobLog) -> JobLogRowsReading { - match acc { - JobLogRowsRefused { refusal: _ } => acc - JobLogRowsRead { phases: _, levels: _ } => - match log { - AttemptJobLogUnreadable { job_id: j, outcome: o } => JobLogRowsRefused { refusal: JobLogUnreadable { job_id: j, outcome: o } } - AttemptJobLogRead { job_id: j, log: text } => - fold(split(s: text, delimiter: "\n"), init: acc, f: (a, line) => read_job_log_line(acc: a, job_id: j, line: line)) - } - } -} - fn level_is_slot(row: FloorCgroupLevelRow, slot_unit: NonEmptyStr) -> Bool { ends_with(s: row.level as String, suffix: concat("/", slot_unit as String)) } -// THE SUPPLIED-INPUT BOUNDARY. Everything the extraction decides is decided here from the sealed -// collection and the reads as they came back; collect_qualification_instruments below is the only -// caller that reaches the network, and it does nothing but perform the reads and hand them in. The -// collection is CollectedQualificationRun and not the collection sum, so an extraction exists only for -// a run conclude_qualification_run admitted -- a refused run has nothing to extract and is not asked. -fn extract_qualification_instruments( - collected: CollectedQualificationRun, - logs: List, - claim_cost: ClaimCostArtifactRead, - slot_unit: NonEmptyStr, -) -> QualificationInstrumentExtraction { - let attempt = collected.attempt - let run_id = collected.run_id - match fold(logs, init: JobLogRowsRead { phases: [], levels: [] }, f: (acc, l) => read_job_log_rows(acc: acc, log: l)) { - JobLogRowsRefused { refusal: r } => QualificationInstrumentsRefused { attempt: attempt, run_id: run_id, refusal: r } +fn phase_row_is_timed(p: FloorPhaseRow) -> Bool { + match p { + FloorPhaseTimed { phase: _, wall: _ } => true + FloorPhaseUntimed { phase: _ } => false + } +} + +fn decode_job_log_instruments(log: String, slot_unit: NonEmptyStr) -> JobLogInstrumentDecode { + match fold(split(s: log, delimiter: "\n"), init: JobLogRowsRead { phases: [], levels: [] }, f: (acc, line) => read_job_log_line(acc: acc, line: line)) { + JobLogRowsRefused { refusal: r } => JobLogInstrumentsUndecodable { refusal: r } JobLogRowsRead { phases: phases, levels: levels } => { let slot_levels = levels |> filter(l => level_is_slot(row: l, slot_unit: slot_unit)) - let timed = phases |> filter(p => match p { FloorPhaseTimed { phase: _, wall_ms: _ } => true FloorPhaseUntimed { phase: _ } => false }) - if count(timed) == 0 { - QualificationInstrumentsRefused { attempt: attempt, run_id: run_id, refusal: FloorPhaseRowsAbsent } + if count(phases |> filter(p => phase_row_is_timed(p: p))) == 0 { + JobLogInstrumentsUndecodable { refusal: FloorPhaseRowsAbsent } } else if count(slot_levels) == 0 { - QualificationInstrumentsRefused { - attempt: attempt, - run_id: run_id, - refusal: SlotCgroupLevelAbsent { slot_unit: slot_unit, levels_seen: levels |> map(l => l.level as String) }, - } + JobLogInstrumentsUndecodable { refusal: SlotCgroupLevelAbsent { slot_unit: slot_unit, levels_seen: levels |> map(l => l.level as String) } } } else { - QualificationInstrumentsRead { - attempt: attempt, - run_id: run_id, - conclusion: collected.conclusion, - readings: QualificationLogReadings { floor_phase_rows: phases, slot_cgroup_rows: slot_levels }, - claim_cost: claim_cost, - } + JobLogInstrumentsDecoded { readings: QualificationLogReadings { floor_phase_rows: phases, slot_cgroup_rows: slot_levels } } } } } } +// ── THE READ, AND THE RECEIPT ONLY THE READ CAN MINT ────────────────────────────────────────── +// +// A log is admitted only if it is the designated job's: the read is addressed by the subject's job +// id, and the join below is what a supplied log of any other job fails. +type AttemptJobLog + = AttemptJobLogRead { job_id: Int, log: String } + | AttemptJobLogUnreadable { job_id: Int, outcome: RestOutcome } + +type JobLogReadRefusal + = JobLogOfAnotherJob { designated_job_id: Int, read_job_id: Int } + | JobLogUnreadable { job_id: Int, outcome: RestOutcome } + | JobLogEmpty { job_id: Int } + | JobLogUndecodable { job_id: Int, refusal: InstrumentReadRefusal } + +type DesignatedJobLog + = DesignatedJobLogText { job_id: Int, log: NonEmptyStr } + | DesignatedJobLogRefused { refusal: JobLogReadRefusal } + +fn designated_job_log(designated_job_id: Int, log: AttemptJobLog) -> DesignatedJobLog { + match log { + AttemptJobLogUnreadable { job_id: j, outcome: o } => + if j != designated_job_id { + DesignatedJobLogRefused { refusal: JobLogOfAnotherJob { designated_job_id: designated_job_id, read_job_id: j } } + } else { + DesignatedJobLogRefused { refusal: JobLogUnreadable { job_id: j, outcome: o } } + } + AttemptJobLogRead { job_id: j, log: text } => + if j != designated_job_id { + DesignatedJobLogRefused { refusal: JobLogOfAnotherJob { designated_job_id: designated_job_id, read_job_id: j } } + } else if text == "" { + DesignatedJobLogRefused { refusal: JobLogEmpty { job_id: j } } + } else { + DesignatedJobLogText { job_id: j, log: text as NonEmptyStr } + } + } +} + +// THE SUCCESS RECEIPT IS SEALED AND CARRIES ITS EVIDENCE. Only collect_qualification_instruments +// writes it, and only after reading the designated job's log over the network: the subject it +// names, the length (in Unicode code points, as the transport hands the body over) and the content +// digest of the complete log body the rows came from, and the rows. A later assessment does not +// have to trust rows detached from the job and the bytes that produced them, and a caller cannot +// author one from invented text -- the decoder above is open, this record is not. +type QualificationJobLogReceipt sole_constructor { + subject: QualificationExtractionSubject + log_code_points: Int + log_digest: Fnv1a64Structural + readings: QualificationLogReadings +} + +// A STANDING PER INSTRUMENT SOURCE, NOT ONE "INSTRUMENTS READ" ARM. The job-log instruments either +// carry the sealed receipt or the typed reason they do not; the claim-cost read carries its own +// typed standing beside it; the boot-leg and cohort instruments are not in this stage's payload at +// all and are rows below. Nothing in this value says the collect stage is complete. +type JobLogInstrumentStanding + = QualificationJobLogInstrumentsRead { receipt: QualificationJobLogReceipt } + | QualificationJobLogInstrumentsRefused { refusal: JobLogReadRefusal } + +type QualificationCollectStage { + job_log: JobLogInstrumentStanding + claim_cost: ClaimCostArtifactRead +} + +type QualificationCollectOutcome + = QualificationCollected { stage: QualificationCollectStage } + | QualificationCollectRefused { refusal: ExtractionSubjectRefusal } + fn read_attempt_job_log(target: QualificationWorkflowTarget, job: WorkflowJobRun) -> AttemptJobLog uses net: Network { let read = github.WorkflowRuns.DownloadJobLogsForWorkflowRun( auth_token: "", @@ -417,6 +559,29 @@ fn read_attempt_job_log(target: QualificationWorkflowTarget, job: WorkflowJobRun } } +fn job_log_standing(subject: QualificationExtractionSubject, log: AttemptJobLog) -> JobLogInstrumentStanding + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments"), + ] +{ + match designated_job_log(designated_job_id: subject.floor_job.id, log: log) { + DesignatedJobLogRefused { refusal: r } => QualificationJobLogInstrumentsRefused { refusal: r } + DesignatedJobLogText { job_id: j, log: text } => + match decode_job_log_instruments(log: text as String, slot_unit: subject.slot.unit) { + JobLogInstrumentsUndecodable { refusal: r } => QualificationJobLogInstrumentsRefused { refusal: JobLogUndecodable { job_id: j, refusal: r } } + JobLogInstrumentsDecoded { readings: rd } => + QualificationJobLogInstrumentsRead { + receipt: QualificationJobLogReceipt { + subject: subject, + log_code_points: string_length(s: text as String), + log_digest: content_hash_atom(value: text), + readings: rd, + }, + } + } + } +} + // THE CLAIM-COST ARTIFACT, READ AS FAR AS ANY BOUND REALIZATION CAN READ IT. It is looked up by the // name the floor workflow uploads it under (gunbc.witness_floor_workflow // required_floor_claim_cost_artifact_name), downloaded through GitHub's redirect, and every way that @@ -458,10 +623,20 @@ fn conclude_claim_cost_listing(outcome: RestOutcome, listed: ActionsArtifactList } } +// A 410 ON THE DOWNLOAD IS UPSTREAM'S EXPIRY ANSWER (actions/download-artifact documents 410 Gone for +// an expired artifact), so it is ClaimCostArtifactExpired like the listing's own flag, not a generic +// unreadable archive. fn conclude_claim_cost_download(artifact_id: Int, outcome: RestOutcome) -> ClaimCostArtifactRead { match outcome { RestOk => ClaimCostArchiveNotInflatable { artifact_id: artifact_id } - other => ClaimCostArchiveUnreadable { artifact_id: artifact_id, outcome: other } + RestStatusRefused { status: st, body: bd } => + if st == 410 { + ClaimCostArtifactExpired { artifact_id: artifact_id } + } else { + ClaimCostArchiveUnreadable { artifact_id: artifact_id, outcome: RestStatusRefused { status: st, body: bd } } + } + RestTransportRefused { cause: c } => ClaimCostArchiveUnreadable { artifact_id: artifact_id, outcome: RestTransportRefused { cause: c } } + RestBodyUndecodable { status: st, cause: c } => ClaimCostArchiveUnreadable { artifact_id: artifact_id, outcome: RestBodyUndecodable { status: st, cause: c } } } } @@ -490,14 +665,26 @@ fn read_claim_cost_artifact(target: QualificationWorkflowTarget, run_id: Int) -> } } +// THE PRODUCTION ENTRY. It takes the sealed dispatch, the collection and the authorized +// registration -- no target, run or unit argument a caller could cross-wire -- mints the subject +// (refusing before any read when the three disagree), reads the designated job's log and the +// claim-cost artifact, and returns a standing per instrument source. Its consumer is the route's +// collect stage (gunbc.runner_throughput_qualification_route CollectInstruments, standing +// mtcollins1_collect_leg), which does not invoke it yet: that edge is the row below. fn collect_qualification_instruments( - target: QualificationWorkflowTarget, - collected: CollectedQualificationRun, - slot_unit: NonEmptyStr, -) -> QualificationInstrumentExtraction uses net: Network { - let logs = collected.attempt_jobs |> map(j => read_attempt_job_log(target: target, job: j)) - let claim_cost = read_claim_cost_artifact(target: target, run_id: collected.run_id) - extract_qualification_instruments(collected: collected, logs: logs, claim_cost: claim_cost, slot_unit: slot_unit) + dispatched: DispatchedQualificationRun, + collection: QualificationRunCollection, + registration: JitRegistration, +) -> QualificationCollectOutcome uses net: Network { + match qualification_extraction_subject(dispatched: dispatched, collection: collection, registration: registration) { + ExtractionSubjectRefused { refusal: r } => QualificationCollectRefused { refusal: r } + ExtractionSubjectMinted { subject: subject } => { + let target = subject.dispatched.target + let log = read_attempt_job_log(target: target, job: subject.floor_job) + let claim_cost = read_claim_cost_artifact(target: target, run_id: subject.collected.run_id) + QualificationCollected { stage: QualificationCollectStage { job_log: job_log_standing(subject: subject, log: log), claim_cost: claim_cost } } + } + } } // ── THE INSTRUMENTS THIS RUN'S LOG DOES NOT CARRY, EACH WITH THE CAPABILITY THAT WOULD READ IT ── @@ -506,6 +693,11 @@ fn collect_qualification_instruments( // log. The remaining four are three facts, so three rows: one per missing capability, so that each // row is retired by its own trigger and by nothing else. data qualification_instrument_extraction_frontier_rows: List = [ + frontier_row_decl( + ref: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments"), + reason: "FloorPhaseRows and FloorCgroupRows have an implemented extraction authority and no production consumer. collect_qualification_instruments reads the designated floor job's log and mints the sealed QualificationJobLogReceipt, but nothing on the route calls it: the collect stage's standing is gunbc.runner_throughput_qualification_route mtcollins1_collect_leg, LegAuthorityImplemented, and only witnesses reach the decoder. Its subject also needs the runner name the authorized mint fixed to name exactly one served job, which is the JitRegistration the route's registration leg (itself LegAuthorityImplemented) would hand it", + dissolution: unbound_dissolution(description: "the qualification route's collect stage invokes collect_qualification_instruments with the DispatchedQualificationRun its dispatch leg returned, the collection collect_qualification_run read back, and the JitRegistration its registration leg minted, on an executed route, so mtcollins1_collect_leg is LegWired and FloorPhaseRows and FloorCgroupRows are read from a real run's designated job. NOT satisfied by a witness calling the decoder or the subject join, nor by the collector compiling"), + ), frontier_row_decl( ref: decl_ref(module_path: "gunbc.floor_cost_distribution", decl_name: "parse_claim_cost_tsv"), reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are both read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding). Neither is on the job log: on the recorded required-floor log of run 35048059968 / job 104642384772 (test.fixture recorded_required_floor_log_excerpt), there are zero [witness] lines and zero occurrences of the specimen identity test.claim.floor_eval_step_calibration_specimen_test.calibration_specimen_performs_its_declared_work, and eval_steps appear only on the [over-cost] preview, a 25-row truncated view that is not the instrument and is not read as one. Two capabilities are missing. FIRST, the required workflow no longer uploads the artifact: cli_run write_required_floor_claim_cost_tsv still writes the TSV, but the emission of .github/workflows/witnesses.yml by gunbc.compiler_gate_workflow carries no upload step for it, and recent required runs publish only required-ci-measurement-receipt and compiler-pair-candidate. SECOND, an uploaded artifact is always a zip, and the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so extdeps.github.actions_artifacts github.Artifacts DownloadArtifact answers RestBodyUndecodable. read_claim_cost_artifact attempts the read on every collection and carries the result as a typed ClaimCostArtifactRead refusal (ClaimCostArtifactAbsent while nothing uploads it)", diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index f9fdc7d96a7..cfdc8631d14 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -46,13 +46,13 @@ import gunbc.runner_throughput_qualification { type StageEffect = BmcWrite { gate: DeclarationRef, boot: RouteLegStanding, image: RouteLegStanding } | GitHubControlPlaneWrite { authority: RouteLegStanding } - | ObservationOnly + | ObservationOnly { collection: RouteLegStanding } type QualificationStage = BootHostUnderApprovalGate { host: HostIdentity, boot: RouteLegStanding, image: RouteLegStanding } | RegisterEphemeralRunnerSlot { unit: NonEmptyStr, labels: List, workspace: WorkspaceBacking, registration: RouteLegStanding } | DispatchFloorToSlot { workload: FloorWorkloadPin, selector: List, dispatch: RouteLegStanding } - | CollectInstruments { instruments: List } + | CollectInstruments { instruments: List, collection: RouteLegStanding } | DeregisterRunnerSlot { unit: NonEmptyStr, deregistration: RouteLegStanding } fn stage_effect(stage: QualificationStage) -> StageEffect { @@ -65,7 +65,7 @@ fn stage_effect(stage: QualificationStage) -> StageEffect { } RegisterEphemeralRunnerSlot { unit: _, labels: _, workspace: _, registration: r } => GitHubControlPlaneWrite { authority: r } DispatchFloorToSlot { workload: _, selector: _, dispatch: d } => GitHubControlPlaneWrite { authority: d } - CollectInstruments { instruments: _ } => ObservationOnly + CollectInstruments { instruments: _, collection: c } => ObservationOnly { collection: c } DeregisterRunnerSlot { unit: _, deregistration: d } => GitHubControlPlaneWrite { authority: d } } } @@ -81,7 +81,7 @@ fn stage_egress(stage: QualificationStage) -> EgressPolicyShape? { BootHostUnderApprovalGate { host: _, boot: _, image: _ } => none RegisterEphemeralRunnerSlot { unit: _, labels: _, workspace: _, registration: _ } => Present { value: runner_host_egress_policy } DispatchFloorToSlot { workload: _, selector: _, dispatch: _ } => Present { value: runner_host_egress_policy } - CollectInstruments { instruments: _ } => none + CollectInstruments { instruments: _, collection: _ } => none DeregisterRunnerSlot { unit: _, deregistration: _ } => Present { value: runner_host_egress_policy } } } @@ -247,6 +247,13 @@ data mtcollins1_deregistration_leg: RouteLegStanding = LegAuthorityImplemented { wiring_owed: "the same route fold calling ensure_jit_runner_deregistered on every slot exit with the registration jit_registration_of minted from the registration leg's dispatch, joined to a host-unit gunbc.runner.runner_teardown_receipt RunnerTeardownResult; SUFFICIENT FOR the route reporting a slot retired only when the runner is absent from the organization listing and the unit's resources are observed gone" as NonEmptyStr, } +// THE COLLECT STAGE'S STANDING. The authority reads the designated floor job's log and the +// claim-cost artifact into a sealed receipt; nothing on the route invokes it yet. +data mtcollins1_collect_leg: RouteLegStanding = LegAuthorityImplemented { + authority: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments"), + wiring_owed: "the same route fold calling collect_qualification_instruments with the DispatchedQualificationRun the dispatch leg returned, the collection collect_qualification_run read back, and the JitRegistration the registration leg minted; SUFFICIENT FOR FloorPhaseRows and FloorCgroupRows being read on every executed attempt from the one job its registered runner ran, as a sealed QualificationJobLogReceipt or a typed refusal (gunbc.runner.runner_qualification_dispatch qualification_instrument_extraction_frontier_rows)" as NonEmptyStr, +} + // THE FLEET-CONVERGE MODE THIS ROUTE BECOMES IS NOT NAMED HERE. A mode row belongs to // `gunbc.fleet_converge_workflow` and lands there with the boot leg (section 3c: a declared frontier // with its trigger beside it); a mode whose boot leg has no authority on main would dispatch nothing @@ -259,6 +266,7 @@ type QualificationRouteLegs { image: RouteLegStanding registration: RouteLegStanding dispatch: RouteLegStanding + collect: RouteLegStanding deregistration: RouteLegStanding } @@ -267,6 +275,7 @@ data mtcollins1_route_legs: QualificationRouteLegs = QualificationRouteLegs { image: mtcollins1_image_leg, registration: mtcollins1_registration_leg, dispatch: mtcollins1_dispatch_leg, + collect: mtcollins1_collect_leg, deregistration: mtcollins1_deregistration_leg, } @@ -283,7 +292,7 @@ fn qualification_route( BootHostUnderApprovalGate { host: host, boot: legs.boot, image: legs.image }, RegisterEphemeralRunnerSlot { unit: unit, labels: labels, workspace: workspace, registration: legs.registration }, DispatchFloorToSlot { workload: workload, selector: labels, dispatch: legs.dispatch }, - CollectInstruments { instruments: qualification_instruments }, + CollectInstruments { instruments: qualification_instruments, collection: legs.collect }, DeregisterRunnerSlot { unit: unit, deregistration: legs.deregistration }, ] } @@ -302,17 +311,19 @@ fn mtcollins1_qualification_route(attempt: NonEmptyStr, workload: FloorWorkloadP fn stage_is_effectful(stage: QualificationStage) -> Bool { match stage_effect(stage: stage) { - ObservationOnly => false + ObservationOnly { collection: _ } => false BmcWrite { gate: _, boot: _, image: _ } => true GitHubControlPlaneWrite { authority: _ } => true } } -// THE LEGS AN EFFECTFUL STAGE STANDS ON. A BMC write stands on its boot and image legs; a -// control-plane write on its authority; observation on nothing. +// THE LEGS A STAGE STANDS ON. A BMC write stands on its boot and image legs; a control-plane write +// on its authority; the collect stage on its collection standing. Observation is not exempt: a +// route whose effectful legs were all wired while nothing invoked the collector would execute and +// read nothing, so the collect stage's standing counts toward route_is_executable like any leg. fn stage_legs(stage: QualificationStage) -> List { match stage_effect(stage: stage) { - ObservationOnly => [] + ObservationOnly { collection: c } => [c] BmcWrite { gate: _, boot: b, image: i } => [b, i] GitHubControlPlaneWrite { authority: a } => [a] } diff --git a/dag/test/claim/public_workload_census_witness_test.dag b/dag/test/claim/public_workload_census_witness_test.dag index 45efa581a32..fea91d8fdd7 100644 --- a/dag/test/claim/public_workload_census_witness_test.dag +++ b/dag/test/claim/public_workload_census_witness_test.dag @@ -74,6 +74,7 @@ data candidate_biome_blob_mismatch_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -123,6 +124,7 @@ data candidate_biome_matrix_selector_mismatch_control: WorkloadCandidate = Workl status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -178,6 +180,7 @@ data candidate_pdns_axis_name_mismatch_control: WorkloadCandidate = WorkloadCand status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -238,6 +241,7 @@ data candidate_pdns_axis_name_swap_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -291,6 +295,7 @@ data candidate_biome_job_key_mismatch_control: WorkloadCandidate = WorkloadCandi status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -339,6 +344,7 @@ data candidate_biome_job_key_titlecase_uninspected_control: WorkloadCandidate = status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -387,6 +393,7 @@ data candidate_biome_multi_label_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["self-hosted", "linux", "arm64"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -435,6 +442,7 @@ data candidate_empty_labels_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -483,6 +491,7 @@ data candidate_absent_conclusion_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Absent, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -531,6 +540,7 @@ data candidate_run_attempt_zero_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -579,6 +589,7 @@ data candidate_empty_matrix_axes_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index ee1ed1205cb..b02665017ac 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -15,10 +15,14 @@ import gunbc.runner.runner_qualification_dispatch { RunJobsTruncated, RunNeverReachedAttemptRunner, RunSubjectMismatch, conclude_qualification_run, qualification_run_collected, AttemptJobLog, AttemptJobLogRead, AttemptJobLogUnreadable, - QualificationInstrumentExtraction, QualificationInstrumentsRead, QualificationInstrumentsRefused, - QualificationLogReadings, InstrumentReadRefusal, JobLogUnreadable, FloorPhaseRowGarbledInLog, FloorCgroupRowGarbledInLog, + DesignatedFloorJob, FloorJobDesignated, FloorJobRefused, designated_floor_job, + ExtractionSubjectRefusal, SubjectRunMismatch, SubjectAttemptMismatch, NoJobOnRegisteredRunner, SeveralJobsOnRegisteredRunner, + DesignatedJobLog, DesignatedJobLogText, DesignatedJobLogRefused, designated_job_log, + JobLogReadRefusal, JobLogOfAnotherJob, JobLogUnreadable, JobLogEmpty, + JobLogInstrumentDecode, JobLogInstrumentsDecoded, JobLogInstrumentsUndecodable, decode_job_log_instruments, + QualificationLogReadings, InstrumentReadRefusal, FloorPhaseRowGarbledInLog, FloorCgroupRowGarbledInLog, FloorPhaseRowsAbsent, SlotCgroupLevelAbsent, - extract_qualification_instruments, qualification_instrument_read_from_job_log, + qualification_instrument_read_from_job_log, ClaimCostArtifactRead, ClaimCostArtifactAbsent, ClaimCostArtifactExpired, ClaimCostArtifactListTruncated, ClaimCostArchiveUnreadable, ClaimCostArchiveNotInflatable, conclude_claim_cost_listing, conclude_claim_cost_download, @@ -26,7 +30,7 @@ import gunbc.runner.runner_qualification_dispatch { } import gunbc.required_ci_phase_roster { FloorPhaseTimed, FloorPhaseUntimed } import extdeps.linux.cgroup_v2_memory { CgroupMemoryLimited } -import std.measure { byte_size_count } +import std.measure { byte_size_count, millisecond_count } import extdeps.github.actions_artifacts { ActionsArtifact, ActionsArtifactList } import test.fixture.recorded_required_floor_log_excerpt { recorded_required_floor_log_excerpt, recorded_floor_log_job_id, recorded_floor_log_slot_unit, @@ -81,6 +85,7 @@ fn job(labels: List, started: Bool) -> WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: labels, + runner_name: none, created_at: Present { value: "2026-10-04T00:00:10Z" }, started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, completed_at: Present { value: "2026-10-04T00:50:00Z" }, @@ -168,134 +173,207 @@ test fn a_readback_of_another_run_refuses_on_the_subject() -> Bool { let foreign_job = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [WorkflowJobRun { id: 501, run_id: 99, run_attempt: 1, name: "floor", status: Completed, conclusion: Present { value: Success }, labels: [attempt as String], + runner_name: none, created_at: none, started_at: Present { value: "2026-10-04T00:01:00Z" }, completed_at: none, }])) (match other_run { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: 77, observed_event: _, foreign_job_run_ids: _ } } => true _ => false }) && (match foreign_job { QualificationRunRefused { attempt: _, run_id: _, refusal: RunSubjectMismatch { observed_run_id: _, observed_event: _, foreign_job_run_ids: f } } => count(f) == 1 && (f |> all(x => x == 99)) _ => false }) } -// ── EXTRACTION, OVER SUPPLIED LOGS ───────────────────────────────────────────────────────────── +// ── THE SUBJECT: ONE RUN, ONE REGISTERED RUNNER, ONE JOB ────────────────────────────────────── // -// These claims are about extract_qualification_instruments alone, so the collection and the logs are -// supplied values. The real producer's bytes are exercised once, by the inhabitance claim at the end. +// designated_floor_job is the pure join qualification_extraction_subject runs before any network +// read. Each claim concludes its own readback (admitted by name to conclude_qualification_run) and +// asks the join for the floor job, so the collection is never minted by a helper here. -data no_claim_cost: ClaimCostArtifactRead = ClaimCostArtifactAbsent { names_seen: ["required-ci-measurement-receipt"] } +data runner: NonEmptyStr = "gunbc-q-mtcollins1-q-20261004-1" -data slot: NonEmptyStr = "gunbc-qualification-mtcollins1-q-20261004-1.service" - -data phase_line: String = "2026-10-04T00:10:00.0000000Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0" -data started_line: String = "2026-10-04T00:09:00.0000000Z [floor-phase] phase=strict-preparation state=started" -data slot_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/gunbc-qualification-mtcollins1-q-20261004-1.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0]" -data slice_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=1 peak=364675248128" - -fn served_jobs() -> WorkflowJobRunList { - jobs(js: [job(labels: [attempt as String], started: true)]) -} - -fn log_of(lines: List) -> List { - [AttemptJobLogRead { job_id: 500, log: join(lines, "\n") }] +fn job_on(id: Int, runner_name: String?) -> WorkflowJobRun { + WorkflowJobRun { + id: id, + run_id: 77, + run_attempt: 1, + name: "floor", + status: Completed, + conclusion: Present { value: Success }, + labels: [attempt as String], + runner_name: runner_name, + created_at: Present { value: "2026-10-04T00:00:10Z" }, + started_at: Present { value: "2026-10-04T00:01:00Z" }, + completed_at: Present { value: "2026-10-04T00:50:00Z" }, + } } -// THE COLLECTION IS SEALED, so no helper here returns one. Each claim below concludes its own served -// readback (it is admitted by name to conclude_qualification_run for that) and hands the collection -// to extraction_of, which only takes a collection apart and cannot mint one. A refused collection -// has nothing to extract and answers none, which every claim below reads as red. -fn extraction_of(c: QualificationRunCollection, logs: List, slot_unit: NonEmptyStr) -> QualificationInstrumentExtraction? { +fn designated_in(c: QualificationRunCollection, dispatched_run_id: Int, slot_attempt: NonEmptyStr) -> DesignatedFloorJob? { match c { QualificationRunCollected { collected: collected } => - Present { value: extract_qualification_instruments(collected: collected, logs: logs, claim_cost: no_claim_cost, slot_unit: slot_unit) } + Present { value: designated_floor_job(dispatched_run_id: dispatched_run_id, dispatched_attempt: attempt, collected: collected, slot_attempt: slot_attempt, runner_name: runner) } QualificationRunRefused { attempt: _, run_id: _, refusal: _ } => none } } -fn refused_with(e: QualificationInstrumentExtraction?, check: fn(InstrumentReadRefusal) -> Bool) -> Bool { - match e { - Present { value: QualificationInstrumentsRefused { attempt: _, run_id: _, refusal: r } } => check(r) +fn subject_refused(d: DesignatedFloorJob?, check: fn(ExtractionSubjectRefusal) -> Bool) -> Bool { + match d { + Present { value: FloorJobRefused { refusal: r } } => check(r) + _ => false + } +} + +// THE POSITIVE CONTROL: of two served jobs, the one the registered runner ran is the floor job. +test fn the_floor_job_is_the_one_the_registered_runner_ran() -> Bool { + let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job_on(id: 501, runner_name: Present { value: "srv4-03" }), job_on(id: 502, runner_name: Present { value: runner as String })])) + match designated_in(c: c, dispatched_run_id: 77, slot_attempt: attempt) { + Present { value: FloorJobDesignated { job: j } } => j.id == 502 _ => false } } +// RED: a target from run B beside a collection of run A refuses before any read -- the dispatched +// run id and the collected run id are both carried, so the cross-wire is located. +test fn a_dispatch_of_another_run_refuses_the_subject() -> Bool { + let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job_on(id: 502, runner_name: Present { value: runner as String })])) + subject_refused(d: designated_in(c: c, dispatched_run_id: 78, slot_attempt: attempt), check: r => match r { + SubjectRunMismatch { dispatched_run_id: 78, collected_run_id: 77 } => true + _ => false + }) +} + +// RED: a slot minted for attempt B beside a run of attempt A refuses, so its unit can never filter +// any cgroup row of this run. +test fn a_slot_of_another_attempt_refuses_the_subject() -> Bool { + let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job_on(id: 502, runner_name: Present { value: runner as String })])) + subject_refused(d: designated_in(c: c, dispatched_run_id: 77, slot_attempt: "q-20261004-2"), check: r => match r { + SubjectAttemptMismatch { dispatched_attempt: _, other_attempt: o } => (o as String) == "q-20261004-2" + _ => false + }) +} + +// RED: no served job ran on the registered runner, or two did. Neither is a floor job. +test fn zero_or_two_jobs_on_the_registered_runner_refuse() -> Bool { + let none_on = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job_on(id: 501, runner_name: Present { value: "srv4-03" }), job_on(id: 503, runner_name: none)])) + let two_on = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: jobs(js: [job_on(id: 501, runner_name: Present { value: runner as String }), job_on(id: 502, runner_name: Present { value: runner as String })])) + subject_refused(d: designated_in(c: none_on, dispatched_run_id: 77, slot_attempt: attempt), check: r => match r { + NoJobOnRegisteredRunner { runner_name: _, runner_names_seen: seen } => count(seen) == 2 && (seen |> any(n => n == "srv4-03")) + _ => false + }) + && subject_refused(d: designated_in(c: two_on, dispatched_run_id: 77, slot_attempt: attempt), check: r => match r { + SeveralJobsOnRegisteredRunner { runner_name: _, job_ids: ids } => count(ids) == 2 + _ => false + }) +} + +// ── THE DESIGNATED JOB'S LOG ────────────────────────────────────────────────────────────────── + +// RED: a log read for any job other than the designated one is refused, whatever it says. +test fn a_log_of_another_job_is_not_the_floor_jobs_log() -> Bool { + (match designated_job_log(designated_job_id: 502, log: AttemptJobLogRead { job_id: 501, log: "[floor-phase] phase=x wall_ms=1" }) { + DesignatedJobLogRefused { refusal: JobLogOfAnotherJob { designated_job_id: 502, read_job_id: 501 } } => true + _ => false + }) + && (match designated_job_log(designated_job_id: 502, log: AttemptJobLogUnreadable { job_id: 502, outcome: RestStatusRefused { status: 410, body: "Gone" } }) { + DesignatedJobLogRefused { refusal: JobLogUnreadable { job_id: 502, outcome: _ } } => true + _ => false + }) + && (match designated_job_log(designated_job_id: 502, log: AttemptJobLogRead { job_id: 502, log: "" }) { + DesignatedJobLogRefused { refusal: JobLogEmpty { job_id: 502 } } => true + _ => false + }) +} + +// ── THE PURE DECODER, OVER SUPPLIED TEXT ────────────────────────────────────────────────────── + +data slot: NonEmptyStr = "gunbc-qualification-mtcollins1-q-20261004-1.service" + +data phase_line: String = "2026-10-04T00:10:00.0000000Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0" +data started_line: String = "2026-10-04T00:09:00.0000000Z [floor-phase] phase=strict-preparation state=started" +data slot_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/gunbc-qualification-mtcollins1-q-20261004-1.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0]" +data slice_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=1 peak=364675248128" + +fn decode(lines: List) -> JobLogInstrumentDecode { + decode_job_log_instruments(log: join(lines, "\n"), slot_unit: slot) +} + +fn undecodable_with(d: JobLogInstrumentDecode, check: fn(InstrumentReadRefusal) -> Bool) -> Bool { + match d { + JobLogInstrumentsUndecodable { refusal: r } => check(r) + JobLogInstrumentsDecoded { readings: _ } => false + } +} + test fn the_job_log_instruments_are_the_two_the_extraction_reads() -> Bool { qualification_instrument_read_from_job_log(i: FloorPhaseRows) && qualification_instrument_read_from_job_log(i: FloorCgroupRows) && count(qualification_instruments |> filter(i => qualification_instrument_read_from_job_log(i: i))) == 2 - && count(qualification_instrument_extraction_frontier_rows) == 3 + && count(qualification_instrument_extraction_frontier_rows) == 4 } -// THE POSITIVE CONTROL: timed and untimed phase rows are both carried, the claim-cost read's refusal -// rides beside them, and only the slot's own level is taken as the cgroup reading -- the slice above -// it has a larger peak and must not be read as the slot's. -test fn a_log_with_phase_rows_and_the_slot_level_is_read() -> Bool { - let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served_jobs()) - match extraction_of(c: c, logs: log_of(lines: ["noise before the floor", started_line, slot_level_line, slice_level_line, phase_line]), slot_unit: slot) { - Present { value: QualificationInstrumentsRead { attempt: a, run_id: r, conclusion: _, readings: rd, claim_cost: cc } } => - a == attempt && r == 77 - && (match cc { ClaimCostArtifactAbsent { names_seen: _ } => true _ => false }) - && count(rd.floor_phase_rows) == 2 +// THE POSITIVE CONTROL: timed and untimed phase rows are both carried, and only the slot's own +// level is taken -- the slice above it has a larger peak and must not be read as the slot's. +test fn a_log_with_phase_rows_and_the_slot_level_is_decoded() -> Bool { + match decode(lines: ["noise before the floor", started_line, slot_level_line, slice_level_line, phase_line]) { + JobLogInstrumentsDecoded { readings: rd } => + count(rd.floor_phase_rows) == 2 && count(rd.slot_cgroup_rows) == 1 && (rd.slot_cgroup_rows |> all(l => byte_size_count(b: l.peak) == 10444185600)) - _ => false + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: _, wall: w } => millisecond_count(m: w) == 22194 _ => false })) + JobLogInstrumentsUndecodable { refusal: _ } => false } } test fn a_garbled_row_refuses_rather_than_being_skipped() -> Bool { - let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served_jobs()) - refused_with( - e: extraction_of(c: c, logs: log_of(lines: [slot_level_line, phase_line, "[floor-phase] phase=gate-closure wall_ms=twelve"]), slot_unit: slot), - check: r => match r { FloorPhaseRowGarbledInLog { job_id: 500, line: _, cause: _ } => true _ => false }, - ) - && refused_with( - e: extraction_of(c: c, logs: log_of(lines: [phase_line, "[floor-cgroup] when=beat-10 level=/sys/fs/cgroup/x.service high=lots peak=1"]), slot_unit: slot), - check: r => match r { FloorCgroupRowGarbledInLog { job_id: 500, line: _, cause: _ } => true _ => false }, - ) + undecodable_with(d: decode(lines: [slot_level_line, phase_line, "[floor-phase] phase=gate-closure wall_ms=twelve"]), check: r => match r { + FloorPhaseRowGarbledInLog { line: _, cause: _ } => true + _ => false + }) + && undecodable_with(d: decode(lines: [phase_line, "[floor-cgroup] when=beat-10 level=/sys/fs/cgroup/x.service high=lots peak=1"]), check: r => match r { + FloorCgroupRowGarbledInLog { line: _, cause: _ } => true + _ => false + }) } -test fn a_log_missing_either_reading_refuses_with_what_it_saw() -> Bool { - let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served_jobs()) - refused_with( - e: extraction_of(c: c, logs: log_of(lines: [started_line, slot_level_line]), slot_unit: slot), - check: r => match r { FloorPhaseRowsAbsent => true _ => false }, - ) - && refused_with( - e: extraction_of(c: c, logs: log_of(lines: [phase_line, slice_level_line]), slot_unit: slot), - check: r => match r { SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } => u == slot && count(seen) == 1 _ => false }, - ) +// RED, THE SPLIT-ACROSS-JOBS CASE: the phase rows of one job and the slot row of another never meet, +// because only the designated job's log is decoded -- read alone, the job carrying only phase rows +// refuses for want of the slot level, and the one carrying only the slot level refuses for want of a +// timed phase row. +test fn rows_split_across_two_jobs_are_not_one_measurement() -> Bool { + undecodable_with(d: decode(lines: [started_line, phase_line]), check: r => match r { + SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } => u == slot && count(seen) == 0 + _ => false + }) + && undecodable_with(d: decode(lines: [slot_level_line, slice_level_line]), check: r => match r { + FloorPhaseRowsAbsent => true + _ => false + }) } -test fn an_unreadable_log_reads_nothing() -> Bool { - let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served_jobs()) - refused_with( - e: extraction_of(c: c, logs: [AttemptJobLogUnreadable { job_id: 500, outcome: RestStatusRefused { status: 410, body: "Gone" } }], slot_unit: slot), - check: r => match r { JobLogUnreadable { job_id: 500, outcome: _ } => true _ => false }, - ) +test fn a_log_without_the_slot_level_names_the_levels_it_saw() -> Bool { + undecodable_with(d: decode(lines: [phase_line, slice_level_line]), check: r => match r { + SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } => u == slot && count(seen) == 1 + _ => false + }) } // ── INHABITANCE: THE REAL PRODUCER'S BYTES ───────────────────────────────────────────────────── // // The supplied lines above are designed against the row shapes; this claim is what keeps them -// readings. It hands extract_qualification_instruments the verbatim [floor-phase] and [floor-cgroup] -// lines claim_executor printed on a recorded required-floor run (test.fixture +// readings. It hands decode_job_log_instruments the verbatim [floor-phase] and [floor-cgroup] lines +// claim_executor printed on a recorded required-floor run (test.fixture // recorded_required_floor_log_excerpt), under that run's real slot unit, and asserts the ROUTE as well // as the answer: every one of the 79 phase rows is read (none garbled, none dropped), the timed rows // carry the gate-closure wall the log printed, and of the 12 level rows only the 4 at the slot's own // level -- floor entry and three heartbeats -- are taken, with the slot's memory.high read as a byte // bound and floor entry's memory.peak as printed. If the host's row encoding moves, this goes red // while the designed fixtures above stay green. -test fn the_extraction_reads_a_recorded_required_floor_log() -> Bool { - let c = conclude_qualification_run(attempt: attempt, run_id: 77, run_outcome: RestOk, run: run(status_completed: true, concluded: true), jobs_outcome: RestOk, jobs: served_jobs()) - match extraction_of( - c: c, - logs: [AttemptJobLogRead { job_id: recorded_floor_log_job_id, log: recorded_required_floor_log_excerpt }], - slot_unit: recorded_floor_log_slot_unit as NonEmptyStr, - ) { - Present { value: QualificationInstrumentsRead { attempt: _, run_id: _, conclusion: _, readings: rd, claim_cost: _ } } => +test fn the_decoder_reads_a_recorded_required_floor_log() -> Bool { + match decode_job_log_instruments(log: recorded_required_floor_log_excerpt, slot_unit: recorded_floor_log_slot_unit as NonEmptyStr) { + JobLogInstrumentsDecoded { readings: rd } => count(rd.floor_phase_rows) == 79 - && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: ph, wall_ms: ms } => (ph as String) == "gate-closure" && ms == 22194 _ => false })) + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: ph, wall: w } => (ph as String) == "gate-closure" && millisecond_count(m: w) == 22194 _ => false })) && (rd.floor_phase_rows |> any(p => match p { FloorPhaseUntimed { phase: ph } => (ph as String) == "strict-preparation" _ => false })) && count(rd.slot_cgroup_rows) == 4 && (rd.slot_cgroup_rows |> all(l => match l.high { CgroupMemoryLimited { bytes: b } => byte_size_count(b: b) == 16106127360 _ => false })) && (rd.slot_cgroup_rows |> any(l => (l.when as String) == "floor-entry" && byte_size_count(b: l.peak) == 10444185600)) - _ => false + JobLogInstrumentsUndecodable { refusal: _ } => false } } @@ -311,6 +389,7 @@ fn artifact(name: String, expired: Bool) -> ActionsArtifact { test fn every_claim_cost_read_ends_in_a_typed_refusal() -> Bool { let present = ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-floor-claim-cost", expired: false)] } + let gone = RestStatusRefused { status: 410, body: "Gone" } (match conclude_claim_cost_listing(outcome: RestOk, listed: ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-ci-measurement-receipt", expired: false)] }) { Present { value: ClaimCostArtifactAbsent { names_seen: ns } } => count(ns) == 1 _ => false @@ -328,6 +407,10 @@ test fn every_claim_cost_read_ends_in_a_typed_refusal() -> Bool { ClaimCostArchiveUnreadable { artifact_id: 9001, outcome: _ } => true _ => false }) + && (match conclude_claim_cost_download(artifact_id: 9001, outcome: gone) { + ClaimCostArtifactExpired { artifact_id: 9001 } => true + _ => false + }) && (match conclude_claim_cost_download(artifact_id: 9001, outcome: RestOk) { ClaimCostArchiveNotInflatable { artifact_id: 9001 } => true _ => false diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index b24f6bbe8d5..af6a9277716 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -739,8 +739,8 @@ test fn the_route_classifies_every_mutation_at_its_grain() -> Bool { match stage_effect(stage: s) { GitHubControlPlaneWrite { authority: _ } => true _ => false } DeregisterRunnerSlot { unit: _, deregistration: _ } => match stage_effect(stage: s) { GitHubControlPlaneWrite { authority: _ } => true _ => false } - CollectInstruments { instruments: _ } => - match stage_effect(stage: s) { ObservationOnly => true _ => false } + CollectInstruments { instruments: _, collection: _ } => + match stage_effect(stage: s) { ObservationOnly { collection: _ } => true _ => false } BootHostUnderApprovalGate { host: _, boot: _, image: _ } => match stage_effect(stage: s) { BmcWrite { gate: _, boot: _, image: _ } => true _ => false } } @@ -781,6 +781,8 @@ test fn the_registration_dispatch_and_deregistration_legs_are_implemented_not_wi && !leg_is_wired(leg: mtcollins1_route_legs.deregistration) && !leg_is_wired(leg: mtcollins1_route_legs.boot) && !leg_is_wired(leg: mtcollins1_route_legs.dispatch) + && leg_names(leg: mtcollins1_route_legs.collect, module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments") + && !leg_is_wired(leg: mtcollins1_route_legs.collect) && !route_is_executable(route: route()) } @@ -796,7 +798,7 @@ test fn only_the_runner_stages_open_the_network_and_only_default_drop() -> Bool Absent => match s { BootHostUnderApprovalGate { host: _, boot: _, image: _ } => true - CollectInstruments { instruments: _ } => true + CollectInstruments { instruments: _, collection: _ } => true _ => false } Present { value: policy } => @@ -818,7 +820,7 @@ test fn the_collect_stage_owes_a_sealed_concurrent_cohort() -> Bool { !leg_is_wired(leg: qualification_run_producer_frontier) && (route() |> any(s => match s { - CollectInstruments { instruments: is } => + CollectInstruments { instruments: is, collection: _ } => is |> any(i => match i { ConcurrentCohortSeatWindows => { @@ -836,7 +838,7 @@ test fn the_collect_stage_owes_a_sealed_concurrent_cohort() -> Bool { test fn the_collect_stage_owes_the_rate_specimen_and_the_cgroup_rows() -> Bool { route() |> any(s => match s { - CollectInstruments { instruments: is } => + CollectInstruments { instruments: is, collection: _ } => (is |> any(i => match i { CalibrationSpecimenWitnessLine => { diff --git a/dag/test/claim/superseded_run_starvation_census_witness_test.dag b/dag/test/claim/superseded_run_starvation_census_witness_test.dag index e5da6157f55..3ab260e8c3a 100644 --- a/dag/test/claim/superseded_run_starvation_census_witness_test.dag +++ b/dag/test/claim/superseded_run_starvation_census_witness_test.dag @@ -213,6 +213,7 @@ data job_cancelled: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Cancelled }, labels: [], + runner_name: none, created_at: Present { value: "2026-09-01T00:01:00Z" }, started_at: Present { value: "2026-09-01T00:10:00Z" }, completed_at: Present { value: "2026-09-01T00:20:00Z" }, @@ -226,6 +227,7 @@ data job_failed: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Failure }, labels: [], + runner_name: none, created_at: Present { value: "2026-09-01T00:00:00Z" }, started_at: Present { value: "2026-09-01T00:00:00Z" }, completed_at: Present { value: "2026-09-01T00:15:00Z" }, @@ -239,6 +241,7 @@ data job_success: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_name: none, created_at: Present { value: "2026-09-01T00:02:00Z" }, started_at: Present { value: "2026-09-01T00:05:00Z" }, completed_at: Present { value: "2026-09-01T00:10:00Z" }, @@ -252,6 +255,7 @@ data job_queued: WorkflowJobRun = WorkflowJobRun { status: Queued, conclusion: none, labels: [], + runner_name: none, created_at: Present { value: "2026-09-01T00:03:00Z" }, started_at: none, completed_at: none, @@ -265,6 +269,7 @@ data job_in_progress: WorkflowJobRun = WorkflowJobRun { status: InProgress, conclusion: none, labels: [], + runner_name: none, created_at: Present { value: "2026-09-01T00:04:00Z" }, started_at: Present { value: "2026-09-01T00:30:00Z" }, completed_at: none, @@ -339,6 +344,7 @@ test fn earliest_created_at_returns_none_when_no_created_at() -> Bool { status: InProgress, conclusion: none, labels: [], + runner_name: none, created_at: none, started_at: none, completed_at: none, From 417b6190555e81aa346bda3377516f8a9c4dd168 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:43:17 +0000 Subject: [PATCH 13/44] leg 3: fix builtin names and witness fixtures (first/last, declaration_ref_in_list arity, no panic) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/auth/privileged_effect_census.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 6 +- ...ization_pattern_selection_witness_test.dag | 2 +- ...er_qualification_dispatch_witness_test.dag | 167 +++++++++++------- 4 files changed, 112 insertions(+), 65 deletions(-) diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index b72c8edb069..e7eb8a52ecb 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -542,7 +542,7 @@ data runner_lifecycle_ruling_scope_interpretation: RulingScopeInterpretation = R // A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE INTERPRETATION NAMES IT. Removing a site from // covers turns its discharge into NoWitnessDischarge, and the selection then decides it afresh. fn ruling_discharge_for(interpretation: RulingScopeInterpretation, site_ref: DeclarationRef) -> WitnessDischarge { - if declaration_ref_in_list(ref: site_ref, list: interpretation.covers) { + if declaration_ref_in_list(target: site_ref, refs: interpretation.covers) { StandingRulingUnderInterlock { ruling: interpretation.ruling } } else { NoWitnessDischarge diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 73f7c6c0d4b..88b195feea7 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -78,7 +78,7 @@ fn qualification_workflow_path() -> NonEmptyStr { } fn qualification_workflow_file_name() -> NonEmptyStr { - match list_last(split(qualification_workflow_path() as String, "/")) { + match last(split(qualification_workflow_path() as String, "/")) { Present { value: f } => f as NonEmptyStr Absent => qualification_workflow_path() } @@ -199,7 +199,7 @@ fn qualification_dispatch_subject_over( } else if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_attempt_dispatch_input) { QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } } else { - match list_find(workflow.jobs, j => j.id == (floor_job_id as String)) { + match first(filter(workflow.jobs, j => j.id == (floor_job_id as String))) { Absent => QualificationSubjectRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } Present { value: job } => if !runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { @@ -413,7 +413,7 @@ type QualificationRunVerdict fn decide_floor_job(subject: QualificationDispatchSubject, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { let floors = jobs |> filter(j => j.name == (subject.floor_job_name as String)) - match list_head(floors) { + match first(floors) { Absent => RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: 0 } } Present { value: floor } => if count(floors) != 1 { diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index d659b3bbdf5..243474165d4 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -409,7 +409,7 @@ test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federa // discharged and the selection stops answering the federated grant. The ruling text is unchanged // either way, and its interlock is the host-generic unit hold. test fn the_dispatch_is_federated_only_while_the_interpretation_names_it() -> Bool { - let row = list_find(privileged_effect_census, r => (r.site.decl_name as String) == "dispatch_qualification_floor") + let row = first(filter(privileged_effect_census, r => (r.site.decl_name as String) == "dispatch_qualification_floor")) let narrowed = RulingScopeInterpretation { ruling: runner_lifecycle_operator_ruling, covers: [ diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 7249fab5a8a..d96ca4cbd52 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -48,16 +48,10 @@ data floor_job_id: NonEmptyStr = "qualification-floor" // decoded by upstream's own outcome fold (runner id 4242). Nothing below constructs a registration, // and every caller of the sealed subject mint is a claim returning Bool (no constructor proxy). -fn host() -> ManagedHost { - match list_head(managed_hosts()) { - Present { value: h } => h - Absent => panic("managed_hosts is empty") - } -} -fn minted(slot_workflow: NonEmptyStr) -> JitMintDispatch { +fn minted(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitMintDispatch { dispatch_jit_mint( - binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: host()), attempt: attempt, workflow: slot_workflow) }, + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: slot_workflow) }, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, @@ -66,15 +60,9 @@ fn minted(slot_workflow: NonEmptyStr) -> JitMintDispatch { ) } -fn registration(slot_workflow: NonEmptyStr) -> JitRegistration { - match jit_registration_of(dispatch: minted(slot_workflow: slot_workflow)) { - Present { value: r } => r - Absent => panic("the fixture mint was refused") - } -} -fn delivery(slot_workflow: NonEmptyStr) -> JitCredentialDelivery { - receive_jit_mint(dispatch: minted(slot_workflow: slot_workflow), outcome: minted_outcome) +fn delivery(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitCredentialDelivery { + receive_jit_mint(dispatch: minted(h: h, slot_workflow: slot_workflow), outcome: minted_outcome) } fn qualification_slot() -> NonEmptyStr { @@ -118,17 +106,20 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { // declared input carrying that attempt, which is a label the slot registered; the effect names the // generated fleet-converge workflow file in gunbc_repository at the pinned revision. test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => s.attempt == attempt && s.runner_id == minted_runner && (s.floor_job_name as String) == "qualification floor" - && (s.host as String) == (host().host as String) + && (s.host as String) == (h.host as String) && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 && (match map_get(qualification_dispatch_inputs(attempt: s.attempt), qualification_attempt_dispatch_input.name) { - Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: host().host, attempt: attempt) |> any(l => l == v)) + Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: h.host, attempt: attempt) |> any(l => l == v)) Absent => false }) && (match qualification_dispatch_effect(subject: s) { @@ -138,21 +129,32 @@ test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { }) QualificationSubjectRefused { refusal: _ } => false } + } + Absent => false + } + Absent => false + } } -fn slot_refuses(slot_workflow: NonEmptyStr) -> Bool { - match qualification_dispatch_subject_over( - registration: registration(slot_workflow: slot_workflow), delivery: delivery(slot_workflow: slot_workflow), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) - _ => false +fn slot_refuses(h: ManagedHost, slot_workflow: NonEmptyStr) -> Bool { + match jit_registration_of(dispatch: minted(h: h, slot_workflow: slot_workflow)) { + Present { value: other_reg } => + match qualification_dispatch_subject_over( + registration: other_reg, delivery: delivery(h: h, slot_workflow: slot_workflow), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) + _ => false + } + Absent => false + } + Absent => false } } -fn runs_on_refuses(runs_on: RunnerSpec) -> Bool { +fn runs_on_refuses(h: ManagedHost, reg: JitRegistration, runs_on: RunnerSpec) -> Bool { match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true @@ -160,9 +162,9 @@ fn runs_on_refuses(runs_on: RunnerSpec) -> Bool { } } -fn undeclared_input_refuses() -> Bool { +fn undeclared_input_refuses(h: ManagedHost, reg: JitRegistration) -> Bool { match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true @@ -180,20 +182,28 @@ fn undeclared_input_refuses() -> Bool { // - the production mint, over today's generated fleet-converge model (no qualification mode yet), // refuses rather than admitting anything. test fn no_other_workflow_or_label_can_be_dispatched_through_the_ruling() -> Bool { - slot_refuses(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") - && slot_refuses(slot_workflow: qualification_selected_workflow(branch: "feature")) - && runs_on_refuses(runs_on: SelfHosted { labels: [attempt as String] }) - && runs_on_refuses(runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) - && runs_on_refuses(runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) - && runs_on_refuses(runs_on: RunsOnExpression { expression: "gunbc-qualification" }) - && undeclared_input_refuses() + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { + slot_refuses(h: h, slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") + && slot_refuses(h: h, slot_workflow: qualification_selected_workflow(branch: "feature")) + && runs_on_refuses(h: h, reg: reg, runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && undeclared_input_refuses(h: h, reg: reg) && (match qualification_dispatch_subject( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: _ } => false QualificationSubjectRefused { refusal: _ } => true }) + } + Absent => false + } + Absent => false + } } fn mutation_standing(rest: RestOutcome) -> QualificationDispatchStanding { @@ -208,19 +218,27 @@ fn ambiguous(s: QualificationDispatchStanding) -> Bool { // an undecodable 200 are commit-ambiguous; only a succeeded exchange dispatches. And the interlock: // the hold covers the slot's own host and no other. test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { (match mutation_standing(rest: RestStatusRefused { status: 422, body: "Unexpected inputs provided" }) { DispatchStandingRefused { status: st, body: _ } => st == 422 _ => false }) && ambiguous(s: mutation_standing(rest: RestTransportRefused { cause: "fixture: connection reset after send" })) && ambiguous(s: mutation_standing(rest: RestStatusRefused { status: 503, body: "fixture" })) && ambiguous(s: mutation_standing(rest: RestBodyUndecodable { status: 200, cause: "fixture: truncated body" })) && (match mutation_standing(rest: RestOk) { DispatchStandingSucceeded => true _ => false }) && match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => - hold_covers_subject(held: host().host, subject: s) && !hold_covers_subject(held: "fixture-other-host" as HostIdentity, subject: s) + hold_covers_subject(held: h.host, subject: s) && !hold_covers_subject(held: "fixture-other-host" as HostIdentity, subject: s) QualificationSubjectRefused { refusal: _ } => false } + } + Absent => false + } + Absent => false + } } // ── THE COLLECTION ───────────────────────────────────────────────────────────────────────────── @@ -276,6 +294,14 @@ fn jobs(js: List) -> WorkflowJobRunList { WorkflowJobRunList { total_count: count(js), jobs: js } } +fn decide_jobs(s: QualificationDispatchSubject, js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: js) +} + +fn decide_run(s: QualificationDispatchSubject, r: WorkflowRun, js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) +} + fn never_reached(v: QualificationRunVerdict) -> Bool { match v { RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { minted_runner_id: m, observed_runner_id: _, observed_runner_name: _, started: _ } } => m == minted_runner @@ -295,78 +321,94 @@ fn mismatch(v: QualificationRunVerdict) -> Bool { // labels on another runner; a helper job on the minted runner while the floor ran elsewhere; a floor // on the minted runner that never started; two jobs answering to the floor's name. test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { - let decide = (js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: js) - (match decide(jobs(js: [floor_on(runner_id: minted_runner)])) { + (match decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: minted_runner)])) { RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) _ => false }) - && never_reached(v: decide(jobs(js: [floor_on(runner_id: 99)]))) - && never_reached(v: decide(jobs(js: [ + && never_reached(v: decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: 99)]))) + && never_reached(v: decide_jobs(s: s, js: jobs(js: [ job(id: 2, name: "helper", runner_id: minted_runner, started: true, attempt_n: 1), floor_on(runner_id: 99), ]))) - && never_reached(v: decide(jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: false, attempt_n: 1)]))) - && (match decide(jobs(js: [floor_on(runner_id: minted_runner), job(id: 2, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 1)])) { + && never_reached(v: decide_jobs(s: s, js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: false, attempt_n: 1)]))) + && (match decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: minted_runner), job(id: 2, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 1)])) { RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 _ => false }) } QualificationSubjectRefused { refusal: _ } => false } + } + Absent => false + } + Absent => false + } } // THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT. REDs: the run re-run to attempt 2 (whose floor // job carries the same labels and runner); a job of attempt 2 in the listing; another run id; // another event; another workflow path. test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { let floor = jobs(js: [floor_on(runner_id: minted_runner)]) let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { - let decide = (r: WorkflowRun, js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) - (match decide(dispatched_run(attempt_n: 2), floor) { + (match decide_run(s: s, r: dispatched_run(attempt_n: 2), js: floor) { RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 _ => false }) - && mismatch(v: decide(dispatched_run(attempt_n: 1), jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 2)]))) - && mismatch(v: decide(run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), floor)) - && mismatch(v: decide(run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), floor)) - && mismatch(v: decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), floor)) + && mismatch(v: decide_run(s: s, r: dispatched_run(attempt_n: 1), js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 2)]))) + && mismatch(v: decide_run(s: s, r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) } QualificationSubjectRefused { refusal: _ } => false } + } + Absent => false + } + Absent => false + } } test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { + Present { value: reg } => { let floor = jobs(js: [floor_on(runner_id: minted_runner)]) let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( - registration: registration(slot_workflow: qualification_slot()), delivery: delivery(slot_workflow: qualification_slot()), + registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { - let decide = (r: WorkflowRun, js: WorkflowJobRunList) => decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) - (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), floor) { + (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true _ => false }) - && (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), floor) { + && (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), js: floor) { RunVerdictRefused { refusal: RunCompletedWithoutConclusion } => true _ => false }) - && (match decide(run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), floor) { + && (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), js: floor) { RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: _, observed: _ } } => true _ => false }) - && (match decide(dispatched_run(attempt_n: 1), WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { + && (match decide_run(s: s, r: dispatched_run(attempt_n: 1), js: WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { RunVerdictRefused { refusal: RunJobsTruncated { returned: rt, total: t } } => rt == 1 && t == 2 _ => false }) @@ -381,4 +423,9 @@ test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { } QualificationSubjectRefused { refusal: _ } => false } + } + Absent => false + } + Absent => false + } } From 3199c077fdd92fffd39e5f46ad6a9953b79ce3ca Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:45:23 +0000 Subject: [PATCH 14/44] leg 3b: state the seal's rung at its evidence -- checked once, not enrolled -- with the capability that would enrol it Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 5d0108de0b9..1845074be9f 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -520,8 +520,23 @@ fn designated_job_log(designated_job_id: Int, log: AttemptJobLog) -> DesignatedJ // writes it, and only after reading the designated job's log over the network: the subject it // names, the length (in Unicode code points, as the transport hands the body over) and the content // digest of the complete log body the rows came from, and the rows. A later assessment does not -// have to trust rows detached from the job and the bytes that produced them, and a caller cannot -// author one from invented text -- the decoder above is open, this record is not. +// have to trust rows detached from the job and the bytes that produced them. The decoder above is +// open; this record is not. +// +// RUNG OF THE SEAL, STATED AT ITS EVIDENCE (DESIGN 4b(1)): CHECKED ONCE, NOT ENROLLED. The seal is +// the compiler's sole_constructor and admit_callers walls applied to this record, the subject above +// and job_log_standing. On 2026-10-04, compile_dag_diagnostic_census probes run once with the +// interpreter gave one SoleConstructorViolation for a forged receipt literal, one for a forged +// subject literal, one ConstructorCallAdmissionRefused for an outside call to job_log_standing, and +// zero for a module that only calls the decoder. Nothing re-runs those probes. A probe that compiles +// against these declarations must import this module, so it is ReadsLiveTree, and the required +// floor declines ReadsLiveTree claims rather than executing them +// (gunbc.qualified_pipe_callee_seed_growth); the SubstrateInputsOnly compile harnesses compile only +// self-contained modules, which would test the compiler's generic wall rather than this seal. +// NEXT-RUNG TRIGGER, A CAPABILITY: the required floor can enrol and execute a ReadsLiveTree +// compile-refusal probe, SUFFICIENT FOR those four probes running on every required run, so that +// removing sole_constructor from either record or the admit list from job_log_standing turns a +// required lane red. type QualificationJobLogReceipt sole_constructor { subject: QualificationExtractionSubject log_code_points: Int From a20fb39695ee361ca8e218075fe4146f56207892 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:58:38 +0000 Subject: [PATCH 15/44] leg 3: share #13225's runner identity (job_ran_on over runner_name == the minted JitRegistration name); drop runner_id; fix witness braces Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/workflow_runs.dag | 12 ++-- dag/gunbc/public_workload_census.dag | 8 --- .../runner/runner_qualification_dispatch.dag | 57 ++++++++++--------- .../public_workload_census_witness_test.dag | 11 ---- ...er_qualification_dispatch_witness_test.dag | 52 +++++++++-------- ...ded_run_starvation_census_witness_test.dag | 6 -- 6 files changed, 64 insertions(+), 82 deletions(-) diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 97ad64e880b..fb05284e41a 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -194,12 +194,11 @@ fn workflow_job_attempt_filter_wire(f: WorkflowJobAttemptFilter) -> String { // rendered name. The parenthetical of combination VALUES in axis order is the rendering this // tree observed on those names; it is not a REST field. Standing is TranscribedUncited until // GitHub's Jobs docs state that encoding. -// runner_id AND runner_name ARE WHICH RUNNER TOOK THE JOB, a third fact beside the declared -// runs-on and the observed labels: two runners can carry identical labels, and only the runner -// identity joins a job to one registration (GitHub returns both null until a runner is assigned). -// The census rows transcribed before these fields existed carry none for both: the field was not -// read, which is the same absence a reader of an unassigned job sees, and no consumer of those rows -// asks it. +// runner_name IS WHICH RUNNER TOOK THE JOB, a third fact beside the declared runs-on and the +// observed labels: two runners can carry identical labels, and the runner's name is what a JIT +// registration fixes before GitHub answers (gunbc.runner.runner_qualification_dispatch job_ran_on +// joins on it). Upstream publishes it as nullable -- null until a runner takes the job -- and every +// record in this tree written before the field existed carries none rather than a guessed name. type WorkflowJobRun { id: Int run_id: Int @@ -208,7 +207,6 @@ type WorkflowJobRun { status: WorkflowRunStatus conclusion: WorkflowRunConclusion? labels: List - runner_id: Int? runner_name: String? created_at: Timestamp? started_at: Timestamp? diff --git a/dag/gunbc/public_workload_census.dag b/dag/gunbc/public_workload_census.dag index ec11c28ce47..56db757bf0d 100644 --- a/dag/gunbc/public_workload_census.dag +++ b/dag/gunbc/public_workload_census.dag @@ -1020,7 +1020,6 @@ data candidate_biome_test: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -1084,7 +1083,6 @@ data candidate_pdns_dnsdist_arm: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -1153,7 +1151,6 @@ data candidate_openobserve_arm_build: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:21Z" }, @@ -1224,7 +1221,6 @@ data candidate_openobserve_amd64: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-8"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:15Z" }, @@ -1279,7 +1275,6 @@ data candidate_openobserve_manifest: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:28:12Z" }, @@ -1334,7 +1329,6 @@ data candidate_biome_test_windows: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-windows-2022-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:31Z" }, @@ -1383,7 +1377,6 @@ data candidate_biome_test_macos: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-macos-latest"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:08:06Z" }, @@ -1430,7 +1423,6 @@ data candidate_openobserve_summary: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-latest"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:29:11Z" }, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 88b195feea7..230359604a8 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -46,7 +46,7 @@ import gunbc.runner_throughput_qualification_route { QualificationInstrument, qu // THE SUBJECT IS SEALED AND ITS SCOPE IS FIXED BY CONSTRUCTION. QualificationDispatchSubject is // minted only by qualification_dispatch_subject_over, and only from: // - the JIT registration the route minted and the credential delivered for that same slot -- so -// the attempt is the slot's own and the runner id is the one GitHub returned for it; +// the attempt is the slot's own and the runner the collection joins on is that registration's; // - the qualification workflow, which is not a parameter of the request: its path is the generated // fleet-converge artifact (gunbc.generated_artifact FleetConvergeYamlArtifact) and production // supplies its model as gunbc.fleet_converge_workflow fleet_converge_workflow, where the route's @@ -100,7 +100,6 @@ type QualificationDispatchSubject sole_constructor { registration: JitRegistration host: HostIdentity attempt: NonEmptyStr - runner_id: Int branch: NonEmptyStr revision: CommitSha floor_job_name: NonEmptyStr @@ -185,7 +184,7 @@ fn qualification_dispatch_subject_over( ] { match delivery { - JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: runner_id } => + JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: _ } => if delivered_slot != registration.slot { QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherSlot } } else { @@ -210,7 +209,6 @@ fn qualification_dispatch_subject_over( registration: registration, host: h.host.host.host, attempt: h.attempt, - runner_id: runner_id, branch: branch, revision: revision, floor_job_name: job_display_name(job: job), @@ -347,10 +345,10 @@ fn dispatch_qualification_floor( // its conclusion is no longer the dispatch's. // // THE RUNNER IS JOINED BY IDENTITY, NOT BY LABEL. Two runners can carry the same labels, and a -// helper job on the attempt's runner says nothing about where the floor ran. So the collection finds -// THE floor job by the name the subject fixed and requires its runner_id to be the one GitHub -// returned when the registration was minted, and that it started. Anything else is -// RunNeverReachedAttemptRunner, carrying what was observed. +// helper job on the attempt's runner says nothing about where the floor ran. So the collection takes +// the one job whose runner_name is the registration's minted name (job_ran_on), and requires it to +// be the floor job the subject fixed, and to have started. An ephemeral JIT runner takes exactly one +// job, so no job on it, several, or a different job are each a typed refusal carrying what was seen. // // THE CONCLUSION IS READ BACK AND CARRIED, NOT FILTERED: a failed floor on the attempt's runner is // still a run of this host. The collected payload keeps the sealed dispatch and the whole observed @@ -367,7 +365,9 @@ type QualificationRunRefusal | RunJobsUnreadable { performance: RestExchangePerformance } | RunJobsTruncated { returned: Int, total: Int } | RunFloorJobNotUnique { floor_job_name: NonEmptyStr, matching: Int } - | RunNeverReachedAttemptRunner { minted_runner_id: Int, observed_runner_id: Int?, observed_runner_name: String?, started: Bool } + | RunNeverReachedAttemptRunner { registered_runner: NonEmptyStr, runner_names_seen: List } + | RunRegisteredRunnerRanAnotherJob { registered_runner: NonEmptyStr, job_name: String } + | RunFloorJobNotStarted { job_id: Int } // INSTRUMENT EXTRACTION IS A DECLARED FRONTIER, carried on the collected arm as the instruments the // run was read for and not yet extracted from (qualification_instrument_extraction_frontier_rows). @@ -396,8 +396,15 @@ fn optional_text_is(o: String?, expected: String) -> Bool { match o { Present { value: v } => v == expected Absent => false } } -fn optional_int_is(o: Int?, expected: Int) -> Bool { - match o { Present { value: v } => v == expected Absent => false } +// THE ONE JOIN FROM A JOB TO A JIT REGISTRATION: the runner GitHub reports took the job is the +// name the authorized mint fixed (gunbc.runner.runner_jit_deregistration JitRegistration name). +// Labels cannot make this join -- two runners can carry identical labels. +fn job_ran_on(job: WorkflowJobRun, runner_name: NonEmptyStr) -> Bool { + optional_text_is(o: job.runner_name, expected: runner_name as String) +} + +fn job_runner_name_seen(job: WorkflowJobRun) -> String { + match job.runner_name { Present { value: n } => n Absent => "(no runner assigned)" } } fn job_started(job: WorkflowJobRun) -> Bool { @@ -412,23 +419,19 @@ type QualificationRunVerdict | RunVerdictRefused { refusal: QualificationRunRefusal } fn decide_floor_job(subject: QualificationDispatchSubject, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { - let floors = jobs |> filter(j => j.name == (subject.floor_job_name as String)) - match first(floors) { - Absent => RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: 0 } } - Present { value: floor } => - if count(floors) != 1 { - RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: count(floors) } } - } else if optional_int_is(o: floor.runner_id, expected: subject.runner_id) && job_started(job: floor) { - RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } + let registered = subject.registration.name.value + let on_runner = jobs |> filter(j => job_ran_on(job: j, runner_name: registered)) + match first(on_runner) { + Absent => RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: registered, runner_names_seen: jobs |> map(j => job_runner_name_seen(job: j)) } } + Present { value: job } => + if count(on_runner) != 1 { + RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: count(on_runner) } } + } else if job.name != (subject.floor_job_name as String) { + RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: registered, job_name: job.name } } + } else if !job_started(job: job) { + RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: job.id } } } else { - RunVerdictRefused { - refusal: RunNeverReachedAttemptRunner { - minted_runner_id: subject.runner_id, - observed_runner_id: floor.runner_id, - observed_runner_name: floor.runner_name, - started: job_started(job: floor), - }, - } + RunVerdictCollected { run: run, floor_job: job, conclusion: conclusion } } } } diff --git a/dag/test/claim/public_workload_census_witness_test.dag b/dag/test/claim/public_workload_census_witness_test.dag index b7488718758..fea91d8fdd7 100644 --- a/dag/test/claim/public_workload_census_witness_test.dag +++ b/dag/test/claim/public_workload_census_witness_test.dag @@ -74,7 +74,6 @@ data candidate_biome_blob_mismatch_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -125,7 +124,6 @@ data candidate_biome_matrix_selector_mismatch_control: WorkloadCandidate = Workl status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -182,7 +180,6 @@ data candidate_pdns_axis_name_mismatch_control: WorkloadCandidate = WorkloadCand status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -244,7 +241,6 @@ data candidate_pdns_axis_name_swap_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -299,7 +295,6 @@ data candidate_biome_job_key_mismatch_control: WorkloadCandidate = WorkloadCandi status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -349,7 +344,6 @@ data candidate_biome_job_key_titlecase_uninspected_control: WorkloadCandidate = status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -399,7 +393,6 @@ data candidate_biome_multi_label_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["self-hosted", "linux", "arm64"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -449,7 +442,6 @@ data candidate_empty_labels_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: [], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -499,7 +491,6 @@ data candidate_absent_conclusion_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Absent, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -549,7 +540,6 @@ data candidate_run_attempt_zero_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -599,7 +589,6 @@ data candidate_empty_matrix_axes_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], - runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index d96ca4cbd52..28a6eab63c3 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -18,7 +18,7 @@ import gunbc.runner_jit_mint { HostUnitSlotBinding } import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } import gunbc.runner.runner_jit_perform { JitMintDispatch, JitCredentialDelivery, dispatch_jit_mint, receive_jit_mint } import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } -import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit } import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } import gunbc.runner.runner_qualification_dispatch { qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_selected_workflow, @@ -31,7 +31,7 @@ import gunbc.runner.runner_qualification_dispatch { hold_covers_subject, decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunNotCompleted, - RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, RunNeverReachedAttemptRunner, + RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, RunNeverReachedAttemptRunner, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -39,7 +39,6 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly data attempt: NonEmptyStr = "q-1" data branch: NonEmptyStr = "main" data revision: CommitSha = "1111111111111111111111111111111111111111" -data minted_runner: Int = 4242 data floor_job_id: NonEmptyStr = "qualification-floor" // ── THE SUBJECT, THROUGH THE REAL MINT ────────────────────────────────────────────────────────── @@ -114,7 +113,7 @@ test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => - s.attempt == attempt && s.runner_id == minted_runner + s.attempt == attempt && (s.registration.name.value as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (s.floor_job_name as String) == "qualification floor" && (s.host as String) == (h.host as String) && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 @@ -146,8 +145,6 @@ fn slot_refuses(h: ManagedHost, slot_workflow: NonEmptyStr) -> Bool { QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) _ => false } - Absent => false - } Absent => false } } @@ -269,7 +266,7 @@ fn dispatched_run(attempt_n: Int) -> WorkflowRun { run(id: 77, attempt_n: attempt_n, event: "workflow_dispatch", path: qualification_workflow_path() as String, sha: revision, completed: true, concluded: true) } -fn job(id: Int, name: String, runner_id: Int, started: Bool, attempt_n: Int) -> WorkflowJobRun { +fn job(id: Int, name: String, runner: String, started: Bool, attempt_n: Int) -> WorkflowJobRun { WorkflowJobRun { id: id, run_id: 77, @@ -278,16 +275,19 @@ fn job(id: Int, name: String, runner_id: Int, started: Bool, attempt_n: Int) -> status: Completed, conclusion: Present { value: Success }, labels: [attempt as String], - runner_id: Present { value: runner_id }, - runner_name: Present { value: "fixture-runner" }, + runner_name: Present { value: runner }, created_at: Present { value: "2026-10-04T00:00:10Z" }, started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, completed_at: Present { value: "2026-10-04T00:50:00Z" }, } } -fn floor_on(runner_id: Int) -> WorkflowJobRun { - job(id: 1, name: "qualification floor", runner_id: runner_id, started: true, attempt_n: 1) +fn floor_on(runner: String) -> WorkflowJobRun { + job(id: 1, name: "qualification floor", runner: runner, started: true, attempt_n: 1) +} + +fn registered(s: QualificationDispatchSubject) -> String { + s.registration.name.value as String } fn jobs(js: List) -> WorkflowJobRunList { @@ -304,7 +304,7 @@ fn decide_run(s: QualificationDispatchSubject, r: WorkflowRun, js: WorkflowJobRu fn never_reached(v: QualificationRunVerdict) -> Bool { match v { - RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { minted_runner_id: m, observed_runner_id: _, observed_runner_name: _, started: _ } } => m == minted_runner + RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: _, runner_names_seen: seen } } => count(seen) > 0 _ => false } } @@ -329,17 +329,23 @@ test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { - (match decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: minted_runner)])) { + (match decide_jobs(s: s, js: jobs(js: [floor_on(runner: registered(s: s))])) { RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) _ => false }) - && never_reached(v: decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: 99)]))) - && never_reached(v: decide_jobs(s: s, js: jobs(js: [ - job(id: 2, name: "helper", runner_id: minted_runner, started: true, attempt_n: 1), - floor_on(runner_id: 99), - ]))) - && never_reached(v: decide_jobs(s: s, js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: false, attempt_n: 1)]))) - && (match decide_jobs(s: s, js: jobs(js: [floor_on(runner_id: minted_runner), job(id: 2, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 1)])) { + && never_reached(v: decide_jobs(s: s, js: jobs(js: [floor_on(runner: "another-runner-with-the-same-labels")]))) + && (match decide_jobs(s: s, js: jobs(js: [ + job(id: 2, name: "helper", runner: registered(s: s), started: true, attempt_n: 1), + floor_on(runner: "another-runner-with-the-same-labels"), + ])) { + RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: _, job_name: n } } => n == "helper" + _ => false + }) + && (match decide_jobs(s: s, js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered(s: s), started: false, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: id } } => id == 1 + _ => false + }) + && (match decide_jobs(s: s, js: jobs(js: [floor_on(runner: registered(s: s)), job(id: 2, name: "qualification floor", runner: registered(s: s), started: true, attempt_n: 1)])) { RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 _ => false }) @@ -360,18 +366,18 @@ test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { match first(managed_hosts()) { Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { Present { value: reg } => { - let floor = jobs(js: [floor_on(runner_id: minted_runner)]) let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { + let floor = jobs(js: [floor_on(runner: registered(s: s))]) (match decide_run(s: s, r: dispatched_run(attempt_n: 2), js: floor) { RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 _ => false }) - && mismatch(v: decide_run(s: s, r: dispatched_run(attempt_n: 1), js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: minted_runner, started: true, attempt_n: 2)]))) + && mismatch(v: decide_run(s: s, r: dispatched_run(attempt_n: 1), js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered(s: s), started: true, attempt_n: 2)]))) && mismatch(v: decide_run(s: s, r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) @@ -389,13 +395,13 @@ test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { match first(managed_hosts()) { Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { Present { value: reg } => { - let floor = jobs(js: [floor_on(runner_id: minted_runner)]) let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { + let floor = jobs(js: [floor_on(runner: registered(s: s))]) (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true _ => false diff --git a/dag/test/claim/superseded_run_starvation_census_witness_test.dag b/dag/test/claim/superseded_run_starvation_census_witness_test.dag index 96091a15fd6..3ab260e8c3a 100644 --- a/dag/test/claim/superseded_run_starvation_census_witness_test.dag +++ b/dag/test/claim/superseded_run_starvation_census_witness_test.dag @@ -213,7 +213,6 @@ data job_cancelled: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Cancelled }, labels: [], - runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:01:00Z" }, started_at: Present { value: "2026-09-01T00:10:00Z" }, @@ -228,7 +227,6 @@ data job_failed: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Failure }, labels: [], - runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:00:00Z" }, started_at: Present { value: "2026-09-01T00:00:00Z" }, @@ -243,7 +241,6 @@ data job_success: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: [], - runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:02:00Z" }, started_at: Present { value: "2026-09-01T00:05:00Z" }, @@ -258,7 +255,6 @@ data job_queued: WorkflowJobRun = WorkflowJobRun { status: Queued, conclusion: none, labels: [], - runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:03:00Z" }, started_at: none, @@ -273,7 +269,6 @@ data job_in_progress: WorkflowJobRun = WorkflowJobRun { status: InProgress, conclusion: none, labels: [], - runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:04:00Z" }, started_at: Present { value: "2026-09-01T00:30:00Z" }, @@ -349,7 +344,6 @@ test fn earliest_created_at_returns_none_when_no_created_at() -> Bool { status: InProgress, conclusion: none, labels: [], - runner_id: none, runner_name: none, created_at: none, started_at: none, From d7838a7182994274e6d583e2291f83540ce1c9c5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 07:42:35 +0000 Subject: [PATCH 16/44] leg 3: exhaustive matches over RunnerSpec, WorkflowTrigger and the template expression (floor NonFoldResidueRosterDiverged) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 230359604a8..9034fd38604 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -12,9 +12,13 @@ import extdeps.transports.rest { } import extdeps.github.effect { GitHubEffect, DispatchWorkflow, GitHubWorkflowRef } import extdeps.github.actions { - Workflow, Job, DispatchInput, InputString, WorkflowDispatch, RunnerSpec, RunsOnExpression, + Workflow, Job, DispatchInput, InputString, RunnerSpec, RunsOnExpression, HostedRunner, SelfHosted, + WorkflowDispatch, Push, PullRequest, Schedule, WorkflowCall, WorkflowRunCompleted, MergeGroup, +} +import extdeps.github.expressions { + ingest_template, context_name, Expression, Interpolation, LiteralText, + ContextAccess, FunctionCall, StringLiteral, LogicalOr, LogicalAnd, Equals, NotEquals, } -import extdeps.github.expressions { ingest_template, Interpolation, ContextAccess, Inputs } import extdeps.github.workflows { WorkflowDispatchReceipt } import extdeps.github.workflow_runs { WorkflowRun, WorkflowRunStatus, Completed, WorkflowRunConclusion, WorkflowJobRun, WorkflowJobRunList, @@ -122,7 +126,12 @@ fn workflow_declares_dispatch_input(workflow: Workflow, input: DispatchInput) -> workflow.on |> any(t => match t { WorkflowDispatch { inputs: ins } => ins |> any(i => i.name == input.name) - _ => false + Push { branches: _, paths: _ } => false + PullRequest { branches: _, types: _ } => false + Schedule { cron: _ } => false + WorkflowCall { inputs: _ } => false + WorkflowRunCompleted { workflows: _, branches: _ } => false + MergeGroup => false }) } @@ -134,20 +143,33 @@ fn runs_on_is_exactly_the_input(runner: RunnerSpec, input: DispatchInput) -> Boo Present { value: t } => count(t.segments) == 1 && (t.segments |> all(s => match s { - Interpolation { expression: ContextAccess { context: Inputs, path: p } } => - count(p) == 1 && (p |> all(x => x == input.name)) - _ => false + Interpolation { expression: e } => expression_reads_exactly_the_input(expression: e, input: input) + LiteralText { text: _ } => false })) Absent => false } - _ => false + HostedRunner { label: _ } => false + SelfHosted { labels: _ } => false + } +} + +fn expression_reads_exactly_the_input(expression: Expression, input: DispatchInput) -> Bool { + match expression { + ContextAccess { context: c, path: p } => context_name(context: c) == "inputs" && count(p) == 1 && (p |> all(x => x == input.name)) + FunctionCall { function: _, args: _ } => false + StringLiteral { value: _ } => false + LogicalOr { left: _, right: _ } => false + LogicalAnd { left: _, right: _ } => false + Equals { left: _, right: _ } => false + NotEquals { left: _, right: _ } => false } } fn runner_spec_text(runner: RunnerSpec) -> String { match runner { RunsOnExpression { expression: e } => e - _ => "a runs-on that is not an expression" + HostedRunner { label: _ } => "a hosted runner label" + SelfHosted { labels: ls } => join(["self-hosted labels: ", join(ls, ",")], "") } } From d3514f330e2a51914d361abbb38d6a4dc12cf685 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 4 Oct 2026 08:12:48 +0000 Subject: [PATCH 17/44] Seal the authorized JIT dispatch and bind the runner listing to its query subject (side-chat RC on #13206) (1) JitMintDispatchAuthorized { dispatch: AuthorizedJitMintDispatch } sole_constructor, minted only by dispatch_jit_mint; dispatch_jit_mint, attempt_dispatch and the witness helper `dispatched` are admit_callers-sealed so no admitted caller returns it onward. (2) OrganizationRunnerListRead carries organization, name, App and installation; readback_subject_refusal runs before the answer is read; conclude_from_readback, its inner step and readback_after_deletes admit only ensure_jit_runner_deregistered. REDs: compile probe test.probe.jit_deregistration_forged_probe (victim-runner forgeries of all four sealed records + unadmitted conclusion) enrolled by test.claim.jit_deregistration_forged_probe_witness; pure readback-subject RED. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/github_effect_perform.dag | 26 ++++-- .../runner/runner_jit_deregistration.dag | 50 +++++++++++- dag/gunbc/runner/runner_jit_perform.dag | 51 +++++++----- .../runner/runner_microvm_slot_controller.dag | 8 +- .../github_app_registry_witness_test.dag | 70 ++++++++++++---- ...registration_forged_probe_witness_test.dag | 56 +++++++++++++ .../probe/jit_deregistration_forged_probe.dag | 79 +++++++++++++++++++ 7 files changed, 290 insertions(+), 50 deletions(-) create mode 100644 dag/test/claim/runner/jit_deregistration_forged_probe_witness_test.dag create mode 100644 dag/test/probe/jit_deregistration_forged_probe.dag diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 1f7806e0fd3..d515fe52d56 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -229,11 +229,11 @@ fn jit_mint_generate_gate(dispatch: JitMintDispatch) -> JitMintGenerateGate { match dispatch { JitMintDispatchRefused { refusal: r } => JitMintDecidedBeforeGenerate { performance: JitMintReceived { delivery: JitCredentialNotDispatched { refusal: r } } } - JitMintDispatchAuthorized { slot: _, app: app, installation: i, organization: o, request: req } => - if app != lifecycle_controller_app { - JitMintDecidedBeforeGenerate { performance: JitMintAppKeyNotCustodied { app: app } } + JitMintDispatchAuthorized { dispatch: d } => + if d.app != lifecycle_controller_app { + JitMintDecidedBeforeGenerate { performance: JitMintAppKeyNotCustodied { app: d.app } } } else { - JitMintProceedsToGenerate { app: app, installation: i, organization: o, request: req } + JitMintProceedsToGenerate { app: d.app, installation: d.installation, organization: d.organization, request: d.request } } } } @@ -274,9 +274,16 @@ fn read_runner_groups_with(token: ControllerInstallationToken, organization: Non // Secret is shed here and nowhere else, as for the mint; the answers are classified at their own // grain -- the listing a read, the delete a mutation -- and the decision over them is the // deregistration module's, taken without I/O. -// SEALED: a listing reaches a deregistration receipt only as an answer this module received from -// GitHub, never as a value a caller assembled (gunbc.runner.runner_jit_deregistration). +// SEALED, AND IT CARRIES ITS QUERY SUBJECT: the organization and runner name it asked about and the +// App and installation of the token it asked with. A listing is evidence only about what it was +// asked -- an empty answer for another name proves nothing about this one -- so the deregistration +// receipt path compares this subject to its registration before reading the answer +// (gunbc.runner.runner_jit_deregistration readback_subject_refusal). type OrganizationRunnerListRead sole_constructor { + organization: NonEmptyStr + name: NonEmptyStr + app: DeclaredGitHubApp + installation: GitHubAppInstallationId performance: RestExchangePerformance body: OrganizationRunnerListWire } @@ -285,7 +292,12 @@ fn list_runners_by_name_with(token: ControllerInstallationToken, organization: N uses net: Network { let listed = github.ActionsSelfHostedRunners.ListOrganizationRunnersByName(installation_token: token.token, org: organization, name: name) - OrganizationRunnerListRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: listed.outcome }), body: listed.result } + OrganizationRunnerListRead { + organization: organization, + name: name, + app: token.app, + installation: token.installation, + performance: classify_rest_outcome(standing: RestReadExchange { rest: listed.outcome }), body: listed.result } } fn delete_runner_with(token: ControllerInstallationToken, organization: NonEmptyStr, runner_id: Int) -> RestExchangePerformance diff --git a/dag/gunbc/runner/runner_jit_deregistration.dag b/dag/gunbc/runner/runner_jit_deregistration.dag index b4ebb6038e7..1db98290489 100644 --- a/dag/gunbc/runner/runner_jit_deregistration.dag +++ b/dag/gunbc/runner/runner_jit_deregistration.dag @@ -2,6 +2,7 @@ module gunbc.runner.runner_jit_deregistration import std.types { Bool, Int, List, NonEmptyStr, String } import std.resources { Network } +import std.decl_ref { decl_ref } import std.upsert_decision { UpsertDecision, Noop, Apply, Refuse } import extdeps.transports.rest { RestExchangePerformance, RestExchangeSucceeded } import extdeps.github.app { GitHubAppInstallationId } @@ -57,8 +58,8 @@ type JitRegistration sole_constructor { fn jit_registration_of(dispatch: JitMintDispatch) -> JitRegistration? { match dispatch { JitMintDispatchRefused { refusal: _ } => none - JitMintDispatchAuthorized { slot: s, app: a, installation: i, organization: o, request: r } => - Present { value: JitRegistration { organization: o, name: r.name, slot: s, app: a, installation: i } } + JitMintDispatchAuthorized { dispatch: d } => + Present { value: JitRegistration { organization: d.organization, name: d.request.name, slot: d.slot, app: d.app, installation: d.installation } } } } @@ -108,6 +109,7 @@ type JitDeregistrationRefusal | DeregistrationAuthorityForAnotherInstallation | DeregistrationCredentialRefused { unsatisfied: List } | DeregistrationListingUnreadable { performance: RestExchangePerformance, cause: NonEmptyStr } + | DeregistrationReadbackForAnotherSubject { organization: NonEmptyStr, name: NonEmptyStr, app: DeclaredGitHubApp, installation: GitHubAppInstallationId } | DeregistrationRunnerStillListed { ids: List, deletes: List } // THE VERDICT OVER A READBACK, decided without I/O and witnessable on supplied listings. It carries @@ -140,12 +142,50 @@ type JitDeregistration = JitRunnerDeregistered { receipt: JitDeregistrationReceipt } | JitDeregistrationRefused { registration: JitRegistration, exit: JitSlotExit, refusal: JitDeregistrationRefusal } +// A READBACK IS EVIDENCE ONLY ABOUT ITS OWN QUERY. Pure over the subject fields, so the refusal is +// witnessable on supplied values; the receipt path applies it to the sealed read before anything +// else is read from it. +fn readback_subject_refusal( + registration: JitRegistration, + organization: NonEmptyStr, + name: NonEmptyStr, + app: DeclaredGitHubApp, + installation: GitHubAppInstallationId, +) -> JitDeregistrationRefusal? { + if (organization as String) != (registration.organization.login as String) + || (name as String) != (registration.name.value as String) + || app != registration.app + || installation != registration.installation { + Present { value: DeregistrationReadbackForAnotherSubject { organization: organization, name: name, app: app, installation: installation } } + } else { + none + } +} + +// THE RECEIPT'S ONLY CONSTRUCTOR, AND IT IS ADMITTED ONLY TO THE ENSURE: a caller holding some real +// listing cannot hand it here beside a registration of its choosing. fn conclude_from_readback( registration: JitRegistration, exit: JitSlotExit, deletes: List, readback: OrganizationRunnerListRead, -) -> JitDeregistration { +) -> JitDeregistration admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), +] { + match readback_subject_refusal(registration: registration, organization: readback.organization, name: readback.name, app: readback.app, installation: readback.installation) { + Present { value: r } => JitDeregistrationRefused { registration: registration, exit: exit, refusal: r } + Absent => conclude_after_subject_check(registration: registration, exit: exit, deletes: deletes, readback: readback) + } +} + +fn conclude_after_subject_check( + registration: JitRegistration, + exit: JitSlotExit, + deletes: List, + readback: OrganizationRunnerListRead, +) -> JitDeregistration admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "conclude_from_readback"), +] { match classify_jit_deregistration(readback: jit_runner_listing_of_read(name: registration.name, read: readback), deletes: deletes) { ReadbackShowsAbsent => JitRunnerDeregistered { receipt: JitDeregistrationReceipt { registration: registration, exit: exit, deletes: deletes, readback: readback } } @@ -172,7 +212,9 @@ fn readback_after_deletes( token: ControllerInstallationToken, registration: JitRegistration, deletes: List, -) -> DeletesThenReadback uses net: Network { +) -> DeletesThenReadback uses net: Network admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), +] { DeletesThenReadback { deletes: deletes, readback: list_runners_by_name_with(token: token, organization: registration.organization.login, name: registration.name.value), diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 7d0d0f9b895..90359964001 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -1,6 +1,7 @@ module gunbc.runner.runner_jit_perform import std.types { String, NonEmptyStr, Bool, Int, List } +import std.decl_ref { decl_ref } import std.disposition { ConstructionMechanism, SingleAuthority } import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.github.app { GitHubAppInstallationId } @@ -96,15 +97,23 @@ type JitDispatchRefusal // a witness over it would be permanently green by construction and worse than absent, because it // would be cited as coverage. It is recorded here as a totality arm and counted as none. type JitMintDispatch - = JitMintDispatchAuthorized { - slot: JitSlot - app: DeclaredGitHubApp - installation: GitHubAppInstallationId - organization: GitHubOrganizationRef - request: JitRunnerConfigRequest - } + = JitMintDispatchAuthorized { dispatch: AuthorizedJitMintDispatch } | JitMintDispatchRefused { refusal: JitDispatchRefusal } +// THE AUTHORIZED PAYLOAD IS SEALED. An open arm could be authored by any module naming any runner, +// and every consumer downstream -- the delivery, the generate gate, the deregistration subject +// (gunbc.runner.runner_jit_deregistration jit_registration_of) -- would take it as a plan that +// passed the gates. dispatch_jit_mint is its only constructor, and dispatch_jit_mint is itself +// admit_callers-sealed, as is every admitted caller that returns the dispatch onward: an admitted +// call edge does not confine a value an exported wrapper hands back. +type AuthorizedJitMintDispatch sole_constructor { + slot: JitSlot + app: DeclaredGitHubApp + installation: GitHubAppInstallationId + organization: GitHubOrganizationRef + request: JitRunnerConfigRequest +} + type AuthorityInstallation { app: DeclaredGitHubApp installation: GitHubAppInstallationId @@ -125,7 +134,11 @@ fn dispatch_jit_mint( organization: GitHubOrganizationRef, runner_group: MicrovmRunnerGroupStanding, attempt_binding: JitAdmissionAxisStanding -) -> JitMintDispatch { +) -> JitMintDispatch admit_callers: [ + decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "attempt_dispatch"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), +] { match plan_jit_mint( binding: binding, authority: authority, @@ -161,11 +174,13 @@ fn dispatch_jit_mint( } Present { value: held } => JitMintDispatchAuthorized { - slot: slot, - app: held.app, - installation: held.installation, - organization: organization, - request: request, + dispatch: AuthorizedJitMintDispatch { + slot: slot, + app: held.app, + installation: held.installation, + organization: organization, + request: request, + }, } } } @@ -201,13 +216,8 @@ fn receive_jit_mint( ) -> JitCredentialDelivery { match dispatch { JitMintDispatchRefused { refusal: r } => JitCredentialNotDispatched { refusal: r } - JitMintDispatchAuthorized { - slot: slot, - app: _, - installation: _, - organization: _, - request: _, - } => + JitMintDispatchAuthorized { dispatch: authorized } => { + let slot = authorized.slot match outcome { JitConfigMinted { config: config, runner_id: runner_id } => JitCredentialBoundToAttempt { @@ -228,6 +238,7 @@ fn receive_jit_mint( outcome: JitConfigMintValidationFailed { reason: reason }, } } + } } } diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index bbdfe99d528..d01463f7758 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -1,5 +1,6 @@ module gunbc.runner_microvm_slot_controller +import std.decl_ref { decl_ref } import std.durable_compare_and_set { cas_unreadable_slot_detail } import std.types { String, NonEmptyStr, Bool, List, Int } import std.algebra { trim } @@ -263,7 +264,10 @@ fn controller_credential_authority() -> GitHubCredentialAuthority { } } -fn attempt_dispatch(binding: MicroVmCellBinding, state: GitHubAppControlPlaneState, runner_group: MicrovmRunnerGroupStanding) -> JitMintDispatch { +fn attempt_dispatch(binding: MicroVmCellBinding, state: GitHubAppControlPlaneState, runner_group: MicrovmRunnerGroupStanding) -> JitMintDispatch admit_callers: [ + decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "mint_under_observed_group"), + decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "mint_attempt_credential"), +] { dispatch_jit_mint( binding: MicroVmSlotBinding { binding: binding }, authority: controller_credential_authority(), @@ -310,7 +314,7 @@ fn mint_attempt_credential(binding: MicroVmCellBinding, state: GitHubAppControlP mint_under_observed_group(binding: binding, state: state) other => JitMintReceived { delivery: JitCredentialNotDispatched { refusal: other } } } - JitMintDispatchAuthorized { slot: _, app: _, installation: _, organization: _, request: _ } => + JitMintDispatchAuthorized { dispatch: _ } => mint_under_observed_group(binding: binding, state: state) } } diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index 3e7b43286d8..38b58718522 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -16,7 +16,7 @@ import extdeps.github.actions_token { ActionsRepositoryPolicyUnobserved, } import extdeps.github.effect { contents_write, workflows_write } -import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.decl_ref { DeclarationRef, WholeDeclaration, decl_ref } import gunbc.repository { gunbc_repository } import gunbc.build_cache_instance { RunnerSlotIdentity } import gunbc.fleet_intent_network { operator_host_srv2 } @@ -63,7 +63,7 @@ import extdeps.bmc.endpoint { BmcControllerEndpoint } import extdeps.github.actions_runner { ActionsRunnerLinuxArm64 } import gunbc.runner_slot_desired { gunbc_runner_slot_desired } import gunbc.runner_throughput_qualification_route { ephemeral_slot_unit, ephemeral_slot_labels, ephemeral_slot_argv } -import gunbc.runner.runner_jit_deregistration { jit_registration_of } +import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of, readback_subject_refusal, DeregistrationReadbackForAnotherSubject } import gunbc.runner.runner_host_unit_slot { HostUnitSlotStanding, HostUnitSlotRefused, HostUnitSlotHostNotBound, host_unit_slot_for, host_unit_slot_listener_argv, } @@ -1158,7 +1158,21 @@ test fn witness_jit_mint_refuses_when_attempt_binding_is_deferred() -> Bool { // proof named. The performer is gunbc.github_effect_perform, and its consumer frontier records that // no production route runs it yet. -fn dispatched(owned: Bool, perms: List) -> JitMintDispatch { +// EGRESS-SEALED: this helper returns the sealed authorized dispatch, so it is admitted only to the +// claims that consume it -- an exported wrapper would otherwise be the open door the seal closes. +fn dispatched(owned: Bool, perms: List) -> JitMintDispatch admit_callers: [ + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_an_unowned_cell_before_naming_a_token_url"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_a_credential_missing_the_runner_permission"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_authorizes_and_names_both_steps_of_the_app_path"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_refused_dispatch_delivers_nothing_even_from_a_minted_credential"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_an_authorized_dispatch_delivers_nothing_when_the_mint_was_not_created"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_minted_credential_is_bound_to_the_dispatched_attempt"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_deregistration_subject_exists_only_for_an_authorized_dispatch"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_readback_for_another_subject_is_refused"), + decl_ref(module_path: "test.claim.github_effect_perform_witness", decl_name: "witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered"), + decl_ref(module_path: "test.claim.github_effect_perform_witness", decl_name: "witness_a_dispatch_for_another_app_is_refused_before_signing"), + decl_ref(module_path: "test.claim.github_effect_perform_witness", decl_name: "witness_a_refused_dispatch_performs_no_mint"), +] { dispatch_jit_mint( binding: jit_binding(owned: owned), authority: app_credential, @@ -1236,17 +1250,14 @@ test fn witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred() -> B // plan authorized, so a dispatch cannot address another organization's runners. test fn witness_jit_dispatch_authorizes_and_names_both_steps_of_the_app_path() -> Bool { match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { - slot: MicroVmJitSlot { attempt: a }, - app: app, - installation: installation, - organization: org, - request: req, - } => - installation.value == 999 - && app == gunbai_bot_declared - && (org.login as String) == "gunb-ai" - && (req.name.value as String) == (runner_microvm_attempt_unit_name(attempt: a) as String) + JitMintDispatchAuthorized { dispatch: d } => + d.installation.value == 999 + && d.app == gunbai_bot_declared + && (d.organization.login as String) == "gunb-ai" + && match d.slot { + MicroVmJitSlot { attempt: a } => (d.request.name.value as String) == (runner_microvm_attempt_unit_name(attempt: a) as String) + _ => false + } _ => false } } @@ -1301,7 +1312,7 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool runner_id: id, } => match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { slot: d, app: _, installation: _, organization: _, request: _ } => a == d + JitMintDispatchAuthorized { dispatch: d } => a == d.slot _ => false } && (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" @@ -1428,12 +1439,37 @@ test fn witness_a_deregistration_subject_exists_only_for_an_authorized_dispatch( Present { value: _ } => false } refused_none && match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { slot: _, app: app, installation: i, organization: _, request: req } => + JitMintDispatchAuthorized { dispatch: d } => match jit_registration_of(dispatch: dispatched(owned: true, perms: runner_grant)) { Present { value: reg } => - (reg.name.value as String) == (req.name.value as String) && reg.app == app && reg.installation == i + (reg.name.value as String) == (d.request.name.value as String) && reg.app == d.app && reg.installation == d.installation Absent => false } _ => false } } + +// RED: A READBACK FOR ANOTHER SUBJECT IS NOT EVIDENCE ABOUT THIS ONE. A real, empty listing for +// `other-name` -- or one taken in another organization or with another installation's token -- must +// refuse before its answer is read, and the registration's own subject must pass. +fn readback_for_another_subject_refuses(reg: JitRegistration, org: NonEmptyStr, name: NonEmptyStr, app: DeclaredGitHubApp, i: GitHubAppInstallationId) -> Bool { + match readback_subject_refusal(registration: reg, organization: org, name: name, app: app, installation: i) { + Present { value: DeregistrationReadbackForAnotherSubject { organization: _, name: _, app: _, installation: _ } } => true + _ => false + } +} + +test fn witness_a_readback_for_another_subject_is_refused() -> Bool { + match jit_registration_of(dispatch: dispatched(owned: true, perms: runner_grant)) { + Absent => false + Present { value: reg } => + (match readback_subject_refusal(registration: reg, organization: reg.organization.login, name: reg.name.value, app: reg.app, installation: reg.installation) { + Absent => true + Present { value: _ } => false + }) + && readback_for_another_subject_refuses(reg: reg, org: reg.organization.login, name: "other-name", app: reg.app, i: reg.installation) + && readback_for_another_subject_refuses(reg: reg, org: "another-org", name: reg.name.value, app: reg.app, i: reg.installation) + && readback_for_another_subject_refuses(reg: reg, org: reg.organization.login, name: reg.name.value, app: gunbai_ci_declared, i: reg.installation) + && readback_for_another_subject_refuses(reg: reg, org: reg.organization.login, name: reg.name.value, app: reg.app, i: other_installation_id) + } +} diff --git a/dag/test/claim/runner/jit_deregistration_forged_probe_witness_test.dag b/dag/test/claim/runner/jit_deregistration_forged_probe_witness_test.dag new file mode 100644 index 00000000000..cbb83654c89 --- /dev/null +++ b/dag/test/claim/runner/jit_deregistration_forged_probe_witness_test.dag @@ -0,0 +1,56 @@ +module test.claim.jit_deregistration_forged_probe_witness + +import std.types { String, Bool, Int, List } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE FORGED DEREGISTRATION ROUTES, ENROLLED AS EXECUTED REDS (DESIGN §4b: a state unrepresentable +// in the accepted corpus is still representable as source handed to the compiler, and the refusal +// is the enrollable evidence). test.probe.jit_deregistration_forged_probe authors an authorized +// dispatch, a deregistration subject, a listing and a receipt for `victim-runner`, and calls the +// receipt's constructor from outside the ensure; each must refuse at its own subject, so the forged +// name reaches neither a listing nor a delete. Counts are scoped to class AND subject, and a control +// source that only NAMES the modules must refuse at none of them, so the refusals are attributable +// to the forgeries rather than to importing the modules at all. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn probe() -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: "dag/test/probe/jit_deregistration_forged_probe.dag").content) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_jit_deregistration_harness_control\nimport std.types { Bool }\nimport gunbc.runner.runner_jit_perform { JitMintDispatch }\nimport gunbc.runner.runner_jit_deregistration { JitDeregistration, jit_runner_deregistered }\nfn done(d: JitDeregistration) -> Bool { jit_runner_deregistered(result: d) }\n" + +data sealed_records: List = ["AuthorizedJitMintDispatch", "JitRegistration", "OrganizationRunnerListRead", "JitDeregistrationReceipt"] + +test fn a_source_that_only_names_the_modules_forges_nothing() -> Bool { + let c = census_of(source: control_source) + (sealed_records |> all(s => blocking(c: c, wanted: "SoleConstructorViolation", subject: s) == 0)) + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "conclude_from_readback") == 0 +} + +test fn every_forged_record_refuses_at_its_own_name() -> Bool { + let c = probe() + sealed_records |> all(s => blocking(c: c, wanted: "SoleConstructorViolation", subject: s) >= 1) +} + +test fn the_receipt_constructor_refuses_a_caller_other_than_the_ensure() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "conclude_from_readback") >= 1 +} diff --git a/dag/test/probe/jit_deregistration_forged_probe.dag b/dag/test/probe/jit_deregistration_forged_probe.dag new file mode 100644 index 00000000000..70426d64c3e --- /dev/null +++ b/dag/test/probe/jit_deregistration_forged_probe.dag @@ -0,0 +1,79 @@ +module test.probe.jit_deregistration_forged_probe + +// THIS MODULE MUST NOT RESOLVE. Each function below is a route by which a caller outside the JIT +// modules would otherwise point the deregistration at a runner it chose -- here `victim-runner`, +// a name no authorized dispatch minted -- or mint a deregistered receipt it did not earn. The +// witness test.claim.jit_deregistration_forged_probe_witness hands this source to the compiler and +// requires each refusal at its subject by name. dag/test/probe/ sits under the whole-tree +// strict-resolve exclusion, so this source reaches the compiler only through that witness. + +import std.types { Int, List, NonEmptyStr, String } +import extdeps.transports.rest { RestExchangeSucceeded } +import extdeps.github.app { GitHubAppInstallationId } +import extdeps.github.effect { GitHubOrganizationRef } +import extdeps.github.actions_jit_runner { JitRunnerConfigRequest, JitRunnerName, JitRunnerGroupId, JitWorkFolder } +import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } +import gunbc.auth.github_apps { DeclaredGitHubApp } +import gunbc.runner_jit_mint { JitSlot } +import gunbc.runner.runner_jit_perform { JitMintDispatch, JitMintDispatchAuthorized, AuthorizedJitMintDispatch } +import gunbc.github_effect_perform { OrganizationRunnerListRead } +import gunbc.runner.runner_jit_deregistration { + JitRegistration, JitDeregistration, JitDeregistrationReceipt, JitRunnerDeregistered, JitSlotExit, SlotJobCompleted, + jit_registration_of, conclude_from_readback, +} + +fn victim_request() -> JitRunnerConfigRequest { + JitRunnerConfigRequest { + name: JitRunnerName { value: "victim-runner" }, + runner_group_id: JitRunnerGroupId { value: 1 }, + labels: [], + work_folder: JitWorkFolder { value: "_work" }, + } +} + +// (a) AN OUTSIDE-AUTHORED AUTHORIZED ARM. Refuses at AuthorizedJitMintDispatch. +fn forged_dispatch(slot: JitSlot, app: DeclaredGitHubApp, installation: GitHubAppInstallationId) -> JitMintDispatch { + JitMintDispatchAuthorized { + dispatch: AuthorizedJitMintDispatch { + slot: slot, + app: app, + installation: installation, + organization: GitHubOrganizationRef { login: "gunb-ai" }, + request: victim_request(), + }, + } +} + +// (b) A HAND-BUILT DEREGISTRATION SUBJECT. Refuses at JitRegistration. +fn forged_registration(slot: JitSlot, app: DeclaredGitHubApp, installation: GitHubAppInstallationId) -> JitRegistration { + JitRegistration { + organization: GitHubOrganizationRef { login: "gunb-ai" }, + name: JitRunnerName { value: "victim-runner" }, + slot: slot, + app: app, + installation: installation, + } +} + +// (c) A HAND-BUILT EMPTY LISTING FOR victim-runner. Refuses at OrganizationRunnerListRead. +fn forged_listing(app: DeclaredGitHubApp, installation: GitHubAppInstallationId) -> OrganizationRunnerListRead { + OrganizationRunnerListRead { + organization: "gunb-ai", + name: "victim-runner", + app: app, + installation: installation, + performance: RestExchangeSucceeded, + body: OrganizationRunnerListWire { total_count: 0, runners: [] }, + } +} + +// (d) THE RECEIPT'S CONSTRUCTOR, CALLED FROM OUTSIDE THE ENSURE with a real registration beside a +// listing of the caller's choosing. Refuses at conclude_from_readback. +fn unadmitted_conclusion(registration: JitRegistration, readback: OrganizationRunnerListRead) -> JitDeregistration { + conclude_from_readback(registration: registration, exit: SlotJobCompleted, deletes: [], readback: readback) +} + +// (e) A HAND-BUILT SUCCESS RECEIPT. Refuses at JitDeregistrationReceipt. +fn forged_receipt(registration: JitRegistration, readback: OrganizationRunnerListRead) -> JitDeregistration { + JitRunnerDeregistered { receipt: JitDeregistrationReceipt { registration: registration, exit: SlotJobCompleted, deletes: [], readback: readback } } +} From dbb305f5e16bcd2ce0252dd07e7a76bc15780bb4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 08:14:29 +0000 Subject: [PATCH 18/44] Merge #13206 d3514f3 (AuthorizedJitMintDispatch sealed); the dispatch subject consumes the sealed authorized dispatch; witness claims mint in place, admitted by name Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 7 ++ .../runner/runner_qualification_dispatch.dag | 19 ++-- ...er_qualification_dispatch_witness_test.dag | 100 ++++++++---------- 3 files changed, 67 insertions(+), 59 deletions(-) diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 90359964001..23a92d5eced 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -138,6 +138,13 @@ fn dispatch_jit_mint( decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "attempt_dispatch"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "slot_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "no_other_workflow_or_label_can_be_dispatched_through_the_ruling"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_dispatch_has_three_standings_and_an_interlock"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_unpinned_or_unreadable_run_refuses"), ] { match plan_jit_mint( binding: binding, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 9034fd38604..76898a1a585 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -31,8 +31,8 @@ import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, } import gunbc.runner_jit_mint { HostUnitJitSlot, MicroVmJitSlot } -import gunbc.runner.runner_jit_perform { JitCredentialDelivery, JitCredentialBoundToAttempt } -import gunbc.runner.runner_jit_deregistration { JitRegistration } +import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt } +import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } import gunbc.managed_host_unit_hold { UnitHoldProof } import gunbc.github_effect_perform { ControllerInstallationToken, dispatch_workflow_with, read_run_with, read_attempt_jobs_with, @@ -49,7 +49,9 @@ import gunbc.runner_throughput_qualification_route { QualificationInstrument, qu // // THE SUBJECT IS SEALED AND ITS SCOPE IS FIXED BY CONSTRUCTION. QualificationDispatchSubject is // minted only by qualification_dispatch_subject_over, and only from: -// - the JIT registration the route minted and the credential delivered for that same slot -- so +// - the sealed AuthorizedJitMintDispatch (gunbc.runner.runner_jit_perform), whose only mint is +// dispatch_jit_mint's authorized arm, its registration (jit_registration_of), and the credential +// delivered for that same slot -- so // the attempt is the slot's own and the runner the collection joins on is that registration's; // - the qualification workflow, which is not a parameter of the request: its path is the generated // fleet-converge artifact (gunbc.generated_artifact FleetConvergeYamlArtifact) and production @@ -111,6 +113,7 @@ type QualificationDispatchSubject sole_constructor { type QualificationSubjectRefusal = SubjectCredentialNotDelivered { delivery: JitCredentialDelivery } + | SubjectNoRegistration | SubjectDeliveryForAnotherSlot | SubjectSlotNotHostUnit | SubjectSlotForAnotherWorkflow { slot_workflow: NonEmptyStr, qualification_workflow: NonEmptyStr } @@ -185,7 +188,7 @@ fn job_display_name(job: Job) -> NonEmptyStr { // model carrying the floor job the boot leg's mode will add. An admit list constrains the call edge, // not where the result goes, so every admitted witness entry is a `test fn` returning Bool. fn qualification_dispatch_subject_over( - registration: JitRegistration, + dispatch: AuthorizedJitMintDispatch, delivery: JitCredentialDelivery, workflow: Workflow, floor_job_id: NonEmptyStr, @@ -205,6 +208,9 @@ fn qualification_dispatch_subject_over( decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "undeclared_input_refuses"), ] { + match jit_registration_of(dispatch: JitMintDispatchAuthorized { dispatch: dispatch }) { + Absent => QualificationSubjectRefused { refusal: SubjectNoRegistration } + Present { value: registration } => match delivery { JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: _ } => if delivered_slot != registration.slot { @@ -243,20 +249,21 @@ fn qualification_dispatch_subject_over( } other => QualificationSubjectRefused { refusal: SubjectCredentialNotDelivered { delivery: other } } } + } } // THE PRODUCTION MINT: the qualification workflow is the generated fleet-converge model. Until the // boot leg lands the route's mode there, it has no floor job reading the attempt input and this // refuses -- the wiring the route leg owes, refused rather than faked. fn qualification_dispatch_subject( - registration: JitRegistration, + dispatch: AuthorizedJitMintDispatch, delivery: JitCredentialDelivery, floor_job_id: NonEmptyStr, branch: NonEmptyStr, revision: CommitSha, ) -> QualificationSubjectStanding { qualification_dispatch_subject_over( - registration: registration, + dispatch: dispatch, delivery: delivery, workflow: fleet_converge_workflow, floor_job_id: floor_job_id, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 28a6eab63c3..7b2d07434e1 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -14,10 +14,9 @@ import extdeps.github.workflow_runs { } import gunbc.managed_host { ManagedHost, managed_hosts, managed_host_binding } import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } -import gunbc.runner_jit_mint { HostUnitSlotBinding } +import gunbc.runner_jit_mint { JitSlotBinding, HostUnitSlotBinding } import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } -import gunbc.runner.runner_jit_perform { JitMintDispatch, JitCredentialDelivery, dispatch_jit_mint, receive_jit_mint } -import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } +import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitMintDispatchRefused, JitCredentialDelivery, dispatch_jit_mint, receive_jit_mint } import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit } import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } import gunbc.runner.runner_qualification_dispatch { @@ -43,25 +42,20 @@ data floor_job_id: NonEmptyStr = "qualification-floor" // ── THE SUBJECT, THROUGH THE REAL MINT ────────────────────────────────────────────────────────── // The registration and the delivered credential come from the route's own JIT mint over the real -// managed-host roster: dispatch_jit_mint, jit_registration_of, and receive_jit_mint over a 201 +// managed-host roster: each claim calls dispatch_jit_mint itself (admitted by name), and the subject mint takes its sealed AuthorizedJitMintDispatch with receive_jit_mint over a 201 // decoded by upstream's own outcome fold (runner id 4242). Nothing below constructs a registration, // and every caller of the sealed subject mint is a claim returning Bool (no constructor proxy). -fn minted(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitMintDispatch { - dispatch_jit_mint( - binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: slot_workflow) }, - authority: app_credential, - control_plane: observed_grant(perms: runner_grant), - organization: gunbai_org_ref, - runner_group: group_restricted_to(workflow: slot_workflow), - attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), - ) -} -fn delivery(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitCredentialDelivery { - receive_jit_mint(dispatch: minted(h: h, slot_workflow: slot_workflow), outcome: minted_outcome) + +fn slot_binding(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitSlotBinding { + HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: slot_workflow) } +} + +fn delivered(d: AuthorizedJitMintDispatch) -> JitCredentialDelivery { + receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome) } fn qualification_slot() -> NonEmptyStr { @@ -106,10 +100,10 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { // generated fleet-converge workflow file in gunbc_repository at the pinned revision. test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => @@ -129,29 +123,29 @@ test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { QualificationSubjectRefused { refusal: _ } => false } } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } } fn slot_refuses(h: ManagedHost, slot_workflow: NonEmptyStr) -> Bool { - match jit_registration_of(dispatch: minted(h: h, slot_workflow: slot_workflow)) { - Present { value: other_reg } => + match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: slot_workflow), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: slot_workflow), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: other } => match qualification_dispatch_subject_over( - registration: other_reg, delivery: delivery(h: h, slot_workflow: slot_workflow), + dispatch: other, delivery: delivered(d: other), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) _ => false } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } } -fn runs_on_refuses(h: ManagedHost, reg: JitRegistration, runs_on: RunnerSpec) -> Bool { +fn runs_on_refuses(h: ManagedHost, d: AuthorizedJitMintDispatch, runs_on: RunnerSpec) -> Bool { match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true @@ -159,9 +153,9 @@ fn runs_on_refuses(h: ManagedHost, reg: JitRegistration, runs_on: RunnerSpec) -> } } -fn undeclared_input_refuses(h: ManagedHost, reg: JitRegistration) -> Bool { +fn undeclared_input_refuses(h: ManagedHost, d: AuthorizedJitMintDispatch) -> Bool { match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true @@ -180,24 +174,24 @@ fn undeclared_input_refuses(h: ManagedHost, reg: JitRegistration) -> Bool { // refuses rather than admitting anything. test fn no_other_workflow_or_label_can_be_dispatched_through_the_ruling() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { slot_refuses(h: h, slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") && slot_refuses(h: h, slot_workflow: qualification_selected_workflow(branch: "feature")) - && runs_on_refuses(h: h, reg: reg, runs_on: SelfHosted { labels: [attempt as String] }) - && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) - && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) - && runs_on_refuses(h: h, reg: reg, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) - && undeclared_input_refuses(h: h, reg: reg) + && runs_on_refuses(h: h, d: d, runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && undeclared_input_refuses(h: h, d: d) && (match qualification_dispatch_subject( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: _ } => false QualificationSubjectRefused { refusal: _ } => true }) } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } @@ -216,15 +210,15 @@ fn ambiguous(s: QualificationDispatchStanding) -> Bool { // the hold covers the slot's own host and no other. test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { (match mutation_standing(rest: RestStatusRefused { status: 422, body: "Unexpected inputs provided" }) { DispatchStandingRefused { status: st, body: _ } => st == 422 _ => false }) && ambiguous(s: mutation_standing(rest: RestTransportRefused { cause: "fixture: connection reset after send" })) && ambiguous(s: mutation_standing(rest: RestStatusRefused { status: 503, body: "fixture" })) && ambiguous(s: mutation_standing(rest: RestBodyUndecodable { status: 200, cause: "fixture: truncated body" })) && (match mutation_standing(rest: RestOk) { DispatchStandingSucceeded => true _ => false }) && match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => @@ -232,7 +226,7 @@ test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { QualificationSubjectRefused { refusal: _ } => false } } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } @@ -322,10 +316,10 @@ fn mismatch(v: QualificationRunVerdict) -> Bool { // on the minted runner that never started; two jobs answering to the floor's name. test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { @@ -353,7 +347,7 @@ test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { QualificationSubjectRefused { refusal: _ } => false } } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } @@ -364,11 +358,11 @@ test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { // another event; another workflow path. test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { @@ -385,7 +379,7 @@ test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { QualificationSubjectRefused { refusal: _ } => false } } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } @@ -393,11 +387,11 @@ test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { match first(managed_hosts()) { - Present { value: h } => match jit_registration_of(dispatch: minted(h: h, slot_workflow: qualification_slot())) { - Present { value: reg } => { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => { let path = qualification_workflow_path() as String match qualification_dispatch_subject_over( - registration: reg, delivery: delivery(h: h, slot_workflow: qualification_slot()), + dispatch: d, delivery: delivered(d: d), workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, ) { QualificationSubjectAdmitted { subject: s } => { @@ -430,7 +424,7 @@ test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { QualificationSubjectRefused { refusal: _ } => false } } - Absent => false + JitMintDispatchRefused { refusal: _ } => false } Absent => false } From d8324c43c01b0dde6858a809a057cc0ebf18e874 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 08:28:26 +0000 Subject: [PATCH 19/44] leg 3b: enrol the job-log receipt seal as executed compile-refusal REDs (#13206's forged-probe pattern) test.probe.qualification_job_log_receipt_forged_probe writes the sealed receipt and calls job_log_standing from outside; test.claim.qualification_job_log_receipt_forged_probe_witness requires SoleConstructorViolation at QualificationJobLogReceipt and ConstructorCallAdmissionRefused at job_log_standing beside a names-only control. The earlier 'checked once, not enrolled' rung rested on a stale premise: v2.workflow.required_floor deleted DeclinedLiveTree, so ReadsLiveTree claims run. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 20 +++---- ..._log_receipt_forged_probe_witness_test.dag | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index d61abed842b..0ca924267ee 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -677,20 +677,12 @@ fn designated_job_log(designated_job_id: Int, log: AttemptJobLog) -> DesignatedJ // have to trust rows detached from the job and the bytes that produced them. The decoder above is // open; this record is not. // -// RUNG OF THE SEAL, STATED AT ITS EVIDENCE (DESIGN 4b(1)): CHECKED ONCE, NOT ENROLLED. The seal is -// the compiler's sole_constructor and admit_callers walls applied to this record and -// job_log_standing. On 2026-10-04, compile_dag_diagnostic_census probes run once with the -// interpreter gave one SoleConstructorViolation for a forged receipt literal (and one for a since- -// deleted subject record, whose role the sealed CollectedQualificationRun now plays), one ConstructorCallAdmissionRefused for an outside call to job_log_standing, and -// zero for a module that only calls the decoder. Nothing re-runs those probes. A probe that compiles -// against these declarations must import this module, so it is ReadsLiveTree, and the required -// floor declines ReadsLiveTree claims rather than executing them -// (gunbc.qualified_pipe_callee_seed_growth); the SubstrateInputsOnly compile harnesses compile only -// self-contained modules, which would test the compiler's generic wall rather than this seal. -// NEXT-RUNG TRIGGER, A CAPABILITY: the required floor can enrol and execute a ReadsLiveTree -// compile-refusal probe, SUFFICIENT FOR those four probes running on every required run, so that -// removing sole_constructor from the record or the admit list from job_log_standing turns a -// required lane red. +// THE SEAL IS ENROLLED AS EXECUTED REDS. test.claim.qualification_job_log_receipt_forged_probe_witness +// compiles test.probe.qualification_job_log_receipt_forged_probe -- a receipt written from invented +// readings, and job_log_standing called from outside the collect stage -- and requires a +// SoleConstructorViolation at QualificationJobLogReceipt and a ConstructorCallAdmissionRefused at +// job_log_standing, beside a control that only names this module and refuses at neither. Removing +// sole_constructor from this record or the admit list from job_log_standing turns that witness red. type QualificationJobLogReceipt sole_constructor { collected: CollectedQualificationRun slot: HostUnitSlot diff --git a/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag new file mode 100644 index 00000000000..39289e42849 --- /dev/null +++ b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag @@ -0,0 +1,53 @@ +module test.claim.qualification_job_log_receipt_forged_probe_witness + +import std.types { String, Bool, Int, List } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE FORGED JOB-LOG RECEIPT ROUTES, ENROLLED AS EXECUTED REDS (DESIGN 4b: a state unrepresentable +// in the accepted corpus is still representable as source handed to the compiler, and the refusal +// is the enrollable evidence). test.probe.qualification_job_log_receipt_forged_probe writes the +// sealed receipt from invented readings and calls its one minter from outside the collect stage; +// each must refuse at its own subject. Counts are scoped to class AND subject, and a control source +// that only NAMES the module and calls its open decoder must refuse at neither, so the refusals are +// attributable to the forgeries rather than to importing the module at all. The pattern is +// test.claim.jit_deregistration_forged_probe_witness's. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn probe() -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: "dag/test/probe/qualification_job_log_receipt_forged_probe.dag").content) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_qualification_receipt_harness_control\nimport std.types { String }\nimport gunbc.runner.runner_qualification_dispatch { JobLogInstrumentDecode, decode_job_log_instruments }\nfn decoded(t: String) -> JobLogInstrumentDecode { decode_job_log_instruments(log: t, slot_unit: \"x.service\") }\n" + +test fn a_source_that_only_names_the_module_forges_nothing() -> Bool { + let c = census_of(source: control_source) + blocking(c: c, wanted: "SoleConstructorViolation", subject: "QualificationJobLogReceipt") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") == 0 +} + +test fn a_forged_job_log_receipt_refuses_at_its_own_name() -> Bool { + blocking(c: probe(), wanted: "SoleConstructorViolation", subject: "QualificationJobLogReceipt") >= 1 +} + +test fn the_receipt_minter_refuses_a_caller_other_than_the_collect_stage() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") >= 1 +} From 8bc906963f6e47443112a2fcfadf8805d99dca48 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 09:05:01 +0000 Subject: [PATCH 20/44] leg 3: split the over-budget scope claim into one claim per independent case (floor COMPLETED-OVER-COST-REQUIREMENT 604048 > 72300) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 5 +- .../runner/runner_qualification_dispatch.dag | 5 +- ...er_qualification_dispatch_witness_test.dag | 68 ++++++++++++------- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 23a92d5eced..c29c82cbde7 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -140,7 +140,10 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "slot_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "no_other_workflow_or_label_can_be_dispatched_through_the_ruling"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_another_workflow_file_cannot_become_a_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_floor_runs_on_other_than_exactly_the_attempt_input_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_refuses_until_the_qualification_mode_lands"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_dispatch_has_three_standings_and_an_interlock"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 76898a1a585..ae3ecbab0ae 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -198,7 +198,10 @@ fn qualification_dispatch_subject_over( admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "no_other_workflow_or_label_can_be_dispatched_through_the_ruling"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_another_workflow_file_cannot_become_a_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_floor_runs_on_other_than_exactly_the_attempt_input_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_refuses_until_the_qualification_mode_lands"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_unpinned_or_unreadable_run_refuses"), diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 7b2d07434e1..5ecbe4bc559 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -163,34 +163,52 @@ fn undeclared_input_refuses(h: ManagedHost, d: AuthorizedJitMintDispatch) -> Boo } } -// THE RULING'S SCOPE IS CONSTRUCTION, AND THIS IS ITS RED. Through the one mint, no other workflow -// and no other label can become a dispatch subject: -// - a slot registered for another workflow file, or for the qualification path on another branch, -// refuses before the workflow model is read; -// - a floor job whose runs-on is a literal label, another input, the attempt input composed with -// anything else, or a bare string, refuses; -// - a workflow that does not declare the input refuses; -// - the production mint, over today's generated fleet-converge model (no qualification mode yet), -// refuses rather than admitting anything. -test fn no_other_workflow_or_label_can_be_dispatched_through_the_ruling() -> Bool { +// THE RULING'S SCOPE IS CONSTRUCTION, AND THESE ARE ITS REDS -- one claim per independent case, so +// each pays for its own mint and nothing else (DESIGN section 3: a conjunction of independent cases +// forced through one budget). Through the one mint, no other workflow and no other label can become +// a dispatch subject. + +// A slot registered for another workflow file refuses before the workflow model is read. +test fn a_slot_for_another_workflow_file_cannot_become_a_subject() -> Bool { + match first(managed_hosts()) { + Present { value: h } => slot_refuses(h: h, slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") + Absent => false + } +} + +// The qualification path on another branch is another selected workflow, and refuses the same way. +test fn a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject() -> Bool { + match first(managed_hosts()) { + Present { value: h } => slot_refuses(h: h, slot_workflow: qualification_selected_workflow(branch: "feature")) + Absent => false + } +} + +// A floor job whose runs-on is a literal label, another input, the attempt input composed with +// anything else, or a bare string, refuses; so does a workflow that does not declare the input. +test fn a_floor_runs_on_other_than_exactly_the_attempt_input_refuses() -> Bool { match first(managed_hosts()) { Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { - slot_refuses(h: h, slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") - && slot_refuses(h: h, slot_workflow: qualification_selected_workflow(branch: "feature")) - && runs_on_refuses(h: h, d: d, runs_on: SelfHosted { labels: [attempt as String] }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) - && undeclared_input_refuses(h: h, d: d) - && (match qualification_dispatch_subject( - dispatch: d, delivery: delivered(d: d), - floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: _ } => false - QualificationSubjectRefused { refusal: _ } => true - }) + JitMintDispatchAuthorized { dispatch: d } => runs_on_refuses(h: h, d: d, runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && undeclared_input_refuses(h: h, d: d) + JitMintDispatchRefused { refusal: _ } => false } + Absent => false + } +} + +// THE REAL PATH: the production mint, over today's generated fleet-converge model -- which has no +// qualification mode yet -- refuses rather than admitting anything. +test fn the_production_mint_refuses_until_the_qualification_mode_lands() -> Bool { + match first(managed_hosts()) { + Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: d } => match qualification_dispatch_subject(dispatch: d, delivery: delivered(d: d), floor_job_id: floor_job_id, branch: branch, revision: revision) { + QualificationSubjectAdmitted { subject: _ } => false + QualificationSubjectRefused { refusal: _ } => true + } JitMintDispatchRefused { refusal: _ } => false } Absent => false From 022f7ecb632b1db6ff78c8e43f31e21429569443 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 09:36:13 +0000 Subject: [PATCH 21/44] leg 3b: commit the forged-receipt probe source (a local probe/ exclude rule had kept it out of the tree) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ification_job_log_receipt_forged_probe.dag | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 dag/test/probe/qualification_job_log_receipt_forged_probe.dag diff --git a/dag/test/probe/qualification_job_log_receipt_forged_probe.dag b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag new file mode 100644 index 00000000000..b6f83a07537 --- /dev/null +++ b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag @@ -0,0 +1,27 @@ +module test.probe.qualification_job_log_receipt_forged_probe + +// THIS MODULE MUST NOT RESOLVE. Each function below is a route by which a caller outside +// gunbc.runner.runner_qualification_dispatch would otherwise hold a job-log receipt for rows it never +// read from a run: (a) writing the sealed receipt from invented readings, and (b) calling the one +// function that mints it from outside the collect stage. The witness +// test.claim.qualification_job_log_receipt_forged_probe_witness hands this source to the compiler and +// requires each refusal at its subject by name. dag/test/probe/ sits under the whole-tree +// strict-resolve exclusion, so this source reaches the compiler only through that witness. + +import std.content_hash { content_hash_atom } +import gunbc.runner.runner_host_unit_slot { HostUnitSlot } +import gunbc.runner.runner_qualification_dispatch { + CollectedQualificationRun, QualificationJobLogReceipt, QualificationLogReadings, + JobLogInstrumentStanding, AttemptJobLog, job_log_standing, +} + +// (a) AN OUTSIDE-AUTHORED RECEIPT. Refuses at QualificationJobLogReceipt. +fn forged_receipt(c: CollectedQualificationRun, h: HostUnitSlot, rd: QualificationLogReadings) -> QualificationJobLogReceipt { + QualificationJobLogReceipt { collected: c, slot: h, log_code_points: 1, log_digest: content_hash_atom(value: "invented"), readings: rd } +} + +// (b) THE MINTER CALLED FROM OUTSIDE THE COLLECT STAGE, over a log this caller supplies. Refuses at +// job_log_standing. +fn sneaked_receipt(c: CollectedQualificationRun, h: HostUnitSlot, l: AttemptJobLog) -> JobLogInstrumentStanding { + job_log_standing(collected: c, slot: h, log: l) +} From ecb67204d9d457921eb941b4505a7c40bf168c9e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 09:39:51 +0000 Subject: [PATCH 22/44] leg 3 witness per DESIGN section 3: supply values at each interface (pure scope checks, run expectation, hold), ONE real-path claim through the mint The scope checks and the run decision take values: qualification_slot_refusal, qualification_floor_admission, QualificationRunExpectation, hold_covers_slot_host. The subject mint composes them; collect derives the expectation from the sealed subject. Only the_real_mint_admits_the_attempt_and_refuses_todays_workflow runs dispatch_jit_mint and the subject mint (admit lists trimmed to it). Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 11 +- .../runner/runner_qualification_dispatch.dag | 103 ++-- ...er_qualification_dispatch_witness_test.dag | 457 +++++++----------- 3 files changed, 251 insertions(+), 320 deletions(-) diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index c29c82cbde7..1db20796fdd 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -138,16 +138,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "attempt_dispatch"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "slot_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_another_workflow_file_cannot_become_a_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_floor_runs_on_other_than_exactly_the_attempt_input_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_refuses_until_the_qualification_mode_lands"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_dispatch_has_three_standings_and_an_interlock"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_unpinned_or_unreadable_run_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_and_refuses_todays_workflow"), ] { match plan_jit_mint( binding: binding, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index ae3ecbab0ae..43f9a12a65f 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -183,6 +183,41 @@ fn job_display_name(job: Job) -> NonEmptyStr { } } +// THE SCOPE CHECKS ARE PURE, SO A CLAIM SUPPLIES THEIR INPUTS AS VALUES (DESIGN section 3: a witness +// discriminates at one interface). The subject mint below composes them and is their only +// production consumer; the witness exercises each one directly, and keeps ONE claim that runs the +// real mint end to end. + +// The slot must be registered for the qualification workflow at this branch. +fn qualification_slot_refusal(slot_workflow: NonEmptyStr, branch: NonEmptyStr) -> QualificationSubjectRefusal? { + if (slot_workflow as String) == (qualification_selected_workflow(branch: branch) as String) { + none + } else { + Present { value: SubjectSlotForAnotherWorkflow { slot_workflow: slot_workflow, qualification_workflow: qualification_selected_workflow(branch: branch) } } + } +} + +type QualificationFloorAdmission + = QualificationFloorAdmitted { floor_job_name: NonEmptyStr } + | QualificationFloorRefused { refusal: QualificationSubjectRefusal } + +// The workflow must declare the attempt input, and its floor job's runs-on must be exactly it. +fn qualification_floor_admission(workflow: Workflow, floor_job_id: NonEmptyStr) -> QualificationFloorAdmission { + if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_attempt_dispatch_input) { + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } + } else { + match first(filter(workflow.jobs, j => j.id == (floor_job_id as String))) { + Absent => QualificationFloorRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } + Present { value: job } => + if runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { + QualificationFloorAdmitted { floor_job_name: job_display_name(job: job) } + } else { + QualificationFloorRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: floor_job_id, runs_on: runner_spec_text(runner: job.runner) } } + } + } + } +} + // THE PURE JOIN, OVER A SUPPLIED WORKFLOW MODEL. Production reaches it only through // qualification_dispatch_subject, which supplies fleet_converge_workflow; the witness claims supply a // model carrying the floor job the boot leg's mode will add. An admit list constrains the call edge, @@ -197,18 +232,7 @@ fn qualification_dispatch_subject_over( ) -> QualificationSubjectStanding admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_subject_binds_the_minted_runner_and_the_attempt_input"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_another_workflow_file_cannot_become_a_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_floor_runs_on_other_than_exactly_the_attempt_input_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_refuses_until_the_qualification_mode_lands"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_collection_joins_the_floor_job_to_the_minted_runner"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_rerun_or_another_run_cannot_be_collected_as_the_dispatch"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "an_unfinished_unpinned_or_unreadable_run_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_dispatch_has_three_standings_and_an_interlock"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "slot_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "runs_on_refuses"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "undeclared_input_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_and_refuses_todays_workflow"), ] { match jit_registration_of(dispatch: JitMintDispatchAuthorized { dispatch: dispatch }) { @@ -222,19 +246,12 @@ fn qualification_dispatch_subject_over( match registration.slot { MicroVmJitSlot { attempt: _ } => QualificationSubjectRefused { refusal: SubjectSlotNotHostUnit } HostUnitJitSlot { slot: h } => - if (h.workflow as String) != (qualification_selected_workflow(branch: branch) as String) { - QualificationSubjectRefused { - refusal: SubjectSlotForAnotherWorkflow { slot_workflow: h.workflow, qualification_workflow: qualification_selected_workflow(branch: branch) }, - } - } else if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_attempt_dispatch_input) { - QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } - } else { - match first(filter(workflow.jobs, j => j.id == (floor_job_id as String))) { - Absent => QualificationSubjectRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } - Present { value: job } => - if !runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { - QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: floor_job_id, runs_on: runner_spec_text(runner: job.runner) } } - } else { + match qualification_slot_refusal(slot_workflow: h.workflow, branch: branch) { + Present { value: r } => QualificationSubjectRefused { refusal: r } + Absent => + match qualification_floor_admission(workflow: workflow, floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: r } => QualificationSubjectRefused { refusal: r } + QualificationFloorAdmitted { floor_job_name: n } => QualificationSubjectAdmitted { subject: QualificationDispatchSubject { registration: registration, @@ -242,11 +259,10 @@ fn qualification_dispatch_subject_over( attempt: h.attempt, branch: branch, revision: revision, - floor_job_name: job_display_name(job: job), + floor_job_name: n, }, } - } - } + } } } } @@ -329,8 +345,8 @@ fn qualification_dispatch_standing(performance: RestExchangePerformance) -> Qual } // THE INTERLOCK, as a pure comparison: the proof's admitted host against the slot's host. -fn hold_covers_subject(held: HostIdentity, subject: QualificationDispatchSubject) -> Bool { - (held as String) == (subject.host as String) +fn hold_covers_slot_host(held: HostIdentity, slot_host: HostIdentity) -> Bool { + (held as String) == (slot_host as String) } fn dispatch_qualification_floor( @@ -341,7 +357,7 @@ fn dispatch_qualification_floor( control_plane: GitHubAppControlPlaneState, ) -> QualificationDispatch uses net: Network { let effect = qualification_dispatch_effect(subject: subject) - if !hold_covers_subject(held: hold.subject.host.host, subject: subject) { + if !hold_covers_slot_host(held: hold.subject.host.host, slot_host: subject.host) { QualificationDispatchRefused { subject: subject, refusal: DispatchHoldForAnotherHost { held: hold.subject.host.host, slot: subject.host } } } else if token.app != subject.registration.app || token.installation != subject.registration.installation { QualificationDispatchRefused { subject: subject, refusal: DispatchTokenForAnotherInstallation } @@ -450,8 +466,25 @@ type QualificationRunVerdict = RunVerdictCollected { run: WorkflowRun, floor_job: WorkflowJobRun, conclusion: WorkflowRunConclusion } | RunVerdictRefused { refusal: QualificationRunRefusal } -fn decide_floor_job(subject: QualificationDispatchSubject, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { - let registered = subject.registration.name.value +// WHAT THE READBACK IS JUDGED AGAINST, as a plain value: the runner name the registration minted, +// the floor job the subject fixed, and the pinned revision. collect_qualification_run derives it from +// the sealed subject (qualification_run_expectation); a claim supplies it directly. +type QualificationRunExpectation { + registered_runner: NonEmptyStr + floor_job_name: NonEmptyStr + revision: CommitSha +} + +fn qualification_run_expectation(subject: QualificationDispatchSubject) -> QualificationRunExpectation { + QualificationRunExpectation { + registered_runner: subject.registration.name.value, + floor_job_name: subject.floor_job_name, + revision: subject.revision, + } +} + +fn decide_floor_job(subject: QualificationRunExpectation, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { + let registered = subject.registered_runner let on_runner = jobs |> filter(j => job_ran_on(job: j, runner_name: registered)) match first(on_runner) { Absent => RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: registered, runner_names_seen: jobs |> map(j => job_runner_name_seen(job: j)) } } @@ -469,7 +502,7 @@ fn decide_floor_job(subject: QualificationDispatchSubject, run: WorkflowRun, con } fn decide_qualification_run( - subject: QualificationDispatchSubject, + subject: QualificationRunExpectation, run_id: Int, run_performance: RestExchangePerformance, run: WorkflowRun, @@ -519,7 +552,7 @@ fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: Cont let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: dispatched_run_attempt) match decide_qualification_run( - subject: dispatched.subject, + subject: qualification_run_expectation(subject: dispatched.subject), run_id: run_id, run_performance: read.performance, run: read.run, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 5ecbe4bc559..d3d2c6c7145 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -12,25 +12,28 @@ import extdeps.github.effect { DispatchWorkflow } import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, } -import gunbc.managed_host { ManagedHost, managed_hosts, managed_host_binding } +import gunbc.managed_host { managed_hosts, managed_host_binding } import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } -import gunbc.runner_jit_mint { JitSlotBinding, HostUnitSlotBinding } +import gunbc.runner_jit_mint { HostUnitSlotBinding } import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } -import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitMintDispatchRefused, JitCredentialDelivery, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit } import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } import gunbc.runner.runner_qualification_dispatch { qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_selected_workflow, qualification_workflow_path, qualification_dispatch_effect, - QualificationDispatchSubject, QualificationSubjectAdmitted, QualificationSubjectRefused, - SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorRunsOnNotTheAttemptInput, + QualificationSubjectAdmitted, QualificationSubjectRefused, + SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, qualification_dispatch_subject, qualification_dispatch_subject_over, + qualification_slot_refusal, qualification_floor_admission, QualificationFloorAdmitted, QualificationFloorRefused, + qualification_run_expectation, QualificationRunExpectation, qualification_dispatch_standing, QualificationDispatchStanding, DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, - hold_covers_subject, + hold_covers_slot_host, decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunNotCompleted, - RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, RunNeverReachedAttemptRunner, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, + RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, + RunNeverReachedAttemptRunner, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -39,28 +42,15 @@ data attempt: NonEmptyStr = "q-1" data branch: NonEmptyStr = "main" data revision: CommitSha = "1111111111111111111111111111111111111111" data floor_job_id: NonEmptyStr = "qualification-floor" +data registered_runner: NonEmptyStr = "gunbc-qualification-fixture-q-1.service" -// ── THE SUBJECT, THROUGH THE REAL MINT ────────────────────────────────────────────────────────── -// The registration and the delivered credential come from the route's own JIT mint over the real -// managed-host roster: each claim calls dispatch_jit_mint itself (admitted by name), and the subject mint takes its sealed AuthorizedJitMintDispatch with receive_jit_mint over a 201 -// decoded by upstream's own outcome fold (runner id 4242). Nothing below constructs a registration, -// and every caller of the sealed subject mint is a claim returning Bool (no constructor proxy). - - - - - -fn slot_binding(h: ManagedHost, slot_workflow: NonEmptyStr) -> JitSlotBinding { - HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: slot_workflow) } -} - -fn delivered(d: AuthorizedJitMintDispatch) -> JitCredentialDelivery { - receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome) -} - -fn qualification_slot() -> NonEmptyStr { - qualification_selected_workflow(branch: branch) -} +// DESIGN section 3, A WITNESS DISCRIMINATES AT ONE INTERFACE. Every claim but one supplies its +// inputs as values at the interface it judges: the pure scope checks (qualification_slot_refusal, +// qualification_floor_admission), the dispatch standing, the hold comparison, and the run decision +// over a supplied QualificationRunExpectation. The pairing obligation is discharged by ONE claim that +// runs the real path: the route's JIT mint over the real managed-host roster, the sealed subject +// mint over a supplied floor workflow, the expectation derived from that subject, and the production +// mint over today's generated fleet-converge model, which refuses. fn floor_job(runs_on: RunnerSpec) -> Job { Job { @@ -94,126 +84,109 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { } } -// POSITIVE CONTROL AND THE ROUTE: the subject carries the slot's own attempt and host, the runner id -// GitHub returned for that registration, and the floor job's name; the dispatch sends exactly the -// declared input carrying that attempt, which is a label the slot registered; the effect names the -// generated fleet-converge workflow file in gunbc_repository at the pinned revision. -test fn the_subject_binds_the_minted_runner_and_the_attempt_input() -> Bool { +// ── THE ONE REAL-PATH CLAIM ───────────────────────────────────────────────────────────────────── +// The route's own JIT mint over the first real managed host, delivered through upstream's 201 fold; +// the sealed subject over a floor workflow carries the slot's attempt and host, the registration's +// minted name (ephemeral_slot_unit), and the floor job's name; the dispatch sends exactly the declared +// input with a label the slot registered; the effect names fleet-converge.yml in gunbc_repository at +// the pinned revision; the expectation the collection judges against is derived from that subject. +// And the production mint, over today's fleet_converge_workflow (no qualification mode yet), refuses. +test fn the_real_mint_admits_the_attempt_and_refuses_todays_workflow() -> Bool { match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: s } => - s.attempt == attempt && (s.registration.name.value as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) - && (s.floor_job_name as String) == "qualification floor" - && (s.host as String) == (h.host as String) - && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 - && (match map_get(qualification_dispatch_inputs(attempt: s.attempt), qualification_attempt_dispatch_input.name) { - Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: h.host, attempt: attempt) |> any(l => l == v)) - Absent => false - }) - && (match qualification_dispatch_effect(subject: s) { - DispatchWorkflow { repository: r, workflow: w, subject: sha } => - r.full_name == "gunb-ai/gunbc" && (w.file_name as String) == "fleet-converge.yml" && (sha as String) == (revision as String) - _ => false - }) - QualificationSubjectRefused { refusal: _ } => false - } - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } -} - -fn slot_refuses(h: ManagedHost, slot_workflow: NonEmptyStr) -> Bool { - match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: slot_workflow), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: slot_workflow), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: other } => - match qualification_dispatch_subject_over( - dispatch: other, delivery: delivered(d: other), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + Present { value: h } => + match dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), ) { - QualificationSubjectRefused { refusal: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) - _ => false + JitMintDispatchAuthorized { dispatch: d } => { + let delivered = receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome) + (match qualification_dispatch_subject_over( + dispatch: d, delivery: delivered, + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectAdmitted { subject: s } => { + let expected = qualification_run_expectation(subject: s) + s.attempt == attempt + && (s.host as String) == (h.host as String) + && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) + && (expected.floor_job_name as String) == "qualification floor" + && (expected.revision as String) == (revision as String) + && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt), qualification_attempt_dispatch_input.name) { + Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: h.host, attempt: attempt) |> any(l => l == v)) + Absent => false + }) + && (match qualification_dispatch_effect(subject: s) { + DispatchWorkflow { repository: r, workflow: w, subject: sha } => + r.full_name == "gunb-ai/gunbc" && (w.file_name as String) == "fleet-converge.yml" && (sha as String) == (revision as String) + _ => false + }) + } + QualificationSubjectRefused { refusal: _ } => false + }) + && (match qualification_dispatch_subject(dispatch: d, delivery: delivered, floor_job_id: floor_job_id, branch: branch, revision: revision) { + QualificationSubjectAdmitted { subject: _ } => false + QualificationSubjectRefused { refusal: _ } => true + }) + } + JitMintDispatchRefused { refusal: _ } => false } - JitMintDispatchRefused { refusal: _ } => false + Absent => false } } -fn runs_on_refuses(h: ManagedHost, d: AuthorizedJitMintDispatch, runs_on: RunnerSpec) -> Bool { - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true - _ => false - } -} +// ── THE RULING'S SCOPE, AT ITS PURE INTERFACES ────────────────────────────────────────────────── +// The subject mint above composes exactly these two checks; these claims supply their inputs. -fn undeclared_input_refuses(h: ManagedHost, d: AuthorizedJitMintDispatch) -> Bool { - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true +fn slot_refused_as_another_workflow(slot_workflow: NonEmptyStr) -> Bool { + match qualification_slot_refusal(slot_workflow: slot_workflow, branch: branch) { + Present { value: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) _ => false } } -// THE RULING'S SCOPE IS CONSTRUCTION, AND THESE ARE ITS REDS -- one claim per independent case, so -// each pays for its own mint and nothing else (DESIGN section 3: a conjunction of independent cases -// forced through one budget). Through the one mint, no other workflow and no other label can become -// a dispatch subject. - -// A slot registered for another workflow file refuses before the workflow model is read. -test fn a_slot_for_another_workflow_file_cannot_become_a_subject() -> Bool { - match first(managed_hosts()) { - Present { value: h } => slot_refuses(h: h, slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") - Absent => false - } +// RED: a slot for another workflow file, or for the qualification path on another branch, cannot +// become a subject; the qualification slot at this branch passes (the positive control). +test fn a_slot_for_another_workflow_or_branch_cannot_become_a_subject() -> Bool { + slot_refused_as_another_workflow(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") + && slot_refused_as_another_workflow(slot_workflow: qualification_selected_workflow(branch: "feature")) + && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: branch), branch: branch) { Absent => true Present { value: _ } => false }) } -// The qualification path on another branch is another selected workflow, and refuses the same way. -test fn a_slot_for_the_qualification_path_on_another_branch_cannot_become_a_subject() -> Bool { - match first(managed_hosts()) { - Present { value: h } => slot_refuses(h: h, slot_workflow: qualification_selected_workflow(branch: "feature")) - Absent => false +fn runs_on_refused(runs_on: RunnerSpec) -> Bool { + match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true + _ => false } } -// A floor job whose runs-on is a literal label, another input, the attempt input composed with -// anything else, or a bare string, refuses; so does a workflow that does not declare the input. +// RED: a floor runs-on that is a literal label, another input, the attempt input composed with +// anything, or a bare string refuses; so does an undeclared input or a missing floor job. Positive +// control: exactly the attempt input admits the floor job by its name. test fn a_floor_runs_on_other_than_exactly_the_attempt_input_refuses() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => runs_on_refuses(h: h, d: d, runs_on: SelfHosted { labels: [attempt as String] }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) - && runs_on_refuses(h: h, d: d, runs_on: RunsOnExpression { expression: "gunbc-qualification" }) - && undeclared_input_refuses(h: h, d: d) - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } -} - -// THE REAL PATH: the production mint, over today's generated fleet-converge model -- which has no -// qualification mode yet -- refuses rather than admitting anything. -test fn the_production_mint_refuses_until_the_qualification_mode_lands() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => match qualification_dispatch_subject(dispatch: d, delivery: delivered(d: d), floor_job_id: floor_job_id, branch: branch, revision: revision) { - QualificationSubjectAdmitted { subject: _ } => false - QualificationSubjectRefused { refusal: _ } => true - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } -} + runs_on_refused(runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true + _ => false + }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: "another-job") { + QualificationFloorRefused { refusal: SubjectFloorJobAbsent { job_id: _ } } => true + _ => false + }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id) { + QualificationFloorAdmitted { floor_job_name: n } => (n as String) == "qualification floor" + QualificationFloorRefused { refusal: _ } => false + }) +} + +// ── THE DISPATCH STANDING AND THE INTERLOCK ───────────────────────────────────────────────────── fn mutation_standing(rest: RestOutcome) -> QualificationDispatchStanding { qualification_dispatch_standing(performance: classify_rest_outcome(standing: RestMutationExchange { rest: rest })) @@ -223,34 +196,26 @@ fn ambiguous(s: QualificationDispatchStanding) -> Bool { match s { DispatchStandingCommitAmbiguous { detail: _ } => true _ => false } } -// THREE STANDINGS, FROM THE MUTATION CLASSIFIER: a decided 422 is refused; no response, a 503, and -// an undecodable 200 are commit-ambiguous; only a succeeded exchange dispatches. And the interlock: -// the hold covers the slot's own host and no other. +// THREE STANDINGS, FROM THE MUTATION CLASSIFIER: a decided 422 is refused; no response, a 503, and an +// undecodable 200 are commit-ambiguous; only a succeeded exchange dispatches. The interlock: a hold +// covers the slot's own host and no other. test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { (match mutation_standing(rest: RestStatusRefused { status: 422, body: "Unexpected inputs provided" }) { DispatchStandingRefused { status: st, body: _ } => st == 422 _ => false }) && ambiguous(s: mutation_standing(rest: RestTransportRefused { cause: "fixture: connection reset after send" })) && ambiguous(s: mutation_standing(rest: RestStatusRefused { status: 503, body: "fixture" })) && ambiguous(s: mutation_standing(rest: RestBodyUndecodable { status: 200, cause: "fixture: truncated body" })) && (match mutation_standing(rest: RestOk) { DispatchStandingSucceeded => true _ => false }) - && match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: s } => - hold_covers_subject(held: h.host, subject: s) && !hold_covers_subject(held: "fixture-other-host" as HostIdentity, subject: s) - QualificationSubjectRefused { refusal: _ } => false - } - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } + && hold_covers_slot_host(held: "mtcollins1" as HostIdentity, slot_host: "mtcollins1" as HostIdentity) + && !hold_covers_slot_host(held: "srv1" as HostIdentity, slot_host: "mtcollins1" as HostIdentity) } -// ── THE COLLECTION ───────────────────────────────────────────────────────────────────────────── +// ── THE COLLECTION, OVER A SUPPLIED EXPECTATION ───────────────────────────────────────────────── + +data expect: QualificationRunExpectation = QualificationRunExpectation { + registered_runner: registered_runner, + floor_job_name: "qualification floor", + revision: revision, +} fn run(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool) -> WorkflowRun { WorkflowRun { @@ -298,27 +263,16 @@ fn floor_on(runner: String) -> WorkflowJobRun { job(id: 1, name: "qualification floor", runner: runner, started: true, attempt_n: 1) } -fn registered(s: QualificationDispatchSubject) -> String { - s.registration.name.value as String -} - fn jobs(js: List) -> WorkflowJobRunList { WorkflowJobRunList { total_count: count(js), jobs: js } } -fn decide_jobs(s: QualificationDispatchSubject, js: WorkflowJobRunList) -> QualificationRunVerdict { - decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: js) -} - -fn decide_run(s: QualificationDispatchSubject, r: WorkflowRun, js: WorkflowJobRunList) -> QualificationRunVerdict { - decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) +fn decide_run(r: WorkflowRun, js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_qualification_run(subject: expect, run_id: 77, run_performance: RestExchangeSucceeded, run: r, jobs_performance: RestExchangeSucceeded, jobs: js) } -fn never_reached(v: QualificationRunVerdict) -> Bool { - match v { - RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: _, runner_names_seen: seen } } => count(seen) > 0 - _ => false - } +fn decide_jobs(js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_run(r: dispatched_run(attempt_n: 1), js: js) } fn mismatch(v: QualificationRunVerdict) -> Bool { @@ -328,122 +282,75 @@ fn mismatch(v: QualificationRunVerdict) -> Bool { } } -// THE RUNNER IS JOINED BY IDENTITY. Positive control: the named floor job on the minted runner id is -// collected with the run's own conclusion (a Failure is still this host's run). REDs: the same -// labels on another runner; a helper job on the minted runner while the floor ran elsewhere; a floor -// on the minted runner that never started; two jobs answering to the floor's name. -test fn the_collection_joins_the_floor_job_to_the_minted_runner() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: s } => { - (match decide_jobs(s: s, js: jobs(js: [floor_on(runner: registered(s: s))])) { - RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) - _ => false - }) - && never_reached(v: decide_jobs(s: s, js: jobs(js: [floor_on(runner: "another-runner-with-the-same-labels")]))) - && (match decide_jobs(s: s, js: jobs(js: [ - job(id: 2, name: "helper", runner: registered(s: s), started: true, attempt_n: 1), - floor_on(runner: "another-runner-with-the-same-labels"), - ])) { - RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: _, job_name: n } } => n == "helper" - _ => false - }) - && (match decide_jobs(s: s, js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered(s: s), started: false, attempt_n: 1)])) { - RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: id } } => id == 1 - _ => false - }) - && (match decide_jobs(s: s, js: jobs(js: [floor_on(runner: registered(s: s)), job(id: 2, name: "qualification floor", runner: registered(s: s), started: true, attempt_n: 1)])) { - RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 - _ => false - }) - } - QualificationSubjectRefused { refusal: _ } => false - } - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } +// THE RUNNER IS JOINED BY IDENTITY. Positive control: the named floor job on the registered runner is +// collected with the run's own conclusion (a Failure is still this host's run). REDs: the same labels +// on another runner; a helper job on the registered runner while the floor ran elsewhere; a floor on +// the registered runner that never started; two jobs on the registered runner. +test fn the_collection_joins_the_floor_job_to_the_registered_runner() -> Bool { + let mine = registered_runner as String + (match decide_jobs(js: jobs(js: [floor_on(runner: mine)])) { + RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) + _ => false + }) + && (match decide_jobs(js: jobs(js: [floor_on(runner: "another-runner-with-the-same-labels")])) { + RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: _, runner_names_seen: seen } } => count(seen) == 1 + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 2, name: "helper", runner: mine, started: true, attempt_n: 1), floor_on(runner: "another-runner-with-the-same-labels")])) { + RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: _, job_name: n } } => n == "helper" + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner: mine, started: false, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: id } } => id == 1 + _ => false + }) + && (match decide_jobs(js: jobs(js: [floor_on(runner: mine), job(id: 2, name: "qualification floor", runner: mine, started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 + _ => false + }) } // THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT. REDs: the run re-run to attempt 2 (whose floor -// job carries the same labels and runner); a job of attempt 2 in the listing; another run id; -// another event; another workflow path. +// job carries the same labels and runner); a job of attempt 2 in the listing; another run id; another +// event; another workflow path. test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { + let floor = jobs(js: [floor_on(runner: registered_runner as String)]) let path = qualification_workflow_path() as String - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: s } => { - let floor = jobs(js: [floor_on(runner: registered(s: s))]) - (match decide_run(s: s, r: dispatched_run(attempt_n: 2), js: floor) { - RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 - _ => false - }) - && mismatch(v: decide_run(s: s, r: dispatched_run(attempt_n: 1), js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered(s: s), started: true, attempt_n: 2)]))) - && mismatch(v: decide_run(s: s, r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) - && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) - && mismatch(v: decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) - } - QualificationSubjectRefused { refusal: _ } => false - } - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } + (match decide_run(r: dispatched_run(attempt_n: 2), js: floor) { + RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 + _ => false + }) + && mismatch(v: decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered_runner as String, started: true, attempt_n: 2)]))) + && mismatch(v: decide_run(r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) } test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { - match first(managed_hosts()) { - Present { value: h } => match dispatch_jit_mint(binding: slot_binding(h: h, slot_workflow: qualification_slot()), authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_restricted_to(workflow: qualification_slot()), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: d } => { + let floor = jobs(js: [floor_on(runner: registered_runner as String)]) let path = qualification_workflow_path() as String - match qualification_dispatch_subject_over( - dispatch: d, delivery: delivered(d: d), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, - ) { - QualificationSubjectAdmitted { subject: s } => { - let floor = jobs(js: [floor_on(runner: registered(s: s))]) - (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { - RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true - _ => false - }) - && (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), js: floor) { - RunVerdictRefused { refusal: RunCompletedWithoutConclusion } => true - _ => false - }) - && (match decide_run(s: s, r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), js: floor) { - RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: _, observed: _ } } => true - _ => false - }) - && (match decide_run(s: s, r: dispatched_run(attempt_n: 1), js: WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { - RunVerdictRefused { refusal: RunJobsTruncated { returned: rt, total: t } } => rt == 1 && t == 2 - _ => false - }) - && (match decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeUnreached { cause: "fixture" }, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: floor) { - RunVerdictRefused { refusal: RunReadUnreadable { performance: _ } } => true - _ => false - }) - && (match decide_qualification_run(subject: s, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeStatusRefused { status: 404, body: "fixture" }, jobs: floor) { - RunVerdictRefused { refusal: RunJobsUnreadable { performance: _ } } => true - _ => false - }) - } - QualificationSubjectRefused { refusal: _ } => false - } - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } + (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { + RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true + _ => false + }) + && (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), js: floor) { + RunVerdictRefused { refusal: RunCompletedWithoutConclusion } => true + _ => false + }) + && (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), js: floor) { + RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: _, observed: _ } } => true + _ => false + }) + && (match decide_run(r: dispatched_run(attempt_n: 1), js: WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { + RunVerdictRefused { refusal: RunJobsTruncated { returned: rt, total: t } } => rt == 1 && t == 2 + _ => false + }) + && (match decide_qualification_run(subject: expect, run_id: 77, run_performance: RestExchangeUnreached { cause: "fixture" }, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeSucceeded, jobs: floor) { + RunVerdictRefused { refusal: RunReadUnreadable { performance: _ } } => true + _ => false + }) + && (match decide_qualification_run(subject: expect, run_id: 77, run_performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1), jobs_performance: RestExchangeStatusRefused { status: 404, body: "fixture" }, jobs: floor) { + RunVerdictRefused { refusal: RunJobsUnreadable { performance: _ } } => true + _ => false + }) } From 67bb2403069b58568559c69840a8289e7e59c005 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 10:26:12 +0000 Subject: [PATCH 23/44] leg 3: keep the real-path claim to the mint and the sealed subject; drop the fleet_converge_workflow evaluation (~600k eval steps, a fact about generated data) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 2 +- ...unner_qualification_dispatch_witness_test.dag | 16 +++++++--------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 1db20796fdd..fcf074193dd 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -138,7 +138,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "gunbc.runner_microvm_slot_controller", decl_name: "attempt_dispatch"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_and_refuses_todays_workflow"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), ] { match plan_jit_mint( binding: binding, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 43f9a12a65f..1b169159f7a 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -232,7 +232,7 @@ fn qualification_dispatch_subject_over( ) -> QualificationSubjectStanding admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), - decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_and_refuses_todays_workflow"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), ] { match jit_registration_of(dispatch: JitMintDispatchAuthorized { dispatch: dispatch }) { diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index d3d2c6c7145..36c3da9acc6 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -24,7 +24,7 @@ import gunbc.runner.runner_qualification_dispatch { qualification_workflow_path, qualification_dispatch_effect, QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, - qualification_dispatch_subject, qualification_dispatch_subject_over, + qualification_dispatch_subject_over, qualification_slot_refusal, qualification_floor_admission, QualificationFloorAdmitted, QualificationFloorRefused, qualification_run_expectation, QualificationRunExpectation, qualification_dispatch_standing, QualificationDispatchStanding, @@ -49,8 +49,7 @@ data registered_runner: NonEmptyStr = "gunbc-qualification-fixture-q-1.service" // qualification_floor_admission), the dispatch standing, the hold comparison, and the run decision // over a supplied QualificationRunExpectation. The pairing obligation is discharged by ONE claim that // runs the real path: the route's JIT mint over the real managed-host roster, the sealed subject -// mint over a supplied floor workflow, the expectation derived from that subject, and the production -// mint over today's generated fleet-converge model, which refuses. +// mint over a supplied floor workflow, and the expectation derived from that subject. fn floor_job(runs_on: RunnerSpec) -> Job { Job { @@ -90,8 +89,11 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { // minted name (ephemeral_slot_unit), and the floor job's name; the dispatch sends exactly the declared // input with a label the slot registered; the effect names fleet-converge.yml in gunbc_repository at // the pinned revision; the expectation the collection judges against is derived from that subject. -// And the production mint, over today's fleet_converge_workflow (no qualification mode yet), refuses. -test fn the_real_mint_admits_the_attempt_and_refuses_todays_workflow() -> Bool { +// The production wrapper qualification_dispatch_subject only supplies fleet_converge_workflow to +// this same mint; evaluating that whole generated model costs ~600k eval steps, so whether today's +// model refuses is not asserted here -- the floor admission it would reach is exercised above at +// its own interface, and the wrapper's consumer is the boot leg's route fold (wiring_owed). +test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { match first(managed_hosts()) { Present { value: h } => match dispatch_jit_mint( @@ -128,10 +130,6 @@ test fn the_real_mint_admits_the_attempt_and_refuses_todays_workflow() -> Bool { } QualificationSubjectRefused { refusal: _ } => false }) - && (match qualification_dispatch_subject(dispatch: d, delivery: delivered, floor_job_id: floor_job_id, branch: branch, revision: revision) { - QualificationSubjectAdmitted { subject: _ } => false - QualificationSubjectRefused { refusal: _ } => true - }) } JitMintDispatchRefused { refusal: _ } => false } From 29003c496bb8a860f0a96d3c58a3954405b724cb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 10:52:28 +0000 Subject: [PATCH 24/44] leg 3: the production mint over the real fleet_converge_workflow is its own claim in the long tier, with its measured cost declared (~600k eval steps building the generated model) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 1 + ...patch_production_workflow_witness_test.dag | 68 +++++++++++++++++++ ...er_qualification_dispatch_witness_test.dag | 7 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index fcf074193dd..7b2cfbe1e26 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -139,6 +139,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.long.runner_qualification_dispatch_production_workflow_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), ] { match plan_jit_mint( binding: binding, diff --git a/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag b/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag new file mode 100644 index 00000000000..422351a8ad1 --- /dev/null +++ b/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag @@ -0,0 +1,68 @@ +module test.claim.long.runner_qualification_dispatch_production_workflow_witness + +import std.types { Bool, CommitSha, NonEmptyStr } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.managed_host { managed_hosts, managed_host_binding } +import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } +import gunbc.runner_jit_mint { HostUnitSlotBinding } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } +import gunbc.runner.runner_qualification_dispatch { + qualification_selected_workflow, qualification_dispatch_subject, + QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectAttemptInputUndeclared, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE PRODUCTION WRAPPER OVER THE REAL GENERATED WORKFLOW, AND WHY IT IS IN THE LONG TIER. +// +// gunbc.runner.runner_qualification_dispatch qualification_dispatch_subject is the production mint: +// it supplies gunbc.fleet_converge_workflow fleet_converge_workflow to the sealed subject mint. This +// claim runs it end to end -- the route's JIT mint over the first real managed host, delivered +// through upstream's 201 fold -- and asserts the ROUTE of today's refusal: the generated +// fleet-converge model declares no runner_attempt_label input, so the subject refuses at the input +// check (SubjectAttemptInputUndeclared), before any floor job is read. When the boot leg lands the +// qualification mode there, this claim turns red and is replaced by the admitted case. +// +// THE COST IS THE WORKFLOW VALUE, NOT THE MINT. Measured by the required floor (claim eval steps): +// with this refusal folded into the module's real-path claim, that claim cost 602,837 eval steps +// (run 37193378129); the earlier claim carrying three JIT mints, five subject-mint calls and the same +// refusal cost 604,048 (run 37188293613). The 1,211-step difference is two mints and four subject +// calls, so ~600k is building fleet_converge_workflow, once per claim -- inherent to reading the real +// generated model, not a repeated derivation. It exceeds the 72,300-step new-witness budget by +// construction, so it is homed here under DESIGN section 3's pairing obligation rather than shrunk; +// the module's own witness (test.claim.runner_qualification_dispatch_witness) keeps the real mint and +// sealed subject path in budget and exercises the floor admission at its interface. + +data attempt: NonEmptyStr = "q-1" +data branch: NonEmptyStr = "main" +data revision: CommitSha = "1111111111111111111111111111111111111111" + +test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input() -> Bool { + match first(managed_hosts()) { + Present { value: h } => + match dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => + match qualification_dispatch_subject( + dispatch: d, + delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome), + floor_job_id: "qualification-floor", + branch: branch, + revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "runner_attempt_label" + _ => false + } + JitMintDispatchRefused { refusal: _ } => false + } + Absent => false + } +} diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 36c3da9acc6..135276eb8d0 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -89,10 +89,9 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { // minted name (ephemeral_slot_unit), and the floor job's name; the dispatch sends exactly the declared // input with a label the slot registered; the effect names fleet-converge.yml in gunbc_repository at // the pinned revision; the expectation the collection judges against is derived from that subject. -// The production wrapper qualification_dispatch_subject only supplies fleet_converge_workflow to -// this same mint; evaluating that whole generated model costs ~600k eval steps, so whether today's -// model refuses is not asserted here -- the floor admission it would reach is exercised above at -// its own interface, and the wrapper's consumer is the boot leg's route fold (wiring_owed). +// The production wrapper qualification_dispatch_subject, over the real fleet_converge_workflow, is +// its own claim in the long tier (test.claim.long.runner_qualification_dispatch_production_workflow_witness): +// building that generated model costs ~600k eval steps per claim, which is the measured reason. test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { match first(managed_hosts()) { Present { value: h } => From cf291596a5b95356f0df10a05cbeed3aae24188d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 12:26:18 +0000 Subject: [PATCH 25/44] leg 3: production mint reads the declared inputs before building fleet_converge_workflow (a shape fix: the ~600k-step jobs were built and never read); its refusal claim returns to the module witness Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_jit_perform.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 42 ++++++++---- ...patch_production_workflow_witness_test.dag | 68 ------------------- ...er_qualification_dispatch_witness_test.dag | 37 ++++++++-- 4 files changed, 63 insertions(+), 86 deletions(-) delete mode 100644 dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 7b2cfbe1e26..4350a48541d 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -139,7 +139,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), - decl_ref(module_path: "test.claim.long.runner_qualification_dispatch_production_workflow_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), ] { match plan_jit_mint( binding: binding, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 1b169159f7a..0639a4d57ec 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -25,7 +25,7 @@ import extdeps.github.workflow_runs { } import gunbc.repository { gunbc_repository } import gunbc.generated_artifact { artifact_path, FleetConvergeYamlArtifact } -import gunbc.fleet_converge_workflow { fleet_converge_workflow } +import gunbc.fleet_converge_workflow { fleet_converge_workflow, fleet_converge_dispatch_inputs } import gunbc.auth.github_apps { GitHubAppControlPlaneState } import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, @@ -125,10 +125,14 @@ type QualificationSubjectStanding = QualificationSubjectAdmitted { subject: QualificationDispatchSubject } | QualificationSubjectRefused { refusal: QualificationSubjectRefusal } +fn dispatch_inputs_declare(inputs: List, input: DispatchInput) -> Bool { + inputs |> any(i => i.name == input.name) +} + fn workflow_declares_dispatch_input(workflow: Workflow, input: DispatchInput) -> Bool { workflow.on |> any(t => match t { - WorkflowDispatch { inputs: ins } => ins |> any(i => i.name == input.name) + WorkflowDispatch { inputs: ins } => dispatch_inputs_declare(inputs: ins, input: input) Push { branches: _, paths: _ } => false PullRequest { branches: _, types: _ } => false Schedule { cron: _ } => false @@ -271,9 +275,17 @@ fn qualification_dispatch_subject_over( } } -// THE PRODUCTION MINT: the qualification workflow is the generated fleet-converge model. Until the -// boot leg lands the route's mode there, it has no floor job reading the attempt input and this -// refuses -- the wiring the route leg owes, refused rather than faked. +// THE PRODUCTION MINT: the qualification workflow is the generated fleet-converge model. +// +// THE DECLARED INPUTS ARE READ BEFORE THE WORKFLOW IS BUILT. The model's workflow_dispatch inputs are +// their own declaration (gunbc.fleet_converge_workflow fleet_converge_dispatch_inputs), and the jobs +// are built by fleet_converge_jobs(); evaluating the whole fleet_converge_workflow builds every job of +// every mode, ~600k eval steps measured by the required floor, and the first check the mint makes +// reads only the inputs. So the wrapper asks that check of the inputs declaration directly and builds +// the workflow only when the inputs declare the attempt input -- the refusal is the same +// SubjectAttemptInputUndeclared the mint would reach, without the construction it would never read. +// Until the boot leg lands the qualification mode there, this refuses -- the wiring the route leg +// owes, refused rather than faked. fn qualification_dispatch_subject( dispatch: AuthorizedJitMintDispatch, delivery: JitCredentialDelivery, @@ -281,14 +293,18 @@ fn qualification_dispatch_subject( branch: NonEmptyStr, revision: CommitSha, ) -> QualificationSubjectStanding { - qualification_dispatch_subject_over( - dispatch: dispatch, - delivery: delivery, - workflow: fleet_converge_workflow, - floor_job_id: floor_job_id, - branch: branch, - revision: revision, - ) + if !dispatch_inputs_declare(inputs: fleet_converge_dispatch_inputs, input: qualification_attempt_dispatch_input) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } + } else { + qualification_dispatch_subject_over( + dispatch: dispatch, + delivery: delivery, + workflow: fleet_converge_workflow, + floor_job_id: floor_job_id, + branch: branch, + revision: revision, + ) + } } fn qualification_dispatch_effect(subject: QualificationDispatchSubject) -> GitHubEffect { diff --git a/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag b/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag deleted file mode 100644 index 422351a8ad1..00000000000 --- a/dag/test/claim/long/runner_qualification_dispatch_production_workflow_witness_test.dag +++ /dev/null @@ -1,68 +0,0 @@ -module test.claim.long.runner_qualification_dispatch_production_workflow_witness - -import std.types { Bool, CommitSha, NonEmptyStr } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import gunbc.managed_host { managed_hosts, managed_host_binding } -import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } -import gunbc.runner_jit_mint { HostUnitSlotBinding } -import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } -import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } -import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } -import gunbc.runner.runner_qualification_dispatch { - qualification_selected_workflow, qualification_dispatch_subject, - QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectAttemptInputUndeclared, -} - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -// THE PRODUCTION WRAPPER OVER THE REAL GENERATED WORKFLOW, AND WHY IT IS IN THE LONG TIER. -// -// gunbc.runner.runner_qualification_dispatch qualification_dispatch_subject is the production mint: -// it supplies gunbc.fleet_converge_workflow fleet_converge_workflow to the sealed subject mint. This -// claim runs it end to end -- the route's JIT mint over the first real managed host, delivered -// through upstream's 201 fold -- and asserts the ROUTE of today's refusal: the generated -// fleet-converge model declares no runner_attempt_label input, so the subject refuses at the input -// check (SubjectAttemptInputUndeclared), before any floor job is read. When the boot leg lands the -// qualification mode there, this claim turns red and is replaced by the admitted case. -// -// THE COST IS THE WORKFLOW VALUE, NOT THE MINT. Measured by the required floor (claim eval steps): -// with this refusal folded into the module's real-path claim, that claim cost 602,837 eval steps -// (run 37193378129); the earlier claim carrying three JIT mints, five subject-mint calls and the same -// refusal cost 604,048 (run 37188293613). The 1,211-step difference is two mints and four subject -// calls, so ~600k is building fleet_converge_workflow, once per claim -- inherent to reading the real -// generated model, not a repeated derivation. It exceeds the 72,300-step new-witness budget by -// construction, so it is homed here under DESIGN section 3's pairing obligation rather than shrunk; -// the module's own witness (test.claim.runner_qualification_dispatch_witness) keeps the real mint and -// sealed subject path in budget and exercises the floor admission at its interface. - -data attempt: NonEmptyStr = "q-1" -data branch: NonEmptyStr = "main" -data revision: CommitSha = "1111111111111111111111111111111111111111" - -test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input() -> Bool { - match first(managed_hosts()) { - Present { value: h } => - match dispatch_jit_mint( - binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, - authority: app_credential, - control_plane: observed_grant(perms: runner_grant), - organization: gunbai_org_ref, - runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), - attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), - ) { - JitMintDispatchAuthorized { dispatch: d } => - match qualification_dispatch_subject( - dispatch: d, - delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome), - floor_job_id: "qualification-floor", - branch: branch, - revision: revision, - ) { - QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "runner_attempt_label" - _ => false - } - JitMintDispatchRefused { refusal: _ } => false - } - Absent => false - } -} diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 135276eb8d0..e66966bfa52 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -24,7 +24,7 @@ import gunbc.runner.runner_qualification_dispatch { qualification_workflow_path, qualification_dispatch_effect, QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, - qualification_dispatch_subject_over, + qualification_dispatch_subject, qualification_dispatch_subject_over, qualification_slot_refusal, qualification_floor_admission, QualificationFloorAdmitted, QualificationFloorRefused, qualification_run_expectation, QualificationRunExpectation, qualification_dispatch_standing, QualificationDispatchStanding, @@ -89,9 +89,6 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { // minted name (ephemeral_slot_unit), and the floor job's name; the dispatch sends exactly the declared // input with a label the slot registered; the effect names fleet-converge.yml in gunbc_repository at // the pinned revision; the expectation the collection judges against is derived from that subject. -// The production wrapper qualification_dispatch_subject, over the real fleet_converge_workflow, is -// its own claim in the long tier (test.claim.long.runner_qualification_dispatch_production_workflow_witness): -// building that generated model costs ~600k eval steps per claim, which is the measured reason. test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { match first(managed_hosts()) { Present { value: h } => @@ -136,6 +133,38 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { } } +// THE PRODUCTION MINT OVER TODAY'S GENERATED WORKFLOW REFUSES, AND THE CLAIM ASSERTS THE ROUTE: the +// real JIT mint, then qualification_dispatch_subject, which reads the real fleet-converge inputs +// declaration first and refuses SubjectAttemptInputUndeclared naming runner_attempt_label. When the +// boot leg lands the qualification mode, this turns red and is replaced by the admitted case. +test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input() -> Bool { + match first(managed_hosts()) { + Present { value: h } => + match dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => + match qualification_dispatch_subject( + dispatch: d, + delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome), + floor_job_id: floor_job_id, + branch: branch, + revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "runner_attempt_label" + _ => false + } + JitMintDispatchRefused { refusal: _ } => false + } + Absent => false + } +} + // ── THE RULING'S SCOPE, AT ITS PURE INTERFACES ────────────────────────────────────────────────── // The subject mint above composes exactly these two checks; these claims supply their inputs. From c22c2b51c1aa4b8e8d5a8b0fcaf4b1917b0da975 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 13:59:51 +0000 Subject: [PATCH 26/44] leg 3 RC items 1-3: delivery carries its authorized dispatch; dispatch authority bound to the token's installation; runner_id join with name corroboration and a unique floor display name - JitCredentialBoundToAttempt gains dispatch: AuthorizedJitMintDispatch (set in receive_jit_mint; an edit to #13206's type made here per quiet-stag-623); the subject refuses a delivery from another dispatch (SubjectDeliveryForAnotherDispatch), RED with two authorized mints for one slot - dispatch_authority_refusal: the authority must project to the registration's App/installation; an ActionsJobCredential refuses (REDs for other App, other installation, job token) - WorkflowJobRun regains runner_id; the subject carries the delivered runner_id and collection joins on it, runner_name corroborating; the floor's display name must be unique at the subject mint Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/workflow_runs.dag | 12 +- dag/gunbc/public_workload_census.dag | 8 + dag/gunbc/runner/runner_attempt_launch.dag | 2 +- dag/gunbc/runner/runner_jit_perform.dag | 4 + .../runner/runner_microvm_slot_controller.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 110 +++++++++----- .../github_app_registry_witness_test.dag | 1 + .../github_effect_perform_witness_test.dag | 2 +- .../public_workload_census_witness_test.dag | 11 ++ .../runner_attempt_launch_witness_test.dag | 15 +- ...r_microvm_slot_controller_witness_test.dag | 8 +- ...er_qualification_dispatch_witness_test.dag | 137 +++++++++++++++--- ...ded_run_starvation_census_witness_test.dag | 6 + 13 files changed, 251 insertions(+), 67 deletions(-) diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index fb05284e41a..beb791f168a 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -194,11 +194,12 @@ fn workflow_job_attempt_filter_wire(f: WorkflowJobAttemptFilter) -> String { // rendered name. The parenthetical of combination VALUES in axis order is the rendering this // tree observed on those names; it is not a REST field. Standing is TranscribedUncited until // GitHub's Jobs docs state that encoding. -// runner_name IS WHICH RUNNER TOOK THE JOB, a third fact beside the declared runs-on and the -// observed labels: two runners can carry identical labels, and the runner's name is what a JIT -// registration fixes before GitHub answers (gunbc.runner.runner_qualification_dispatch job_ran_on -// joins on it). Upstream publishes it as nullable -- null until a runner takes the job -- and every -// record in this tree written before the field existed carries none rather than a guessed name. +// runner_id AND runner_name ARE WHICH RUNNER TOOK THE JOB, a third fact beside the declared runs-on +// and the observed labels: two runners can carry identical labels. runner_id is the exact identity +// GitHub returned when the JIT registration was minted (gunbc.runner.runner_qualification_dispatch +// job_ran_on joins on it); runner_name is corroboration, since one name can resolve to several ids. +// Upstream publishes both as nullable -- null until a runner takes the job -- and every record in +// this tree written before the fields existed carries none rather than a guessed value. type WorkflowJobRun { id: Int run_id: Int @@ -207,6 +208,7 @@ type WorkflowJobRun { status: WorkflowRunStatus conclusion: WorkflowRunConclusion? labels: List + runner_id: Int? runner_name: String? created_at: Timestamp? started_at: Timestamp? diff --git a/dag/gunbc/public_workload_census.dag b/dag/gunbc/public_workload_census.dag index 56db757bf0d..ec11c28ce47 100644 --- a/dag/gunbc/public_workload_census.dag +++ b/dag/gunbc/public_workload_census.dag @@ -1020,6 +1020,7 @@ data candidate_biome_test: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -1083,6 +1084,7 @@ data candidate_pdns_dnsdist_arm: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -1151,6 +1153,7 @@ data candidate_openobserve_arm_build: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:21Z" }, @@ -1221,6 +1224,7 @@ data candidate_openobserve_amd64: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-8"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:02:15Z" }, @@ -1275,6 +1279,7 @@ data candidate_openobserve_manifest: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubicloud-standard-16-arm"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:28:12Z" }, @@ -1329,6 +1334,7 @@ data candidate_biome_test_windows: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-windows-2022-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:31Z" }, @@ -1377,6 +1383,7 @@ data candidate_biome_test_macos: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-macos-latest"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:08:06Z" }, @@ -1423,6 +1430,7 @@ data candidate_openobserve_summary: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-latest"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-01T10:29:11Z" }, diff --git a/dag/gunbc/runner/runner_attempt_launch.dag b/dag/gunbc/runner/runner_attempt_launch.dag index 230408165af..b168ee16231 100644 --- a/dag/gunbc/runner/runner_attempt_launch.dag +++ b/dag/gunbc/runner/runner_attempt_launch.dag @@ -207,7 +207,7 @@ fn admit_jit_credential(attempt: MicroVmAttempt, mint: JitMintPerformance) -> Ji match mint { JitMintReceived { delivery: delivery } => match delivery { - JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id } => + JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id, dispatch: _ } => if !host_mint_admission_is_required() { JitCredentialRefused { refusal: JitMintAdmissionAxisNotRequired } } else if bound != MicroVmJitSlot { attempt: attempt } { diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 4350a48541d..7e097ad43e5 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -139,6 +139,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), ] { match plan_jit_mint( @@ -208,6 +209,7 @@ type JitCredentialDelivery slot: JitSlot credential: EncodedJitConfig runner_id: Int + dispatch: AuthorizedJitMintDispatch } | JitCredentialNotDispatched { refusal: JitDispatchRefusal } | JitCredentialMintNotCreated { outcome: JitConfigMintOutcome } @@ -226,6 +228,7 @@ fn receive_jit_mint( slot: slot, credential: config, runner_id: runner_id, + dispatch: authorized, } JitConfigMintRefused { status: s, reason: reason } => JitCredentialMintNotCreated { @@ -250,6 +253,7 @@ fn jit_credential_delivered(delivery: JitCredentialDelivery) -> Bool { slot: _, credential: _, runner_id: _, + dispatch: _, } => true JitCredentialNotDispatched { refusal: _ } => false JitCredentialMintNotCreated { outcome: _ } => false diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index 621d93818fa..2548d2711a8 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -514,7 +514,7 @@ fn minted_registration(mint: JitMintPerformance) -> Int? { match mint { JitMintReceived { delivery: d } => match d { - JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id } => Present { value: id } + JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id, dispatch: _ } => Present { value: id } JitCredentialNotDispatched { refusal: _ } => none JitCredentialMintNotCreated { outcome: _ } => none } diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 0639a4d57ec..29f83c58d36 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -11,6 +11,7 @@ import extdeps.transports.rest { RestExchangeCommitAmbiguous, RestExchangeUnreached, RestExchangeUndecodable, } import extdeps.github.effect { GitHubEffect, DispatchWorkflow, GitHubWorkflowRef } +import extdeps.github.app { GitHubAppInstallationId } import extdeps.github.actions { Workflow, Job, DispatchInput, InputString, RunnerSpec, RunsOnExpression, HostedRunner, SelfHosted, WorkflowDispatch, Push, PullRequest, Schedule, WorkflowCall, WorkflowRunCompleted, MergeGroup, @@ -26,12 +27,12 @@ import extdeps.github.workflow_runs { import gunbc.repository { gunbc_repository } import gunbc.generated_artifact { artifact_path, FleetConvergeYamlArtifact } import gunbc.fleet_converge_workflow { fleet_converge_workflow, fleet_converge_dispatch_inputs } -import gunbc.auth.github_apps { GitHubAppControlPlaneState } +import gunbc.auth.github_apps { DeclaredGitHubApp, GitHubAppControlPlaneState } import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, } import gunbc.runner_jit_mint { HostUnitJitSlot, MicroVmJitSlot } -import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt } +import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt, authority_installation } import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } import gunbc.managed_host_unit_hold { UnitHoldProof } import gunbc.github_effect_perform { @@ -106,6 +107,7 @@ type QualificationDispatchSubject sole_constructor { registration: JitRegistration host: HostIdentity attempt: NonEmptyStr + runner_id: Int branch: NonEmptyStr revision: CommitSha floor_job_name: NonEmptyStr @@ -114,12 +116,13 @@ type QualificationDispatchSubject sole_constructor { type QualificationSubjectRefusal = SubjectCredentialNotDelivered { delivery: JitCredentialDelivery } | SubjectNoRegistration - | SubjectDeliveryForAnotherSlot + | SubjectDeliveryForAnotherDispatch | SubjectSlotNotHostUnit | SubjectSlotForAnotherWorkflow { slot_workflow: NonEmptyStr, qualification_workflow: NonEmptyStr } | SubjectAttemptInputUndeclared { input: String } | SubjectFloorJobAbsent { job_id: NonEmptyStr } | SubjectFloorRunsOnNotTheAttemptInput { job_id: NonEmptyStr, runs_on: String } + | SubjectFloorNameNotUnique { name: NonEmptyStr, jobs_with_name: Int } type QualificationSubjectStanding = QualificationSubjectAdmitted { subject: QualificationDispatchSubject } @@ -213,10 +216,16 @@ fn qualification_floor_admission(workflow: Workflow, floor_job_id: NonEmptyStr) match first(filter(workflow.jobs, j => j.id == (floor_job_id as String))) { Absent => QualificationFloorRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } Present { value: job } => - if runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { - QualificationFloorAdmitted { floor_job_name: job_display_name(job: job) } - } else { + if !runs_on_is_exactly_the_input(runner: job.runner, input: qualification_attempt_dispatch_input) { QualificationFloorRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: floor_job_id, runs_on: runner_spec_text(runner: job.runner) } } + } else { + let name = job_display_name(job: job) + let sharing = workflow.jobs |> filter(j => (job_display_name(job: j) as String) == (name as String)) + if count(sharing) != 1 { + QualificationFloorRefused { refusal: SubjectFloorNameNotUnique { name: name, jobs_with_name: count(sharing) } } + } else { + QualificationFloorAdmitted { floor_job_name: name } + } } } } @@ -237,15 +246,16 @@ fn qualification_dispatch_subject_over( admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), ] { match jit_registration_of(dispatch: JitMintDispatchAuthorized { dispatch: dispatch }) { Absent => QualificationSubjectRefused { refusal: SubjectNoRegistration } Present { value: registration } => match delivery { - JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: _ } => - if delivered_slot != registration.slot { - QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherSlot } + JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: runner_id, dispatch: delivered_by } => + if delivered_by != dispatch { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } } else { match registration.slot { MicroVmJitSlot { attempt: _ } => QualificationSubjectRefused { refusal: SubjectSlotNotHostUnit } @@ -261,6 +271,7 @@ fn qualification_dispatch_subject_over( registration: registration, host: h.host.host.host, attempt: h.attempt, + runner_id: runner_id, branch: branch, revision: revision, floor_job_name: n, @@ -334,6 +345,8 @@ type DispatchedQualificationRun sole_constructor { type QualificationDispatchRefusal = DispatchCredentialRefused { unsatisfied: List } | DispatchTokenForAnotherInstallation + | DispatchAuthorityForAnotherInstallation { authority_app: DeclaredGitHubApp, authority_installation: GitHubAppInstallationId } + | DispatchAuthorityNotAnInstallation | DispatchHoldForAnotherHost { held: HostIdentity, slot: HostIdentity } | DispatchStatusRefused { status: Int, body: String } @@ -360,6 +373,23 @@ fn qualification_dispatch_standing(performance: RestExchangePerformance) -> Qual } } +// THE PERMISSION IS ASKED OF THE INSTALLATION THAT PERFORMS THE REQUEST. credential_authorizes answers +// for whatever authority it is given; the request is made with the controller token. So the +// authority must name the very App and installation the registration (and therefore the token) was +// minted under, or the actions:write it proves is some other installation's. An Actions job +// credential names no installation at all and cannot authorize an installation-token request. +fn dispatch_authority_refusal(authority: GitHubCredentialAuthority, app: DeclaredGitHubApp, installation: GitHubAppInstallationId) -> QualificationDispatchRefusal? { + match authority_installation(authority: authority) { + Absent => Present { value: DispatchAuthorityNotAnInstallation } + Present { value: held } => + if held.app == app && held.installation == installation { + none + } else { + Present { value: DispatchAuthorityForAnotherInstallation { authority_app: held.app, authority_installation: held.installation } } + } + } +} + // THE INTERLOCK, as a pure comparison: the proof's admitted host against the slot's host. fn hold_covers_slot_host(held: HostIdentity, slot_host: HostIdentity) -> Bool { (held as String) == (slot_host as String) @@ -377,24 +407,30 @@ fn dispatch_qualification_floor( QualificationDispatchRefused { subject: subject, refusal: DispatchHoldForAnotherHost { held: hold.subject.host.host, slot: subject.host } } } else if token.app != subject.registration.app || token.installation != subject.registration.installation { QualificationDispatchRefused { subject: subject, refusal: DispatchTokenForAnotherInstallation } - } else if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { - QualificationDispatchRefused { - subject: subject, - refusal: DispatchCredentialRefused { unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect) }, - } } else { - let answer = dispatch_workflow_with( - token: token, - owner: gunbc_repository.owner as NonEmptyStr, - repo: gunbc_repository.name as NonEmptyStr, - workflow_id: qualification_workflow_file_name(), - git_ref: subject.branch, - inputs: qualification_dispatch_inputs(attempt: subject.attempt), - ) - match qualification_dispatch_standing(performance: answer.performance) { - DispatchStandingSucceeded => QualificationDispatched { run: DispatchedQualificationRun { subject: subject, receipt: answer.receipt } } - DispatchStandingRefused { status: s, body: b } => QualificationDispatchRefused { subject: subject, refusal: DispatchStatusRefused { status: s, body: b } } - DispatchStandingCommitAmbiguous { detail: d } => QualificationDispatchCommitAmbiguous { subject: subject, detail: d } + match dispatch_authority_refusal(authority: authority, app: subject.registration.app, installation: subject.registration.installation) { + Present { value: r } => QualificationDispatchRefused { subject: subject, refusal: r } + Absent => + if !credential_authorizes(authority: authority, control_plane: control_plane, effect: effect) { + QualificationDispatchRefused { + subject: subject, + refusal: DispatchCredentialRefused { unsatisfied: unsatisfied_capabilities(authority: authority, control_plane: control_plane, effect: effect) }, + } + } else { + let answer = dispatch_workflow_with( + token: token, + owner: gunbc_repository.owner as NonEmptyStr, + repo: gunbc_repository.name as NonEmptyStr, + workflow_id: qualification_workflow_file_name(), + git_ref: subject.branch, + inputs: qualification_dispatch_inputs(attempt: subject.attempt), + ) + match qualification_dispatch_standing(performance: answer.performance) { + DispatchStandingSucceeded => QualificationDispatched { run: DispatchedQualificationRun { subject: subject, receipt: answer.receipt } } + DispatchStandingRefused { status: s, body: b } => QualificationDispatchRefused { subject: subject, refusal: DispatchStatusRefused { status: s, body: b } } + DispatchStandingCommitAmbiguous { detail: d } => QualificationDispatchCommitAmbiguous { subject: subject, detail: d } + } + } } } } @@ -429,7 +465,8 @@ type QualificationRunRefusal | RunJobsUnreadable { performance: RestExchangePerformance } | RunJobsTruncated { returned: Int, total: Int } | RunFloorJobNotUnique { floor_job_name: NonEmptyStr, matching: Int } - | RunNeverReachedAttemptRunner { registered_runner: NonEmptyStr, runner_names_seen: List } + | RunNeverReachedAttemptRunner { registered_runner_id: Int, runner_ids_seen: List } + | RunRunnerNameDisagrees { registered_runner_id: Int, registered_runner: NonEmptyStr, observed_runner_name: String? } | RunRegisteredRunnerRanAnotherJob { registered_runner: NonEmptyStr, job_name: String } | RunFloorJobNotStarted { job_id: Int } @@ -463,12 +500,8 @@ fn optional_text_is(o: String?, expected: String) -> Bool { // THE ONE JOIN FROM A JOB TO A JIT REGISTRATION: the runner GitHub reports took the job is the // name the authorized mint fixed (gunbc.runner.runner_jit_deregistration JitRegistration name). // Labels cannot make this join -- two runners can carry identical labels. -fn job_ran_on(job: WorkflowJobRun, runner_name: NonEmptyStr) -> Bool { - optional_text_is(o: job.runner_name, expected: runner_name as String) -} - -fn job_runner_name_seen(job: WorkflowJobRun) -> String { - match job.runner_name { Present { value: n } => n Absent => "(no runner assigned)" } +fn job_ran_on(job: WorkflowJobRun, runner_id: Int) -> Bool { + match job.runner_id { Present { value: v } => v == runner_id Absent => false } } fn job_started(job: WorkflowJobRun) -> Bool { @@ -486,6 +519,7 @@ type QualificationRunVerdict // the floor job the subject fixed, and the pinned revision. collect_qualification_run derives it from // the sealed subject (qualification_run_expectation); a claim supplies it directly. type QualificationRunExpectation { + registered_runner_id: Int registered_runner: NonEmptyStr floor_job_name: NonEmptyStr revision: CommitSha @@ -493,6 +527,7 @@ type QualificationRunExpectation { fn qualification_run_expectation(subject: QualificationDispatchSubject) -> QualificationRunExpectation { QualificationRunExpectation { + registered_runner_id: subject.runner_id, registered_runner: subject.registration.name.value, floor_job_name: subject.floor_job_name, revision: subject.revision, @@ -500,15 +535,16 @@ fn qualification_run_expectation(subject: QualificationDispatchSubject) -> Quali } fn decide_floor_job(subject: QualificationRunExpectation, run: WorkflowRun, conclusion: WorkflowRunConclusion, jobs: List) -> QualificationRunVerdict { - let registered = subject.registered_runner - let on_runner = jobs |> filter(j => job_ran_on(job: j, runner_name: registered)) + let on_runner = jobs |> filter(j => job_ran_on(job: j, runner_id: subject.registered_runner_id)) match first(on_runner) { - Absent => RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: registered, runner_names_seen: jobs |> map(j => job_runner_name_seen(job: j)) } } + Absent => RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner_id: subject.registered_runner_id, runner_ids_seen: jobs |> map(j => j.runner_id) } } Present { value: job } => if count(on_runner) != 1 { RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: subject.floor_job_name, matching: count(on_runner) } } + } else if !optional_text_is(o: job.runner_name, expected: subject.registered_runner as String) { + RunVerdictRefused { refusal: RunRunnerNameDisagrees { registered_runner_id: subject.registered_runner_id, registered_runner: subject.registered_runner, observed_runner_name: job.runner_name } } } else if job.name != (subject.floor_job_name as String) { - RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: registered, job_name: job.name } } + RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: subject.registered_runner, job_name: job.name } } } else if !job_started(job: job) { RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: job.id } } } else { diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index 294dbf896ea..c61f32d8efb 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -1310,6 +1310,7 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool slot: a, credential: c, runner_id: id, + dispatch: _, } => match dispatched(owned: true, perms: runner_grant) { JitMintDispatchAuthorized { dispatch: d } => a == d.slot diff --git a/dag/test/claim/github_effect_perform_witness_test.dag b/dag/test/claim/github_effect_perform_witness_test.dag index 1b399fdf1f3..f21dcf18a2e 100644 --- a/dag/test/claim/github_effect_perform_witness_test.dag +++ b/dag/test/claim/github_effect_perform_witness_test.dag @@ -102,7 +102,7 @@ data created_body: JitConfigCreatedBody = JitConfigCreatedBody { test fn witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered() -> Bool { let d = dispatched(owned: true, perms: runner_grant) let delivered = match jit_mint_performance_of_generate(dispatch: d, outcome: RestOk, created: created_body) { - JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id } } => + JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id, dispatch: _ } } => (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" && id == 4242 _ => false } diff --git a/dag/test/claim/public_workload_census_witness_test.dag b/dag/test/claim/public_workload_census_witness_test.dag index fea91d8fdd7..b7488718758 100644 --- a/dag/test/claim/public_workload_census_witness_test.dag +++ b/dag/test/claim/public_workload_census_witness_test.dag @@ -74,6 +74,7 @@ data candidate_biome_blob_mismatch_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -124,6 +125,7 @@ data candidate_biome_matrix_selector_mismatch_control: WorkloadCandidate = Workl status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -180,6 +182,7 @@ data candidate_pdns_axis_name_mismatch_control: WorkloadCandidate = WorkloadCand status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -241,6 +244,7 @@ data candidate_pdns_axis_name_swap_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, @@ -295,6 +299,7 @@ data candidate_biome_job_key_mismatch_control: WorkloadCandidate = WorkloadCandi status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -344,6 +349,7 @@ data candidate_biome_job_key_titlecase_uninspected_control: WorkloadCandidate = status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -393,6 +399,7 @@ data candidate_biome_multi_label_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["self-hosted", "linux", "arm64"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -442,6 +449,7 @@ data candidate_empty_labels_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -491,6 +499,7 @@ data candidate_absent_conclusion_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Absent, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -540,6 +549,7 @@ data candidate_run_attempt_zero_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, @@ -589,6 +599,7 @@ data candidate_empty_matrix_axes_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index 2b513f6758f..2cf9010ce42 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -1,5 +1,7 @@ module test.claim.runner_attempt_launch_witness +import test.claim.github_app_registry { dispatched, runner_grant } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused } import gunbc.runner_attempt_launch { GitHubLaunchPayload, launch_payload_device } import std.types { String, Bool, Int, List, NonEmptyStr } @@ -135,11 +137,20 @@ fn minted_credential(encoded: String) -> List { // something that is not a whole credential, handed on as if it were. data short_blob: String = "ZmFrZS1qaXQtY29uZmln" +// THE DISPATCH ON A HAND-BUILT DELIVERY. JitCredentialBoundToAttempt carries the sealed authorized +// dispatch that produced it (gunbc.runner.runner_qualification_dispatch joins on it). This fixture +// builds a delivery directly to drive the launch decision, which reads only the slot, the credential +// and the runner id, so it takes the dispatch from test.claim.github_app_registry dispatched -- the +// one admitted fixture mint -- rather than forging one. fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance { match minted_credential(encoded: encoded) { Cons { head: c, tail: _ } => - JitMintReceived { - delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id }, + match dispatched(owned: true, perms: runner_grant) { + JitMintDispatchAuthorized { dispatch: d } => + JitMintReceived { + delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id, dispatch: d }, + } + JitMintDispatchRefused { refusal: _ } => JitMintAppJwtUnsigned { detail: "fixture: the admitted dispatch fixture was refused" } } Empty {} => JitMintAppJwtUnsigned { detail: "fixture: credential fixture did not mint" } } diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 6d7fc92e04b..55cda16e83f 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -1,5 +1,7 @@ module test.claim.runner.runner_microvm_slot_controller_witness_test +import test.claim.github_app_registry { dispatched, runner_grant } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused } import v2.std.algebra { any } import std.types { String, NonEmptyStr, Bool, Int, List } import std.nat { Nat } @@ -294,7 +296,11 @@ fn delivered_mint() -> Bool { Present { value: a } => match jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: "") { JitConfigMinted { config: c, runner_id: id } => - minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id } }) == Present { value: 4242 } + match dispatched(owned: true, perms: runner_grant) { + JitMintDispatchAuthorized { dispatch: d } => + minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id, dispatch: d } }) == Present { value: 4242 } + JitMintDispatchRefused { refusal: _ } => false + } _ => false } } diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index e66966bfa52..4414888399c 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -8,7 +8,7 @@ import extdeps.transports.rest { RestOutcome, RestOk, RestTransportRefused, RestStatusRefused, RestBodyUndecodable, RestMutationExchange, classify_rest_outcome, } import extdeps.github.actions { Workflow, Job, WorkflowDispatch, RunsOnExpression, SelfHosted, RunnerSpec } -import extdeps.github.effect { DispatchWorkflow } +import extdeps.github.effect { DispatchWorkflow, GitHubOrganizationRef } import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, } @@ -18,12 +18,18 @@ import gunbc.runner_jit_mint { HostUnitSlotBinding } import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit } -import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome } +import test.claim.github_app_registry { + app_credential, other_app_credential, other_installation_credential, a_pull_request_job_credential, + bot_installation_id, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome, +} +import gunbc.auth.github_apps { gunbai_bot_declared } import gunbc.runner.runner_qualification_dispatch { qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_selected_workflow, qualification_workflow_path, qualification_dispatch_effect, QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, + SubjectFloorNameNotUnique, SubjectDeliveryForAnotherDispatch, + dispatch_authority_refusal, DispatchAuthorityForAnotherInstallation, DispatchAuthorityNotAnInstallation, qualification_dispatch_subject, qualification_dispatch_subject_over, qualification_slot_refusal, qualification_floor_admission, QualificationFloorAdmitted, QualificationFloorRefused, qualification_run_expectation, QualificationRunExpectation, @@ -33,7 +39,7 @@ import gunbc.runner.runner_qualification_dispatch { decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, - RunNeverReachedAttemptRunner, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, + RunNeverReachedAttemptRunner, RunRunnerNameDisagrees, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -43,6 +49,7 @@ data branch: NonEmptyStr = "main" data revision: CommitSha = "1111111111111111111111111111111111111111" data floor_job_id: NonEmptyStr = "qualification-floor" data registered_runner: NonEmptyStr = "gunbc-qualification-fixture-q-1.service" +data registered_runner_id: Int = 4242 // DESIGN section 3, A WITNESS DISCRIMINATES AT ONE INTERFACE. Every claim but one supplies its // inputs as values at the interface it judges: the pure scope checks (qualification_slot_refusal, @@ -109,6 +116,7 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { QualificationSubjectAdmitted { subject: s } => { let expected = qualification_run_expectation(subject: s) s.attempt == attempt + && s.runner_id == registered_runner_id && expected.registered_runner_id == registered_runner_id && (s.host as String) == (h.host as String) && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (expected.floor_job_name as String) == "qualification floor" @@ -210,6 +218,90 @@ test fn a_floor_runs_on_other_than_exactly_the_attempt_input_refuses() -> Bool { QualificationFloorAdmitted { floor_job_name: n } => (n as String) == "qualification floor" QualificationFloorRefused { refusal: _ } => false }) + && (match qualification_floor_admission(workflow: workflow_sharing_the_floor_name(), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorNameNotUnique { name: _, jobs_with_name: k } } => k == 2 + _ => false + }) +} + +// A HELPER JOB WITH THE FLOOR'S DISPLAY NAME. The readback identifies the floor by the name GitHub +// reports, so two authored jobs sharing it would let the helper stand in for the floor; the subject +// mint refuses such a workflow before any dispatch exists. +fn workflow_sharing_the_floor_name() -> Workflow { + let helper = Job { + id: "helper", + name: Present { value: "qualification floor" }, + runner: SelfHosted { labels: ["self-hosted"] }, + environment: none, + steps: [], + needs: [], + env: none, + outputs: none, + if_condition: none, + timeout_minutes: none, + continue_on_error: none, + concurrency: none, + permissions: none, + } + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: [qualification_attempt_dispatch_input] }], + concurrency: none, + jobs: [floor_job(runs_on: attempt_runs_on), helper], + env: none, + permissions: none, + } +} + +// ── THE INSTALLATION THAT PERFORMS THE REQUEST IS THE ONE WHOSE PERMISSION IS ASKED ───────────── +// The registration and token are App gunbai-bot installation 999 (app_credential). RED: an authority +// for another App, or for another installation of the same App, refuses before any network; an +// Actions job credential names no installation and refuses. Positive control: the matching authority. +test fn the_dispatch_authority_must_name_the_tokens_installation() -> Bool { + (match dispatch_authority_refusal(authority: app_credential, app: gunbai_bot_declared, installation: bot_installation_id) { Absent => true Present { value: _ } => false }) + && (match dispatch_authority_refusal(authority: other_app_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityForAnotherInstallation { authority_app: _, authority_installation: _ } } => true + _ => false + }) + && (match dispatch_authority_refusal(authority: other_installation_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityForAnotherInstallation { authority_app: _, authority_installation: _ } } => true + _ => false + }) + && (match dispatch_authority_refusal(authority: a_pull_request_job_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityNotAnInstallation } => true + _ => false + }) +} + +// A DELIVERY CARRIES THE AUTHORIZED DISPATCH THAT PRODUCED IT. Two authorized mints for the SAME +// host-unit slot -- here under different organizations -- produce deliveries that agree on slot, +// credential and runner id; the subject over dispatch A refuses delivery B rather than claiming A's +// registration for a credential B minted. +test fn a_delivery_from_another_dispatch_on_the_same_slot_refuses() -> Bool { + match first(managed_hosts()) { + Present { value: h } => { + let binding = HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) } + let group = group_restricted_to(workflow: qualification_selected_workflow(branch: branch)) + match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: a } => + match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: GitHubOrganizationRef { login: "another-org" }, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: b } => + a.slot == b.slot + && (match qualification_dispatch_subject_over( + dispatch: a, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: b }, outcome: minted_outcome), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + ) { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true + _ => false + }) + JitMintDispatchRefused { refusal: _ } => false + } + JitMintDispatchRefused { refusal: _ } => false + } + } + Absent => false + } } // ── THE DISPATCH STANDING AND THE INTERLOCK ───────────────────────────────────────────────────── @@ -238,6 +330,7 @@ test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { // ── THE COLLECTION, OVER A SUPPLIED EXPECTATION ───────────────────────────────────────────────── data expect: QualificationRunExpectation = QualificationRunExpectation { + registered_runner_id: registered_runner_id, registered_runner: registered_runner, floor_job_name: "qualification floor", revision: revision, @@ -269,7 +362,7 @@ fn dispatched_run(attempt_n: Int) -> WorkflowRun { run(id: 77, attempt_n: attempt_n, event: "workflow_dispatch", path: qualification_workflow_path() as String, sha: revision, completed: true, concluded: true) } -fn job(id: Int, name: String, runner: String, started: Bool, attempt_n: Int) -> WorkflowJobRun { +fn job(id: Int, name: String, runner_id: Int, runner: String, started: Bool, attempt_n: Int) -> WorkflowJobRun { WorkflowJobRun { id: id, run_id: 77, @@ -278,6 +371,7 @@ fn job(id: Int, name: String, runner: String, started: Bool, attempt_n: Int) -> status: Completed, conclusion: Present { value: Success }, labels: [attempt as String], + runner_id: Present { value: runner_id }, runner_name: Present { value: runner }, created_at: Present { value: "2026-10-04T00:00:10Z" }, started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, @@ -285,8 +379,8 @@ fn job(id: Int, name: String, runner: String, started: Bool, attempt_n: Int) -> } } -fn floor_on(runner: String) -> WorkflowJobRun { - job(id: 1, name: "qualification floor", runner: runner, started: true, attempt_n: 1) +fn floor_on(runner_id: Int) -> WorkflowJobRun { + job(id: 1, name: "qualification floor", runner_id: runner_id, runner: registered_runner as String, started: true, attempt_n: 1) } fn jobs(js: List) -> WorkflowJobRunList { @@ -308,29 +402,34 @@ fn mismatch(v: QualificationRunVerdict) -> Bool { } } -// THE RUNNER IS JOINED BY IDENTITY. Positive control: the named floor job on the registered runner is -// collected with the run's own conclusion (a Failure is still this host's run). REDs: the same labels -// on another runner; a helper job on the registered runner while the floor ran elsewhere; a floor on -// the registered runner that never started; two jobs on the registered runner. +// THE RUNNER IS JOINED BY ITS MINTED ID, WITH THE NAME AS CORROBORATION. Positive control: the named +// floor job on the registered runner id is collected with the run's own conclusion (a Failure is +// still this host's run). REDs: the same runner name and labels under another runner id; the minted +// id reported under another name; a helper job on the registered runner while the floor ran +// elsewhere; a floor on the registered runner that never started; two jobs on the registered runner. test fn the_collection_joins_the_floor_job_to_the_registered_runner() -> Bool { let mine = registered_runner as String - (match decide_jobs(js: jobs(js: [floor_on(runner: mine)])) { + (match decide_jobs(js: jobs(js: [floor_on(runner_id: registered_runner_id)])) { RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) _ => false }) - && (match decide_jobs(js: jobs(js: [floor_on(runner: "another-runner-with-the-same-labels")])) { - RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner: _, runner_names_seen: seen } } => count(seen) == 1 + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: 99, runner: mine, started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner_id: rid, runner_ids_seen: seen } } => rid == registered_runner_id && count(seen) == 1 + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: "another-name", started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunRunnerNameDisagrees { registered_runner_id: _, registered_runner: _, observed_runner_name: _ } } => true _ => false }) - && (match decide_jobs(js: jobs(js: [job(id: 2, name: "helper", runner: mine, started: true, attempt_n: 1), floor_on(runner: "another-runner-with-the-same-labels")])) { + && (match decide_jobs(js: jobs(js: [job(id: 2, name: "helper", runner_id: registered_runner_id, runner: mine, started: true, attempt_n: 1), floor_on(runner_id: 99)])) { RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: _, job_name: n } } => n == "helper" _ => false }) - && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner: mine, started: false, attempt_n: 1)])) { + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: mine, started: false, attempt_n: 1)])) { RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: id } } => id == 1 _ => false }) - && (match decide_jobs(js: jobs(js: [floor_on(runner: mine), job(id: 2, name: "qualification floor", runner: mine, started: true, attempt_n: 1)])) { + && (match decide_jobs(js: jobs(js: [floor_on(runner_id: registered_runner_id), job(id: 2, name: "qualification floor", runner_id: registered_runner_id, runner: mine, started: true, attempt_n: 1)])) { RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 _ => false }) @@ -340,20 +439,20 @@ test fn the_collection_joins_the_floor_job_to_the_registered_runner() -> Bool { // job carries the same labels and runner); a job of attempt 2 in the listing; another run id; another // event; another workflow path. test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { - let floor = jobs(js: [floor_on(runner: registered_runner as String)]) + let floor = jobs(js: [floor_on(runner_id: registered_runner_id)]) let path = qualification_workflow_path() as String (match decide_run(r: dispatched_run(attempt_n: 2), js: floor) { RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 _ => false }) - && mismatch(v: decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner: registered_runner as String, started: true, attempt_n: 2)]))) + && mismatch(v: decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: registered_runner as String, started: true, attempt_n: 2)]))) && mismatch(v: decide_run(r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) } test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { - let floor = jobs(js: [floor_on(runner: registered_runner as String)]) + let floor = jobs(js: [floor_on(runner_id: registered_runner_id)]) let path = qualification_workflow_path() as String (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true diff --git a/dag/test/claim/superseded_run_starvation_census_witness_test.dag b/dag/test/claim/superseded_run_starvation_census_witness_test.dag index 3ab260e8c3a..96091a15fd6 100644 --- a/dag/test/claim/superseded_run_starvation_census_witness_test.dag +++ b/dag/test/claim/superseded_run_starvation_census_witness_test.dag @@ -213,6 +213,7 @@ data job_cancelled: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Cancelled }, labels: [], + runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:01:00Z" }, started_at: Present { value: "2026-09-01T00:10:00Z" }, @@ -227,6 +228,7 @@ data job_failed: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Failure }, labels: [], + runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:00:00Z" }, started_at: Present { value: "2026-09-01T00:00:00Z" }, @@ -241,6 +243,7 @@ data job_success: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:02:00Z" }, started_at: Present { value: "2026-09-01T00:05:00Z" }, @@ -255,6 +258,7 @@ data job_queued: WorkflowJobRun = WorkflowJobRun { status: Queued, conclusion: none, labels: [], + runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:03:00Z" }, started_at: none, @@ -269,6 +273,7 @@ data job_in_progress: WorkflowJobRun = WorkflowJobRun { status: InProgress, conclusion: none, labels: [], + runner_id: none, runner_name: none, created_at: Present { value: "2026-09-01T00:04:00Z" }, started_at: Present { value: "2026-09-01T00:30:00Z" }, @@ -344,6 +349,7 @@ test fn earliest_created_at_returns_none_when_no_created_at() -> Bool { status: InProgress, conclusion: none, labels: [], + runner_id: none, runner_name: none, created_at: none, started_at: none, From 82498c3df13865a36b4a06620b010e68a5e2a4aa Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 14:05:57 +0000 Subject: [PATCH 27/44] leg 3 RC item 4: per-attempt branch pin -- create-once refs/heads/qualification/ at the revision, the dedicated group pinned to it, removal by readback on every exit GitHub selects a dispatched (non-reusable) workflow in a runner group only at a branch (docs cited in extdeps.github.org_actions), so the pin is a branch, not a SHA or tag. New: - extdeps.github.git_database CreateRef/DeleteRef (+ auth_input beside GITHUB_TOKEN; heal unchanged) - extdeps.github.org_actions UpdateOrganizationRunnerGroupWorkflows - gunbc.runner.runner_qualification_ref_pin: QualificationBranchName (one segment under the prefix), QualificationGroupWriteTarget (dedicated group only), sealed QualificationRefPin, ensure + removal - the dispatch subject carries the pin; dispatch ref = the pin branch; mint must be into the pinned group; collection refuses a run on another branch (RunOnAnotherBranch) - census rows + interpretation extension (eager-gull-22, 2026-10-04) - witnesses: out-of-prefix, pre-existing branch, another group, removal readback, no push workflow fires on qualification/**, run on another branch Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/git_database.dag | 69 ++++ dag/extdeps/github/org_actions.dag | 37 ++ dag/gunbc/auth/privileged_effect_census.dag | 34 +- dag/gunbc/github_effect_perform.dag | 34 ++ .../runner/runner_qualification_dispatch.dag | 50 +-- .../runner/runner_qualification_ref_pin.dag | 316 ++++++++++++++++++ .../runner_throughput_qualification_route.dag | 2 +- ...ization_pattern_selection_witness_test.dag | 4 +- ...er_qualification_dispatch_witness_test.dag | 91 +++-- ...ner_qualification_ref_pin_witness_test.dag | 164 +++++++++ 10 files changed, 749 insertions(+), 52 deletions(-) create mode 100644 dag/gunbc/runner/runner_qualification_ref_pin.dag create mode 100644 dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag diff --git a/dag/extdeps/github/git_database.dag b/dag/extdeps/github/git_database.dag index 8979f3a157d..5198424cadc 100644 --- a/dag/extdeps/github/git_database.dag +++ b/dag/extdeps/github/git_database.dag @@ -38,6 +38,8 @@ type GitRefObjectWire { sha: String } +type GitRefDeletedBody {} + type GitRefWire { ref: String object: GitRefObjectWire @@ -111,10 +113,21 @@ data git_blob_encoding_base64: NonEmptyStr = "base64" // so no caller of UpdateRefFastForward can express a forced update; a moved branch answers 422 // "Update is not a fast forward" and that refusal IS the compare-and-swap. A forced variant would be // a different operation with its own name and its own consumer. +// CreateRef AND DeleteRef (docs.github.com/en/rest/git/refs#create-a-reference and +// #delete-a-reference, read 2026-10-04). CreateRef takes the FULL ref name (`refs/tags/`) in its +// body and answers 201 with the reference; an existing ref answers 422 "Reference already exists", +// so creation never moves a ref and that 422 is the create-once refusal. DeleteRef takes the +// `tags/` form in its path and answers 204 with no body; a ref already gone answers 422 +// "Reference does not exist". Their consumer is gunbc.runner.runner_qualification_ref_pin (a create-once branch per qualification attempt and its removal). +// +// THE BEARER IS AN INPUT WHEN ONE IS GIVEN. Every operation already took `auth_token`; the service read +// only GITHUB_TOKEN. Declaring both lets a caller that holds an installation token present it, and a +// caller passing "" (gunbc.heal_publication) falls through to GITHUB_TOKEN exactly as before. service github.GitDatabase { config { endpoint: default_api_base auth: Bearer + auth_input: auth_token auth_source: EnvVar { name: "GITHUB_TOKEN" } } @@ -313,4 +326,60 @@ service github.GitDatabase { 5xx => GitHubErrorShape } } + + operation CreateRef { + requires Network + input { + auth_token: Secret + owner: String + repo: String + full_ref: String + sha: String + } + output { + reference: GitRefWire + outcome: RestOutcome + } + transport rest { + method: POST, + path: "/repos/\{owner\}/\{repo\}/git/refs", + body: { ref: full_ref, sha: sha } + } + response { + 201 => GitRefWire + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 409 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } + + operation DeleteRef { + requires Network + input { + auth_token: Secret + owner: String + repo: String + ref: String + } + output { + result: GitRefDeletedBody + outcome: RestOutcome + } + transport rest { + method: DELETE, + path: "/repos/\{owner\}/\{repo\}/git/refs/\{ref\}" + } + response { + 204 => GitRefDeletedBody + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 409 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } } diff --git a/dag/extdeps/github/org_actions.dag b/dag/extdeps/github/org_actions.dag index 9ac42f67977..1ad989c1da3 100644 --- a/dag/extdeps/github/org_actions.dag +++ b/dag/extdeps/github/org_actions.dag @@ -130,6 +130,16 @@ type OrgRunnerGroupListWire { data org_runner_groups_max_per_page: Int = 100 +// UpdateOrganizationRunnerGroupWorkflows: +// update a self-hosted runner group for an organization (docs.github.com/en/rest/actions/ +// self-hosted-runner-groups#update-a-self-hosted-runner-group-for-an-organization, REST +// 2022-11-28, read 2026-10-04): PATCH sets restricted_to_workflows and selected_workflows and +// answers 200 with the group. Each selected entry is `//@`; per +// docs.github.com/en/enterprise-cloud@latest/actions/how-tos/manage-runners/self-hosted-runners/ +// manage-access (read 2026-10-04), "Pin non-reusable workflows to a branch. Pin reusable +// workflows to a branch, tag, or full SHA" -- a dispatched (non-reusable) workflow is selectable +// only at a branch, which is why a qualification pin is a branch and not a SHA. Its consumer is +// gunbc.runner.runner_qualification_ref_pin, which writes only the dedicated qualification group. service github.OrgRunnerGroupsRest { config { endpoint: default_api_base @@ -162,4 +172,31 @@ service github.OrgRunnerGroupsRest { 5xx => GitHubErrorShape } } + + operation UpdateOrganizationRunnerGroupWorkflows { + requires Network + input { + installation_token: Secret + org: NonEmptyStr + runner_group_id: Int + selected_workflows: List + } + output { + result: OrgRunnerGroupWire + outcome: RestOutcome + } + transport rest { + method: PATCH, + path: "/orgs/\{org\}/actions/runner-groups/\{runner_group_id\}", + body: { restricted_to_workflows: true, selected_workflows: selected_workflows } + } + response { + 200 => OrgRunnerGroupWire + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } } diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index 6c1a57a7aba..211469142fe 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -555,10 +555,12 @@ data runner_lifecycle_ruling_scope_interpretation: RulingScopeInterpretation = R decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), + decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), ], interpreted_by: "eager-gull-22" as NonEmptyStr, interpreted_on: "2026-10-04" as NonEmptyStr, - standing: "covers register, deregister and the qualification dispatch; reported to the operator, who may narrow it" as NonEmptyStr, + standing: "covers register, deregister and the qualification dispatch, and -- as part of registration -- the create-once per-attempt branch refs/heads/qualification/ at the pinned revision, its removal on every exit, and pinning the DEDICATED qualification runner group's selected workflow to that branch; reported to the operator, who may narrow it" as NonEmptyStr, } // A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE INTERPRETATION NAMES IT. Removing a site from @@ -641,6 +643,36 @@ data privileged_effect_census: List = [ realized: RealizedFederatedGrant, divergence_reason: none, }, + PrivilegedEffectSite { + site: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), + effect: PrivilegedEffect { + subject: "create the per-attempt branch refs/heads/qualification/ once at the pinned revision (CreateRef, never moved; 422 on an existing ref is refused), read it back, and pin the dedicated runner-qualification group's selected workflow to the qualification workflow at that branch, read back; under the gunbai-ci installation token, whose contents:write reaches every ref of the repository and whose organization self-hosted-runners write reaches every runner group -- the confinement to the prefix and to the dedicated group is this module's construction, not the token's scope" as NonEmptyStr, + frequency: EveryProvision, + reversibility: ReversibleByReapply, + surface: ApiSurface, + workload_identity: convergence_principal(), + minted_reach: MintsNoCredential, + billing: NoBillingConsequence, + witness_discharge: NoWitnessDischarge, + }, + realized: RealizedFederatedGrant, + divergence_reason: none, + }, + PrivilegedEffectSite { + site: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), + effect: PrivilegedEffect { + subject: "on every exit, delete the attempt's refs/heads/qualification/ branch and confirm it absent by readback, under the same installation token" as NonEmptyStr, + frequency: EveryProvision, + reversibility: ReversibleByReapply, + surface: ApiSurface, + workload_identity: convergence_principal(), + minted_reach: MintsNoCredential, + billing: NoBillingConsequence, + witness_discharge: NoWitnessDischarge, + }, + realized: RealizedFederatedGrant, + divergence_reason: none, + }, PrivilegedEffectSite { site: site(module_path: "gunbc.runner_group_restriction_ensure_run", decl_name: "microvm_runner_group_after_gate"), effect: PrivilegedEffect { diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 81881535442..d195d807d6b 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -31,6 +31,9 @@ import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire } import extdeps.github.workflows import extdeps.github.workflows { WorkflowDispatchReceipt } import extdeps.github.workflow_runs +import extdeps.github.git_database +import extdeps.github.git_database { GitRefWire } +import extdeps.github.org_actions import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRunList } import gunbc.auth.github_apps { DeclaredGitHubApp, gunbai_ci_declared } import extdeps.github.app { GitHubAppInstallationId } @@ -361,3 +364,34 @@ fn read_attempt_jobs_with(token: ControllerInstallationToken, owner: NonEmptyStr let read = github.WorkflowRuns.ListAttemptJobs(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String, attempt_number: attempt) WorkflowAttemptJobsRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: read.outcome }), jobs: read.result } } + +// THE QUALIFICATION REF PIN'S EXCHANGES (gunbc.runner.runner_qualification_ref_pin), under the same +// controller installation token. Ref and group reads are reads; the create, delete and group write +// are mutations, so a lost answer is commit-ambiguous. Each answer is sealed for the same reason as +// the dispatch's: the decision module judges only what GitHub returned. +type GitRefRead sole_constructor { + performance: RestExchangePerformance + reference: GitRefWire +} + +fn read_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, ref: NonEmptyStr) -> GitRefRead uses net: Network { + let read = github.GitDatabase.GetRef(auth_token: token.token, owner: owner as String, repo: repo as String, ref: ref as String) + GitRefRead { performance: classify_rest_outcome(standing: RestReadExchange { rest: read.outcome }), reference: read.reference } +} + +fn create_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, full_ref: NonEmptyStr, sha: String) -> RestExchangePerformance uses net: Network { + let created = github.GitDatabase.CreateRef(auth_token: token.token, owner: owner as String, repo: repo as String, full_ref: full_ref as String, sha: sha) + classify_rest_outcome(standing: RestMutationExchange { rest: created.outcome }) +} + +fn delete_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, ref: NonEmptyStr) -> RestExchangePerformance uses net: Network { + let deleted = github.GitDatabase.DeleteRef(auth_token: token.token, owner: owner as String, repo: repo as String, ref: ref as String) + classify_rest_outcome(standing: RestMutationExchange { rest: deleted.outcome }) +} + +fn update_runner_group_workflows_with(token: ControllerInstallationToken, organization: NonEmptyStr, runner_group_id: Int, selected_workflows: List) -> RestExchangePerformance uses net: Network { + let updated = github.OrgRunnerGroupsRest.UpdateOrganizationRunnerGroupWorkflows( + installation_token: token.token, org: organization, runner_group_id: runner_group_id, selected_workflows: selected_workflows, + ) + classify_rest_outcome(standing: RestMutationExchange { rest: updated.outcome }) +} diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 29f83c58d36..e4e1de39869 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -25,7 +25,6 @@ import extdeps.github.workflow_runs { WorkflowRun, WorkflowRunStatus, Completed, WorkflowRunConclusion, WorkflowJobRun, WorkflowJobRunList, } import gunbc.repository { gunbc_repository } -import gunbc.generated_artifact { artifact_path, FleetConvergeYamlArtifact } import gunbc.fleet_converge_workflow { fleet_converge_workflow, fleet_converge_dispatch_inputs } import gunbc.auth.github_apps { DeclaredGitHubApp, GitHubAppControlPlaneState } import gunbc.auth.github_credential { @@ -39,6 +38,9 @@ import gunbc.github_effect_perform { ControllerInstallationToken, dispatch_workflow_with, read_run_with, read_attempt_jobs_with, } import gunbc.runner_throughput_qualification_route { QualificationInstrument, qualification_instruments } +import gunbc.runner.runner_qualification_ref_pin { + QualificationRefPin, QualificationBranchName, qualification_workflow_path, qualification_selected_workflow, +} // THE DISPATCH LEG OF THE QUALIFICATION ROUTE, AND THE COLLECT STAGE THAT READS ITS RUN BACK. // @@ -80,10 +82,6 @@ data qualification_attempt_dispatch_input: DispatchInput = DispatchInput { type: InputString, } -fn qualification_workflow_path() -> NonEmptyStr { - artifact_path(a: FleetConvergeYamlArtifact) as NonEmptyStr -} - fn qualification_workflow_file_name() -> NonEmptyStr { match last(split(qualification_workflow_path() as String, "/")) { Present { value: f } => f as NonEmptyStr @@ -91,12 +89,6 @@ fn qualification_workflow_file_name() -> NonEmptyStr { } } -// The selected-workflow spelling a restricted runner group carries (owner/repo/path@ref), the same -// form gunbc.runner_group_restriction_observation microvm_shakedown_selected_workflow derives. -fn qualification_selected_workflow(branch: NonEmptyStr) -> NonEmptyStr { - join([gunbc_repository.full_name, "/", qualification_workflow_path() as String, "@refs/heads/", branch as String], "") as NonEmptyStr -} - fn qualification_dispatch_inputs(attempt: NonEmptyStr) -> Map { map_insert(m: empty_map(), key: qualification_attempt_dispatch_input.name, value: attempt as String) } @@ -108,14 +100,15 @@ type QualificationDispatchSubject sole_constructor { host: HostIdentity attempt: NonEmptyStr runner_id: Int - branch: NonEmptyStr - revision: CommitSha + pin: QualificationRefPin floor_job_name: NonEmptyStr } type QualificationSubjectRefusal = SubjectCredentialNotDelivered { delivery: JitCredentialDelivery } | SubjectNoRegistration + | SubjectMintedIntoAnotherGroup { minted_group: Int, pinned_group: Int } + | SubjectPinForAnotherAttempt { pin_attempt: NonEmptyStr, slot_attempt: NonEmptyStr } | SubjectDeliveryForAnotherDispatch | SubjectSlotNotHostUnit | SubjectSlotForAnotherWorkflow { slot_workflow: NonEmptyStr, qualification_workflow: NonEmptyStr } @@ -240,8 +233,7 @@ fn qualification_dispatch_subject_over( delivery: JitCredentialDelivery, workflow: Workflow, floor_job_id: NonEmptyStr, - branch: NonEmptyStr, - revision: CommitSha, + pin: QualificationRefPin, ) -> QualificationSubjectStanding admit_callers: [ decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_dispatch_subject"), @@ -260,7 +252,12 @@ fn qualification_dispatch_subject_over( match registration.slot { MicroVmJitSlot { attempt: _ } => QualificationSubjectRefused { refusal: SubjectSlotNotHostUnit } HostUnitJitSlot { slot: h } => - match qualification_slot_refusal(slot_workflow: h.workflow, branch: branch) { + if (pin.name.attempt as String) != (h.attempt as String) { + QualificationSubjectRefused { refusal: SubjectPinForAnotherAttempt { pin_attempt: pin.name.attempt, slot_attempt: h.attempt } } + } else if dispatch.request.runner_group_id.value != pin.group.group.value { + QualificationSubjectRefused { refusal: SubjectMintedIntoAnotherGroup { minted_group: dispatch.request.runner_group_id.value, pinned_group: pin.group.group.value } } + } else { + match qualification_slot_refusal(slot_workflow: h.workflow, branch: pin.name.branch) { Present { value: r } => QualificationSubjectRefused { refusal: r } Absent => match qualification_floor_admission(workflow: workflow, floor_job_id: floor_job_id) { @@ -272,13 +269,13 @@ fn qualification_dispatch_subject_over( host: h.host.host.host, attempt: h.attempt, runner_id: runner_id, - branch: branch, - revision: revision, + pin: pin, floor_job_name: n, }, } } } + } } } other => QualificationSubjectRefused { refusal: SubjectCredentialNotDelivered { delivery: other } } @@ -301,8 +298,7 @@ fn qualification_dispatch_subject( dispatch: AuthorizedJitMintDispatch, delivery: JitCredentialDelivery, floor_job_id: NonEmptyStr, - branch: NonEmptyStr, - revision: CommitSha, + pin: QualificationRefPin, ) -> QualificationSubjectStanding { if !dispatch_inputs_declare(inputs: fleet_converge_dispatch_inputs, input: qualification_attempt_dispatch_input) { QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } @@ -312,8 +308,7 @@ fn qualification_dispatch_subject( delivery: delivery, workflow: fleet_converge_workflow, floor_job_id: floor_job_id, - branch: branch, - revision: revision, + pin: pin, ) } } @@ -322,7 +317,7 @@ fn qualification_dispatch_effect(subject: QualificationDispatchSubject) -> GitHu DispatchWorkflow { repository: gunbc_repository, workflow: GitHubWorkflowRef { file_name: qualification_workflow_file_name() }, - subject: subject.revision, + subject: subject.pin.revision, } } @@ -422,7 +417,7 @@ fn dispatch_qualification_floor( owner: gunbc_repository.owner as NonEmptyStr, repo: gunbc_repository.name as NonEmptyStr, workflow_id: qualification_workflow_file_name(), - git_ref: subject.branch, + git_ref: subject.pin.name.branch, inputs: qualification_dispatch_inputs(attempt: subject.attempt), ) match qualification_dispatch_standing(performance: answer.performance) { @@ -460,6 +455,7 @@ type QualificationRunRefusal | RunSubjectMismatch { observed_run_id: Int, observed_event: String, observed_path: String?, foreign_jobs: List } | RunAdvancedPastDispatchedAttempt { observed_attempt: Int } | RunRevisionNotPinned { pinned: CommitSha, observed: CommitSha } + | RunOnAnotherBranch { pinned: NonEmptyStr, observed: String? } | RunNotCompleted { status: WorkflowRunStatus } | RunCompletedWithoutConclusion | RunJobsUnreadable { performance: RestExchangePerformance } @@ -523,6 +519,7 @@ type QualificationRunExpectation { registered_runner: NonEmptyStr floor_job_name: NonEmptyStr revision: CommitSha + branch: NonEmptyStr } fn qualification_run_expectation(subject: QualificationDispatchSubject) -> QualificationRunExpectation { @@ -530,7 +527,8 @@ fn qualification_run_expectation(subject: QualificationDispatchSubject) -> Quali registered_runner_id: subject.runner_id, registered_runner: subject.registration.name.value, floor_job_name: subject.floor_job_name, - revision: subject.revision, + revision: subject.pin.revision, + branch: subject.pin.name.branch, } } @@ -567,6 +565,8 @@ fn decide_qualification_run( RunVerdictRefused { refusal: RunSubjectMismatch { observed_run_id: run.id, observed_event: run.event, observed_path: run.path, foreign_jobs: [] } } } else if run.run_attempt != dispatched_run_attempt { RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: run.run_attempt } } + } else if !optional_text_is(o: run.head_branch, expected: subject.branch as String) { + RunVerdictRefused { refusal: RunOnAnotherBranch { pinned: subject.branch, observed: run.head_branch } } } else if (run.head_sha as String) != (subject.revision as String) { RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: subject.revision, observed: run.head_sha } } } else { diff --git a/dag/gunbc/runner/runner_qualification_ref_pin.dag b/dag/gunbc/runner/runner_qualification_ref_pin.dag new file mode 100644 index 00000000000..aca9aa5740c --- /dev/null +++ b/dag/gunbc/runner/runner_qualification_ref_pin.dag @@ -0,0 +1,316 @@ +module gunbc.runner.runner_qualification_ref_pin + +import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } +import std.resources { Network } +import std.decl_ref { decl_ref } +import extdeps.transports.rest { + RestExchangePerformance, RestExchangeSucceeded, RestExchangeStatusRefused, + RestExchangeCommitAmbiguous, RestExchangeUnreached, RestExchangeUndecodable, +} +import gunbc.repository { gunbc_repository } +import gunbc.generated_artifact { artifact_path, FleetConvergeYamlArtifact } +import gunbc.fleet.org_actions_inspection { RunnerGroupListDecode, RunnerGroupListDecoded, RunnerGroupListDecodeRefused } +import gunbc.runner_group_restriction_observation { + MicrovmRunnerGroupStanding, RunnerGroupRestrictedToWorkflow, RunnerGroupRestrictionRefused, RestrictedRunnerGroup, + observe_runner_group_restriction, +} +import gunbc.github_effect_perform { + ControllerInstallationToken, GitRefRead, read_git_ref_with, create_git_ref_with, delete_git_ref_with, + read_runner_groups_with, update_runner_group_workflows_with, +} + +// THE QUALIFICATION REF PIN: ONE CREATE-ONCE BRANCH PER ATTEMPT, AND THE DEDICATED GROUP PINNED TO IT. +// +// WHY A BRANCH. The floor must run the pinned revision and nothing else, and the runner that serves +// it must accept no other code. The JIT slot registers into a runner group restricted to one +// selected workflow, and GitHub selects a dispatched (non-reusable) workflow only at a branch +// ("Pin non-reusable workflows to a branch", extdeps.github.org_actions github.OrgRunnerGroupsRest); +// a SHA or tag pin is not available to it. So the pin is a branch created for this attempt at the +// pinned revision, and the dedicated qualification group is restricted to the qualification workflow +// at THAT branch. A dispatch can then only run the revision the branch was created at, and a run on +// any other ref -- the default branch after a move, another attempt's branch -- does not match the +// group, so the attempt's runner never takes it and collection ends RunNeverReachedAttemptRunner. +// +// CREATE-ONCE. The branch is created by CreateRef, which never moves an existing ref (422 "Reference +// already exists"); a branch already present before creation is refused, never adopted or moved, +// because its revision is not one this attempt pinned. Readback must show the exact revision. +// +// CONFINED BY CONSTRUCTION. Every ref this module touches is a QualificationBranchName, minted only by +// qualification_branch_name, which admits a single path segment under refs/heads/qualification/. +// Every group write goes through a QualificationGroupWriteTarget, minted only from a read whose name +// is qualification_runner_group_name -- the DEDICATED group; no other group can be written. +// +// REMOVED ON EVERY EXIT, by readback (remove_qualification_ref_pin), the same ensure shape as +// gunbc.runner.runner_jit_deregistration: absence is read, never inferred from a delete status. +// The group stays restricted to the deleted branch's path until the next attempt pins it: no run can +// match a branch that no longer exists, so the stale entry admits nothing. +// +// AUTHORIZATION. Pre-approved under the dispatching run's authorization: the operator's 2026-10-03 +// ruling as interpreted by eager-gull-22 on 2026-10-04 to cover this pin as part of registration, +// limited to the dedicated qualification group (gunbc.auth.privileged_effect_census +// runner_lifecycle_ruling_scope_interpretation). The token is the gunbai-ci installation token +// (contents:write for the ref; organization self-hosted-runners write for the group). No workflow +// fires on creating the branch: see test.claim.runner_qualification_ref_pin_witness. + +data qualification_branch_prefix: NonEmptyStr = "qualification/" + +// THE QUALIFICATION WORKFLOW IS THE GENERATED FLEET-CONVERGE WORKFLOW, and the group selects it at +// the pin branch. These live here, beside the group write that consumes them; the dispatch module +// (gunbc.runner.runner_qualification_dispatch) imports them. +fn qualification_workflow_path() -> NonEmptyStr { + artifact_path(a: FleetConvergeYamlArtifact) as NonEmptyStr +} + +// The selected-workflow spelling a restricted runner group carries (owner/repo/path@ref), the same +// form gunbc.runner_group_restriction_observation microvm_shakedown_selected_workflow derives. +fn qualification_selected_workflow(branch: NonEmptyStr) -> NonEmptyStr { + join([gunbc_repository.full_name, "/", qualification_workflow_path() as String, "@refs/heads/", branch as String], "") as NonEmptyStr +} +data qualification_runner_group_name: NonEmptyStr = "runner-qualification" + +// ── THE BRANCH NAME ───────────────────────────────────────────────────────────────────────────── + +type QualificationBranchName sole_constructor { + attempt: NonEmptyStr + branch: NonEmptyStr +} + +type QualificationBranchNameStanding + = QualificationBranchNamed { name: QualificationBranchName } + | QualificationBranchNameRefused { attempt: String, cause: NonEmptyStr } + +// ONE PATH SEGMENT UNDER THE PREFIX. An attempt label carrying a slash, a `..`, whitespace, a ref +// metacharacter or a leading dot would address a ref outside refs/heads/qualification/ or an +// invalid one, so it refuses rather than being escaped. +fn attempt_is_one_ref_segment(attempt: String) -> Bool { + string_length(s: attempt) > 0 + && !starts_with(s: attempt, prefix: ".") + && !string_contains(s: attempt, pattern: "/") + && !string_contains(s: attempt, pattern: "..") + && !string_contains(s: attempt, pattern: " ") + && !string_contains(s: attempt, pattern: "~") + && !string_contains(s: attempt, pattern: "^") + && !string_contains(s: attempt, pattern: ":") + && !string_contains(s: attempt, pattern: "?") + && !string_contains(s: attempt, pattern: "*") + && !string_contains(s: attempt, pattern: "[") + && !string_contains(s: attempt, pattern: "\\") + && !string_contains(s: attempt, pattern: "@{") + && !ends_with(s: attempt, suffix: ".lock") +} + +fn qualification_branch_name(attempt: NonEmptyStr) -> QualificationBranchNameStanding { + if attempt_is_one_ref_segment(attempt: attempt as String) { + QualificationBranchNamed { + name: QualificationBranchName { attempt: attempt, branch: concat(qualification_branch_prefix as String, attempt as String) as NonEmptyStr }, + } + } else { + QualificationBranchNameRefused { attempt: attempt as String, cause: "the attempt label is not one ref segment, so the branch would leave refs/heads/qualification/" as NonEmptyStr } + } +} + +fn branch_full_ref(name: QualificationBranchName) -> NonEmptyStr { + concat("refs/heads/", name.branch as String) as NonEmptyStr +} + +fn branch_api_ref(name: QualificationBranchName) -> NonEmptyStr { + concat("heads/", name.branch as String) as NonEmptyStr +} + +// ── THE DEDICATED GROUP ───────────────────────────────────────────────────────────────────────── + +type QualificationGroupWriteTarget sole_constructor { + group_id: Int +} + +type QualificationGroupTargetStanding + = QualificationGroupTargeted { target: QualificationGroupWriteTarget } + | QualificationGroupNotTargetable { cause: NonEmptyStr } + +// The only group that may be written is the one whose name is the dedicated qualification group's, +// found exactly once in a complete read. Any other name -- the shakedown group, Default -- refuses. +fn qualification_group_target(requested: NonEmptyStr, listed: RunnerGroupListDecode) -> QualificationGroupTargetStanding { + if (requested as String) != (qualification_runner_group_name as String) { + QualificationGroupNotTargetable { cause: concat("only the dedicated qualification group may be written; refused: ", requested as String) as NonEmptyStr } + } else { + match listed { + RunnerGroupListDecodeRefused { reason: r } => QualificationGroupNotTargetable { cause: r } + RunnerGroupListDecoded { groups: gs } => { + let rows = gs |> filter(g => (g.name as String) == (requested as String)) + match first(rows) { + Absent => QualificationGroupNotTargetable { cause: "the dedicated qualification runner group does not exist" as NonEmptyStr } + Present { value: row } => + if count(rows) != 1 { + QualificationGroupNotTargetable { cause: "the dedicated qualification runner group name is duplicated" as NonEmptyStr } + } else { + QualificationGroupTargeted { target: QualificationGroupWriteTarget { group_id: row.id } } + } + } + } + } + } +} + +// ── THE PIN ───────────────────────────────────────────────────────────────────────────────────── + +type QualificationRefPin sole_constructor { + name: QualificationBranchName + revision: CommitSha + group: RestrictedRunnerGroup +} + +type QualificationRefPinRefusal + = PinBranchNameRefused { cause: NonEmptyStr } + | PinBranchPreexisting { observed_sha: String } + | PinBranchReadUnreadable { performance: RestExchangePerformance } + | PinBranchCreateRefused { status: Int, body: String } + | PinBranchCreateAmbiguous { detail: NonEmptyStr } + | PinBranchReadbackMismatch { pinned: CommitSha, observed: String } + | PinGroupNotTargetable { cause: NonEmptyStr } + | PinGroupWriteNotSucceeded { performance: RestExchangePerformance } + | PinGroupReadbackRefused { standing: MicrovmRunnerGroupStanding } + +type QualificationRefPinStanding + = QualificationRefPinned { pin: QualificationRefPin } + | QualificationRefPinRefused { attempt: NonEmptyStr, refusal: QualificationRefPinRefusal } + +fn branch_absent(performance: RestExchangePerformance) -> Bool { + match performance { + RestExchangeStatusRefused { status: s, body: _ } => (s as Int) == 404 + _ => false + } +} + +// THE CREATE'S STANDING, decided from the mutation classifier: only a created ref proceeds to +// readback; a 422 is the create-once refusal (the ref exists); an ambiguous answer is its own arm -- +// the ref may exist, so removal is still owed. +fn branch_create_refusal(performance: RestExchangePerformance) -> QualificationRefPinRefusal? { + match performance { + RestExchangeSucceeded => none + RestExchangeStatusRefused { status: s, body: b } => + if (s as Int) == 422 { Present { value: PinBranchPreexisting { observed_sha: "(422 Reference already exists)" } } } + else { Present { value: PinBranchCreateRefused { status: s as Int, body: b } } } + RestExchangeCommitAmbiguous { detail: d } => Present { value: PinBranchCreateAmbiguous { detail: d } } + RestExchangeUnreached { cause: c } => Present { value: PinBranchCreateAmbiguous { detail: c } } + RestExchangeUndecodable { status: _, cause: c } => Present { value: PinBranchCreateAmbiguous { detail: c } } + } +} + +// THE PIN'S ONLY MINT, over values read back. Callers are admitted by name: the effectful ensure, and +// the witness claims that supply readbacks at this interface and return Bool. +fn qualification_ref_pin_of( + name: QualificationBranchName, + revision: CommitSha, + readback: RestExchangePerformance, + readback_sha: String, + group: MicrovmRunnerGroupStanding, +) -> QualificationRefPinStanding + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), + decl_ref(module_path: "test.claim.runner_qualification_ref_pin_witness", decl_name: "the_pin_is_minted_only_from_the_exact_revision_and_the_pinned_group"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), + ] +{ + match readback { + RestExchangeSucceeded => + if readback_sha != (revision as String) { + QualificationRefPinRefused { attempt: name.attempt, refusal: PinBranchReadbackMismatch { pinned: revision, observed: readback_sha } } + } else { + match group { + RunnerGroupRestrictedToWorkflow { restriction: r } => + QualificationRefPinned { pin: QualificationRefPin { name: name, revision: revision, group: r } } + other => QualificationRefPinRefused { attempt: name.attempt, refusal: PinGroupReadbackRefused { standing: other } } + } + } + other => QualificationRefPinRefused { attempt: name.attempt, refusal: PinBranchReadUnreadable { performance: other } } + } +} + +fn pin_refused(attempt: NonEmptyStr, refusal: QualificationRefPinRefusal) -> QualificationRefPinStanding { + QualificationRefPinRefused { attempt: attempt, refusal: refusal } +} + +fn ensure_qualification_ref_pin( + token: ControllerInstallationToken, + organization: NonEmptyStr, + attempt: NonEmptyStr, + revision: CommitSha, +) -> QualificationRefPinStanding uses net: Network { + let owner = gunbc_repository.owner as NonEmptyStr + let repo = gunbc_repository.name as NonEmptyStr + match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: c } => pin_refused(attempt: attempt, refusal: PinBranchNameRefused { cause: c }) + QualificationBranchNamed { name: name } => { + let before = read_git_ref_with(token: token, owner: owner, repo: repo, ref: branch_api_ref(name: name)) + match before.performance { + RestExchangeSucceeded => pin_refused(attempt: attempt, refusal: PinBranchPreexisting { observed_sha: before.reference.object.sha }) + other => + if !branch_absent(performance: other) { + pin_refused(attempt: attempt, refusal: PinBranchReadUnreadable { performance: other }) + } else { + match branch_create_refusal(performance: create_git_ref_with(token: token, owner: owner, repo: repo, full_ref: branch_full_ref(name: name), sha: revision as String)) { + Present { value: r } => pin_refused(attempt: attempt, refusal: r) + Absent => { + let after = read_git_ref_with(token: token, owner: owner, repo: repo, ref: branch_api_ref(name: name)) + let selected = qualification_selected_workflow(branch: name.branch) + match qualification_group_target(requested: qualification_runner_group_name, listed: read_runner_groups_with(token: token, organization: organization)) { + QualificationGroupNotTargetable { cause: c } => pin_refused(attempt: attempt, refusal: PinGroupNotTargetable { cause: c }) + QualificationGroupTargeted { target: t } => + match update_runner_group_workflows_with(token: token, organization: organization, runner_group_id: t.group_id, selected_workflows: [selected as String]) { + RestExchangeSucceeded => + qualification_ref_pin_of( + name: name, + revision: revision, + readback: after.performance, + readback_sha: after.reference.object.sha, + group: observe_runner_group_restriction(name: qualification_runner_group_name, selected_workflow: selected, listed: read_runner_groups_with(token: token, organization: organization)), + ) + other_write => pin_refused(attempt: attempt, refusal: PinGroupWriteNotSucceeded { performance: other_write }) + } + } + } + } + } + } + } + } +} + +// ── REMOVAL, ON EVERY EXIT ────────────────────────────────────────────────────────────────────── + +type QualificationRefPinRemoval + = QualificationBranchRemoved { name: QualificationBranchName, deleted: Bool } + | QualificationBranchRemovalRefused { name: QualificationBranchName, cause: QualificationBranchRemovalRefusal } + +type QualificationBranchRemovalRefusal + = BranchStillPresent { observed_sha: String, delete: RestExchangePerformance } + | BranchReadUnreadable { performance: RestExchangePerformance } + +// THE VERDICT READS THE READBACK AND NOTHING ELSE: a branch is removed when a read finds it absent. +fn classify_branch_removal(name: QualificationBranchName, readback: RestExchangePerformance, readback_sha: String, deleted: Bool, delete: RestExchangePerformance) -> QualificationRefPinRemoval { + match readback { + RestExchangeSucceeded => QualificationBranchRemovalRefused { name: name, cause: BranchStillPresent { observed_sha: readback_sha, delete: delete } } + other => + if branch_absent(performance: other) { + QualificationBranchRemoved { name: name, deleted: deleted } + } else { + QualificationBranchRemovalRefused { name: name, cause: BranchReadUnreadable { performance: other } } + } + } +} + +fn remove_qualification_ref_pin(token: ControllerInstallationToken, name: QualificationBranchName) -> QualificationRefPinRemoval uses net: Network { + let owner = gunbc_repository.owner as NonEmptyStr + let repo = gunbc_repository.name as NonEmptyStr + let before = read_git_ref_with(token: token, owner: owner, repo: repo, ref: branch_api_ref(name: name)) + match before.performance { + RestExchangeSucceeded => { + let delete = delete_git_ref_with(token: token, owner: owner, repo: repo, ref: branch_api_ref(name: name)) + let after = read_git_ref_with(token: token, owner: owner, repo: repo, ref: branch_api_ref(name: name)) + classify_branch_removal(name: name, readback: after.performance, readback_sha: after.reference.object.sha, deleted: true, delete: delete) + } + other => classify_branch_removal(name: name, readback: other, readback_sha: "", deleted: false, delete: other) + } +} diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index 410b7561122..ef28bea5a39 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -239,7 +239,7 @@ data mtcollins1_registration_leg: RouteLegStanding = LegAuthorityImplemented { data mtcollins1_dispatch_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), - wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a floor job in fleet_converge_workflow whose runs-on is exactly the qualification_attempt_dispatch_input it declares, and a route fold that takes the slot host's unit hold, mints the registration, builds qualification_dispatch_subject from it, calls dispatch_qualification_floor and then collect_qualification_run over the DispatchedQualificationRun it returns, reconciling a commit-ambiguous dispatch before any retry; SUFFICIENT FOR the floor run of each attempt being served only by the runner minted for that attempt and read back as collected or as a typed refusal" as NonEmptyStr, + wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a floor job in fleet_converge_workflow whose runs-on is exactly the qualification_attempt_dispatch_input it declares, and a route fold that takes the slot host's unit hold, ensures the per-attempt pin (gunbc.runner.runner_qualification_ref_pin ensure_qualification_ref_pin: branch qualification/ at the revision, the dedicated group pinned to it), mints the registration into that group, builds qualification_dispatch_subject from the pin, calls dispatch_qualification_floor and then collect_qualification_run, reconciling a commit-ambiguous dispatch before any retry, and removes the pin on every exit; SUFFICIENT FOR the floor run of each attempt being served only by the runner minted for that attempt at the pinned revision and read back as collected or as a typed refusal" as NonEmptyStr, } data mtcollins1_deregistration_leg: RouteLegStanding = LegAuthorityImplemented { diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 243474165d4..49952efca4e 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -391,12 +391,12 @@ test fn every_root_of_the_standing_ruling_is_a_conforming_census_row() -> Bool { } // THE 2026-10-03 PRE-APPROVAL IS A SELECTION, NOT A NEW VOCABULARY. JIT registration, -// deregistration and the qualification dispatch are each a +// deregistration, the qualification dispatch and its per-attempt branch pin are each a // recurring automated effect under the dispatching run's federated identity, so the selection must // answer the federated scoped grant and the row must conform; rowing it as an approval capability or // a per-registration ntfy approval turns this red. test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federated_grant_and_conform() -> Bool { - ["dispatch_jit_mint", "ensure_jit_runner_deregistered", "dispatch_qualification_floor"] |> all(name => + ["dispatch_jit_mint", "ensure_jit_runner_deregistered", "dispatch_qualification_floor", "ensure_qualification_ref_pin", "remove_qualification_ref_pin"] |> all(name => any(census_verdicts(), v => (v.site.decl_name as String) == name && selected_federated(s: v.selection) diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 4414888399c..4a6550c586d 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -23,9 +23,12 @@ import test.claim.github_app_registry { bot_installation_id, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome, } import gunbc.auth.github_apps { gunbai_bot_declared } +import gunbc.runner.runner_qualification_ref_pin { + qualification_selected_workflow, qualification_workflow_path, qualification_branch_name, QualificationBranchNamed, QualificationBranchNameRefused, + qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, +} import gunbc.runner.runner_qualification_dispatch { - qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_selected_workflow, - qualification_workflow_path, qualification_dispatch_effect, + qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_dispatch_effect, QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, SubjectFloorNameNotUnique, SubjectDeliveryForAnotherDispatch, @@ -37,7 +40,7 @@ import gunbc.runner.runner_qualification_dispatch { DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, hold_covers_slot_host, decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, - RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunNotCompleted, + RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunOnAnotherBranch, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, RunNeverReachedAttemptRunner, RunRunnerNameDisagrees, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, } @@ -45,7 +48,7 @@ import gunbc.runner.runner_qualification_dispatch { data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly data attempt: NonEmptyStr = "q-1" -data branch: NonEmptyStr = "main" +data pin_branch: NonEmptyStr = "qualification/q-1" data revision: CommitSha = "1111111111111111111111111111111111111111" data floor_job_id: NonEmptyStr = "qualification-floor" data registered_runner: NonEmptyStr = "gunbc-qualification-fixture-q-1.service" @@ -100,18 +103,27 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { match first(managed_hosts()) { Present { value: h } => match dispatch_jit_mint( - binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) }, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, - runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), ) { - JitMintDispatchAuthorized { dispatch: d } => { + JitMintDispatchAuthorized { dispatch: d } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestExchangeSucceeded, readback_sha: revision as String, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + { let delivered = receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome) (match qualification_dispatch_subject_over( dispatch: d, delivery: delivered, - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, ) { QualificationSubjectAdmitted { subject: s } => { let expected = qualification_run_expectation(subject: s) @@ -134,6 +146,8 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { } QualificationSubjectRefused { refusal: _ } => false }) + } + } } JitMintDispatchRefused { refusal: _ } => false } @@ -149,24 +163,34 @@ test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_a match first(managed_hosts()) { Present { value: h } => match dispatch_jit_mint( - binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) }, + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) }, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, - runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: branch)), + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), ) { - JitMintDispatchAuthorized { dispatch: d } => + JitMintDispatchAuthorized { dispatch: d } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestExchangeSucceeded, readback_sha: revision as String, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + match qualification_dispatch_subject( dispatch: d, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome), floor_job_id: floor_job_id, - branch: branch, - revision: revision, + pin: p, ) { QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "runner_attempt_label" _ => false } + } + } JitMintDispatchRefused { refusal: _ } => false } Absent => false @@ -177,7 +201,7 @@ test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_a // The subject mint above composes exactly these two checks; these claims supply their inputs. fn slot_refused_as_another_workflow(slot_workflow: NonEmptyStr) -> Bool { - match qualification_slot_refusal(slot_workflow: slot_workflow, branch: branch) { + match qualification_slot_refusal(slot_workflow: slot_workflow, branch: pin_branch) { Present { value: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) _ => false } @@ -188,7 +212,7 @@ fn slot_refused_as_another_workflow(slot_workflow: NonEmptyStr) -> Bool { test fn a_slot_for_another_workflow_or_branch_cannot_become_a_subject() -> Bool { slot_refused_as_another_workflow(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") && slot_refused_as_another_workflow(slot_workflow: qualification_selected_workflow(branch: "feature")) - && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: branch), branch: branch) { Absent => true Present { value: _ } => false }) + && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: pin_branch), branch: branch) { Absent => true Present { value: _ } => false }) } fn runs_on_refused(runs_on: RunnerSpec) -> Bool { @@ -281,22 +305,33 @@ test fn the_dispatch_authority_must_name_the_tokens_installation() -> Bool { test fn a_delivery_from_another_dispatch_on_the_same_slot_refuses() -> Bool { match first(managed_hosts()) { Present { value: h } => { - let binding = HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: branch)) } - let group = group_restricted_to(workflow: qualification_selected_workflow(branch: branch)) + let binding = HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) } + let group = group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)) match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { - JitMintDispatchAuthorized { dispatch: a } => + JitMintDispatchAuthorized { dispatch: a } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestExchangeSucceeded, readback_sha: revision as String, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: GitHubOrganizationRef { login: "another-org" }, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { JitMintDispatchAuthorized { dispatch: b } => a.slot == b.slot && (match qualification_dispatch_subject_over( dispatch: a, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: b }, outcome: minted_outcome), - workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, branch: branch, revision: revision, + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, ) { QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true _ => false }) JitMintDispatchRefused { refusal: _ } => false } + } + } JitMintDispatchRefused { refusal: _ } => false } } @@ -334,15 +369,20 @@ data expect: QualificationRunExpectation = QualificationRunExpectation { registered_runner: registered_runner, floor_job_name: "qualification floor", revision: revision, + branch: pin_branch, } fn run(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool) -> WorkflowRun { + run_on(id: id, attempt_n: attempt_n, event: event, path: path, sha: sha, completed: completed, concluded: concluded, on_branch: pin_branch as String) +} + +fn run_on(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool, on_branch: String) -> WorkflowRun { WorkflowRun { id: id, name: none, path: Present { value: path }, head_sha: sha, - head_branch: Present { value: branch as String }, + head_branch: Present { value: on_branch }, event: event, status: if completed { Completed } else { InProgress }, conclusion: if concluded { Present { value: Failure } } else { none }, @@ -435,9 +475,10 @@ test fn the_collection_joins_the_floor_job_to_the_registered_runner() -> Bool { }) } -// THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT. REDs: the run re-run to attempt 2 (whose floor -// job carries the same labels and runner); a job of attempt 2 in the listing; another run id; another -// event; another workflow path. +// THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT ON THE PIN BRANCH. REDs: the run re-run to +// attempt 2 (whose floor job carries the same labels and runner); a job of attempt 2 in the listing; +// another run id; another event; another workflow path; a run on another branch (the default branch +// after a move, which the pinned group would also refuse to hand our runner). test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { let floor = jobs(js: [floor_on(runner_id: registered_runner_id)]) let path = qualification_workflow_path() as String @@ -449,6 +490,10 @@ test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { && mismatch(v: decide_run(r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) + && (match decide_run(r: run_on(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true, on_branch: "main"), js: floor) { + RunVerdictRefused { refusal: RunOnAnotherBranch { pinned: _, observed: _ } } => true + _ => false + }) } test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { diff --git a/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag b/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag new file mode 100644 index 00000000000..5500bf1a4c5 --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag @@ -0,0 +1,164 @@ +module test.claim.runner_qualification_ref_pin_witness + +import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.transports.rest { + RestExchangePerformance, RestExchangeSucceeded, RestExchangeStatusRefused, RestExchangeCommitAmbiguous, +} +import extdeps.github.actions { + WorkflowTrigger, Push, PullRequest, Schedule, WorkflowDispatch, WorkflowCall, WorkflowRunCompleted, MergeGroup, +} +import gunbc.fleet.org_actions_inspection { RunnerGroupListDecoded, RunnerGroupIdentity } +import gunbc.runner_group_restriction_observation { RunnerGroupRestrictionRefused, RunnerGroupAbsent } +import gunbc.fleet_desired_admission_workflow { fleet_desired_admission_workflow } +import test.claim.github_app_registry { group_restricted_to } +import gunbc.runner.runner_qualification_ref_pin { + qualification_branch_name, QualificationBranchNamed, QualificationBranchNameRefused, branch_full_ref, + qualification_selected_workflow, qualification_runner_group_name, + qualification_group_target, QualificationGroupTargeted, QualificationGroupNotTargetable, + branch_create_refusal, PinBranchPreexisting, PinBranchCreateRefused, PinBranchCreateAmbiguous, + qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, PinBranchReadbackMismatch, PinGroupReadbackRefused, + classify_branch_removal, QualificationBranchRemoved, QualificationBranchRemovalRefused, BranchStillPresent, BranchReadUnreadable, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data revision: CommitSha = "1111111111111111111111111111111111111111" + +fn named_ok(attempt: NonEmptyStr) -> Bool { + match qualification_branch_name(attempt: attempt) { + QualificationBranchNamed { name: n } => (n.branch as String) == concat("qualification/", attempt as String) && (branch_full_ref(name: n) as String) == concat("refs/heads/qualification/", attempt as String) + QualificationBranchNameRefused { attempt: _, cause: _ } => false + } +} + +fn named_refused(attempt: NonEmptyStr) -> Bool { + match qualification_branch_name(attempt: attempt) { + QualificationBranchNamed { name: _ } => false + QualificationBranchNameRefused { attempt: _, cause: _ } => true + } +} + +// RED: OUT OF PREFIX. A label carrying a slash, a parent step, a ref metacharacter, a leading dot or +// a .lock suffix cannot name refs/heads/qualification/ and refuses rather than escaping. +// Positive control: an ordinary attempt label names exactly refs/heads/qualification/. +test fn a_ref_outside_refs_heads_qualification_cannot_be_named() -> Bool { + named_ok(attempt: "q-1") + && named_refused(attempt: "../main") + && named_refused(attempt: "q/1") + && named_refused(attempt: "main~1") + && named_refused(attempt: ".hidden") + && named_refused(attempt: "q-1.lock") + && named_refused(attempt: "q 1") +} + +// RED: CREATE-ONCE. GitHub's 422 on CreateRef (the ref exists) is the pre-existing refusal, never an +// adoption; a lost or undecodable answer is ambiguous (removal still owed); only a created ref +// proceeds to readback. +test fn a_preexisting_branch_refuses_and_a_lost_create_is_ambiguous() -> Bool { + (match branch_create_refusal(performance: RestExchangeStatusRefused { status: 422, body: "Reference already exists" }) { + Present { value: PinBranchPreexisting { observed_sha: _ } } => true + _ => false + }) + && (match branch_create_refusal(performance: RestExchangeStatusRefused { status: 403, body: "fixture" }) { + Present { value: PinBranchCreateRefused { status: s, body: _ } } => s == 403 + _ => false + }) + && (match branch_create_refusal(performance: RestExchangeCommitAmbiguous { detail: "fixture: 502 after the write" }) { + Present { value: PinBranchCreateAmbiguous { detail: _ } } => true + _ => false + }) + && (match branch_create_refusal(performance: RestExchangeSucceeded) { Absent => true Present { value: _ } => false }) +} + +fn group_row(id: Int, name: NonEmptyStr) -> RunnerGroupIdentity { + RunnerGroupIdentity { id: id, name: name, allows_public_repositories: false, restricted_to_workflows: true, selected_workflows: [] } +} + +// RED: A WRITE TO ANY OTHER GROUP. Only the dedicated qualification group can become a write target: +// the shakedown group refuses even when it is present, as do an absent or duplicated dedicated group. +// Positive control: the dedicated group found once. +test fn a_restriction_write_to_any_other_group_refuses() -> Bool { + let listed = RunnerGroupListDecoded { groups: [group_row(id: 1, name: "microvm-shakedown"), group_row(id: 3, name: qualification_runner_group_name)] } + (match qualification_group_target(requested: "microvm-shakedown", listed: listed) { QualificationGroupNotTargetable { cause: _ } => true _ => false }) + && (match qualification_group_target(requested: "Default", listed: listed) { QualificationGroupNotTargetable { cause: _ } => true _ => false }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: RunnerGroupListDecoded { groups: [group_row(id: 1, name: "microvm-shakedown")] }) { + QualificationGroupNotTargetable { cause: _ } => true + _ => false + }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: RunnerGroupListDecoded { groups: [group_row(id: 3, name: qualification_runner_group_name), group_row(id: 4, name: qualification_runner_group_name)] }) { + QualificationGroupNotTargetable { cause: _ } => true + _ => false + }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: listed) { + QualificationGroupTargeted { target: t } => t.group_id == 3 + QualificationGroupNotTargetable { cause: _ } => false + }) +} + +// THE PIN IS MINTED ONLY FROM A READBACK AT THE EXACT REVISION AND A GROUP OBSERVED PINNED TO THE +// BRANCH. REDs: the branch read back at another commit; the group not observed restricted to the +// qualification workflow at the branch. Positive control: both hold. +test fn the_pin_is_minted_only_from_the_exact_revision_and_the_pinned_group() -> Bool { + match qualification_branch_name(attempt: "q-1") { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: n } => { + let pinned_group = group_restricted_to(workflow: qualification_selected_workflow(branch: n.branch)) + (match qualification_ref_pin_of(name: n, revision: revision, readback: RestExchangeSucceeded, readback_sha: revision as String, group: pinned_group) { + QualificationRefPinned { pin: p } => (p.name.branch as String) == "qualification/q-1" && (p.revision as String) == (revision as String) && p.group.group.value == 3 + QualificationRefPinRefused { attempt: _, refusal: _ } => false + }) + && (match qualification_ref_pin_of(name: n, revision: revision, readback: RestExchangeSucceeded, readback_sha: "2222222222222222222222222222222222222222", group: pinned_group) { + QualificationRefPinRefused { attempt: _, refusal: PinBranchReadbackMismatch { pinned: _, observed: _ } } => true + _ => false + }) + && (match qualification_ref_pin_of(name: n, revision: revision, readback: RestExchangeSucceeded, readback_sha: revision as String, group: RunnerGroupRestrictionRefused { name: qualification_runner_group_name, cause: RunnerGroupAbsent }) { + QualificationRefPinRefused { attempt: _, refusal: PinGroupReadbackRefused { standing: _ } } => true + _ => false + }) + } + } +} + +// REMOVAL IS A READBACK. A read finding the branch absent (404) is removed; a branch still present +// after the delete refuses BranchStillPresent; an unreadable read refuses rather than reporting it gone. +test fn the_branch_is_removed_only_when_a_read_finds_it_absent() -> Bool { + match qualification_branch_name(attempt: "q-1") { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: n } => + (match classify_branch_removal(name: n, readback: RestExchangeStatusRefused { status: 404, body: "Not Found" }, readback_sha: "", deleted: true, delete: RestExchangeSucceeded) { + QualificationBranchRemoved { name: _, deleted: d } => d + _ => false + }) + && (match classify_branch_removal(name: n, readback: RestExchangeSucceeded, readback_sha: revision as String, deleted: true, delete: RestExchangeSucceeded) { + QualificationBranchRemovalRefused { name: _, cause: BranchStillPresent { observed_sha: _, delete: _ } } => true + _ => false + }) + && (match classify_branch_removal(name: n, readback: RestExchangeStatusRefused { status: 500, body: "fixture" }, readback_sha: "", deleted: true, delete: RestExchangeSucceeded) { + QualificationBranchRemovalRefused { name: _, cause: BranchReadUnreadable { performance: _ } } => true + _ => false + }) + } +} + +// CREATING A QUALIFICATION BRANCH FIRES NO WORKFLOW. WorkflowTrigger has no `create` variant, so no +// generated workflow can trigger on branch creation by construction; the one generated workflow with a +// push trigger is gunbc.fleet_desired_admission_workflow, whose branch filter must not admit +// qualification/. A filter containing any glob is read as possibly matching (refused here +// rather than evaluated), so a widened filter turns this red. +fn push_trigger_could_fire_on(trigger: WorkflowTrigger, branch: String) -> Bool { + match trigger { + Push { branches: bs, paths: _ } => count(bs) == 0 || (bs |> any(b => b == branch || string_contains(s: b, pattern: "*"))) + PullRequest { branches: _, types: _ } => false + Schedule { cron: _ } => false + WorkflowDispatch { inputs: _ } => false + WorkflowCall { inputs: _ } => false + WorkflowRunCompleted { workflows: _, branches: _ } => false + MergeGroup => false + } +} + +test fn creating_a_qualification_branch_fires_no_generated_push_workflow() -> Bool { + !(fleet_desired_admission_workflow.on |> any(t => push_trigger_could_fire_on(trigger: t, branch: "qualification/q-1"))) + && push_trigger_could_fire_on(trigger: Push { branches: ["qualification/**"], paths: [] }, branch: "qualification/q-1") +} From 6ce55dd73f7f145d178f847c00fd2187b9feeaf3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 14:22:16 +0000 Subject: [PATCH 28/44] leg 3 item 1: the delivery carries its mint's plain JitMintIdentity (not the sealed dispatch); fixtures build it as a value The sealed fixture mint (test.claim.github_app_registry dispatched) admits named callers only, so carrying the sealed dispatch on the open delivery variant would make every hand-built delivery a proxy. The identity is what the subject's comparison needs. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_attempt_launch.dag | 2 +- dag/gunbc/runner/runner_jit_perform.dag | 29 +++++++++++++++++-- .../runner/runner_microvm_slot_controller.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 12 ++++---- .../github_app_registry_witness_test.dag | 2 +- .../github_effect_perform_witness_test.dag | 2 +- .../runner_attempt_launch_witness_test.dag | 21 +++++--------- ...r_microvm_slot_controller_witness_test.dag | 11 +++---- 8 files changed, 49 insertions(+), 32 deletions(-) diff --git a/dag/gunbc/runner/runner_attempt_launch.dag b/dag/gunbc/runner/runner_attempt_launch.dag index b168ee16231..c4c7f8573b9 100644 --- a/dag/gunbc/runner/runner_attempt_launch.dag +++ b/dag/gunbc/runner/runner_attempt_launch.dag @@ -207,7 +207,7 @@ fn admit_jit_credential(attempt: MicroVmAttempt, mint: JitMintPerformance) -> Ji match mint { JitMintReceived { delivery: delivery } => match delivery { - JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id, dispatch: _ } => + JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id, minted_for: _ } => if !host_mint_admission_is_required() { JitCredentialRefused { refusal: JitMintAdmissionAxisNotRequired } } else if bound != MicroVmJitSlot { attempt: attempt } { diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 7e097ad43e5..f52e5e640c2 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -114,6 +114,29 @@ type AuthorizedJitMintDispatch sole_constructor { request: JitRunnerConfigRequest } +// WHICH MINT A DELIVERY CAME FROM, AS A PLAIN VALUE. A delivery agrees with another on slot, +// credential and runner id when two authorized mints name the same slot; what tells them apart is +// the App, installation and organization the mint was performed under and the runner name it fixed. +// receive_jit_mint stamps that identity from the authorized dispatch, and a consumer that holds the +// sealed dispatch (gunbc.runner.runner_qualification_dispatch) compares the two. It is a value, not +// the sealed dispatch itself, because the delivery variant is not sealed: carrying the dispatch would +// hand every holder of a delivery a sealed value, and the identity is what the comparison needs. +type JitMintIdentity { + app: DeclaredGitHubApp + installation: GitHubAppInstallationId + organization: GitHubOrganizationRef + runner_name: NonEmptyStr +} + +fn jit_mint_identity_of(dispatch: AuthorizedJitMintDispatch) -> JitMintIdentity { + JitMintIdentity { + app: dispatch.app, + installation: dispatch.installation, + organization: dispatch.organization, + runner_name: dispatch.request.name.value, + } +} + type AuthorityInstallation { app: DeclaredGitHubApp installation: GitHubAppInstallationId @@ -209,7 +232,7 @@ type JitCredentialDelivery slot: JitSlot credential: EncodedJitConfig runner_id: Int - dispatch: AuthorizedJitMintDispatch + minted_for: JitMintIdentity } | JitCredentialNotDispatched { refusal: JitDispatchRefusal } | JitCredentialMintNotCreated { outcome: JitConfigMintOutcome } @@ -228,7 +251,7 @@ fn receive_jit_mint( slot: slot, credential: config, runner_id: runner_id, - dispatch: authorized, + minted_for: jit_mint_identity_of(dispatch: authorized), } JitConfigMintRefused { status: s, reason: reason } => JitCredentialMintNotCreated { @@ -253,7 +276,7 @@ fn jit_credential_delivered(delivery: JitCredentialDelivery) -> Bool { slot: _, credential: _, runner_id: _, - dispatch: _, + minted_for: _, } => true JitCredentialNotDispatched { refusal: _ } => false JitCredentialMintNotCreated { outcome: _ } => false diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index 2548d2711a8..b991328a0f5 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -514,7 +514,7 @@ fn minted_registration(mint: JitMintPerformance) -> Int? { match mint { JitMintReceived { delivery: d } => match d { - JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id, dispatch: _ } => Present { value: id } + JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id, minted_for: _ } => Present { value: id } JitCredentialNotDispatched { refusal: _ } => none JitCredentialMintNotCreated { outcome: _ } => none } diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index e4e1de39869..ea55b5cf991 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -31,7 +31,7 @@ import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, } import gunbc.runner_jit_mint { HostUnitJitSlot, MicroVmJitSlot } -import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt, authority_installation } +import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt, authority_installation, jit_mint_identity_of } import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } import gunbc.managed_host_unit_hold { UnitHoldProof } import gunbc.github_effect_perform { @@ -53,8 +53,10 @@ import gunbc.runner.runner_qualification_ref_pin { // THE SUBJECT IS SEALED AND ITS SCOPE IS FIXED BY CONSTRUCTION. QualificationDispatchSubject is // minted only by qualification_dispatch_subject_over, and only from: // - the sealed AuthorizedJitMintDispatch (gunbc.runner.runner_jit_perform), whose only mint is -// dispatch_jit_mint's authorized arm, its registration (jit_registration_of), and the credential -// delivered for that same slot -- so +// dispatch_jit_mint's authorized arm, its registration (jit_registration_of), and a credential +// delivered by THAT mint -- the delivery's JitMintIdentity (App, installation, organization, +// runner name) and slot must equal the dispatch's, so a same-slot delivery from another mint +// refuses -- so // the attempt is the slot's own and the runner the collection joins on is that registration's; // - the qualification workflow, which is not a parameter of the request: its path is the generated // fleet-converge artifact (gunbc.generated_artifact FleetConvergeYamlArtifact) and production @@ -245,8 +247,8 @@ fn qualification_dispatch_subject_over( Absent => QualificationSubjectRefused { refusal: SubjectNoRegistration } Present { value: registration } => match delivery { - JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: runner_id, dispatch: delivered_by } => - if delivered_by != dispatch { + JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: runner_id, minted_for: minted_for } => + if minted_for != jit_mint_identity_of(dispatch: dispatch) || delivered_slot != dispatch.slot { QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } } else { match registration.slot { diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index c61f32d8efb..a2b54a209b5 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -1310,7 +1310,7 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool slot: a, credential: c, runner_id: id, - dispatch: _, + minted_for: _, } => match dispatched(owned: true, perms: runner_grant) { JitMintDispatchAuthorized { dispatch: d } => a == d.slot diff --git a/dag/test/claim/github_effect_perform_witness_test.dag b/dag/test/claim/github_effect_perform_witness_test.dag index f21dcf18a2e..b3eb77d45d0 100644 --- a/dag/test/claim/github_effect_perform_witness_test.dag +++ b/dag/test/claim/github_effect_perform_witness_test.dag @@ -102,7 +102,7 @@ data created_body: JitConfigCreatedBody = JitConfigCreatedBody { test fn witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered() -> Bool { let d = dispatched(owned: true, perms: runner_grant) let delivered = match jit_mint_performance_of_generate(dispatch: d, outcome: RestOk, created: created_body) { - JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id, dispatch: _ } } => + JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id, minted_for: _ } } => (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" && id == 4242 _ => false } diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index 2cf9010ce42..bb2b3d05e93 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -1,7 +1,8 @@ module test.claim.runner_attempt_launch_witness -import test.claim.github_app_registry { dispatched, runner_grant } -import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused } +import gunbc.runner.runner_jit_perform { JitMintIdentity } +import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_declared_installation_id } +import extdeps.github.effect { GitHubOrganizationRef } import gunbc.runner_attempt_launch { GitHubLaunchPayload, launch_payload_device } import std.types { String, Bool, Int, List, NonEmptyStr } @@ -137,20 +138,14 @@ fn minted_credential(encoded: String) -> List { // something that is not a whole credential, handed on as if it were. data short_blob: String = "ZmFrZS1qaXQtY29uZmln" -// THE DISPATCH ON A HAND-BUILT DELIVERY. JitCredentialBoundToAttempt carries the sealed authorized -// dispatch that produced it (gunbc.runner.runner_qualification_dispatch joins on it). This fixture -// builds a delivery directly to drive the launch decision, which reads only the slot, the credential -// and the runner id, so it takes the dispatch from test.claim.github_app_registry dispatched -- the -// one admitted fixture mint -- rather than forging one. +// THE MINT IDENTITY ON A HAND-BUILT DELIVERY. A delivery names the mint it came from +// (JitMintIdentity); the launch decision this fixture drives reads only the slot, the credential and +// the runner id, so the identity here is a plain fixture value. fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance { match minted_credential(encoded: encoded) { Cons { head: c, tail: _ } => - match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { dispatch: d } => - JitMintReceived { - delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id, dispatch: d }, - } - JitMintDispatchRefused { refusal: _ } => JitMintAppJwtUnsigned { detail: "fixture: the admitted dispatch fixture was refused" } + JitMintReceived { + delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id, minted_for: JitMintIdentity { app: gunbai_ci_declared, installation: gunbai_ci_declared_installation_id, organization: GitHubOrganizationRef { login: "gunb-ai" }, runner_name: "fixture-jit-runner" } }, } Empty {} => JitMintAppJwtUnsigned { detail: "fixture: credential fixture did not mint" } } diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 55cda16e83f..4980997ce77 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -1,7 +1,8 @@ module test.claim.runner.runner_microvm_slot_controller_witness_test -import test.claim.github_app_registry { dispatched, runner_grant } -import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused } +import gunbc.runner.runner_jit_perform { JitMintIdentity } +import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_declared_installation_id } +import extdeps.github.effect { GitHubOrganizationRef } import v2.std.algebra { any } import std.types { String, NonEmptyStr, Bool, Int, List } import std.nat { Nat } @@ -296,11 +297,7 @@ fn delivered_mint() -> Bool { Present { value: a } => match jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: "") { JitConfigMinted { config: c, runner_id: id } => - match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { dispatch: d } => - minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id, dispatch: d } }) == Present { value: 4242 } - JitMintDispatchRefused { refusal: _ } => false - } + minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id, minted_for: JitMintIdentity { app: gunbai_ci_declared, installation: gunbai_ci_declared_installation_id, organization: GitHubOrganizationRef { login: "gunb-ai" }, runner_name: "fixture-jit-runner" } } }) == Present { value: 4242 } _ => false } } From c0a4c9bc506126371582ec2725597b011886d4c9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 14:23:12 +0000 Subject: [PATCH 29/44] leg 3 witness: the slot positive control passes pin_branch (a bare 'branch' resolved to another module's declaration; floor UnimportedBareProvider) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../claim/runner/runner_qualification_dispatch_witness_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 4a6550c586d..b10131bd5b8 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -212,7 +212,7 @@ fn slot_refused_as_another_workflow(slot_workflow: NonEmptyStr) -> Bool { test fn a_slot_for_another_workflow_or_branch_cannot_become_a_subject() -> Bool { slot_refused_as_another_workflow(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") && slot_refused_as_another_workflow(slot_workflow: qualification_selected_workflow(branch: "feature")) - && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: pin_branch), branch: branch) { Absent => true Present { value: _ } => false }) + && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: pin_branch), branch: pin_branch) { Absent => true Present { value: _ } => false }) } fn runs_on_refused(runs_on: RunnerSpec) -> Bool { From 392995fa2ee93fe974601eb6fe952ef3a680be34 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 14:55:05 +0000 Subject: [PATCH 30/44] leg 3: branch_absent matches every RestExchangePerformance arm (floor NonFoldResidueRosterDiverged) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/runner/runner_qualification_ref_pin.dag | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dag/gunbc/runner/runner_qualification_ref_pin.dag b/dag/gunbc/runner/runner_qualification_ref_pin.dag index aca9aa5740c..71bbd8091f5 100644 --- a/dag/gunbc/runner/runner_qualification_ref_pin.dag +++ b/dag/gunbc/runner/runner_qualification_ref_pin.dag @@ -177,7 +177,10 @@ type QualificationRefPinStanding fn branch_absent(performance: RestExchangePerformance) -> Bool { match performance { RestExchangeStatusRefused { status: s, body: _ } => (s as Int) == 404 - _ => false + RestExchangeSucceeded => false + RestExchangeCommitAmbiguous { detail: _ } => false + RestExchangeUnreached { cause: _ } => false + RestExchangeUndecodable { status: _, cause: _ } => false } } From 8bb3f19b30db10bc745147872b3b4e4b9188b5ca Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 16:15:52 +0000 Subject: [PATCH 31/44] leg 3 RC (392995f): sealed delivery carrier, collection token join, WorkflowRuns auth_input 1. JitCredentialBoundToAttempt { bound: BoundJitCredential } -- sole_constructor, built only by receive_jit_mint, carrying the complete AuthorizedJitMintDispatch with the credential and runner id (JitMintIdentity deleted). The subject requires bound.dispatch == its dispatch: the complete request (App, installation, organization, name, runner group, labels, work folder). REDs: same slot/App/ installation/org/name with another runner group refuses; a rewrapped delivery refuses at BoundJitCredential (compiler probe, test.claim.runner_qualification_delivery_rewrap_witness). The two hand-built delivery fixtures now mint for their own attempt (admitted helpers). 3. github.WorkflowRuns declares auth_input: auth_token beside GITHUB_TOKEN (callers passing "" keep the fallback); collect_qualification_run refuses a token for another App/installation before either read (RunCollectedUnderAnotherInstallation), RED in the real-path claim. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/workflow_runs.dag | 7 +++ dag/gunbc/runner/runner_attempt_launch.dag | 8 ++- dag/gunbc/runner/runner_jit_perform.dag | 62 +++++++------------ .../runner/runner_microvm_slot_controller.dag | 2 +- .../runner/runner_qualification_dispatch.dag | 35 ++++++++--- .../github_app_registry_witness_test.dag | 13 ++-- .../github_effect_perform_witness_test.dag | 4 +- .../runner_attempt_launch_witness_test.dag | 35 ++++++++--- ...r_microvm_slot_controller_witness_test.dag | 27 ++++++-- ...ification_delivery_rewrap_witness_test.dag | 41 ++++++++++++ ...er_qualification_dispatch_witness_test.dag | 56 ++++++++++++++--- 11 files changed, 207 insertions(+), 83 deletions(-) create mode 100644 dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index beb791f168a..87fd8257ecd 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -301,10 +301,17 @@ data structural_coverage_gap_workflow_run_codec_hand_rolled: List = [ // drops are execution that really happened and really occupied a runner. A caller measuring // occupancy needs "all"; a caller reading current state needs "latest"; neither is a property of // this operation, so the choice moves to the input with "latest" as the compatible default. +// THE BEARER IS THE CALLER'S auth_token WHEN ONE IS GIVEN. Every operation already took auth_token, +// and the service read only GITHUB_TOKEN, so a caller holding an installation token +// (gunbc.github_effect_perform read_run_with / read_attempt_jobs_with) was silently read under the +// ambient Actions credential. Declaring auth_input beside auth_source makes a non-empty auth_token the +// bearer; a caller passing "" (gunbc.pr_base_freshness, gunbc.fleet_desired_admission, +// gunbc.roadmap_launch_deployment_cli) falls through to GITHUB_TOKEN exactly as before. service github.WorkflowRuns { config { endpoint: default_api_base auth: Bearer + auth_input: auth_token auth_source: EnvVar { name: "GITHUB_TOKEN" } } diff --git a/dag/gunbc/runner/runner_attempt_launch.dag b/dag/gunbc/runner/runner_attempt_launch.dag index c4c7f8573b9..325f26234e2 100644 --- a/dag/gunbc/runner/runner_attempt_launch.dag +++ b/dag/gunbc/runner/runner_attempt_launch.dag @@ -40,7 +40,7 @@ import gunbc.guarantee_rung { import extdeps.github.actions_jit_runner { EncodedJitConfig, JitRunnerName } import gunbc.github_effect_perform { JitMintPerformance, JitMintReceived } import gunbc.runner.runner_jit_perform { - JitCredentialDelivery, JitCredentialBoundToAttempt, + JitCredentialDelivery, JitCredentialBoundToAttempt, bound_jit_slot, } import gunbc.runner_jit_mint { jit_runner_name_for, JitSlot, MicroVmJitSlot } import gunbc.runner.runner_jit_admission { @@ -207,7 +207,10 @@ fn admit_jit_credential(attempt: MicroVmAttempt, mint: JitMintPerformance) -> Ji match mint { JitMintReceived { delivery: delivery } => match delivery { - JitCredentialBoundToAttempt { slot: bound, credential: c, runner_id: id, minted_for: _ } => + JitCredentialBoundToAttempt { bound: b } => { + let bound = bound_jit_slot(bound: b) + let c = b.credential + let id = b.runner_id if !host_mint_admission_is_required() { JitCredentialRefused { refusal: JitMintAdmissionAxisNotRequired } } else if bound != MicroVmJitSlot { attempt: attempt } { @@ -225,6 +228,7 @@ fn admit_jit_credential(attempt: MicroVmAttempt, mint: JitMintPerformance) -> Ji }, } } + } other => JitCredentialRefused { refusal: JitCredentialNotDelivered { delivery: other } } } other => JitCredentialRefused { refusal: JitMintNotReceived { performance: other } } diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index f52e5e640c2..7b6a2c7d42d 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -114,29 +114,6 @@ type AuthorizedJitMintDispatch sole_constructor { request: JitRunnerConfigRequest } -// WHICH MINT A DELIVERY CAME FROM, AS A PLAIN VALUE. A delivery agrees with another on slot, -// credential and runner id when two authorized mints name the same slot; what tells them apart is -// the App, installation and organization the mint was performed under and the runner name it fixed. -// receive_jit_mint stamps that identity from the authorized dispatch, and a consumer that holds the -// sealed dispatch (gunbc.runner.runner_qualification_dispatch) compares the two. It is a value, not -// the sealed dispatch itself, because the delivery variant is not sealed: carrying the dispatch would -// hand every holder of a delivery a sealed value, and the identity is what the comparison needs. -type JitMintIdentity { - app: DeclaredGitHubApp - installation: GitHubAppInstallationId - organization: GitHubOrganizationRef - runner_name: NonEmptyStr -} - -fn jit_mint_identity_of(dispatch: AuthorizedJitMintDispatch) -> JitMintIdentity { - JitMintIdentity { - app: dispatch.app, - installation: dispatch.installation, - organization: dispatch.organization, - runner_name: dispatch.request.name.value, - } -} - type AuthorityInstallation { app: DeclaredGitHubApp installation: GitHubAppInstallationId @@ -162,6 +139,8 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.runner_attempt_launch_witness", decl_name: "received_for"), + decl_ref(module_path: "test.claim.runner.runner_microvm_slot_controller_witness_test", decl_name: "delivered_mint"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), ] { @@ -227,13 +206,25 @@ fn dispatch_jit_mint( // mitigatable, and its next-rung trigger is a substrate capability for naming a sum minus one of // its arms, which does not exist today. Duplicating the vocabulary to reach rung four would trade a // state no fold writes for a fork that drifts. +// THE SUCCESSFUL DELIVERY IS SEALED, AND IT CARRIES THE WHOLE AUTHORIZED DISPATCH THAT PRODUCED IT. +// receive_jit_mint is its only constructor, so the credential, the runner id GitHub returned and the +// dispatch -- App, installation, organization and the complete request (name, runner group, labels, +// work folder) -- are one value no caller can re-assemble. An open arm holding these as separate +// fields let a holder of two deliveries rewrap one's credential with the other's dispatch; a consumer +// that needs to know which mint a credential came from (gunbc.runner.runner_qualification_dispatch) +// compares bound.dispatch with its own sealed dispatch, which covers the complete request. +type BoundJitCredential sole_constructor { + dispatch: AuthorizedJitMintDispatch + credential: EncodedJitConfig + runner_id: Int +} + +fn bound_jit_slot(bound: BoundJitCredential) -> JitSlot { + bound.dispatch.slot +} + type JitCredentialDelivery - = JitCredentialBoundToAttempt { - slot: JitSlot - credential: EncodedJitConfig - runner_id: Int - minted_for: JitMintIdentity - } + = JitCredentialBoundToAttempt { bound: BoundJitCredential } | JitCredentialNotDispatched { refusal: JitDispatchRefusal } | JitCredentialMintNotCreated { outcome: JitConfigMintOutcome } @@ -244,14 +235,10 @@ fn receive_jit_mint( match dispatch { JitMintDispatchRefused { refusal: r } => JitCredentialNotDispatched { refusal: r } JitMintDispatchAuthorized { dispatch: authorized } => { - let slot = authorized.slot match outcome { JitConfigMinted { config: config, runner_id: runner_id } => JitCredentialBoundToAttempt { - slot: slot, - credential: config, - runner_id: runner_id, - minted_for: jit_mint_identity_of(dispatch: authorized), + bound: BoundJitCredential { dispatch: authorized, credential: config, runner_id: runner_id }, } JitConfigMintRefused { status: s, reason: reason } => JitCredentialMintNotCreated { @@ -272,12 +259,7 @@ fn receive_jit_mint( fn jit_credential_delivered(delivery: JitCredentialDelivery) -> Bool { match delivery { - JitCredentialBoundToAttempt { - slot: _, - credential: _, - runner_id: _, - minted_for: _, - } => true + JitCredentialBoundToAttempt { bound: _ } => true JitCredentialNotDispatched { refusal: _ } => false JitCredentialMintNotCreated { outcome: _ } => false } diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index b991328a0f5..3c345867ca4 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -514,7 +514,7 @@ fn minted_registration(mint: JitMintPerformance) -> Int? { match mint { JitMintReceived { delivery: d } => match d { - JitCredentialBoundToAttempt { slot: _, credential: _, runner_id: id, minted_for: _ } => Present { value: id } + JitCredentialBoundToAttempt { bound: b } => Present { value: b.runner_id } JitCredentialNotDispatched { refusal: _ } => none JitCredentialMintNotCreated { outcome: _ } => none } diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index ea55b5cf991..7272131c93d 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -31,7 +31,7 @@ import gunbc.auth.github_credential { GitHubCredentialAuthority, CredentialCapability, credential_authorizes, unsatisfied_capabilities, } import gunbc.runner_jit_mint { HostUnitJitSlot, MicroVmJitSlot } -import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt, authority_installation, jit_mint_identity_of } +import gunbc.runner.runner_jit_perform { AuthorizedJitMintDispatch, JitMintDispatchAuthorized, JitCredentialDelivery, JitCredentialBoundToAttempt, authority_installation } import gunbc.runner.runner_jit_deregistration { JitRegistration, jit_registration_of } import gunbc.managed_host_unit_hold { UnitHoldProof } import gunbc.github_effect_perform { @@ -54,9 +54,9 @@ import gunbc.runner.runner_qualification_ref_pin { // minted only by qualification_dispatch_subject_over, and only from: // - the sealed AuthorizedJitMintDispatch (gunbc.runner.runner_jit_perform), whose only mint is // dispatch_jit_mint's authorized arm, its registration (jit_registration_of), and a credential -// delivered by THAT mint -- the delivery's JitMintIdentity (App, installation, organization, -// runner name) and slot must equal the dispatch's, so a same-slot delivery from another mint -// refuses -- so +// delivered by THAT mint -- the delivery is a sealed BoundJitCredential carrying the dispatch that +// produced it, which must equal this dispatch over the complete request (App, installation, +// organization, name, runner group, labels, work folder); a rewrap is unconstructable -- so // the attempt is the slot's own and the runner the collection joins on is that registration's; // - the qualification workflow, which is not a parameter of the request: its path is the generated // fleet-converge artifact (gunbc.generated_artifact FleetConvergeYamlArtifact) and production @@ -247,8 +247,9 @@ fn qualification_dispatch_subject_over( Absent => QualificationSubjectRefused { refusal: SubjectNoRegistration } Present { value: registration } => match delivery { - JitCredentialBoundToAttempt { slot: delivered_slot, credential: _, runner_id: runner_id, minted_for: minted_for } => - if minted_for != jit_mint_identity_of(dispatch: dispatch) || delivered_slot != dispatch.slot { + JitCredentialBoundToAttempt { bound: bound } => { + let runner_id = bound.runner_id + if bound.dispatch != dispatch { QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } } else { match registration.slot { @@ -280,6 +281,7 @@ fn qualification_dispatch_subject_over( } } } + } other => QualificationSubjectRefused { refusal: SubjectCredentialNotDelivered { delivery: other } } } } @@ -402,7 +404,7 @@ fn dispatch_qualification_floor( let effect = qualification_dispatch_effect(subject: subject) if !hold_covers_slot_host(held: hold.subject.host.host, slot_host: subject.host) { QualificationDispatchRefused { subject: subject, refusal: DispatchHoldForAnotherHost { held: hold.subject.host.host, slot: subject.host } } - } else if token.app != subject.registration.app || token.installation != subject.registration.installation { + } else if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: subject) { QualificationDispatchRefused { subject: subject, refusal: DispatchTokenForAnotherInstallation } } else { match dispatch_authority_refusal(authority: authority, app: subject.registration.app, installation: subject.registration.installation) { @@ -454,6 +456,7 @@ fn dispatch_qualification_floor( type QualificationRunRefusal = RunReadUnreadable { performance: RestExchangePerformance } + | RunCollectedUnderAnotherInstallation { token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId } | RunSubjectMismatch { observed_run_id: Int, observed_event: String, observed_path: String?, foreign_jobs: List } | RunAdvancedPastDispatchedAttempt { observed_attempt: Int } | RunRevisionNotPinned { pinned: CommitSha, observed: CommitSha } @@ -599,7 +602,25 @@ fn decide_qualification_run( } } +// THE READBACK IS PERFORMED UNDER THE INSTALLATION THAT DISPATCHED. A sealed controller token for some +// other App or installation could read a run and report it as installation A's; the token is joined +// to the registration before either read, exactly as at dispatch. +fn token_names_registration(token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId, subject: QualificationDispatchSubject) -> Bool { + token_app == subject.registration.app && token_installation == subject.registration.installation +} + fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { + if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: dispatched.subject) { + QualificationRunRefused { + run_id: dispatched.receipt.workflow_run_id, + refusal: RunCollectedUnderAnotherInstallation { token_app: token.app, token_installation: token.installation }, + } + } else { + collect_qualification_run_under(dispatched: dispatched, token: token) + } +} + +fn collect_qualification_run_under(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { let run_id = dispatched.receipt.workflow_run_id let owner = gunbc_repository.owner as NonEmptyStr let repo = gunbc_repository.name as NonEmptyStr diff --git a/dag/test/claim/github_app_registry_witness_test.dag b/dag/test/claim/github_app_registry_witness_test.dag index a2b54a209b5..cd21975e256 100644 --- a/dag/test/claim/github_app_registry_witness_test.dag +++ b/dag/test/claim/github_app_registry_witness_test.dag @@ -1306,18 +1306,13 @@ test fn witness_a_minted_credential_is_bound_to_the_dispatched_attempt() -> Bool ) jit_credential_delivered(delivery: delivery) && match delivery { - JitCredentialBoundToAttempt { - slot: a, - credential: c, - runner_id: id, - minted_for: _, - } => + JitCredentialBoundToAttempt { bound: b } => match dispatched(owned: true, perms: runner_grant) { - JitMintDispatchAuthorized { dispatch: d } => a == d.slot + JitMintDispatchAuthorized { dispatch: d } => b.dispatch.slot == d.slot && b.dispatch == d _ => false } - && (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" - && id == 4242 + && (b.credential.encoded as String) == "ZmFrZS1qaXQtY29uZmln" + && b.runner_id == 4242 _ => false } } diff --git a/dag/test/claim/github_effect_perform_witness_test.dag b/dag/test/claim/github_effect_perform_witness_test.dag index b3eb77d45d0..00cbe4600f3 100644 --- a/dag/test/claim/github_effect_perform_witness_test.dag +++ b/dag/test/claim/github_effect_perform_witness_test.dag @@ -102,8 +102,8 @@ data created_body: JitConfigCreatedBody = JitConfigCreatedBody { test fn witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered() -> Bool { let d = dispatched(owned: true, perms: runner_grant) let delivered = match jit_mint_performance_of_generate(dispatch: d, outcome: RestOk, created: created_body) { - JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: _, credential: c, runner_id: id, minted_for: _ } } => - (c.encoded as String) == "ZmFrZS1qaXQtY29uZmln" && id == 4242 + JitMintReceived { delivery: JitCredentialBoundToAttempt { bound: b } } => + (b.credential.encoded as String) == "ZmFrZS1qaXQtY29uZmln" && b.runner_id == 4242 _ => false } let conflicted = match jit_mint_performance_of_generate( diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index bb2b3d05e93..47120282805 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -1,8 +1,11 @@ module test.claim.runner_attempt_launch_witness -import gunbc.runner.runner_jit_perform { JitMintIdentity } -import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_declared_installation_id } -import extdeps.github.effect { GitHubOrganizationRef } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner_jit_mint { MicroVmSlotBinding } +import gunbc.runner_microvm_attempt { bind_owned_attempt } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, restricted_group_1 } +import extdeps.github.actions_jit_runner { jit_config_mint_outcome } import gunbc.runner_attempt_launch { GitHubLaunchPayload, launch_payload_device } import std.types { String, Bool, Int, List, NonEmptyStr } @@ -138,16 +141,28 @@ fn minted_credential(encoded: String) -> List { // something that is not a whole credential, handed on as if it were. data short_blob: String = "ZmFrZS1qaXQtY29uZmln" -// THE MINT IDENTITY ON A HAND-BUILT DELIVERY. A delivery names the mint it came from -// (JitMintIdentity); the launch decision this fixture drives reads only the slot, the credential and -// the runner id, so the identity here is a plain fixture value. +// THE DELIVERY IS MINTED, NOT HAND-BUILT. A successful delivery is a sealed BoundJitCredential that only +// receive_jit_mint constructs, carrying the authorized dispatch that produced it; this fixture takes the +// real route for the attempt under test -- an owned cell binding, dispatch_jit_mint (which admits this +// helper by name), and receive_jit_mint over upstream's own 201 outcome fold with the encoding the +// claim chooses. fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance { - match minted_credential(encoded: encoded) { - Cons { head: c, tail: _ } => + match dispatch_jit_mint( + binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: attempt, owned: true) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: restricted_group_1(), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => JitMintReceived { - delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id, minted_for: JitMintIdentity { app: gunbai_ci_declared, installation: gunbai_ci_declared_installation_id, organization: GitHubOrganizationRef { login: "gunb-ai" }, runner_name: "fixture-jit-runner" } }, + delivery: receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: d }, + outcome: jit_config_mint_outcome(status: 201, encoded: encoded, runner_id: registration_fixture_id, error_body: ""), + ), } - Empty {} => JitMintAppJwtUnsigned { detail: "fixture: credential fixture did not mint" } + JitMintDispatchRefused { refusal: _ } => JitMintAppJwtUnsigned { detail: "fixture: the attempt's dispatch was refused" } } } diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 4980997ce77..652d426ff89 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -1,8 +1,11 @@ module test.claim.runner.runner_microvm_slot_controller_witness_test -import gunbc.runner.runner_jit_perform { JitMintIdentity } -import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_declared_installation_id } -import extdeps.github.effect { GitHubOrganizationRef } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner_jit_mint { MicroVmSlotBinding } +import gunbc.runner_microvm_attempt { bind_owned_attempt } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, restricted_group_1 } +import extdeps.github.actions_jit_runner { jit_config_mint_outcome } import v2.std.algebra { any } import std.types { String, NonEmptyStr, Bool, Int, List } import std.nat { Nat } @@ -296,8 +299,22 @@ fn delivered_mint() -> Bool { Absent => false Present { value: a } => match jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: "") { - JitConfigMinted { config: c, runner_id: id } => - minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id, minted_for: JitMintIdentity { app: gunbai_ci_declared, installation: gunbai_ci_declared_installation_id, organization: GitHubOrganizationRef { login: "gunb-ai" }, runner_name: "fixture-jit-runner" } } }) == Present { value: 4242 } + JitConfigMinted { config: _, runner_id: _ } => + match dispatch_jit_mint( + binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: a, owned: true) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: restricted_group_1(), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => + minted_registration(mint: JitMintReceived { delivery: receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: d }, + outcome: jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: ""), + ) }) == Present { value: 4242 } + JitMintDispatchRefused { refusal: _ } => false + } _ => false } } diff --git a/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag new file mode 100644 index 00000000000..ccc1726ef2f --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag @@ -0,0 +1,41 @@ +module test.claim.runner_qualification_delivery_rewrap_witness + +import std.types { String, Bool, Int } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE REWRAP ROUTE, ENROLLED AS AN EXECUTED RED (DESIGN 4b: a state unrepresentable in the accepted +// corpus is still representable as source handed to the compiler). test.probe.qualification_delivery_ +// rewrap_probe relabels delivery B's credential and runner id as dispatch A's; it must refuse at +// BoundJitCredential, the sealed carrier receive_jit_mint alone constructs, so no +// QualificationDispatchSubject can ever be built over a rewrapped delivery. A control source that only +// NAMES the same types must refuse at none of them, so the refusal is attributable to the forgery. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_qualification_delivery_control\nimport std.types { Bool }\nimport gunbc.runner.runner_jit_perform { BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, jit_credential_delivered }\nfn delivered(d: JitCredentialDelivery) -> Bool { jit_credential_delivered(delivery: d) }\n" + +test fn a_source_that_only_names_the_carrier_forges_nothing() -> Bool { + blocking(c: census_of(source: control_source), wanted: "SoleConstructorViolation", subject: "BoundJitCredential") == 0 +} + +test fn a_rewrapped_delivery_refuses_at_the_sealed_carrier() -> Bool { + blocking(c: census_of(source: filesystem_read(path: "dag/test/probe/qualification_delivery_rewrap_probe.dag").content), wanted: "SoleConstructorViolation", subject: "BoundJitCredential") >= 1 +} diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index b10131bd5b8..f6d4430315d 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -20,9 +20,12 @@ import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispa import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit } import test.claim.github_app_registry { app_credential, other_app_credential, other_installation_credential, a_pull_request_job_credential, - bot_installation_id, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome, + bot_installation_id, other_installation_id, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome, } -import gunbc.auth.github_apps { gunbai_bot_declared } +import gunbc.auth.github_apps { gunbai_bot_declared, gunbai_ci_declared } +import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, observe_runner_group_restriction } +import gunbc.fleet.org_actions_inspection { runner_groups_of_rest_read } +import extdeps.github.org_actions { OrgRunnerGroupWire, OrgRunnerGroupListWire } import gunbc.runner.runner_qualification_ref_pin { qualification_selected_workflow, qualification_workflow_path, qualification_branch_name, QualificationBranchNamed, QualificationBranchNameRefused, qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, @@ -38,7 +41,7 @@ import gunbc.runner.runner_qualification_dispatch { qualification_run_expectation, QualificationRunExpectation, qualification_dispatch_standing, QualificationDispatchStanding, DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, - hold_covers_slot_host, + hold_covers_slot_host, token_names_registration, decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunOnAnotherBranch, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, @@ -61,6 +64,26 @@ data registered_runner_id: Int = 4242 // runs the real path: the route's JIT mint over the real managed-host roster, the sealed subject // mint over a supplied floor workflow, and the expectation derived from that subject. +// A group standing for the dedicated name under another id, through the same sealed observer. +fn group_with_id(id: Int, workflow: NonEmptyStr) -> MicrovmRunnerGroupStanding { + observe_runner_group_restriction( + name: "runner-qualification", + selected_workflow: workflow, + listed: runner_groups_of_rest_read(outcome: RestOk, body: OrgRunnerGroupListWire { + total_count: 1, + runner_groups: [OrgRunnerGroupWire { + id: id, + name: "runner-qualification", + visibility: "selected", + allows_public_repositories: false, + restricted_to_workflows: true, + selected_workflows: [workflow as String], + workflow_restrictions_read_only: false, + }], + }), + ) +} + fn floor_job(runs_on: RunnerSpec) -> Job { Job { id: floor_job_id as String, @@ -129,6 +152,9 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { let expected = qualification_run_expectation(subject: s) s.attempt == attempt && s.runner_id == registered_runner_id && expected.registered_runner_id == registered_runner_id + && token_names_registration(token_app: s.registration.app, token_installation: s.registration.installation, subject: s) + && !token_names_registration(token_app: gunbai_ci_declared, token_installation: s.registration.installation, subject: s) + && !token_names_registration(token_app: s.registration.app, token_installation: other_installation_id, subject: s) && (s.host as String) == (h.host as String) && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (expected.floor_job_name as String) == "qualification floor" @@ -298,10 +324,13 @@ test fn the_dispatch_authority_must_name_the_tokens_installation() -> Bool { }) } -// A DELIVERY CARRIES THE AUTHORIZED DISPATCH THAT PRODUCED IT. Two authorized mints for the SAME -// host-unit slot -- here under different organizations -- produce deliveries that agree on slot, -// credential and runner id; the subject over dispatch A refuses delivery B rather than claiming A's -// registration for a credential B minted. +// A DELIVERY CARRIES THE AUTHORIZED DISPATCH THAT PRODUCED IT, SEALED, AND THE SUBJECT COMPARES THE +// COMPLETE REQUEST. Two authorized mints for the SAME host-unit slot -- under another organization, and +// under the same slot, App, installation, organization and runner name but another runner group -- +// produce deliveries that agree on slot, credential and runner id; the subject over dispatch A refuses +// each rather than claiming A's registration for a credential another mint produced. (Rewrapping one +// delivery's credential with A's dispatch is unconstructable: +// test.claim.runner_qualification_delivery_rewrap_witness.) test fn a_delivery_from_another_dispatch_on_the_same_slot_refuses() -> Bool { match first(managed_hosts()) { Present { value: h } => { @@ -328,6 +357,19 @@ test fn a_delivery_from_another_dispatch_on_the_same_slot_refuses() -> Bool { QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true _ => false }) + && (match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_with_id(id: 4, workflow: qualification_selected_workflow(branch: pin_branch)), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: c } => + a.slot == c.slot && a.app == c.app && a.installation == c.installation && a.organization == c.organization && a.request.name == c.request.name + && a.request.runner_group_id != c.request.runner_group_id + && (match qualification_dispatch_subject_over( + dispatch: a, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: c }, outcome: minted_outcome), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, + ) { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true + _ => false + }) + JitMintDispatchRefused { refusal: _ } => false + }) JitMintDispatchRefused { refusal: _ } => false } } From 5a610d35edb269664b5f162a667503c32d0cf32e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 16:49:38 +0000 Subject: [PATCH 32/44] leg 3: commit the rewrap probe the rewrap witness compiles (a local .git/info/exclude 'probe/' rule kept it out of the earlier commit) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../qualification_delivery_rewrap_probe.dag | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 dag/test/probe/qualification_delivery_rewrap_probe.dag diff --git a/dag/test/probe/qualification_delivery_rewrap_probe.dag b/dag/test/probe/qualification_delivery_rewrap_probe.dag new file mode 100644 index 00000000000..4d81ba655b4 --- /dev/null +++ b/dag/test/probe/qualification_delivery_rewrap_probe.dag @@ -0,0 +1,19 @@ +module test.probe.qualification_delivery_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. Each function is a route by which a holder of two legitimate JIT +// deliveries would relabel one: keep delivery B's sealed credential and the runner id GitHub returned +// for B, and claim dispatch A produced them -- so a qualification subject over A would carry B's +// runner. The witness test.claim.runner_qualification_delivery_rewrap_witness hands this source to the +// compiler and requires the refusal at the sealed carrier by name. dag/test/probe/ sits under the +// whole-tree strict-resolve exclusion, so this source reaches the compiler only through that witness. + +import std.types { Int } +import gunbc.runner.runner_jit_perform { + AuthorizedJitMintDispatch, BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, +} + +fn rewrap(a: AuthorizedJitMintDispatch, b: BoundJitCredential) -> JitCredentialDelivery { + JitCredentialBoundToAttempt { + bound: BoundJitCredential { dispatch: a, credential: b.credential, runner_id: b.runner_id }, + } +} From f354d7783b2aae72498b786369923f7cc7e8a3a5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 16:53:06 +0000 Subject: [PATCH 33/44] leg 3b: address review on a1df1b6 -- token-bound collector, Artifacts auth_input, attempt-bound and unambiguous claim-cost read, narrowed claim-cost frontier (Merges #13211 8bb3f19b30, which owns github.WorkflowRuns auth_input and the token join.) 1. github.Artifacts declares auth_input: auth_token. collect_qualification_instruments refuses before any read unless token_names_registration holds (QualificationCollectUnderAnotherInstallation); job_log_standing is admitted to collect_qualification_instruments_under. 2. The claim-cost row no longer says the upload is missing (#13227 restored it); it names binary REST transport, the exact TSV member's inflation, parse_claim_cost_tsv, and binding to this run. 3. Same-name duplicates refuse (ClaimCostArtifactAmbiguous); the run is re-read after the listing and a run past the collected attempt refuses (ClaimCostRunAdvancedPastAttempt / ClaimCostRunUnreadable). Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/actions_artifacts.dag | 6 ++ .../runner/runner_qualification_dispatch.dag | 61 ++++++++++++++++--- ...er_qualification_dispatch_witness_test.dag | 30 ++++++++- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag index 75bb59e8167..c1d38558bf5 100644 --- a/dag/extdeps/github/actions_artifacts.dag +++ b/dag/extdeps/github/actions_artifacts.dag @@ -59,10 +59,16 @@ type ActionsArtifactList { // other spelling representable, and a coproduct input interpolated into a path is rendered by the // seed with its AUTHORED variant name (the regression extdeps.github.workflow_runs // WorkflowJobAttemptFilter records), so neither would be more faithful than the literal. +// AUTHENTICATION IS THE CALLER'S TOKEN. Accepting an auth_token operation input does not by itself +// bind authentication; auth_input names that input as the credential, as extdeps.github.git_database +// and github.WorkflowRuns declare, so the controller installation token the qualification collector +// passes is the one the request carries rather than the ambient GITHUB_TOKEN. An empty token keeps +// the env fallback, the same dual declaration github.WorkflowRuns uses. service github.Artifacts { config { endpoint: default_api_base auth: Bearer + auth_input: auth_token auth_source: EnvVar { name: "GITHUB_TOKEN" } } diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index e972f69d0a8..f8ce01ced30 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -846,7 +846,7 @@ type QualificationCollectStage { fn job_log_standing(collected: CollectedQualificationRun, slot: HostUnitSlot, log: AttemptJobLog) -> JobLogInstrumentStanding admit_callers: [ - decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments"), + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments_under"), ] { match designated_job_log(designated_job_id: collected.floor_job.id, log: log) { @@ -887,12 +887,20 @@ type ClaimCostArtifactRead = ClaimCostArtifactListUnreadable { performance: RestExchangePerformance } | ClaimCostArtifactListTruncated { returned: Int, total: Int } | ClaimCostArtifactAbsent { names_seen: List } + | ClaimCostArtifactAmbiguous { artifact_ids: List } + | ClaimCostRunUnreadable { performance: RestExchangePerformance } + | ClaimCostRunAdvancedPastAttempt { observed_attempt: Int } | ClaimCostArtifactExpired { artifact_id: Int } | ClaimCostArchiveUnreadable { artifact_id: Int, performance: RestExchangePerformance } | ClaimCostArchiveNotInflatable { artifact_id: Int } +fn claim_cost_artifacts_named(listed: ActionsArtifactList) -> List { + listed.artifacts |> filter(a => a.name == required_floor_claim_cost_artifact_name) +} + fn claim_cost_artifact_listed(listed: ActionsArtifactList) -> ActionsArtifact? { - listed.artifacts |> filter(a => a.name == required_floor_claim_cost_artifact_name) |> first + let named = claim_cost_artifacts_named(listed: listed) + if count(named) == 1 { named |> first } else { none } } // none: the listing names an unexpired claim-cost artifact and the download is owed. @@ -903,7 +911,12 @@ fn conclude_claim_cost_listing(performance: RestExchangePerformance, listed: Act Present { value: ClaimCostArtifactListTruncated { returned: count(listed.artifacts), total: listed.total_count } } } else { match claim_cost_artifact_listed(listed: listed) { - Absent => Present { value: ClaimCostArtifactAbsent { names_seen: listed.artifacts |> map(a => a.name) } } + Absent => + if count(claim_cost_artifacts_named(listed: listed)) > 1 { + Present { value: ClaimCostArtifactAmbiguous { artifact_ids: claim_cost_artifacts_named(listed: listed) |> map(a => a.id) } } + } else { + Present { value: ClaimCostArtifactAbsent { names_seen: listed.artifacts |> map(a => a.name) } } + } Present { value: a } => if a.expired { Present { value: ClaimCostArtifactExpired { artifact_id: a.id } } } else { none } } @@ -912,6 +925,24 @@ fn conclude_claim_cost_listing(performance: RestExchangePerformance, listed: Act } } +// THE ARTIFACT IS BOUND TO ATTEMPT 1 BY READING THE RUN AFTER THE LISTING. Upstream's artifact object +// names its workflow run's id, head SHA and branch but no attempt, and a re-run reuses the run id, so a +// listing alone cannot tell attempt 1's artifact from a re-run's. A re-run that uploaded before the +// listing was taken had already advanced the run, so re-reading the run AFTER the listing and requiring +// run_attempt to still be the dispatched attempt excludes every artifact the listing could have seen +// from a later attempt. none: the run is still at the collected attempt. +fn conclude_claim_cost_attempt(performance: RestExchangePerformance, run: WorkflowRun) -> ClaimCostArtifactRead? { + match performance { + RestExchangeSucceeded => + if run.run_attempt != dispatched_run_attempt { + Present { value: ClaimCostRunAdvancedPastAttempt { observed_attempt: run.run_attempt } } + } else { + none + } + other => Present { value: ClaimCostRunUnreadable { performance: other } } + } +} + // A 410 ON THE DOWNLOAD IS UPSTREAM'S EXPIRY ANSWER (actions/download-artifact documents 410 Gone for // an expired artifact), so it is ClaimCostArtifactExpired like the listing's own flag, not a generic // unreadable archive. @@ -932,9 +963,13 @@ fn conclude_claim_cost_download(artifact_id: Int, performance: RestExchangePerfo fn read_claim_cost_artifact(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> ClaimCostArtifactRead uses net: Network { let listed = list_run_artifacts_with(token: token, owner: owner, repo: repo, run_id: run_id) + let after = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) match conclude_claim_cost_listing(performance: listed.performance, listed: listed.listed) { Present { value: refused } => refused Absent => + match conclude_claim_cost_attempt(performance: after.performance, run: after.run) { + Present { value: refused } => refused + Absent => match claim_cost_artifact_listed(listed: listed.listed) { Absent => ClaimCostArtifactAbsent { names_seen: [] } Present { value: a } => { @@ -942,11 +977,14 @@ fn read_claim_cost_artifact(token: ControllerInstallationToken, owner: NonEmptyS conclude_claim_cost_download(artifact_id: a.id, performance: archive.performance) } } + } } } -// THE PRODUCTION ENTRY. It takes the sealed collection and the controller token, and nothing a caller -// could cross-wire: the repository is gunbc_repository, the run, revision and attempt are the +// THE PRODUCTION ENTRY. It takes the sealed collection and the controller token, and refuses before any +// read unless that token is the registration's App and installation (token_names_registration, the +// join collect_qualification_run makes), so it reads nothing under another installation. Nothing +// else a caller could cross-wire: the repository is gunbc_repository, the run, revision and attempt are the // collection's, the floor job is the one collect_qualification_run joined to the minted runner, and // the slot is that registration's own host-unit slot. It reads the floor job's log and the claim-cost // artifact and returns a standing per instrument source. Its consumer is the route's collect stage @@ -957,8 +995,17 @@ fn read_claim_cost_artifact(token: ControllerInstallationToken, owner: NonEmptyS type QualificationCollectOutcome = QualificationCollected { stage: QualificationCollectStage } | QualificationCollectSlotNotAHostUnit + | QualificationCollectUnderAnotherInstallation { token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId } fn collect_qualification_instruments(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { + if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: collected.dispatched.subject) { + QualificationCollectUnderAnotherInstallation { token_app: token.app, token_installation: token.installation } + } else { + collect_qualification_instruments_under(collected: collected, token: token) + } +} + +fn collect_qualification_instruments_under(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { let owner = gunbc_repository.owner as NonEmptyStr let repo = gunbc_repository.name as NonEmptyStr match collected.dispatched.subject.registration.slot { @@ -984,8 +1031,8 @@ data qualification_instrument_extraction_frontier_rows: List = [ ), frontier_row_decl( ref: decl_ref(module_path: "gunbc.floor_cost_distribution", decl_name: "parse_claim_cost_tsv"), - reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are both read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding). Neither is on the job log: on the recorded required-floor log of run 35048059968 / job 104642384772 (test.fixture recorded_required_floor_log_excerpt), there are zero [witness] lines and zero occurrences of the specimen identity test.claim.floor_eval_step_calibration_specimen_test.calibration_specimen_performs_its_declared_work, and eval_steps appear only on the [over-cost] preview, a 25-row truncated view that is not the instrument and is not read as one. Two capabilities are missing. FIRST, the required workflow no longer uploads the artifact: cli_run write_required_floor_claim_cost_tsv still writes the TSV, but the emission of .github/workflows/witnesses.yml by gunbc.compiler_gate_workflow carries no upload step for it, and recent required runs publish only required-ci-measurement-receipt and compiler-pair-candidate. SECOND, an uploaded artifact is always a zip, and the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so extdeps.github.actions_artifacts github.Artifacts DownloadArtifact answers RestBodyUndecodable. read_claim_cost_artifact attempts the read on every collection and carries the result as a typed ClaimCostArtifactRead refusal (ClaimCostArtifactAbsent while nothing uploads it)", - dissolution: unbound_dissolution(description: "BOTH capabilities hold. (1) The required workflow uploads required-floor-claim-cost again, as an upload step in the existing floor job emitted by gunbc.compiler_gate_workflow under gunbc.witness_floor_workflow required_floor_claim_cost_artifact_name -- an emission change, not a new job. (2) The REST transport carries a binary body and an inflate reader can take one member out of one upload-artifact zip. Together these are SUFFICIENT FOR collect_qualification_instruments to read ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from the same collected run through parse_claim_cost_tsv, and for qualification_instrument_source to place both on that read. NOT satisfied by either capability alone, by the [over-cost] preview or any other truncated view, by a new print of eval_steps into the log, or by an artifact downloaded outside this route"), + reason: "ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine are both read from the required-floor-claim-cost TSV (gunbc.floor_cost_distribution parse_claim_cost_tsv; the calibration specimen's row is joined there by calibration_specimen_grounding). Neither is on the job log: on the recorded required-floor log of run 35048059968 / job 104642384772 (test.fixture recorded_required_floor_log_excerpt) there are zero [witness] lines and zero occurrences of the specimen identity, and eval_steps appear only on the [over-cost] preview, a 25-row truncated view that is not the instrument. The required floor job uploads the artifact (gunbc.witness_floor_workflow required_floor_claim_cost_upload_bound_step, bound by gunbc.compiler_gate_workflow), and read_claim_cost_artifact lists it under the registration's installation token, refuses zero or several same-name artifacts, binds the listing to the collected attempt by re-reading the run after it (ClaimCostRunAdvancedPastAttempt), and downloads the one artifact. What does not exist is the reading after the download: an uploaded artifact is always a zip, the REST handler reads every body as UTF-8 text (extdeps.transports.rest RestBodyObservation body: String), so DownloadArtifact answers RestBodyUndecodable today and a body that did arrive is ClaimCostArchiveNotInflatable", + dissolution: unbound_dissolution(description: "the REST transport carries a binary response body, AND an inflate reader takes the exact required_floor_claim_cost.tsv member out of one upload-artifact zip, AND that member is parsed by parse_claim_cost_tsv into a reading bound to this collected run -- the artifact downloaded is the one listed for the sealed run id at the collected attempt, carried with its artifact id beside the parsed rows in the sealed receipt -- SUFFICIENT FOR collect_qualification_instruments to read ClaimCostArtifactEvalSteps and CalibrationSpecimenWitnessLine from the same run as the job log, and for qualification_instrument_source to place both on that read. NOT satisfied by the binary transport or the inflate reader alone, by reading any other member, by the [over-cost] preview or any other truncated view, by a new print of eval_steps into the log, or by an artifact downloaded outside this route"), ), frontier_row_decl( ref: decl_ref(module_path: "gunbc.runner_throughput_qualification_route", decl_name: "mtcollins1_boot_leg"), diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 8d878621cf0..7fdc7b8bc43 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -55,7 +55,8 @@ import gunbc.runner.runner_qualification_dispatch { qualification_instrument_read_from_job_log, qualification_instrument_extraction_frontier_rows, ClaimCostArtifactRead, ClaimCostArtifactAbsent, ClaimCostArtifactExpired, ClaimCostArtifactListTruncated, ClaimCostArchiveUnreadable, ClaimCostArchiveNotInflatable, - conclude_claim_cost_listing, conclude_claim_cost_download, + conclude_claim_cost_listing, conclude_claim_cost_download, conclude_claim_cost_attempt, + ClaimCostArtifactAmbiguous, ClaimCostRunAdvancedPastAttempt, ClaimCostRunUnreadable, } import gunbc.required_ci_phase_roster { FloorPhaseTimed, FloorPhaseUntimed } import extdeps.linux.cgroup_v2_memory { CgroupMemoryLimited } @@ -739,3 +740,30 @@ test fn every_claim_cost_read_ends_in_a_typed_refusal() -> Bool { _ => false }) } + +// TWO SAME-NAME ARTIFACTS ARE A REFUSAL, NEVER FIRST-ONE-WINS. +fn claim_cost_artifact_with(id: Int) -> ActionsArtifact { + ActionsArtifact { id: id, name: "required-floor-claim-cost", expired: false } +} + +test fn two_same_name_claim_cost_artifacts_refuse_as_ambiguous() -> Bool { + match conclude_claim_cost_listing(performance: RestExchangeSucceeded, listed: ActionsArtifactList { total_count: 2, artifacts: [claim_cost_artifact_with(id: 1), claim_cost_artifact_with(id: 2)] }) { + Present { value: ClaimCostArtifactAmbiguous { artifact_ids: ids } } => count(ids) == 2 + _ => false + } +} + +// THE LISTING IS BOUND TO THE COLLECTED ATTEMPT: a run re-read after the listing at attempt 2 means the +// listing may hold attempt 2's artifact, so no claim-cost read proceeds for attempt 1; at attempt 1 it +// proceeds; an unreadable re-read refuses rather than assuming attempt 1. +test fn a_rerun_seen_after_the_listing_refuses_the_claim_cost_read() -> Bool { + (match conclude_claim_cost_attempt(performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 2)) { + Present { value: ClaimCostRunAdvancedPastAttempt { observed_attempt: 2 } } => true + _ => false + }) + && (match conclude_claim_cost_attempt(performance: RestExchangeSucceeded, run: dispatched_run(attempt_n: 1)) { Absent => true _ => false }) + && (match conclude_claim_cost_attempt(performance: RestExchangeUnreached { cause: "unreached" }, run: dispatched_run(attempt_n: 1)) { + Present { value: ClaimCostRunUnreadable { performance: _ } } => true + _ => false + }) +} From 635377f6f6de29d15af2e2eace4e0abda0074aaf Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 17:09:56 +0000 Subject: [PATCH 34/44] leg 3b: the collector's token guard is a pure read plan, executed as a RED inside the real-mint claim collect_read_plan(subject, token app/installation, floor job id, run id) plans no reads for a token of another App or installation and the job log plus artifacts otherwise; collect_qualification_instruments performs only the planned reads with the plan's ids. The real-mint claim asserts both arms over its inline-minted subject. Control: a planner that always plans reds exactly that claim. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 54 ++++++++++++++++--- ...er_qualification_dispatch_witness_test.dag | 11 ++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index f8ce01ced30..f0c0ff421d1 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -997,22 +997,62 @@ type QualificationCollectOutcome | QualificationCollectSlotNotAHostUnit | QualificationCollectUnderAnotherInstallation { token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId } -fn collect_qualification_instruments(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { - if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: collected.dispatched.subject) { - QualificationCollectUnderAnotherInstallation { token_app: token.app, token_installation: token.installation } +// THE READ PLAN, PURE: WHICH READS THE COLLECTOR MAY MAKE, DECIDED BEFORE ANY OF THEM. A token that +// does not name the registration's App and installation (token_names_registration, the same join +// collect_qualification_run makes) plans no reads at all; otherwise the plan is the designated floor +// job's log and this run's artifacts, by id. The collector below performs exactly the reads its plan +// names and takes the ids from the plan, so a claim on this function with supplied values is a claim +// on what the collector reads. +type CollectRead + = ReadFloorJobLog { job_id: Int } + | ReadRunArtifacts { run_id: Int } + +type CollectReadPlan + = CollectReadsRefused { token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId } + | CollectReadsPlanned { job_id: Int, run_id: Int } + +fn collect_read_plan( + subject: QualificationDispatchSubject, + token_app: DeclaredGitHubApp, + token_installation: GitHubAppInstallationId, + floor_job_id: Int, + run_id: Int, +) -> CollectReadPlan { + if token_names_registration(token_app: token_app, token_installation: token_installation, subject: subject) { + CollectReadsPlanned { job_id: floor_job_id, run_id: run_id } } else { - collect_qualification_instruments_under(collected: collected, token: token) + CollectReadsRefused { token_app: token_app, token_installation: token_installation } + } +} + +fn collect_read_plan_reads(plan: CollectReadPlan) -> List { + match plan { + CollectReadsRefused { token_app: _, token_installation: _ } => [] + CollectReadsPlanned { job_id: j, run_id: r } => [ReadFloorJobLog { job_id: j }, ReadRunArtifacts { run_id: r }] + } +} + +fn collect_qualification_instruments(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { + match collect_read_plan( + subject: collected.dispatched.subject, + token_app: token.app, + token_installation: token.installation, + floor_job_id: collected.floor_job.id, + run_id: collected.dispatched.receipt.workflow_run_id, + ) { + CollectReadsRefused { token_app: a, token_installation: i } => QualificationCollectUnderAnotherInstallation { token_app: a, token_installation: i } + CollectReadsPlanned { job_id: j, run_id: r } => collect_qualification_instruments_under(collected: collected, token: token, job_id: j, run_id: r) } } -fn collect_qualification_instruments_under(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { +fn collect_qualification_instruments_under(collected: CollectedQualificationRun, token: ControllerInstallationToken, job_id: Int, run_id: Int) -> QualificationCollectOutcome uses net: Network { let owner = gunbc_repository.owner as NonEmptyStr let repo = gunbc_repository.name as NonEmptyStr match collected.dispatched.subject.registration.slot { MicroVmJitSlot { attempt: _ } => QualificationCollectSlotNotAHostUnit HostUnitJitSlot { slot: slot } => { - let log = attempt_job_log_of(read: read_job_log_with(token: token, owner: owner, repo: repo, job_id: collected.floor_job.id)) - let claim_cost = read_claim_cost_artifact(token: token, owner: owner, repo: repo, run_id: collected.dispatched.receipt.workflow_run_id) + let log = attempt_job_log_of(read: read_job_log_with(token: token, owner: owner, repo: repo, job_id: job_id)) + let claim_cost = read_claim_cost_artifact(token: token, owner: owner, repo: repo, run_id: run_id) QualificationCollected { stage: QualificationCollectStage { job_log: job_log_standing(collected: collected, slot: slot, log: log), claim_cost: claim_cost } } } } diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index 7fdc7b8bc43..6b524eb13a8 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -56,6 +56,7 @@ import gunbc.runner.runner_qualification_dispatch { ClaimCostArtifactRead, ClaimCostArtifactAbsent, ClaimCostArtifactExpired, ClaimCostArtifactListTruncated, ClaimCostArchiveUnreadable, ClaimCostArchiveNotInflatable, conclude_claim_cost_listing, conclude_claim_cost_download, conclude_claim_cost_attempt, + collect_read_plan, collect_read_plan_reads, CollectReadsRefused, CollectReadsPlanned, ClaimCostArtifactAmbiguous, ClaimCostRunAdvancedPastAttempt, ClaimCostRunUnreadable, } import gunbc.required_ci_phase_roster { FloorPhaseTimed, FloorPhaseUntimed } @@ -173,6 +174,16 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { && token_names_registration(token_app: s.registration.app, token_installation: s.registration.installation, subject: s) && !token_names_registration(token_app: gunbai_ci_declared, token_installation: s.registration.installation, subject: s) && !token_names_registration(token_app: s.registration.app, token_installation: other_installation_id, subject: s) + && count(collect_read_plan_reads(plan: collect_read_plan(subject: s, token_app: gunbai_ci_declared, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77))) == 0 + && count(collect_read_plan_reads(plan: collect_read_plan(subject: s, token_app: s.registration.app, token_installation: other_installation_id, floor_job_id: 501, run_id: 77))) == 0 + && (match collect_read_plan(subject: s, token_app: gunbai_ci_declared, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77) { + CollectReadsRefused { token_app: _, token_installation: _ } => true + CollectReadsPlanned { job_id: _, run_id: _ } => false + }) + && (match collect_read_plan(subject: s, token_app: s.registration.app, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77) { + CollectReadsPlanned { job_id: 501, run_id: 77 } => true + _ => false + }) && (s.host as String) == (h.host as String) && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (expected.floor_job_name as String) == "qualification floor" From ea89826dd4d112f4a6a96f522e93cff06a987f94 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 19:10:07 +0000 Subject: [PATCH 35/44] leg 3 RC item 2 by sha-checkout (decision eager-gull-22): the floor job checks out inputs.qualification_revision first and refuses on a mismatch; the residual declared - qualification_revision_dispatch_input; the dispatch sends both the attempt label and the revision - qualification_floor_revision_guard_step: fetch + checkout the revision, exit 1 unless HEAD equals it; the floor contract (qualification_floor_admission) requires it as the FIRST step and refuses any step or job that runs past a failure (if:, continue-on-error); emission is the dispatch leg's wiring_owed - REDs: revision input undeclared, no guard, guard not first, later step with if: always(), later step with continue-on-error; the guard compares HEAD to the input and exits 1; post-run head_sha refusal remains RunRevisionNotPinned - RFM qualification_floor_guard_skippable_by_a_workflow_file_edit: a rewritten workflow file on the qualification branch can drop the guard (needs repo push); rung, ceiling, next trigger Co-Authored-By: Claude Opus 5.5 (1M context) --- ...uard_skippable_by_a_workflow_file_edit.dag | 26 ++++++ .../runner/runner_qualification_dispatch.dag | 82 ++++++++++++++++- .../runner_throughput_qualification_route.dag | 2 +- ...er_qualification_dispatch_witness_test.dag | 87 +++++++++++++++++-- 4 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/qualification_floor_guard_skippable_by_a_workflow_file_edit.dag diff --git a/dag/gunbc/recurring_failure_mode/qualification_floor_guard_skippable_by_a_workflow_file_edit.dag b/dag/gunbc/recurring_failure_mode/qualification_floor_guard_skippable_by_a_workflow_file_edit.dag new file mode 100644 index 00000000000..c5d3bf017db --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/qualification_floor_guard_skippable_by_a_workflow_file_edit.dag @@ -0,0 +1,26 @@ +module gunbc.recurring_failure_mode.qualification_floor_guard_skippable_by_a_workflow_file_edit + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, decl_ref } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data qualification_floor_guard_skippable_by_a_workflow_file_edit: RecurringFailureMode = RecurringFailureMode { + identity: "qualification_floor_guard_skippable_by_a_workflow_file_edit" as NonEmptyStr, + + receipts: [ + "**the qualification floor's revision guard is skippable by an edit to the workflow file on the qualification branch** (declared with sha-checkout, decision eager-gull-22 2026-10-04).", + + "INVALID STATE: a qualification run whose floor job executes code other than the pinned revision while the run reports the pinned head. The dispatch names the per-attempt branch refs/heads/qualification/; GitHub runs that branch's copy of the workflow file, and the guard that checks out inputs.qualification_revision and refuses on a mismatch is a step INSIDE that file (gunbc.runner.runner_qualification_dispatch qualification_floor_revision_guard_step). An actor who rewrites the workflow file on that branch -- deleting the guard, or adding steps before it -- runs other code on the attempt's runner.", + + "HARM: a qualification receipt measured on code that is not the pinned workload, on the dedicated host. Bounded by who can cause it: writing the branch's workflow file needs repository push (contents write), an actor already trusted to change any code in the repository, and the per-attempt branch is created by the route at the pinned revision immediately before dispatch.", + + "DISTINGUISHING FACTS: the guard's absence is caught by the floor contract only for the MODEL the subject mint reads (qualification_floor_admission refuses a floor job whose first step is not the guard, or whose later steps run past a failure); it is not caught for the file GitHub actually executes from the branch. The post-run check RunRevisionNotPinned (gunbc.runner.runner_qualification_dispatch decide_qualification_run) refuses a run whose head_sha is not the pinned revision, but an edit committed on the branch moves head_sha too, so that check catches a moved branch and not a rewritten file at the same head.", + + "RUNG: 2 for a moved branch (the guard and RunRevisionNotPinned each refuse, before any step and after the run). 1 for a rewritten workflow file at the branch head: contained by repository-push trust, not refused. CEILING: 3 needs the executed workflow file to be an identity the route admits rather than whatever the branch holds -- e.g. a reusable workflow selected by full SHA in the dedicated group (GitHub admits a SHA pin only for REUSABLE workflows), with the guard inside it. NEXT TRIGGER: the qualification mode lands in gunbc.fleet_converge_workflow (the dispatch leg's wiring_owed); deciding there whether the floor job is a SHA-selected reusable workflow is what would climb this row.", + ], + + evidence: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_floor_revision_guard_step"), + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "qualification_floor_admission"), + ], +} diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 7272131c93d..d20bb2c47cd 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -15,7 +15,9 @@ import extdeps.github.app { GitHubAppInstallationId } import extdeps.github.actions { Workflow, Job, DispatchInput, InputString, RunnerSpec, RunsOnExpression, HostedRunner, SelfHosted, WorkflowDispatch, Push, PullRequest, Schedule, WorkflowCall, WorkflowRunCompleted, MergeGroup, + Step, RunStep, UsesStep, } +import extdeps.languages.yaml.types { kv, yaml_string } import extdeps.github.expressions { ingest_template, context_name, Expression, Interpolation, LiteralText, ContextAccess, FunctionCall, StringLiteral, LogicalOr, LogicalAnd, Equals, NotEquals, @@ -91,8 +93,64 @@ fn qualification_workflow_file_name() -> NonEmptyStr { } } -fn qualification_dispatch_inputs(attempt: NonEmptyStr) -> Map { - map_insert(m: empty_map(), key: qualification_attempt_dispatch_input.name, value: attempt as String) +// THE REVISION THE FLOOR RUNS IS AN INPUT TOO (decision: eager-gull-22, 2026-10-04 -- sha-checkout). +// A dispatch names a branch, and a branch can move between the pin's readback and the run; a ruleset +// could not stop a delete-then-recreate, and the App that would administer it could remove it. So the +// floor job itself checks out exactly this revision as its FIRST step and refuses before any other +// step on a mismatch (qualification_floor_revision_guard_step), and collection refuses a run whose +// head_sha is not the pinned revision (RunRevisionNotPinned). The residual -- an edit to the workflow +// file on the qualification branch can drop the guard -- needs repository push, and is declared as +// gunbc.recurring_failure_mode qualification_floor_guard_skippable_by_a_workflow_file_edit. +data qualification_revision_dispatch_input: DispatchInput = DispatchInput { + name: "qualification_revision", + description: Present { value: "the pinned revision the floor must run; the floor job's first step checks it out and refuses on a mismatch before any other step" }, + required: true, + default: none, + type: InputString, +} + +fn qualification_dispatch_inputs(attempt: NonEmptyStr, revision: CommitSha) -> Map { + map_insert( + m: map_insert(m: empty_map(), key: qualification_attempt_dispatch_input.name, value: attempt as String), + key: qualification_revision_dispatch_input.name, + value: revision as String, + ) +} + +// THE GUARD, AS THE ONE STEP THE FLOOR JOB MUST BEGIN WITH. It fetches and checks out the revision +// input into the runner's empty workspace and fails the job unless HEAD is exactly that revision; GitHub +// skips every later step of a failed job unless the step opts in with an if: condition, which the floor +// contract forbids (qualification_floor_admission). Its emission into the generated fleet-converge +// workflow is owed with the qualification mode (the dispatch leg's wiring_owed). +fn qualification_floor_revision_guard_step() -> Step { + RunStep { + name: Present { value: "Check out the pinned qualification revision and refuse on any mismatch" }, + id: Present { value: "qualification_revision_guard" }, + run: join([ + "set -euo pipefail", + "git init -q .", + "git remote add origin \"https://github.com/$GITHUB_REPOSITORY\"", + "git fetch -q --no-tags --depth=1 origin \"$QUALIFICATION_REVISION\"", + "git checkout -q --force --detach FETCH_HEAD", + "observed=\"$(git rev-parse HEAD)\"", + "if [ \"$observed\" != \"$QUALIFICATION_REVISION\" ]; then echo \"QualificationRevisionMismatch: expected=$QUALIFICATION_REVISION observed=$observed\" >&2; exit 1; fi", + ], "\n"), + shell: none, + env: Present { value: [kv(key: "QUALIFICATION_REVISION", value: yaml_string(s: "${{ inputs.qualification_revision }}"))] }, + working_directory: none, + if_condition: none, + continue_on_error: none, + timeout_minutes: none, + } +} + +fn step_runs_after_a_failure(step: Step) -> Bool { + match step { + RunStep { name: _, id: _, run: _, shell: _, env: _, working_directory: _, if_condition: c, continue_on_error: e, timeout_minutes: _ } => + (match c { Present { value: _ } => true Absent => false }) || (match e { Present { value: v } => v Absent => false }) + UsesStep { name: _, id: _, uses: _, with: _, env: _, if_condition: c, continue_on_error: e, timeout_minutes: _ } => + (match c { Present { value: _ } => true Absent => false }) || (match e { Present { value: v } => v Absent => false }) + } } // ── THE SUBJECT ─────────────────────────────────────────────────────────────────────────────── @@ -118,6 +176,8 @@ type QualificationSubjectRefusal | SubjectFloorJobAbsent { job_id: NonEmptyStr } | SubjectFloorRunsOnNotTheAttemptInput { job_id: NonEmptyStr, runs_on: String } | SubjectFloorNameNotUnique { name: NonEmptyStr, jobs_with_name: Int } + | SubjectFloorRevisionGuardNotFirst { job_id: NonEmptyStr } + | SubjectFloorStepRunsPastTheGuard { job_id: NonEmptyStr } type QualificationSubjectStanding = QualificationSubjectAdmitted { subject: QualificationDispatchSubject } @@ -207,6 +267,8 @@ type QualificationFloorAdmission fn qualification_floor_admission(workflow: Workflow, floor_job_id: NonEmptyStr) -> QualificationFloorAdmission { if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_attempt_dispatch_input) { QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_attempt_dispatch_input.name } } + } else if !workflow_declares_dispatch_input(workflow: workflow, input: qualification_revision_dispatch_input) { + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: qualification_revision_dispatch_input.name } } } else { match first(filter(workflow.jobs, j => j.id == (floor_job_id as String))) { Absent => QualificationFloorRefused { refusal: SubjectFloorJobAbsent { job_id: floor_job_id } } @@ -219,7 +281,19 @@ fn qualification_floor_admission(workflow: Workflow, floor_job_id: NonEmptyStr) if count(sharing) != 1 { QualificationFloorRefused { refusal: SubjectFloorNameNotUnique { name: name, jobs_with_name: count(sharing) } } } else { - QualificationFloorAdmitted { floor_job_name: name } + match first(job.steps) { + Absent => QualificationFloorRefused { refusal: SubjectFloorRevisionGuardNotFirst { job_id: floor_job_id } } + Present { value: s0 } => + if s0 != qualification_floor_revision_guard_step() { + QualificationFloorRefused { refusal: SubjectFloorRevisionGuardNotFirst { job_id: floor_job_id } } + } else if job.steps |> any(st => step_runs_after_a_failure(step: st)) { + QualificationFloorRefused { refusal: SubjectFloorStepRunsPastTheGuard { job_id: floor_job_id } } + } else if match job.continue_on_error { Present { value: v } => v Absent => false } { + QualificationFloorRefused { refusal: SubjectFloorStepRunsPastTheGuard { job_id: floor_job_id } } + } else { + QualificationFloorAdmitted { floor_job_name: name } + } + } } } } @@ -422,7 +496,7 @@ fn dispatch_qualification_floor( repo: gunbc_repository.name as NonEmptyStr, workflow_id: qualification_workflow_file_name(), git_ref: subject.pin.name.branch, - inputs: qualification_dispatch_inputs(attempt: subject.attempt), + inputs: qualification_dispatch_inputs(attempt: subject.attempt, revision: subject.pin.revision), ) match qualification_dispatch_standing(performance: answer.performance) { DispatchStandingSucceeded => QualificationDispatched { run: DispatchedQualificationRun { subject: subject, receipt: answer.receipt } } diff --git a/dag/gunbc/runner/runner_throughput_qualification_route.dag b/dag/gunbc/runner/runner_throughput_qualification_route.dag index ef28bea5a39..516968e18a8 100644 --- a/dag/gunbc/runner/runner_throughput_qualification_route.dag +++ b/dag/gunbc/runner/runner_throughput_qualification_route.dag @@ -239,7 +239,7 @@ data mtcollins1_registration_leg: RouteLegStanding = LegAuthorityImplemented { data mtcollins1_dispatch_leg: RouteLegStanding = LegAuthorityImplemented { authority: decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), - wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a floor job in fleet_converge_workflow whose runs-on is exactly the qualification_attempt_dispatch_input it declares, and a route fold that takes the slot host's unit hold, ensures the per-attempt pin (gunbc.runner.runner_qualification_ref_pin ensure_qualification_ref_pin: branch qualification/ at the revision, the dedicated group pinned to it), mints the registration into that group, builds qualification_dispatch_subject from the pin, calls dispatch_qualification_floor and then collect_qualification_run, reconciling a commit-ambiguous dispatch before any retry, and removes the pin on every exit; SUFFICIENT FOR the floor run of each attempt being served only by the runner minted for that attempt at the pinned revision and read back as collected or as a typed refusal" as NonEmptyStr, + wiring_owed: "the gunbc.fleet_converge_workflow mode that lands with the boot leg: a floor job in fleet_converge_workflow whose runs-on is exactly the qualification_attempt_dispatch_input it declares and whose first step is qualification_floor_revision_guard_step over the declared qualification_revision_dispatch_input, with no step that runs past a failure, and a route fold that takes the slot host's unit hold, ensures the per-attempt pin (gunbc.runner.runner_qualification_ref_pin ensure_qualification_ref_pin: branch qualification/ at the revision, the dedicated group pinned to it), mints the registration into that group, builds qualification_dispatch_subject from the pin, calls dispatch_qualification_floor and then collect_qualification_run, reconciling a commit-ambiguous dispatch before any retry, and removes the pin on every exit; SUFFICIENT FOR the floor run of each attempt being served only by the runner minted for that attempt at the pinned revision and read back as collected or as a typed refusal" as NonEmptyStr, } data mtcollins1_deregistration_leg: RouteLegStanding = LegAuthorityImplemented { diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index f6d4430315d..ee5c45efd48 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -7,7 +7,7 @@ import extdeps.transports.rest { RestExchangeSucceeded, RestExchangeStatusRefused, RestExchangeUnreached, RestOutcome, RestOk, RestTransportRefused, RestStatusRefused, RestBodyUndecodable, RestMutationExchange, classify_rest_outcome, } -import extdeps.github.actions { Workflow, Job, WorkflowDispatch, RunsOnExpression, SelfHosted, RunnerSpec } +import extdeps.github.actions { Workflow, Job, WorkflowDispatch, RunsOnExpression, SelfHosted, RunnerSpec, Step, RunStep, UsesStep } import extdeps.github.effect { DispatchWorkflow, GitHubOrganizationRef } import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, @@ -31,7 +31,8 @@ import gunbc.runner.runner_qualification_ref_pin { qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, } import gunbc.runner.runner_qualification_dispatch { - qualification_attempt_dispatch_input, qualification_dispatch_inputs, qualification_dispatch_effect, + qualification_attempt_dispatch_input, qualification_revision_dispatch_input, qualification_dispatch_inputs, qualification_dispatch_effect, + qualification_floor_revision_guard_step, SubjectFloorRevisionGuardNotFirst, SubjectFloorStepRunsPastTheGuard, QualificationSubjectAdmitted, QualificationSubjectRefused, SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, SubjectFloorNameNotUnique, SubjectDeliveryForAnotherDispatch, @@ -85,12 +86,23 @@ fn group_with_id(id: Int, workflow: NonEmptyStr) -> MicrovmRunnerGroupStanding { } fn floor_job(runs_on: RunnerSpec) -> Job { + floor_job_with_steps(runs_on: runs_on, steps: [qualification_floor_revision_guard_step(), floor_work_step(if_condition: none)]) +} + +fn floor_work_step(if_condition: String?) -> Step { + RunStep { + name: Present { value: "run the floor" }, id: none, run: "claim_executor --required-ci", + shell: none, env: none, working_directory: none, if_condition: if_condition, continue_on_error: none, timeout_minutes: none, + } +} + +fn floor_job_with_steps(runs_on: RunnerSpec, steps: List) -> Job { Job { id: floor_job_id as String, name: Present { value: "qualification floor" }, runner: runs_on, environment: none, - steps: [], + steps: steps, needs: [], env: none, outputs: none, @@ -108,7 +120,7 @@ fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { Workflow { name: "fleet-converge", run_name: none, - on: [WorkflowDispatch { inputs: if declares_input { [qualification_attempt_dispatch_input] } else { [] } }], + on: [WorkflowDispatch { inputs: if declares_input { [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] } else { [qualification_revision_dispatch_input] } }], concurrency: none, jobs: [floor_job(runs_on: runs_on)], env: none, @@ -159,8 +171,12 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (expected.floor_job_name as String) == "qualification floor" && (expected.revision as String) == (revision as String) - && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt))) == 1 - && (match map_get(qualification_dispatch_inputs(attempt: s.attempt), qualification_attempt_dispatch_input.name) { + && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision))) == 2 + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision), qualification_revision_dispatch_input.name) { + Present { value: v } => v == (revision as String) + Absent => false + }) + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision), qualification_attempt_dispatch_input.name) { Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: h.host, attempt: attempt) |> any(l => l == v)) Absent => false }) @@ -296,7 +312,7 @@ fn workflow_sharing_the_floor_name() -> Workflow { Workflow { name: "fleet-converge", run_name: none, - on: [WorkflowDispatch { inputs: [qualification_attempt_dispatch_input] }], + on: [WorkflowDispatch { inputs: [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] }], concurrency: none, jobs: [floor_job(runs_on: attempt_runs_on), helper], env: none, @@ -566,3 +582,60 @@ test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { _ => false }) } + +// THE FLOOR CONTRACT CHECKS OUT THE PINNED REVISION FIRST AND NOTHING RUNS PAST A REFUSAL +// (decision: eager-gull-22, 2026-10-04). The guard is the floor job's FIRST step; on a mismatch it exits +// nonzero, and GitHub skips every later step of a failed job unless the step opts back in -- so the +// contract also refuses any step with an if: condition or continue-on-error, and a job with +// continue-on-error. REDs: the revision input undeclared; no guard; the guard after another step; a +// later step with if: always(); a later step with continue-on-error. The guard itself compares HEAD with +// the revision input and exits 1 on a mismatch. The post-run head_sha refusal is +// RunRevisionNotPinned (an_unfinished_unpinned_or_unreadable_run_refuses). +fn floor_refused(workflow: Workflow) -> Bool { + match qualification_floor_admission(workflow: workflow, floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorRevisionGuardNotFirst { job_id: _ } } => true + QualificationFloorRefused { refusal: SubjectFloorStepRunsPastTheGuard { job_id: _ } } => true + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "qualification_revision" + _ => false + } +} + +fn workflow_with_floor(job: Job, inputs_declare_revision: Bool) -> Workflow { + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: if inputs_declare_revision { [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] } else { [qualification_attempt_dispatch_input] } }], + concurrency: none, + jobs: [job], + env: none, + permissions: none, + } +} + +test fn the_floor_checks_out_the_pinned_revision_first_and_nothing_runs_past_a_refusal() -> Bool { + let guard = qualification_floor_revision_guard_step() + let work = floor_work_step(if_condition: none) + let tolerant = RunStep { + name: Present { value: "run the floor anyway" }, id: none, run: "claim_executor --required-ci", + shell: none, env: none, working_directory: none, if_condition: none, continue_on_error: Present { value: true }, timeout_minutes: none, + } + (match qualification_floor_admission(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, work]), inputs_declare_revision: true), floor_job_id: floor_job_id) { + QualificationFloorAdmitted { floor_job_name: _ } => true + QualificationFloorRefused { refusal: _ } => false + }) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, work]), inputs_declare_revision: false)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [work]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [work, guard]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, floor_work_step(if_condition: Present { value: "always()" })]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, tolerant]), inputs_declare_revision: true)) + && (match guard { + RunStep { name: _, id: _, run: r, shell: _, env: e, working_directory: _, if_condition: c, continue_on_error: ce, timeout_minutes: _ } => + string_contains(s: r, pattern: "git rev-parse HEAD") + && string_contains(s: r, pattern: "!= \"$QUALIFICATION_REVISION\"") + && string_contains(s: r, pattern: "exit 1") + && (match c { Absent => true Present { value: _ } => false }) + && (match ce { Absent => true Present { value: _ } => false }) + && (match e { Present { value: kvs } => kvs |> any(x => x.key == "QUALIFICATION_REVISION") Absent => false }) + UsesStep { name: _, id: _, uses: _, with: _, env: _, if_condition: _, continue_on_error: _, timeout_minutes: _ } => false + }) +} From b49e0e23c0dfecd6e5283eb9d62018d79867de3c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 00:12:34 +0000 Subject: [PATCH 36/44] leg 3 RC (review 5408711178): receive_jit_mint and the generate fold admit only the response-bound path and Bool claims; the delivery fixture hands back a plan, not a delivery; collection reads live inside the token join (no _under); three executed REDs Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/github_effect_perform.dag | 10 +++- dag/gunbc/runner/runner_jit_perform.dag | 22 ++++++- .../runner/runner_qualification_dispatch.dag | 57 +++++++++---------- .../runner_attempt_launch_witness_test.dag | 55 ++++++++++-------- ...ification_delivery_rewrap_witness_test.dag | 40 ++++++++++++- ...qualification_collection_relabel_probe.dag | 19 +++++++ ...ification_generate_answer_rewrap_probe.dag | 15 +++++ ...fication_receive_jit_mint_rewrap_probe.dag | 21 +++++++ 8 files changed, 181 insertions(+), 58 deletions(-) create mode 100644 dag/test/probe/qualification_collection_relabel_probe.dag create mode 100644 dag/test/probe/qualification_generate_answer_rewrap_probe.dag create mode 100644 dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index d195d807d6b..3e395837079 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -2,6 +2,7 @@ module gunbc.github_effect_perform import std.types { String, NonEmptyStr, Int, List, Secret, EpochSecs } import std.resources { Network } +import std.decl_ref { decl_ref } import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.clock { clock_unix_millis_read, ClockUnixMillisObserved, ClockUnixMillisRefused, epoch_secs_of_millis, @@ -144,11 +145,18 @@ type JitMintAppJwt // same jit_config_mint_outcome every status goes through, so an empty blob under 201 is still // refused there; a non-2xx the remote DECIDED is a JitConfigMintOutcome refusal; an answer that // never arrived is commit-ambiguous and is NOT reported as a refusal. +// +// ADMITTED ONLY FROM THE PERFORM FUNCTION AND A CLAIM. This fold takes the dispatch and the answer +// separately, so an open call would mint a delivery for dispatch A from any 201 body; the perform +// function is the one caller whose answer is GitHub's response to dispatch.request. fn jit_mint_performance_of_generate( dispatch: JitMintDispatch, outcome: RestOutcome, created: JitConfigCreatedBody, -) -> JitMintPerformance { +) -> JitMintPerformance admit_callers: [ + decl_ref(module_path: "gunbc.github_effect_perform", decl_name: "perform_organization_jit_mint"), + decl_ref(module_path: "test.claim.github_effect_perform_witness", decl_name: "witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered"), +] { match classify_rest_outcome(standing: RestMutationExchange { rest: outcome }) { RestExchangeSucceeded => JitMintReceived { diff --git a/dag/gunbc/runner/runner_jit_perform.dag b/dag/gunbc/runner/runner_jit_perform.dag index 7b6a2c7d42d..dc3058a8f09 100644 --- a/dag/gunbc/runner/runner_jit_perform.dag +++ b/dag/gunbc/runner/runner_jit_perform.dag @@ -139,7 +139,7 @@ fn dispatch_jit_mint( decl_ref(module_path: "test.claim.github_app_registry", decl_name: "dispatched"), decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_jit_dispatch_refuses_when_the_attempt_binding_is_deferred"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), - decl_ref(module_path: "test.claim.runner_attempt_launch_witness", decl_name: "received_for"), + decl_ref(module_path: "test.claim.runner_attempt_launch_witness", decl_name: "planned_with"), decl_ref(module_path: "test.claim.runner.runner_microvm_slot_controller_witness_test", decl_name: "delivered_mint"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), @@ -228,10 +228,28 @@ type JitCredentialDelivery | JitCredentialNotDispatched { refusal: JitDispatchRefusal } | JitCredentialMintNotCreated { outcome: JitConfigMintOutcome } +// THE MINT IS ADMITTED ONLY WHERE A RESPONSE IS BOUND TO ITS REQUEST. receive_jit_mint pairs a +// dispatch with an outcome it is handed, so an open call relabels any delivery's credential under any +// authorized dispatch: receive_jit_mint(dispatch: A, outcome: JitConfigMinted { B's credential, B's +// runner id }) carries bound.dispatch == A and passes every equality on the dispatch. Production +// reaches it only through gunbc.github_effect_perform jit_mint_performance_of_generate, which is +// itself admitted only from the perform function that sends dispatch.request and decodes GitHub's +// answer to that request. Every other admitted caller is a claim returning Bool, or a fixture +// returning a value that carries no delivery, so no admitted edge hands a delivery back out. fn receive_jit_mint( dispatch: JitMintDispatch, outcome: JitConfigMintOutcome -) -> JitCredentialDelivery { +) -> JitCredentialDelivery admit_callers: [ + decl_ref(module_path: "gunbc.github_effect_perform", decl_name: "jit_mint_performance_of_generate"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_refused_dispatch_delivers_nothing_even_from_a_minted_credential"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_an_authorized_dispatch_delivers_nothing_when_the_mint_was_not_created"), + decl_ref(module_path: "test.claim.github_app_registry", decl_name: "witness_a_minted_credential_is_bound_to_the_dispatched_attempt"), + decl_ref(module_path: "test.claim.runner.runner_microvm_slot_controller_witness_test", decl_name: "delivered_mint"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_real_mint_admits_the_attempt_through_the_sealed_subject"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input"), + decl_ref(module_path: "test.claim.runner_qualification_dispatch_witness", decl_name: "a_delivery_from_another_dispatch_on_the_same_slot_refuses"), + decl_ref(module_path: "test.claim.runner_attempt_launch_witness", decl_name: "planned_with"), +] { match dispatch { JitMintDispatchRefused { refusal: r } => JitCredentialNotDispatched { refusal: r } JitMintDispatchAuthorized { dispatch: authorized } => { diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index d20bb2c47cd..9981cfd3011 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -683,6 +683,9 @@ fn token_names_registration(token_app: DeclaredGitHubApp, token_installation: Gi token_app == subject.registration.app && token_installation == subject.registration.installation } +// ONE EFFECTFUL ENTRY. The join is not a wrapper a caller may route around: the reads and the mint of +// CollectedQualificationRun live in the arm the join admits, so there is no second function that +// reads under an unchecked token (a split helper was exactly that bypass). fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: dispatched.subject) { QualificationRunRefused { @@ -690,35 +693,31 @@ fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: Cont refusal: RunCollectedUnderAnotherInstallation { token_app: token.app, token_installation: token.installation }, } } else { - collect_qualification_run_under(dispatched: dispatched, token: token) - } -} - -fn collect_qualification_run_under(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { - let run_id = dispatched.receipt.workflow_run_id - let owner = gunbc_repository.owner as NonEmptyStr - let repo = gunbc_repository.name as NonEmptyStr - let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) - let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: dispatched_run_attempt) - match decide_qualification_run( - subject: qualification_run_expectation(subject: dispatched.subject), - run_id: run_id, - run_performance: read.performance, - run: read.run, - jobs_performance: jobs.performance, - jobs: jobs.jobs, - ) { - RunVerdictRefused { refusal: r } => QualificationRunRefused { run_id: run_id, refusal: r } - RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } => - QualificationRunCollected { - collected: CollectedQualificationRun { - dispatched: dispatched, - run: run, - floor_job: floor, - conclusion: conclusion, - instruments_unextracted: qualification_instruments, - }, - } + let run_id = dispatched.receipt.workflow_run_id + let owner = gunbc_repository.owner as NonEmptyStr + let repo = gunbc_repository.name as NonEmptyStr + let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) + let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: dispatched_run_attempt) + match decide_qualification_run( + subject: qualification_run_expectation(subject: dispatched.subject), + run_id: run_id, + run_performance: read.performance, + run: read.run, + jobs_performance: jobs.performance, + jobs: jobs.jobs, + ) { + RunVerdictRefused { refusal: r } => QualificationRunRefused { run_id: run_id, refusal: r } + RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } => + QualificationRunCollected { + collected: CollectedQualificationRun { + dispatched: dispatched, + run: run, + floor_job: floor, + conclusion: conclusion, + instruments_unextracted: qualification_instruments, + }, + } + } } } diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index 47120282805..70e0de0b543 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -141,14 +141,24 @@ fn minted_credential(encoded: String) -> List { // something that is not a whole credential, handed on as if it were. data short_blob: String = "ZmFrZS1qaXQtY29uZmln" -// THE DELIVERY IS MINTED, NOT HAND-BUILT. A successful delivery is a sealed BoundJitCredential that only -// receive_jit_mint constructs, carrying the authorized dispatch that produced it; this fixture takes the -// real route for the attempt under test -- an owned cell binding, dispatch_jit_mint (which admits this -// helper by name), and receive_jit_mint over upstream's own 201 outcome fold with the encoding the -// claim chooses. -fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance { - match dispatch_jit_mint( - binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: attempt, owned: true) }, +// THE DELIVERY IS MINTED, NOT HAND-BUILT, AND IT NEVER LEAVES THIS FUNCTION. A successful delivery is +// a sealed BoundJitCredential that only receive_jit_mint constructs, carrying the authorized dispatch +// that produced it; this fixture takes the real route for the credential's attempt -- an owned cell +// binding, dispatch_jit_mint and receive_jit_mint over upstream's own 201 outcome fold, both of which +// admit this function by name -- and hands back only the PLAN the planner built from it. A fixture +// that returned the delivery would be a public proxy for the mint (an admitted call edge does not +// confine a value the caller hands back), so a plan, which carries no delivery, is the only egress. +// minted_for differs from attempt in the claim about a credential bound to a sibling attempt. +fn planned_with( + pair: IssuedPair, + account: MoneyAccount, + attempt: MicroVmAttempt, + minted_for: MicroVmAttempt, + encoded: String, + ground: AttemptGround, +) -> AttemptLaunchPlan { + let mint = match dispatch_jit_mint( + binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: minted_for, owned: true) }, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, @@ -164,10 +174,7 @@ fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance } JitMintDispatchRefused { refusal: _ } => JitMintAppJwtUnsigned { detail: "fixture: the attempt's dispatch was refused" } } -} - -fn live_grant(attempt: MicroVmAttempt) -> JitMintPerformance { - received_for(attempt: attempt, encoded: doubled(s: "A", k: 10)) + plan_for(pair: pair, account: account, attempt: attempt, mint: mint, ground: ground) } fn plan_for( @@ -230,7 +237,7 @@ test fn a_below_floor_credential_stages_no_device_and_starts_no_vmm() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 1, id: "cred0") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: short_blob), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: short_blob, ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialBelowFloor { bytes: n }, tail: _ } => byte_size_count(b: n) == 20 @@ -254,7 +261,7 @@ test fn a_credential_at_the_ceiling_in_code_points_but_over_it_in_bytes_is_refus match accepted_attempt(i: 7, id: "multibyte") { Cons { head: a, tail: _ } => { let wide = doubled(s: "\u{00e9}", k: 16) - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: wide), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: wide, ground: GroundClear) string_length(wide) == 65536 && byte_size_count(b: jit_drive_utf8_size(text: wide)) == 131072 && no_device_no_vmm(plan: plan) @@ -274,7 +281,7 @@ test fn an_above_ceiling_credential_stages_no_device_and_starts_no_vmm() -> Bool Cons { head: pair, tail: _ } => match accepted_attempt(i: 1, id: "credbig") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: doubled(s: "A", k: 17)), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 17), ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialAboveCeiling { bytes: _ }, tail: _ } => true @@ -296,7 +303,7 @@ test fn a_credential_bound_to_another_attempt_stages_no_device() -> Bool { Cons { head: a, tail: _ } => match accepted_attempt(i: 6, id: "theirs") { Cons { head: b, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: b), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: b, encoded: doubled(s: "A", k: 10), ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialBoundToDifferentAttempt { bound_to: other }, tail: _ } => other == MicroVmJitSlot { attempt: b } @@ -357,9 +364,9 @@ test fn an_unauthorized_grant_yields_zero_jailer_launches() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "unauth") { Cons { head: a, tail: _ } => - match plan_for(pair: pair, account: exhausted_account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) { + match planned_with(pair: pair, account: exhausted_account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) { LaunchRefusedUnauthorized { authorization: _ } => - no_device_no_vmm(plan: plan_for(pair: pair, account: exhausted_account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear)) + no_device_no_vmm(plan: planned_with(pair: pair, account: exhausted_account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear)) _ => false } Empty {} => false @@ -373,9 +380,9 @@ test fn prior_resources_refuse_rather_than_reuse() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 3, id: "prior") { Cons { head: a, tail: _ } => - match plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundOccupied) { + match planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundOccupied) { LaunchRefusedPriorResources => - no_device_no_vmm(plan: plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundOccupied)) + no_device_no_vmm(plan: planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundOccupied)) _ => false } Empty {} => false @@ -399,7 +406,7 @@ test fn a_live_grant_stages_exactly_one_device_and_records_the_registration() -> Cons { head: pair, tail: _ } => match accepted_attempt(i: 4, id: "ok1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) planned_jailer_launches(plan: plan) == 1 && match plan { LaunchAuthorized { @@ -571,7 +578,7 @@ fn staging_with(jit_of: Bool, obs: WorkspaceStagingObservation, ground: AttemptG Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: ground) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: ground) staging_label(v: attempt_staging_verdict( plan: plan, jail: staged_jail_material(), @@ -621,7 +628,7 @@ fn jail_label(m: JailMaterialStaging) -> String { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) staging_label(v: attempt_staging_verdict( plan: plan, jail: m, @@ -673,7 +680,7 @@ fn admitted_receipt_names_this_attempts_paths() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) match attempt_staging_verdict(plan: plan, jail: staged_jail_material(), jit: staged_device_of(plan: plan), workspace: good_workspace()) { StagingAdmitsJailer { receipt: r } => r.bootstrap_device_path == concat(attempt_jail_chroot_root(attempt: a, firecracker_binary: "/bin/firecracker"), jail_jit_path as String) diff --git a/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag index ccc1726ef2f..a6994244df9 100644 --- a/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag @@ -30,10 +30,46 @@ fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int } } -data control_source: String = "module probe_qualification_delivery_control\nimport std.types { Bool }\nimport gunbc.runner.runner_jit_perform { BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, jit_credential_delivered }\nfn delivered(d: JitCredentialDelivery) -> Bool { jit_credential_delivered(delivery: d) }\n" +data control_source: String = "module probe_qualification_delivery_control\nimport std.types { Bool }\nimport gunbc.runner.runner_jit_perform { BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, jit_credential_delivered, receive_jit_mint }\nimport gunbc.github_effect_perform { jit_mint_performance_of_generate }\nimport gunbc.runner.runner_qualification_dispatch { CollectedQualificationRun }\nfn delivered(d: JitCredentialDelivery) -> Bool { jit_credential_delivered(delivery: d) }\n" +fn blocking_class(c: CompileDiagnosticCensus, wanted: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +fn probe(name: String) -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: concat(concat("dag/test/probe/", name), ".dag")).content) +} + +// ONE CONTROL FOR EVERY RED BELOW: a source that IMPORTS each sealed name -- the carriers and the two +// admitted mints -- and calls none of them refuses at none of the walls, so each red is the call or +// the literal, never the import. test fn a_source_that_only_names_the_carrier_forges_nothing() -> Bool { - blocking(c: census_of(source: control_source), wanted: "SoleConstructorViolation", subject: "BoundJitCredential") == 0 + let c = census_of(source: control_source) + blocking(c: c, wanted: "SoleConstructorViolation", subject: "BoundJitCredential") == 0 + && blocking_class(c: c, wanted: "SoleConstructorViolation") == 0 + && blocking_class(c: c, wanted: "ConstructorCallAdmissionRefused") == 0 +} + +// THE REWRAP WITH NO FORGED LITERAL (review 5408711178 item 1): delivery B's credential and runner id +// handed to receive_jit_mint under dispatch A. Each probe holds ONE route, so its refusal is that +// route's. +test fn a_rewrap_through_receive_jit_mint_refuses_at_its_admission() -> Bool { + blocking_class(c: probe(name: "qualification_receive_jit_mint_rewrap_probe"), wanted: "ConstructorCallAdmissionRefused") >= 1 +} + +test fn a_supplied_generate_answer_refuses_outside_the_perform_function() -> Bool { + blocking_class(c: probe(name: "qualification_generate_answer_rewrap_probe"), wanted: "ConstructorCallAdmissionRefused") >= 1 +} + +// item 2: collecting outside the token join is minting a collection the join never admitted. +test fn a_collection_minted_outside_the_token_join_refuses_at_the_sealed_carrier() -> Bool { + blocking(c: probe(name: "qualification_collection_relabel_probe"), wanted: "SoleConstructorViolation", subject: "CollectedQualificationRun") >= 1 } test fn a_rewrapped_delivery_refuses_at_the_sealed_carrier() -> Bool { diff --git a/dag/test/probe/qualification_collection_relabel_probe.dag b/dag/test/probe/qualification_collection_relabel_probe.dag new file mode 100644 index 00000000000..1df99eb5bdc --- /dev/null +++ b/dag/test/probe/qualification_collection_relabel_probe.dag @@ -0,0 +1,19 @@ +module test.probe.qualification_collection_relabel_probe + +// THIS MODULE MUST NOT RESOLVE. Collecting outside the token join is minting a collection the join +// never admitted: relabel another collection's run and floor job as dispatch d's. The carrier is +// sealed, so the one function that mints it -- gunbc.runner.runner_qualification_dispatch +// collect_qualification_run, whose reads sit in the arm the token join admits -- is the only route. +// Reached only through test.claim.runner_qualification_delivery_rewrap_witness. + +import gunbc.runner.runner_qualification_dispatch { DispatchedQualificationRun, CollectedQualificationRun } + +fn relabel(d: DispatchedQualificationRun, c: CollectedQualificationRun) -> CollectedQualificationRun { + CollectedQualificationRun { + dispatched: d, + run: c.run, + floor_job: c.floor_job, + conclusion: c.conclusion, + instruments_unextracted: c.instruments_unextracted, + } +} diff --git a/dag/test/probe/qualification_generate_answer_rewrap_probe.dag b/dag/test/probe/qualification_generate_answer_rewrap_probe.dag new file mode 100644 index 00000000000..f827885a5cc --- /dev/null +++ b/dag/test/probe/qualification_generate_answer_rewrap_probe.dag @@ -0,0 +1,15 @@ +module test.probe.qualification_generate_answer_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. The generate fold pairs a dispatch with a 201 body it is handed, so +// an outside caller would mint a delivery for dispatch A from any created body. Only the perform +// function, whose body is GitHub's answer to dispatch.request, is admitted. Reached only through +// test.claim.runner_qualification_delivery_rewrap_witness. + +import extdeps.transports.rest { RestOk } +import extdeps.github.actions_jit_runner { JitConfigCreatedBody } +import gunbc.runner.runner_jit_perform { JitMintDispatch } +import gunbc.github_effect_perform { JitMintPerformance, jit_mint_performance_of_generate } + +fn mint_from_a_supplied_body(a: JitMintDispatch, body: JitConfigCreatedBody) -> JitMintPerformance { + jit_mint_performance_of_generate(dispatch: a, outcome: RestOk, created: body) +} diff --git a/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag b/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag new file mode 100644 index 00000000000..ed6688ed778 --- /dev/null +++ b/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag @@ -0,0 +1,21 @@ +module test.probe.qualification_receive_jit_mint_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. It is the rewrap that needs no forged literal: hold an authorized +// dispatch A and a legitimate delivery B, and hand B's credential and runner id to receive_jit_mint +// under A. Unsealed, the result carries bound.dispatch == A and every equality on the dispatch admits +// it. The witness test.claim.runner_qualification_delivery_rewrap_witness requires the refusal at +// receive_jit_mint's admission. dag/test/probe/ sits under the whole-tree strict-resolve exclusion, +// so this source reaches the compiler only through that witness. + +import gunbc.runner.runner_jit_perform { + AuthorizedJitMintDispatch, BoundJitCredential, JitCredentialDelivery, JitMintDispatchAuthorized, + receive_jit_mint, +} +import extdeps.github.actions_jit_runner { JitConfigMinted } + +fn rewrap_through_the_receiver(a: AuthorizedJitMintDispatch, b: BoundJitCredential) -> JitCredentialDelivery { + receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: a }, + outcome: JitConfigMinted { config: b.credential, runner_id: b.runner_id }, + ) +} From 364148293377e5a28a3c3cbd69fd18187d5eb6c4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 00:45:00 +0000 Subject: [PATCH 37/44] leg 3b: confine the collector's reads to the checked entry (review on 4872c1e) collect_qualification_instruments_under takes only the sealed collection and the token, derives the job and run ids from the collection, and admits only collect_qualification_instruments (which runs the read plan first); read_claim_cost_artifact admits only _under. The enrolled forged probe adds (c) an outside call to _under and (d) a free-token, other-run claim-cost read, each required to refuse at its name; the names-only control stays at zero. Control: dropping _under's admit list reds exactly that claim. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 22 ++++++++++++++++--- ..._log_receipt_forged_probe_witness_test.dag | 14 ++++++++++++ ...ification_job_log_receipt_forged_probe.dag | 22 +++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index f0c0ff421d1..e782e4cdeaa 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -961,7 +961,12 @@ fn conclude_claim_cost_download(artifact_id: Int, performance: RestExchangePerfo } } -fn read_claim_cost_artifact(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> ClaimCostArtifactRead uses net: Network { +fn read_claim_cost_artifact(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> ClaimCostArtifactRead + uses net: Network + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments_under"), + ] +{ let listed = list_run_artifacts_with(token: token, owner: owner, repo: repo, run_id: run_id) let after = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) match conclude_claim_cost_listing(performance: listed.performance, listed: listed.listed) { @@ -1041,11 +1046,22 @@ fn collect_qualification_instruments(collected: CollectedQualificationRun, token run_id: collected.dispatched.receipt.workflow_run_id, ) { CollectReadsRefused { token_app: a, token_installation: i } => QualificationCollectUnderAnotherInstallation { token_app: a, token_installation: i } - CollectReadsPlanned { job_id: j, run_id: r } => collect_qualification_instruments_under(collected: collected, token: token, job_id: j, run_id: r) + CollectReadsPlanned { job_id: _, run_id: _ } => collect_qualification_instruments_under(collected: collected, token: token) } } -fn collect_qualification_instruments_under(collected: CollectedQualificationRun, token: ControllerInstallationToken, job_id: Int, run_id: Int) -> QualificationCollectOutcome uses net: Network { +// THE READS, CONFINED TO THE CHECKED ENTRY. Only collect_qualification_instruments may call this, and +// only after its read plan admitted the token; the job id and run id are the sealed collection's own, +// never arguments, so no caller can pair a collection with another run's id or read under a token the +// plan did not admit. +fn collect_qualification_instruments_under(collected: CollectedQualificationRun, token: ControllerInstallationToken) -> QualificationCollectOutcome + uses net: Network + admit_callers: [ + decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments"), + ] +{ + let job_id = collected.floor_job.id + let run_id = collected.dispatched.receipt.workflow_run_id let owner = gunbc_repository.owner as NonEmptyStr let repo = gunbc_repository.name as NonEmptyStr match collected.dispatched.subject.registration.slot { diff --git a/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag index 39289e42849..f61a111ca19 100644 --- a/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag +++ b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag @@ -42,6 +42,8 @@ test fn a_source_that_only_names_the_module_forges_nothing() -> Bool { let c = census_of(source: control_source) blocking(c: c, wanted: "SoleConstructorViolation", subject: "QualificationJobLogReceipt") == 0 && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "collect_qualification_instruments_under") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "read_claim_cost_artifact") == 0 } test fn a_forged_job_log_receipt_refuses_at_its_own_name() -> Bool { @@ -51,3 +53,15 @@ test fn a_forged_job_log_receipt_refuses_at_its_own_name() -> Bool { test fn the_receipt_minter_refuses_a_caller_other_than_the_collect_stage() -> Bool { blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") >= 1 } + +// THE COLLECTOR'S READS ARE REACHABLE ONLY THROUGH ITS READ PLAN. collect_qualification_instruments_under +// takes no job or run id -- they are the sealed collection's -- and admits only the checked entry, so a +// caller holding a genuine collection cannot read it under another installation's token. +test fn the_reads_refuse_a_caller_other_than_the_checked_entry() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "collect_qualification_instruments_under") >= 1 +} + +// AND THE CLAIM-COST READ CANNOT BE AIMED AT ANOTHER RUN: it admits only the confined reads above. +test fn the_claim_cost_read_refuses_a_free_token_and_run_id() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "read_claim_cost_artifact") >= 1 +} diff --git a/dag/test/probe/qualification_job_log_receipt_forged_probe.dag b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag index b6f83a07537..5e0c7f066a8 100644 --- a/dag/test/probe/qualification_job_log_receipt_forged_probe.dag +++ b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag @@ -2,17 +2,24 @@ module test.probe.qualification_job_log_receipt_forged_probe // THIS MODULE MUST NOT RESOLVE. Each function below is a route by which a caller outside // gunbc.runner.runner_qualification_dispatch would otherwise hold a job-log receipt for rows it never -// read from a run: (a) writing the sealed receipt from invented readings, and (b) calling the one -// function that mints it from outside the collect stage. The witness +// read from a run, or read a run's outputs under a token or run id the collector never admitted: (a) +// writing the sealed receipt from invented readings, (b) calling the one function that mints it from +// outside the collect stage, (c) calling the reads past the read plan, (d) reading the claim-cost +// artifact for a free token and run id. The witness // test.claim.qualification_job_log_receipt_forged_probe_witness hands this source to the compiler and // requires each refusal at its subject by name. dag/test/probe/ sits under the whole-tree // strict-resolve exclusion, so this source reaches the compiler only through that witness. import std.content_hash { content_hash_atom } +import std.types { Int, NonEmptyStr } +import std.resources { Network } +import gunbc.github_effect_perform { ControllerInstallationToken } import gunbc.runner.runner_host_unit_slot { HostUnitSlot } import gunbc.runner.runner_qualification_dispatch { CollectedQualificationRun, QualificationJobLogReceipt, QualificationLogReadings, JobLogInstrumentStanding, AttemptJobLog, job_log_standing, + QualificationCollectOutcome, collect_qualification_instruments_under, + ClaimCostArtifactRead, read_claim_cost_artifact, } // (a) AN OUTSIDE-AUTHORED RECEIPT. Refuses at QualificationJobLogReceipt. @@ -25,3 +32,14 @@ fn forged_receipt(c: CollectedQualificationRun, h: HostUnitSlot, rd: Qualificati fn sneaked_receipt(c: CollectedQualificationRun, h: HostUnitSlot, l: AttemptJobLog) -> JobLogInstrumentStanding { job_log_standing(collected: c, slot: h, log: l) } + +// (c) THE READS CALLED PAST THE READ PLAN: a genuine collection under a token the plan never admitted. +// Refuses at collect_qualification_instruments_under. +fn reads_past_the_plan(c: CollectedQualificationRun, t: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { + collect_qualification_instruments_under(collected: c, token: t) +} + +// (d) THE CLAIM-COST READ UNDER A FREE TOKEN AND ANOTHER RUN'S ID. Refuses at read_claim_cost_artifact. +fn claim_cost_for_another_run(t: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, other_run_id: Int) -> ClaimCostArtifactRead uses net: Network { + read_claim_cost_artifact(token: t, owner: owner, repo: repo, run_id: other_run_id) +} From 3e99b761ee2f56c66d5313e43db5ac13b7d4b635 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 00:46:39 +0000 Subject: [PATCH 38/44] leg 3: collection admission is a pure read plan (plan_qualification_collection over the dispatched subject and the token); collect performs only the planned reads; foreign-token RED inside the real-mint claim Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 53 +++++++++++++++---- ...er_qualification_dispatch_witness_test.dag | 13 +++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 9981cfd3011..bcc01d8ad4c 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -683,21 +683,54 @@ fn token_names_registration(token_app: DeclaredGitHubApp, token_installation: Gi token_app == subject.registration.app && token_installation == subject.registration.installation } -// ONE EFFECTFUL ENTRY. The join is not a wrapper a caller may route around: the reads and the mint of -// CollectedQualificationRun live in the arm the join admits, so there is no second function that -// reads under an unchecked token (a split helper was exactly that bypass). -fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { - if !token_names_registration(token_app: token.app, token_installation: token.installation, subject: dispatched.subject) { - QualificationRunRefused { - run_id: dispatched.receipt.workflow_run_id, - refusal: RunCollectedUnderAnotherInstallation { token_app: token.app, token_installation: token.installation }, +// THE COLLECTION'S ADMISSION IS A PLAN, DECIDED WITHOUT I/O: the reads it will make, or the refusal +// that makes none. collect_qualification_run performs exactly the planned reads and nothing else, so +// "a token for another App or installation reads nothing" is a claim about this pure function a +// witness executes over a really minted subject (test.claim.runner_qualification_dispatch_witness +// the_real_mint_admits_the_attempt_through_the_sealed_subject), not a property of an effectful path +// no test can reach. +type QualificationCollectionRead + = ReadQualificationRun { run_id: Int } + | ReadQualificationAttemptJobs { run_id: Int, attempt: Int } + +type QualificationCollectionPlan + = CollectionRefusedBeforeAnyRead { refusal: QualificationRunRefusal } + | CollectionReadsPlanned { run_id: Int, attempt: Int } + +fn plan_qualification_collection(subject: QualificationDispatchSubject, run_id: Int, token_app: DeclaredGitHubApp, token_installation: GitHubAppInstallationId) -> QualificationCollectionPlan { + if !token_names_registration(token_app: token_app, token_installation: token_installation, subject: subject) { + CollectionRefusedBeforeAnyRead { + refusal: RunCollectedUnderAnotherInstallation { token_app: token_app, token_installation: token_installation }, } } else { - let run_id = dispatched.receipt.workflow_run_id + CollectionReadsPlanned { run_id: run_id, attempt: dispatched_run_attempt } + } +} + +fn planned_collection_reads(plan: QualificationCollectionPlan) -> List { + match plan { + CollectionRefusedBeforeAnyRead { refusal: _ } => [] + CollectionReadsPlanned { run_id: id, attempt: n } => + [ReadQualificationRun { run_id: id }, ReadQualificationAttemptJobs { run_id: id, attempt: n }] + } +} + +// ONE EFFECTFUL ENTRY, AND IT PERFORMS ONLY WHAT THE PLAN NAMES: the reads take their run id and +// attempt from the CollectionReadsPlanned arm, and the refusal arm reaches no read. The reads and the +// mint of CollectedQualificationRun live in that arm, so there is no second function that reads under +// an unchecked token (a split helper was exactly that bypass). +fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: ControllerInstallationToken) -> QualificationRunCollection uses net: Network { + match plan_qualification_collection( + subject: dispatched.subject, run_id: dispatched.receipt.workflow_run_id, + token_app: token.app, token_installation: token.installation, + ) { + CollectionRefusedBeforeAnyRead { refusal: r } => + QualificationRunRefused { run_id: dispatched.receipt.workflow_run_id, refusal: r } + CollectionReadsPlanned { run_id: run_id, attempt: attempt } => { let owner = gunbc_repository.owner as NonEmptyStr let repo = gunbc_repository.name as NonEmptyStr let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) - let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: dispatched_run_attempt) + let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: attempt) match decide_qualification_run( subject: qualification_run_expectation(subject: dispatched.subject), run_id: run_id, diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag index ee5c45efd48..6558daa38c1 100644 --- a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.runner_qualification_dispatch_witness import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } +import std.algebra { Cons, Empty } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import product.placement_supply { HostIdentity } import extdeps.transports.rest { @@ -43,6 +44,8 @@ import gunbc.runner.runner_qualification_dispatch { qualification_dispatch_standing, QualificationDispatchStanding, DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, hold_covers_slot_host, token_names_registration, + plan_qualification_collection, planned_collection_reads, CollectionRefusedBeforeAnyRead, + ReadQualificationRun, ReadQualificationAttemptJobs, RunCollectedUnderAnotherInstallation, decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunOnAnotherBranch, RunNotCompleted, RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, @@ -167,6 +170,16 @@ test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { && token_names_registration(token_app: s.registration.app, token_installation: s.registration.installation, subject: s) && !token_names_registration(token_app: gunbai_ci_declared, token_installation: s.registration.installation, subject: s) && !token_names_registration(token_app: s.registration.app, token_installation: other_installation_id, subject: s) + && count(planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: gunbai_ci_declared, token_installation: s.registration.installation))) == 0 + && count(planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: s.registration.app, token_installation: other_installation_id))) == 0 + && (match plan_qualification_collection(subject: s, run_id: 77, token_app: gunbai_ci_declared, token_installation: s.registration.installation) { + CollectionRefusedBeforeAnyRead { refusal: RunCollectedUnderAnotherInstallation { token_app: a, token_installation: _ } } => a == gunbai_ci_declared + _ => false + }) + && (match planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: s.registration.app, token_installation: s.registration.installation)) { + Cons { head: ReadQualificationRun { run_id: 77 }, tail: Cons { head: ReadQualificationAttemptJobs { run_id: 77, attempt: 1 }, tail: Empty {} } } => true + _ => false + }) && (s.host as String) == (h.host as String) && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) && (expected.floor_job_name as String) == "qualification floor" From 7ebc19e01aa6076c4d5acc87089a2c00570589a6 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 01:00:31 +0000 Subject: [PATCH 39/44] leg 3: close the planned-reads arm (unterminated function body at 3e99b76) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runner/runner_qualification_dispatch.dag | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index bcc01d8ad4c..5e44d4149ff 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -727,29 +727,30 @@ fn collect_qualification_run(dispatched: DispatchedQualificationRun, token: Cont CollectionRefusedBeforeAnyRead { refusal: r } => QualificationRunRefused { run_id: dispatched.receipt.workflow_run_id, refusal: r } CollectionReadsPlanned { run_id: run_id, attempt: attempt } => { - let owner = gunbc_repository.owner as NonEmptyStr - let repo = gunbc_repository.name as NonEmptyStr - let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) - let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: attempt) - match decide_qualification_run( - subject: qualification_run_expectation(subject: dispatched.subject), - run_id: run_id, - run_performance: read.performance, - run: read.run, - jobs_performance: jobs.performance, - jobs: jobs.jobs, - ) { - RunVerdictRefused { refusal: r } => QualificationRunRefused { run_id: run_id, refusal: r } - RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } => - QualificationRunCollected { - collected: CollectedQualificationRun { - dispatched: dispatched, - run: run, - floor_job: floor, - conclusion: conclusion, - instruments_unextracted: qualification_instruments, - }, - } + let owner = gunbc_repository.owner as NonEmptyStr + let repo = gunbc_repository.name as NonEmptyStr + let read = read_run_with(token: token, owner: owner, repo: repo, run_id: run_id) + let jobs = read_attempt_jobs_with(token: token, owner: owner, repo: repo, run_id: run_id, attempt: attempt) + match decide_qualification_run( + subject: qualification_run_expectation(subject: dispatched.subject), + run_id: run_id, + run_performance: read.performance, + run: read.run, + jobs_performance: jobs.performance, + jobs: jobs.jobs, + ) { + RunVerdictRefused { refusal: r } => QualificationRunRefused { run_id: run_id, refusal: r } + RunVerdictCollected { run: run, floor_job: floor, conclusion: conclusion } => + QualificationRunCollected { + collected: CollectedQualificationRun { + dispatched: dispatched, + run: run, + floor_job: floor, + conclusion: conclusion, + instruments_unextracted: qualification_instruments, + }, + } + } } } } From ac790288bffdce99e374842f28a29c995ac503fc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 05:39:49 +0000 Subject: [PATCH 40/44] leg 3 (review 76234): the ruling record says only what the ruling says; covers is the single source of which rows take its discharge (branch-pin sites dropped: federated on their own parameters); claim pins the iff Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/auth/privileged_effect_census.dag | 9 ++++--- ...ization_pattern_selection_witness_test.dag | 24 +++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index 5c404ac8215..3ef50ec3910 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -549,7 +549,8 @@ fn present_trigger_declarations() -> List { // decides the pattern from the effect's parameters with the ruling as the witness discharge, conditional on // its interlock (D0's durable claim, rostered in privileged_effect_interlocks); the census does not assert it. // THE OPERATOR'S 2026-10-03 RUNNER-LIFECYCLE RULING, VERBATIM, AND ITS SCOPE INTERPRETATION AS A -// SEPARATE CITED FACT. The ruling text names registration and deregistration. Whether it also +// SEPARATE CITED FACT. The ruling text names registration and deregistration, and the ruling's +// effect_subject says no more than its text. Whether it also // covers dispatching the qualification floor is not in its words; that coverage is an // interpretation made by eager-gull-22 on 2026-10-04 and reported to the operator, who may narrow // it. The two are kept apart so narrowing the interpretation is an edit to one row that changes the @@ -558,7 +559,7 @@ fn present_trigger_declarations() -> List { // UnitHoldProof): the dispatch takes the proof and refuses unless it is the slot's own host's. data runner_lifecycle_operator_ruling: StandingDestructiveAuthorization = StandingDestructiveAuthorization { ruling_text: "Runner registration and deregistration are PRE-APPROVED. Approval moves up to the workflow dispatcher (the fleet-converge run's own authorization). There is no per-registration approval step. Classify this through gunbc.auth.authorization_pattern_selection, recording that ruling rather than a per-effect ntfy approval. (operator decision, 2026-10-03)" as NonEmptyStr, - effect_subject: "ephemeral JIT runner lifecycle: registration, deregistration, and the dispatch of the qualification floor to the attempt's own runner" as NonEmptyStr, + effect_subject: "ephemeral JIT runner lifecycle: registration and deregistration" as NonEmptyStr, interlock: decl_ref(module_path: "gunbc.managed_host_unit_hold", decl_name: "UnitHoldProof"), } @@ -576,12 +577,10 @@ data runner_lifecycle_ruling_scope_interpretation: RulingScopeInterpretation = R decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), - decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), - decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), ], interpreted_by: "eager-gull-22" as NonEmptyStr, interpreted_on: "2026-10-04" as NonEmptyStr, - standing: "covers register, deregister and the qualification dispatch, and -- as part of registration -- the create-once per-attempt branch refs/heads/qualification/ at the pinned revision, its removal on every exit, and pinning the DEDICATED qualification runner group's selected workflow to that branch; reported to the operator, who may narrow it" as NonEmptyStr, + standing: "covers register, deregister and the qualification dispatch; reported to the operator, who may narrow it. The per-attempt branch pin and its removal are NOT covered: they are reversible by reapply and the selection federates them on their own parameters, so they need no ruling" as NonEmptyStr, } // A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE INTERPRETATION NAMES IT. Removing a site from diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 6d50cb271cb..cadaf9c1fda 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -10,7 +10,7 @@ import gunbc.auth.authorization_pattern_selection { MintsNoCredential, MintsResourceScopedCredential, MintsAccountWideCredential, ReversibleByReapply, IrreversibleEffect, BillsExternally, NoBillingConsequence, - NoWitnessDischarge, + NoWitnessDischarge, StandingRulingUnderInterlock, WitnessDischarge, FederatedScopedGrant, OperatorApprovedCapability, HumanOnlyStep, OperatorOwnSession, PastedOperatorToken, PatternSelected, ManualStepRequired, NoAdmissiblePattern, PatternSelectionNeedsEvidence, PatternSelectionNeedsPolicy, PatternSelectionRefused, @@ -393,7 +393,8 @@ test fn every_root_of_the_standing_ruling_is_a_conforming_census_row() -> Bool { // THE 2026-10-03 PRE-APPROVAL IS A SELECTION, NOT A NEW VOCABULARY. JIT registration, // deregistration, the qualification dispatch and its per-attempt branch pin are each a -// recurring automated effect under the dispatching run's federated identity, so the selection must +// recurring automated effect under the dispatching run's federated identity (the pin and its removal +// on their own parameters, with no ruling discharge), so the selection must // answer the federated scoped grant and the row must conform; rowing it as an approval capability or // a per-registration ntfy approval turns this red. test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federated_grant_and_conform() -> Bool { @@ -404,6 +405,25 @@ test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federa && match v.conformance { Conforms => true _ => false })) } +// ONE ANSWER FOR WHICH SITES THE RUNNER-LIFECYCLE RULING DISCHARGES: the interpretation's covers list. +// A census row carries that ruling's discharge exactly when covers names its site, so a row cannot +// claim the ruling while covers omits it, and covers cannot name a row that never takes the discharge +// (the branch-pin rows once did both at once). The red is either drift: a row hard-coding the ruling, +// or a covers entry whose row hard-codes NoWitnessDischarge. +fn discharged_by_runner_lifecycle_ruling(d: WitnessDischarge) -> Bool { + match d { + NoWitnessDischarge => false + StandingRulingUnderInterlock { ruling: g } => (g.ruling_text as String) == (runner_lifecycle_operator_ruling.ruling_text as String) + } +} + +test fn a_row_takes_the_runner_lifecycle_ruling_exactly_when_the_interpretation_covers_it() -> Bool { + privileged_effect_census |> all(r => + discharged_by_runner_lifecycle_ruling(d: r.effect.witness_discharge) + == any(runner_lifecycle_ruling_scope_interpretation.covers, c => declaration_ref_eq(a: c, b: r.site))) + && runner_lifecycle_ruling_scope_interpretation.covers |> all(c => any(privileged_effect_census, r => declaration_ref_eq(a: c, b: r.site))) +} + // THE DISPATCH'S COVERAGE IS THE INTERPRETATION'S, NOT THE RULING TEXT'S. The census row's // discharge reads runner_lifecycle_ruling_scope_interpretation; narrowed to registration and // deregistration -- the ruling's own words -- the same irreversible dispatch effect is no longer From 08be556443261c0dc45f36ef5513725dbfe79789 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 05:49:19 +0000 Subject: [PATCH 41/44] leg 3 (review 76234): the operator's 2026-10-05 scope ruling (escalation msg_5756a200) recorded as its own row beside the verbatim 10-03 ruling, replacing the agent interpretation; dispatch and branch pins back in covers and every covered row derives its discharge from it Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/auth/privileged_effect_census.dag | 58 +++++++++++-------- .../runner/runner_qualification_dispatch.dag | 6 +- .../runner/runner_qualification_ref_pin.dag | 6 +- ...ization_pattern_selection_witness_test.dag | 37 ++++++------ 4 files changed, 58 insertions(+), 49 deletions(-) diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index 3ef50ec3910..d0b1a8071d8 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -548,46 +548,48 @@ fn present_trigger_declarations() -> List { // committed standing grant covers is admitted by that grant, never filed to the broker. The selection // decides the pattern from the effect's parameters with the ruling as the witness discharge, conditional on // its interlock (D0's durable claim, rostered in privileged_effect_interlocks); the census does not assert it. -// THE OPERATOR'S 2026-10-03 RUNNER-LIFECYCLE RULING, VERBATIM, AND ITS SCOPE INTERPRETATION AS A -// SEPARATE CITED FACT. The ruling text names registration and deregistration, and the ruling's -// effect_subject says no more than its text. Whether it also -// covers dispatching the qualification floor is not in its words; that coverage is an -// interpretation made by eager-gull-22 on 2026-10-04 and reported to the operator, who may narrow -// it. The two are kept apart so narrowing the interpretation is an edit to one row that changes the -// discharge of exactly the sites it names, and the ruling text is never rewritten to say more than -// the operator said. The interlock is the host-generic unit hold (gunbc.managed_host_unit_hold -// UnitHoldProof): the dispatch takes the proof and refuses unless it is the slot's own host's. +// THE OPERATOR'S 2026-10-03 RUNNER-LIFECYCLE RULING, VERBATIM, AND THE OPERATOR'S 2026-10-05 SCOPE +// RULING BESIDE IT. The 10-03 text names registration and deregistration and is never rewritten to +// say more. Its scope beyond those words is the operator's own second ruling, decided through +// escalation msg_5756a200-fcb5-470a-87eb-40867bfe9cdb: no agent's reading discharges a site (review +// 76234, DESIGN §5: approval is external to the diff, so the row cites where the operator decided). +// covers is the single source of which census sites take the 10-03 discharge; narrowing it is an +// edit to one list that changes the discharge of exactly the sites it names. The interlock is the +// host-generic unit hold (gunbc.managed_host_unit_hold UnitHoldProof): the dispatch takes the proof +// and refuses unless it is the slot's own host's. data runner_lifecycle_operator_ruling: StandingDestructiveAuthorization = StandingDestructiveAuthorization { ruling_text: "Runner registration and deregistration are PRE-APPROVED. Approval moves up to the workflow dispatcher (the fleet-converge run's own authorization). There is no per-registration approval step. Classify this through gunbc.auth.authorization_pattern_selection, recording that ruling rather than a per-effect ntfy approval. (operator decision, 2026-10-03)" as NonEmptyStr, effect_subject: "ephemeral JIT runner lifecycle: registration and deregistration" as NonEmptyStr, interlock: decl_ref(module_path: "gunbc.managed_host_unit_hold", decl_name: "UnitHoldProof"), } -type RulingScopeInterpretation { +type OperatorScopeRuling { ruling: StandingDestructiveAuthorization + scope_text: NonEmptyStr + decided_on: NonEmptyStr + decided_through: NonEmptyStr covers: List - interpreted_by: NonEmptyStr - interpreted_on: NonEmptyStr - standing: NonEmptyStr } -data runner_lifecycle_ruling_scope_interpretation: RulingScopeInterpretation = RulingScopeInterpretation { +data runner_lifecycle_scope_ruling: OperatorScopeRuling = OperatorScopeRuling { ruling: runner_lifecycle_operator_ruling, + scope_text: "decision: 'Yes, confirm the wider scope' (operator, 2026-10-05). Scope as relayed with it by quiet-stag-623 (msg_24760d96-078a-46cf-809e-2c92968dba31): the 2026-10-03 runner pre-approval covers registration, deregistration, the qualification CreateDispatch on that attempt's runner, and the per-attempt qualification/ branch create and delete" as NonEmptyStr, + decided_on: "2026-10-05" as NonEmptyStr, + decided_through: "escalation msg_5756a200-fcb5-470a-87eb-40867bfe9cdb" as NonEmptyStr, covers: [ decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), decl_ref(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), + decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), + decl_ref(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), ], - interpreted_by: "eager-gull-22" as NonEmptyStr, - interpreted_on: "2026-10-04" as NonEmptyStr, - standing: "covers register, deregister and the qualification dispatch; reported to the operator, who may narrow it. The per-attempt branch pin and its removal are NOT covered: they are reversible by reapply and the selection federates them on their own parameters, so they need no ruling" as NonEmptyStr, } -// A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE INTERPRETATION NAMES IT. Removing a site from -// covers turns its discharge into NoWitnessDischarge, and the selection then decides it afresh. -fn ruling_discharge_for(interpretation: RulingScopeInterpretation, site_ref: DeclarationRef) -> WitnessDischarge { - if declaration_ref_in_list(target: site_ref, refs: interpretation.covers) { - StandingRulingUnderInterlock { ruling: interpretation.ruling } +// A SITE IS DISCHARGED BY THE RULING ONLY WHILE THE OPERATOR'S SCOPE RULING NAMES IT. Removing a site +// from covers turns its discharge into NoWitnessDischarge, and the selection then decides it afresh. +fn ruling_discharge_for(scope: OperatorScopeRuling, site_ref: DeclarationRef) -> WitnessDischarge { + if declaration_ref_in_list(target: site_ref, refs: scope.covers) { + StandingRulingUnderInterlock { ruling: scope.ruling } } else { NoWitnessDischarge } @@ -656,7 +658,7 @@ data privileged_effect_census: List = [ minted_reach: MintsNoCredential, billing: NoBillingConsequence, witness_discharge: ruling_discharge_for( - interpretation: runner_lifecycle_ruling_scope_interpretation, + scope: runner_lifecycle_scope_ruling, site_ref: site(module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor"), ), }, @@ -673,7 +675,10 @@ data privileged_effect_census: List = [ workload_identity: convergence_principal(), minted_reach: MintsNoCredential, billing: NoBillingConsequence, - witness_discharge: NoWitnessDischarge, + witness_discharge: ruling_discharge_for( + scope: runner_lifecycle_scope_ruling, + site_ref: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), + ), }, realized: RealizedFederatedGrant, divergence_reason: none, @@ -688,7 +693,10 @@ data privileged_effect_census: List = [ workload_identity: convergence_principal(), minted_reach: MintsNoCredential, billing: NoBillingConsequence, - witness_discharge: NoWitnessDischarge, + witness_discharge: ruling_discharge_for( + scope: runner_lifecycle_scope_ruling, + site_ref: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), + ), }, realized: RealizedFederatedGrant, divergence_reason: none, diff --git a/dag/gunbc/runner/runner_qualification_dispatch.dag b/dag/gunbc/runner/runner_qualification_dispatch.dag index 5e44d4149ff..1fbf3d19188 100644 --- a/dag/gunbc/runner/runner_qualification_dispatch.dag +++ b/dag/gunbc/runner/runner_qualification_dispatch.dag @@ -72,9 +72,9 @@ import gunbc.runner.runner_qualification_ref_pin { // AUTHORIZATION. The dispatch runs under the gunbai-ci installation token the registration was // minted with (gunbc.github_effect_perform ControllerInstallationToken, App key read over WIF); the // credential is checked against the DispatchWorkflow effect (actions: write) before any request, and -// a token for another App or installation refuses. The operator's 2026-10-03 ruling, as interpreted -// to cover this dispatch (gunbc.auth.privileged_effect_census -// runner_lifecycle_ruling_scope_interpretation), stands only beside its interlock: +// a token for another App or installation refuses. The operator's 2026-10-03 ruling, whose scope the +// operator confirmed on 2026-10-05 to cover this dispatch (gunbc.auth.privileged_effect_census +// runner_lifecycle_scope_ruling), stands only beside its interlock: // dispatch_qualification_floor takes the host-generic unit hold proof // (gunbc.managed_host_unit_hold UnitHoldProof) and refuses unless it is the slot's own host's. diff --git a/dag/gunbc/runner/runner_qualification_ref_pin.dag b/dag/gunbc/runner/runner_qualification_ref_pin.dag index 71bbd8091f5..da68f145d67 100644 --- a/dag/gunbc/runner/runner_qualification_ref_pin.dag +++ b/dag/gunbc/runner/runner_qualification_ref_pin.dag @@ -46,9 +46,9 @@ import gunbc.github_effect_perform { // match a branch that no longer exists, so the stale entry admits nothing. // // AUTHORIZATION. Pre-approved under the dispatching run's authorization: the operator's 2026-10-03 -// ruling as interpreted by eager-gull-22 on 2026-10-04 to cover this pin as part of registration, -// limited to the dedicated qualification group (gunbc.auth.privileged_effect_census -// runner_lifecycle_ruling_scope_interpretation). The token is the gunbai-ci installation token +// ruling, whose scope the operator confirmed on 2026-10-05 to cover the per-attempt branch create and +// delete, limited to the dedicated qualification group (gunbc.auth.privileged_effect_census +// runner_lifecycle_scope_ruling). The token is the gunbai-ci installation token // (contents:write for the ref; organization self-hosted-runners write for the group). No workflow // fires on creating the branch: see test.claim.runner_qualification_ref_pin_witness. diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index cadaf9c1fda..212045aea23 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -24,7 +24,7 @@ import gunbc.auth.privileged_effect_census { site_verdict, Conforms, DivergesWithReason, Diverges, SiteNotDecidable, mtcollins1_boot_effect, privileged_effect_interlocks, interlocked_sites_missing_from_census, rulings_without_a_rostered_interlock, ruling_roots_missing_from_census, census_sites_outside_ruling_roots, - runner_lifecycle_ruling_scope_interpretation, runner_lifecycle_operator_ruling, RulingScopeInterpretation, ruling_discharge_for, + runner_lifecycle_scope_ruling, runner_lifecycle_operator_ruling, OperatorScopeRuling, ruling_discharge_for, RealizedFederatedGrant, RealizedRunSelected, RealizedApprovalCapability, RealizedHumanStep, RealizedOperatorSession, RealizedUnauthorized, } import std.human_intervention { @@ -405,10 +405,10 @@ test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federa && match v.conformance { Conforms => true _ => false })) } -// ONE ANSWER FOR WHICH SITES THE RUNNER-LIFECYCLE RULING DISCHARGES: the interpretation's covers list. +// ONE ANSWER FOR WHICH SITES THE RUNNER-LIFECYCLE RULING DISCHARGES: the operator's scope ruling's covers list. // A census row carries that ruling's discharge exactly when covers names its site, so a row cannot // claim the ruling while covers omits it, and covers cannot name a row that never takes the discharge -// (the branch-pin rows once did both at once). The red is either drift: a row hard-coding the ruling, +// (the branch-pin rows once did one without the other). The red is either drift: a row hard-coding the ruling, // or a covers entry whose row hard-codes NoWitnessDischarge. fn discharged_by_runner_lifecycle_ruling(d: WitnessDischarge) -> Bool { match d { @@ -417,31 +417,32 @@ fn discharged_by_runner_lifecycle_ruling(d: WitnessDischarge) -> Bool { } } -test fn a_row_takes_the_runner_lifecycle_ruling_exactly_when_the_interpretation_covers_it() -> Bool { +test fn a_row_takes_the_runner_lifecycle_ruling_exactly_when_the_operator_scope_ruling_covers_it() -> Bool { privileged_effect_census |> all(r => discharged_by_runner_lifecycle_ruling(d: r.effect.witness_discharge) - == any(runner_lifecycle_ruling_scope_interpretation.covers, c => declaration_ref_eq(a: c, b: r.site))) - && runner_lifecycle_ruling_scope_interpretation.covers |> all(c => any(privileged_effect_census, r => declaration_ref_eq(a: c, b: r.site))) + == any(runner_lifecycle_scope_ruling.covers, c => declaration_ref_eq(a: c, b: r.site))) + && runner_lifecycle_scope_ruling.covers |> all(c => any(privileged_effect_census, r => declaration_ref_eq(a: c, b: r.site))) } -// THE DISPATCH'S COVERAGE IS THE INTERPRETATION'S, NOT THE RULING TEXT'S. The census row's -// discharge reads runner_lifecycle_ruling_scope_interpretation; narrowed to registration and -// deregistration -- the ruling's own words -- the same irreversible dispatch effect is no longer -// discharged and the selection stops answering the federated grant. The ruling text is unchanged -// either way, and its interlock is the host-generic unit hold. -test fn the_dispatch_is_federated_only_while_the_interpretation_names_it() -> Bool { +// THE DISPATCH'S COVERAGE IS THE OPERATOR'S 2026-10-05 SCOPE RULING, NOT THE 10-03 TEXT. The census +// row's discharge reads runner_lifecycle_scope_ruling; narrowed to registration and deregistration -- +// the 10-03 ruling's own words -- the same irreversible dispatch effect is no longer discharged and +// the selection stops answering the federated grant. The 10-03 text is unchanged either way, and its +// interlock is the host-generic unit hold. +test fn the_dispatch_is_federated_only_while_the_scope_ruling_names_it() -> Bool { let row = first(filter(privileged_effect_census, r => (r.site.decl_name as String) == "dispatch_qualification_floor")) - let narrowed = RulingScopeInterpretation { + let narrowed = OperatorScopeRuling { ruling: runner_lifecycle_operator_ruling, + scope_text: "narrowed to the 2026-10-03 ruling's own words" as NonEmptyStr, + decided_on: runner_lifecycle_scope_ruling.decided_on, + decided_through: runner_lifecycle_scope_ruling.decided_through, covers: [ decl_ref(module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint"), decl_ref(module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered"), ], - interpreted_by: runner_lifecycle_ruling_scope_interpretation.interpreted_by, - interpreted_on: runner_lifecycle_ruling_scope_interpretation.interpreted_on, - standing: "narrowed to the ruling's own words" as NonEmptyStr, } - (runner_lifecycle_operator_ruling.interlock.decl_name as String) == "UnitHoldProof" + (runner_lifecycle_scope_ruling.decided_through as String) == "escalation msg_5756a200-fcb5-470a-87eb-40867bfe9cdb" + && (runner_lifecycle_operator_ruling.interlock.decl_name as String) == "UnitHoldProof" && (runner_lifecycle_operator_ruling.interlock.module_path as String) == "gunbc.managed_host_unit_hold" && match row { Present { value: r } => @@ -454,7 +455,7 @@ test fn the_dispatch_is_federated_only_while_the_interpretation_names_it() -> Bo workload_identity: r.effect.workload_identity, minted_reach: r.effect.minted_reach, billing: r.effect.billing, - witness_discharge: ruling_discharge_for(interpretation: narrowed, site_ref: r.site), + witness_discharge: ruling_discharge_for(scope: narrowed, site_ref: r.site), })) Absent => false } From 38e5456b16dc7f88356da20891272c39f573edda Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 06:15:34 +0000 Subject: [PATCH 42/44] leg 3: the scope ruling bounds where the 10-03 discharge may apply (a row takes it only if covers names it); covered sites the selection federates on their own parameters -- registration, deregistration, the reversible branch pins -- do not consume it and owe no interlock Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/auth/privileged_effect_census.dag | 17 +++++++---------- ...orization_pattern_selection_witness_test.dag | 16 ++++++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index d0b1a8071d8..6f11fddece2 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -553,8 +553,11 @@ fn present_trigger_declarations() -> List { // say more. Its scope beyond those words is the operator's own second ruling, decided through // escalation msg_5756a200-fcb5-470a-87eb-40867bfe9cdb: no agent's reading discharges a site (review // 76234, DESIGN §5: approval is external to the diff, so the row cites where the operator decided). -// covers is the single source of which census sites take the 10-03 discharge; narrowing it is an -// edit to one list that changes the discharge of exactly the sites it names. The interlock is the +// covers is the single source of the operator's scope: no census row may take the 10-03 discharge +// unless covers names its site. The converse does not hold, on purpose: a covered site whose effect +// the selection already federates on its own parameters -- registration, deregistration and the +// reversible branch create and delete -- does not consume the discharge, and only a consumer owes a +// rostered interlock. The dispatch, being irreversible, is the one consumer. The interlock is the // host-generic unit hold (gunbc.managed_host_unit_hold UnitHoldProof): the dispatch takes the proof // and refuses unless it is the slot's own host's. data runner_lifecycle_operator_ruling: StandingDestructiveAuthorization = StandingDestructiveAuthorization { @@ -675,10 +678,7 @@ data privileged_effect_census: List = [ workload_identity: convergence_principal(), minted_reach: MintsNoCredential, billing: NoBillingConsequence, - witness_discharge: ruling_discharge_for( - scope: runner_lifecycle_scope_ruling, - site_ref: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "ensure_qualification_ref_pin"), - ), + witness_discharge: NoWitnessDischarge, }, realized: RealizedFederatedGrant, divergence_reason: none, @@ -693,10 +693,7 @@ data privileged_effect_census: List = [ workload_identity: convergence_principal(), minted_reach: MintsNoCredential, billing: NoBillingConsequence, - witness_discharge: ruling_discharge_for( - scope: runner_lifecycle_scope_ruling, - site_ref: site(module_path: "gunbc.runner.runner_qualification_ref_pin", decl_name: "remove_qualification_ref_pin"), - ), + witness_discharge: NoWitnessDischarge, }, realized: RealizedFederatedGrant, divergence_reason: none, diff --git a/dag/test/claim/authorization_pattern_selection_witness_test.dag b/dag/test/claim/authorization_pattern_selection_witness_test.dag index 212045aea23..3da3815d443 100644 --- a/dag/test/claim/authorization_pattern_selection_witness_test.dag +++ b/dag/test/claim/authorization_pattern_selection_witness_test.dag @@ -405,11 +405,11 @@ test fn the_jit_registration_deregistration_and_dispatch_sites_select_the_federa && match v.conformance { Conforms => true _ => false })) } -// ONE ANSWER FOR WHICH SITES THE RUNNER-LIFECYCLE RULING DISCHARGES: the operator's scope ruling's covers list. -// A census row carries that ruling's discharge exactly when covers names its site, so a row cannot -// claim the ruling while covers omits it, and covers cannot name a row that never takes the discharge -// (the branch-pin rows once did one without the other). The red is either drift: a row hard-coding the ruling, -// or a covers entry whose row hard-codes NoWitnessDischarge. +// THE OPERATOR'S SCOPE RULING IS THE ONE SOURCE OF WHERE THE RUNNER-LIFECYCLE DISCHARGE MAY APPLY: a +// census row takes it only when covers names its site, and every covered site is a census row. The +// red is a row claiming the ruling outside the operator's scope, or a covers entry naming no site. A +// covered row that does not consume the discharge is admitted: the selection federates it on its own +// parameters, so it owes no interlock (the converse direction is not a wall, by design). fn discharged_by_runner_lifecycle_ruling(d: WitnessDischarge) -> Bool { match d { NoWitnessDischarge => false @@ -417,10 +417,10 @@ fn discharged_by_runner_lifecycle_ruling(d: WitnessDischarge) -> Bool { } } -test fn a_row_takes_the_runner_lifecycle_ruling_exactly_when_the_operator_scope_ruling_covers_it() -> Bool { +test fn a_row_takes_the_runner_lifecycle_ruling_only_where_the_operator_scope_ruling_covers_it() -> Bool { privileged_effect_census |> all(r => - discharged_by_runner_lifecycle_ruling(d: r.effect.witness_discharge) - == any(runner_lifecycle_scope_ruling.covers, c => declaration_ref_eq(a: c, b: r.site))) + !discharged_by_runner_lifecycle_ruling(d: r.effect.witness_discharge) + || any(runner_lifecycle_scope_ruling.covers, c => declaration_ref_eq(a: c, b: r.site))) && runner_lifecycle_scope_ruling.covers |> all(c => any(privileged_effect_census, r => declaration_ref_eq(a: c, b: r.site))) } From b1646813556449a5462ad157c20b395fb5b74d58 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 06:36:15 +0000 Subject: [PATCH 43/44] leg 3: the dedicated group's selected-workflow pin is authorized by its own federated standing, not by the 10-03/10-05 rulings; covers lists ensure_qualification_ref_pin for its branch create only Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/auth/privileged_effect_census.dag | 6 +++++- dag/gunbc/runner/runner_qualification_ref_pin.dag | 10 ++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dag/gunbc/auth/privileged_effect_census.dag b/dag/gunbc/auth/privileged_effect_census.dag index 6f11fddece2..16ec09ed3e6 100644 --- a/dag/gunbc/auth/privileged_effect_census.dag +++ b/dag/gunbc/auth/privileged_effect_census.dag @@ -557,7 +557,11 @@ fn present_trigger_declarations() -> List { // unless covers names its site. The converse does not hold, on purpose: a covered site whose effect // the selection already federates on its own parameters -- registration, deregistration and the // reversible branch create and delete -- does not consume the discharge, and only a consumer owes a -// rostered interlock. The dispatch, being irreversible, is the one consumer. The interlock is the +// rostered interlock. The dispatch, being irreversible, is the one consumer. +// THE DEDICATED GROUP'S SELECTED-WORKFLOW PIN IS NOT UNDER EITHER RULING. ensure_qualification_ref_pin +// is listed for its branch create; the group pin it performs in the same function is authorized by +// its own federated standing -- the census row's own parameters, selected without any discharge -- +// and the 10-03 and 10-05 rulings say nothing about it (eager-gull-22, 2026-10-05). The interlock is the // host-generic unit hold (gunbc.managed_host_unit_hold UnitHoldProof): the dispatch takes the proof // and refuses unless it is the slot's own host's. data runner_lifecycle_operator_ruling: StandingDestructiveAuthorization = StandingDestructiveAuthorization { diff --git a/dag/gunbc/runner/runner_qualification_ref_pin.dag b/dag/gunbc/runner/runner_qualification_ref_pin.dag index da68f145d67..71330a5a051 100644 --- a/dag/gunbc/runner/runner_qualification_ref_pin.dag +++ b/dag/gunbc/runner/runner_qualification_ref_pin.dag @@ -45,10 +45,12 @@ import gunbc.github_effect_perform { // The group stays restricted to the deleted branch's path until the next attempt pins it: no run can // match a branch that no longer exists, so the stale entry admits nothing. // -// AUTHORIZATION. Pre-approved under the dispatching run's authorization: the operator's 2026-10-03 -// ruling, whose scope the operator confirmed on 2026-10-05 to cover the per-attempt branch create and -// delete, limited to the dedicated qualification group (gunbc.auth.privileged_effect_census -// runner_lifecycle_scope_ruling). The token is the gunbai-ci installation token +// AUTHORIZATION, TWO FACTS. The per-attempt branch create and delete are pre-approved by the +// operator's 2026-10-03 ruling, whose scope the operator confirmed on 2026-10-05 +// (gunbc.auth.privileged_effect_census runner_lifecycle_scope_ruling). The dedicated group's +// selected-workflow pin is NOT under that ruling: it is authorized by its own federated standing, the +// census row's parameters selected without a ruling discharge, and is limited to the dedicated +// qualification group by this module's construction. The token is the gunbai-ci installation token // (contents:write for the ref; organization self-hosted-runners write for the group). No workflow // fires on creating the branch: see test.claim.runner_qualification_ref_pin_witness. From ee6a021a5ce43cbb2512ae270a43988ab49da36d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 5 Oct 2026 13:40:32 +0000 Subject: [PATCH 44/44] actions_artifacts: import GET from extdeps.ietf.http_semantics (floor UnimportedBareProvider) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/extdeps/github/actions_artifacts.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag index 6f2cf3452d3..48daa030940 100644 --- a/dag/extdeps/github/actions_artifacts.dag +++ b/dag/extdeps/github/actions_artifacts.dag @@ -2,13 +2,13 @@ module extdeps.github.actions_artifacts import extdeps.github.github { default_api_base, max_per_page } import extdeps.github.errors { GitHubErrorShape } - import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import std.credentials { EnvVar } import std.types { Bool, Bytes, Int, List, String } import std.resources { Network } import std.serialization { Text } +import extdeps.ietf.http_semantics { GET } // THE GITHUB ACTIONS ARTIFACTS REST API, as the upstream names it: "List workflow run artifacts" // (operationId actions/list-workflow-run-artifacts) and "Download an artifact"