From 6a15db24b21fc485a81b05610936ba1927b82186 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 4 Sep 2026 23:06:15 +0000 Subject: [PATCH 1/3] Fleet converge gaps: ephemeral registrations retire, the teardown grant enrolls the slot grammar, diverged is not unrelated, and refusals name their subject Four climbs measured against today's fleet, each at its authority: - Slot provenance carries a registration KIND, not a Bool. srv2-11 and srv2-12 sat refused for months because a `.runner` file was present; the file says Ephemeral=True and GitHub unregisters an ephemeral runner after one job (extdeps.github.actions_runner actions_runner_registration_decode, cited), so a dead ephemeral registration is a retired incarnation and needs no org credential to retire. A persistent registration still refuses. - The retired-tree removal grant enrolls the host's slot grammar up to a declared index bound (64) rather than the current desired width, still one exact path per sudoers line with no wildcard, so a narrowed host no longer refuses at apply the removals a width change created. The bound is pinned above every committed width by witness. - DivergedHistories is its own arm of DeployRevisionRelation. The srv1 deploy refused with "share no ancestry" for a sibling with a merge base two commits back; the neither-ancestor case is now named as diverged, the no-common-ancestor case stays unrelated, and the two-probe fold declares it cannot tell them apart instead of picking one. - The plan artifact write refusal names which path refused and why. The srv2 run refused bare on a /tmp/fleet-converge-plan owned by another principal from a local run; the shared literal dir is the underlying defect and is declared here, not solved. - The fleet-converge job roster witness counts the four jobs main actually has. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FbXKcgqW4Jx7e78BPWc8ci --- dag/extdeps/github/actions_runner.dag | 47 +++++++++++ dag/gunbc/executor_privileged_operation.dag | 12 +++ dag/gunbc/fleet/fleet_converge_plan_cli.dag | 74 +++++++++++++++-- dag/gunbc/fleet/fleet_main_revision.dag | 13 +++ dag/gunbc/live_deploy/revision.dag | 28 +++++-- dag/gunbc/runner/runner_host_grants.dag | 31 ++++++- dag/gunbc/runner/runner_slot_provision.dag | 55 +++++++++--- .../claim/deploy_revision_witness_test.dag | 13 ++- ...utor_privileged_operation_witness_test.dag | 26 +++++- .../fleet_main_revision_witness_test.dag | 3 +- ...leet_revision_relation_wet_matrix_test.dag | 7 +- .../target_decision_witness_test.dag | 8 +- .../runner_slot_provision_witness_test.dag | 67 +++++++++++++-- .../workflow_dispatch_input_witness_test.dag | 4 +- provisioning/srv1/gunbc-ghrunner.sudoers | 83 ++++++++++++++----- provisioning/srv2/gunbc-ghrunner.sudoers | 67 ++++++++++++++- provisioning/srv3/gunbc-ghrunner.sudoers | 82 +++++++++++++----- provisioning/srv4/gunbc-ghrunner.sudoers | 83 ++++++++++++++----- 18 files changed, 602 insertions(+), 101 deletions(-) diff --git a/dag/extdeps/github/actions_runner.dag b/dag/extdeps/github/actions_runner.dag index 1bfd27659e2..4eb1bc0a1a6 100644 --- a/dag/extdeps/github/actions_runner.dag +++ b/dag/extdeps/github/actions_runner.dag @@ -18,6 +18,11 @@ import extdeps.toolchain.types { Wasm32, } import std.types { NonEmptyStr, String, List } +import extdeps.languages.json.parse { + parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_document_gap_text, + json_object_unique_member, JsonMemberFound, JsonMemberAbsent, JsonMemberDuplicated, JsonMemberNotAnObject, +} +import extdeps.languages.json.emit { JsonString } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } @@ -134,6 +139,48 @@ data actions_runner_registration_file_name: String = ".runner" data actions_runner_cache_dir: String = "/opt/actions-runner-cache" +// THE REGISTRATION FILE SAYS WHETHER THE REGISTRATION CAN OUTLIVE ONE JOB. `.runner` is the JSON +// `config.sh` writes; its `Ephemeral` member is the string "True" when the runner was configured +// with `--ephemeral`. Upstream semantics (docs.github.com, "Autoscaling with self-hosted runners", +// Using ephemeral runners): an ephemeral runner is automatically unregistered from GitHub after it +// completes one job, and is not re-registered by restarting the process. So an ephemeral +// registration file beside a unit that is neither active nor enabled records a registration GitHub +// has already dropped; nothing remains to `config.sh remove`, and no credential is needed to know +// it. A persistent registration (`Ephemeral` absent or "False") stays registered org-side until +// removed, which needs an org-scoped removal token this repository does not hold, so a persistent +// registration is NOT decidable from the file alone. +type ActionsRunnerRegistration + = ActionsRunnerRegistrationPersistent + | ActionsRunnerRegistrationEphemeral + +type ActionsRunnerRegistrationRead + = ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistration } + | ActionsRunnerRegistrationUnreadable { reason: String } + +data actions_runner_registration_ephemeral_member: String = "Ephemeral" + +fn actions_runner_registration_decode(raw: String) -> ActionsRunnerRegistrationRead { + match parse_json_document(s: raw) { + JsonDocumentUnreadable { gap } => + ActionsRunnerRegistrationUnreadable { reason: concat(".runner is ", json_document_gap_text(gap: gap)) } + JsonDocumentParsed { value: doc } => + match json_object_unique_member(v: doc, key: actions_runner_registration_ephemeral_member) { + JsonMemberFound { value: JsonString { value: flag } } => + if flag == "True" { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } + } else if flag == "False" { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } + } else { + ActionsRunnerRegistrationUnreadable { reason: concat(".runner Ephemeral is outside True/False: ", flag) } + } + JsonMemberFound { value: _ } => ActionsRunnerRegistrationUnreadable { reason: ".runner Ephemeral is not a string" } + JsonMemberAbsent => ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } + JsonMemberDuplicated { count: _ } => ActionsRunnerRegistrationUnreadable { reason: ".runner Ephemeral is duplicated" } + JsonMemberNotAnObject => ActionsRunnerRegistrationUnreadable { reason: ".runner is not a JSON object" } + } + } +} + data actions_runner_slot_extract_script_scaffold: Disposition = Scaffold { dissolves_to: SingleAuthority, bind: DeclarationRef { diff --git a/dag/gunbc/executor_privileged_operation.dag b/dag/gunbc/executor_privileged_operation.dag index b052a7b7431..357f5add778 100644 --- a/dag/gunbc/executor_privileged_operation.dag +++ b/dag/gunbc/executor_privileged_operation.dag @@ -173,6 +173,18 @@ fn executor_privileged_operations_sudoers_body( // longer generated file and removes the traversal entirely. data executor_managed_directory_mode: NonEmptyStr = "755" as NonEmptyStr +fn runner_slot_install_operations( + slot_dirs: List, + slot_owner: NonEmptyStr, +) -> List { + map(slot_dirs, d => EnsureOwnedDirectory { + path: d, + owner: slot_owner, + group: slot_owner, + mode: executor_managed_directory_mode, + }) +} + fn runner_slot_privileged_operations( slot_dirs: List, slot_owner: NonEmptyStr, diff --git a/dag/gunbc/fleet/fleet_converge_plan_cli.dag b/dag/gunbc/fleet/fleet_converge_plan_cli.dag index 8f9ed349d6b..b3f23aeb671 100644 --- a/dag/gunbc/fleet/fleet_converge_plan_cli.dag +++ b/dag/gunbc/fleet/fleet_converge_plan_cli.dag @@ -29,6 +29,7 @@ import gunbc.runner_slot_provision { runner_slot_name_in_host_grammar, RunnerSlotProvenance, SlotProvenanceObserved, + RunnerSlotRegistration, SlotUnregistered, SlotRegisteredPersistent, SlotRegisteredEphemeral, SlotRegistrationUnreadable, RunnerSlotObservation, RunnerSlotMember, RunnerSlotsObserved, @@ -36,7 +37,12 @@ import gunbc.runner_slot_provision { observe_runner_slot_members_wet, } import product.placement_supply { HostIdentity } -import extdeps.github.actions_runner { actions_runner_registration_file_name } +import extdeps.github.actions_runner { + actions_runner_registration_file_name, + actions_runner_registration_decode, + ActionsRunnerRegistrationDecoded, ActionsRunnerRegistrationUnreadable, + ActionsRunnerRegistrationPersistent, ActionsRunnerRegistrationEphemeral, +} import gunbc.runner_unit { runner_unit_name_of_registration } import gunbc.fabric_cell_observation_admission { admit_fabric_cell_probe, FabricCellObservationDecision } import gunbc.fabric_cell_acquire { fabric_cell_probe_wet } @@ -583,7 +589,18 @@ func write_fleet_converge_plan_artifact_wet(artifact: FleetConvergePlanArtifact) transport: LocalExec, ) } else { - exit_failure(reason: "fleet_converge_plan: artifact write refused") + exit_failure(reason: fleet_converge_plan_artifact_write_refusal(rows: [ + fleet_converge_write_refusal_row(path: fleet_converge_plan_body_path, success: w_plan.success, error: w_plan.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_baseline_hex_path, success: w_base.success, error: w_base.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_content_hash_path, success: w_hash.success, error: w_hash.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_apply_shell_path, success: w_apply.success, error: w_apply.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_host_path, success: w_host.success, error: w_host.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_generation_path, success: w_gen.success, error: w_gen.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_prior_generation_path, success: w_prior.success, error: w_prior.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_member_set_fp_path, success: w_member_fp.success, error: w_member_fp.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_scope_path, success: w_scope.success, error: w_scope.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_spark_typed_actions_wire_path, success: w_spark_wire.success, error: w_spark_wire.error), + ])) } } } @@ -994,7 +1011,18 @@ func fleet_converge_plan_scoped_bound_wet( ) } } else { - exit_failure(reason: "fleet_converge_plan: artifact write refused") + exit_failure(reason: fleet_converge_plan_artifact_write_refusal(rows: [ + fleet_converge_write_refusal_row(path: fleet_converge_plan_body_path, success: w_plan.success, error: w_plan.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_baseline_hex_path, success: w_base.success, error: w_base.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_content_hash_path, success: w_hash.success, error: w_hash.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_apply_shell_path, success: w_apply.success, error: w_apply.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_host_path, success: w_host.success, error: w_host.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_generation_path, success: w_gen.success, error: w_gen.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_prior_generation_path, success: w_prior.success, error: w_prior.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_member_set_fp_path, success: w_member_fp.success, error: w_member_fp.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_scope_path, success: w_scope.success, error: w_scope.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_spark_typed_actions_wire_path, success: w_spark_wire.success, error: w_spark_wire.error), + ])) } } } @@ -1581,6 +1609,38 @@ func fleet_converge_local_apply_wet(plan_run_id: String, plan_hash: String) -> P // unit's ActiveState and its UnitFileState -- and a name outside the grammar gets none, because no // unit or registration is defined for it. Any unreadable unit property refuses the WHOLE // observation: a roster in which one tree's liveness was guessed could remove a live runner. +// The registration KIND comes from the file's own bytes, read only when the file is present; an +// unreadable or undecodable file is carried as its own arm so ownership refuses on it rather than +// defaulting either way. +fn observe_runner_slot_registration_wet(present: Bool, path: String) -> RunnerSlotRegistration { + if !present { + SlotUnregistered + } else { + let read = Filesystem.Read(path: path) + if !read.success { + SlotRegistrationUnreadable { reason: join([".runner present but unreadable: ", read.error], "") } + } else { + match actions_runner_registration_decode(raw: read.content) { + ActionsRunnerRegistrationUnreadable { reason } => SlotRegistrationUnreadable { reason: reason } + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } => SlotRegisteredEphemeral + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } => SlotRegisteredPersistent + } + } + } +} + +// A refused artifact write names WHICH path refused and why. The srv2 plan run of 2026-09-04 22:42 +// refused with the bare sentence and the cause -- /tmp/fleet-converge-plan owned by another +// principal from a local plan run -- had to be found by hand on the host. The shared literal dir +// is the underlying defect and is declared, not solved, here. +fn fleet_converge_write_refusal_row(path: String, success: Bool, error: String) -> List { + if success { [] } else { [join([path, ": ", error], "")] } +} + +fn fleet_converge_plan_artifact_write_refusal(rows: List>) -> String { + join(["fleet_converge_plan: artifact write refused: ", join(flat_map(rows, r => r), "; ")], "") +} + type RunnerSlotProvenanceRead = SlotProvenanceRead { member: RunnerSlotMember } | SlotProvenanceRefused { reason: String } @@ -1593,14 +1653,16 @@ func observe_runner_slot_provenance_wet(host: NonEmptyStr, member: RunnerSlotMem SlotProvenanceRead { member: runner_slot_member_observed( instance_name: name as String, - provenance: SlotProvenanceObserved { in_host_grammar: false, registered: false, unit_active: false, unit_enabled: false }, + provenance: SlotProvenanceObserved { in_host_grammar: false, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ), } } else { + let registration_path = join([member.slot_dir as String, "/", actions_runner_registration_file_name], "") let registered = process_outcome_admitted(outcome: run_shell_command_capture( - command: test_regular_file_command(path: join([member.slot_dir as String, "/", actions_runner_registration_file_name], "")), + command: test_regular_file_command(path: registration_path), transport: LocalExec, )) + let registration = observe_runner_slot_registration_wet(present: registered, path: registration_path) let unit = runner_unit_name_of_registration(registration_name: name) match read_unit_property(unit: unit, property: ActiveState) { UnitPropertyUnreadable { unit: u, property: p } => @@ -1615,7 +1677,7 @@ func observe_runner_slot_provenance_wet(host: NonEmptyStr, member: RunnerSlotMem instance_name: name as String, provenance: SlotProvenanceObserved { in_host_grammar: true, - registered: registered, + registration: registration, unit_active: active_state == "active", unit_enabled: file_state == "enabled", }, diff --git a/dag/gunbc/fleet/fleet_main_revision.dag b/dag/gunbc/fleet/fleet_main_revision.dag index 31aed8f5319..552f4e62e22 100644 --- a/dag/gunbc/fleet/fleet_main_revision.dag +++ b/dag/gunbc/fleet/fleet_main_revision.dag @@ -21,6 +21,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_revision_acceptance { AcceptedFleetRevision } @@ -285,6 +286,7 @@ fn relation_from_merge_base_oid( type FleetDesiredAdvanceRefusal = AdvanceExpectationUnavailable { cause: FleetDesiredExpectationRefusal } | AdvanceHistoriesUnrelated { current: GitObjectId, accepted: GitObjectId } + | AdvanceHistoriesDiverged { current: GitObjectId, accepted: GitObjectId } | AdvanceRelationUnobservable { current: GitObjectId, accepted: GitObjectId, cause: NonEmptyStr } | InitialFleetDesiredCreationNotAuthorized { accepted: GitObjectId } | AdvanceRelationIsAboutAnotherPair { @@ -398,6 +400,13 @@ fn fleet_desired_advance_refusal_message(cause: FleetDesiredAdvanceRefusal) -> S ", accepted ", git_object_id_wire_hex(oid: acc) as String, ") -- advancing would move the ref sideways", ], "") + AdvanceHistoriesDiverged { current: cur, accepted: acc } => + join([ + "the revisions have diverged: they share an ancestor but neither is an ancestor of the other (current ", + git_object_id_wire_hex(oid: cur) as String, + ", accepted ", git_object_id_wire_hex(oid: acc) as String, + ") -- advancing would move the ref sideways", + ], "") AdvanceRelationUnobservable { current: cur, accepted: acc, cause: c } => join([ "the ancestry of ", git_object_id_wire_hex(oid: cur) as String, @@ -536,6 +545,10 @@ fn classify_admitted_relation( FleetDesiredAdvanceRefused { cause: AdvanceHistoriesUnrelated { current: current, accepted: accepted.revision }, } + DivergedHistories => + FleetDesiredAdvanceRefused { + cause: AdvanceHistoriesDiverged { current: current, accepted: accepted.revision }, + } RelationUnverifiable { cause: c } => FleetDesiredAdvanceRefused { cause: AdvanceRelationUnobservable { diff --git a/dag/gunbc/live_deploy/revision.dag b/dag/gunbc/live_deploy/revision.dag index 423567cbe89..7b9791cc441 100644 --- a/dag/gunbc/live_deploy/revision.dag +++ b/dag/gunbc/live_deploy/revision.dag @@ -69,6 +69,7 @@ type DeployRevisionRelation | DeployedIsAncestor | CandidateIsAncestor | UnrelatedHistories + | DivergedHistories | RelationUnverifiable { cause: NonEmptyStr } type DeployRevisionVerdict @@ -86,6 +87,10 @@ fn classify_deploy_revision(relation: DeployRevisionRelation) -> DeployRevisionV RevisionRefused { cause: "deployed and candidate revisions share no ancestry (force-push, rewritten history, or foreign repository) — refusing rather than guessing which is newer" as NonEmptyStr } + DivergedHistories => + RevisionRefused { + cause: "deployed and candidate revisions have diverged: they share an ancestor but neither is an ancestor of the other, so installing the candidate would move the running release sideways — refusing rather than guessing which changes would be lost" as NonEmptyStr, + } RelationUnverifiable { cause: why } => RevisionRefused { cause: why } } } @@ -156,22 +161,33 @@ fn revision_relation_from_ancestry_facts( if deployed_is_ancestor_of_candidate { if candidate_is_ancestor_of_deployed { SameRevision } else { DeployedIsAncestor } } else { - if candidate_is_ancestor_of_deployed { CandidateIsAncestor } else { UnrelatedHistories } + if candidate_is_ancestor_of_deployed { CandidateIsAncestor } else { DivergedHistories } } } } +// DIVERGED IS NOT UNRELATED. The srv1 deploy of 2026-09-04 refused with 'share no ancestry' when the +// running release was a sibling of the candidate with a merge base two commits back: the neither- +// ancestor arm had been folded into the no-common-ancestor arm, so the refusal named a force-push +// that never happened. revision_relation_from_ancestry_facts is reached only once a merge base was +// REPORTED (relation_from_merge_base_oid), so 'neither' there means diverged; a merge-base exit of 1 +// reaches UnrelatedHistories on its own path. Two bare is-ancestor probes cannot tell the two apart, +// so the probe fold below says so rather than picking one. fn deploy_revision_relation_from_probes( deployed: CommitSha, candidate: CommitSha, deployed_is_ancestor_of_candidate: Bool, candidate_is_ancestor_of_deployed: Bool, ) -> DeployRevisionRelation { - revision_relation_from_ancestry_facts( - same: deployed == candidate, - deployed_is_ancestor_of_candidate: deployed_is_ancestor_of_candidate, - candidate_is_ancestor_of_deployed: candidate_is_ancestor_of_deployed, - ) + if deployed == candidate { + SameRevision + } else if deployed_is_ancestor_of_candidate { + DeployedIsAncestor + } else if candidate_is_ancestor_of_deployed { + CandidateIsAncestor + } else { + RelationUnverifiable { cause: "two is-ancestor probes answered no in both directions, which cannot distinguish diverged histories from unrelated ones; a merge-base observation decides" as NonEmptyStr } + } } // The deployed-revision record lives OUTSIDE the directory the deploy rsyncs into, and that diff --git a/dag/gunbc/runner/runner_host_grants.dag b/dag/gunbc/runner/runner_host_grants.dag index 8001a1cfbe7..76082b74671 100644 --- a/dag/gunbc/runner/runner_host_grants.dag +++ b/dag/gunbc/runner/runner_host_grants.dag @@ -21,7 +21,7 @@ import gunbc.executor_privileged_operation { runner_slot_privileged_operations, } import extdeps.systemd.unit_file { SystemdSliceDirective, systemd_slice_directive_line } -import gunbc.build_cache_instance { RunnerSlotIdentity } +import gunbc.build_cache_instance { RunnerSlotIdentity, runner_slot_instance_name } import gunbc.runner_slot_allocation { fabric_execution_slot_identities } import gunbc.fabric_cell_converge { fabric_cell_base_dir, @@ -44,7 +44,9 @@ import gunbc.fabric_allocation_store_substrate { fabric_allocation_store_ensure_effect, } import gunbc.runner_host_deploy { RunnerHostDeploy } -import gunbc.runner_slot_provision { desired_runner_slot_members } +import product.placement_supply { HostIdentity } +import gunbc.runner_slot_provision { desired_runner_slot_members, runner_slot_instance_dir } +import gunbc.runner_host_deploy { runner_index_seq } import gunbc.runner_unit { runner_unit_name_of_registration } // THE MANAGED STATE DIRECTORY, DECLARED ONCE. It is currently spelled THREE times in @@ -249,10 +251,33 @@ fn fabric_allocation_store_privileged_operations(deploy: RunnerHostDeploy) -> Li } } +// THE TEARDOWN GRANT ENROLLS THE SLOT GRAMMAR, NOT THE CURRENT WIDTH. Sudoers is derived from desired +// state, but the trees a reconcile removes are RETIRED ones -- by definition outside the desired +// width -- so a grant that named only desired dirs authorized exactly the removals that never +// happen and refused every one that does (runner_slot_teardown_grant_note). The removal grant now +// names every slot dir the host's grammar can render up to a declared index bound, still one exact +// path per line with no wildcard (the `..` traversal argument in executor_privileged_operation +// stands). The bound is a policy budget: the widest committed width in the fleet is 21 and the +// retired incarnations reclaimed on srv2 on 2026-09-04 ran well past it, so 64 covers every tree +// that exists with headroom for growth, at 64 sudoers lines per host. A retired tree above the bound refuses at apply exactly as +// before, and the witness pins the bound above every committed width so a width change cannot +// silently outgrow its own teardown. +data runner_slot_teardown_grant_index_bound: Int = 64 + +fn runner_slot_grammar_dirs_to_bound(host: HostIdentity, bound: Int) -> List { + map(runner_index_seq(n: bound), i => + runner_slot_instance_dir(instance_name: runner_slot_instance_name(slot: RunnerSlotIdentity { host: host, slot_index: i }) as NonEmptyStr)) +} + +fn runner_slot_teardown_grant_operations(deploy: RunnerHostDeploy) -> List { + map(runner_slot_grammar_dirs_to_bound(host: deploy.host_label as HostIdentity, bound: runner_slot_teardown_grant_index_bound), d => RemoveDirectoryTree { path: d }) +} + fn runner_host_privileged_operations(deploy: RunnerHostDeploy) -> List { let job = deploy.job_user let slot_dirs = map(desired_runner_slot_members(deploy: deploy), m => m.slot_dir) - runner_slot_privileged_operations(slot_dirs: slot_dirs, slot_owner: job) + runner_slot_teardown_grant_operations(deploy: deploy) + + runner_slot_install_operations(slot_dirs: slot_dirs, slot_owner: job) + runner_slot_activation_operations(deploy: deploy) + fabric_cell_privileged_operations(deploy: deploy) + fabric_allocation_store_privileged_operations(deploy: deploy) diff --git a/dag/gunbc/runner/runner_slot_provision.dag b/dag/gunbc/runner/runner_slot_provision.dag index f75f672e1b4..d59b382a8dd 100644 --- a/dag/gunbc/runner/runner_slot_provision.dag +++ b/dag/gunbc/runner/runner_slot_provision.dag @@ -69,21 +69,54 @@ type RunnerSlotMemberAt = NonEmptyStr // out-of-roster tree refused OwnershipUnknown forever and 45 dead runner trees sat on srv2 at two // gigabytes each. A tree is a retired incarnation of a gunbc-managed slot when four observed facts // agree: its name is in this host's slot grammar (the allocation authority's own rendering round- -// trips through it), no `.runner` registration is present, and its unit is neither active nor -// enabled. That tree was created by this reconcile's own ADD arm and abandoned by a width change, -// so it is Owned and teardown-eligible. Anything else -- a registered tree, an active unit, a name -// outside the grammar such as the operator scripts in the base dir -- stays OwnershipUnknown and -// refuses exactly as before. Desired members are constructed from names and carry Unobserved, -// which ownership never admits; only an observation can make a member removable. +// trips through it), its `.runner` registration is absent OR ephemeral, and its unit is neither +// active nor enabled. That tree was created by this reconcile's own ADD arm and abandoned by a +// width change, so it is Owned and teardown-eligible. Anything else -- a persistently registered +// tree, an unreadable registration, an active unit, a name outside the grammar such as the +// operator scripts in the base dir -- stays OwnershipUnknown and refuses exactly as before. +// Desired members are constructed from names and carry Unobserved, which ownership never admits; +// only an observation can make a member removable. +// +// REGISTRATION IS A KIND, NOT A BOOL. `registered: Bool` read only the file's presence, and on +// srv2 two trees (srv2-11, srv2-12) carried a `.runner` beside a unit dead since June: the file +// says `Ephemeral: True`, and an ephemeral registration is unregistered by GitHub after one job +// (extdeps.github.actions_runner actions_runner_registration_decode), so those trees were retired +// incarnations that a Bool could not distinguish from a live persistent registration. The +// persistent arm still refuses: deciding it needs an org-scoped observation this executor does +// not hold. +type RunnerSlotRegistration + = SlotUnregistered + | SlotRegisteredPersistent + | SlotRegisteredEphemeral + | SlotRegistrationUnreadable { reason: String } + type RunnerSlotProvenance = SlotProvenanceUnobserved | SlotProvenanceObserved { in_host_grammar: Bool - registered: Bool + registration: RunnerSlotRegistration unit_active: Bool unit_enabled: Bool } +fn runner_slot_registration_is_retired(r: RunnerSlotRegistration) -> Bool { + match r { + SlotUnregistered => true + SlotRegisteredEphemeral => true + SlotRegisteredPersistent => false + SlotRegistrationUnreadable { reason: _ } => false + } +} + +fn runner_slot_registration_wire(r: RunnerSlotRegistration) -> String { + match r { + SlotUnregistered => "unregistered" + SlotRegisteredPersistent => "registered" + SlotRegisteredEphemeral => "ephemeral" + SlotRegistrationUnreadable { reason: _ } => "registration-unreadable" + } +} + type RunnerSlotMember { instance_name: NonEmptyStr slot_dir: NonEmptyStr @@ -93,18 +126,18 @@ type RunnerSlotMember { fn runner_slot_provenance_is_retired_incarnation(p: RunnerSlotProvenance) -> Bool { match p { SlotProvenanceUnobserved => false - SlotProvenanceObserved { in_host_grammar: g, registered: r, unit_active: a, unit_enabled: e } => - g && !r && !a && !e + SlotProvenanceObserved { in_host_grammar: g, registration: r, unit_active: a, unit_enabled: e } => + g && runner_slot_registration_is_retired(r: r) && !a && !e } } fn runner_slot_provenance_wire(p: RunnerSlotProvenance) -> String { match p { SlotProvenanceUnobserved => "unobserved" - SlotProvenanceObserved { in_host_grammar: g, registered: r, unit_active: a, unit_enabled: e } => + SlotProvenanceObserved { in_host_grammar: g, registration: r, unit_active: a, unit_enabled: e } => join([ if g { "grammar" } else { "foreign" }, ",", - if r { "registered" } else { "unregistered" }, ",", + runner_slot_registration_wire(r: r), ",", if a { "active" } else { "inactive" }, ",", if e { "enabled" } else { "disabled" }, ], "") diff --git a/dag/test/claim/deploy_revision_witness_test.dag b/dag/test/claim/deploy_revision_witness_test.dag index 08c27ac3672..4cd3abf4f69 100644 --- a/dag/test/claim/deploy_revision_witness_test.dag +++ b/dag/test/claim/deploy_revision_witness_test.dag @@ -7,6 +7,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, DeployRevisionVerdict, RevisionAlreadyConverged, @@ -35,6 +36,7 @@ data all_relations: List = [ DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable { cause: "probe failed" as NonEmptyStr }, ] @@ -73,6 +75,15 @@ test fn unrelated_histories_refuse() -> Bool { && !deploy_revision_terminal_success(verdict: v) } +test fn diverged_histories_refuse_with_their_own_cause() -> Bool { + match classify_deploy_revision(relation: DivergedHistories) { + RevisionRefused { cause } => + string_contains(s: cause as String, pattern: "diverged") + && !string_contains(s: cause as String, pattern: "no ancestry") + _ => false + } +} + test fn unverifiable_relation_refuses_and_keeps_its_cause() -> Bool { let why = "no persisted revision on host" as NonEmptyStr match classify_deploy_revision(relation: RelationUnverifiable { cause: why }) { @@ -113,7 +124,7 @@ test fn probe_pair_folds_to_the_four_cases() -> Bool { && match deploy_revision_relation_from_probes( deployed: sha_older, candidate: sha_newer, deployed_is_ancestor_of_candidate: false, candidate_is_ancestor_of_deployed: false - ) { UnrelatedHistories => true _ => false } + ) { RelationUnverifiable { cause: c } => string_contains(s: c as String, pattern: "merge-base") _ => false } } test fn equal_shas_short_circuit_before_the_probes() -> Bool { diff --git a/dag/test/claim/executor_privileged_operation_witness_test.dag b/dag/test/claim/executor_privileged_operation_witness_test.dag index a9c016973a1..c9ec03da6aa 100644 --- a/dag/test/claim/executor_privileged_operation_witness_test.dag +++ b/dag/test/claim/executor_privileged_operation_witness_test.dag @@ -14,6 +14,9 @@ import gunbc.generated_artifact_emit { } import gunbc.fleet_intent_network { operator_host_srv1, operator_host_srv3 } import product.placement_supply { HostIdentity } +import gunbc.runner_host_grants { runner_slot_teardown_grant_index_bound, runner_slot_grammar_dirs_to_bound, fabric_allocation_store_privileged_operations } +import gunbc.runner_slot_allocation { gunbc_runner_slots_per_host } +import gunbc.fleet_intent_network { operator_host_srv2, operator_host_srv4 } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -128,14 +131,34 @@ test fn witness_population_tracks_the_slot_roster() -> Bool { let fabric_slots = fabric_cell_host_slots(deploy: srv3_runner_host_deploy).length() let directives = fabric_cell_slice_desired_directives().length() let fabric_expected = 1 + fabric_slots * (2 + directives + 1) + let allocation_store = fabric_allocation_store_privileged_operations(deploy: srv3_runner_host_deploy).length() runner_host_privileged_operations(deploy: srv3_runner_host_deploy).length() - == (slots * 4) + 4 + fabric_expected + == runner_slot_teardown_grant_index_bound + (slots * 3) + 4 + fabric_expected + allocation_store + && allocation_store == 2 && fabric_cell_privileged_operations(deploy: srv3_runner_host_deploy).length() == fabric_expected && activation == slots * 2 && slots > 0 && fabric_slots > 0 } +// THE TEARDOWN GRANT COVERS EVERY WIDTH THE FLEET COMMITS TO, with the bound pinned above each +// host's committed width so a width change cannot outgrow its own teardown; and it is exact paths +// only -- the highest grammar dir is named and the one past the bound is not. +test fn witness_teardown_grant_bound_covers_every_committed_width_exactly() -> Bool { + let bound = runner_slot_teardown_grant_index_bound + let dirs = runner_slot_grammar_dirs_to_bound(host: operator_host_srv2, bound: bound) + bound >= gunbc_runner_slots_per_host(host: operator_host_srv1) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv2) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv3) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv4) + && bound >= 12 + && dirs.length() == bound + && any(dirs, d => (d as String) == "/opt/actions-runner/srv2-12") + && any(dirs, d => (d as String) == "/opt/actions-runner/srv2-64") + && !any(dirs, d => (d as String) == "/opt/actions-runner/srv2-65") + && !any(dirs, d => string_contains(s: d as String, pattern: "*")) +} + // THE JOIN, ASSERTED THROUGH THE PRODUCTION PATH. This is the witness the module exists for: the // command the emitted provisioning script RUNS and the command the sudoers file GRANTS are the same // word vector, reached from opposite ends of the tree. The left side comes from @@ -176,3 +199,4 @@ test fn witness_bootstrap_principal_is_not_the_grantee() -> Bool { deploy: srv3_runner_host_deploy, ) } + diff --git a/dag/test/claim/fleet/fleet_main_revision_witness_test.dag b/dag/test/claim/fleet/fleet_main_revision_witness_test.dag index 312d654aa6e..79f67211ba0 100644 --- a/dag/test/claim/fleet/fleet_main_revision_witness_test.dag +++ b/dag/test/claim/fleet/fleet_main_revision_witness_test.dag @@ -18,6 +18,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_revision_acceptance { @@ -364,7 +365,7 @@ test fn one_merge_base_decides_both_directions() -> Bool { Present { value: third } => relation_from_merge_base_oid(current: cur, accepted: acc, base: cur) == DeployedIsAncestor && relation_from_merge_base_oid(current: cur, accepted: acc, base: acc) == CandidateIsAncestor - && relation_from_merge_base_oid(current: cur, accepted: acc, base: third) == UnrelatedHistories + && relation_from_merge_base_oid(current: cur, accepted: acc, base: third) == DivergedHistories _ => false } _ => false diff --git a/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag b/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag index bad91be89cd..f052ae22a46 100644 --- a/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag +++ b/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag @@ -8,6 +8,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_main_revision { FleetRevisionRelationObservation } @@ -122,6 +123,10 @@ fn is_unrelated(r: DeployRevisionRelation?) -> Bool { match r { Present { value: UnrelatedHistories } => true _ => false } } +fn is_diverged(r: DeployRevisionRelation?) -> Bool { + match r { Present { value: DivergedHistories } => true _ => false } +} + fn is_unverifiable(r: DeployRevisionRelation?) -> Bool { match r { Present { value: RelationUnverifiable { cause: _ } } => true @@ -158,7 +163,7 @@ test fn an_ancestor_accepted_is_superseded() -> Bool { // CASE 4 -- DIVERGED, WITH A SHARED ANCESTOR. Exit 0 and a real merge base (A), which is NEITHER // endpoint. This is the case whose receipt used to claim a disjoint history. test fn siblings_sharing_an_ancestor_are_neither_ancestor_of_the_other() -> Bool { - is_unrelated(r: relation_in(repository_path: fixture_repository_path, current_hex: revision_b_hex, accepted_hex: revision_c_hex)) + is_diverged(r: relation_in(repository_path: fixture_repository_path, current_hex: revision_b_hex, accepted_hex: revision_c_hex)) } // CASE 5 -- NO COMMON ANCESTOR AT ALL. Exit 1, which is an ANSWER: git looked and there is none. diff --git a/dag/test/claim/live_deploy/target_decision_witness_test.dag b/dag/test/claim/live_deploy/target_decision_witness_test.dag index 78b69c82edc..e6ee46e2dd2 100644 --- a/dag/test/claim/live_deploy/target_decision_witness_test.dag +++ b/dag/test/claim/live_deploy/target_decision_witness_test.dag @@ -23,6 +23,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.live_deploy.target_decision { @@ -83,6 +84,7 @@ test fn witness_every_decision_lands_in_exactly_one_disposition() -> Bool { related(relation: SameRevision), related(relation: CandidateIsAncestor), related(relation: UnrelatedHistories), + related(relation: DivergedHistories), related(relation: RelationUnverifiable { cause: "probe died" as NonEmptyStr }), decide_deploy_target(input: TargetIdle { unit_state: "inactive" as NonEmptyStr }, candidate: candidate()), decide_deploy_target(input: TargetUnreadable { cause: "no answer" }, candidate: candidate()), @@ -108,8 +110,12 @@ test fn witness_a_superseded_candidate_is_a_terminal_no_op_and_never_proceeds() // UNRELATED HISTORIES AND AN UNANSWERABLE PROBE ARE THE ONLY REDS, and neither may proceed. test fn witness_unrelated_and_unverifiable_refuse_without_mutating() -> Bool { let unrelated = related(relation: UnrelatedHistories) + let diverged = related(relation: DivergedHistories) let unverifiable = related(relation: RelationUnverifiable { cause: "bad object" as NonEmptyStr }) - deploy_target_refuses(decision: unrelated) + deploy_target_refuses(decision: diverged) + && !deploy_target_proceeds(decision: diverged) + && string_contains(s: deploy_target_decision_line(decision: diverged), pattern: "diverged") + && deploy_target_refuses(decision: unrelated) && !deploy_target_proceeds(decision: unrelated) && deploy_target_refuses(decision: unverifiable) && !deploy_target_proceeds(decision: unverifiable) diff --git a/dag/test/claim/runner/runner_slot_provision_witness_test.dag b/dag/test/claim/runner/runner_slot_provision_witness_test.dag index b8586f2d092..93268f62c2a 100644 --- a/dag/test/claim/runner/runner_slot_provision_witness_test.dag +++ b/dag/test/claim/runner/runner_slot_provision_witness_test.dag @@ -4,6 +4,9 @@ import std.logic { Bool } import std.types { list_length, NonEmptyStr, List, String } import extdeps.toolchain.types { Aarch64 } import extdeps.github.actions_runner { + actions_runner_registration_decode, + ActionsRunnerRegistrationDecoded, ActionsRunnerRegistrationUnreadable, + ActionsRunnerRegistrationPersistent, ActionsRunnerRegistrationEphemeral, actions_runner_release_2_336_0, actions_runner_binary_artifact_for_arch, actions_runner_download_url, @@ -29,6 +32,8 @@ import gunbc.runner_slot_provision { RunnerSlotProvenance, SlotProvenanceObserved, SlotProvenanceUnobserved, + RunnerSlotRegistration, SlotUnregistered, SlotRegisteredPersistent, SlotRegisteredEphemeral, SlotRegistrationUnreadable, + runner_slot_provenance_wire, RunnerSlotMemberAt, desired_runner_slot_members, runner_slot_membership_reconcile, @@ -388,7 +393,7 @@ fn provenance_witness_reconcile(observed: List) -> MembershipP test fn a_retired_incarnation_is_owned_and_plans_removal() -> Bool { let retired = provenance_witness_member( name: "wfix-40", - p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) (runner_slot_member_ownership(m: retired) == Present { value: Owned }) && (provenance_witness_reconcile(observed: [retired]).actions |> all(a => match a { @@ -403,9 +408,9 @@ test fn a_retired_incarnation_is_owned_and_plans_removal() -> Bool { test fn a_registered_or_active_or_enabled_tree_still_refuses_removal() -> Bool { let live = [ - provenance_witness_member(name: "wfix-41", p: SlotProvenanceObserved { in_host_grammar: true, registered: true, unit_active: false, unit_enabled: false }), - provenance_witness_member(name: "wfix-42", p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: true, unit_enabled: false }), - provenance_witness_member(name: "wfix-43", p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: true }), + provenance_witness_member(name: "wfix-41", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredPersistent, unit_active: false, unit_enabled: false }), + provenance_witness_member(name: "wfix-42", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: true, unit_enabled: false }), + provenance_witness_member(name: "wfix-43", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: true }), provenance_witness_member(name: "wfix-44", p: SlotProvenanceUnobserved), ] (live |> all(m => runner_slot_member_ownership(m: m) == Absent)) @@ -419,10 +424,60 @@ test fn a_registered_or_active_or_enabled_tree_still_refuses_removal() -> Bool { })) } +// srv2-11 and srv2-12 on 2026-09-04: `.runner` present with Ephemeral=True, unit inactive and +// disabled since June. GitHub had already unregistered them (ephemeral registrations end after +// one job), so the tree is a retired incarnation; a Bool registration flag refused it forever. +test fn a_dead_ephemeral_registration_is_a_retired_incarnation() -> Bool { + let ghost = provenance_witness_member( + name: "wfix-11", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredEphemeral, unit_active: false, unit_enabled: false }, + ) + (runner_slot_member_ownership(m: ghost) == Present { value: Owned }) + && (provenance_witness_reconcile(observed: [ghost]).actions |> count) == 1 + && string_contains(s: runner_slot_provenance_wire(p: ghost.provenance), pattern: "ephemeral") +} + +// RED controls on the same shape: an ACTIVE ephemeral registration is a live runner, and an +// unreadable registration file decides nothing. +test fn an_active_ephemeral_registration_and_an_unreadable_one_still_refuse() -> Bool { + let live = provenance_witness_member( + name: "wfix-12", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredEphemeral, unit_active: true, unit_enabled: true }, + ) + let unreadable = provenance_witness_member( + name: "wfix-13", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegistrationUnreadable { reason: "not json" }, unit_active: false, unit_enabled: false }, + ) + runner_slot_member_ownership(m: live) == Absent && runner_slot_member_ownership(m: unreadable) == Absent +} + +data fixture_runner_registration_ephemeral: String = "{\"AgentId\":\"1\",\"AgentName\":\"wfix-11-1-1\",\"Ephemeral\":\"True\",\"PoolId\":\"1\",\"GitHubUrl\":\"https://github.com/example\"}" +data fixture_runner_registration_persistent: String = "{\"AgentId\":\"2\",\"AgentName\":\"wfix-01\",\"PoolId\":\"1\",\"GitHubUrl\":\"https://github.com/example\"}" + +test fn the_registration_file_decodes_its_ephemeral_member() -> Bool { + let ephemeral = match actions_runner_registration_decode(raw: fixture_runner_registration_ephemeral) { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } => true + _ => false + } + let persistent = match actions_runner_registration_decode(raw: fixture_runner_registration_persistent) { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } => true + _ => false + } + let garbage = match actions_runner_registration_decode(raw: "not json") { + ActionsRunnerRegistrationUnreadable { reason: _ } => true + _ => false + } + let widened = match actions_runner_registration_decode(raw: "{\"Ephemeral\":\"maybe\"}") { + ActionsRunnerRegistrationUnreadable { reason } => string_contains(s: reason, pattern: "outside") + _ => false + } + ephemeral && persistent && garbage && widened +} + test fn a_name_outside_the_host_grammar_refuses_even_when_dead() -> Bool { let script = provenance_witness_member( name: "jit-runner.sh", - p: SlotProvenanceObserved { in_host_grammar: false, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: false, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) runner_slot_member_ownership(m: script) == Absent } @@ -444,7 +499,7 @@ test fn the_plan_realizes_removal_through_the_privileged_seam() -> Bool { Present { value: artifact } => { let retired = provenance_witness_member( name: "wfix-40", - p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) let plan = runner_slot_provision_plan(deploy: fixture_five_slot_deploy(), artifact: artifact, observed: [retired]) (plan.teardown_scripts |> count) == 1 diff --git a/dag/test/claim/workflow_dispatch_input_witness_test.dag b/dag/test/claim/workflow_dispatch_input_witness_test.dag index d773bc9c70e..1246e5e1a6e 100644 --- a/dag/test/claim/workflow_dispatch_input_witness_test.dag +++ b/dag/test/claim/workflow_dispatch_input_witness_test.dag @@ -154,12 +154,14 @@ test fn workflow_dispatch_choice_input_projects_options_list() -> Bool { // release binaries are produced once and consumed by everything that installs them, so a job that // mutates a host without that edge would be installing whatever happened to be lying around. test fn fleet_converge_workflow_has_build_job_needs_release_bins() -> Bool { - fleet_converge_workflow.jobs.length() == 3 + fleet_converge_workflow.jobs.length() == 4 && fleet_converge_workflow.jobs[0].id == "build" && fleet_converge_workflow.jobs[1].id == "fleet-converge" && fleet_converge_workflow.jobs[1].needs == ["build"] && fleet_converge_workflow.jobs[2].id == "dashboard-deploy" && fleet_converge_workflow.jobs[2].needs == ["build"] + && fleet_converge_workflow.jobs[3].id == "rlm-launch-deployment-receipt" + && fleet_converge_workflow.jobs[3].needs == ["build"] } fn expected_fleet_converge_yml_content() -> String { diff --git a/provisioning/srv1/gunbc-ghrunner.sudoers b/provisioning/srv1/gunbc-ghrunner.sudoers index d0b773461b1..dfe10bf696e 100644 --- a/provisioning/srv1/gunbc-ghrunner.sudoers +++ b/provisioning/srv1/gunbc-ghrunner.sudoers @@ -1,45 +1,88 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv1/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-06 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-01.service diff --git a/provisioning/srv2/gunbc-ghrunner.sudoers b/provisioning/srv2/gunbc-ghrunner.sudoers index c729b5f24a7..fae096d343a 100644 --- a/provisioning/srv2/gunbc-ghrunner.sudoers +++ b/provisioning/srv2/gunbc-ghrunner.sudoers @@ -1,13 +1,72 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv2/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-20 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv2-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv2-01.service diff --git a/provisioning/srv3/gunbc-ghrunner.sudoers b/provisioning/srv3/gunbc-ghrunner.sudoers index 5d349c15ed3..0058624b209 100644 --- a/provisioning/srv3/gunbc-ghrunner.sudoers +++ b/provisioning/srv3/gunbc-ghrunner.sudoers @@ -1,43 +1,87 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv3/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-01.service diff --git a/provisioning/srv4/gunbc-ghrunner.sudoers b/provisioning/srv4/gunbc-ghrunner.sudoers index c1afb9fa20b..ade6dd4c218 100644 --- a/provisioning/srv4/gunbc-ghrunner.sudoers +++ b/provisioning/srv4/gunbc-ghrunner.sudoers @@ -1,45 +1,88 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv4/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-06 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-01.service From 1ba5b4b35978527c40d0a6c26370f9b6ea9f63d0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 5 Sep 2026 03:38:25 +0000 Subject: [PATCH 2/3] The org-admin credential is minted in-run from the gunbai-ci App key; no human token, no repository secret The interim design asked the operator to mint a fine-grained PAT and paste it into GUNBC_ORG_ADMIN_TOKEN. It was never taken and was never needed: the gunbai-ci GitHub App is installed on the organization, its private key is in Secret Manager behind the same WIF read the fleet key uses, and the runner installer already mints registration tokens with it. gunbc.ci_spec gunbc_ci_org_admin_app_token_prelude follows the fleet-key lifecycle (WIF token by env, the SecretRef's own access URL, 0600 under RUNNER_TEMP, trap before the key touches disk), signs a ten-minute RS256 JWT with openssl, exchanges it for a one-hour installation token at the cited endpoint (extdeps.github.org_admin_auth github_app_installation_access_token_url), classifies anything but 201 as OrgAdminInstallationTokenRefused with the status line, wipes the key, and exports the token into that step's environment only. The workflow step drops its secrets.GUNBC_ORG_ADMIN_TOKEN rows and takes the WIF access token instead. The acquisition plan's interim section is marked superseded; the witness asserts the endpoint, the secret version path, the signing, the refusal name, and the absence of any repository secret reference. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FbXKcgqW4Jx7e78BPWc8ci --- .github/workflows/fleet-converge.yml | 31 +++++- dag/extdeps/github/org_admin_auth.dag | 17 ++- dag/gunbc/ci/ci_spec.dag | 104 +++++++++++++++++- dag/gunbc/fleet/fleet_converge_workflow.dag | 3 +- dag/gunbc/fleet/org_actions_converge.dag | 2 +- .../workflow_dispatch_input_witness_test.dag | 14 ++- .../plans/org-admin-credential-acquisition.md | 7 ++ 7 files changed, 167 insertions(+), 11 deletions(-) diff --git a/.github/workflows/fleet-converge.yml b/.github/workflows/fleet-converge.yml index de953b1c39c..cc1d9f66800 100644 --- a/.github/workflows/fleet-converge.yml +++ b/.github/workflows/fleet-converge.yml @@ -293,11 +293,38 @@ jobs: - name: Org Actions credential validation + read-only settings diff id: org_actions_observe run: | + set -euo pipefail + umask 077 + KEY_FILE="$RUNNER_TEMP/org-admin-app-key" + HDR_FILE="$RUNNER_TEMP/org-admin-auth-header" + JWT_FILE="$RUNNER_TEMP/org-admin-app-jwt" + TOKEN_HEADERS="$RUNNER_TEMP/org-admin-token-headers" + TOKEN_BODY="$RUNNER_TEMP/org-admin-token-body" + cleanup() { rm -f "$KEY_FILE" "$HDR_FILE" "$JWT_FILE" "$TOKEN_HEADERS" "$TOKEN_BODY"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; } + trap cleanup EXIT + printf 'Authorization: Bearer %s\n' "$WIF_ACCESS_TOKEN" > "$HDR_FILE" + curl -sSf -H @"$HDR_FILE" "https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest:access" | python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(sys.stdin)["payload"]["data"]))' > "$KEY_FILE" + rm -f "$HDR_FILE" + chmod 600 "$KEY_FILE" + NOW=$(date +%s) + b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; } + JWT_HEADER=$(printf '%s' '{"alg":"RS256","typ":"JWT"}' | b64url) + JWT_PAYLOAD=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$((NOW-60))" "$((NOW+540))" "2653532" | b64url) + JWT_SIG=$(printf '%s.%s' "$JWT_HEADER" "$JWT_PAYLOAD" | openssl dgst -sha256 -sign "$KEY_FILE" | b64url) + rm -f "$KEY_FILE" + printf 'Authorization: Bearer %s.%s.%s\n' "$JWT_HEADER" "$JWT_PAYLOAD" "$JWT_SIG" > "$JWT_FILE" + curl -sS -X POST -H @"$JWT_FILE" -H 'Accept: application/vnd.github+json' -D "$TOKEN_HEADERS" -o "$TOKEN_BODY" "https://api.github.com/app/installations/104134109/access_tokens" + rm -f "$JWT_FILE" + if ! head -n1 "$TOKEN_HEADERS" | grep -q ' 201 '; then echo "OrgAdminInstallationTokenRefused: GitHub answered $(head -n1 "$TOKEN_HEADERS" | tr -d '\r') for the gunbai-ci installation; a revoked key, a removed installation, or a missing Self-hosted-runners permission each look like this -- inspect the App's installation on the organization" >&2; exit 1; fi + GUNBC_ORG_ADMIN_TOKEN="$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1]))["token"])' "$TOKEN_BODY")" + rm -f "$TOKEN_HEADERS" "$TOKEN_BODY" + export GUNBC_ORG_ADMIN_TOKEN + export GH_TOKEN="$GUNBC_ORG_ADMIN_TOKEN" + echo "org-admin: installation token minted in-run for app gunbai-ci installation 104134109 (one-hour lifetime; key wiped; token held in this step's environment only)" ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) "$ROOT/target/release/gunbc" run --source-root "$ROOT/dag" --source-root "$ROOT/src/v2" --entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet env: - GUNBC_ORG_ADMIN_TOKEN: ${{ secrets.GUNBC_ORG_ADMIN_TOKEN }} - GH_TOKEN: ${{ secrets.GUNBC_ORG_ADMIN_TOKEN }} + WIF_ACCESS_TOKEN: ${{ steps.wif_auth.outputs.access_token }} if: github.event.inputs.mode == 'org_actions_observe' timeout-minutes: 5 - name: Upload org Actions credential validation receipt diff --git a/dag/extdeps/github/org_admin_auth.dag b/dag/extdeps/github/org_admin_auth.dag index a574f328b59..ec0ee5a484c 100644 --- a/dag/extdeps/github/org_admin_auth.dag +++ b/dag/extdeps/github/org_admin_auth.dag @@ -1,6 +1,6 @@ module extdeps.github.org_admin_auth -import std.types { NonEmptyStr, Timestamp } +import std.types { NonEmptyStr, Timestamp, String, Int } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } @@ -23,6 +23,21 @@ data installation_token_citation: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https, locator: "docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app" } } +// The installation access token endpoint and the App JWT bounds are facts of GitHub's API +// (installation_token_citation): the JWT is signed RS256 with the App's private key, its `iat` may +// be backdated up to 60 s for clock skew, its `exp` may be at most 10 minutes after issue, and the +// POST returns a token valid for one hour. Modeled here so the workflow step that mints one names +// the endpoint through the citation rather than spelling a URL. +data github_app_installation_access_token_url_prefix: String = "https://api.github.com/app/installations/" + +fn github_app_installation_access_token_url(installation_id: Int) -> String { + join([github_app_installation_access_token_url_prefix, to_string(installation_id), "/access_tokens"], "") +} + +data github_app_jwt_backdate_seconds: Int = 60 + +data github_app_jwt_lifetime_seconds: Int = 540 + data oauth_flow_citation: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https, locator: "docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps" } } diff --git a/dag/gunbc/ci/ci_spec.dag b/dag/gunbc/ci/ci_spec.dag index 7a53c96d5df..3c9242a054f 100644 --- a/dag/gunbc/ci/ci_spec.dag +++ b/dag/gunbc/ci/ci_spec.dag @@ -1,6 +1,12 @@ module gunbc.ci_spec import gunbc.ledger_row_coherence { heal_repair_declaration_artifact_path } import extdeps.cloud.gcp.secret_ref { secret_ref_access_url, secret_ref_version_resource } +import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_app_pem_secret, gunbai_ci_declared_installation_id } +import extdeps.github.org_admin_auth { + github_app_installation_access_token_url, + github_app_jwt_backdate_seconds, + github_app_jwt_lifetime_seconds, +} import gunbc.spark.credential_workflow { spark_administrator_password_secret_ref } import gunbc.spark.grant_privileged_operation { spark_grant_credential_path_env } import gunbc.spark.bootstrap_provision { spark_bootstrap_cloud_principal_member } @@ -1362,8 +1368,104 @@ fn gunbc_ci_fleet_converge_apply_invoke() -> String { ) } +// THE ORG-ADMIN CREDENTIAL IS MINTED IN-RUN FROM THE App KEY THE FLEET ALREADY HOLDS; NO HUMAN +// TOKEN EXISTS. The interim in docs/plans/org-admin-credential-acquisition.md asked the operator to +// mint a fine-grained PAT and paste it into an Actions secret. That step was never taken, and it +// was never necessary: the gunbai-ci GitHub App (gunbc.auth.github_apps gunbai_ci_declared) is +// installed on the organization, its private key sits in Secret Manager behind the same WIF read +// the fleet key uses, and the runner installer already mints registration tokens with it. So this +// prelude follows the fleet-key lifecycle exactly -- WIF token by env, the SecretRef's own access +// URL, a 0600 file under RUNNER_TEMP, a trap armed before the key touches disk -- then signs a +// ten-minute RS256 JWT with openssl, exchanges it for a one-hour installation token +// (extdeps.github.org_admin_auth installation_token_citation), wipes the key, and exports the token +// into THIS step's environment only. Nothing reaches GITHUB_ENV; the trap unsets it on exit. +// +// THE HTTP STATUS IS CLASSIFIED, NOT COLLAPSED (the lesson gunbc_ci_fleet_key_agent_prelude's +// administrator sibling records): the mint POST writes its headers and body to files, and anything +// but 201 refuses with the status line, so a revoked key, a removed installation and a missing +// permission each surface as what they are rather than as a JSON decode traceback. +fn gunbc_ci_org_admin_app_token_prelude() -> List { + [ + Do { run: ci_retry_body_run(command: "set -euo pipefail") }, + Do { run: ci_retry_body_run(command: "umask 077") }, + Do { run: ci_retry_body_run(command: "KEY_FILE=\"$RUNNER_TEMP/org-admin-app-key\"") }, + Do { run: ci_retry_body_run(command: "HDR_FILE=\"$RUNNER_TEMP/org-admin-auth-header\"") }, + Do { run: ci_retry_body_run(command: "JWT_FILE=\"$RUNNER_TEMP/org-admin-app-jwt\"") }, + Do { run: ci_retry_body_run(command: "TOKEN_HEADERS=\"$RUNNER_TEMP/org-admin-token-headers\"") }, + Do { run: ci_retry_body_run(command: "TOKEN_BODY=\"$RUNNER_TEMP/org-admin-token-body\"") }, + Do { + run: ci_retry_body_run( + command: "cleanup() { rm -f \"$KEY_FILE\" \"$HDR_FILE\" \"$JWT_FILE\" \"$TOKEN_HEADERS\" \"$TOKEN_BODY\"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; }" + ) + }, + Do { run: ci_retry_body_run(command: "trap cleanup EXIT") }, + Do { run: ci_retry_body_run(command: gunbc_ci_auth_header_file_write_command()) }, + Do { + run: ci_retry_body_run( + command: join([ + "curl -sSf -H @\"$HDR_FILE\" \"", + secret_ref_access_url(ref: gunbai_ci_app_pem_secret), + "\" | python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(sys.stdin)[\"payload\"][\"data\"]))' > \"$KEY_FILE\"" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "rm -f \"$HDR_FILE\"") }, + Do { run: ci_retry_body_run(command: "chmod 600 \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "NOW=$(date +%s)") }, + Do { run: ci_retry_body_run(command: "b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; }") }, + Do { run: ci_retry_body_run(command: "JWT_HEADER=$(printf '%s' '\{\"alg\":\"RS256\",\"typ\":\"JWT\"}' | b64url)") }, + Do { + run: ci_retry_body_run( + command: join([ + "JWT_PAYLOAD=$(printf '\{\"iat\":%d,\"exp\":%d,\"iss\":\"%s\"}' \"$((NOW-", + to_string(github_app_jwt_backdate_seconds), + "))\" \"$((NOW+", + to_string(github_app_jwt_lifetime_seconds), + "))\" \"", + to_string(gunbai_ci_declared.app_id.value), + "\" | b64url)" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "JWT_SIG=$(printf '%s.%s' \"$JWT_HEADER\" \"$JWT_PAYLOAD\" | openssl dgst -sha256 -sign \"$KEY_FILE\" | b64url)") }, + Do { run: ci_retry_body_run(command: "rm -f \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "printf 'Authorization: Bearer %s.%s.%s\\n' \"$JWT_HEADER\" \"$JWT_PAYLOAD\" \"$JWT_SIG\" > \"$JWT_FILE\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "curl -sS -X POST -H @\"$JWT_FILE\" -H 'Accept: application/vnd.github+json' -D \"$TOKEN_HEADERS\" -o \"$TOKEN_BODY\" \"", + github_app_installation_access_token_url(installation_id: gunbai_ci_declared_installation_id.value), + "\"" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "rm -f \"$JWT_FILE\"") }, + Do { + run: ci_retry_body_run( + command: "if ! head -n1 \"$TOKEN_HEADERS\" | grep -q ' 201 '; then echo \"OrgAdminInstallationTokenRefused: GitHub answered $(head -n1 \"$TOKEN_HEADERS\" | tr -d '\\r') for the gunbai-ci installation; a revoked key, a removed installation, or a missing Self-hosted-runners permission each look like this -- inspect the App's installation on the organization\" >&2; exit 1; fi" + ) + }, + Do { run: ci_retry_body_run(command: "GUNBC_ORG_ADMIN_TOKEN=\"$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1]))[\"token\"])' \"$TOKEN_BODY\")\"") }, + Do { run: ci_retry_body_run(command: "rm -f \"$TOKEN_HEADERS\" \"$TOKEN_BODY\"") }, + Do { run: ci_retry_body_run(command: "export GUNBC_ORG_ADMIN_TOKEN") }, + Do { run: ci_retry_body_run(command: "export GH_TOKEN=\"$GUNBC_ORG_ADMIN_TOKEN\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "echo \"org-admin: installation token minted in-run for app ", + gunbai_ci_declared.slug as String, + " installation ", + to_string(gunbai_ci_declared_installation_id.value), + " (one-hour lifetime; key wiped; token held in this step's environment only)\"" + ], "") + ) + } + ] +} + fn gunbc_ci_org_actions_converge_invoke() -> String { - gunbc_run_step_script( + gunbc_run_step_script_with_prelude( + prelude: gunbc_ci_org_admin_app_token_prelude(), source_roots: witness_layer_roots, entry: gunbc_ci_org_actions_converge_target.entry, function: gunbc_ci_org_actions_converge_target.function, diff --git a/dag/gunbc/fleet/fleet_converge_workflow.dag b/dag/gunbc/fleet/fleet_converge_workflow.dag index edb7b476218..650262788e7 100644 --- a/dag/gunbc/fleet/fleet_converge_workflow.dag +++ b/dag/gunbc/fleet/fleet_converge_workflow.dag @@ -350,8 +350,7 @@ fn fleet_converge_org_actions_step() -> Step { run: gunbc_ci_org_actions_converge_invoke(), shell: none, env: Present { value: [ - kv(key: "GUNBC_ORG_ADMIN_TOKEN", value: yaml_string(s: "${{ secrets.GUNBC_ORG_ADMIN_TOKEN }}")), - kv(key: "GH_TOKEN", value: yaml_string(s: "${{ secrets.GUNBC_ORG_ADMIN_TOKEN }}")), + kv(key: "WIF_ACCESS_TOKEN", value: yaml_string(s: "${{ steps.wif_auth.outputs.access_token }}")), ] }, working_directory: none, if_condition: Present { value: fleet_converge_org_actions_step_if }, diff --git a/dag/gunbc/fleet/org_actions_converge.dag b/dag/gunbc/fleet/org_actions_converge.dag index 2365444382a..210ab584b30 100644 --- a/dag/gunbc/fleet/org_actions_converge.dag +++ b/dag/gunbc/fleet/org_actions_converge.dag @@ -228,7 +228,7 @@ fn completed_groups(reads: List) -> List func org_actions_converge_wet() -> ProcessExit { let credential = env_credential(env_var: org_admin_token_env_name) match credential.token { - Absent => exit_failure(reason: "OrgActionsCredentialMissing: GUNBC_ORG_ADMIN_TOKEN is absent") + Absent => exit_failure(reason: "OrgActionsCredentialMissing: GUNBC_ORG_ADMIN_TOKEN is absent -- the org-actions workflow step mints it in-run from the gunbai-ci App key (gunbc.ci_spec gunbc_ci_org_admin_app_token_prelude); outside that step there is no credential to read") Present { value: _ } => { let listed = github.CliOrgRunnerGroups.ListRunnerGroupsJson(org: org_actions_organization) if !listed.success { diff --git a/dag/test/claim/workflow_dispatch_input_witness_test.dag b/dag/test/claim/workflow_dispatch_input_witness_test.dag index 1246e5e1a6e..052d024e5d9 100644 --- a/dag/test/claim/workflow_dispatch_input_witness_test.dag +++ b/dag/test/claim/workflow_dispatch_input_witness_test.dag @@ -187,9 +187,15 @@ test fn fleet_converge_apply_step_binds_plan_hash_to_env_not_shell_literal() -> && string_contains(s: gunbc_ci_fleet_converge_apply_invoke(), pattern: "$\{EXPECTED_HASH:-\}") } +// The org-actions mode mints its credential in-run from the App key: the step names the App's +// installation token endpoint and the key's Secret Manager version, and the workflow carries no +// repository secret for it (the interim PAT design was superseded before it was ever taken). test fn fleet_converge_org_actions_mode_binds_secret_and_modeled_entry_holds() -> Bool { - string_contains( - s: gunbc_ci_org_actions_converge_invoke(), - pattern: "--entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet", - ) + let invoke = gunbc_ci_org_actions_converge_invoke() + string_contains(s: invoke, pattern: "--entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet") + && string_contains(s: invoke, pattern: "app/installations/104134109/access_tokens") + && string_contains(s: invoke, pattern: "secrets/ci-github-app-private-key/") + && string_contains(s: invoke, pattern: "openssl dgst -sha256 -sign") + && string_contains(s: invoke, pattern: "OrgAdminInstallationTokenRefused") + && !string_contains(s: expected_fleet_converge_yml_content(), pattern: "secrets.GUNBC_ORG_ADMIN_TOKEN") } diff --git a/docs/plans/org-admin-credential-acquisition.md b/docs/plans/org-admin-credential-acquisition.md index 1de32e07cbd..46d7e818fe1 100644 --- a/docs/plans/org-admin-credential-acquisition.md +++ b/docs/plans/org-admin-credential-acquisition.md @@ -88,6 +88,13 @@ performs the live read without a code-path switch. ## Interim human handoff +**Superseded 2026-09-05, never taken.** The `org_actions_observe` step now mints a one-hour installation +token in-run from the existing `gunbai-ci` App key (Secret Manager `ci-github-app-private-key`, read +through the same WIF path as the fleet key), so no personal token is created and no Actions secret +holds a credential. The steps below are kept as the record of the design that was replaced. What +remains of the terminal migration is narrowing to a dedicated least-privilege App; the read path and +custody contract are already the terminal ones. + 1. In GitHub's fine-grained token UI, the operator selects `gunb-ai` as resource owner, grants organization `Self-hosted runners: write` and `Administration: write`, and chooses a bounded expiry. No repository content permission is needed for the org-settings probe itself. From f740b474a7289967821bde4a0c37f374b9c39bfd Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 5 Sep 2026 03:54:29 +0000 Subject: [PATCH 3/3] The App key read classifies its Secret Manager status, and its accessor binding is a converge the control plane runs The first live org_actions_observe run refused with a JSON decode traceback: Secret Manager answered 403 for ci-github-app-private-key to the fleet-cloud-convergence principal, and `curl -sSf | python3` collapsed that into "Expecting value" -- the exact status-collapse the administrator prelude's note records. The read now captures the HTTP code, refuses OrgAdminAppKeyUnreadable naming the version resource, the principal, the 403 ambiguity, and the remedy, and decodes only a 200 body. The remedy is modeled rather than a hand gcloud line: gunbc.spark.secret_access_ensure is generalized over its target secret (secret_access_ensure_for; the spark entry is one caller), and gunbc.fleet.org_actions_converge gains org_admin_app_key_access_converge_with_supplied_token, which reconciles roles/secretmanager.secretAccessor on the App key for the workload principal using a control-plane token from GUNBC_GCP_ACCESS_TOKEN_FILE. The 14 spark access rows still pass against the generalized ensure; the dispatch witness asserts the classified read. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FbXKcgqW4Jx7e78BPWc8ci --- .github/workflows/fleet-converge.yml | 8 ++++++-- dag/gunbc/ci/ci_spec.dag | 18 +++++++++++++++--- dag/gunbc/fleet/org_actions_converge.dag | 18 ++++++++++++++++++ dag/gunbc/spark/secret_access_ensure.dag | 13 +++++++++++-- .../workflow_dispatch_input_witness_test.dag | 2 ++ 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/.github/workflows/fleet-converge.yml b/.github/workflows/fleet-converge.yml index cc1d9f66800..4a623c87805 100644 --- a/.github/workflows/fleet-converge.yml +++ b/.github/workflows/fleet-converge.yml @@ -300,11 +300,15 @@ jobs: JWT_FILE="$RUNNER_TEMP/org-admin-app-jwt" TOKEN_HEADERS="$RUNNER_TEMP/org-admin-token-headers" TOKEN_BODY="$RUNNER_TEMP/org-admin-token-body" - cleanup() { rm -f "$KEY_FILE" "$HDR_FILE" "$JWT_FILE" "$TOKEN_HEADERS" "$TOKEN_BODY"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; } + cleanup() { rm -f "$KEY_FILE" "$HDR_FILE" "$JWT_FILE" "$TOKEN_HEADERS" "$TOKEN_BODY" "$RUNNER_TEMP/org-admin-app-key-sm-body"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; } trap cleanup EXIT printf 'Authorization: Bearer %s\n' "$WIF_ACCESS_TOKEN" > "$HDR_FILE" - curl -sSf -H @"$HDR_FILE" "https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest:access" | python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(sys.stdin)["payload"]["data"]))' > "$KEY_FILE" + SM_BODY="$RUNNER_TEMP/org-admin-app-key-sm-body" + SM_CODE="$(curl -sS -o "$SM_BODY" -w '%{http_code}' -H @"$HDR_FILE" "https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest:access")" rm -f "$HDR_FILE" + if [ "$SM_CODE" != 200 ]; then rm -f "$SM_BODY"; echo "OrgAdminAppKeyUnreadable: Secret Manager answered HTTP $SM_CODE for projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest to the fleet-cloud-convergence principal. 403 means the version exists and is not readable OR does not exist -- REMEDY: run org_admin_app_key_access_converge_with_supplied_token (gunbc.fleet.org_actions_converge) with a control-plane token to bind roles/secretmanager.secretAccessor, and confirm the version exists. Minting NO token." >&2; exit 1; fi + python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(open(sys.argv[1]))["payload"]["data"]))' "$SM_BODY" > "$KEY_FILE" + rm -f "$SM_BODY" chmod 600 "$KEY_FILE" NOW=$(date +%s) b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; } diff --git a/dag/gunbc/ci/ci_spec.dag b/dag/gunbc/ci/ci_spec.dag index 3c9242a054f..9d2eeb28021 100644 --- a/dag/gunbc/ci/ci_spec.dag +++ b/dag/gunbc/ci/ci_spec.dag @@ -1395,21 +1395,33 @@ fn gunbc_ci_org_admin_app_token_prelude() -> List { Do { run: ci_retry_body_run(command: "TOKEN_BODY=\"$RUNNER_TEMP/org-admin-token-body\"") }, Do { run: ci_retry_body_run( - command: "cleanup() { rm -f \"$KEY_FILE\" \"$HDR_FILE\" \"$JWT_FILE\" \"$TOKEN_HEADERS\" \"$TOKEN_BODY\"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; }" + command: "cleanup() { rm -f \"$KEY_FILE\" \"$HDR_FILE\" \"$JWT_FILE\" \"$TOKEN_HEADERS\" \"$TOKEN_BODY\" \"$RUNNER_TEMP/org-admin-app-key-sm-body\"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; }" ) }, Do { run: ci_retry_body_run(command: "trap cleanup EXIT") }, Do { run: ci_retry_body_run(command: gunbc_ci_auth_header_file_write_command()) }, + Do { run: ci_retry_body_run(command: "SM_BODY=\"$RUNNER_TEMP/org-admin-app-key-sm-body\"") }, Do { run: ci_retry_body_run( command: join([ - "curl -sSf -H @\"$HDR_FILE\" \"", + "SM_CODE=\"$(curl -sS -o \"$SM_BODY\" -w '%\{http_code}' -H @\"$HDR_FILE\" \"", secret_ref_access_url(ref: gunbai_ci_app_pem_secret), - "\" | python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(sys.stdin)[\"payload\"][\"data\"]))' > \"$KEY_FILE\"" + "\")\"" ], "") ) }, Do { run: ci_retry_body_run(command: "rm -f \"$HDR_FILE\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "if [ \"$SM_CODE\" != 200 ]; then rm -f \"$SM_BODY\"; echo \"OrgAdminAppKeyUnreadable: Secret Manager answered HTTP $SM_CODE for ", + secret_ref_version_resource(ref: gunbai_ci_app_pem_secret), + " to the fleet-cloud-convergence principal. 403 means the version exists and is not readable OR does not exist -- REMEDY: run org_admin_app_key_access_converge_with_supplied_token (gunbc.fleet.org_actions_converge) with a control-plane token to bind roles/secretmanager.secretAccessor, and confirm the version exists. Minting NO token.\" >&2; exit 1; fi" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(open(sys.argv[1]))[\"payload\"][\"data\"]))' \"$SM_BODY\" > \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "rm -f \"$SM_BODY\"") }, Do { run: ci_retry_body_run(command: "chmod 600 \"$KEY_FILE\"") }, Do { run: ci_retry_body_run(command: "NOW=$(date +%s)") }, Do { run: ci_retry_body_run(command: "b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; }") }, diff --git a/dag/gunbc/fleet/org_actions_converge.dag b/dag/gunbc/fleet/org_actions_converge.dag index 210ab584b30..560493804e0 100644 --- a/dag/gunbc/fleet/org_actions_converge.dag +++ b/dag/gunbc/fleet/org_actions_converge.dag @@ -15,6 +15,8 @@ import extdeps.languages.json.parse { JsonMemberNotAnObject } import extdeps.shell.credentials { env_credential } +import gunbc.auth.github_apps { gunbai_ci_app_pem_secret } +import gunbc.spark.secret_access_ensure { secret_access_ensure_for, read_supplied_access_token, SuppliedTokenReady, SuppliedTokenUnavailable, OperatorSuppliedToken } import extdeps.github.org_admin_auth { RunnerGroupsRead, RunnerGroupRepositoriesRead } import extdeps.filesystem.filesystem_io { Filesystem } import gunbc.clock_read { clock_now_probed_at_or_unknown } @@ -272,3 +274,19 @@ func org_actions_converge_wet() -> ProcessExit { } } } + +// THE ACCESSOR BINDING FOR THE App KEY IS A CONVERGE, RUN BY THE CONTROL-PLANE PRINCIPAL. The +// workload principal (fleet-cloud-convergence) may read secrets it is bound to and may not bind +// itself, so this entry takes an operator-supplied access token (GUNBC_GCP_ACCESS_TOKEN_FILE, the +// same channel gunbc.spark.secret_access_ensure uses) and reconciles roles/secretmanager.secretAccessor +// on ci-github-app-private-key for that principal: get policy, delta, set policy, read back the +// grant. Once it has run, the org-actions step's in-run mint stops answering 403. +func org_admin_app_key_access_converge_with_supplied_token() -> ProcessExit + uses net: Network +{ + match read_supplied_access_token() { + SuppliedTokenUnavailable { cause: c } => exit_failure(reason: c as String) + SuppliedTokenReady { token: t } => + secret_access_ensure_for(target: gunbai_ci_app_pem_secret, token_source: OperatorSuppliedToken { token: t }) + } +} diff --git a/dag/gunbc/spark/secret_access_ensure.dag b/dag/gunbc/spark/secret_access_ensure.dag index d866784fc0c..c2a787960a9 100644 --- a/dag/gunbc/spark/secret_access_ensure.dag +++ b/dag/gunbc/spark/secret_access_ensure.dag @@ -126,7 +126,11 @@ fn spark_secret_grant_present(policy: GcpPolicy, member: String) -> Bool { // operation inside the arm, and return early, so the converged case performs NO WRITE. The no-op // is structural rather than promised, which is why reconcile_gcp_policy's AlreadyConverged is // consumed as its own arm rather than a delta that happens to be empty. -func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit +// THE ENSURE IS OVER A TARGET SECRET, NOT OVER THE SPARK PASSWORD. The gunbai-ci App key +// (gunbc.auth.github_apps gunbai_ci_app_pem_secret) needs the same accessor binding for the same +// workload principal -- the org-actions credential is minted from it in-run and Secret Manager +// answered 403 on 2026-09-05 -- so the target is a parameter and the spark entry is one caller. +func secret_access_ensure_for(target: SecretRef, token_source: AccessTokenSource) -> ProcessExit uses net: Network { match spark_grant_member_resolution() { @@ -138,7 +142,6 @@ func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit AccessTokenEnsureRefused { reason: reason } => return exit_failure(reason: reason as String) AccessTokenReady { token: token } => { - let target = spark_secret_target() observed = gcp.SecretManager.GetSecretIamPolicy( access_token: token, project_id: target.project, @@ -200,6 +203,12 @@ func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit // the correct binding for a stated reason -- the control plane that may call setIamPolicy is // deliberately NOT the workload, so this entry authenticates as the human running it and never as // the convergence service account. +func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit + uses net: Network +{ + secret_access_ensure_for(target: spark_secret_target(), token_source: token_source) +} + func spark_secret_access_converge() -> ProcessExit uses net: Network { diff --git a/dag/test/claim/workflow_dispatch_input_witness_test.dag b/dag/test/claim/workflow_dispatch_input_witness_test.dag index 052d024e5d9..87652d49ea3 100644 --- a/dag/test/claim/workflow_dispatch_input_witness_test.dag +++ b/dag/test/claim/workflow_dispatch_input_witness_test.dag @@ -197,5 +197,7 @@ test fn fleet_converge_org_actions_mode_binds_secret_and_modeled_entry_holds() - && string_contains(s: invoke, pattern: "secrets/ci-github-app-private-key/") && string_contains(s: invoke, pattern: "openssl dgst -sha256 -sign") && string_contains(s: invoke, pattern: "OrgAdminInstallationTokenRefused") + && string_contains(s: invoke, pattern: "OrgAdminAppKeyUnreadable") + && !string_contains(s: invoke, pattern: "curl -sSf -H @\"$HDR_FILE\" \"https://secretmanager") && !string_contains(s: expected_fleet_converge_yml_content(), pattern: "secrets.GUNBC_ORG_ADMIN_TOKEN") }