Context — the estate-wide "Actions-policy CI outage"
Two failure modes recur across the estate: mode-1 (allowed_actions=selected + empty patterns_allowed → external uses: rejected → startup_failure) and mode-2 (sha_pinning_required=true + tag-pinned workflow files → github-owned actions fail the pin rule). The squabbler is the per-PR / per-repo reactive lane for a single stuck gate — but today it mis-attributes this class:
- a
governance-reusable startup_failure → AssignGateOwner{OwnedUpstream:standards} (wrong: the reusable is fine; the local Actions policy blocks it);
- a tag-pinned
codeql.yml startup_failure → EscalateToExpert{Security} (wrong: a scan won't fix a tag-vs-SHA rejection).
The symptom already lands in the Gate (fetch.rs::parse_rollup maps STARTUP_FAILURE → CheckRun::Failed); what's missing is the why-probe. This is a real added rule inside the existing diagnose→classify→move pipeline. Fleet-scale remediation stays out of scope (escalate to ExpertGroup::HypatiaFleet → gitbot-fleet).
Decision of record
Estate default = allowed_actions=all + sha_pinning_required=true. The squabbler's settings-move should propose that posture (or an explicit patterns_allowed superset) while keeping sha_pinning_required=true — never disable pinning. squabble ≠ bypass still holds: these moves only let a required check start; they drop no required context and weaken no check.
Changes
-
crates/squabble-cli/src/fetch.rs — add an Actions-policy why-probe. When any required context resolves to STARTUP_FAILURE (already handled at parse_rollup, ~L171–184), additionally call gh api repos/{o}/{r}/actions/permissions + .../selected-actions and thread {allowed_actions, sha_pinning_required, patterns_allowed} into the fight as classification input (it already fetches the ruleset via gh api .../rules/branches/{b}).
-
crates/squabble-fight/src/workflows.rs — extend WorkflowInfo with tag_pinned_uses: Vec<String> and external_uses: Vec<String> (parse in parse_workflow alongside reusable_repo()). In classify(), when the emitting workflow uses an external reusable/action absent from a selected allowlist (mode-1) or a tag-pinned uses: under sha_pinning_required (mode-2), return the NEW move instead of the current OwnedUpstream / Escalate-Security mis-attribution.
-
crates/squabble-core/src/moves.rs — add Move variants: PinWorkflowActions { workflow, refs } (self-win, SHA-pin at source; twin of the existing RepinReusableWorkflow) and ReconcileActionsPolicy { blocked_ref, add_patterns } / SetActionsAllowedAll (a legitimate non-bypass settings move). Add them to every_move_is_legitimate_by_construction and describe() coverage so the SPARK invariant still holds.
-
crates/squabble-core/src/lib.rs — no structural change; diagnose_with_hints is the existing plug seam — feed it the new hints from the host.
-
crates/squabble-fight/src/lib.rs::structural_scan() — add a probe surfacing "allowed_actions=selected with empty/insufficient patterns_allowed while an external reusable/action is used" and "tag-pinned uses: under sha_pinning_required" even before the check has (yet) startup_failure'd, mirroring the existing well-known/path-filter/AGPL probes.
-
examples/actions-policy-deadlock.json — new offline gate fixture (mirroring examples/gate-deadlock.json) with a required check as missing/startup_failure for regression coverage. The squabbler's own repo currently shows a Secret Scanner startup_failure — an in-repo dogfood target for .github/workflows/dogfood-gate.yml.
-
docs/CHARTER.adoc — add the "Actions-policy startup_failure deadlock" class to the v0.1 fight playbook, stating the tradeoff explicitly (prefer allowed_actions=all while KEEPING sha_pinning_required=true, plus SHA-pin the workflow refs).
DO-NOT pointer: .github/settings.yml (probot/settings) manages branch protection/labels/merge only and cannot set Actions permissions — the fix goes via the REST actions/permissions + .../selected-actions endpoints (single-repo from the squabbler, fleet-wide from gitbot-fleet), never via settings.yml.
Acceptance criteria
- A required-context
STARTUP_FAILURE triggers the why-probe; the fight receives {allowed_actions, sha_pinning_required, patterns_allowed}.
classify() returns PinWorkflowActions for a tag-pinned-under-sha-pinning gate and ReconcileActionsPolicy/SetActionsAllowedAll for an empty-allowlist gate — not OwnedUpstream{standards} / Escalate{Security}.
- New
Move variants pass every_move_is_legitimate_by_construction (SPARK invariant intact) and have describe() text.
examples/actions-policy-deadlock.json is diagnosed to the new move in a unit test.
Model tier
Opus — Rust across three crates plus the SPARK-legitimacy invariant; the classification changes are subtle (distinguishing "policy blocks the check from starting" from genuine upstream/out-of-lane reds).
Related
Fleet-scale detection/remediation belong to hyperpolymath/hypatia + hyperpolymath/gitbot-fleet (the squabbler escalates the fleet dimension to ExpertGroup::HypatiaFleet); source-of-truth allowlist in hyperpolymath/standards.
Context — the estate-wide "Actions-policy CI outage"
Two failure modes recur across the estate: mode-1 (
allowed_actions=selected+ emptypatterns_allowed→ externaluses:rejected →startup_failure) and mode-2 (sha_pinning_required=true+ tag-pinned workflow files → github-owned actions fail the pin rule). The squabbler is the per-PR / per-repo reactive lane for a single stuck gate — but today it mis-attributes this class:governance-reusablestartup_failure→AssignGateOwner{OwnedUpstream:standards}(wrong: the reusable is fine; the local Actions policy blocks it);codeql.ymlstartup_failure→EscalateToExpert{Security}(wrong: a scan won't fix a tag-vs-SHA rejection).The symptom already lands in the Gate (
fetch.rs::parse_rollupmapsSTARTUP_FAILURE → CheckRun::Failed); what's missing is the why-probe. This is a real added rule inside the existing diagnose→classify→move pipeline. Fleet-scale remediation stays out of scope (escalate toExpertGroup::HypatiaFleet→ gitbot-fleet).Decision of record
Estate default =
allowed_actions=all+sha_pinning_required=true. The squabbler's settings-move should propose that posture (or an explicitpatterns_allowedsuperset) while keepingsha_pinning_required=true— never disable pinning.squabble ≠ bypassstill holds: these moves only let a required check start; they drop no required context and weaken no check.Changes
crates/squabble-cli/src/fetch.rs— add an Actions-policy why-probe. When any required context resolves toSTARTUP_FAILURE(already handled atparse_rollup, ~L171–184), additionally callgh api repos/{o}/{r}/actions/permissions+.../selected-actionsand thread{allowed_actions, sha_pinning_required, patterns_allowed}into the fight as classification input (it already fetches the ruleset viagh api .../rules/branches/{b}).crates/squabble-fight/src/workflows.rs— extendWorkflowInfowithtag_pinned_uses: Vec<String>andexternal_uses: Vec<String>(parse inparse_workflowalongsidereusable_repo()). Inclassify(), when the emitting workflow uses an external reusable/action absent from aselectedallowlist (mode-1) or a tag-pinneduses:undersha_pinning_required(mode-2), return the NEW move instead of the currentOwnedUpstream/Escalate-Securitymis-attribution.crates/squabble-core/src/moves.rs— addMovevariants:PinWorkflowActions { workflow, refs }(self-win, SHA-pin at source; twin of the existingRepinReusableWorkflow) andReconcileActionsPolicy { blocked_ref, add_patterns }/SetActionsAllowedAll(a legitimate non-bypass settings move). Add them toevery_move_is_legitimate_by_constructionanddescribe()coverage so the SPARK invariant still holds.crates/squabble-core/src/lib.rs— no structural change;diagnose_with_hintsis the existing plug seam — feed it the new hints from the host.crates/squabble-fight/src/lib.rs::structural_scan()— add a probe surfacing "allowed_actions=selectedwith empty/insufficientpatterns_allowedwhile an external reusable/action is used" and "tag-pinneduses:undersha_pinning_required" even before the check has (yet)startup_failure'd, mirroring the existing well-known/path-filter/AGPL probes.examples/actions-policy-deadlock.json— new offline gate fixture (mirroringexamples/gate-deadlock.json) with a required check asmissing/startup_failurefor regression coverage. The squabbler's own repo currently shows aSecret Scannerstartup_failure— an in-repo dogfood target for.github/workflows/dogfood-gate.yml.docs/CHARTER.adoc— add the "Actions-policystartup_failuredeadlock" class to the v0.1 fight playbook, stating the tradeoff explicitly (preferallowed_actions=allwhile KEEPINGsha_pinning_required=true, plus SHA-pin the workflow refs).DO-NOT pointer:
.github/settings.yml(probot/settings) manages branch protection/labels/merge only and cannot set Actions permissions — the fix goes via the RESTactions/permissions+.../selected-actionsendpoints (single-repo from the squabbler, fleet-wide from gitbot-fleet), never via settings.yml.Acceptance criteria
STARTUP_FAILUREtriggers the why-probe; the fight receives{allowed_actions, sha_pinning_required, patterns_allowed}.classify()returnsPinWorkflowActionsfor a tag-pinned-under-sha-pinning gate andReconcileActionsPolicy/SetActionsAllowedAllfor an empty-allowlist gate — notOwnedUpstream{standards}/Escalate{Security}.Movevariants passevery_move_is_legitimate_by_construction(SPARK invariant intact) and havedescribe()text.examples/actions-policy-deadlock.jsonis diagnosed to the new move in a unit test.Model tier
Opus — Rust across three crates plus the SPARK-legitimacy invariant; the classification changes are subtle (distinguishing "policy blocks the check from starting" from genuine upstream/out-of-lane reds).
Related
Fleet-scale detection/remediation belong to
hyperpolymath/hypatia+hyperpolymath/gitbot-fleet(the squabbler escalates the fleet dimension toExpertGroup::HypatiaFleet); source-of-truth allowlist inhyperpolymath/standards.