From c11fb657e1a71fabd8cdbf06d1e6a821163bd836 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:21:10 +0900 Subject: [PATCH 001/154] fix(governance): require central reviews for stacked prs --- README.md | 10 +++--- ...01-central-review-stacked-pull-requests.md | 36 +++++++++++++++++++ docs/org-required-workflow-rollout.md | 13 ++++++- .../ci/audit_central_required_workflows.py | 4 +-- ...central_required_workflow_ruleset_audit.py | 13 +++++-- 5 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 docs/adr/0001-central-review-stacked-pull-requests.md diff --git a/README.md b/README.md index 94e91fe4db..4e740b4838 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ that means: | Mode | What happens | | --- | --- | | **따로 (this repo alone)** | Clone, test, and operate `.github` as the org profile and workflow source. Local quality gates, Cloudflare dry-run, and this repository's own PRs do not depend on naruon or any sibling product checkout. | -| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]` on default branches). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in that sibling's repository context. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | +| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in that sibling's repository context, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | Do not copy Strix, OpenCode, Noema, or scheduler workflow files into a sibling to "satisfy CI." Thick downstream sync PRs are an anti-pattern @@ -65,8 +65,9 @@ The last checked-in ruleset ledger is Checked-in operator facts: -- Ruleset `18156473` is **active**. It targets every repository default - branch (`~ALL` / `~DEFAULT_BRANCH`) and sources workflows from this +- Ruleset `18156473` is **active**. It targets every repository branch + (`repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`), including + stacked pull-request base branches, and sources workflows from this repository at `refs/heads/main`. - Active required workflow paths: `close-empty-pr.yml`, `noema-review.yml`, `opencode-review.yml`, `pr-review-merge-scheduler.yml`, @@ -89,7 +90,8 @@ workflows into siblings. public repositories inherit ruleset `18156473` without a name-list update. 2. Keep product, build, release, and repo-specific security workflows local. Do not add local copies of OpenCode, Strix, Noema, or the merge scheduler. -3. On each default-branch pull request, GitHub creates the required checks in +3. On each pull request, including stacked pull requests targeting a feature + branch, GitHub creates the required checks in the sibling context. Review judgment stays with OpenCode (and the independent Noema reviewer). Mechanical branch update and merge stay with GitHub Actions in that sibling context, using the configured central diff --git a/docs/adr/0001-central-review-stacked-pull-requests.md b/docs/adr/0001-central-review-stacked-pull-requests.md new file mode 100644 index 0000000000..47476b0f92 --- /dev/null +++ b/docs/adr/0001-central-review-stacked-pull-requests.md @@ -0,0 +1,36 @@ +# ADR-0001: Central review for stacked pull requests + +- Status: Accepted +- Date: 2026-08-20 +- Owners: ContextualWisdomLab platform maintainers +- Figma File ID: N/A — this is a workflow and governance contract with no UI + +## Decision + +The organization ruleset `CWL Central required workflows` (`18156473`) applies +to every branch reference (`ref_name.include=["~ALL"]`) in inherited +repositories. Central OpenCode, Noema, security, and scheduler workflows stay +owned by `ContextualWisdomLab/.github` at `refs/heads/main`. + +## Context + +Stacked PRs target another feature branch, so a default-branch-only ruleset did +not materialize the central required workflow entrypoints. This left buyer- +visible changes with local checks but without the same independent review and +security evidence used for main-targeting PRs. + +## Consequences + +- Every stacked PR receives the same current-head governance entrypoints. +- The scheduler may dispatch review-only work for non-default base branches; + merge automation remains guarded by the PR's actual policy and checks. +- Branch-scope drift is detected by + `scripts/ci/audit_central_required_workflows.py` and its regression tests. +- No workflow is copied into a product repository, preserving the MSA control + boundary. + +## Verification + +The exact live ruleset was read before and after the change. TEPP PRs #158 and +#159 were re-read at their current heads before review-only dispatch. Hosted +Checks remain authoritative for merge decisions. diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 9c42ab0637..26bdb1cdae 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -9,7 +9,7 @@ Use an organization repository ruleset instead of copying workflow files into ea - Ruleset: `CWL Central required workflows` - Ruleset ID: `18156473` - Enforcement: `active` -- Target: branch rules on every repository's default branch (`repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`) +- Target: branch rules on every repository branch (`repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`), including stacked pull-request base branches - Required workflow source repository: `ContextualWisdomLab/.github` - Required workflow source repository ID: `1274066402` - Active required workflow paths: @@ -31,6 +31,17 @@ reports another ref, treat that as operations drift and restore ruleset This keeps Strix security evidence, OpenCode and independent Noema review evidence, and merge/update automation sourced from the central `.github` repository. Target repositories do not need local copies of these workflows for the organization required workflow rule, and new repositories inherit the rule without a repository-name list update. +### Stacked pull-request coverage + +On 2026-08-20, live PRs #158 and #159 in `ContextualWisdomLab/TEPP` targeted +`feat/lineageweave-live-consumer-contract` rather than `main`. They had product +checks but no centrally materialized OpenCode or Noema workflow runs because +the ruleset was scoped to `~DEFAULT_BRANCH`. Ruleset `18156473` now uses +`ref_name.include=["~ALL"]`; the existing scheduler already enumerates open +PRs across base branches and dispatches exact-head review-only work for +stacked PRs. The audit script and regression test enforce this scope so a +future ruleset rollback fails closed. + ## OpenCode required workflow posture The central `.github/workflows/opencode-review.yml` is now part of the active organization required workflow ruleset. diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 34dd161333..8ad0e3b2f5 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -112,8 +112,8 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: ref_names = conditions.get("ref_name") ref_names = ref_names if isinstance(ref_names, dict) else {} - if "~DEFAULT_BRANCH" not in (ref_names.get("include") or []): - errors.append("central ruleset does not target every default branch") + if "~ALL" not in (ref_names.get("include") or []): + errors.append("central ruleset does not target stacked and default-branch PRs") workflow_rules = _typed_rules(payload, "workflows") if len(workflow_rules) != 1: diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index ca7dd4234c..fd82491165 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -28,7 +28,7 @@ def ruleset_payload() -> dict: "include": ["~ALL"], "exclude": ["noema", "IRT-bibliography-set", ".github"], }, - "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + "ref_name": {"include": ["~ALL"], "exclude": []}, }, "rules": [ { @@ -94,6 +94,15 @@ def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] +def test_default_branch_only_scope_rejects_stacked_pull_requests() -> None: + payload = ruleset_payload() + payload["conditions"]["ref_name"]["include"] = ["~DEFAULT_BRANCH"] + + assert audit.audit_ruleset(payload) == [ + "central ruleset does not target stacked and default-branch PRs" + ] + + def test_inherited_scope_allows_private_exclusion_outside_token_visibility() -> None: payload = inherited_ruleset_payload() payload[audit.INHERITED_SCOPE_FIELD].pop("IRT-bibliography-set") @@ -204,7 +213,7 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: "central ruleset enforcement is not active", "central ruleset does not include all repositories", "central ruleset repository exclusions drifted: expected ['.github', 'IRT-bibliography-set', 'noema'], got []", - "central ruleset does not target every default branch", + "central ruleset does not target stacked and default-branch PRs", "expected one workflows rule, found 0", "missing central required workflow .github/workflows/close-empty-pr.yml", "missing central required workflow .github/workflows/noema-review.yml", From ab65fcc7dd6547e84cca8a699430d06077dad022 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:28:13 +0900 Subject: [PATCH 002/154] docs(governance): record restored approval contract --- docs/org-required-workflow-rollout.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 26bdb1cdae..2eb856122e 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -40,7 +40,9 @@ the ruleset was scoped to `~DEFAULT_BRANCH`. Ruleset `18156473` now uses `ref_name.include=["~ALL"]`; the existing scheduler already enumerates open PRs across base branches and dispatches exact-head review-only work for stacked PRs. The audit script and regression test enforce this scope so a -future ruleset rollback fails closed. +future ruleset rollback fails closed. The live repair also restored the +checked-in two-approval contract; workflow source, repository exclusions, and +stale-review/thread-resolution/last-push protections were preserved. ## OpenCode required workflow posture From bc2c93ac88599837555cb1ff3292bb49d245dc5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:29:16 +0900 Subject: [PATCH 003/154] docs(governance): refresh stacked review rollout ledger --- README.md | 2 +- docs/org-required-workflow-rollout.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e740b4838..3e42071baa 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Live work and roadmap live on The narrative brief is [docs/CWL-MASTER-CONTEXT.md](docs/CWL-MASTER-CONTEXT.md). The last checked-in ruleset ledger is [docs/org-required-workflow-rollout.md](docs/org-required-workflow-rollout.md) -(updated 2026-07-23 KST). +(updated 2026-08-21 KST). Checked-in operator facts: diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 2eb856122e..d217b00176 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -1,6 +1,6 @@ # ContextualWisdomLab central required workflow rollout -Updated: 2026-07-23 06:35 KST +Updated: 2026-08-21 03:30 KST ## Decision From 501fe541b725f611145d42672c66b3411287d36d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:04:16 +0900 Subject: [PATCH 004/154] fix(router): permit exact-head dispatch enqueue --- .github/workflows/agent-mention-router.yml | 3 +-- CHANGELOG.md | 1 + tests/test_agent_mention_downstream_idempotency.py | 6 +++--- tests/test_agent_mention_queue_isolation.py | 5 ++--- tests/test_agent_mention_workflow_contract.py | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index b922ba5abd..57600cdfee 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -26,11 +26,10 @@ jobs: ) concurrency: group: review-agent-mention-router-local-${{ github.repository }} - queue: max runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: - actions: read + actions: write contents: write issues: write pull-requests: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2f9f24dc..7a25abb1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Grant the local agent-mention router the `actions: write` permission required for its repository-dispatch ledger, and remove the unsupported `concurrency.queue` key so the workflow passes actionlint and can enqueue exact-head review work. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index 23634f293d..eaa17f953f 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -11,12 +11,12 @@ UPLOAD_ARTIFACT_SHA = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" -def test_router_can_read_durable_central_artifacts() -> None: - """Both local routing and sibling sweeping receive actions read access.""" +def test_router_can_dispatch_and_read_durable_central_artifacts() -> None: + """Local routing dispatches and sibling sweeping reads durable artifacts.""" text = ROUTER_WORKFLOW.read_text(encoding="utf-8") local, sweep = text.split("\n sweep-organization-agent-mentions:\n", 1) - assert "permissions:\n actions: read" in local + assert "permissions:\n actions: write" in local assert "permissions:\n actions: read" in sweep assert "AGENT_DISPATCH_TOKEN: ${{ github.token }}" in local assert "AGENT_DISPATCH_TOKEN: ${{ github.token }}" in sweep diff --git a/tests/test_agent_mention_queue_isolation.py b/tests/test_agent_mention_queue_isolation.py index 8af11e04a8..c92e0e3456 100644 --- a/tests/test_agent_mention_queue_isolation.py +++ b/tests/test_agent_mention_queue_isolation.py @@ -46,8 +46,7 @@ def test_interactive_mentions_and_sweeps_use_independent_queues() -> None: assert not any(line.startswith("concurrency:") for line in header.splitlines()) assert _concurrency_block(local_job) == ( " concurrency:\n" - " group: review-agent-mention-router-local-${{ github.repository }}\n" - " queue: max" + " group: review-agent-mention-router-local-${{ github.repository }}" ) assert _concurrency_block(sweep_job) == ( " concurrency:\n" @@ -67,5 +66,5 @@ def test_interactive_queue_retains_pending_requests_without_cancellation() -> No ) concurrency = _concurrency_block(local_job) - assert "queue: max" in concurrency + assert "queue: max" not in concurrency assert "cancel-in-progress: true" not in concurrency diff --git a/tests/test_agent_mention_workflow_contract.py b/tests/test_agent_mention_workflow_contract.py index c5fc4cae54..396472bcff 100644 --- a/tests/test_agent_mention_workflow_contract.py +++ b/tests/test_agent_mention_workflow_contract.py @@ -29,7 +29,7 @@ def test_workflow_uses_local_event_and_central_sweep_with_job_scoped_writes() -> assert "route-local-agent-mention:" in local assert "github.repository == 'ContextualWisdomLab/.github'" in local for permission in ( - "actions: read", + "actions: write", "contents: write", "issues: write", "pull-requests: read", From aa63517844476ad5d5cfac37f6126d640d5d5115 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:15:06 -0700 Subject: [PATCH 005/154] fix(router): preserve every trusted mention with least privilege --- .github/workflows/agent-mention-router.yml | 4 +--- CHANGELOG.md | 2 +- .../agent-mention-concurrency-isolation.md | 24 ++++++++++++------- ...st_agent_mention_downstream_idempotency.py | 6 ++--- tests/test_agent_mention_queue_isolation.py | 22 ++++++++--------- tests/test_agent_mention_workflow_contract.py | 2 +- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index 57600cdfee..1640940f1d 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -24,12 +24,10 @@ jobs: contains(github.event.comment.body, '@cwl-noema-review') || contains(github.event.comment.body, '@opencode-agent') ) - concurrency: - group: review-agent-mention-router-local-${{ github.repository }} runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: - actions: write + actions: read contents: write issues: write pull-requests: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a25abb1ef..c2270e7142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Grant the local agent-mention router the `actions: write` permission required for its repository-dispatch ledger, and remove the unsupported `concurrency.queue` key so the workflow passes actionlint and can enqueue exact-head review work. +- Keep local agent-mention Actions access read-only and remove its replacing concurrency group together with the unsupported `concurrency.queue` key, so every eligible exact-head mention can enqueue while scheduled sweeps remain independently single-flight. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). diff --git a/docs/doctoring/agent-mention-concurrency-isolation.md b/docs/doctoring/agent-mention-concurrency-isolation.md index 163a5bc850..266af2c4ca 100644 --- a/docs/doctoring/agent-mention-concurrency-isolation.md +++ b/docs/doctoring/agent-mention-concurrency-isolation.md @@ -16,7 +16,7 @@ Neither defect is evidence that the requesting maintainer, model, repository all The permanent regression contracts were committed before their corresponding production changes. - `tests/test_agent_mention_dispatch_payload_limit.py` requires both dispatch hops to stay at or below ten top-level payload properties and requires the router to reject an oversized payload before GitHub does. -- `tests/test_agent_mention_queue_isolation.py` requires the interactive route and scheduled sweep to use different job-level concurrency groups, with `queue: max` on the interactive route and no cancellation of in-progress interactive work. +- `tests/test_agent_mention_queue_isolation.py` requires the interactive route to have no replacing concurrency group while the scheduled sweep remains independently single-flight. ## Decision @@ -34,15 +34,15 @@ merge_mode=disabled The wrapper-to-scheduler payload carries exactly ten fields, including the three values that override unsafe scheduler defaults. The wrapper therefore remains review-only and cannot merge or update a branch. -### Isolated concurrency queues +### Non-replacing interactive routing -Concurrency is scoped to each job rather than the whole workflow: +The event-driven local route has no concurrency group, so each eligible +`issue_comment` event receives its own run. The scheduled sweep remains +single-flight and cannot cancel an interactive run: ```yaml route-local-agent-mention: - concurrency: - group: review-agent-mention-router-local-${{ github.repository }} - queue: max + runs-on: ubuntu-24.04 sweep-organization-agent-mentions: concurrency: @@ -50,7 +50,13 @@ sweep-organization-agent-mentions: cancel-in-progress: false ``` -GitHub documents that `queue: max` permits up to 100 pending jobs or workflow runs in one concurrency group and cannot be combined with `cancel-in-progress: true`. The interactive queue therefore retains bounded pending requests instead of replacing the previous pending request. Scheduled sweeps retain coalescing behavior in a separate group and cannot displace interactive work. +GitHub's supported concurrency contract permits at most one running and one +pending member per group, and a newly queued member replaces an older pending +member. The unsupported `queue: max` key cannot provide durability. Omitting a +local concurrency group is therefore the smallest supported boundary that does +not discard a trusted mention before its durable invocation claim exists. +Scheduled sweeps still coalesce in their separate group and cannot displace +interactive work. Concurrency is not the idempotency authority. Duplicate forwarding remains governed by the complete canonical invocation key, exact-key downstream concurrency, and the immutable exact-name Actions artifact ledger. @@ -58,7 +64,7 @@ Concurrency is not the idempotency authority. Duplicate forwarding remains gover - No model provider, reviewer identity, repository allowlist, token name, credential scope, or branch-protection rule changes. - `COPILOT_GITHUB_TOKEN` remains unused. -- Workflow-default permissions remain read-only; existing bounded jobs keep only their required writes. +- Workflow-default permissions remain read-only; the local router has read-only Actions access and keeps only the content/comment writes required for dispatch and acknowledgement. - Only trusted non-bot `OWNER`, `MEMBER`, or `COLLABORATOR` comments on open pull requests are eligible. - Pull request number, exact head and base SHAs, base branch, source comment, requested agent, and requesting actor remain bound to the invocation key. - Mention routing remains unable to approve, merge, update branches, publish, or release. @@ -81,7 +87,7 @@ Do not restore either defective boundary: - do not increase the first- or second-hop payload beyond GitHub's limit; - do not move local and scheduled work back into one workflow-level concurrency group; -- do not replace `queue: max` with the default single-pending interactive queue unless another independently reviewed durable queue preserves every eligible request. +- do not add a local concurrency group whose default single-pending contract can replace an eligible interactive request before the durable claim exists. A safe emergency degradation may suspend the scheduled sweep while retaining the isolated interactive route. diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index eaa17f953f..cc65570c8c 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -11,12 +11,12 @@ UPLOAD_ARTIFACT_SHA = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" -def test_router_can_dispatch_and_read_durable_central_artifacts() -> None: - """Local routing dispatches and sibling sweeping reads durable artifacts.""" +def test_router_uses_read_only_actions_access_for_durable_artifacts() -> None: + """Both local routing and sibling sweeping only read Actions artifacts.""" text = ROUTER_WORKFLOW.read_text(encoding="utf-8") local, sweep = text.split("\n sweep-organization-agent-mentions:\n", 1) - assert "permissions:\n actions: write" in local + assert "permissions:\n actions: read" in local assert "permissions:\n actions: read" in sweep assert "AGENT_DISPATCH_TOKEN: ${{ github.token }}" in local assert "AGENT_DISPATCH_TOKEN: ${{ github.token }}" in sweep diff --git a/tests/test_agent_mention_queue_isolation.py b/tests/test_agent_mention_queue_isolation.py index c92e0e3456..15f1ae4ea2 100644 --- a/tests/test_agent_mention_queue_isolation.py +++ b/tests/test_agent_mention_queue_isolation.py @@ -22,13 +22,15 @@ def _job_block(workflow: str, job_name: str, next_job_name: str | None) -> str: def _concurrency_block(job: str) -> str: """Return the job-scoped concurrency mapping before ``runs-on``.""" + if " concurrency:\n" not in job: + return "" start = job.index(" concurrency:\n") end = job.index("\n runs-on:", start) return job[start:end] -def test_interactive_mentions_and_sweeps_use_independent_queues() -> None: - """A scheduled sweep cannot replace a pending trusted mention request.""" +def test_interactive_mentions_run_without_a_replacing_concurrency_queue() -> None: + """Every trusted mention receives a run while sweeps stay single-flight.""" workflow = WORKFLOW.read_text(encoding="utf-8") header = workflow.split("\njobs:\n", 1)[0] @@ -44,10 +46,7 @@ def test_interactive_mentions_and_sweeps_use_independent_queues() -> None: ) assert not any(line.startswith("concurrency:") for line in header.splitlines()) - assert _concurrency_block(local_job) == ( - " concurrency:\n" - " group: review-agent-mention-router-local-${{ github.repository }}" - ) + assert _concurrency_block(local_job) == "" assert _concurrency_block(sweep_job) == ( " concurrency:\n" " group: review-agent-mention-router-sweep-${{ github.repository }}\n" @@ -55,8 +54,8 @@ def test_interactive_mentions_and_sweeps_use_independent_queues() -> None: ) -def test_interactive_queue_retains_pending_requests_without_cancellation() -> None: - """The bounded interactive queue retains work and never cancels in progress.""" +def test_interactive_route_has_no_unsupported_or_replacing_queue_controls() -> None: + """Interactive work is neither invalid YAML nor a replaceable pending run.""" workflow = WORKFLOW.read_text(encoding="utf-8") local_job = _job_block( @@ -64,7 +63,6 @@ def test_interactive_queue_retains_pending_requests_without_cancellation() -> No "route-local-agent-mention", "sweep-organization-agent-mentions", ) - concurrency = _concurrency_block(local_job) - - assert "queue: max" not in concurrency - assert "cancel-in-progress: true" not in concurrency + assert _concurrency_block(local_job) == "" + assert "queue: max" not in local_job + assert "cancel-in-progress" not in local_job diff --git a/tests/test_agent_mention_workflow_contract.py b/tests/test_agent_mention_workflow_contract.py index 396472bcff..c5fc4cae54 100644 --- a/tests/test_agent_mention_workflow_contract.py +++ b/tests/test_agent_mention_workflow_contract.py @@ -29,7 +29,7 @@ def test_workflow_uses_local_event_and_central_sweep_with_job_scoped_writes() -> assert "route-local-agent-mention:" in local assert "github.repository == 'ContextualWisdomLab/.github'" in local for permission in ( - "actions: write", + "actions: read", "contents: write", "issues: write", "pull-requests: read", From a7aeb56804c043b97f837a82d9066dfc03eec938 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:02:02 +0900 Subject: [PATCH 006/154] fix(workflows): remove unsupported concurrency queue --- .github/workflows/agent-mention-noema-dispatch.yml | 1 - .github/workflows/agent-mention-opencode-dispatch.yml | 1 - tests/test_agent_mention_downstream_idempotency.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/agent-mention-noema-dispatch.yml b/.github/workflows/agent-mention-noema-dispatch.yml index 4912e5addc..8b09f9b473 100644 --- a/.github/workflows/agent-mention-noema-dispatch.yml +++ b/.github/workflows/agent-mention-noema-dispatch.yml @@ -11,7 +11,6 @@ on: concurrency: group: agent-mention-noema-${{ github.event.client_payload.agent_invocation_key || github.run_id }} cancel-in-progress: false - queue: max permissions: contents: read diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 02a3f6f082..7a8cfadd69 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -11,7 +11,6 @@ on: concurrency: group: agent-mention-opencode-${{ github.event.client_payload.agent_invocation_key || github.run_id }} cancel-in-progress: false - queue: max permissions: contents: read diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index cc65570c8c..31095eba45 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -33,7 +33,7 @@ def test_downstream_workflows_claim_artifacts_and_bind_exact_key() -> None: assert "source_comment_id" in text assert "requested_agent" in text assert "cancel-in-progress: false" in text - assert "queue: max" in text + assert "queue: max" not in text assert "cancel-in-progress: true" not in text assert "^[0-9a-f]{64}$" in text assert "^[1-9][0-9]*$" in text From 158f090fbe2c659b3217cc8ec74714b5416903c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:41:24 +0900 Subject: [PATCH 007/154] fix(governance): enforce exact central ref scope --- CHANGELOG.md | 3 +++ README.md | 11 ++++++----- ...001-central-review-stacked-pull-requests.md | 6 +++--- docs/org-required-workflow-rollout.md | 2 +- scripts/ci/audit_central_required_workflows.py | 2 +- ..._central_required_workflow_ruleset_audit.py | 18 ++++++++++++++++++ 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa46462882..f72ce57211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Tightened the central ruleset auditor to require the exact `ref_name` shape + (`include=["~ALL"]`, `exclude=[]`) and aligned operator documentation with + the three repository exclusions. - Keep local agent-mention Actions access read-only and remove its replacing concurrency group together with the unsupported `concurrency.queue` key, so every eligible exact-head mention can enqueue while scheduled sweeps remain independently single-flight. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). diff --git a/README.md b/README.md index 3e42071baa..1f5ffdc5cb 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ that means: | Mode | What happens | | --- | --- | | **따로 (this repo alone)** | Clone, test, and operate `.github` as the org profile and workflow source. Local quality gates, Cloudflare dry-run, and this repository's own PRs do not depend on naruon or any sibling product checkout. | -| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in that sibling's repository context, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | +| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]` with exclusions `.github`, `IRT-bibliography-set`, and `noema`, plus `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in that sibling's repository context, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | Do not copy Strix, OpenCode, Noema, or scheduler workflow files into a sibling to "satisfy CI." Thick downstream sync PRs are an anti-pattern @@ -65,10 +65,11 @@ The last checked-in ruleset ledger is Checked-in operator facts: -- Ruleset `18156473` is **active**. It targets every repository branch - (`repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`), including - stacked pull-request base branches, and sources workflows from this - repository at `refs/heads/main`. +- Ruleset `18156473` is **active**. It targets every non-excluded repository + branch (`repository_name.include=["~ALL"]`, exclusions `.github`, + `IRT-bibliography-set`, and `noema`, and `ref_name.include=["~ALL"]`), + including stacked pull-request base branches, and sources workflows from + this repository at `refs/heads/main`. - Active required workflow paths: `close-empty-pr.yml`, `noema-review.yml`, `opencode-review.yml`, `pr-review-merge-scheduler.yml`, `security-scan.yml`, `strix.yml`, and `sast-semgrep.yml`. diff --git a/docs/adr/0001-central-review-stacked-pull-requests.md b/docs/adr/0001-central-review-stacked-pull-requests.md index 47476b0f92..cb59cc384c 100644 --- a/docs/adr/0001-central-review-stacked-pull-requests.md +++ b/docs/adr/0001-central-review-stacked-pull-requests.md @@ -31,6 +31,6 @@ security evidence used for main-targeting PRs. ## Verification -The exact live ruleset was read before and after the change. TEPP PRs #158 and -#159 were re-read at their current heads before review-only dispatch. Hosted -Checks remain authoritative for merge decisions. +The exact live ruleset was read before and after the change. TEPP pull requests +`#158` and `#159` were re-read at their current heads before review-only +dispatch. Hosted Checks remain authoritative for merge decisions. diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index d217b00176..95be025043 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -9,7 +9,7 @@ Use an organization repository ruleset instead of copying workflow files into ea - Ruleset: `CWL Central required workflows` - Ruleset ID: `18156473` - Enforcement: `active` -- Target: branch rules on every repository branch (`repository_name.include=["~ALL"]`, `ref_name.include=["~ALL"]`), including stacked pull-request base branches +- Target: branch rules on every non-excluded repository branch (`repository_name.include=["~ALL"]`, `repository_name.exclude=[".github", "IRT-bibliography-set", "noema"]`, `ref_name.include=["~ALL"]`, `ref_name.exclude=[]`), including stacked pull-request base branches - Required workflow source repository: `ContextualWisdomLab/.github` - Required workflow source repository ID: `1274066402` - Active required workflow paths: diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 8ad0e3b2f5..43979f3b9e 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -112,7 +112,7 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: ref_names = conditions.get("ref_name") ref_names = ref_names if isinstance(ref_names, dict) else {} - if "~ALL" not in (ref_names.get("include") or []): + if ref_names.get("include") != ["~ALL"] or ref_names.get("exclude") != []: errors.append("central ruleset does not target stacked and default-branch PRs") workflow_rules = _typed_rules(payload, "workflows") diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index fd82491165..842fd9d73e 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -103,6 +103,24 @@ def test_default_branch_only_scope_rejects_stacked_pull_requests() -> None: ] +def test_ref_scope_rejects_branch_exclusions() -> None: + payload = ruleset_payload() + payload["conditions"]["ref_name"]["exclude"] = ["refs/heads/release/*"] + + assert audit.audit_ruleset(payload) == [ + "central ruleset does not target stacked and default-branch PRs" + ] + + +def test_ref_scope_rejects_string_include() -> None: + payload = ruleset_payload() + payload["conditions"]["ref_name"]["include"] = "~ALL" + + assert audit.audit_ruleset(payload) == [ + "central ruleset does not target stacked and default-branch PRs" + ] + + def test_inherited_scope_allows_private_exclusion_outside_token_visibility() -> None: payload = inherited_ruleset_payload() payload[audit.INHERITED_SCOPE_FIELD].pop("IRT-bibliography-set") From b873e7157c7f3fb345c1d9de347cc4295b8024d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:51:21 +0900 Subject: [PATCH 008/154] fix(router): use reviewer token for sibling acknowledgements --- .github/workflows/agent-mention-router.yml | 15 ++++++++++++--- CHANGELOG.md | 2 +- README.md | 2 +- .../agent-mention-concurrency-isolation.md | 4 +++- docs/org-required-workflow-rollout.md | 3 ++- organization_commercial_readiness_fixtures.py | 2 ++ .../ci/organization_commercial_readiness_loop.py | 4 +++- .../test_agent_mention_downstream_idempotency.py | 13 +++++++++++++ ...est_central_required_workflow_ruleset_audit.py | 3 +++ 9 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index 1640940f1d..8e74c7325f 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -58,9 +58,18 @@ jobs: "$SOURCE_EVENT_PATH" >"${RUNNER_TEMP}/agent-mention-event.json" - name: Route trusted local agent mention - run: >- - python3 scripts/ci/agent_mention_router.py - --event-path "${RUNNER_TEMP}/agent-mention-event.json" + env: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} + run: | + set -euo pipefail + if [ -n "$PR_REVIEW_MERGE_TOKEN" ]; then + export TARGET_REPOSITORY_TOKEN="$PR_REVIEW_MERGE_TOKEN" + elif [ -n "$OPENCODE_APPROVE_TOKEN" ]; then + export TARGET_REPOSITORY_TOKEN="$OPENCODE_APPROVE_TOKEN" + fi + python3 scripts/ci/agent_mention_router.py \ + --event-path "${RUNNER_TEMP}/agent-mention-event.json" sweep-organization-agent-mentions: if: >- diff --git a/CHANGELOG.md b/CHANGELOG.md index f72ce57211..29609cb596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ Semantic Versioning where the repository publishes a release. - Tightened the central ruleset auditor to require the exact `ref_name` shape (`include=["~ALL"]`, `exclude=[]`) and aligned operator documentation with the three repository exclusions. -- Keep local agent-mention Actions access read-only and remove its replacing concurrency group together with the unsupported `concurrency.queue` key, so every eligible exact-head mention can enqueue while scheduled sweeps remain independently single-flight. +- Keep local agent-mention Actions access read-only, use the established reviewer tokens for sibling-repository acknowledgements, and remove the replacing concurrency group together with the unsupported `concurrency.queue` key, so every eligible exact-head mention can enqueue while scheduled sweeps remain independently single-flight. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). diff --git a/README.md b/README.md index 1f5ffdc5cb..4eb3ebedbd 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ that means: | Mode | What happens | | --- | --- | | **따로 (this repo alone)** | Clone, test, and operate `.github` as the org profile and workflow source. Local quality gates, Cloudflare dry-run, and this repository's own PRs do not depend on naruon or any sibling product checkout. | -| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]` with exclusions `.github`, `IRT-bibliography-set`, and `noema`, plus `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in that sibling's repository context, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | +| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]` with `.github`, `IRT-bibliography-set`, and `noema` excluded, plus `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in every non-excluded sibling repository branch, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | Do not copy Strix, OpenCode, Noema, or scheduler workflow files into a sibling to "satisfy CI." Thick downstream sync PRs are an anti-pattern diff --git a/docs/doctoring/agent-mention-concurrency-isolation.md b/docs/doctoring/agent-mention-concurrency-isolation.md index 266af2c4ca..402bf685b0 100644 --- a/docs/doctoring/agent-mention-concurrency-isolation.md +++ b/docs/doctoring/agent-mention-concurrency-isolation.md @@ -65,6 +65,7 @@ Concurrency is not the idempotency authority. Duplicate forwarding remains gover - No model provider, reviewer identity, repository allowlist, token name, credential scope, or branch-protection rule changes. - `COPILOT_GITHUB_TOKEN` remains unused. - Workflow-default permissions remain read-only; the local router has read-only Actions access and keeps only the content/comment writes required for dispatch and acknowledgement. +- Local sibling-repository acknowledgement writes prefer the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` reviewer credentials; `github.token` remains the central repository-dispatch fallback and no new credential name is introduced. - Only trusted non-bot `OWNER`, `MEMBER`, or `COLLABORATOR` comments on open pull requests are eligible. - Pull request number, exact head and base SHAs, base branch, source comment, requested agent, and requesting actor remain bound to the invocation key. - Mention routing remains unable to approve, merge, update branches, publish, or release. @@ -77,7 +78,8 @@ After protected integration: 2. require the hidden receipt marker, acknowledgement comment, or durable exact-name artifact for the source comment; 3. require the trusted OpenCode wrapper and review-only scheduler dispatch to start for the same repository, pull request, and exact head; 4. verify that a scheduled sweep cannot cancel or replace the interactive route; -5. distinguish downstream provider or review failure from routing failure rather than treating every missing verdict as the same incident. +5. verify the local route uses an established cross-repository reviewer token for target acknowledgement when configured; +6. distinguish downstream provider or review failure from routing failure rather than treating every missing verdict as the same incident. A receipt proves routing and durable claim processing. It is not an approval and never substitutes for exact-head checks or branch protection. diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 95be025043..ca1c5275a0 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -191,7 +191,8 @@ SARIF/dependency evidence, test evidence, and review marker all bind to The active ruleset no longer maintains a repository-name allowlist. Live ruleset inspection on 2026-07-02 18:15 KST reports -`repository_name.include=["~ALL"]`, so all current and future organization +`repository_name.include=["~ALL"]` with `.github`, `IRT-bibliography-set`, and +`noema` excluded, so all current and future non-excluded organization repositories inherit the seven central required workflows on their default branch unless a later ruleset exclusion is added. The table below is the public non-fork inventory snapshot and rollout ledger, not the ruleset target list. diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index d865961962..0ba0e0a01d 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -90,6 +90,8 @@ def __init__( repositories: list[dict[str, Any]], snapshots: dict[str, list[RepositorySnapshot | Exception]], ) -> None: + """Initialize a deterministic client with queued snapshot outcomes.""" + self.repositories = repositories self.snapshots = snapshots self.dispatched_repairs: list[tuple[str, str]] = [] diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0a..8d11096c34 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,8 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize the transport with one required scheduler credential.""" + if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +855,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index 31095eba45..f417b3ff36 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -22,6 +22,19 @@ def test_router_uses_read_only_actions_access_for_durable_artifacts() -> None: assert "AGENT_DISPATCH_TOKEN: ${{ github.token }}" in sweep +def test_local_router_prefers_existing_cross_repository_review_tokens() -> None: + """Sibling acknowledgements use the existing reviewer token scheme.""" + + text = ROUTER_WORKFLOW.read_text(encoding="utf-8") + local = text.split("\n sweep-organization-agent-mentions:\n", 1)[0] + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in local + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in local + assert 'if [ -n "$PR_REVIEW_MERGE_TOKEN" ]; then' in local + assert 'elif [ -n "$OPENCODE_APPROVE_TOKEN" ]; then' in local + assert 'export TARGET_REPOSITORY_TOKEN="$PR_REVIEW_MERGE_TOKEN"' in local + assert 'export TARGET_REPOSITORY_TOKEN="$OPENCODE_APPROVE_TOKEN"' in local + + def test_downstream_workflows_claim_artifacts_and_bind_exact_key() -> None: """Exact-key concurrency serializes claims before authoritative forwarding.""" diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 842fd9d73e..6ae8b8a46b 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -104,6 +104,8 @@ def test_default_branch_only_scope_rejects_stacked_pull_requests() -> None: def test_ref_scope_rejects_branch_exclusions() -> None: + """All branch refs must be included without exclusions.""" + payload = ruleset_payload() payload["conditions"]["ref_name"]["exclude"] = ["refs/heads/release/*"] @@ -113,6 +115,7 @@ def test_ref_scope_rejects_branch_exclusions() -> None: def test_ref_scope_rejects_string_include() -> None: + """The ruleset API contract requires an exact include list.""" payload = ruleset_payload() payload["conditions"]["ref_name"]["include"] = "~ALL" From a56bf7fb5078497d47fe65113be3ed070f65669c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:33:07 +0900 Subject: [PATCH 009/154] fix(strix): classify caido sandbox startup failure --- .github/workflows/strix.yml | 5 +-- CHANGELOG.md | 1 + .../strix-nvidia-nim-not-found-fallback.md | 5 +++ ...est_strix_nvidia_nim_not_found_fallback.py | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 4155c73461..b362dc94f6 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -844,7 +844,8 @@ jobs: # exit 1 both for genuine blocking vulnerabilities AND for # LLM-backend-unavailable outcomes (GitHub Models "Too many requests" # rate limits, OpenAI quota starvation, 413 tokens_limit_reached - # token-cap, connection/warm-up failures) that could not complete a scan. A backend outage is CI + # token-cap, connection/warm-up failures, and the local Caido guest-login + # sandbox startup failure) that could not complete a scan. A backend outage is CI # infrastructure noise, not a security finding, so it must not fail # the required check and block merges. strix_run_log="$RUNNER_TEMP/strix_gate_console.log" @@ -866,7 +867,7 @@ jobs: fi # Recognized signals that the LLM backend was unavailable / starved. - backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404' + backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|loginAsGuest failed after [0-9]+ attempts:[^[:cntrl:]]*curl exit 7:[^[:cntrl:]]*Failed to connect to 127\.0\.0\.1 port 48080' # Any evidence that a vulnerability was actually reported. Its presence # forces a hard failure so real findings are NEVER downgraded. Keep the # severity branch anchored away from identifiers so environment lines diff --git a/CHANGELOG.md b/CHANGELOG.md index 29609cb596..c4a94b04c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Semantic Versioning where the repository publishes a release. ### Changed +- Classified the known Caido guest-login sandbox startup failure on `127.0.0.1:48080` as bounded Strix infrastructure evidence, while retaining fail-closed behavior for arbitrary connection failures and all vulnerability signals. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. diff --git a/docs/doctoring/strix-nvidia-nim-not-found-fallback.md b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md index 70299ebdf7..8842a017f9 100644 --- a/docs/doctoring/strix-nvidia-nim-not-found-fallback.md +++ b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md @@ -35,6 +35,11 @@ when the run log contains no vulnerability signal. Any reported severity or non-zero vulnerability count remains blocking. Scanner reports and attempt logs remain available as artifacts. +The same bounded infrastructure classification covers the known Caido guest-login +sandbox startup failure (`loginAsGuest` followed by `curl exit 7` to the local +`127.0.0.1:48080` service). It does not cover arbitrary loopback or curl failures, +and a vulnerability signal still fails the check. + ## Verification contract Regression evidence proves that: diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index a48f3092d2..0c38dcdef6 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -233,6 +233,38 @@ def test_outer_workflow_rejects_nvidia_404_without_litellm_context(self) -> None ) ) + def test_outer_workflow_neutralizes_caido_guest_login_sandbox_failure(self) -> None: + """Neutralize only the known local Caido guest-login startup failure.""" + + self.assertTrue( + _workflow_neutralizes( + "loginAsGuest failed after 10 attempts: curl exit 7: " + "Failed to connect to 127.0.0.1 port 48080\n" + "Vulnerabilities 0\n" + ) + ) + + def test_outer_workflow_does_not_neutralize_caido_failure_with_findings(self) -> None: + """Keep vulnerability evidence blocking despite a Caido startup failure.""" + + self.assertFalse( + _workflow_neutralizes( + "loginAsGuest failed after 10 attempts: curl exit 7: " + "Failed to connect to 127.0.0.1 port 48080\n" + "Severity: HIGH\nVulnerabilities 1\n" + ) + ) + + def test_outer_workflow_does_not_neutralize_arbitrary_local_connection_failure(self) -> None: + """Do not broaden the exception to arbitrary curl or loopback failures.""" + + self.assertFalse( + _workflow_neutralizes( + "curl exit 7: Failed to connect to 127.0.0.1 port 48081\n" + "Vulnerabilities 0\n" + ) + ) + def test_outer_workflow_never_neutralizes_reported_vulnerabilities(self) -> None: """Keep a real vulnerability signal blocking despite provider failure.""" From 33b85a8cf48d5b6e0880d5071b360ffa46f83457 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:45:52 +0900 Subject: [PATCH 010/154] fix(strix): require trusted caido traceback marker --- .github/workflows/strix.yml | 2 +- CHANGELOG.md | 2 +- .../strix-nvidia-nim-not-found-fallback.md | 7 ++++--- tests/test_strix_nvidia_nim_not_found_fallback.py | 15 +++++++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index b362dc94f6..24f0f98b5f 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -867,7 +867,7 @@ jobs: fi # Recognized signals that the LLM backend was unavailable / starved. - backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|loginAsGuest failed after [0-9]+ attempts:[^[:cntrl:]]*curl exit 7:[^[:cntrl:]]*Failed to connect to 127\.0\.0\.1 port 48080' + backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|RuntimeError:[[:space:]]*loginAsGuest failed after [0-9]+ attempts:[^[:cntrl:]]*curl exit 7:[^[:cntrl:]]*Failed to connect to 127\.0\.0\.1 port 48080' # Any evidence that a vulnerability was actually reported. Its presence # forces a hard failure so real findings are NEVER downgraded. Keep the # severity branch anchored away from identifiers so environment lines diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a94b04c1..de4d4c5060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Semantic Versioning where the repository publishes a release. ### Changed -- Classified the known Caido guest-login sandbox startup failure on `127.0.0.1:48080` as bounded Strix infrastructure evidence, while retaining fail-closed behavior for arbitrary connection failures and all vulnerability signals. +- Classified only the known Caido `RuntimeError: loginAsGuest` sandbox startup failure on `127.0.0.1:48080` as bounded Strix infrastructure evidence, while retaining fail-closed behavior for arbitrary connection failures, repository-output text, and all vulnerability signals. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. diff --git a/docs/doctoring/strix-nvidia-nim-not-found-fallback.md b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md index 8842a017f9..761883b0de 100644 --- a/docs/doctoring/strix-nvidia-nim-not-found-fallback.md +++ b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md @@ -36,9 +36,10 @@ non-zero vulnerability count remains blocking. Scanner reports and attempt logs remain available as artifacts. The same bounded infrastructure classification covers the known Caido guest-login -sandbox startup failure (`loginAsGuest` followed by `curl exit 7` to the local -`127.0.0.1:48080` service). It does not cover arbitrary loopback or curl failures, -and a vulnerability signal still fails the check. +sandbox startup failure when the runtime traceback contains `RuntimeError: +loginAsGuest`, `curl exit 7`, and the local `127.0.0.1:48080` service. It does not +cover arbitrary loopback, curl, or repository-output text, and a vulnerability +signal still fails the check. ## Verification contract diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index 0c38dcdef6..8ceae5d190 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -238,7 +238,7 @@ def test_outer_workflow_neutralizes_caido_guest_login_sandbox_failure(self) -> N self.assertTrue( _workflow_neutralizes( - "loginAsGuest failed after 10 attempts: curl exit 7: " + "RuntimeError: loginAsGuest failed after 10 attempts: curl exit 7: " "Failed to connect to 127.0.0.1 port 48080\n" "Vulnerabilities 0\n" ) @@ -249,12 +249,23 @@ def test_outer_workflow_does_not_neutralize_caido_failure_with_findings(self) -> self.assertFalse( _workflow_neutralizes( - "loginAsGuest failed after 10 attempts: curl exit 7: " + "RuntimeError: loginAsGuest failed after 10 attempts: curl exit 7: " "Failed to connect to 127.0.0.1 port 48080\n" "Severity: HIGH\nVulnerabilities 1\n" ) ) + def test_outer_workflow_rejects_untrusted_caido_like_text_without_traceback(self) -> None: + """Require the trusted runtime-error marker before classifying Caido output.""" + + self.assertFalse( + _workflow_neutralizes( + "loginAsGuest failed after 10 attempts: curl exit 7: " + "Failed to connect to 127.0.0.1 port 48080\n" + "Vulnerabilities 0\n" + ) + ) + def test_outer_workflow_does_not_neutralize_arbitrary_local_connection_failure(self) -> None: """Do not broaden the exception to arbitrary curl or loopback failures.""" From b628e88871ecc845a46989bde48b15292a41690e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:18:59 -0700 Subject: [PATCH 011/154] fix(governance): preserve proposal branch create transition --- CHANGELOG.md | 8 ++-- README.md | 12 +++--- ...01-central-review-stacked-pull-requests.md | 26 ++++++----- docs/org-required-workflow-rollout.md | 25 ++++++----- .../ci/audit_central_required_workflows.py | 18 +++++--- ...central_required_workflow_ruleset_audit.py | 43 +++++++++++++------ 6 files changed, 82 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eed7f6ace4..201343d210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,11 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Tightened the central ruleset auditor to require the exact `ref_name` shape - (`include=["~ALL"]`, `exclude=[]`) and aligned operator documentation with - the three repository exclusions. +- Tightened the central ruleset auditor to require exact default-branch scope + (`include=["~DEFAULT_BRANCH"]`, `exclude=[]`) and non-blocking create + transitions, rejecting `~ALL` and every extra proposal-ref target after live + 409/422 canaries proved the broader combined ruleset deadlocked normal branch + creation and updates. - Keep local agent-mention Actions access read-only, use the established reviewer tokens for sibling-repository acknowledgements, and remove the replacing concurrency group together with the unsupported `concurrency.queue` key, so every eligible exact-head mention can enqueue while scheduled sweeps remain independently single-flight. - Refused PR Review Merge Scheduler head mutations, `update-branch` and the last-push approval head restamp, whenever the resolved mutation credential is the workflow `GITHUB_TOKEN`. GitHub starts no workflow run for events created with that credential, so the moved head collected no current-head required checks and the PR stayed permanently `BLOCKED` with a `github-actions[bot]` merge commit that no later scheduler run could repair, because the branch was no longer behind. The scheduler now waits with `head_mutation_credential_upgrade` guidance naming `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, and the OpenCode app token exchange. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. diff --git a/README.md b/README.md index 4eb3ebedbd..602f582b7f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ that means: | Mode | What happens | | --- | --- | | **따로 (this repo alone)** | Clone, test, and operate `.github` as the org profile and workflow source. Local quality gates, Cloudflare dry-run, and this repository's own PRs do not depend on naruon or any sibling product checkout. | -| **또 같이 (siblings call it)** | A sibling enables the org required-workflow ruleset (already `repository_name.include=["~ALL"]` with `.github`, `IRT-bibliography-set`, and `noema` excluded, plus `ref_name.include=["~ALL"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` in every non-excluded sibling repository branch, including stacked PR base branches. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | +| **또 같이 (siblings call it)** | A sibling inherits the org required-workflow ruleset (`repository_name.include=["~ALL"]` with `.github`, `IRT-bibliography-set`, and `noema` excluded; `ref_name.include=["~DEFAULT_BRANCH"]`). GitHub runs the trusted workflows from `ContextualWisdomLab/.github@main` at the default-branch integration boundary. The scheduler provides review-only evidence for stacked PRs without governing proposal refs into a create/update deadlock. Optional reusable callers (`deploy-pages.yml`, `pr-review-fix-scheduler.yml`) are `workflow_call` entry points, not files to copy. | Do not copy Strix, OpenCode, Noema, or scheduler workflow files into a sibling to "satisfy CI." Thick downstream sync PRs are an anti-pattern @@ -65,11 +65,11 @@ The last checked-in ruleset ledger is Checked-in operator facts: -- Ruleset `18156473` is **active**. It targets every non-excluded repository - branch (`repository_name.include=["~ALL"]`, exclusions `.github`, - `IRT-bibliography-set`, and `noema`, and `ref_name.include=["~ALL"]`), - including stacked pull-request base branches, and sources workflows from - this repository at `refs/heads/main`. +- Ruleset `18156473` is **active**. It targets the default branch of every + non-excluded repository (`repository_name.include=["~ALL"]`, exclusions + `.github`, `IRT-bibliography-set`, and `noema`, and + `ref_name.include=["~DEFAULT_BRANCH"]`), permits the create transition, and + sources workflows from this repository at `refs/heads/main`. - Active required workflow paths: `close-empty-pr.yml`, `noema-review.yml`, `opencode-review.yml`, `pr-review-merge-scheduler.yml`, `security-scan.yml`, `strix.yml`, and `sast-semgrep.yml`. diff --git a/docs/adr/0001-central-review-stacked-pull-requests.md b/docs/adr/0001-central-review-stacked-pull-requests.md index cb59cc384c..5123d7810d 100644 --- a/docs/adr/0001-central-review-stacked-pull-requests.md +++ b/docs/adr/0001-central-review-stacked-pull-requests.md @@ -8,22 +8,28 @@ ## Decision The organization ruleset `CWL Central required workflows` (`18156473`) applies -to every branch reference (`ref_name.include=["~ALL"]`) in inherited -repositories. Central OpenCode, Noema, security, and scheduler workflows stay -owned by `ContextualWisdomLab/.github` at `refs/heads/main`. +exactly to each inherited repository's default branch +(`ref_name.include=["~DEFAULT_BRANCH"]`, `ref_name.exclude=[]`). Its workflows +use `do_not_enforce_on_create=true`. Central OpenCode, Noema, security, and +scheduler workflows stay owned by `ContextualWisdomLab/.github` at +`refs/heads/main`. ## Context -Stacked PRs target another feature branch, so a default-branch-only ruleset did -not materialize the central required workflow entrypoints. This left buyer- -visible changes with local checks but without the same independent review and -security evidence used for main-targeting PRs. +Stacked PRs target another feature branch, so the default-branch ruleset does +not materialize required-workflow entrypoints for that intermediate PR. The +central scheduler supplies exact-head review-only evidence for the stacked +phase. Applying the combined workflow, pull-request, deletion, and +non-fast-forward rules to every ref was rejected after live 409/422 canaries +proved that it made normal proposal-branch creation and updates impossible. ## Consequences -- Every stacked PR receives the same current-head governance entrypoints. -- The scheduler may dispatch review-only work for non-default base branches; - merge automation remains guarded by the PR's actual policy and checks. +- Every stacked PR receives current-head central review-only dispatch. +- The scheduler does not update or merge non-default-base PRs; the final + default-branch integration PR receives the full required-workflow gate. +- Proposal refs can be created and updated without a circular requirement for + checks that cannot exist before the ref exists. - Branch-scope drift is detected by `scripts/ci/audit_central_required_workflows.py` and its regression tests. - No workflow is copied into a product repository, preserving the MSA control diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index ca1c5275a0..b66a5cc292 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -9,7 +9,8 @@ Use an organization repository ruleset instead of copying workflow files into ea - Ruleset: `CWL Central required workflows` - Ruleset ID: `18156473` - Enforcement: `active` -- Target: branch rules on every non-excluded repository branch (`repository_name.include=["~ALL"]`, `repository_name.exclude=[".github", "IRT-bibliography-set", "noema"]`, `ref_name.include=["~ALL"]`, `ref_name.exclude=[]`), including stacked pull-request base branches +- Target: the default branch of every non-excluded repository (`repository_name.include=["~ALL"]`, `repository_name.exclude=[".github", "IRT-bibliography-set", "noema"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, `ref_name.exclude=[]`) +- Create transition: required workflows use `do_not_enforce_on_create=true`; proposal branches do not match this strict ruleset, and a new repository can establish its first default-branch commit before subsequent changes are governed - Required workflow source repository: `ContextualWisdomLab/.github` - Required workflow source repository ID: `1274066402` - Active required workflow paths: @@ -33,16 +34,18 @@ This keeps Strix security evidence, OpenCode and independent Noema review eviden ### Stacked pull-request coverage -On 2026-08-20, live PRs #158 and #159 in `ContextualWisdomLab/TEPP` targeted -`feat/lineageweave-live-consumer-contract` rather than `main`. They had product -checks but no centrally materialized OpenCode or Noema workflow runs because -the ruleset was scoped to `~DEFAULT_BRANCH`. Ruleset `18156473` now uses -`ref_name.include=["~ALL"]`; the existing scheduler already enumerates open -PRs across base branches and dispatches exact-head review-only work for -stacked PRs. The audit script and regression test enforce this scope so a -future ruleset rollback fails closed. The live repair also restored the -checked-in two-approval contract; workflow source, repository exclusions, and -stale-review/thread-resolution/last-push protections were preserved. +On 2026-08-20, live PRs #158 and #159 in `ContextualWisdomLab/TEPP` showed that +stacked PRs need centrally materialized OpenCode and Noema review evidence. +Expanding the combined required-workflow and pull-request ruleset to `~ALL` +was later falsified by independent 409/422 canaries: proposal branches could +not be created or updated because GitHub required a PR and required-workflow +evidence before the proposal ref could exist. Ruleset `18156473` therefore +stays exact-default-branch. The central scheduler enumerates non-default-base +PRs and dispatches exact-head review-only work; merge and branch mutation stay +disabled for that stacked phase. The final integration PR to the default +branch remains subject to the full required-workflow, two-approval, +stale-review, thread-resolution, last-push, deletion, and non-fast-forward +contract. ## OpenCode required workflow posture diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 43979f3b9e..af5a33b0d4 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -5,11 +5,10 @@ import argparse import json -from pathlib import Path import sys +from pathlib import Path from typing import Any, TextIO - RULESET_ID = 18156473 RULESET_NAME = "CWL Central required workflows" SOURCE_REPOSITORY_ID = 1274066402 @@ -112,19 +111,26 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: ref_names = conditions.get("ref_name") ref_names = ref_names if isinstance(ref_names, dict) else {} - if ref_names.get("include") != ["~ALL"] or ref_names.get("exclude") != []: - errors.append("central ruleset does not target stacked and default-branch PRs") + if ( + ref_names.get("include") != ["~DEFAULT_BRANCH"] + or ref_names.get("exclude") != [] + ): + errors.append("central ruleset ref scope must be exactly the default branch") workflow_rules = _typed_rules(payload, "workflows") + workflow_parameters: dict[str, Any] = {} if len(workflow_rules) != 1: errors.append(f"expected one workflows rule, found {len(workflow_rules)}") workflows: list[Any] = [] else: parameters = workflow_rules[0].get("parameters") - parameters = parameters if isinstance(parameters, dict) else {} - workflows = parameters.get("workflows") + workflow_parameters = parameters if isinstance(parameters, dict) else {} + workflows = workflow_parameters.get("workflows") workflows = workflows if isinstance(workflows, list) else [] + if workflow_parameters.get("do_not_enforce_on_create") is not True: + errors.append("central required workflows block the branch create transition") + workflows_by_path: dict[str, list[dict[str, Any]]] = {} for workflow in workflows: if not isinstance(workflow, dict) or not isinstance(workflow.get("path"), str): diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 6ae8b8a46b..b4cd997c76 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -1,11 +1,10 @@ +import json from copy import deepcopy from io import StringIO -import json from pathlib import Path from scripts.ci import audit_central_required_workflows as audit - REPO_ROOT = Path(__file__).resolve().parents[1] def ruleset_payload() -> dict: """Return the expected live central required-workflow ruleset shape.""" @@ -28,13 +27,13 @@ def ruleset_payload() -> dict: "include": ["~ALL"], "exclude": ["noema", "IRT-bibliography-set", ".github"], }, - "ref_name": {"include": ["~ALL"], "exclude": []}, + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, }, "rules": [ { "type": "workflows", "parameters": { - "do_not_enforce_on_create": False, + "do_not_enforce_on_create": True, "workflows": [ { "repository_id": 1274066402, @@ -94,23 +93,28 @@ def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] -def test_default_branch_only_scope_rejects_stacked_pull_requests() -> None: - payload = ruleset_payload() - payload["conditions"]["ref_name"]["include"] = ["~DEFAULT_BRANCH"] +def test_ref_scope_rejects_all_branch_and_extra_proposal_branch_targets() -> None: + for include in ( + ["~ALL"], + ["~DEFAULT_BRANCH", "~ALL"], + ["~DEFAULT_BRANCH", "refs/heads/feature/*"], + ): + payload = ruleset_payload() + payload["conditions"]["ref_name"]["include"] = include - assert audit.audit_ruleset(payload) == [ - "central ruleset does not target stacked and default-branch PRs" - ] + assert audit.audit_ruleset(payload) == [ + "central ruleset ref scope must be exactly the default branch" + ] def test_ref_scope_rejects_branch_exclusions() -> None: - """All branch refs must be included without exclusions.""" + """The strict default-branch ruleset must not hide excluded refs.""" payload = ruleset_payload() payload["conditions"]["ref_name"]["exclude"] = ["refs/heads/release/*"] assert audit.audit_ruleset(payload) == [ - "central ruleset does not target stacked and default-branch PRs" + "central ruleset ref scope must be exactly the default branch" ] @@ -120,7 +124,17 @@ def test_ref_scope_rejects_string_include() -> None: payload["conditions"]["ref_name"]["include"] = "~ALL" assert audit.audit_ruleset(payload) == [ - "central ruleset does not target stacked and default-branch PRs" + "central ruleset ref scope must be exactly the default branch" + ] + + +def test_workflows_must_not_block_branch_create_transition() -> None: + payload = ruleset_payload() + workflow_rule = next(rule for rule in payload["rules"] if rule["type"] == "workflows") + workflow_rule["parameters"]["do_not_enforce_on_create"] = False + + assert audit.audit_ruleset(payload) == [ + "central required workflows block the branch create transition" ] @@ -234,8 +248,9 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: "central ruleset enforcement is not active", "central ruleset does not include all repositories", "central ruleset repository exclusions drifted: expected ['.github', 'IRT-bibliography-set', 'noema'], got []", - "central ruleset does not target stacked and default-branch PRs", + "central ruleset ref scope must be exactly the default branch", "expected one workflows rule, found 0", + "central required workflows block the branch create transition", "missing central required workflow .github/workflows/close-empty-pr.yml", "missing central required workflow .github/workflows/noema-review.yml", "missing central required workflow .github/workflows/opencode-review.yml", From 8923badcc65094b3119b40cfca44f751a4751612 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:32:52 +0900 Subject: [PATCH 012/154] fix(ci): refresh audit lock and scheduler assertion --- requirements-pip-audit-ci-hashes.txt | 6 +++--- scripts/ci/test_strix_quick_gate.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49a..0ae099d8fe 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8bd..bbe9aa8b21 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,8 +1506,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" - assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository)" "scheduler keeps manual queue scans isolated per repository" + assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number) }}" "scheduler cancels stale PR/review/manual queue scans and orphaned workflow runs without cancelling active workflow-run evidence" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews" assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads" From c6e1ba3cee48edd233e0c63b05d1f3785e307f98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:55:10 -0700 Subject: [PATCH 013/154] chore(governance): restore canonical pip lock ownership --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index 0ae099d8fe..ade197a49a 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.2.1 \ - --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ - --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ From 6bf0447388c08f699f9550e4ed4f71c84fb4c723 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:04:02 +0900 Subject: [PATCH 014/154] fix(governance): avoid misleading multi-rule drift --- scripts/ci/audit_central_required_workflows.py | 2 +- .../test_central_required_workflow_ruleset_audit.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index af5a33b0d4..02bc7a4553 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -128,7 +128,7 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: workflows = workflow_parameters.get("workflows") workflows = workflows if isinstance(workflows, list) else [] - if workflow_parameters.get("do_not_enforce_on_create") is not True: + if len(workflow_rules) == 1 and workflow_parameters.get("do_not_enforce_on_create") is not True: errors.append("central required workflows block the branch create transition") workflows_by_path: dict[str, list[dict[str, Any]]] = {} diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index b4cd997c76..74eedf9fa5 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -138,6 +138,17 @@ def test_workflows_must_not_block_branch_create_transition() -> None: ] +def test_multiple_workflow_rules_do_not_invent_create_transition_drift() -> None: + """Report structural multiplicity without attributing a missing flag to it.""" + payload = ruleset_payload() + payload["rules"].append(payload["rules"][0].copy()) + + errors = audit.audit_ruleset(payload) + + assert "expected one workflows rule, found 2" in errors + assert "central required workflows block the branch create transition" not in errors + + def test_inherited_scope_allows_private_exclusion_outside_token_visibility() -> None: payload = inherited_ruleset_payload() payload[audit.INHERITED_SCOPE_FIELD].pop("IRT-bibliography-set") @@ -250,7 +261,6 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: "central ruleset repository exclusions drifted: expected ['.github', 'IRT-bibliography-set', 'noema'], got []", "central ruleset ref scope must be exactly the default branch", "expected one workflows rule, found 0", - "central required workflows block the branch create transition", "missing central required workflow .github/workflows/close-empty-pr.yml", "missing central required workflow .github/workflows/noema-review.yml", "missing central required workflow .github/workflows/opencode-review.yml", From 5d641026076713af1d200b67a7a571e758348b6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:57:44 +0900 Subject: [PATCH 015/154] ci: refresh dependency and scheduler contracts --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49a..0ae099d8fe 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ From 4ae3c6184ae96b588e98ac1e66820a63bdc9ca24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:23:42 -0700 Subject: [PATCH 016/154] fix(governance): reject hidden ruleset drift --- .../ci/audit_central_required_workflows.py | 25 ++++++-- ...central_required_workflow_ruleset_audit.py | 57 ++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index f2ad2cd10c..a14c543cc4 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -137,12 +137,27 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: if len(workflow_rules) == 1 and workflow_parameters.get("do_not_enforce_on_create") is not True: errors.append("central required workflows block the branch create transition") + malformed_workflows = sum( + 1 + for workflow in workflows + if not isinstance(workflow, dict) or not isinstance(workflow.get("path"), str) + ) + if malformed_workflows: + suffix = "entry" if malformed_workflows == 1 else "entries" + errors.append( + f"central required workflows contain {malformed_workflows} malformed {suffix}" + ) + workflows_by_path: dict[str, list[dict[str, Any]]] = {} for workflow in workflows: if not isinstance(workflow, dict) or not isinstance(workflow.get("path"), str): continue workflows_by_path.setdefault(workflow["path"], []).append(workflow) + unexpected_workflows = sorted(set(workflows_by_path) - set(REQUIRED_WORKFLOW_PATHS)) + if unexpected_workflows: + errors.append(f"unexpected central required workflows: {unexpected_workflows}") + for path in REQUIRED_WORKFLOW_PATHS: matches = workflows_by_path.get(path, []) if not matches: @@ -176,8 +191,8 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: if parameters.get("required_review_thread_resolution") is not True: errors.append("review-thread resolution protection is disabled") allowed_methods = set(parameters.get("allowed_merge_methods") or []) - if not {"merge", "squash"}.issubset(allowed_methods): - errors.append("merge and squash are not both allowed merge methods") + if allowed_methods != {"merge", "squash"}: + errors.append("only merge and squash may be allowed merge methods") if not _typed_rules(payload, "deletion"): errors.append("default-branch deletion protection is missing") @@ -260,6 +275,8 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append("repository ruleset target is not branch") if payload.get("enforcement") != "active": errors.append("repository ruleset enforcement is not active") + if payload.get("bypass_actors", []) != []: + errors.append("repository ruleset must not configure bypass actors") conditions = payload.get("conditions") conditions = conditions if isinstance(conditions, dict) else {} @@ -283,8 +300,8 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: if parameters.get("required_review_thread_resolution") is not True: errors.append("repository ruleset review-thread resolution protection is disabled") allowed_methods = set(parameters.get("allowed_merge_methods") or []) - if not {"merge", "squash"}.issubset(allowed_methods): - errors.append("repository ruleset does not allow both merge and squash") + if allowed_methods != {"merge", "squash"}: + errors.append("repository ruleset must allow only merge and squash") if not _typed_rules(payload, "deletion"): errors.append("repository default-branch deletion protection is missing") diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index fcb0d29a19..d599f4e9d9 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -149,6 +149,34 @@ def test_expected_central_ruleset_passes(monkeypatch, capsys) -> None: ) +def test_central_ruleset_rejects_unexpected_and_malformed_workflows() -> None: + payload = ruleset_payload() + workflow_rule = next(rule for rule in payload["rules"] if rule["type"] == "workflows") + workflow_rule["parameters"]["workflows"].extend( + [ + { + "repository_id": 1274066402, + "path": ".github/workflows/unexpected.yml", + "ref": "refs/heads/main", + }, + {"repository_id": 1274066402, "path": 42, "ref": "refs/heads/main"}, + ] + ) + + errors = audit.audit_ruleset(payload) + + assert "unexpected central required workflows: ['.github/workflows/unexpected.yml']" in errors + assert "central required workflows contain 1 malformed entry" in errors + + +def test_central_ruleset_rejects_rebase_merge_method() -> None: + payload = ruleset_payload() + review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") + review_rule["parameters"]["allowed_merge_methods"].append("rebase") + + assert "only merge and squash may be allowed merge methods" in audit.audit_ruleset(payload) + + def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] @@ -175,6 +203,31 @@ def test_repository_ruleset_rejects_live_weakened_review_controls() -> None: ] +def test_repository_ruleset_rejects_rebase_merge_method() -> None: + payload = repository_ruleset_payload() + review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") + review_rule["parameters"]["allowed_merge_methods"].append("rebase") + + assert audit.audit_repository_ruleset(payload) == [ + "repository ruleset must allow only merge and squash", + ] + + +def test_repository_ruleset_rejects_bypass_actors() -> None: + payload = repository_ruleset_payload() + payload["bypass_actors"] = [ + { + "actor_id": None, + "actor_type": "OrganizationAdmin", + "bypass_mode": "always", + } + ] + + assert audit.audit_repository_ruleset(payload) == [ + "repository ruleset must not configure bypass actors", + ] + + def test_repository_ruleset_reports_structural_and_protection_drift() -> None: payload = { "id": 0, @@ -210,7 +263,7 @@ def test_repository_ruleset_rejects_malformed_review_parameters() -> None: "repository ruleset stale-review dismissal on push is disabled", "repository ruleset last-push approval protection is disabled", "repository ruleset review-thread resolution protection is disabled", - "repository ruleset does not allow both merge and squash", + "repository ruleset must allow only merge and squash", ] @@ -496,7 +549,7 @@ def test_audit_reports_malformed_duplicate_workflows_and_weak_review_parameters( assert "stale-review dismissal on push is disabled" in errors assert "last-push approval protection is disabled" in errors assert "review-thread resolution protection is disabled" in errors - assert "merge and squash are not both allowed merge methods" in errors + assert "only merge and squash may be allowed merge methods" in errors def test_audit_handles_malformed_rule_parameter_shapes() -> None: From d2224010a7eafa5ff23682521e10cbf09f94b5b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:28:50 -0700 Subject: [PATCH 017/154] fix(governance): audit organization bypass actors --- scripts/ci/audit_central_required_workflows.py | 2 ++ ...est_central_required_workflow_ruleset_audit.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index a14c543cc4..fc7f655732 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -61,6 +61,8 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: errors.append("central ruleset target is not branch") if payload.get("enforcement") != "active": errors.append("central ruleset enforcement is not active") + if payload.get("bypass_actors", []) != []: + errors.append("central ruleset must not configure bypass actors") conditions = payload.get("conditions") conditions = conditions if isinstance(conditions, dict) else {} diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index d599f4e9d9..b3d4d4afef 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -177,6 +177,21 @@ def test_central_ruleset_rejects_rebase_merge_method() -> None: assert "only merge and squash may be allowed merge methods" in audit.audit_ruleset(payload) +def test_central_ruleset_rejects_bypass_actors() -> None: + payload = ruleset_payload() + payload["bypass_actors"] = [ + { + "actor_id": None, + "actor_type": "OrganizationAdmin", + "bypass_mode": "always", + } + ] + + assert audit.audit_ruleset(payload) == [ + "central ruleset must not configure bypass actors", + ] + + def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] From 0b0a45b34aa135f10db82d2a81d685f2d5f64980 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:41:33 -0700 Subject: [PATCH 018/154] fix(governance): fail closed on missing bypass evidence --- .../ci/audit_central_required_workflows.py | 4 ++-- ...central_required_workflow_ruleset_audit.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index fc7f655732..0eeaf87c6a 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -61,7 +61,7 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: errors.append("central ruleset target is not branch") if payload.get("enforcement") != "active": errors.append("central ruleset enforcement is not active") - if payload.get("bypass_actors", []) != []: + if payload.get("bypass_actors") != []: errors.append("central ruleset must not configure bypass actors") conditions = payload.get("conditions") @@ -277,7 +277,7 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append("repository ruleset target is not branch") if payload.get("enforcement") != "active": errors.append("repository ruleset enforcement is not active") - if payload.get("bypass_actors", []) != []: + if payload.get("bypass_actors") != []: errors.append("repository ruleset must not configure bypass actors") conditions = payload.get("conditions") diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index b3d4d4afef..35b050eb88 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -24,6 +24,7 @@ def ruleset_payload() -> dict: "name": "CWL Central required workflows", "target": "branch", "enforcement": "active", + "bypass_actors": [], "conditions": { "repository_name": { "include": ["~ALL"], @@ -119,6 +120,7 @@ def repository_ruleset_payload() -> dict: "source_type": "Repository", "source": "ContextualWisdomLab/.github", "enforcement": "active", + "bypass_actors": [], "conditions": { "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, }, @@ -192,6 +194,15 @@ def test_central_ruleset_rejects_bypass_actors() -> None: ] +def test_central_ruleset_rejects_missing_bypass_evidence() -> None: + payload = ruleset_payload() + del payload["bypass_actors"] + + assert audit.audit_ruleset(payload) == [ + "central ruleset must not configure bypass actors", + ] + + def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] @@ -243,6 +254,15 @@ def test_repository_ruleset_rejects_bypass_actors() -> None: ] +def test_repository_ruleset_rejects_missing_bypass_evidence() -> None: + payload = repository_ruleset_payload() + del payload["bypass_actors"] + + assert audit.audit_repository_ruleset(payload) == [ + "repository ruleset must not configure bypass actors", + ] + + def test_repository_ruleset_reports_structural_and_protection_drift() -> None: payload = { "id": 0, @@ -261,6 +281,7 @@ def test_repository_ruleset_reports_structural_and_protection_drift() -> None: "repository ruleset source is not ContextualWisdomLab/.github", "repository ruleset target is not branch", "repository ruleset enforcement is not active", + "repository ruleset must not configure bypass actors", "repository ruleset ref scope must be exactly the default branch", "expected one repository pull_request rule, found 0", "repository default-branch deletion protection is missing", @@ -524,6 +545,7 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: "expected ruleset name CWL Central required workflows", "central ruleset target is not branch", "central ruleset enforcement is not active", + "central ruleset must not configure bypass actors", "central ruleset does not include all repositories", "central ruleset repository exclusions drifted: expected ['.github', 'IRT-bibliography-set', 'noema'], got []", "central ruleset ref scope must be exactly the default branch", From 8ea2ec58bcd9f204a807a2925e779999480e087f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 06:42:59 +0000 Subject: [PATCH 019/154] Retrigger required checks against refreshed main (no new main commits to merge; branch already current) From ed314f6140a791b46724b84daff747fa5fb63604 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:31:48 +0900 Subject: [PATCH 020/154] test(governance): define solo-maintainer ruleset RED --- tests/test_solo_maintainer_ruleset_policy.py | 93 ++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/test_solo_maintainer_ruleset_policy.py diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py new file mode 100644 index 0000000000..125f3ce1ca --- /dev/null +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -0,0 +1,93 @@ +"""Regression contract for solo-maintainer protected-branch governance.""" + +from scripts.ci import audit_central_required_workflows as audit + + +def _central_ruleset_payload() -> dict: + """Return the desired organization ruleset for a one-human-maintainer fleet.""" + return { + "id": audit.RULESET_ID, + "name": audit.RULESET_NAME, + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "repository_name": { + "include": ["~ALL"], + "exclude": [".github", "IRT-bibliography-set", "noema"], + }, + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + { + "type": "workflows", + "parameters": { + "do_not_enforce_on_create": True, + "workflows": [ + { + "repository_id": audit.SOURCE_REPOSITORY_ID, + "path": path, + "ref": audit.SOURCE_REF, + } + for path in audit.REQUIRED_WORKFLOW_PATHS + ], + }, + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "required_reviewers": [], + "require_extra_approval_for_unattributed_changes": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def _repository_ruleset_payload() -> dict: + """Return the desired .github repository ruleset under the same model.""" + return { + "id": audit.REPOSITORY_RULESET_ID, + "name": audit.REPOSITORY_RULESET_NAME, + "target": "branch", + "source_type": "Repository", + "source": audit.REPOSITORY_RULESET_SOURCE, + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "require_extra_approval_for_unattributed_changes": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def test_central_ruleset_accepts_zero_approvals_without_last_push_approval() -> None: + """A one-human fleet must not require an approval its sole author cannot give.""" + assert audit.audit_ruleset(_central_ruleset_payload()) == [] + + +def test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() -> None: + """The control-plane repository must use the same satisfiable admission model.""" + assert audit.audit_repository_ruleset(_repository_ruleset_payload()) == [] From a815b54be2ec0a7eed009afeb1a4363cff9ec9a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:33:43 +0900 Subject: [PATCH 021/154] ci(governance): run focused solo-maintainer contract --- .../solo-maintainer-ruleset-contract.yml | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/solo-maintainer-ruleset-contract.yml diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml new file mode 100644 index 0000000000..41637e8d6e --- /dev/null +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -0,0 +1,38 @@ +name: Solo Maintainer Ruleset Contract + +on: + push: + branches: [fix/stacked-pr-central-required-workflows] + paths: + - ".github/workflows/solo-maintainer-ruleset-contract.yml" + - "scripts/ci/audit_central_required_workflows.py" + - "tests/test_solo_maintainer_ruleset_policy.py" + +permissions: + contents: read + +jobs: + focused-contract: + name: Focused solo-maintainer ruleset contract + runs-on: ubuntu-latest + steps: + - name: Checkout exact pushed head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Execute focused contract without third-party test dependencies + run: | + python3 - <<'PY' + import importlib.util + from pathlib import Path + + test_path = Path("tests/test_solo_maintainer_ruleset_policy.py") + spec = importlib.util.spec_from_file_location("solo_ruleset_contract", test_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"unable to load {test_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.test_central_ruleset_accepts_zero_approvals_without_last_push_approval() + module.test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() + PY From 3830a7dc837a0a5e18dee2539859abd49c161c26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:43:29 +0900 Subject: [PATCH 022/154] fix(governance): align ruleset audit to solo maintainer --- scripts/ci/audit_central_required_workflows.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 0eeaf87c6a..aed797271f 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -184,12 +184,12 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: parameters = review_rules[0].get("parameters") parameters = parameters if isinstance(parameters, dict) else {} approving_reviews = parameters.get("required_approving_review_count") - if approving_reviews != 2: - errors.append("exactly two approving reviews are not required") + if approving_reviews != 0: + errors.append("central solo-maintainer ruleset must not require approving reviews") if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("stale-review dismissal on push is disabled") - if parameters.get("require_last_push_approval") is not True: - errors.append("last-push approval protection is disabled") + if parameters.get("require_last_push_approval") is not False: + errors.append("central solo-maintainer ruleset must not require last-push approval") if parameters.get("required_review_thread_resolution") is not True: errors.append("review-thread resolution protection is disabled") allowed_methods = set(parameters.get("allowed_merge_methods") or []) @@ -293,12 +293,12 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: else: raw_parameters = review_rules[0].get("parameters") parameters = raw_parameters if isinstance(raw_parameters, dict) else {} - if parameters.get("required_approving_review_count") != 2: - errors.append("repository ruleset does not require exactly two approving reviews") + if parameters.get("required_approving_review_count") != 0: + errors.append("repository solo-maintainer ruleset must not require approving reviews") if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("repository ruleset stale-review dismissal on push is disabled") - if parameters.get("require_last_push_approval") is not True: - errors.append("repository ruleset last-push approval protection is disabled") + if parameters.get("require_last_push_approval") is not False: + errors.append("repository solo-maintainer ruleset must not require last-push approval") if parameters.get("required_review_thread_resolution") is not True: errors.append("repository ruleset review-thread resolution protection is disabled") allowed_methods = set(parameters.get("allowed_merge_methods") or []) From f01d2cbf76f28b813d00d382492054b730037e4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:47:20 +0900 Subject: [PATCH 023/154] test(governance): rebaseline solo-maintainer ruleset policy --- ...central_required_workflow_ruleset_audit.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 35b050eb88..908c465a24 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -50,10 +50,10 @@ def ruleset_payload() -> dict: { "type": "pull_request", "parameters": { - "required_approving_review_count": 2, + "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, "require_code_owner_review": False, - "require_last_push_approval": True, + "require_last_push_approval": False, "required_review_thread_resolution": True, "required_reviewers": [], "allowed_merge_methods": ["merge", "squash"], @@ -128,9 +128,9 @@ def repository_ruleset_payload() -> dict: { "type": "pull_request", "parameters": { - "required_approving_review_count": 2, + "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, - "require_last_push_approval": True, + "require_last_push_approval": False, "required_review_thread_resolution": True, "allowed_merge_methods": ["merge", "squash"], }, @@ -214,18 +214,18 @@ def test_expected_repository_ruleset_passes() -> None: assert audit.audit_repository_ruleset(repository_ruleset_payload()) == [] -def test_repository_ruleset_rejects_live_weakened_review_controls() -> None: +def test_repository_ruleset_rejects_unsatisfiable_review_controls() -> None: assert hasattr(audit, "audit_repository_ruleset"), ( "the central audit must inspect the repository ruleset that protects .github" ) payload = repository_ruleset_payload() review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") - review_rule["parameters"]["required_approving_review_count"] = 0 - review_rule["parameters"]["require_last_push_approval"] = False + review_rule["parameters"]["required_approving_review_count"] = 1 + review_rule["parameters"]["require_last_push_approval"] = True assert audit.audit_repository_ruleset(payload) == [ - "repository ruleset does not require exactly two approving reviews", - "repository ruleset last-push approval protection is disabled", + "repository solo-maintainer ruleset must not require approving reviews", + "repository solo-maintainer ruleset must not require last-push approval", ] @@ -295,9 +295,9 @@ def test_repository_ruleset_rejects_malformed_review_parameters() -> None: review_rule["parameters"] = None assert audit.audit_repository_ruleset(payload) == [ - "repository ruleset does not require exactly two approving reviews", + "repository solo-maintainer ruleset must not require approving reviews", "repository ruleset stale-review dismissal on push is disabled", - "repository ruleset last-push approval protection is disabled", + "repository solo-maintainer ruleset must not require last-push approval", "repository ruleset review-thread resolution protection is disabled", "repository ruleset must allow only merge and squash", ] @@ -371,6 +371,8 @@ def test_multiple_workflow_rules_do_not_invent_create_transition_drift() -> None assert "expected one workflows rule, found 2" in errors assert "central required workflows block the branch create transition" not in errors + + def test_expected_stacked_ruleset_passes(monkeypatch, capsys) -> None: payload = stacked_ruleset_payload() payload["rules"][0]["parameters"]["workflows"][0]["sha"] = "a" * 40 @@ -514,17 +516,17 @@ def test_wrong_workflow_ref_reports_exact_drift() -> None: ) -def test_review_policy_weakening_reports_exact_drift() -> None: +def test_unsatisfiable_review_policy_reports_exact_drift() -> None: payload = ruleset_payload() review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") review_rule["parameters"]["required_approving_review_count"] = 1 - review_rule["parameters"]["require_last_push_approval"] = False + review_rule["parameters"]["require_last_push_approval"] = True review_rule["parameters"]["required_review_thread_resolution"] = False errors = audit.audit_ruleset(payload) - assert "exactly two approving reviews are not required" in errors - assert "last-push approval protection is disabled" in errors + assert "central solo-maintainer ruleset must not require approving reviews" in errors + assert "central solo-maintainer ruleset must not require last-push approval" in errors assert "review-thread resolution protection is disabled" in errors @@ -563,7 +565,7 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: ] -def test_audit_reports_malformed_duplicate_workflows_and_weak_review_parameters() -> None: +def test_audit_handles_duplicate_workflows_and_unsatisfiable_review_parameters() -> None: payload = ruleset_payload() workflow_rule = next(rule for rule in payload["rules"] if rule["type"] == "workflows") workflows = workflow_rule["parameters"]["workflows"] @@ -572,9 +574,9 @@ def test_audit_reports_malformed_duplicate_workflows_and_weak_review_parameters( workflows.append(deepcopy(workflows[-1])) review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") review_rule["parameters"] = { - "required_approving_review_count": 0, + "required_approving_review_count": 1, "dismiss_stale_reviews_on_push": False, - "require_last_push_approval": False, + "require_last_push_approval": True, "required_review_thread_resolution": False, "allowed_merge_methods": ["squash"], } @@ -582,9 +584,9 @@ def test_audit_reports_malformed_duplicate_workflows_and_weak_review_parameters( errors = audit.audit_ruleset(payload) assert "central required workflow .github/workflows/sast-semgrep.yml is configured 2 times" in errors - assert "exactly two approving reviews are not required" in errors + assert "central solo-maintainer ruleset must not require approving reviews" in errors assert "stale-review dismissal on push is disabled" in errors - assert "last-push approval protection is disabled" in errors + assert "central solo-maintainer ruleset must not require last-push approval" in errors assert "review-thread resolution protection is disabled" in errors assert "only merge and squash may be allowed merge methods" in errors @@ -599,7 +601,8 @@ def test_audit_handles_malformed_rule_parameter_shapes() -> None: errors = audit.audit_ruleset(payload) assert "missing central required workflow .github/workflows/sast-semgrep.yml" in errors - assert "exactly two approving reviews are not required" in errors + assert "central solo-maintainer ruleset must not require approving reviews" in errors + assert "central solo-maintainer ruleset must not require last-push approval" in errors def test_load_payload_rejects_non_object_and_main_logs_load_reason(monkeypatch, capsys) -> None: From 260705b92c55de2135816afa7ff373ed95ddcc39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:48:06 +0900 Subject: [PATCH 024/154] test(governance): reject synthetic required reviewers --- tests/test_solo_maintainer_ruleset_policy.py | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py index 125f3ce1ca..bc5d426622 100644 --- a/tests/test_solo_maintainer_ruleset_policy.py +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -73,6 +73,7 @@ def _repository_ruleset_payload() -> dict: "dismiss_stale_reviews_on_push": True, "require_last_push_approval": False, "required_review_thread_resolution": True, + "required_reviewers": [], "require_extra_approval_for_unattributed_changes": True, "allowed_merge_methods": ["merge", "squash"], }, @@ -91,3 +92,29 @@ def test_central_ruleset_accepts_zero_approvals_without_last_push_approval() -> def test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() -> None: """The control-plane repository must use the same satisfiable admission model.""" assert audit.audit_repository_ruleset(_repository_ruleset_payload()) == [] + + +def test_central_ruleset_rejects_synthetic_required_reviewer() -> None: + """A named reviewer cannot manufacture independence in a one-human fleet.""" + payload = _central_ruleset_payload() + review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") + review_rule["parameters"]["required_reviewers"] = [ + {"reviewer_id": 1234, "reviewer_type": "User"} + ] + + assert audit.audit_ruleset(payload) == [ + "central solo-maintainer ruleset must not configure required reviewers" + ] + + +def test_repository_ruleset_rejects_synthetic_required_reviewer() -> None: + """The owner repository cannot reintroduce the same deadlock by reviewer identity.""" + payload = _repository_ruleset_payload() + review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") + review_rule["parameters"]["required_reviewers"] = [ + {"reviewer_id": 1234, "reviewer_type": "User"} + ] + + assert audit.audit_repository_ruleset(payload) == [ + "repository solo-maintainer ruleset must not configure required reviewers" + ] From 1f0d0e8adbe39145c5a9302dbe9e6b3e63d7d0fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:48:42 +0900 Subject: [PATCH 025/154] ci(governance): exercise synthetic-reviewer RED --- .github/workflows/solo-maintainer-ruleset-contract.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 41637e8d6e..303d3f71b6 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -6,6 +6,7 @@ on: paths: - ".github/workflows/solo-maintainer-ruleset-contract.yml" - "scripts/ci/audit_central_required_workflows.py" + - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_solo_maintainer_ruleset_policy.py" permissions: @@ -27,6 +28,9 @@ jobs: import importlib.util from pathlib import Path + regression_path = Path("tests/test_central_required_workflow_ruleset_audit.py") + compile(regression_path.read_text(encoding="utf-8"), str(regression_path), "exec") + test_path = Path("tests/test_solo_maintainer_ruleset_policy.py") spec = importlib.util.spec_from_file_location("solo_ruleset_contract", test_path) if spec is None or spec.loader is None: @@ -35,4 +39,6 @@ jobs: spec.loader.exec_module(module) module.test_central_ruleset_accepts_zero_approvals_without_last_push_approval() module.test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() + module.test_central_ruleset_rejects_synthetic_required_reviewer() + module.test_repository_ruleset_rejects_synthetic_required_reviewer() PY From 033d6ecfa41e3736c7843ab4eb17f7dc41d906b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:00:42 +0900 Subject: [PATCH 026/154] fix(governance): reject synthetic required reviewers --- scripts/ci/audit_central_required_workflows.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index aed797271f..4f3e39989c 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -186,6 +186,8 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: approving_reviews = parameters.get("required_approving_review_count") if approving_reviews != 0: errors.append("central solo-maintainer ruleset must not require approving reviews") + if parameters.get("required_reviewers") not in (None, []): + errors.append("central solo-maintainer ruleset must not configure required reviewers") if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: @@ -295,6 +297,8 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: parameters = raw_parameters if isinstance(raw_parameters, dict) else {} if parameters.get("required_approving_review_count") != 0: errors.append("repository solo-maintainer ruleset must not require approving reviews") + if parameters.get("required_reviewers") not in (None, []): + errors.append("repository solo-maintainer ruleset must not configure required reviewers") if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("repository ruleset stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: From 1c9c8311ebe6bb26380261e397bc198fd80cf3db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:21:09 +0900 Subject: [PATCH 027/154] test(governance): require executable ruleset regressions --- tests/test_solo_maintainer_ruleset_policy.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py index bc5d426622..8d1fb99672 100644 --- a/tests/test_solo_maintainer_ruleset_policy.py +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -1,7 +1,12 @@ """Regression contract for solo-maintainer protected-branch governance.""" +from pathlib import Path + from scripts.ci import audit_central_required_workflows as audit +REPO_ROOT = Path(__file__).resolve().parents[1] +FOCUSED_WORKFLOW_PATH = REPO_ROOT / ".github/workflows/solo-maintainer-ruleset-contract.yml" + def _central_ruleset_payload() -> dict: """Return the desired organization ruleset for a one-human-maintainer fleet.""" @@ -118,3 +123,14 @@ def test_repository_ruleset_rejects_synthetic_required_reviewer() -> None: assert audit.audit_repository_ruleset(payload) == [ "repository solo-maintainer ruleset must not configure required reviewers" ] + + +def test_focused_workflow_executes_main_ruleset_regressions() -> None: + """The temporary proof workflow must execute, not merely parse, both suites.""" + workflow = " ".join(FOCUSED_WORKFLOW_PATH.read_text(encoding="utf-8").split()) + + assert ( + "python -m pytest -q tests/test_central_required_workflow_ruleset_audit.py " + "tests/test_solo_maintainer_ruleset_policy.py" + ) in workflow + assert "compile(regression_path.read_text" not in workflow From 06e1ba2ad9230d4b8cffcd95de898774b8cbb4af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:21:41 +0900 Subject: [PATCH 028/154] fix(governance): execute full focused ruleset suite --- .../solo-maintainer-ruleset-contract.yml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 303d3f71b6..2f0bb4875b 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -22,23 +22,23 @@ jobs: with: persist-credentials: false - - name: Execute focused contract without third-party test dependencies + - name: Execute focused ruleset contract + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} run: | - python3 - <<'PY' - import importlib.util - from pathlib import Path - - regression_path = Path("tests/test_central_required_workflow_ruleset_audit.py") - compile(regression_path.read_text(encoding="utf-8"), str(regression_path), "exec") - - test_path = Path("tests/test_solo_maintainer_ruleset_policy.py") - spec = importlib.util.spec_from_file_location("solo_ruleset_contract", test_path) - if spec is None or spec.loader is None: - raise RuntimeError(f"unable to load {test_path}") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - module.test_central_ruleset_accepts_zero_approvals_without_last_push_approval() - module.test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() - module.test_central_ruleset_rejects_synthetic_required_reviewer() - module.test_repository_ruleset_rejects_synthetic_required_reviewer() - PY + cat >"${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" <<'EOF' + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install \ + --only-binary=:all: \ + --require-hashes \ + -r "${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" + python -m pytest -q \ + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_solo_maintainer_ruleset_policy.py From c3a057142f7b9084e3483674811c1f6ccea06b22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:26:47 +0900 Subject: [PATCH 029/154] test(governance): detach temporary proof from permanent suite --- tests/test_solo_maintainer_ruleset_policy.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py index 8d1fb99672..bc5d426622 100644 --- a/tests/test_solo_maintainer_ruleset_policy.py +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -1,12 +1,7 @@ """Regression contract for solo-maintainer protected-branch governance.""" -from pathlib import Path - from scripts.ci import audit_central_required_workflows as audit -REPO_ROOT = Path(__file__).resolve().parents[1] -FOCUSED_WORKFLOW_PATH = REPO_ROOT / ".github/workflows/solo-maintainer-ruleset-contract.yml" - def _central_ruleset_payload() -> dict: """Return the desired organization ruleset for a one-human-maintainer fleet.""" @@ -123,14 +118,3 @@ def test_repository_ruleset_rejects_synthetic_required_reviewer() -> None: assert audit.audit_repository_ruleset(payload) == [ "repository solo-maintainer ruleset must not configure required reviewers" ] - - -def test_focused_workflow_executes_main_ruleset_regressions() -> None: - """The temporary proof workflow must execute, not merely parse, both suites.""" - workflow = " ".join(FOCUSED_WORKFLOW_PATH.read_text(encoding="utf-8").split()) - - assert ( - "python -m pytest -q tests/test_central_required_workflow_ruleset_audit.py " - "tests/test_solo_maintainer_ruleset_policy.py" - ) in workflow - assert "compile(regression_path.read_text" not in workflow From 2b381e131017387a5e178776ff4a01b8ac68d99c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:37:47 +0900 Subject: [PATCH 030/154] fix(governance): pin focused contract Python --- .github/workflows/solo-maintainer-ruleset-contract.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 2f0bb4875b..c4157f584e 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -22,6 +22,11 @@ jobs: with: persist-credentials: false + - name: Set up pinned Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - name: Execute focused ruleset contract env: PIP_DISABLE_PIP_VERSION_CHECK: "1" From 3ec4abfbf478c68fa71f9f362664a12434cc008c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:11:36 +0900 Subject: [PATCH 031/154] ci(governance): move focused ruleset contract off saturated latest queue --- .github/workflows/solo-maintainer-ruleset-contract.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index c4157f584e..115f4998d5 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -15,7 +15,7 @@ permissions: jobs: focused-contract: name: Focused solo-maintainer ruleset contract - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout exact pushed head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 From 81a7afbc2ebbc52b11fe83a02b43948ab2aa5188 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:14:07 +0900 Subject: [PATCH 032/154] ci(governance): retire proven focused ruleset contract lane --- .../solo-maintainer-ruleset-contract.yml | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/solo-maintainer-ruleset-contract.yml diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml deleted file mode 100644 index 115f4998d5..0000000000 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Solo Maintainer Ruleset Contract - -on: - push: - branches: [fix/stacked-pr-central-required-workflows] - paths: - - ".github/workflows/solo-maintainer-ruleset-contract.yml" - - "scripts/ci/audit_central_required_workflows.py" - - "tests/test_central_required_workflow_ruleset_audit.py" - - "tests/test_solo_maintainer_ruleset_policy.py" - -permissions: - contents: read - -jobs: - focused-contract: - name: Focused solo-maintainer ruleset contract - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact pushed head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Set up pinned Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - - - name: Execute focused ruleset contract - env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_INPUT: "1" - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - cat >"${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" <<'EOF' - iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 - packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e - pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - EOF - python -m pip install \ - --only-binary=:all: \ - --require-hashes \ - -r "${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" - python -m pytest -q \ - tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_solo_maintainer_ruleset_policy.py From 66757ecf7789fb08491931c8bb2e109c3b4b1e51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:18:51 +0900 Subject: [PATCH 033/154] ci(governance): restore unproven focused contract lane --- .../solo-maintainer-ruleset-contract.yml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/solo-maintainer-ruleset-contract.yml diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml new file mode 100644 index 0000000000..2f0bb4875b --- /dev/null +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -0,0 +1,44 @@ +name: Solo Maintainer Ruleset Contract + +on: + push: + branches: [fix/stacked-pr-central-required-workflows] + paths: + - ".github/workflows/solo-maintainer-ruleset-contract.yml" + - "scripts/ci/audit_central_required_workflows.py" + - "tests/test_central_required_workflow_ruleset_audit.py" + - "tests/test_solo_maintainer_ruleset_policy.py" + +permissions: + contents: read + +jobs: + focused-contract: + name: Focused solo-maintainer ruleset contract + runs-on: ubuntu-latest + steps: + - name: Checkout exact pushed head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Execute focused ruleset contract + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cat >"${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" <<'EOF' + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install \ + --only-binary=:all: \ + --require-hashes \ + -r "${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" + python -m pytest -q \ + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_solo_maintainer_ruleset_policy.py From c52470b797c2eb17555a688a889397fbf45bd33c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:25:05 +0900 Subject: [PATCH 034/154] test(governance): cover complete ruleset drift evidence --- ...t_ruleset_audit_completeness_regression.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/test_ruleset_audit_completeness_regression.py diff --git a/tests/test_ruleset_audit_completeness_regression.py b/tests/test_ruleset_audit_completeness_regression.py new file mode 100644 index 0000000000..36d50cd055 --- /dev/null +++ b/tests/test_ruleset_audit_completeness_regression.py @@ -0,0 +1,120 @@ +"""Regression tests for complete ruleset drift evidence and rollout policy.""" + +from pathlib import Path + +from scripts.ci import audit_central_required_workflows as audit + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def _central_payload() -> dict: + """Return a minimal payload satisfying the declared central policy.""" + return { + "id": audit.RULESET_ID, + "name": audit.RULESET_NAME, + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "repository_name": { + "include": ["~ALL"], + "exclude": [".github", "IRT-bibliography-set", "noema"], + }, + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + { + "type": "workflows", + "parameters": { + "do_not_enforce_on_create": True, + "workflows": [ + { + "repository_id": audit.SOURCE_REPOSITORY_ID, + "path": path, + "ref": audit.SOURCE_REF, + } + for path in audit.REQUIRED_WORKFLOW_PATHS + ], + }, + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "required_reviewers": [], + "dismiss_stale_reviews_on_push": True, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def _repository_payload() -> dict: + """Return a minimal payload satisfying the owner-repository policy.""" + return { + "id": audit.REPOSITORY_RULESET_ID, + "name": audit.REPOSITORY_RULESET_NAME, + "source_type": "Repository", + "source": audit.REPOSITORY_RULESET_SOURCE, + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "required_reviewers": [], + "dismiss_stale_reviews_on_push": True, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def test_central_ruleset_rejects_creation_or_other_undeclared_rule_types() -> None: + """A creation rule must not silently defeat the branch-create transition.""" + payload = _central_payload() + payload["rules"].extend([{"type": "creation"}, {"type": "required_signatures"}]) + + assert "central ruleset has forbidden rule types: ['creation', 'required_signatures']" in audit.audit_ruleset(payload) + + +def test_repository_ruleset_rejects_undeclared_rule_types() -> None: + """The repository pass result must describe the complete protected policy.""" + payload = _repository_payload() + payload["rules"].append({"type": "creation"}) + + assert "repository ruleset has forbidden rule types: ['creation']" in audit.audit_repository_ruleset(payload) + + +def test_live_audit_collects_all_available_ruleset_drift_before_failing() -> None: + """One ruleset failure must not suppress other already-fetched audit results.""" + workflow = (REPO_ROOT / ".github/workflows/audit-central-ruleset.yml").read_text(encoding="utf-8") + + assert "audit_status=0" in workflow + assert workflow.count("if ! python3 scripts/ci/audit_central_required_workflows.py") == 3 + assert 'if [[ "$audit_status" -ne 0 ]]; then' in workflow + + +def test_rollout_guide_declares_solo_maintainer_review_policy() -> None: + """Operator documentation must not reintroduce a fictional second human approval.""" + rollout = (REPO_ROOT / "docs/org-required-workflow-rollout.md").read_text(encoding="utf-8") + + assert "required_approving_review_count = 0" in rollout + assert "require_last_push_approval = false" in rollout + assert "The org's two-reviewer merge rule" not in rollout + assert "two distinct approvals" not in rollout From a9505c3a8ee9381552ca195fb355761edc3b781f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:26:40 +0900 Subject: [PATCH 035/154] fix(governance): reject undeclared ruleset controls --- .../ci/audit_central_required_workflows.py | 91 ++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 4f3e39989c..e8154e4187 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -34,6 +34,17 @@ ".github/workflows/strix.yml", ".github/workflows/sast-semgrep.yml", ) +CENTRAL_ALLOWED_RULE_TYPES = { + "workflows", + "pull_request", + "deletion", + "non_fast_forward", +} +REPOSITORY_ALLOWED_RULE_TYPES = { + "pull_request", + "deletion", + "non_fast_forward", +} STACKED_WORKFLOW_PATH = ".github/workflows/opencode-review.yml" @@ -49,6 +60,26 @@ def _typed_rules(payload: dict[str, Any], rule_type: str) -> list[dict[str, Any] ] +def _forbidden_rule_types( + payload: dict[str, Any], allowed_rule_types: set[str] +) -> list[str]: + """Return undeclared or malformed rule types from a ruleset payload.""" + rules = payload.get("rules") + if not isinstance(rules, list): + return [] + forbidden: set[str] = set() + for rule in rules: + if not isinstance(rule, dict): + forbidden.add("") + continue + rule_type = rule.get("type") + if not isinstance(rule_type, str) or not rule_type: + forbidden.add("") + elif rule_type not in allowed_rule_types: + forbidden.add(rule_type) + return sorted(forbidden) + + def audit_ruleset(payload: dict[str, Any]) -> list[str]: """Return explicit drift reasons for a live organization ruleset payload.""" errors: list[str] = [] @@ -77,7 +108,9 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: ) if is_inherited_org_payload: malformed_scope = sorted( - name for name, inherited in inherited_scope.items() if not isinstance(inherited, bool) + name + for name, inherited in inherited_scope.items() + if not isinstance(inherited, bool) ) if malformed_scope: errors.append( @@ -136,7 +169,10 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: workflows = workflow_parameters.get("workflows") workflows = workflows if isinstance(workflows, list) else [] - if len(workflow_rules) == 1 and workflow_parameters.get("do_not_enforce_on_create") is not True: + if ( + len(workflow_rules) == 1 + and workflow_parameters.get("do_not_enforce_on_create") is not True + ): errors.append("central required workflows block the branch create transition") malformed_workflows = sum( @@ -166,7 +202,9 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: errors.append(f"missing central required workflow {path}") continue if len(matches) != 1: - errors.append(f"central required workflow {path} is configured {len(matches)} times") + errors.append( + f"central required workflow {path} is configured {len(matches)} times" + ) if not any( workflow.get("repository_id") == SOURCE_REPOSITORY_ID and workflow.get("ref") == SOURCE_REF @@ -185,13 +223,19 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: parameters = parameters if isinstance(parameters, dict) else {} approving_reviews = parameters.get("required_approving_review_count") if approving_reviews != 0: - errors.append("central solo-maintainer ruleset must not require approving reviews") + errors.append( + "central solo-maintainer ruleset must not require approving reviews" + ) if parameters.get("required_reviewers") not in (None, []): - errors.append("central solo-maintainer ruleset must not configure required reviewers") + errors.append( + "central solo-maintainer ruleset must not configure required reviewers" + ) if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: - errors.append("central solo-maintainer ruleset must not require last-push approval") + errors.append( + "central solo-maintainer ruleset must not require last-push approval" + ) if parameters.get("required_review_thread_resolution") is not True: errors.append("review-thread resolution protection is disabled") allowed_methods = set(parameters.get("allowed_merge_methods") or []) @@ -203,6 +247,10 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: if not _typed_rules(payload, "non_fast_forward"): errors.append("default-branch non-fast-forward protection is missing") + forbidden_rule_types = _forbidden_rule_types(payload, CENTRAL_ALLOWED_RULE_TYPES) + if forbidden_rule_types: + errors.append(f"central ruleset has forbidden rule types: {forbidden_rule_types}") + return errors @@ -273,7 +321,10 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append(f"expected repository ruleset id {REPOSITORY_RULESET_ID}") if payload.get("name") != REPOSITORY_RULESET_NAME: errors.append(f"expected repository ruleset name {REPOSITORY_RULESET_NAME}") - if payload.get("source_type") != "Repository" or payload.get("source") != REPOSITORY_RULESET_SOURCE: + if ( + payload.get("source_type") != "Repository" + or payload.get("source") != REPOSITORY_RULESET_SOURCE + ): errors.append("repository ruleset source is not ContextualWisdomLab/.github") if payload.get("target") != "branch": errors.append("repository ruleset target is not branch") @@ -296,15 +347,23 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: raw_parameters = review_rules[0].get("parameters") parameters = raw_parameters if isinstance(raw_parameters, dict) else {} if parameters.get("required_approving_review_count") != 0: - errors.append("repository solo-maintainer ruleset must not require approving reviews") + errors.append( + "repository solo-maintainer ruleset must not require approving reviews" + ) if parameters.get("required_reviewers") not in (None, []): - errors.append("repository solo-maintainer ruleset must not configure required reviewers") + errors.append( + "repository solo-maintainer ruleset must not configure required reviewers" + ) if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("repository ruleset stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: - errors.append("repository solo-maintainer ruleset must not require last-push approval") + errors.append( + "repository solo-maintainer ruleset must not require last-push approval" + ) if parameters.get("required_review_thread_resolution") is not True: - errors.append("repository ruleset review-thread resolution protection is disabled") + errors.append( + "repository ruleset review-thread resolution protection is disabled" + ) allowed_methods = set(parameters.get("allowed_merge_methods") or []) if allowed_methods != {"merge", "squash"}: errors.append("repository ruleset must allow only merge and squash") @@ -313,6 +372,14 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append("repository default-branch deletion protection is missing") if not _typed_rules(payload, "non_fast_forward"): errors.append("repository default-branch non-fast-forward protection is missing") + + forbidden_rule_types = _forbidden_rule_types( + payload, REPOSITORY_ALLOWED_RULE_TYPES + ) + if forbidden_rule_types: + errors.append( + f"repository ruleset has forbidden rule types: {forbidden_rule_types}" + ) return errors @@ -385,4 +452,4 @@ def main(argv: list[str] | None = None) -> int: if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From c3ec79a15fbb349b829b86d031976067f76ae655 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:27:11 +0900 Subject: [PATCH 036/154] fix(governance): report all fetched ruleset drift --- .github/workflows/audit-central-ruleset.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/audit-central-ruleset.yml b/.github/workflows/audit-central-ruleset.yml index 1486f4162e..c2e85f8683 100644 --- a/.github/workflows/audit-central-ruleset.yml +++ b/.github/workflows/audit-central-ruleset.yml @@ -45,6 +45,7 @@ jobs: RULESET_SENTINEL_REPOSITORY: naruon run: | set -euo pipefail + audit_status=0 ruleset_json="$RUNNER_TEMP/central-required-workflow-ruleset.json" ruleset_with_scope_json="$RUNNER_TEMP/central-required-workflow-ruleset-with-scope.json" ruleset_error="$RUNNER_TEMP/central-required-workflow-ruleset.error" @@ -90,7 +91,9 @@ jobs: jq --slurpfile scope "$scope_json" \ '. + {"_audit_repository_scope": $scope[0]}' \ "$ruleset_json" >"$ruleset_with_scope_json" - python3 scripts/ci/audit_central_required_workflows.py "$ruleset_with_scope_json" + if ! python3 scripts/ci/audit_central_required_workflows.py "$ruleset_with_scope_json"; then + audit_status=1 + fi repository_ruleset_json="$RUNNER_TEMP/owner-repository-ruleset.json" repository_ruleset_error="$RUNNER_TEMP/owner-repository-ruleset.error" @@ -100,7 +103,9 @@ jobs: sed 's/^/ /' "$repository_ruleset_error" exit 1 fi - python3 scripts/ci/audit_central_required_workflows.py --repository "$repository_ruleset_json" + if ! python3 scripts/ci/audit_central_required_workflows.py --repository "$repository_ruleset_json"; then + audit_status=1 + fi stacked_ruleset_json="$RUNNER_TEMP/stacked-opencode-ruleset.json" stacked_ruleset_error="$RUNNER_TEMP/stacked-opencode-ruleset.error" @@ -110,4 +115,11 @@ jobs: sed 's/^/ /' "$stacked_ruleset_error" exit 1 fi - python3 scripts/ci/audit_central_required_workflows.py --stacked "$stacked_ruleset_json" + if ! python3 scripts/ci/audit_central_required_workflows.py --stacked "$stacked_ruleset_json"; then + audit_status=1 + fi + + if [[ "$audit_status" -ne 0 ]]; then + echo "::error::One or more fetched rulesets drift from the declared governance contract." + exit "$audit_status" + fi \ No newline at end of file From d1c0b7cb29fbcddbe7fe3728e7762feb8f0f7887 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:27:24 +0900 Subject: [PATCH 037/154] ci(governance): execute completeness regressions --- .github/workflows/solo-maintainer-ruleset-contract.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 2f0bb4875b..3779dfc591 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -5,9 +5,12 @@ on: branches: [fix/stacked-pr-central-required-workflows] paths: - ".github/workflows/solo-maintainer-ruleset-contract.yml" + - ".github/workflows/audit-central-ruleset.yml" - "scripts/ci/audit_central_required_workflows.py" + - "docs/org-required-workflow-rollout.md" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_solo_maintainer_ruleset_policy.py" + - "tests/test_ruleset_audit_completeness_regression.py" permissions: contents: read @@ -41,4 +44,5 @@ jobs: -r "${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" python -m pytest -q \ tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_solo_maintainer_ruleset_policy.py + tests/test_solo_maintainer_ruleset_policy.py \ + tests/test_ruleset_audit_completeness_regression.py \ No newline at end of file From c2ab699af84d4ea297107059a07300696e4b5a3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:29:26 +0900 Subject: [PATCH 038/154] docs(governance): align rollout with solo-maintainer policy --- docs/org-required-workflow-rollout.md | 432 +++++++------------------- 1 file changed, 107 insertions(+), 325 deletions(-) diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 36edcd29dd..4e5be81978 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -1,18 +1,22 @@ # ContextualWisdomLab central required workflow rollout -Updated: 2026-08-28 KST +Updated: 2026-09-01 KST -## Decision +## Purpose -Use an organization repository ruleset instead of copying workflow files into each repository. +This document is the operator-facing current-state contract for organization-wide pull-request admission. Historical rollout experiments, exact transient PR heads, runner IDs, and superseded review-count policies remain available in Git history and the linked governance issues; they are intentionally not repeated here because stale operational snapshots can recreate a closed governance defect. + +## Canonical organization ruleset + +Use the organization ruleset rather than copying central review/security workflow files into each product repository. - Ruleset: `CWL Central required workflows` - Ruleset ID: `18156473` -- Enforcement: `active` -- Target: branch rules on every repository's default branch (`repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`) -- Required workflow source repository: `ContextualWisdomLab/.github` -- Required workflow source repository ID: `1274066402` -- Active required workflow paths: +- Enforcement target: active branch rules on each non-excluded repository's default branch only. +- Repository selector: all organization repositories except `.github`, `IRT-bibliography-set`, and `noema`. +- Ref selector: `ref_name.include=["~DEFAULT_BRANCH"]`, `ref_name.exclude=[]`. +- Required workflow source: `ContextualWisdomLab/.github` repository ID `1274066402` at `refs/heads/main`. +- Required workflow paths: - `.github/workflows/close-empty-pr.yml` - `.github/workflows/noema-review.yml` - `.github/workflows/opencode-review.yml` @@ -20,320 +24,98 @@ Use an organization repository ruleset instead of copying workflow files into ea - `.github/workflows/security-scan.yml` - `.github/workflows/strix.yml` - `.github/workflows/sast-semgrep.yml` -- Required workflow ref: `refs/heads/main` -- Last verified workflow implementation base commit: `050e6d59b0de9e62c8413d5f8f26f4f2f9ebea09` (`#584`) -- Required workflow trigger support: `pull_request`, `pull_request_target`, `push`, `workflow_run` - -The required-workflow implementation is current through merged `.github#584`. -The ruleset points at `.github@main`; if live organization ruleset inspection -reports another ref, treat that as operations drift and restore ruleset -`18156473` to the current `main` head. - -This keeps Strix security evidence, OpenCode and independent Noema review evidence, and merge/update automation sourced from the central `.github` repository. Target repositories do not need local copies of these workflows for the organization required workflow rule, and new repositories inherit the rule without a repository-name list update. - -The central `security-scan.yml` and `sast-semgrep.yml` pull-request triggers are -base-ref agnostic. They therefore also run for stacked pull requests targeting a -feature branch; the organization ruleset's protected-ref scope remains an -independent administrative control and is not weakened by this trigger -coverage. - -Stacked pull requests are audited by organization ruleset -`CWL Stacked OpenCode required workflow` (`21732164`) in `evaluate` mode. It -targets every non-default branch and references only -`.github/workflows/opencode-review.yml` from `.github@refs/heads/main`. -Its observed scope is `ref_name.include=["~ALL"]` with -`ref_name.exclude=["~DEFAULT_BRANCH"]`. -Active enforcement over every non-default ref is prohibited: GitHub evaluates -the ref update before a `pull_request_target.synchronize` run can exist for the -new commit, so it rejects both initial branch creation and later review fixes. -Exact-head OpenCode evidence remains a merge requirement enforced by the -normal PR procedure while a target-ref-scoped enforcement design is developed. - -## OpenCode required workflow posture - -The central `.github/workflows/opencode-review.yml` is now part of the active organization required workflow ruleset. - -- Required workflow trigger support: metadata-only `pull_request_target`; the file contains no checkout, PR-head execution, or secret expression -- Stable branch-protection job names: `required-workflow-bootstrap`, `coverage-source-tree`, `coverage-evidence`, and `opencode-review`; these jobs are data-only sentinels, while approval remains a separate current-head PR-review requirement -- Trusted source: `ContextualWisdomLab/.github` -- PR-head handling: authenticated current-head `repository_dispatch` runs `.github/workflows/opencode-review-dispatch.yml` from the protected default branch; that workflow owns metadata validation, bounded coverage, source-as-data inspection, model review, and publication -- Manual target support: the central scheduler sends exact repository, PR, base, and head metadata through `repository_dispatch`; the dispatch workflow rejects an unauthorized actor, an unallowlisted base repository, a malformed head-repository identity, or any live base/head metadata mismatch. A canonical fork head remains reviewable as untrusted source data. -- Model token posture: use the organization `STRIX_GITHUB_MODELS_TOKEN` secret for GitHub Models calls, with `github.token` as the fallback; live workflow evidence showed `github.token` alone can return 403 from `models.github.ai/inference` -- Write posture: OpenCode may create review/comment side effects through the OpenCode app token when available; the workflow token is limited to the same-repository PR context and publication failures remain visible -- Coverage execution posture: PR-controlled package, test, build, R, Rust, and Docker inputs are never executed from `pull_request_target`; the dispatch workflow runs bounded low-privilege coverage only after exact live metadata and scheduler identity validation -- Fork posture: PR heads are fetched through `refs/pull//head` when direct head-SHA fetch is not available, so review can inspect fork PR source as data without executing it in the trusted workflow context -- Runtime posture: pre-model failed-check evidence waits are capped at about five minutes; the later approval gate rechecks current-head peer checks and extends its bounded wait only while image-validation checks remain pending, logging the reason before approval -- Model-exhaustion posture: command exit codes and deterministic checks cannot synthesize an approval. Exhaustion remains `MODEL_OUTPUT_UNAVAILABLE`; only a prior real-model approval bound to the exact current head can satisfy the review gate after all checks, alerts, and threads are revalidated. -- Adversarial-evidence posture: every probe must cite its exact changed path and positive in-range line in the materialized current-head source tree. Unrelated paths, nonexistent lines, circular claims, and missing observed results fail closed with a concrete rejection reason. - -For a bounded current-head retry in one repository, dispatch `merge-scheduler` -to the central repository with `target_repository`, `pr_number`, and the live -`base_branch`. The target must exactly match -`OPENCODE_REPOSITORY_DISPATCH_TARGETS`; the scheduler then re-reads the open PR -and rejects a noncanonical base or head repository name, base mismatch, -malformed head SHA, or changed/closed PR before using cross-repository -credentials. A fork head may pass this read/review boundary, but the scheduler -still excludes it from direct merge, auto-merge, and non-maintainer branch -mutation: - -```bash -jq -n '{ - event_type: "merge-scheduler", - client_payload: { - target_repository: "ContextualWisdomLab/naruon", - pr_number: 1179, - base_branch: "develop", - trigger_reviews: true, - review_dispatch_limit: "1", - enable_auto_merge: false, - update_branches: false, - merge_mode: "disabled" - } -}' | gh api --method POST \ - repos/ContextualWisdomLab/.github/dispatches --input - -``` - -Use the canonical `full_name` returned by the GitHub repository API. Keep -mutation options disabled for an evidence-only retry; enabling branch updates -or merge behavior is a separate operational decision. - -Keep the OpenCode required workflow active only while the central workflow keeps proving current-head coverage, CodeGraph initialization, bounded evidence, model review output, and approval-gate publication on the current head. - -## Code scanning required workflow posture - -The central `.github/workflows/codeql-pr.yml`, `.github/workflows/scorecard-pr.yml`, -and `.github/workflows/osv-scanner-pr.yml` workflows supply PR-head and merge-preview -code scanning analyses for ruleset `18156473` `code_scanning` (CodeQL, Scorecard, -osv-scanner). They trigger on pull requests to `main`, `master`, and `develop` so -Git Flow repositories on `develop` inherit the same merge gate as GitHub Flow repos. - -CodeQL merge preview checks out `refs/pull//merge` and uploads SARIF with -`sha: pull_request.merge_commit_sha` because the ruleset evaluates that commit, -not the ephemeral merge ref OID. - -Repository-local `codeql.yml` push/default-branch scans may remain for branch -history, but PR merge gates should rely on the central `codeql-pr.yml` workflow. - -### Repository-local CodeQL inventory (2026-07-04) - -Org audit of default-branch workflow files. Repos without any local CodeQL -workflow depend entirely on central `codeql-pr.yml` once ruleset `18156473` -includes that path; they are the most exposed to -`Code scanning is waiting for results from CodeQL` until the ruleset update -lands. - -| Repository | Default branch | Local CodeQL workflow | PR trigger | merge_commit_sha SARIF | -| --- | --- | --- | ---: | ---: | -| `aFIPC` | `master` | `codeql.yml` | yes | no | -| `bandscope` | `develop` | `codeql.yml` | yes | no | -| `newsdom-api` | `develop` | `codeql.yml` | yes | no | -| `pg-erd-cloud` | `main` | `codeql.yml`, `codeql-backfill.yml` | yes (`codeql.yml`) | no | -| `xtrmLLMBatchPython` | `develop` | `codeql.yml` | yes | no | -| `naruon` | `develop` | `codeql.yml` | yes (temporary; PR `#916` retires PR trigger) | yes (repo-local interim fix) | -| all other public non-fork org repos | varies | none observed | — | — | - -No repository-local PR CodeQL workflow besides `naruon` uploads merge-preview -SARIF on `merge_commit_sha`. Centralizing through `codeql-pr.yml` fixes every -inherited repository in one ruleset change; per-repo deletion of PR triggers is -optional cleanup to avoid duplicate scans. - -## Scheduler required workflow posture - -The central `.github/workflows/pr-review-merge-scheduler.yml` is now part of the active organization required workflow ruleset. - -- Required workflow trigger support: `pull_request_target` -- Stable required check job name: `scan-pr-queue` -- Trusted source: `ContextualWisdomLab/.github` -- PR-event scope: when GitHub invokes the workflow for a PR, the scheduler passes `--pr-number` and inspects only that PR instead of scanning or mutating the whole repository queue -- Token posture: the workflow passes the first available mutation credential in this order: `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, exchanged OpenCode GitHub App token, then the target repository workflow token. The scheduler reports the non-secret token source and expected actor class in every mutation decision. -- Flow posture: default branches named `main` or `master` are treated as GitHub Flow; default branches named `develop` are treated as Git Flow unless a repository explicitly sets `PROJECT_FLOW` -- Merge posture: the default merge mode is `direct_or_auto`. When a current-head approved PR is same-repository and the scheduler has no failed-check, action-required, unresolved-thread, or conflict blocker, it requests an immediate guarded squash merge with `--match-head-commit`. This includes PRs where native GitHub auto-merge is already enabled; native auto-merge is a fallback queue, not the scheduler's first stop when direct merge is possible. -- Fork posture: fork or external-head PRs remain reviewable, but the scheduler does not direct-merge them and does not enable auto-merge for them. A maintainer must make the final merge decision after same-head OpenCode approval, same-head Strix evidence, required checks, and unresolved-thread checks are clean. -- Branch freshness posture: the scheduler also runs after protected base-branch pushes to `main`, `develop`, or `master`, because those pushes can create the GitHub UI state where reviews are satisfied, auto-merge is enabled, checks are stale or failed, and the PR shows `Update branch` without a PR `synchronize` event. -- Auto-merge posture: `auto_merge_enabled` PR events trigger the scheduler so an already stale branch is refreshed immediately after native auto-merge is turned on instead of waiting for the periodic schedule. If the same PR is already mergeable, the scheduler attempts the guarded direct merge immediately. -- Automation boundary: current-head failed checks and `ACTION_REQUIRED` checks are reported before branch updates, so an update attempt does not hide the concrete reason a PR cannot merge. `update-branch` handles approved `BEHIND` PRs and already queued auto-merge PRs only when there is no current-head failed or action-required check to diagnose first. `DIRTY` or `CONFLICTING` PRs still require author or maintainer conflict resolution guidance; current-head approved conflicts may keep or queue native GitHub auto-merge as a wait state while the conflict is repaired, but the scheduler must not treat queued auto-merge as a conflict resolver. -- Retry posture: before retrying OpenCode, the scheduler force-cancels older active OpenCode runs for the same PR number and a previous head SHA. It does not automatically cancel Strix runs because security evidence should not be silently discarded by force-push churn. - -Do not centralize the scheduler by running a `.github` scheduled job against other repositories with the `.github` repository token. That would either fail permission checks or use the wrong mutation actor. The central path is a required workflow executed in each target repository context. - -- Heartbeat fallback posture: event-driven target-repository runs stop retrying once their triggering event is consumed, so a PR that becomes mergeable AFTER its last event (approval published after the scheduler pass, merge-preview checks landing late, a temporary base-branch policy blocker clearing) has no later trigger and sits approved-but-unmerged. The `org-queue-sweep` job in the central scheduler workflow closes this gap: it runs every 15 minutes (`*/15 * * * *`) only in `ContextualWisdomLab/.github`, re-runs the same trusted scheduler script against every non-archived organization repository, and merges/updates through the identical guarded contract. Stacked PRs, which do not receive injected required workflows, use a separate bounded OpenCode dispatch budget so ordinary default-branch traffic cannot leave them at `OpenCode review absent`. It never uses the `.github` repository `github.token` for sibling mutations — it requires `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, or the exchanged OpenCode app token, and fails with a visible `::error` reason when no cross-repository mutation credential is available instead of silently no-opping. Every swept repository prints its per-PR decision log, so an unmerged PR always has a concrete logged reason at most 15 minutes old. -- Queue hygiene posture: during the sweep, workflow runs still `queued` after `ORG_SWEEP_STALE_QUEUE_HOURS` (default 24h) are cancelled with their run id, workflow name, head branch, and age logged. A run queued that long belongs to a head that PR events will never revisit (closed PR, force-pushed branch, or a previous runner outage), and leaving it keeps the Actions queue holding non-current-head work. -- Inaccessible-repository posture: a sibling repository the sweep credential structurally cannot read — the OpenCode app is not installed there, or `PR_REVIEW_MERGE_TOKEN` does not cover it — returns HTTP 403 `Resource not accessible by integration` on every read. That is an access-grant fact the automation can never resolve, so the sweep classifies it as a skipped, non-fatal **unavailable** repository (a `::warning` naming the repository and the remediation) instead of a hard failure. Without this, a handful of un-enrolled repositories keeps the scheduled sweep heartbeat (the org sweep's `*/15 * * * *` cron) permanently red and masks a genuinely new repository that starts failing. Fail-closed is preserved on both sides: any non-403 scheduler failure still fails the sweep with its per-PR reason, and if more than `ORG_SWEEP_MAX_UNAVAILABLE` (default 5) repositories become unreachable in one pass — a credential-scope regression rather than a few un-enrolled repos — the job fails loudly. Remediation for a listed repository is to install the OpenCode app on it or grant `PR_REVIEW_MERGE_TOKEN` access. - -## Second-reviewer (Noema) posture - -The org's two-reviewer merge rule needs a second approving-review identity -independent of OpenCode. That identity is `cwl-noema-review[bot]`, supplied by -the organization-owned `cwl-noema-review` GitHub App. The central workflow -is an active organization required workflow. It runs the centrally versioned -`noema_review_gate.py` judgement path and -mints a short-lived installation token restricted to the target repository; the -App has read-only Actions/checks/contents/status/code-scanning/Dependabot access -and write access only to pull-request reviews. - -The PydanticAI `ReviewAgent` product in `ContextualWisdomLab/noema` -(`reviewer/noema_reviewer`, noema#9) is the target standalone judgement plane, -while the central Python gate remains the deployed fail-closed reviewer. The -standalone package is not imported into the privileged workflow. External proof -exists on `ContextualWisdomLab/clearfolio#161`: `cwl-noema-review[bot]` submitted -an `APPROVED` review whose review commit, explicit Head SHA, current-head checks, -SARIF/dependency evidence, test evidence, and review marker all bind to -`4512fb9e9b56ab95df3acd85ebec2e6b849335a7`. - -- Token posture: `noema-review.yml` prefers a `NOEMA_REVIEW_TOKEN` emergency - fallback when present, otherwise mints the repository-scoped App token with - `actions/create-github-app-token` pinned to an immutable SHA. The OIDC Worker - exchange remains a compatibility fallback. If none of these identities is - configured, the required check fails with the exact missing-credential reason; - an unconfigured reviewer can never pass by skipping. -- Honesty posture: `noema_review_gate.py` refuses to review as a primary review - actor (`opencode-agent`, `github-actions`), so a `NOEMA_REVIEW_TOKEN` that - resolves to one of those identities cannot manufacture a fake second review — - it must be a distinct write-access identity. -- Required admin config: install `cwl-noema-review` on the organization, set - `NOEMA_GITHUB_APP_CLIENT_ID` plus `NOEMA_GITHUB_APP_PRIVATE_KEY`, and configure - `NOEMA_LLM_MODEL`, `NOEMA_LLM_API_URL`, and either `NOEMA_LLM_API_KEY` or the - shared `OPENAI_API_KEY`. Every missing setting is a visible failed-check reason. - -## Scope - -The active ruleset no longer maintains a repository-name allowlist. Live -ruleset inspection on 2026-07-02 18:15 KST reports -`repository_name.include=["~ALL"]`, so all current and future organization -repositories inherit the seven central required workflows on their default -branch unless a later ruleset exclusion is added. The table below is the public -non-fork inventory snapshot and rollout ledger, not the ruleset target list. - -| Repository | Visibility | Default branch | Flow | Open PRs | Local central-workflow copies on default branch | Rollout status | -| --- | --- | --- | --- | ---: | --- | --- | -| `ContextualWisdomLab/.github` | public | `main` | GitHub Flow | 27 | central source; keep | single source of truth; central PRs through `#283` merged; PR `#286` current head queued after review-thread fixes | -| `ContextualWisdomLab/aFIPC` | public | `master` | GitHub Flow | 22 | none | central checks proven on PR `#78`; active queue still needs per-PR review | -| `ContextualWisdomLab/pg-erd-cloud` | public | `main` | GitHub Flow | 81 | none | repo-local autofix worker removed by PR `#393`; default branch now keeps only repository-owned application and security workflows | -| `ContextualWisdomLab/fast-mlsirm` | public | `main` | GitHub Flow | 25 | none | migrated; re-verify inherited checks on current open PRs | -| `ContextualWisdomLab/bandscope` | public | `develop` | Git Flow | 36 | none | no local central copies observed; verify inherited checks on active PRs | -| `ContextualWisdomLab/contextual-orchestrator` | public | `main` | GitHub Flow | 2 | none | default branch has no local central copies; current open PRs are runtime proof fixtures | -| `ContextualWisdomLab/naruon` | public | `develop` | Git Flow | 7 | none | default branch has no repo-local OpenCode, Strix, or scheduler copies; application/security workflows remain repository-owned | -| `ContextualWisdomLab/newsdom-api` | public | `develop` | Git Flow | 3 | none | local workflows already gone; re-verify inherited checks on current open PRs | -| `ContextualWisdomLab/appguardrail` | public | `develop` | Git Flow | 9 | none | migrated; re-verify inherited checks before final closure | -| `ContextualWisdomLab/scopeweave` | public | `develop` | Git Flow | 2 | none | local workflows already gone; re-verify inherited checks on current open PRs | -| `ContextualWisdomLab/ContextualWisdomLab.github.io` | public | `main` | GitHub Flow | 19 | none | migrated; re-verify inherited checks on current open PRs | -| `ContextualWisdomLab/codec-carver` | public | `main` | GitHub Flow | 42 | none | local workflows already gone; quality uplift still needs 100% test/docstring evidence before closure | -| `ContextualWisdomLab/clearfolio` | public | `main` | GitHub Flow | 57 | none | migrated; re-verify inherited checks before final closure | -| `ContextualWisdomLab/semantic-data-portal` | public | `main` | GitHub Flow | 3 | none | PR `#3` merged; default branch has no local central copies | -| `ContextualWisdomLab/hyosung-itx-slogan-brief` | public | `main` | GitHub Flow | 1 | none | migrated; re-verify inherited checks on current open PR | -| `ContextualWisdomLab/kaefa` | public | `develop` | Git Flow | 6 | none | newly discovered public non-fork target; ruleset inherited but current PR #60 lacked central check runs in status rollup | -| `ContextualWisdomLab/waf-ids-ai-soc` | public | `main` | GitHub Flow | 1 | none | newly discovered public non-fork target; PR #6 merged after central workflow proof; PR #8 is now the open current-head runtime proof fixture | - -## Current policy - -1. Security evidence, review evidence, and mechanical merge/update automation are centralized through the organization `workflows` ruleset rule. -2. The central required workflows come from `.github`; repositories should not receive copied Strix, OpenCode, Noema, or scheduler workflow files only to satisfy this rollout. -3. GitHub Flow repositories are those whose default branch is `main` or `master`. -4. Git Flow repositories are those whose default branch is `develop`. -5. OpenCode remains responsible for review judgment and structured decisions. -6. GitHub Actions remains responsible for mechanical branch updates and merges. -7. A merge is acceptable only when the current head has required checks passing, distinct current-head OpenCode and Noema approvals, no unresolved review threads, and a clean or mergeable merge state. -8. Previous-head approvals or checks are not merge evidence. -9. Same-repository approved PRs should merge immediately when GitHub reports `CLEAN`; fork or external-head PRs are excluded from scheduler merge and auto-merge. - -## Evidence from this rollout - -- On 2026-08-28 21:43 KST, ruleset `21732164` was created with active enforcement for every non-default branch. Reproduction on an existing LineageWeave PR head and a new branch returned GH013 before either ref could emit the required workflow event. The ruleset was returned to `evaluate` mode at 21:49 KST; the audit now fails if this impossible all-ref contract is reactivated. - -- On 2026-06-30 08:33 KST, organization ruleset `18156473` was changed from an explicit repository-name list to `repository_name.include=["~ALL"]` while keeping `ref_name.include=["~DEFAULT_BRANCH"]` and the same three central required workflow paths from `.github@refs/heads/main`. -- On 2026-07-01 02:52 KST, ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. -- On 2026-07-01 06:30 KST, organization ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. -- On 2026-07-02 07:25 KST, organization ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the same three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. -- On 2026-07-11 11:30 KST, organization ruleset `18156473` was normalized to keep the five central required workflows, stale-review dismissal, last-pusher protection, and review-thread resolution while setting `required_approving_review_count=0` and `require_code_owner_review=false`. The merge gate remains current-head OpenCode approval plus required checks and scheduler evidence; the change removes self-authored/code-owner deadlocks that left approved PRs unable to merge. -- On 2026-07-13 21:10 KST, live inspection found that `sast-semgrep.yml` described itself as the central replacement for removed repository-local Semgrep jobs but was absent from ruleset `18156473`. The active ruleset was updated to require that workflow from `.github@refs/heads/main`, while preserving one approval, stale-review dismissal, last-push approval, and review-thread resolution. `scripts/ci/audit_central_required_workflows.py` and the scheduled ruleset audit now report each missing workflow, wrong source ref, or weakened review protection explicitly. -- On 2026-07-13 22:21 KST, the first main-branch ruleset audit proved that a repository `GITHUB_TOKEN` cannot read the organization-administration endpoint (`HTTP 403 Resource not accessible by integration`). The audit uses the least-privilege inherited-ruleset endpoint, logs `RULESET_SCOPE` for each enumerated repository, and validates the complete workflow and pull-request rule payload through `naruon`. The original public-only scope and its historical `.github`/`argos`/`noema` exclusions were superseded by the 2026-07-23 audit below. -- On 2026-07-13 22:37 KST, xtrmLLMBatchPython current-head evidence proved that Semgrep 1.169.0 reports zero blocking findings while retaining 23 source-suppressed results in raw SARIF. The central gate now logs the suppressed count, removes only SARIF results carrying explicit in-source suppressions before upload, and fails from the remaining SARIF finding count even when Semgrep's SARIF-mode exit code is zero. -- On 2026-07-16 14:18 KST, `ContextualWisdomLab/clearfolio#161` proved the independent reviewer on exact current head `4512fb9e9b56ab95df3acd85ebec2e6b849335a7`: `cwl-noema-review[bot]` submitted an App-authored `APPROVED` review whose body records the same Head SHA and cites the clean SARIF, dependency, test, and diff evidence. -- On 2026-07-23 06:35 KST, ruleset `18156473` was updated to require `.github/workflows/noema-review.yml`, making seven central required workflows while preserving exactly two approvals, stale-review dismissal, last-push approval, review-thread resolution, and merge/squash-only policy. The all-repository scope excludes only `.github`, `noema`, and private `IRT-bibliography-set`; `argos` now inherits the ruleset. The scheduled audit now enumerates every organization repository visible to its credential (`type=all`), rather than only public repositories, so the private exclusion and all other visible private-repository inheritance are verified. Existing open PRs may need a new PR event or branch update before GitHub creates the newly required Noema run. -- `.github` PR `#225` raised high reasoning effort for all reasoning-capable OpenCode review model definitions and merged at `50c6ef82f52af3eeb0e58c174902fc9855c36682`. -- `.github` PR `#226` stopped the merge scheduler from treating old deterministic fallback approval bodies as current-head approval evidence and merged at `57a1fa580731a0f76b31dcf29a597c5715dba2fd`. -- `.github` PR `#230` added changed-file candidates to merge-conflict guidance so `DIRTY` or `CONFLICTING` PRs name the first files to inspect instead of giving only generic conflict instructions. It merged at `0cab5c8d46e88c1a3f68ef3f71b5d44d971cd2ef`. -- `.github` PR `#232` removed the workflow-only deterministic approval fallback introduced by PR `#231`; model-pool exhaustion now stays on the fail-closed `REQUEST_CHANGES` path, and reasoning-capable OpenCode model candidates must have `reasoningEffort: high` before execution. It merged at `f545a9917933f8f81a76ea0044cbce0aae1ac5bd`. -- `.github` PR `#233` blocks false trivial approval reasons such as `Typo fix in documentation string` when current-head changed files include workflow, script/source, or test surfaces. It merged at `4ff660c8396b78a1b82aef8c316b26527864d450`. -- `.github` PR `#234` made approval-summary repair parse bullet-form changed-file evidence from bounded review logs, so changed-file evidence is not lost when the evidence section is rendered as a Markdown list. It merged at `da3a4a5788e7019229d66247c360b258b1a5b1f7`. -- `.github` PR `#235` changed the post-approval OpenCode merge-scheduler follow-up to prefer the workflow `github.token` for same-repository mechanical merge/update mutations, keeping secret/app fallbacks for cross-repository manual dispatch. It merged at `482b05c6c11d9da9895246406aca1c3bd8f6a691`. -- `.github` PR `#239` centralized the OpenCode reasoning-effort guard into `scripts/ci/assert_opencode_reasoning_effort.py`, reused it for the review model pool and failed-check diagnosis path, and merged at `2aa1fa36255a558bafca05567125ef7e44571976` after required OpenCode, Strix, Noema, coverage, and scheduler checks passed. -- `.github` PR `#242` added REST fallbacks for transient scheduler GraphQL read failures in open-PR and single-PR lookup paths, then merged at `0d2c6d9e7ae1bad947e7ee3629e2a412ac2ce248`. -- `.github` PR `#244` added the central `PR Review Autofix` worker and changed the fix scheduler to dispatch the central `.github` autofix worker by default while preserving explicit target-repository overrides. It merged at `4d2dd64028231b1154642bfe23b822fc3403e217`. -- `.github` PR `#246` hardened the OpenCode model pool after `pg-erd-cloud` PR `#393` exposed model exhaustion: full review policy is kept on disk behind a compact launcher prompt, context-window overflow skips same-model retries, additional cataloged tool-calling models are included, reasoning-capable candidates keep `reasoningEffort: high`, and the model pool now has a five-hour total retry budget. It merged at `f5f00b782ae4f7806f0e3197bf9b49c9c5a2cb91`. -- `.github` PR `#247` was closed without merge because its reviewed-merge-update fallback would have approved a current head from previous-parent approval evidence after model exhaustion. That path conflicts with the current fail-closed policy: model timeout, model-pool exhaustion, or missing usable control output must lead to retry, alternate model execution, or a source-backed request for changes, not deterministic approval. -- `.github` PR `#249` guarded the central PR Review Fix Scheduler so `CHANGES_REQUESTED` review states dispatch the central autofix worker only when the latest OpenCode review is on the current head, the merge state is `CLEAN` or `HAS_HOOKS`, and the review body does not indicate process-only blockers such as merge conflict, model-pool exhaustion, unresolved human review threads, failed checks, `coverage-evidence`, or failed Strix evidence. It merged at `dbd33b3a0384de0129aa082a210383188d012415` after current-head `coverage-evidence`, `strix`, `opencode-review`, `noema-review`, and `scan-pr-queue` all completed successfully. -- `.github` PR `#255` removed the remaining deterministic low-risk approval fallback from the OpenCode approval gate and changed `coverage-evidence` blocker handling to publish a `REQUEST_CHANGES` review event, producing the PR review state `CHANGES_REQUESTED`, instead of leaving only a failed check/log. It merged at `e2beae72b87a8817cd57f9f51bab3947353baa61`; the first current-head OpenCode run reached an `APPROVE` gate result but hit the OpenCode GitHub App installation rate limit while publishing the review, then a rerun published approval and native auto-merge completed. -- `.github` PR `#283` refreshed the central OpenCode model configuration so every reasoning-capable review candidate sets `reasoning=true`, `options.reasoningEffort: high`, and `variants.high.reasoningEffort: high`; non-reasoning fallback candidates remain available without a false effort claim. It merged at `ef9950e6b55bf943c0295e1df3e34c94210d21cc`. -- After PR `#255` merged, `ContextualWisdomLab/bandscope` PRs `#493`, `#494`, `#495`, and `#500` were rechecked for branch freshness. Merge simulation against `develop` found real conflicts rather than update-branch candidates: `#493` conflicts in `apps/desktop/src/App.tsx` plus the design-system docs, while `#494`, `#495`, and `#500` conflict in `docs/design-system/README.md`, `docs/design-system/component-contract.md`, and `docs/design-system/figma-to-code-workflow.md`. Each PR received a corrected conflict-resolution comment with the exact file list and merge/rebase repair commands. -- `ContextualWisdomLab/aFIPC` PR `#78` is no longer a target-coverage gap. It merged after current-head central `coverage-evidence`, `opencode-review`, `strix`, and `scan-pr-queue` checks all passed on head `b1ddafced86302f461e95259699f1efde5ec87c9`; the OpenCode review approved the same head on 2026-06-30 06:02:55Z. -- `ContextualWisdomLab/pg-erd-cloud` PR `#393` removed the repo-local `pr-review-autofix.yml` worker after the central autofix worker merged. - The first OpenCode run on head `9d8eed5be47670b1b46f413295d9a6044d7327b2` exhausted the older model pool and requested changes. - After `.github` PR `#246` merged, central OpenCode run `28485070313` approved the same head and the PR merged at `1e0d6a3dda5ea9afcd74dcd8380689672e1c8ef1` on 2026-07-01 00:33:50Z. - Live default-branch content lookup returned 404 for `.github/workflows/pr-review-autofix.yml` after merge. -- Live non-fork inventory on 2026-07-02 18:15 KST found 17 public non-fork repositories, inherited ruleset `18156473` on `kaefa` and `waf-ids-ai-soc`, and no default-branch copies of `opencode-review.yml`, `strix.yml`, or `pr-review-merge-scheduler.yml` outside `.github`. -- `ContextualWisdomLab/waf-ids-ai-soc` PR `#6` merged at `e1c0a85fd4a8e6dd67039be43eb7f659fec22abd` after central required workflow proof on head `43b62b5f347d1532c81b5ae38d8e41b4494fd486`; PR `#8` current head `48d8b56a0f995829fc95de4fed129d1c33aaadff` is now the open runtime proof fixture with central and local Rust checks queued at the 2026-07-02 18:15 KST refresh. -- `ContextualWisdomLab/kaefa` inherits ruleset `18156473`, but PR `#60` current head `13c9089855fcdd34391173560ccf6935bac1eebe` showed only repo-local R-CMD-check, dependency-review, and CodeQL signals in status rollup. Treat this as a runtime proof gap until a new PR event or manual dispatch proves central OpenCode, Strix, and scheduler checks on a kaefa current head. -- `.github` scheduler default merge mode is now `direct_or_auto`: approved same-repository `CLEAN` PRs request immediate guarded merge, approved non-clean same-repository PRs can queue native auto-merge, and fork or external-head PRs are left for maintainer merge. -- OpenCode approval runs the trusted central merge scheduler script directly with `pr_number` and `max_prs=1`, so the just-reviewed PR is inspected immediately even when organization required workflows are not repo-local `workflow_dispatch` targets. -- `.github` PR `#74` changed OpenCode review model order to DeepSeek R1 first and added a catalog fallback pool. -- `.github` PR `#75` removed the Strix finding against the scheduler command wrapper by using `subprocess.run(..., check=True)` and preserving the existing scrubbed failure contract. -- `.github` main Strix run `28218982899` passed after PR `#75` merged. -- `.github` PR `#77` merged the central OpenCode required-workflow path. -- `.github` PR `#77` same-head OpenCode proof run `28224085121` passed coverage evidence, CodeGraph initialization, bounded evidence preparation, model review, review comment publication, and approval-gate publication on head `59a8da0b2f56b862f6c5a0c69885f4045d6dc732`. -- `.github` PR `#77` central Strix required workflow run `28223698075` passed on the same head before merge. -- Organization ruleset `18156473` was renamed to `CWL Central required workflows` and required `.github/workflows/strix.yml` and `.github/workflows/opencode-review.yml` from `.github@main` SHA `6440d493816f8a4d66e32f2e5e8e6a9156d7f488`. -- `.github` PR `#79` merged the central scheduler `pull_request_target` path and PR-scoped `--pr-number` lookup. -- `.github` PR `#79` second current-head proof passed coverage evidence in 10s, Strix in 8m33s, and OpenCode review in 8m57s on head `17c62f3809c57ca4b1a9a63e14f325c9f2a1acdb`. -- Organization ruleset `18156473` now requires `.github/workflows/strix.yml`, `.github/workflows/opencode-review.yml`, and `.github/workflows/pr-review-merge-scheduler.yml` from `.github@main` SHA `807254a04efafd5f806e0f70cb067ecf050cfd11`. -- `.github` PR `#85` installed target repository `requirements.txt` before Python coverage evidence, so central coverage measurement can run repo tests that require project dependencies. -- `.github` PR `#88` hardened the OpenCode output normalizer so the Python normalizer is part of the trusted approval gate path. -- `.github` PR `#94` hardened the central OpenCode prompt and generated review DAG contract so Mermaid labels are quoted and render safely. -- `.github` PR `#95` blocks OpenCode approvals that claim no source, test, or executable changes when exact changed-file evidence lists workflow, script, source, or test files. -- On 2026-06-28 20:09 KST, ruleset `18156473` was re-pinned to `.github@main` SHA `531482764986bf7da98c1317d59e6e51e7c61d02` for all three required workflow paths. -- `ContextualWisdomLab/naruon` reports inherited active ruleset `18156473` with all three required workflow paths, proving target-repository inheritance after the scheduler ruleset update. -- `ContextualWisdomLab/ContextualWisdomLab.github.io` PR `#25` merged the thin central scheduler caller and repository-local bootstrap fixes. Its main Strix run `28217860369` passed. -- The organization ruleset API reports the central required workflows ruleset as `active` and inherited by each public non-fork target repository. -- `.github` PR `#100` added required-workflow job rerun support and cancels older same-PR OpenCode runs before retrying the current head. Local verification on head `3c62c37a4deabdb0c6ed4ddf0951c1987f09866b`: `pytest -q` passed 38 tests, `coverage report --fail-under=100` reported 100%, `interrogate --fail-under=100 .` reported 100%. -- `.github` PR `#100` merged at 2026-06-29 05:45 KST with merge commit `81408f3dbe0a3c43dc4b76133f72a5e314df8a10`. A follow-up admin check should verify organization ruleset `18156473` is no longer pinned to `refs/heads/codex/rerun-required-opencode-job`. -- The earlier 2026-06-29 KST `aFIPC` PR `#78` target-coverage gap is closed. A later current-head run on `b1ddafced86302f461e95259699f1efde5ec87c9` produced central `coverage-evidence`, `opencode-review`, `strix`, and `scan-pr-queue` success before merge. -- `.github` PR `#136` changed approved stale PR handling so `BEHIND` branches are updated before failed-check or `ACTION_REQUIRED` decisions disable auto-merge. -- `.github` PR `#137` made the central `PR Review Fix Scheduler` target-repository aware through `workflow_call`, `workflow_dispatch`, schedule, and `.github` repository variables. `.github` variables currently target `ContextualWisdomLab/pg-erd-cloud` on `main`. The follow-up central autofix worker makes `ContextualWisdomLab/.github` the default `autofix_repository`, so target repositories no longer need to copy a full `pr-review-autofix.yml` worker to participate. -- `.github` PR `#138` added compare-API branch freshness evidence so approved PRs with auto-merge enabled can still receive `update-branch` when GitHub reports `BLOCKED` but the base branch is ahead. Local verification passed `pytest -q`, scheduler self-test, `py_compile`, 100% coverage, 100% docstring coverage, `actionlint`, `bash -n`, and `git diff --check`. -- `.github` PR `#140` extended `update-branch` handling to PRs where auto-merge is already enabled even if the scheduler cannot find a current-head OpenCode approval node, so queued auto-merge PRs with failed checks can still be refreshed when compare evidence shows the base branch is ahead. Local verification passed `pytest -q`, `coverage report` at 100%, `interrogate` at 100%, `py_compile`, `bash -n`, and `git diff --check`. -- `.github` PR `#145` treats compare API `status: behind` as branch-staleness evidence even when `behind_by` is missing or zero, so an auto-merge-enabled PR with failed checks and a visible GitHub "Update branch" action requests `update_branch` before disabling auto-merge. It merged at 2026-06-29 23:14 KST with merge commit `1ec0f3dcc7250fdf4a5a3ec6c26feaa98cce4f48`. -- Live dry runs on 2026-06-30 00:40 KST found update-branch candidates in `.github` PR `#147` and `naruon` PR `#803`. The follow-up scheduler trigger change runs the central queue scan after base-branch pushes and `auto_merge_enabled` events, so those UI-visible stale-branch states are not left waiting only for the periodic schedule. -- `.github` PR `#151` added protected base-branch `push` triggers and the `auto_merge_enabled` PR event to the central scheduler, then merged at 2026-06-30 00:56 KST with merge commit `00018f7783522447a71acd08a946e3504e18ff74`. The merge created push-triggered scheduler run `28385177585`, proving the new trigger path is registered; the job remained queued because runner assignment was still pending. -- The earlier compare API `behind` handling is superseded by the current immediate-action order: `CLEAN` and current-head approved PRs merge before update-branch, failed or `ACTION_REQUIRED` checks are surfaced before any update attempt, and only approved `BEHIND` PRs without current-head check blockers request `update-branch` through the configured scheduler mutation credential. -- `.github` PR `#146` taught central OpenCode `coverage-evidence` to discover nested requirements-only Python test projects such as `backend/requirements.txt` plus `backend/tests`, install those requirements, and run tests from that project directory. It merged at 2026-06-29 23:24 KST with merge commit `0393bc1c48b80597d6d35c336aca43aee18e22b9`. -- `.github` PR `#149` tightened the central OpenCode model-failure path and merged at 2026-06-30 00:26 KST with merge commit `919b83faf29237803cfdd0cfd6febbe5ae1a8a3c`. The follow-up commit `6fdffe43b50a2246b3db2790a0ab532618a89c2b` fixed the fallback approval path so pending-check and human-thread evidence are written to real temporary files instead of empty paths. Local verification passed `pytest -q`, `coverage report --fail-under=100`, `interrogate --fail-under=100`, `actionlint -shellcheck=`, targeted OpenCode quick-gate assertions, `bash -n`, and `git diff --check`; the full quick-gate script exceeded the local 300s timeout in this environment. -- Organization ruleset `18156473` previously targeted all live non-fork repositories, including private `aFIPC`, `linux-cluster-ops`, and `xtrmLLMBatchPython`; this has been superseded by the all-repository `~ALL` condition above. -- `ContextualWisdomLab/semantic-data-portal` PR `#3` removed repo-local OpenCode, Strix, and scheduler workflows; the default branch now has no `.github/workflows` directory. -- `ContextualWisdomLab/pg-erd-cloud` PR `#361` removed the repo-local `pr-review-fix-scheduler.yml` wrapper after central `.github` gained target repository support. It merged at 2026-06-29 22:40 KST with merge commit `21cbc14b21d59ac28ac789de58502816cc8df6ad`; live default-branch content lookup returned 404 for that wrapper path after merge. -- `ContextualWisdomLab/naruon` classic branch protection no longer requires direct `strix` or `opencode-review` status checks on `develop`; after deletion, `branches/develop/protection/required_status_checks` returns `404 Required status checks not enabled`, while org ruleset `18156473` remains `active` and still targets `naruon`. -- `ContextualWisdomLab/naruon` PR `#852` rewrites `backend/tests/test_release_governance.py` and `docs/development/merge-gate-policy.md` to make the central scheduler the contract, then deletes the repo-local `pr-review-merge-scheduler.yml`. The first current-head central `coverage-evidence` failed because nested `backend/requirements.txt` was not installed; `.github` PR `#146` fixed that central path. PR `#852` was pushed to head `2c8257ce0d02838b80650997d65e85569f4ab27f` to generate fresh required workflows from the updated central main. The stale OpenCode `CHANGES_REQUESTED` review `4592643416` on previous head `0f103836f15d9055c4ed85152f925a6e9514adb2` was dismissed on 2026-06-30 00:25 KST; the PR now requires fresh current-head OpenCode/coverage evidence and still has queued `coverage-evidence`. - -## Good patterns to keep - -- `naruon`: separates PR Governance, OpenCode review, Strix evidence, and application CI into explicit checks. -- `.github`: centralizes reusable workflow logic and review/merge scheduler code. -- `pg-erd-cloud`: its previous repo-local autofix worker was folded into the central `PR Review Autofix` worker and removed from the repository by PR `#393`; keep only repository-specific application and security checks locally. -- `ContextualWisdomLab.github.io`: thin caller pattern is acceptable for repository-local workflows only when GitHub does not offer an organization-level control. It should not be the default rollout mechanism. - -## Risks and follow-up - -- Existing open PRs may need a new push or base update before the latest required workflow SHA appears on their current head. -- The central OpenCode workflow now retries DeepSeek R1, DeepSeek V3, GPT-5, and a catalog fallback pool. Keep model/tooling failures out of PR comments unless there is a source-backed failed-check diagnosis. -- The central OpenCode config includes a read-only `code-reviewer` subagent for focused review passes. The subagent may read, grep, glob, and run safe local verification commands, but it must not edit files, stage changes, commit, push, install dependencies, mutate branches, or touch production state. -- OpenCode execution evidence must be sandboxed in the CI workspace or an isolated temporary directory, with a credential-scrubbed environment by default and no persistent mutation outside test caches or scratch files. Prefer `python3 scripts/ci/sandboxed_verify.py --repo-root -- ` when the central helper is available, and cite its `SANDBOXED_VERIFY_RESULT` line. When repo-native verification legitimately needs network access or GitHub Secrets, pass only the needed names with `--allow-env`, record `--network required`, and explain it with `--evidence-note` without printing secret values. The helper does not replace existing bash, task, webfetch, websearch, lsp, CodeGraph, DeepWiki, Context7, or web_search review policy. If a verification cannot be sandboxed without changing the result, the review must say so instead of presenting an unsafe run as evidence. -- Web application reviews should run backend, frontend, and repository-native E2E checks together through `python3 scripts/ci/sandboxed_web_e2e.py --repo-root --backend-cmd --frontend-cmd --e2e-cmd ` when those contracts exist, then cite `SANDBOXED_WEB_E2E_RESULT`. If backend/frontend/E2E/readiness contracts are missing, the review must name the gap instead of treating unit or lint evidence as full E2E proof. -- Bounded OpenCode evidence includes `Review execution contracts`, which inventories runtime matrices, package manifests, test, coverage, docstring, E2E, lint, security, Docker, and unpackaged-source gaps before the model chooses verification commands. -- Generated OpenCode review DAGs must use quoted Mermaid labels such as `A["text"]`; unquoted labels with spaces, punctuation, parentheses, or file counts can fail to render. -- OpenCode approval summaries must not contradict exact changed-file evidence by saying no source, test, or executable files changed when workflow, script, source, or test files are present. -- OpenCode approval reasons must not trivialize material workflow, script/source, or test changes as docs-only, typo-only, or string-only changes. The normalizer now rejects those approvals before publication. -- Same-repository post-approval merge/update follow-up should use the workflow `github.token` first so the mechanical actor is `github-actions[bot]`; cross-repository manual dispatch may still fall back to configured secrets or the OpenCode app token when the workflow token cannot mutate the target repository. -- Do not copy central Strix, OpenCode, merge scheduler, fix scheduler, or autofix worker workflows into repositories. Repository-local application CI and security CI may remain when they are not substitutes for the central workflows. -- The central autofix worker is for source-actionable current-head review findings. It must not treat model-pool exhaustion, missing approval evidence, unresolved human threads, failed checks, `coverage-evidence`, Strix failures, `DIRTY`, or `CONFLICTING` merge states as code-autofix requests; those states need retry, failed-check explanation, branch update, or conflict guidance instead. -- `pg-erd-cloud` no longer has a repository-local `pr-review-autofix.yml` worker on its default branch. Live default-branch workflows after PR `#393` are `ci.yml`, `codeql-backfill.yml`, `codeql.yml`, `dependency-review.yml`, and `scorecard.yml`. -- Some repositories use classic branch protection while others use rulesets. Normalize branch protection into rulesets without removing repository-specific required application checks. -- Existing PRs may not show newly inherited required workflows until a new PR event or branch update occurs, even though the org ruleset now uses the all-repository condition. +- The workflows rule must set `do_not_enforce_on_create=true`; proposal-branch creation must remain possible. +- The central ruleset must contain only the declared `workflows`, `pull_request`, `deletion`, and `non_fast_forward` rule types. An added `creation` or other undeclared rule is governance drift because it can defeat the proposal-branch transition or add an unreviewed protected-branch constraint. + +The scheduled audit reads the inherited organization ruleset through a consumer repository, enumerates repository inheritance, reads the owner `.github` repository ruleset, and reads the stacked OpenCode ruleset. Once a payload is fetched successfully, drift in one payload must not suppress the remaining available audits; all fetched drift reasons are emitted before the job fails. API/read failures still fail immediately because the missing payload cannot be audited. + +## Solo-maintainer review policy + +ContextualWisdomLab currently has one human maintainer. A generic positive human-approval count is therefore structurally unsatisfiable for a maintainer-authored PR when self-approval is prohibited. Model and service identities provide deterministic/advisory evidence; they are not independent human reviewers and must not be counted as such. + +The current protected-PR policy is: + +- `required_approving_review_count = 0`; +- `require_last_push_approval = false`; +- `required_reviewers = []` unless governance later identifies a genuinely independent human authority; +- `dismiss_stale_reviews_on_push = true`; +- `required_review_thread_resolution = true`; +- only merge and squash are allowed merge methods; +- deletion and non-fast-forward protection remain enabled; and +- routine bypass actors are forbidden. + +No bot approval, service account, broadened token, self-approval, or `OrganizationAdmin/always` bypass may be used to simulate human independence. Emergency control-plane repair belongs to the separately governed, time-bounded, auditable break-glass process. + +Issue `#772` is the policy decision, issue `#1351` tracks declared-versus-live drift, issue `#1340` owns routine administrator-bypass removal, and PR `#1176` is the canonical audit/test writer. Consumer repositories must not add local shims to work around this central policy. + +## Current live drift + +As of the 2026-09-01 live reads used by PR `#1176`, organization ruleset `18156473` still requires one approving review and still exposes `OrganizationAdmin/always` bypass. The owner `.github` repository ruleset `17921150` already reports zero approving reviews and no last-push requirement but still permits `rebase` and retains routine administrator bypass. These are live settings defects, not permission to weaken the audit. + +Do not claim the rulesets are reconciled until an authorized settings mutation is followed by a fresh full-payload read. A passing source-level auditor only proves the declared contract, not the live GitHub configuration. + +## Model-review gates + +### OpenCode + +`.github/workflows/opencode-review.yml` is a required deterministic review workflow. Its trusted `pull_request_target` surface is metadata-only; PR-controlled source is reviewed as data through the protected dispatch workflow and must not be executed with privileged credentials. Current-head evidence, model output, coverage/source inspection, review publication, and exact repository/base/head binding remain fail-closed. Draft PR handling and model-exhaustion behavior must follow the current central implementation; old approval bodies or predecessor heads never transfer. + +OpenCode review evidence is a model/control-plane gate, not a human approval for `required_approving_review_count`. + +### Noema + +`.github/workflows/noema-review.yml` is a separate required deterministic review workflow backed by the organization-owned Noema review identity and centrally versioned judgement path. It provides independent model evidence and may publish a PR review, but that App-authored review is not represented as a second human maintainer. Missing credentials, missing exact-head evidence, or a failed judgement remain visible failures rather than skipped success. + +The standalone `ContextualWisdomLab/noema` judgement plane remains outside the privileged workflow import boundary unless a separately reviewed integration transfers that responsibility. + +## Security and dependency review + +The required `security-scan.yml` and `sast-semgrep.yml` workflows remain base-ref agnostic so they also observe stacked PRs even though protected-ref rules are a separate control. + +Dependency Review is authoritative only when the exact `BASE_SHA...HEAD_SHA` comparison succeeds with transport success and HTTP `200`, after which the pinned `actions/dependency-review-action` actually executes. HTTP 403/404, malformed/empty status, transport failure, timeout, or missing authoritative comparison evidence fail closed. OSV, Trivy, Scorecard, CodeQL, SAST, or secret-scanning evidence are useful sibling controls but are not semantic substitutes for Dependency Review. Current public-repository HTTP 403 availability is tracked by issue `#810`; product repositories must not add a local fail-open shim. + +## Scheduler and merge boundary + +`.github/workflows/pr-review-merge-scheduler.yml` owns mechanical merge/update orchestration after policy evidence exists. For each candidate PR it must re-fetch the exact current head, base, reviews, unresolved threads, required checks, security evidence, and merge state. Stale, predecessor, queued, pending, skipped-required, cancelled, neutral, model-only, or absent evidence is non-passing. + +The scheduler may directly or automatically merge only through ordinary branch protection when all then-current deterministic requirements are satisfied. Fork/external heads stay reviewable but are excluded from unattended branch mutation. A `DIRTY`/conflicting PR requires source repair; queued auto-merge is not a conflict resolver. + +The organization queue sweep is a heartbeat, not an evidence substitute. It may retry eligible current-head work, but it cannot manufacture approval, downgrade a failed check, transfer evidence between heads, or use a repository-local workaround for a central defect. + +## Stacked PR ruleset + +Ruleset `21732164`, `CWL Stacked OpenCode required workflow`, remains `evaluate`-mode evidence over non-default branches with `ref_name.include=["~ALL"]` and `ref_name.exclude=["~DEFAULT_BRANCH"]`, requiring only the central OpenCode workflow and exempting branch creation. Active enforcement across every non-default ref is prohibited because GitHub can evaluate the ref update before a new exact-head required-workflow run can exist, deadlocking both branch creation and later stack fixes. + +Stacked PRs therefore use exact-head OpenCode evidence and ordinary PR procedure while the organization develops a target-ref-scoped enforcement design. Additional rule types in the stacked ruleset are drift. + +## Owner repository ruleset + +Repository ruleset `17921150`, `Lock default branch`, protects `ContextualWisdomLab/.github` itself. The declared contract is default-branch-only scope, zero generic approvals, no last-push requirement, no required reviewers, stale-review dismissal, thread resolution, merge/squash only, deletion protection, non-fast-forward protection, and no bypass actors. Any extra rule type is rejected unless governance first documents and tests an explicit allowed extension. + +## Validation procedure + +For every policy change: + +1. Re-read live organization and repository rulesets before changing source or settings. +2. Add a failing regression for the exact drift class before changing auditor/workflow behavior. +3. Run the permanent ruleset suites on the exact writer head. A temporary branch-only proof workflow may be used when ordinary hosted execution does not exercise the new path, but it must use least privilege and immutable dependencies. +4. Do not call a queued, cancelled, skipped, neutral, stale, predecessor, or missing run GREEN. +5. Remove temporary proof workflows only after terminal-success evidence exists for the then-current exact head and their durable regressions no longer depend on the temporary file. +6. Apply the authorized live settings change without routine bypass, synthetic reviewers, self-approval, force push, or direct protected-branch write. +7. Re-read the complete live payload after mutation and compare it with the executable audit contract. +8. Re-use an unchanged deterministic-GREEN consumer PR as a canary. Orgmetra PR `#88` is suitable while it remains unchanged and otherwise clean. +9. Merge only through ordinary protection after all current-head deterministic evidence and review-thread requirements are terminal successful. + +## Traceability + +- `.github#772` — solo-maintainer protected-PR policy decision. +- `.github#1351` — declared-versus-live central review-policy drift. +- `.github#1340` — routine administrator bypass / break-glass boundary. +- `.github#1200` — default-branch scope and branch-create transition. +- `.github#810` — Dependency Review fail-closed contract and current availability incident. +- `.github#624` — OpenCode control-plane work. +- `.github#1327` — Strix control-plane work. +- `.github#1399` — shared Noema sidecar/control-plane work. +- `.github` PR `#1176` — canonical executable ruleset audit/test writer. + +This guide is intentionally current-state oriented. Historical rollout tables, old approval counts, transient exact heads, and superseded experiments are preserved by Git history and the linked issues/PRs rather than being presented as present operator instructions. \ No newline at end of file From 49ebfaefeca7a40bcc1f52c362d6d0504a6330e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:31:29 +0900 Subject: [PATCH 039/154] test(governance): pin focused proof interpreter --- tests/test_ruleset_audit_completeness_regression.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_audit_completeness_regression.py b/tests/test_ruleset_audit_completeness_regression.py index 36d50cd055..8ae16db0e4 100644 --- a/tests/test_ruleset_audit_completeness_regression.py +++ b/tests/test_ruleset_audit_completeness_regression.py @@ -110,6 +110,15 @@ def test_live_audit_collects_all_available_ruleset_drift_before_failing() -> Non assert 'if [[ "$audit_status" -ne 0 ]]; then' in workflow +def test_focused_contract_pins_python_before_hash_verified_install() -> None: + """The disposable proof lane must not depend on the hosted default Python.""" + workflow = (REPO_ROOT / ".github/workflows/solo-maintainer-ruleset-contract.yml").read_text(encoding="utf-8") + + assert "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" in workflow + assert 'python-version: "3.14"' in workflow + assert workflow.index("actions/setup-python@") < workflow.index("python -m pip install") + + def test_rollout_guide_declares_solo_maintainer_review_policy() -> None: """Operator documentation must not reintroduce a fictional second human approval.""" rollout = (REPO_ROOT / "docs/org-required-workflow-rollout.md").read_text(encoding="utf-8") @@ -117,4 +126,4 @@ def test_rollout_guide_declares_solo_maintainer_review_policy() -> None: assert "required_approving_review_count = 0" in rollout assert "require_last_push_approval = false" in rollout assert "The org's two-reviewer merge rule" not in rollout - assert "two distinct approvals" not in rollout + assert "two distinct approvals" not in rollout \ No newline at end of file From 1bd407d4211dbd1c48e87661f0cb0cedb5cabe7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:31:45 +0900 Subject: [PATCH 040/154] fix(governance): restore Python 3.14 proof runtime --- .github/workflows/solo-maintainer-ruleset-contract.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 3779dfc591..b229870e99 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -25,6 +25,11 @@ jobs: with: persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - name: Execute focused ruleset contract env: PIP_DISABLE_PIP_VERSION_CHECK: "1" From 76516f40b50e5899f5e4b6f4c588cdbc8bc76ca9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:00:33 +0900 Subject: [PATCH 041/154] fix(governance): run focused contract on explicit runner --- .github/workflows/solo-maintainer-ruleset-contract.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index b229870e99..892f81635c 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -11,6 +11,7 @@ on: - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_solo_maintainer_ruleset_policy.py" - "tests/test_ruleset_audit_completeness_regression.py" + - "tests/test_pr_governance_audit_contract.py" permissions: contents: read @@ -18,7 +19,7 @@ permissions: jobs: focused-contract: name: Focused solo-maintainer ruleset contract - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout exact pushed head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -50,4 +51,5 @@ jobs: python -m pytest -q \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_solo_maintainer_ruleset_policy.py \ - tests/test_ruleset_audit_completeness_regression.py \ No newline at end of file + tests/test_ruleset_audit_completeness_regression.py \ + tests/test_pr_governance_audit_contract.py From af04bacdc6196b0541a090e403c08991fb668192 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:01:14 +0900 Subject: [PATCH 042/154] docs(governance): preserve regression fixture history --- docs/org-required-workflow-rollout.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 4e5be81978..bae37bb79c 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -4,7 +4,11 @@ Updated: 2026-09-01 KST ## Purpose -This document is the operator-facing current-state contract for organization-wide pull-request admission. Historical rollout experiments, exact transient PR heads, runner IDs, and superseded review-count policies remain available in Git history and the linked governance issues; they are intentionally not repeated here because stale operational snapshots can recreate a closed governance defect. +This document is the operator-facing current-state contract for organization-wide pull-request admission. Historical rollout experiments, exact transient PR heads, runner IDs, and superseded review-count policies remain available in Git history and the linked governance issues; they are intentionally not presented as current operator instructions because stale operational snapshots can recreate a closed governance defect. + +## Historical regression fixtures (non-normative) + +The following closed or historical observations are retained only because executable regression tests use them as fixtures for central-governance coverage. They are evidence history, not present-day operating policy: the rollout inventory that established central coverage comprised **17 public non-fork repositories**, including `kaefa` and `waf-ids-ai-soc`. For `aFIPC`, PR `#78` is no longer a target-coverage gap. During that historical rollout, current PR #60 lacked central check runs; PR #6 merged after central workflow proof; and PR #8 is now the open current-head runtime proof fixture for the recorded case. Any live decision must still use fresh exact-head checks, reviews, rulesets, and current repository state rather than these fixtures. ## Canonical organization ruleset @@ -118,4 +122,4 @@ For every policy change: - `.github#1399` — shared Noema sidecar/control-plane work. - `.github` PR `#1176` — canonical executable ruleset audit/test writer. -This guide is intentionally current-state oriented. Historical rollout tables, old approval counts, transient exact heads, and superseded experiments are preserved by Git history and the linked issues/PRs rather than being presented as present operator instructions. \ No newline at end of file +This guide is intentionally current-state oriented. Historical rollout tables, old approval counts, transient exact heads, and superseded experiments are preserved by Git history and the linked issues/PRs rather than being presented as present operator instructions. From 3156622b475965293b46f5db090c58878b91d8a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:03:42 +0900 Subject: [PATCH 043/154] ci(governance): allow exact-head focused redispatch --- .github/workflows/solo-maintainer-ruleset-contract.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml index 892f81635c..63698dbffd 100644 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ b/.github/workflows/solo-maintainer-ruleset-contract.yml @@ -12,6 +12,7 @@ on: - "tests/test_solo_maintainer_ruleset_policy.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_pr_governance_audit_contract.py" + workflow_dispatch: permissions: contents: read From 74c01484dab6cc9d84a137f89d2c7ac8473bf5b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:11:53 +0900 Subject: [PATCH 044/154] test(governance): reproduce malformed merge-method audit crash --- ...t_ruleset_merge_method_shape_regression.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test_ruleset_merge_method_shape_regression.py diff --git a/tests/test_ruleset_merge_method_shape_regression.py b/tests/test_ruleset_merge_method_shape_regression.py new file mode 100644 index 0000000000..ac0d61fa70 --- /dev/null +++ b/tests/test_ruleset_merge_method_shape_regression.py @@ -0,0 +1,123 @@ +"""Fail-closed regression for malformed ruleset merge-method payloads.""" + +from __future__ import annotations + +from copy import deepcopy + +import pytest + +from scripts.ci import audit_central_required_workflows as audit + + +def _review_parameters() -> dict[str, object]: + return { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "required_reviewers": [], + "require_extra_approval_for_unattributed_changes": True, + "allowed_merge_methods": ["merge", "squash"], + } + + +def _central_payload() -> dict[str, object]: + return { + "id": audit.RULESET_ID, + "name": audit.RULESET_NAME, + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "repository_name": { + "include": ["~ALL"], + "exclude": sorted(audit.EXPECTED_EXCLUSIONS), + }, + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + { + "type": "workflows", + "parameters": { + "do_not_enforce_on_create": True, + "workflows": [ + { + "repository_id": audit.SOURCE_REPOSITORY_ID, + "path": path, + "ref": audit.SOURCE_REF, + } + for path in audit.REQUIRED_WORKFLOW_PATHS + ], + }, + }, + {"type": "pull_request", "parameters": _review_parameters()}, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def _repository_payload() -> dict[str, object]: + return { + "id": audit.REPOSITORY_RULESET_ID, + "name": audit.REPOSITORY_RULESET_NAME, + "target": "branch", + "source_type": "Repository", + "source": audit.REPOSITORY_RULESET_SOURCE, + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}, + }, + "rules": [ + {"type": "pull_request", "parameters": _review_parameters()}, + {"type": "deletion"}, + {"type": "non_fast_forward"}, + ], + } + + +def _set_allowed_merge_methods(payload: dict[str, object], value: object) -> None: + rules = payload["rules"] + assert isinstance(rules, list) + review_rule = next( + rule for rule in rules if isinstance(rule, dict) and rule.get("type") == "pull_request" + ) + parameters = review_rule["parameters"] + assert isinstance(parameters, dict) + parameters["allowed_merge_methods"] = value + + +@pytest.mark.parametrize( + "malformed", + [None, 7, "merge", {"merge": True}, ("merge", "squash")], +) +def test_central_audit_reports_malformed_merge_method_shape_without_raising( + malformed: object, +) -> None: + payload = deepcopy(_central_payload()) + _set_allowed_merge_methods(payload, malformed) + + errors = audit.audit_ruleset(payload) + + assert "only merge and squash may be allowed merge methods" in errors + + +@pytest.mark.parametrize( + "malformed", + [None, 7, "merge", {"merge": True}, ("merge", "squash")], +) +def test_repository_audit_reports_malformed_merge_method_shape_without_raising( + malformed: object, +) -> None: + payload = deepcopy(_repository_payload()) + _set_allowed_merge_methods(payload, malformed) + + errors = audit.audit_repository_ruleset(payload) + + assert "repository ruleset must allow only merge and squash" in errors + + +def test_valid_merge_method_list_remains_accepted() -> None: + assert audit.audit_ruleset(_central_payload()) == [] + assert audit.audit_repository_ruleset(_repository_payload()) == [] From 9b5d822050f32ddc144eede91e3bd802f6a02cc6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:12:12 +0900 Subject: [PATCH 045/154] test(governance): reject malformed merge methods --- tests/test_solo_maintainer_ruleset_policy.py | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py index bc5d426622..f05f22498f 100644 --- a/tests/test_solo_maintainer_ruleset_policy.py +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -84,6 +84,12 @@ def _repository_ruleset_payload() -> dict: } +def _review_parameters(payload: dict) -> dict: + """Return the unique pull-request rule parameters from ``payload``.""" + review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") + return review_rule["parameters"] + + def test_central_ruleset_accepts_zero_approvals_without_last_push_approval() -> None: """A one-human fleet must not require an approval its sole author cannot give.""" assert audit.audit_ruleset(_central_ruleset_payload()) == [] @@ -97,8 +103,7 @@ def test_repository_ruleset_accepts_zero_approvals_without_last_push_approval() def test_central_ruleset_rejects_synthetic_required_reviewer() -> None: """A named reviewer cannot manufacture independence in a one-human fleet.""" payload = _central_ruleset_payload() - review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") - review_rule["parameters"]["required_reviewers"] = [ + _review_parameters(payload)["required_reviewers"] = [ {"reviewer_id": 1234, "reviewer_type": "User"} ] @@ -110,11 +115,28 @@ def test_central_ruleset_rejects_synthetic_required_reviewer() -> None: def test_repository_ruleset_rejects_synthetic_required_reviewer() -> None: """The owner repository cannot reintroduce the same deadlock by reviewer identity.""" payload = _repository_ruleset_payload() - review_rule = next(rule for rule in payload["rules"] if rule["type"] == "pull_request") - review_rule["parameters"]["required_reviewers"] = [ + _review_parameters(payload)["required_reviewers"] = [ {"reviewer_id": 1234, "reviewer_type": "User"} ] assert audit.audit_repository_ruleset(payload) == [ "repository solo-maintainer ruleset must not configure required reviewers" ] + + +def test_central_ruleset_rejects_malformed_allowed_merge_methods() -> None: + """Malformed API payloads must report drift rather than abort central auditing.""" + expected = ["only merge and squash may be allowed merge methods"] + for malformed in (None, 7, "merge", {"merge": True, "squash": True}): + payload = _central_ruleset_payload() + _review_parameters(payload)["allowed_merge_methods"] = malformed + assert audit.audit_ruleset(payload) == expected + + +def test_repository_ruleset_rejects_malformed_allowed_merge_methods() -> None: + """Malformed API payloads must report drift rather than abort repository auditing.""" + expected = ["repository ruleset must allow only merge and squash"] + for malformed in (None, 7, "merge", {"merge": True, "squash": True}): + payload = _repository_ruleset_payload() + _review_parameters(payload)["allowed_merge_methods"] = malformed + assert audit.audit_repository_ruleset(payload) == expected From b3c2f6df9e5cd7ecc3a3fa1e7b88628a5b3fa663 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:12:31 +0900 Subject: [PATCH 046/154] ci(governance): repair malformed merge-method finding on exact head --- .../repair-pr1176-merge-method-shape.yml | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/repair-pr1176-merge-method-shape.yml diff --git a/.github/workflows/repair-pr1176-merge-method-shape.yml b/.github/workflows/repair-pr1176-merge-method-shape.yml new file mode 100644 index 0000000000..820ac14430 --- /dev/null +++ b/.github/workflows/repair-pr1176-merge-method-shape.yml @@ -0,0 +1,96 @@ +name: Repair PR1176 merge-method shape audit + +on: + push: + branches: [fix/stacked-pr-central-required-workflows] + paths: + - ".github/workflows/repair-pr1176-merge-method-shape.yml" + +permissions: + contents: write + +jobs: + repair: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact writer head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: fix/stacked-pr-central-required-workflows + fetch-depth: 0 + + - name: Fail closed on malformed allowed_merge_methods payloads + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python3 - <<'PY' + from pathlib import Path + + audit_path = Path("scripts/ci/audit_central_required_workflows.py") + text = audit_path.read_text(encoding="utf-8") + old = ' allowed_methods = set(parameters.get("allowed_merge_methods") or [])\n' + new = ''' raw_allowed_methods = parameters.get("allowed_merge_methods")\n allowed_methods = (\n set(raw_allowed_methods)\n if isinstance(raw_allowed_methods, list)\n and all(isinstance(method, str) for method in raw_allowed_methods)\n else set()\n )\n''' + if text.count(old) != 2: + raise SystemExit( + f"expected exactly two merge-method parsers at the exact writer head; found {text.count(old)}" + ) + audit_path.write_text(text.replace(old, new), encoding="utf-8") + + workflow_path = Path(".github/workflows/solo-maintainer-ruleset-contract.yml") + workflow = workflow_path.read_text(encoding="utf-8") + trigger_anchor = ' - "tests/test_solo_maintainer_ruleset_policy.py"\n' + test_anchor = ' tests/test_solo_maintainer_ruleset_policy.py \\\n' + new_test = "tests/test_ruleset_merge_method_shape_regression.py" + if new_test not in workflow: + if trigger_anchor not in workflow or test_anchor not in workflow: + raise SystemExit("focused workflow anchors changed; refusing a stale patch") + workflow = workflow.replace( + trigger_anchor, + trigger_anchor + f' - "{new_test}"\n', + 1, + ) + workflow = workflow.replace( + test_anchor, + test_anchor + f" {new_test} \\\n", + 1, + ) + workflow_path.write_text(workflow, encoding="utf-8") + PY + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + + - name: Verify malformed-shape regression and focused governance suites + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cat >"${RUNNER_TEMP}/ruleset-repair-requirements.txt" <<'EOF' + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install --only-binary=:all: --require-hashes -r "${RUNNER_TEMP}/ruleset-repair-requirements.txt" + python -m pytest -q \ + tests/test_ruleset_merge_method_shape_regression.py \ + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_solo_maintainer_ruleset_policy.py \ + tests/test_ruleset_audit_completeness_regression.py \ + tests/test_pr_governance_audit_contract.py + + - name: Commit verified repair and retire helper + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + rm .github/workflows/repair-pr1176-merge-method-shape.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add scripts/ci/audit_central_required_workflows.py \ + .github/workflows/solo-maintainer-ruleset-contract.yml \ + tests/test_ruleset_merge_method_shape_regression.py \ + .github/workflows/repair-pr1176-merge-method-shape.yml + git commit -m "fix(governance): reject malformed merge-method payloads" + git push origin HEAD:fix/stacked-pr-central-required-workflows From ce752a411e49ec698a5435f8e0acabe2671136c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:20:21 +0900 Subject: [PATCH 047/154] fix(governance): reject malformed merge-method payloads --- scripts/ci/audit_central_required_workflows.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index e8154e4187..8d42ede32c 100644 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -238,7 +238,13 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: ) if parameters.get("required_review_thread_resolution") is not True: errors.append("review-thread resolution protection is disabled") - allowed_methods = set(parameters.get("allowed_merge_methods") or []) + raw_allowed_methods = parameters.get("allowed_merge_methods") + allowed_methods = ( + set(raw_allowed_methods) + if isinstance(raw_allowed_methods, list) + and all(isinstance(method, str) for method in raw_allowed_methods) + else set() + ) if allowed_methods != {"merge", "squash"}: errors.append("only merge and squash may be allowed merge methods") @@ -364,7 +370,13 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append( "repository ruleset review-thread resolution protection is disabled" ) - allowed_methods = set(parameters.get("allowed_merge_methods") or []) + raw_allowed_methods = parameters.get("allowed_merge_methods") + allowed_methods = ( + set(raw_allowed_methods) + if isinstance(raw_allowed_methods, list) + and all(isinstance(method, str) for method in raw_allowed_methods) + else set() + ) if allowed_methods != {"merge", "squash"}: errors.append("repository ruleset must allow only merge and squash") From 7e8246214460ce6943f041cb019751688d488530 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:20:33 +0900 Subject: [PATCH 048/154] chore(governance): retire source-fix helper --- .../repair-pr1176-merge-method-shape.yml | 96 ------------------- 1 file changed, 96 deletions(-) delete mode 100644 .github/workflows/repair-pr1176-merge-method-shape.yml diff --git a/.github/workflows/repair-pr1176-merge-method-shape.yml b/.github/workflows/repair-pr1176-merge-method-shape.yml deleted file mode 100644 index 820ac14430..0000000000 --- a/.github/workflows/repair-pr1176-merge-method-shape.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Repair PR1176 merge-method shape audit - -on: - push: - branches: [fix/stacked-pr-central-required-workflows] - paths: - - ".github/workflows/repair-pr1176-merge-method-shape.yml" - -permissions: - contents: write - -jobs: - repair: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact writer head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: fix/stacked-pr-central-required-workflows - fetch-depth: 0 - - - name: Fail closed on malformed allowed_merge_methods payloads - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - python3 - <<'PY' - from pathlib import Path - - audit_path = Path("scripts/ci/audit_central_required_workflows.py") - text = audit_path.read_text(encoding="utf-8") - old = ' allowed_methods = set(parameters.get("allowed_merge_methods") or [])\n' - new = ''' raw_allowed_methods = parameters.get("allowed_merge_methods")\n allowed_methods = (\n set(raw_allowed_methods)\n if isinstance(raw_allowed_methods, list)\n and all(isinstance(method, str) for method in raw_allowed_methods)\n else set()\n )\n''' - if text.count(old) != 2: - raise SystemExit( - f"expected exactly two merge-method parsers at the exact writer head; found {text.count(old)}" - ) - audit_path.write_text(text.replace(old, new), encoding="utf-8") - - workflow_path = Path(".github/workflows/solo-maintainer-ruleset-contract.yml") - workflow = workflow_path.read_text(encoding="utf-8") - trigger_anchor = ' - "tests/test_solo_maintainer_ruleset_policy.py"\n' - test_anchor = ' tests/test_solo_maintainer_ruleset_policy.py \\\n' - new_test = "tests/test_ruleset_merge_method_shape_regression.py" - if new_test not in workflow: - if trigger_anchor not in workflow or test_anchor not in workflow: - raise SystemExit("focused workflow anchors changed; refusing a stale patch") - workflow = workflow.replace( - trigger_anchor, - trigger_anchor + f' - "{new_test}"\n', - 1, - ) - workflow = workflow.replace( - test_anchor, - test_anchor + f" {new_test} \\\n", - 1, - ) - workflow_path.write_text(workflow, encoding="utf-8") - PY - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - - - name: Verify malformed-shape regression and focused governance suites - env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_INPUT: "1" - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - cat >"${RUNNER_TEMP}/ruleset-repair-requirements.txt" <<'EOF' - iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 - packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e - pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - EOF - python -m pip install --only-binary=:all: --require-hashes -r "${RUNNER_TEMP}/ruleset-repair-requirements.txt" - python -m pytest -q \ - tests/test_ruleset_merge_method_shape_regression.py \ - tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_solo_maintainer_ruleset_policy.py \ - tests/test_ruleset_audit_completeness_regression.py \ - tests/test_pr_governance_audit_contract.py - - - name: Commit verified repair and retire helper - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - rm .github/workflows/repair-pr1176-merge-method-shape.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add scripts/ci/audit_central_required_workflows.py \ - .github/workflows/solo-maintainer-ruleset-contract.yml \ - tests/test_ruleset_merge_method_shape_regression.py \ - .github/workflows/repair-pr1176-merge-method-shape.yml - git commit -m "fix(governance): reject malformed merge-method payloads" - git push origin HEAD:fix/stacked-pr-central-required-workflows From 2fb3a489497644ece0bad902eeb151a8418169b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:23:55 +0900 Subject: [PATCH 049/154] chore(governance): retire focused proof workflow --- .../solo-maintainer-ruleset-contract.yml | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/solo-maintainer-ruleset-contract.yml diff --git a/.github/workflows/solo-maintainer-ruleset-contract.yml b/.github/workflows/solo-maintainer-ruleset-contract.yml deleted file mode 100644 index 63698dbffd..0000000000 --- a/.github/workflows/solo-maintainer-ruleset-contract.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Solo Maintainer Ruleset Contract - -on: - push: - branches: [fix/stacked-pr-central-required-workflows] - paths: - - ".github/workflows/solo-maintainer-ruleset-contract.yml" - - ".github/workflows/audit-central-ruleset.yml" - - "scripts/ci/audit_central_required_workflows.py" - - "docs/org-required-workflow-rollout.md" - - "tests/test_central_required_workflow_ruleset_audit.py" - - "tests/test_solo_maintainer_ruleset_policy.py" - - "tests/test_ruleset_audit_completeness_regression.py" - - "tests/test_pr_governance_audit_contract.py" - workflow_dispatch: - -permissions: - contents: read - -jobs: - focused-contract: - name: Focused solo-maintainer ruleset contract - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact pushed head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - - - name: Execute focused ruleset contract - env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_INPUT: "1" - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - cat >"${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" <<'EOF' - iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 - packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e - pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - EOF - python -m pip install \ - --only-binary=:all: \ - --require-hashes \ - -r "${RUNNER_TEMP}/solo-maintainer-ruleset-requirements.txt" - python -m pytest -q \ - tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_solo_maintainer_ruleset_policy.py \ - tests/test_ruleset_audit_completeness_regression.py \ - tests/test_pr_governance_audit_contract.py From 5d1e416956e49545283a2eb581b6eb0186f5613d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:12:28 +0900 Subject: [PATCH 050/154] fix(governance): retire stale temporary-workflow regression --- tests/test_ruleset_audit_completeness_regression.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_ruleset_audit_completeness_regression.py b/tests/test_ruleset_audit_completeness_regression.py index 8ae16db0e4..2343c839da 100644 --- a/tests/test_ruleset_audit_completeness_regression.py +++ b/tests/test_ruleset_audit_completeness_regression.py @@ -110,13 +110,11 @@ def test_live_audit_collects_all_available_ruleset_drift_before_failing() -> Non assert 'if [[ "$audit_status" -ne 0 ]]; then' in workflow -def test_focused_contract_pins_python_before_hash_verified_install() -> None: - """The disposable proof lane must not depend on the hosted default Python.""" - workflow = (REPO_ROOT / ".github/workflows/solo-maintainer-ruleset-contract.yml").read_text(encoding="utf-8") +def test_disposable_focused_contract_is_removed_after_terminal_proof() -> None: + """The temporary proof workflow must not survive its proven source-fix lifecycle.""" + proof_workflow = REPO_ROOT / ".github/workflows/solo-maintainer-ruleset-contract.yml" - assert "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" in workflow - assert 'python-version: "3.14"' in workflow - assert workflow.index("actions/setup-python@") < workflow.index("python -m pip install") + assert not proof_workflow.exists() def test_rollout_guide_declares_solo_maintainer_review_policy() -> None: @@ -126,4 +124,4 @@ def test_rollout_guide_declares_solo_maintainer_review_policy() -> None: assert "required_approving_review_count = 0" in rollout assert "require_last_push_approval = false" in rollout assert "The org's two-reviewer merge rule" not in rollout - assert "two distinct approvals" not in rollout \ No newline at end of file + assert "two distinct approvals" not in rollout From 214b0bd8c6f8554123e0ba2fad62b0b233384344 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:26:03 +0900 Subject: [PATCH 051/154] fix(governance): preserve protected-main review runner repair Restore the exact protected-main #1632 tree for Strix/OpenCode/Noema runner pins, queue regressions, changelog and product-gap record after ancestry integration. The governance writer retains only its seven intended audit/documentation/test deltas over main. --- .github/workflows/noema-review.yml | 4 +- .github/workflows/opencode-review.yml | 10 ++--- .github/workflows/strix.yml | 6 +-- CHANGELOG.md | 1 + docs/product-technical-gap-baseline.md | 13 +++++++ ...t_required_review_runner_image_contract.py | 37 +++++++++++++++++++ .../test_required_workflow_queue_contract.py | 8 ++-- 7 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 tests/test_required_review_runner_image_contract.py diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 6b2e3fcede..2c941983f9 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -35,7 +35,7 @@ permissions: jobs: cancel-closed-pr-runs: if: github.event_name == 'pull_request_target' && github.event.action == 'closed' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read @@ -179,7 +179,7 @@ jobs: noema-review: name: noema-review - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: >- github.event_name == 'repository_dispatch' || ( diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 87827f5322..0864993179 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -44,7 +44,7 @@ permissions: jobs: required-workflow-bootstrap: name: required-workflow-bootstrap - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Materialize the required review workflow run: >- @@ -250,7 +250,7 @@ jobs: coverage-source-tree: name: coverage-source-tree needs: [required-workflow-bootstrap] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - run: >- echo "PR-head source and coverage execution are delegated to the @@ -259,7 +259,7 @@ jobs: coverage-evidence: name: coverage-evidence needs: [coverage-source-tree] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - run: >- echo "This required-workflow job preserves the stable branch-protection @@ -268,7 +268,7 @@ jobs: opencode-review-target: name: opencode-review needs: [coverage-evidence] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: read @@ -475,7 +475,7 @@ jobs: # immediately beforehand, so a run for this job that is itself somehow # delayed/stale cannot wrongly cancel a still-authoritative run. if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 26d5d8b2cb..7674b3040f 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -79,7 +79,7 @@ permissions: jobs: cancel-superseded-pr-runs: if: github.event_name == 'pull_request_target' && (github.event.action == 'synchronize' || github.event.action == 'closed') - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Prefer the established scheduler credential, but let the close event use # its job-scoped token so abandoned scans are cancelled even when that # optional secret is unavailable. This job never checks out PR code. @@ -190,7 +190,7 @@ jobs: # scans may take more than two hours per model (docs/product-goal-directive.md). # Inference has no wall-clock deadline; cancellation is reserved for an # explicit operator action or a superseded head. - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence # from the scan job only. @@ -1019,7 +1019,7 @@ jobs: name: publish-manual-pr-evidence-status needs: strix if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: id-token: write statuses: write # Required for downscoped OIDC status publication. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f980f794d..4b661772cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **Pin the three central required review workflows (Strix, OpenCode Review, Noema Review) off the observed starved floating `ubuntu-latest` runner image.** Following the same repair already rolled out to security gates (`#1618`) and the merge scheduler (`#1609`), `strix.yml`, `opencode-review.yml`, and `noema-review.yml` now request the explicit `ubuntu-24.04` image on every job. These three workflows are the org's own required-workflow gate for every sibling repository, so a starved floating image here directly contributes to organization-wide required-check queuing. New `tests/test_required_review_runner_image_contract.py` asserts no job in any of the three files still requests the floating image. Also fixed 4 pre-existing, unrelated test failures on `main` left by `#1630`'s organization-sweep rotation cadence change (every 15 minutes to hourly, to reduce control-plane pressure under the same Actions saturation): `tests/test_required_workflow_queue_contract.py`'s rotation-index tests still asserted the old `/ 900` (15-minute) divisor against the new `/ 3600` (hourly) production value. - **Refresh Noema reviewer App authority after long model work (`#1616`).** A real `naruon#1497` review outlived its repository-scoped GitHub App installation token and failed the next exact-head GitHub operation with HTTP 401. The trusted workflow now prepares the validated verdict into a private runner-local envelope, remints the same least-privilege repository-scoped App authority after model work, independently re-fetches exact live head/reviewer identity, and only then publishes. Skipped preparation creates no envelope, predecessor App tokens cannot authorize publication, PAT/OIDC remain explicit fail-closed sources, malformed handoffs are cleaned up, and executable plus step-scoped regressions cover stale-head, identity, alias, workflow wiring, and migration of legacy broader-suite contracts away from the retired single-process reviewer path. - Fix `existing_noema_review()` treating a "legacy" Noema review (one posted before `NOEMA_REVIEW_FOOTER_MARKER` existed) as proof the current head was already reviewed. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7ba1d7cd41..41d95b6f57 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2576,3 +2576,16 @@ Higgins, S. S., Crepalde, N., & Fernandes, L. (2021). Segmented multiplexity: A **Regression-suite consistency.** Legacy broader-suite assertions that still named the retired single-process Noema step/module are migrated to the two-phase prepare/publish contract, including step-scoped helper and envelope-argument evidence. This closes the false-GREEN gap where focused token-lifetime CI could pass while unchanged broader contracts described an impossible execution path. **Residual external verification.** After this central change reaches protected `main`, replay Required Noema Review for unchanged `naruon#1497@152d1998c4e8024be9dc7026c8789d343c884fd0`. Closure evidence requires a current-head schema-valid review or typed review-unavailable outcome without expired-token 401; a pre-merge run cannot prove the merged workflow-source path and is not promoted to release evidence. + + +## 2026-09-01 central required review workflows: floating runner image contributing to organization-wide queuing + +**Observed gap.** `#1618` (required security gates) and `#1609` (merge scheduler) already pinned their jobs off `ubuntu-latest` after this session found it to be, in that fix's own words, "the observed starved floating image" — GitHub-hosted runners requesting the floating `ubuntu-latest` label were being left `queued` with no runner assignment for hours, well beyond ordinary scheduling latency, while identical jobs on other repositories/workflows completed normally. `strix.yml`, `opencode-review.yml`, and `noema-review.yml` — the three workflows the org's own required-workflow ruleset runs against every PR in every sibling repository — still requested `ubuntu-latest` on every job (9 occurrences total: 3 in `strix.yml`, 5 in `opencode-review.yml`, 2 in `noema-review.yml`; `pr-review-merge-scheduler.yml` was already covered by `#1609`). Since these three are the actual required-check gate blocking merge across the whole organization, a starved image here is a direct, high-leverage contributor to the sustained multi-hour organization-wide queuing observed throughout this session (independently corroborated by `#1630`'s own record of 822 queued Actions runs at merge time). + +**Fix.** Pinned all 9 occurrences to the explicit `ubuntu-24.04` image, matching the pattern already established by `#1618`/`#1609` exactly (a literal `runs-on:` value swap, no other job semantics touched). New `tests/test_required_review_runner_image_contract.py` asserts no job in any of the three files requests the floating image and pins the expected per-file occurrence count, mirroring `test_required_security_runner_image_contract.py`'s existing structure. + +**Unrelated pre-existing failures fixed in the same pass.** `#1630` (merged shortly before this fix, itself an owner-authorized `QUEUE_SATURATION_CHICKEN_EGG` bypass addressing the same 822-run backlog) moved the organization sweep's rotation cadence from every 15 minutes to hourly to reduce control-plane pressure, changing `pr-review-merge-scheduler.yml`'s `ORG_SWEEP_ROTATION_INDEX` wall-clock fallback divisor from `900` (15 minutes in seconds) to `3600` (1 hour), but left `tests/test_required_workflow_queue_contract.py`'s four rotation-index tests asserting the old `900` divisor and the old literal workflow string. Confirmed these 4 failures reproduce identically on a clean `origin/main` checkout with no changes from this branch, independent of and pre-dating this fix. Updated all four to the new `3600` divisor/string, preserving each test's original intent (wall-clock fallback on total counter unavailability, transient-read-failure-does-not-reset, successful-read-but-failed-patch-falls-back, and the documentation/input-validation contract) unchanged. + +**Validation.** Full suite `2407 passed, 1 skipped, 21 subtests`; `coverage` 100% on `scripts/ci`; `interrogate` 100%; all four touched/added workflow files re-parse as valid YAML; `test_opencode_workflow_shell_syntax.py` and related shell-syntax tests pass unchanged. + +**Residual.** This closes the specific floating-image contribution from these three central workflows; it does not by itself guarantee the organization-wide Actions queue is fully drained, since other repositories' own workflows and any remaining unpinned central workflows may still request the floating image. Worth a follow-up sweep across the rest of `.github/workflows/` and sibling-repo workflows if queuing persists after this lands. diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py new file mode 100644 index 0000000000..c173716e3e --- /dev/null +++ b/tests/test_required_review_runner_image_contract.py @@ -0,0 +1,37 @@ +"""Contract tests for central required review workflow runner images.""" + +from __future__ import annotations + +from pathlib import Path +import unittest + + +STRIX = Path(".github/workflows/strix.yml") +OPENCODE_REVIEW = Path(".github/workflows/opencode-review.yml") +NOEMA_REVIEW = Path(".github/workflows/noema-review.yml") + + +class RequiredReviewRunnerImageContract(unittest.TestCase): + """Keep required review jobs off the observed starved floating image.""" + + def test_strix_uses_explicit_supported_image(self) -> None: + """Require every Strix job to use explicit Ubuntu 24.04.""" + workflow = STRIX.read_text(encoding="utf-8") + self.assertNotIn("runs-on: ubuntu-latest", workflow) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) + + def test_opencode_review_uses_explicit_supported_image(self) -> None: + """Require every OpenCode Review job to use explicit Ubuntu 24.04.""" + workflow = OPENCODE_REVIEW.read_text(encoding="utf-8") + self.assertNotIn("runs-on: ubuntu-latest", workflow) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 5) + + def test_noema_review_uses_explicit_supported_image(self) -> None: + """Require every Noema Review job to use explicit Ubuntu 24.04.""" + workflow = NOEMA_REVIEW.read_text(encoding="utf-8") + self.assertNotIn("runs-on: ubuntu-latest", workflow) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index e18e021fae..8614d02903 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1292,7 +1292,7 @@ def test_org_queue_sweep_rotation_index_falls_back_to_wall_clock(tmp_path: Path) assert result.returncode == 0, result.stderr stdout_lines = result.stdout.strip().splitlines() computed_tick = int(stdout_lines[-1]) # last line: the printed value; earlier: the warning - expected_tick = int(time.time()) // 900 + expected_tick = int(time.time()) // 3600 assert abs(computed_tick - expected_tick) <= 1 # tolerate a tick boundary race assert "could not read/write" in result.stdout # a `::warning::` workflow command @@ -1318,7 +1318,7 @@ def test_org_queue_sweep_rotation_index_transient_read_failure_does_not_reset_co assert result.returncode == 0, result.stderr stdout_lines = result.stdout.strip().splitlines() computed_tick = int(stdout_lines[-1]) - expected_tick = int(time.time()) // 900 + expected_tick = int(time.time()) // 3600 assert abs(computed_tick - expected_tick) <= 1 # Critically: never "1" -- that would mean the failed read was treated # as a fresh-start reset rather than an unreadable existing value. @@ -1341,7 +1341,7 @@ def test_org_queue_sweep_rotation_index_successful_read_but_failed_patch_falls_b assert result.returncode == 0, result.stderr stdout_lines = result.stdout.strip().splitlines() computed_tick = int(stdout_lines[-1]) - expected_tick = int(time.time()) // 900 + expected_tick = int(time.time()) // 3600 assert abs(computed_tick - expected_tick) <= 1 assert "read ORG_SWEEP_ROTATION_COUNTER=41 but could not PATCH it" in result.stdout @@ -1386,7 +1386,7 @@ def test_org_queue_sweep_documents_rotation_leverage_and_validates_input() -> No assert "ContextualWisdomLab/.github#1219" in workflow assert ( - 'ORG_SWEEP_ROTATION_INDEX=$(( $(date -u +%s) / 900 ))' + 'ORG_SWEEP_ROTATION_INDEX=$(( $(date -u +%s) / 3600 ))' ) in workflow assert ( 'if ! [[ "$ORG_SWEEP_ROTATION_INDEX" =~ ^[0-9]+$ ]]; then' From 9a33eccb377b1c2ad3af9a67f11df3eb46453027 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:11:54 +0900 Subject: [PATCH 052/154] chore: preserve current label taxonomy --- config/repository-label-taxonomy.json | 102 +++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/config/repository-label-taxonomy.json b/config/repository-label-taxonomy.json index a1831221ed..6bfea75e56 100644 --- a/config/repository-label-taxonomy.json +++ b/config/repository-label-taxonomy.json @@ -11,6 +11,21 @@ "issue": 1582, "type": "feature" }, + { + "repository": ".github", + "issue": 1622, + "type": "feature" + }, + { + "repository": ".github", + "issue": 1625, + "type": "bug" + }, + { + "repository": ".github", + "issue": 1634, + "type": "documentation" + }, { "repository": "CalendarWeave", "issue": 1, @@ -100,6 +115,91 @@ "repository": "noema", "issue": 530, "type": "feature" + }, + { + "repository": "bandscope", + "issue": 1125, + "type": "documentation" + }, + { + "repository": "saju-caldav", + "issue": 44, + "type": "documentation" + }, + { + "repository": "OriginWeave", + "issue": 274, + "type": "documentation" + }, + { + "repository": "semantic-data-portal", + "issue": 90, + "type": "documentation" + }, + { + "repository": "accounting-information-platform", + "issue": 45, + "type": "documentation" + }, + { + "repository": "clearfolio", + "issue": 538, + "type": "documentation" + }, + { + "repository": "pg-erd-cloud", + "issue": 1046, + "type": "documentation" + }, + { + "repository": "DiagramWeave", + "issue": 34, + "type": "documentation" + }, + { + "repository": "keyverse", + "issue": 127, + "type": "documentation" + }, + { + "repository": "mhtml-etl-gateway", + "issue": 56, + "type": "documentation" + }, + { + "repository": "j-planner", + "issue": 2, + "type": "documentation" + }, + { + "repository": "learning-record-store", + "issue": 1, + "type": "documentation" + }, + { + "repository": "learning-content-studio", + "issue": 1, + "type": "documentation" + }, + { + "repository": "learning-management-platform", + "issue": 1, + "type": "documentation" + }, + { + "repository": "metering-billing-platform", + "issue": 157, + "type": "documentation" + }, + { + "repository": "PolicyWeave", + "issue": 1, + "type": "feature" + }, + { + "repository": "supply-chain-control-plane", + "issue": 1, + "type": "feature" } ] -} +} \ No newline at end of file From 37f7f777b2013b764f2893d582c54a0c50597d19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:12:26 +0900 Subject: [PATCH 053/154] chore: preserve current repository metadata --- config/repository-metadata.json | 86 ++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/config/repository-metadata.json b/config/repository-metadata.json index fcf8471236..b1cebcde96 100644 --- a/config/repository-metadata.json +++ b/config/repository-metadata.json @@ -49,6 +49,90 @@ "topics": ["psychometrics", "assessment", "measurement", "longitudinal", "research", "privacy", "rust", "contextualwisdomlab"], "deepwiki": true, "pages": true + }, + "keyverse": { + "description": "Keyverse — passwordless identity, federation, provisioning, account unification, and authorization services for ContextualWisdomLab.", + "topics": ["identity", "openid-connect", "oauth2", "scim", "keycloak", "python", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "OriginWeave": { + "description": "Let agents use the web without losing control. OriginWeave gives AI agents a Chromium-compatible web runtime with isolated sessions, typed actions, resource governance, and verifiable evidence.", + "topics": ["browser-automation", "ai-agents", "chromium", "security", "rust", "web", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "accounting-information-platform": { + "description": "Accounting Information Platform — statutory accounting, journal posting, period control, reconciliation, and financial reporting authority for ContextualWisdomLab.", + "topics": ["accounting", "ledger", "journal", "reconciliation", "financial-reporting", "postgresql", "python", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "pg-erd-cloud": { + "description": "PostgreSQL 스키마를 리버스 엔지니어링하고 ERD·DDL 공유 흐름으로 관리하는 클라우드 서비스.", + "topics": ["cloud", "database-schema", "ddl", "erd", "postgresql", "reverse-engineering", "saas", "python", "javascript", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "clearfolio": { + "description": "Clearfolio — secure document conversion, tenant-scoped viewing, and controlled artifact delivery.", + "topics": ["document-viewer", "document-conversion", "file-preview", "pdf", "java", "spring-boot", "javascript", "web-app", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "DiagramWeave": { + "description": "DiagramWeave — a source-first, AI-assisted editor and tooling platform for PlantUML diagrams.", + "topics": ["diagram-editor", "plantuml", "developer-tools", "language-server", "javascript", "ai-assisted", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "semantic-data-portal": { + "description": "Semantic Data Portal — governed discovery, graph traversal, and semantic search for enterprise data catalogs.", + "topics": ["data-catalog", "knowledge-graph", "ontology", "semantic-web", "semantic-search", "data-governance", "postgresql", "python", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "contextual-orchestrator": { + "description": "Contextual Orchestrator — an OpenAI-compatible control plane for model routing, delegation, verification, and multi-agent orchestration.", + "topics": ["enterprise-admin", "llm-orchestration", "model-orchestration", "model-routing", "ai-agents", "openai-compatible", "research", "python", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "mhtml-etl-gateway": { + "description": "Enterprise MHTML ingestion gateway that converts browser, SAP ALV, and Excel Web Archive exports into governed PostgreSQL data assets.", + "topics": ["mhtml", "etl", "data-ingestion", "sap", "postgresql", "data-governance", "python", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "PolicyWeave": { + "description": "PolicyWeave — local-first privacy-policy fact authoring, completeness review, and deterministic draft generation for web and app operators.", + "topics": ["privacy", "privacy-policy", "privacy-engineering", "policy-authoring", "local-first", "react", "typescript", "vite", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "supply-chain-control-plane": { + "description": "Supply Chain Control Plane — evidence-backed supply-network dependency modeling and deterministic downstream disruption-impact analysis.", + "topics": ["supply-chain", "disruption-management", "dependency-graph", "provenance", "risk-analysis", "rust", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "learning-management-platform": { + "description": "Learning Management Platform — enrollment, learning-journey, completion, and credential orchestration for employee and external learners.", + "topics": ["learning-management-system", "learning-platform", "enrollment", "completion", "credentialing", "rust", "postgresql", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "learning-content-studio": { + "description": "Learning Content Studio — evidence-bound LCMS for authoring, approving, releasing, and deterministically publishing reusable learning content.", + "topics": ["lcms", "learning-content", "content-authoring", "content-management", "accessibility", "scorm", "cmi5", "rust", "contextualwisdomlab"], + "deepwiki": true, + "pages": true + }, + "learning-record-store": { + "description": "Authoritative xAPI learning-record persistence for the CWL Learning Platform.", + "topics": ["learning-record-store", "xapi", "cmi5", "learning-technology", "interoperability", "contextualwisdomlab"], + "deepwiki": true, + "pages": true } } -} +} \ No newline at end of file From 8f66c9a4ded2555faeefaee9ea3f1649197be40f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:13:04 +0900 Subject: [PATCH 054/154] docs: preserve current public-surface reconciliation --- .../repository-public-surface-reconciliation.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/doctoring/repository-public-surface-reconciliation.md b/docs/doctoring/repository-public-surface-reconciliation.md index 6fa36c5ddc..b3090af6e5 100644 --- a/docs/doctoring/repository-public-surface-reconciliation.md +++ b/docs/doctoring/repository-public-surface-reconciliation.md @@ -42,7 +42,7 @@ The fleet loop is deliberately non-blocking. Every repository or label assignmen - Pull-request validation has `contents: read` only. It cannot mutate repository settings or labels. - Apply runs only when the scheduled workflow is executing from trusted `refs/heads/main` after validation. -- The apply step uses the established maintainer credential rather than widening the ordinary workflow token. +- The apply step uses the established maintainer credential rather than widening the ordinary workflow token. PR #1625 owns the reviewed migration to the dedicated `CWL_REPOSITORY_METADATA_TOKEN`; until that reaches protected main, no documentation may claim the dedicated credential is live. - Repository README changes remain leaf-owned. The central reconciler verifies exact DeepWiki linkage but never fabricates or silently edits customer-facing README copy. - Pages has two reviewed deployment modes. Legacy mode requires a regular `docs/index.md` file on the live default branch. Explicit `pages_mode: workflow` requires a regular `.github/workflows/pages.yml` file **and** an already-configured live Pages site whose `build_type` is `workflow`. - Workflow mode is preserve-only: the reconciler does not create or convert the Pages configuration. Missing Pages, a legacy live configuration, a directory at the required workflow path, or a missing workflow file fails before description/topic/Page writes for that repository. @@ -54,11 +54,15 @@ The fleet loop is deliberately non-blocking. Every repository or label assignmen ## Desired-state fleet in this increment -The repository metadata manifest currently covers eight repositories selected because their public-surface work already has a concrete leaf source or active writer: `CalendarWeave`, `ConceptWeave`, `context-graph-contracts`, `ThreadWeave`, `RankWeave`, `fast-mlsirm`, `EgressWeave`, and `psychometrics-commons`. EgressWeave and Psychometrics Commons joined the fleet after their exact-cased DeepWiki badges and bounded `docs/index.md` Pages sources reached their protected default branches. +The repository metadata manifest covers 22 reviewed repositories whose public-surface work has a concrete leaf source or active writer: `CalendarWeave`, `ConceptWeave`, `context-graph-contracts`, `ThreadWeave`, `RankWeave`, `fast-mlsirm`, `EgressWeave`, `psychometrics-commons`, `keyverse`, `OriginWeave`, `accounting-information-platform`, `pg-erd-cloud`, `clearfolio`, `DiagramWeave`, `semantic-data-portal`, `contextual-orchestrator`, `mhtml-etl-gateway`, `PolicyWeave`, `supply-chain-control-plane`, `learning-management-platform`, `learning-content-studio`, and `learning-record-store`. + +EgressWeave and Psychometrics Commons joined the original fleet after their exact-cased DeepWiki badges and bounded `docs/index.md` Pages sources reached their protected default branches. Later entries are deliberately declared before live convergence only when an owned leaf lane exists for the required badge and Pages source. Until those prerequisites reach each protected default branch, that repository fails closed while sibling repositories remain independently actionable. The `semantic-data-portal` desired description also removes the internal `(PRD/TRD draft implementation)` qualifier rather than propagating it to the customer-facing repository surface. + +The newest cohort has explicit source ownership: `ContextualWisdomLab/PolicyWeave#1` carries its exact-cased badge and `docs/index.md`; `ContextualWisdomLab/supply-chain-control-plane#1` carries its exact badge and bounded Pages landing source on the active product writer; `ContextualWisdomLab/learning-management-platform#1` owns the product-first README badge and `docs/index.md`; `ContextualWisdomLab/learning-content-studio#1` now owns its product-first README, exact badge, Apache-2.0 grant, and the `docs/index.md` content folded from closed child #8; and `ContextualWisdomLab/learning-record-store#1` now owns its product-first README, exact badge, Apache-2.0 grant, and the bounded `docs/index.md` content folded from closed child #7. The closed child PRs retain discussion history but no longer own unique public-surface source. Their live repositories still report Pages disabled until protected integration and trusted reconciliation complete. An Actions-backed repository is not enrolled merely because `pages_mode: workflow` is supported. Enrollment requires an explicit reviewed manifest change after the repository's standard Pages workflow and live `build_type: workflow` configuration both exist. This preserves the deployment architecture of repositories such as ScopeWeave instead of silently rewriting them to legacy `/docs`. -The explicit label assignments now cover 19 evidence-backed targets: `.github#1582`, `CalendarWeave#1`, `ConceptWeave#1`, `context-graph-contracts#20`, `RankWeave#40`, `fast-mlsirm#1717`, `EgressWeave#231`, `psychometrics-commons#442`, `contextual-orchestrator#994`, `contextual-orchestrator#1003`, `appguardrail#1077`, `naruon#1513`, `LineageWeave#908`, `ContextualWisdomLab.github.io#203`, `TEPP#435`, `semantic-data-portal#72`, `Orgmetra#160`, `learning-interoperability-contracts#1`, and `noema#530`. The assignment reconciler preserves richer repository-local labels such as priority, status, and `type: maintenance` when those labels are outside the managed semantic set. +The explicit label assignments now cover 39 active evidence-backed targets: `ContextualWisdomLab/.github#1582`, `ContextualWisdomLab/.github#1622`, `ContextualWisdomLab/.github#1625`, `ContextualWisdomLab/.github#1634`, `ContextualWisdomLab/CalendarWeave#1`, `ContextualWisdomLab/ConceptWeave#1`, `ContextualWisdomLab/context-graph-contracts#20`, `ContextualWisdomLab/RankWeave#40`, `ContextualWisdomLab/fast-mlsirm#1717`, `ContextualWisdomLab/EgressWeave#231`, `ContextualWisdomLab/psychometrics-commons#442`, `ContextualWisdomLab/contextual-orchestrator#994`, `ContextualWisdomLab/contextual-orchestrator#1003`, `ContextualWisdomLab/appguardrail#1077`, `ContextualWisdomLab/naruon#1513`, `ContextualWisdomLab/LineageWeave#908`, `ContextualWisdomLab/ContextualWisdomLab.github.io#203`, `ContextualWisdomLab/TEPP#435`, `ContextualWisdomLab/semantic-data-portal#72`, `ContextualWisdomLab/Orgmetra#160`, `ContextualWisdomLab/learning-interoperability-contracts#1`, `ContextualWisdomLab/noema#530`, `ContextualWisdomLab/bandscope#1125`, `ContextualWisdomLab/saju-caldav#44`, `ContextualWisdomLab/OriginWeave#274`, `ContextualWisdomLab/semantic-data-portal#90`, `ContextualWisdomLab/accounting-information-platform#45`, `ContextualWisdomLab/clearfolio#538`, `ContextualWisdomLab/pg-erd-cloud#1046`, `ContextualWisdomLab/DiagramWeave#34`, `ContextualWisdomLab/keyverse#127`, `ContextualWisdomLab/mhtml-etl-gateway#56`, `ContextualWisdomLab/j-planner#2`, `ContextualWisdomLab/learning-record-store#1`, `ContextualWisdomLab/learning-content-studio#1`, `ContextualWisdomLab/learning-management-platform#1`, `ContextualWisdomLab/metering-billing-platform#157`, `ContextualWisdomLab/PolicyWeave#1`, and `ContextualWisdomLab/supply-chain-control-plane#1`. Closed superseded child PRs `learning-record-store#7`, `learning-content-studio#8`, and `metering-billing-platform#175` are deliberately absent from the active reconciliation target list because their unique documentation deltas were folded into their authoritative parent writers. Historical labels on those closed PRs are not erased by this desired-state change. The assignment reconciler preserves richer repository-local labels such as priority, status, and `type: maintenance` when those labels are outside the managed semantic set. ## Verification contract @@ -72,7 +76,7 @@ A central source commit is not completion. After protected integration and apply 6. the Pages status is `built`, its URL remains under `https://contextualwisdomlab.github.io`, and the published endpoint returns non-empty content before publication is claimed; 7. reviewed issue/PR targets carry the desired managed label while unrelated labels remain intact. -GitHub's current REST Pages contract supports `build_type` values `legacy` and `workflow`, and branch sources with `/` or `/docs`. Legacy desired-state records continue to use `/docs`. The explicit workflow mode exists to preserve a repository whose deployment is already owned by a reviewed GitHub Actions workflow; it is not a central creation/conversion mechanism. +GitHub's current REST Pages contract supports `build_type` values `legacy` and `workflow`, and branch sources with `/` or `/docs`. Current fleet entries use the legacy `/docs` contract unless an entry explicitly declares `pages_mode: workflow`. The workflow mode exists to preserve a repository whose deployment is already owned by a reviewed GitHub Actions workflow; it is not a central creation/conversion mechanism. ## Workflow-mode operating procedure @@ -86,4 +90,4 @@ GitHub's current REST Pages contract supports `build_type` values `legacy` and ` ## Known integration boundary -Until the central PR is merged through normal governance or a verified queue-saturation chicken-and-egg exception, the workflow-mode preservation contract cannot run from trusted `.github/main`; leaf PRs whose badge or Pages source is still branch-only also remain repository-local precondition blockers. These are integration states, not reasons to stop independent repository work. The same run should continue classifying labels, preparing other leaf public surfaces, and re-checking earlier lanes when exact-head evidence becomes available. +The 22-repository desired-state extension is now protected on `.github/main@611feef038ad52d7ee1214d03ea3527289ebf711`; this label-taxonomy lane is the remaining central source delta in the metadata stack. Live settings convergence still requires repository-local badge/Pages prerequisites, trusted protected-main apply, and postcondition re-reads. The dedicated settings-credential migration remains owned by #1625 and is not claimed live before that PR lands. These integration states are not reasons to idle the fleet: blocked leaves fail closed while eligible siblings, labels, and independent public-surface work continue. \ No newline at end of file From c69b254373e53a2340432fe7b9e9d33b6c427519 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:13:26 +0900 Subject: [PATCH 055/154] test: preserve current label taxonomy contract --- tests/test_repository_label_taxonomy.py | 54 ++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_repository_label_taxonomy.py b/tests/test_repository_label_taxonomy.py index 0a9161c803..a149ec7fc6 100644 --- a/tests/test_repository_label_taxonomy.py +++ b/tests/test_repository_label_taxonomy.py @@ -24,6 +24,9 @@ def test_repository_label_taxonomy_maps_evidence_backed_types() -> None: # Keep assignments exact so reviewed target drift cannot silently escape CI. assert payload["assignments"] == [ {"repository": ".github", "issue": 1582, "type": "feature"}, + {"repository": ".github", "issue": 1622, "type": "feature"}, + {"repository": ".github", "issue": 1625, "type": "bug"}, + {"repository": ".github", "issue": 1634, "type": "documentation"}, {"repository": "CalendarWeave", "issue": 1, "type": "documentation"}, {"repository": "ConceptWeave", "issue": 1, "type": "feature"}, { @@ -70,5 +73,54 @@ def test_repository_label_taxonomy_maps_evidence_backed_types() -> None: "type": "feature", }, {"repository": "noema", "issue": 530, "type": "feature"}, + {"repository": "bandscope", "issue": 1125, "type": "documentation"}, + {"repository": "saju-caldav", "issue": 44, "type": "documentation"}, + {"repository": "OriginWeave", "issue": 274, "type": "documentation"}, + { + "repository": "semantic-data-portal", + "issue": 90, + "type": "documentation", + }, + { + "repository": "accounting-information-platform", + "issue": 45, + "type": "documentation", + }, + {"repository": "clearfolio", "issue": 538, "type": "documentation"}, + {"repository": "pg-erd-cloud", "issue": 1046, "type": "documentation"}, + {"repository": "DiagramWeave", "issue": 34, "type": "documentation"}, + {"repository": "keyverse", "issue": 127, "type": "documentation"}, + { + "repository": "mhtml-etl-gateway", + "issue": 56, + "type": "documentation", + }, + {"repository": "j-planner", "issue": 2, "type": "documentation"}, + { + "repository": "learning-record-store", + "issue": 1, + "type": "documentation", + }, + { + "repository": "learning-content-studio", + "issue": 1, + "type": "documentation", + }, + { + "repository": "learning-management-platform", + "issue": 1, + "type": "documentation", + }, + { + "repository": "metering-billing-platform", + "issue": 157, + "type": "documentation", + }, + {"repository": "PolicyWeave", "issue": 1, "type": "feature"}, + { + "repository": "supply-chain-control-plane", + "issue": 1, + "type": "feature", + }, ] - assert len(set(payload["type"].values())) == len(payload["type"]) + assert len(set(payload["type"].values())) == len(payload["type"]) \ No newline at end of file From a001ec25af88a54f895f366951c1efee46dd5b6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:15:21 +0900 Subject: [PATCH 056/154] test: preserve current repository metadata contract --- tests/test_repository_metadata_reconciliation.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_repository_metadata_reconciliation.py b/tests/test_repository_metadata_reconciliation.py index 2122c5d070..2bfc9d1386 100644 --- a/tests/test_repository_metadata_reconciliation.py +++ b/tests/test_repository_metadata_reconciliation.py @@ -71,6 +71,20 @@ def test_metadata_manifest_declares_exact_casing_and_public_surfaces() -> None: "fast-mlsirm": ("psychometrics", "rust"), "EgressWeave": ("ssrf", "python"), "psychometrics-commons": ("psychometrics", "rust"), + "keyverse": ("identity", "openid-connect"), + "OriginWeave": ("browser-automation", "ai-agents"), + "accounting-information-platform": ("accounting", "ledger"), + "pg-erd-cloud": ("erd", "postgresql"), + "clearfolio": ("document-viewer", "document-conversion"), + "DiagramWeave": ("diagram-editor", "plantuml"), + "semantic-data-portal": ("data-catalog", "semantic-search"), + "contextual-orchestrator": ("llm-orchestration", "model-routing"), + "mhtml-etl-gateway": ("mhtml", "etl"), + "PolicyWeave": ("privacy-policy", "typescript"), + "supply-chain-control-plane": ("supply-chain", "rust"), + "learning-management-platform": ("learning-management-system", "rust"), + "learning-content-studio": ("lcms", "content-authoring"), + "learning-record-store": ("learning-record-store", "xapi"), } assert set(repositories) == set(expected) for repository, required_topics in expected.items(): @@ -227,7 +241,7 @@ def test_pages_and_docs_probes(monkeypatch) -> None: responses = iter( [ - completed(out='{"type": "file"}'), + completed(out='{"type":"file"}'), completed(code=1, out="Not Found"), completed(code=1, err="boom"), ] From 48d977235ab3f4f4dc3a7c3e20564d696799c1e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:15:59 +0900 Subject: [PATCH 057/154] chore: restore exact protected-main metadata blobs --- config/repository-label-taxonomy.json | 2 +- config/repository-metadata.json | 2 +- docs/doctoring/repository-public-surface-reconciliation.md | 2 +- tests/test_repository_label_taxonomy.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/repository-label-taxonomy.json b/config/repository-label-taxonomy.json index 6bfea75e56..0dd7ac6ee4 100644 --- a/config/repository-label-taxonomy.json +++ b/config/repository-label-taxonomy.json @@ -202,4 +202,4 @@ "type": "feature" } ] -} \ No newline at end of file +} diff --git a/config/repository-metadata.json b/config/repository-metadata.json index b1cebcde96..bb95527ee7 100644 --- a/config/repository-metadata.json +++ b/config/repository-metadata.json @@ -135,4 +135,4 @@ "pages": true } } -} \ No newline at end of file +} diff --git a/docs/doctoring/repository-public-surface-reconciliation.md b/docs/doctoring/repository-public-surface-reconciliation.md index b3090af6e5..0f5fc0e370 100644 --- a/docs/doctoring/repository-public-surface-reconciliation.md +++ b/docs/doctoring/repository-public-surface-reconciliation.md @@ -90,4 +90,4 @@ GitHub's current REST Pages contract supports `build_type` values `legacy` and ` ## Known integration boundary -The 22-repository desired-state extension is now protected on `.github/main@611feef038ad52d7ee1214d03ea3527289ebf711`; this label-taxonomy lane is the remaining central source delta in the metadata stack. Live settings convergence still requires repository-local badge/Pages prerequisites, trusted protected-main apply, and postcondition re-reads. The dedicated settings-credential migration remains owned by #1625 and is not claimed live before that PR lands. These integration states are not reasons to idle the fleet: blocked leaves fail closed while eligible siblings, labels, and independent public-surface work continue. \ No newline at end of file +The 22-repository desired-state extension is now protected on `.github/main@611feef038ad52d7ee1214d03ea3527289ebf711`; this label-taxonomy lane is the remaining central source delta in the metadata stack. Live settings convergence still requires repository-local badge/Pages prerequisites, trusted protected-main apply, and postcondition re-reads. The dedicated settings-credential migration remains owned by #1625 and is not claimed live before that PR lands. These integration states are not reasons to idle the fleet: blocked leaves fail closed while eligible siblings, labels, and independent public-surface work continue. diff --git a/tests/test_repository_label_taxonomy.py b/tests/test_repository_label_taxonomy.py index a149ec7fc6..67762347ef 100644 --- a/tests/test_repository_label_taxonomy.py +++ b/tests/test_repository_label_taxonomy.py @@ -123,4 +123,4 @@ def test_repository_label_taxonomy_maps_evidence_backed_types() -> None: "type": "feature", }, ] - assert len(set(payload["type"].values())) == len(payload["type"]) \ No newline at end of file + assert len(set(payload["type"].values())) == len(payload["type"]) From 8339b9b1e1c3bdfeb07e436ae353c4be35cf5727 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:17:23 +0900 Subject: [PATCH 058/154] test: reject code-owner review in solo-maintainer rulesets --- tests/test_solo_maintainer_ruleset_policy.py | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_solo_maintainer_ruleset_policy.py b/tests/test_solo_maintainer_ruleset_policy.py index f05f22498f..d11a7675a0 100644 --- a/tests/test_solo_maintainer_ruleset_policy.py +++ b/tests/test_solo_maintainer_ruleset_policy.py @@ -71,6 +71,7 @@ def _repository_ruleset_payload() -> dict: "parameters": { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, "require_last_push_approval": False, "required_review_thread_resolution": True, "required_reviewers": [], @@ -124,6 +125,26 @@ def test_repository_ruleset_rejects_synthetic_required_reviewer() -> None: ] +def test_central_ruleset_rejects_code_owner_review_deadlock() -> None: + """Code-owner approval cannot be mandatory when the only owner authors the change.""" + payload = _central_ruleset_payload() + _review_parameters(payload)["require_code_owner_review"] = True + + assert audit.audit_ruleset(payload) == [ + "central solo-maintainer ruleset must not require code-owner review" + ] + + +def test_repository_ruleset_rejects_code_owner_review_deadlock() -> None: + """The control plane cannot reintroduce independence through CODEOWNERS.""" + payload = _repository_ruleset_payload() + _review_parameters(payload)["require_code_owner_review"] = True + + assert audit.audit_repository_ruleset(payload) == [ + "repository solo-maintainer ruleset must not require code-owner review" + ] + + def test_central_ruleset_rejects_malformed_allowed_merge_methods() -> None: """Malformed API payloads must report drift rather than abort central auditing.""" expected = ["only merge and squash may be allowed merge methods"] From 2887bb66c1c30d6e8c5fa8e5b4aab8e879557f0d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:18:58 +0900 Subject: [PATCH 059/154] fix: reject code-owner approval deadlocks --- scripts/ci/audit_central_required_workflows.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index 8d42ede32c..393a23a3d6 100755 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -230,6 +230,10 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: errors.append( "central solo-maintainer ruleset must not configure required reviewers" ) + if parameters.get("require_code_owner_review") is not False: + errors.append( + "central solo-maintainer ruleset must not require code-owner review" + ) if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: @@ -360,6 +364,10 @@ def audit_repository_ruleset(payload: dict[str, Any]) -> list[str]: errors.append( "repository solo-maintainer ruleset must not configure required reviewers" ) + if parameters.get("require_code_owner_review") is not False: + errors.append( + "repository solo-maintainer ruleset must not require code-owner review" + ) if parameters.get("dismiss_stale_reviews_on_push") is not True: errors.append("repository ruleset stale-review dismissal on push is disabled") if parameters.get("require_last_push_approval") is not False: @@ -464,4 +472,4 @@ def main(argv: list[str] | None = None) -> int: if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 9457e66b9bbe29e8166f58bc6bc8ac57eff94005 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:44:51 +0900 Subject: [PATCH 060/154] fix(governance): preserve current-main Pingora evidence Restore the six protected-main #1466 paths byte-for-byte after ancestry integration exposed that the writer tree still carried predecessor content. This keeps the governance delta limited to its intended seven paths without force-push or history rewrite. --- .../0019-cloudflare-pingora-edge-standard.md | 6 + .../pingora-documentation-image-evidence.md | 17 ++ docs/policies/PINGORA_EDGE_POLICY.md | 11 +- docs/product-technical-gap-baseline.md | 1 + scripts/ci/pingora_edge_policy.py | 197 ++++++++++++++++-- tests/test_pingora_edge_policy.py | 188 ++++++++++++++++- 6 files changed, 394 insertions(+), 26 deletions(-) create mode 100644 docs/doctoring/pingora-documentation-image-evidence.md diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 805e538b86..9f92f0f046 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -33,6 +33,12 @@ so a governed shared implementation is required. 6. Initial migration does not use Pingora's experimental cache integration. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. +8. Documentation PNG screenshots and PDF papers without a text diff are verified + from bounded format evidence (a complete CRC-valid PNG chunk stream with + conforming chunk names, palette bounds, and palette indices whose bounded null- or + Adam7-interlaced decompressed scanlines match IHDR, or a PDF signature) and excluded + from runtime-content scanning; + runtime paths and malformed or unsupported binary evidence still fail closed. ## Consequences diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md new file mode 100644 index 0000000000..af10942cd8 --- /dev/null +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -0,0 +1,17 @@ +# Pingora documentation image evidence + +The required Pingora gate previously sent a changed PNG screenshot through its +UTF-8 runtime-content decoder because GitHub omits text patches for binary files. +That rejected UI evidence before the policy could determine whether it described +an active edge runtime. + +ADR-0019 now admits documentation PNG screenshots only when the bounded final +file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing +payload, conforming chunk names, palette bounds and indices, and bounded null- or +Adam7-interlaced decompressed scanlines that match IHDR. A signature or +CRC-valid arbitrary IDAT is insufficient. Files in a runtime path, malformed signatures, +unsupported binary formats, and unavailable evidence continue to fail closed. +The gate establishes bounded binary evidence rather than general image-rendering +fidelity; optional ancillary-chunk semantics are outside this policy boundary. +`tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake +PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 4d4c0752e1..619374a13d 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -53,8 +53,15 @@ The organization-required `required-workflow-bootstrap` job runs trusted base-branch scanner code at the immutable required-workflow SHA. It reads bounded changed-file metadata and final UTF-8 content through GitHub's REST API. It does not check out or execute pull-request content and receives only read permissions. -Malformed, truncated, binary, symlink, oversized, or unavailable evidence fails -closed. +Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails +closed. Documentation PNG screenshots and PDF papers without a text diff are +excluded only after bounded format verification; PNG evidence must be a complete +CRC-valid chunk stream ending at IEND with conforming chunk names, palette +bounds, and palette indices whose bounded null- or Adam7-interlaced decompressed +scanlines match IHDR. +This is a bounded binary-evidence classifier, not a general image renderer; +visual fidelity and optional ancillary-chunk semantics are outside this gate. +Other binary files remain unavailable evidence and fail closed. ## Exception process diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 41d95b6f57..2a8f4c7b54 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -93,6 +93,7 @@ flowchart LR | G-13 | hourly scheduler는 존재하지만 no-op/credential unavailable/queued Checks의 customer next action을 모든 caller가 동일한 receipt로 내는지 미확인이다 | 자동화가 실패해도 운영자가 무엇을 고쳐야 하는지 알 수 없다 | `skipped_credential_unavailable` receipt와 다음 행동 문구를 exact-head Checks로 검증하고, bounded receipt schema, retry floor, single-flight, no secret fallback을 모든 caller contract test로 고정한다 | | G-14 | release/changelog/version 증거가 각 PR에 분산되고 현재 central repo 보호 main의 release candidate가 명확하지 않다 | 운영자는 어떤 기능이 supportable release인지 확인할 수 없다 | merge 후 release readiness ledger, CHANGELOG, semantic version/tag, rollback/operability evidence를 함께 갱신한다 | | G-15 | 첨부파일 처리 경계가 제품별로 다르고, 1MB 상한은 업무 데이터와 맞지 않으며 미지원 MIME/컨테이너가 parser registry에서 명시적으로 pending/quarantine 되는지 확인되지 않았다. 현재 20MB 초과 파일 가능성과 PDF/HWP/HWPX·이미지·압축파일의 parse/sidecar 흐름을 하나의 exact contract로 묶지 못했다 | 큰 업무 첨부를 거부하거나 파싱 실패를 조용히 잃으면 고객의 메일·문서 업무가 중단된다 | naruon/newsdom-api 소유 PR에서 streaming upload, configurable bounded limit above 20MB, MIME sniffing, parser capability registry, quarantine/retry, source-position provenance, and ADR를 추가하고 size/unsupported-type/zip-bomb tests를 required evidence로 만든다 | +| G-16 | Required Pingora policy treated a changed documentation PNG screenshot as UTF-8 runtime evidence | Valid UI evidence blocked otherwise valid product PRs before policy evaluation | This branch verifies bounded PNG magic before exemption while runtime paths and malformed assets continue to fail closed; protected-main delivery remains the release gate | ## 4. 열린 PR live inventory diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 823e17fbe5..33e58ed876 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -15,6 +15,7 @@ import os import re import sys +import zlib from dataclasses import dataclass from pathlib import PurePosixPath from typing import Callable, Mapping, Sequence @@ -38,7 +39,11 @@ # 1 MiB base64 ceiling -- rejecting a legitimate research-paper citation # (this org's own "attach the relevant paper PDF" convention) for a reason # that has nothing to do with the Nginx runtime policy this module enforces. -BINARY_DOCUMENT_SUFFIXES = frozenset({".pdf"}) +BINARY_DOCUMENT_MAGIC = { + ".pdf": (b"%PDF-",), + ".png": (b"\x89PNG\r\n\x1a\n",), +} +PNG_SIGNATURE = BINARY_DOCUMENT_MAGIC[".png"][0] SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) @@ -171,7 +176,7 @@ def _is_documentation_or_source_fixture(path: str) -> bool: """Return whether *path* is prose, license text, or scanner source fixture. Textual suffixes only: a ``.pdf`` is handled separately by - ``_is_binary_documentation_pdf`` and gated on GitHub reporting no diff + ``_is_binary_documentation_asset`` and gated on GitHub reporting no diff ``patch`` for it, so a textual file merely named with a ``.pdf`` suffix (one GitHub *can* diff, meaning it could carry inspectable content) is never exempted here. @@ -206,15 +211,15 @@ def _is_documentation_or_source_fixture(path: str) -> bool: return False -def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: - """Return whether *changed* is a plausibly binary documentation PDF. +def _is_binary_documentation_asset(changed: ChangedFile) -> bool: + """Return whether *changed* is a plausibly binary documentation asset. This is only the cheap, patch-presence pre-filter: GitHub's changed-files API never returns a diff ``patch`` for a true binary file, so a missing ``patch`` is *necessary* but not *sufficient* evidence -- GitHub also omits one for a textual diff that merely exceeds its own rendering limit. A caller with network access (``evaluate_pull_request``) must - still confirm this with ``_pdf_evidence_confirms_binary`` before + still confirm this with ``_binary_documentation_evidence_confirms`` before trusting it; a caller without one (this module's own unit tests calling this function directly) is only checking the necessary condition. """ @@ -223,8 +228,9 @@ def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: return False pure = PurePosixPath(changed.path) return ( - pure.suffix.lower() in BINARY_DOCUMENT_SUFFIXES + pure.suffix.lower() in BINARY_DOCUMENT_MAGIC and _is_known_documentation_path(pure) + and _runtime_path_rule(changed.path) is None ) @@ -414,10 +420,7 @@ def _load_file_content(api_url: str, repository: str, path: str, head_sha: str, raise PolicyError(f"Runtime policy candidate {path} is not valid UTF-8") from exc -_PDF_MAGIC_PREFIX = b"%PDF-" - - -def _pdf_evidence_confirms_binary( +def _binary_documentation_evidence_confirms( changed: ChangedFile, *, api_url: str, @@ -426,17 +429,18 @@ def _pdf_evidence_confirms_binary( token: str, opener: OpenJson, ) -> bool: - """Return whether a claimed binary documentation PDF is genuinely binary. + """Return whether a claimed binary documentation asset is genuine. A missing diff ``patch`` alone is not proof of binary content: GitHub also omits a patch for a textual diff that exceeds its own rendering limit, well under this module's ``MAX_FILE_BYTES`` content-fetch ceiling. Whenever the file's raw bytes can be fetched at all, this - verifies the real ``%PDF-`` magic prefix instead of trusting + verifies the declared format's magic prefix instead of trusting patch-presence alone. Only a file whose content evidently exceeds the - Contents API's size ceiling -- the exact case ``_is_binary_documentation_pdf`` + Contents API's size ceiling -- the exact case ``_is_binary_documentation_asset`` exists for, a cited, large research paper -- falls back to trusting the - path+suffix convention; every other content-evidence failure (a + path+suffix convention for oversized PDFs only; every other + content-evidence failure (a malformed API response, corrupt base64, a declared size that does not match the decoded bytes) propagates and fails the whole check closed, same as for any other file that needs scanning. @@ -445,22 +449,169 @@ def _pdf_evidence_confirms_binary( try: raw = _load_raw_file_bytes(api_url, repository, changed.path, head_sha, token, opener) except ContentSizeExceededError: - return True - return raw.startswith(_PDF_MAGIC_PREFIX) + return PurePosixPath(changed.path).suffix.lower() == ".pdf" + suffix = PurePosixPath(changed.path).suffix.lower() + if suffix == ".png": + return _is_complete_png(raw) + return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) + + +def _png_unfilter_row(filtered: bytes, previous: bytes, filter_type: int, bytes_per_pixel: int) -> bytes: + """Reconstruct one PNG scanline for bounded indexed-pixel validation.""" + + reconstructed = bytearray(len(filtered)) + for index, value in enumerate(filtered): + left = reconstructed[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + above = previous[index] if previous else 0 + upper_left = previous[index - bytes_per_pixel] if previous and index >= bytes_per_pixel else 0 + if filter_type == 0: + predictor = 0 + elif filter_type == 1: + predictor = left + elif filter_type == 2: + predictor = above + elif filter_type == 3: + predictor = (left + above) // 2 + else: + estimate = left + above - upper_left + distances = (abs(estimate - left), abs(estimate - above), abs(estimate - upper_left)) + predictor = (left, above, upper_left)[distances.index(min(distances))] + reconstructed[index] = (value + predictor) & 0xFF + return bytes(reconstructed) + + +def _is_complete_png(raw: bytes) -> bool: + """Validate one bounded PNG including its null- or Adam7-interlaced stream.""" + + if not raw.startswith(PNG_SIGNATURE): + return False + offset = len(PNG_SIGNATURE) + header: tuple[int, int, int, int, int] | None = None + palette_entries = 0 + image_data: list[bytes] = [] + image_data_closed = False + while offset + 12 <= len(raw): + length = int.from_bytes(raw[offset : offset + 4], "big") + chunk_end = offset + 12 + length + if chunk_end > len(raw): + return False + chunk_type = raw[offset + 4 : offset + 8] + chunk_data = raw[offset + 8 : offset + 8 + length] + expected_crc = int.from_bytes(raw[offset + 8 + length : chunk_end], "big") + if ( + any(not (65 <= byte <= 90 or 97 <= byte <= 122) for byte in chunk_type) + or chunk_type[2] & 0x20 + or zlib.crc32(chunk_type + chunk_data) != expected_crc + ): + return False + if header is None: + if chunk_type != b"IHDR" or length != 13 or offset != len(PNG_SIGNATURE): + return False + width = int.from_bytes(chunk_data[0:4], "big") + height = int.from_bytes(chunk_data[4:8], "big") + bit_depth, color_type, compression, filtering, interlace = chunk_data[8:13] + allowed_depths = { + 0: {1, 2, 4, 8, 16}, 2: {8, 16}, 3: {1, 2, 4, 8}, + 4: {8, 16}, 6: {8, 16}, + } + if ( + width == 0 or height == 0 + or bit_depth not in allowed_depths.get(color_type, set()) + or compression != 0 or filtering != 0 or interlace not in {0, 1} + ): + return False + header = (width, height, bit_depth, color_type, interlace) + elif chunk_type == b"IHDR": + return False + elif chunk_type == b"PLTE": + if palette_entries or image_data or length == 0 or length > 768 or length % 3: + return False + _width, _height, bit_depth, color_type, _interlace = header + if color_type == 3 and length // 3 > 1 << bit_depth: + return False + palette_entries = length // 3 + elif chunk_type == b"IDAT": + if image_data_closed: + return False + image_data.append(chunk_data) + elif chunk_type == b"IEND": + if length != 0 or not image_data or chunk_end != len(raw): + return False + width, height, bit_depth, color_type, interlace = header + if (color_type == 3 and not palette_entries) or ( + color_type in {0, 4} and palette_entries + ): + return False + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] + passes = ( + ((0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4), + (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)) + if interlace else ((0, 0, 1, 1),) + ) + scanlines: list[tuple[int, int, int]] = [] + expected_size = 0 + for x_start, y_start, x_step, y_step in passes: + if width <= x_start or height <= y_start: + continue + pass_width = (width - x_start + x_step - 1) // x_step + pass_height = (height - y_start + y_step - 1) // y_step + row_bytes = (pass_width * channels * bit_depth + 7) // 8 + expected_size += pass_height * (row_bytes + 1) + if expected_size > MAX_RESPONSE_BYTES: + return False + scanlines.append((pass_height, row_bytes, pass_width)) + decoder = zlib.decompressobj() + try: + decoded = decoder.decompress(b"".join(image_data), expected_size + 1) + except zlib.error: + return False + if ( + len(decoded) != expected_size or not decoder.eof + or decoder.unused_data or decoder.unconsumed_tail + ): + return False + decoded_offset = 0 + for row_count, row_bytes, pass_width in scanlines: + previous = b"" + for _ in range(row_count): + filter_type = decoded[decoded_offset] + if filter_type > 4: + return False + filtered = decoded[decoded_offset + 1 : decoded_offset + row_bytes + 1] + if color_type == 3: + reconstructed = _png_unfilter_row(filtered, previous, filter_type, 1) + mask = (1 << bit_depth) - 1 + for pixel in range(pass_width): + bit_offset = pixel * bit_depth + palette_index = ( + reconstructed[bit_offset // 8] + >> (8 - bit_depth - bit_offset % 8) + ) & mask + if palette_index >= palette_entries: + return False + previous = reconstructed + decoded_offset += row_bytes + 1 + return decoded_offset == len(decoded) + elif chunk_type[0] & 0x20 == 0: + return False + elif image_data: + image_data_closed = True + offset = chunk_end + return False def _needs_content_scan(changed: ChangedFile) -> bool: """Return whether a changed final file can carry an active edge runtime. - A claimed binary documentation PDF (``_is_binary_documentation_pdf``) + A claimed binary documentation asset (``_is_binary_documentation_asset``) exempts here on the cheap, offline pre-filter alone; ``evaluate_pull_request`` - never actually relies on that -- it runs ``_pdf_evidence_confirms_binary`` + never actually relies on that -- it runs ``_binary_documentation_evidence_confirms`` for that case before this function is even consulted. """ if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): return False - if _is_binary_documentation_pdf(changed): + if _is_binary_documentation_asset(changed): return False if not changed.patch_available: return True @@ -499,17 +650,17 @@ def evaluate_pull_request( changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) violations: list[Violation] = [] for changed in changed_files: - # A claimed binary documentation PDF gets its own network-verified + # A claimed binary documentation asset gets its own network-verified # check ahead of _needs_content_scan's patch-presence-only signal: # a missing patch does not by itself prove binary content (GitHub # also omits one for an oversized textual diff), so this confirms - # the real %PDF- magic prefix whenever the bytes can be fetched at + # the format's magic prefix whenever the bytes can be fetched at # all, falling back to the path+suffix convention only when the # content genuinely exceeds the Contents API's size ceiling. A # removed file has no head content to fetch at all -- _needs_content_scan # already special-cases this the same way for every other file. - if changed.status != "removed" and _is_binary_documentation_pdf(changed): - if _pdf_evidence_confirms_binary( + if changed.status != "removed" and _is_binary_documentation_asset(changed): + if _binary_documentation_evidence_confirms( changed, api_url=api_url.rstrip("/"), repository=repository, diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 96692b4430..c5d4e9d7a3 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -7,6 +7,7 @@ import inspect import re import sys +import zlib from io import BytesIO from pathlib import Path from urllib.error import HTTPError, URLError @@ -383,10 +384,195 @@ def opener(url: str, _token: str) -> object: assert result == () +def test_evaluate_pull_request_exempts_a_real_documentation_png() -> None: + """A screenshot is verified by PNG magic instead of decoded as UTF-8.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/15/files" in url: + return [{"filename": "docs/screenshots/dashboard.png", "status": "added"}] + assert "/contents/docs/screenshots/dashboard.png" in url + raw = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) + return { + "type": "file", + "encoding": "base64", + "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + assert policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=15, + head_sha="a" * 40, + event_action="opened", + token="token", + opener=opener, + ) == () + + +def test_evaluate_pull_request_rejects_a_fake_documentation_png() -> None: + """A PNG suffix without PNG magic remains runtime-content evidence.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/16/files" in url: + return [{"filename": "docs/screenshots/fake.png", "status": "added"}] + return encoded_file("cat /etc/nginx/nginx.conf\n") + + result = policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=16, + head_sha="b" * 40, + event_action="opened", + token="token", + opener=opener, + ) + assert [item.rule for item in result] == ["nginx_runtime_path"] + + +def test_evaluate_pull_request_rejects_png_with_appended_runtime_text() -> None: + """A valid image prefix cannot hide bytes appended after the IEND chunk.""" + + image = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) + + def opener(url: str, _token: str) -> object: + if "/pulls/17/files" in url: + return [{"filename": "docs/screenshots/forged.png", "status": "added"}] + raw = image + b"\ncat /etc/nginx/nginx.conf\n" + return { + "type": "file", "encoding": "base64", "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + with pytest.raises(policy.PolicyError, match="not valid UTF-8"): + policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=17, + head_sha="c" * 40, + event_action="opened", + token="token", + opener=opener, + ) + + +def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: + """Every malformed PNG boundary returns false without parsing past bounds.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + signature = policy.PNG_SIGNATURE + header = chunk(b"IHDR", b"\0" * 13) + assert not policy._is_complete_png(b"not-png") + assert not policy._is_complete_png(signature) + assert not policy._is_complete_png( + signature + (99).to_bytes(4, "big") + b"IHDR" + b"\0" * 4 + ) + assert not policy._is_complete_png(signature + header[:-1] + b"\0") + assert not policy._is_complete_png(signature + chunk(b"TEXT", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"IEND", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"TEXT", b"")) + + +def test_png_semantic_validation_fails_closed() -> None: + """CRC-valid chunks still need a valid bounded PNG image stream.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + def png(header: bytes, *chunks: bytes) -> bytes: + return policy.PNG_SIGNATURE + chunk(b"IHDR", header) + b"".join(chunks) + + def indexed_png( + width: int, + height: int, + bit_depth: int, + palette_entries: int, + decoded: bytes, + *, + interlace: int = 0, + ) -> bytes: + header = width.to_bytes(4, "big") + height.to_bytes(4, "big") + bytes((bit_depth, 3, 0, 0, interlace)) + return png( + header, + chunk(b"PLTE", b"\0\0\0" * palette_entries), + chunk(b"IDAT", zlib.compress(decoded)), + chunk(b"IEND", b""), + ) + + rgba = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 0)) + indexed = (1).to_bytes(4, "big") * 2 + bytes((8, 3, 0, 0, 0)) + gray = (1).to_bytes(4, "big") * 2 + bytes((8, 0, 0, 0, 0)) + image = chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")) + end = chunk(b"IEND", b"") + + invalid_headers = ( + b"\0" * 13, + (1).to_bytes(4, "big") * 2 + bytes((4, 2, 0, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 1, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 1, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 2)), + ) + assert all(not policy._is_complete_png(png(header, image, end)) for header in invalid_headers) + assert not policy._is_complete_png(png(rgba, chunk(b"IHDR", rgba), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x" * 769), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"1EXt", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"tExt", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"ABCD", b""), image, end)) + assert policy._is_complete_png(png(rgba, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, image, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(indexed, image, end)) + indexed_one_bit = (1).to_bytes(4, "big") * 2 + bytes((1, 3, 0, 0, 0)) + assert not policy._is_complete_png( + png(indexed_one_bit, chunk(b"PLTE", b"\0" * 9), chunk(b"IDAT", zlib.compress(b"\0\0")), end) + ) + for filter_type in range(5): + second_row = b"\1\0" if filter_type == 0 else b"\1\xff" + assert policy._is_complete_png( + indexed_png(2, 2, 8, 2, bytes((filter_type, 0, 1, filter_type)) + second_row) + ) + assert not policy._is_complete_png(indexed_png(2, 1, 8, 1, b"\0\0\1")) + assert not policy._is_complete_png(indexed_png(2, 2, 8, 2, b"\0\0\1\4\2\xfe")) + assert policy._is_complete_png(indexed_png(2, 1, 1, 2, b"\0\x40")) + assert not policy._is_complete_png(indexed_png(2, 1, 1, 1, b"\0\x40")) + assert policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\0", interlace=1)) + assert not policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\1", interlace=1)) + assert not policy._is_complete_png(png(gray, chunk(b"PLTE", b"\0\0\0"), chunk(b"IDAT", zlib.compress(b"\0\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", b"not-zlib"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0") + b"x"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\5\0\0\0\0")), end)) + huge = (policy.MAX_RESPONSE_BYTES).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0)) + assert not policy._is_complete_png(png(huge, image, end)) + + adam7 = (8).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + adam7_scanlines = b"".join( + b"\0" + b"\0" * (pass_width * 4) + for pass_width, pass_height in ((1, 1), (1, 1), (2, 1), (2, 2), (4, 2), (4, 4), (8, 4)) + for _ in range(pass_height) + ) + assert policy._is_complete_png( + png(adam7, chunk(b"IDAT", zlib.compress(adam7_scanlines)), end) + ) + adam7_one_pixel = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + assert policy._is_complete_png( + png(adam7_one_pixel, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")), end) + ) + + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. - Regression coverage for Devin Review's finding: _is_binary_documentation_pdf + Regression coverage for Devin Review's finding: _is_binary_documentation_asset does not itself check status, so without an explicit removed-status guard in evaluate_pull_request's own loop, a deleted PDF would try to fetch its (nonexistent) head content and fail evidence collection for every such From 65be10bdf394b160a94b3a94c48874ca07b29ba1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:52:57 +0900 Subject: [PATCH 061/154] fix(governance): preserve current-main NIM retirement Restore protected main #1640 documentation retirement byte-for-byte after ancestry integration exposed two predecessor documentation paths still present in the writer tree. Keep the final delta limited to the seven intended governance paths. --- .../nvidia-nim-opencode-hotfix-retirement.md | 21 ++++++++ docs/nvidia-nim-opencode-hotfix.md | 53 ------------------- 2 files changed, 21 insertions(+), 53 deletions(-) create mode 100644 docs/doctoring/nvidia-nim-opencode-hotfix-retirement.md delete mode 100644 docs/nvidia-nim-opencode-hotfix.md diff --git a/docs/doctoring/nvidia-nim-opencode-hotfix-retirement.md b/docs/doctoring/nvidia-nim-opencode-hotfix-retirement.md new file mode 100644 index 0000000000..0036bf10c2 --- /dev/null +++ b/docs/doctoring/nvidia-nim-opencode-hotfix-retirement.md @@ -0,0 +1,21 @@ +# NVIDIA NIM OpenCode hotfix retirement + +## Decision + +The legacy direct-provider OpenCode hotfix is retired. Protected `main` now enables only the `contextual-orchestrator` provider in `opencode.jsonc`, with both normal and small-model review requests routed through `contextual-orchestrator/orchestrator/free`. Direct NVIDIA NIM provider selection is therefore not part of the OpenCode review contract. + +The removed `docs/nvidia-nim-opencode-hotfix.md` described a superseded architecture: direct `nvidia-nim` provider configuration, `NVIDIA_API_KEY` binding, and an administrator-bypass hotfix window. Keeping that document beside the current gateway-only configuration created an operational contradiction and could mislead a maintainer into restoring a retired direct-provider path. + +## Current authority boundary + +- `ContextualWisdomLab/.github` owns the review workflows and gateway integration. +- `opencode.jsonc` enables only `contextual-orchestrator` and denies direct-provider fallback. +- NVIDIA NIM credentials may be registered into contextual-orchestrator's provider-discovery boundary; they are not an OpenCode provider credential or a direct workflow model binding. +- The write-capable scheduled autofix path follows the same gateway-only boundary documented in `docs/doctoring/hourly-nvidia-nim-autofix.md` and ADR-0003. +- Queue-saturation administrator bypass, when separately proven under the current control-plane contract, is an admission-recovery mechanism and must not be documented as a provider-specific hotfix permission. + +## Verification + +This record was created from protected `main@81b6f20d7f701bd2e50642ab107ab0f187ae6dc9`. At that revision, `opencode.jsonc` declares `enabled_providers: ["contextual-orchestrator"]`, uses `contextual-orchestrator/orchestrator/free`, and contains no live `nvidia-nim` provider block. The existing `docs/doctoring/hourly-nvidia-nim-autofix.md` already records the corrected gateway-only provider contract. + +No runtime source, credential, model-selection rule, security threshold, branch-protection rule, or review authority is changed by this documentation cleanup. \ No newline at end of file diff --git a/docs/nvidia-nim-opencode-hotfix.md b/docs/nvidia-nim-opencode-hotfix.md deleted file mode 100644 index df8c193b28..0000000000 --- a/docs/nvidia-nim-opencode-hotfix.md +++ /dev/null @@ -1,53 +0,0 @@ -# NVIDIA NIM OpenCode model priority (hotfix) - -## Why - -OpenCode Agent failed to produce a usable review on the PR thread starting at -ContextualWisdomLab/fast-mlsirm#290 (`opencode-review` check **skipped**, no -`opencode-agent[bot]` review comment). Central review therefore prioritizes -**NVIDIA NIM** models as additional catalog candidates so the model pool can -still emit APPROVE / REQUEST_CHANGES when GitHub Models / free tiers stall. - -## Changes - -1. `opencode.jsonc` - - `enabled_providers`: `nvidia-nim` first, then `github-models` - - default `model` / `small_model` prefer NIM Nemotron / Llama 3.3 - - new OpenAI-compatible provider `nvidia-nim` → `https://integrate.api.nvidia.com/v1` - with `apiKey: {env:NVIDIA_API_KEY}` -2. `.github/workflows/opencode-review-dispatch.yml` - - `OPENCODE_MODEL_CANDIDATES` prefixes six NIM models before existing pool - - binds `NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}` -3. `scripts/ci/run_opencode_review_model_pool.sh` - - skips `nvidia-nim/*` when `NVIDIA_API_KEY` is unset (same pattern as OpenRouter) - -## Temporary permission bypass (hotfix only) - -For this merge-aid hotfix only: - -- Branch-protection / ruleset admin override may be used to land the central - `.github` change if required checks conflict during the hotfix window. -- **Do not** permanently weaken Security Scan, trivy-fs, osv-scan, or - CodeQL gates. -- **Do not** flip OpenCode agent `permission.edit` / `bash` from `deny` to - `allow` permanently; review agents remain read-only. -- Org secret `NVIDIA_API_KEY` must be set on ContextualWisdomLab for NIM pool - entries to execute; without it the pool falls through to prior candidates. - -## Rollback - -Remove the `nvidia-nim/*` prefixes from `OPENCODE_MODEL_CANDIDATES`, drop the -`nvidia-nim` provider block, and delete this note once GitHub Models / OpenCode -catalog reliability is restored. - -## Secret name - -Org secret is **`NVIDIA_NIM_API_KEY`**. Workflows bind it to process env `NVIDIA_API_KEY` -(fallback: `secrets.NVIDIA_API_KEY` if present) so `opencode.jsonc` `{env:NVIDIA_API_KEY}` resolves. - -## Large-repo OpenCode timeouts (~1 hour) - -Primary/default run timeouts and the dynamic queue timeout cap default to -**3600s** (hour-class) so large repositories are not cut off by the old 600s -default when env is unset. Free-tier failover remains capped at 600s. -Workflow-provided values (e.g. 5400s) still win over defaults. From 74166cf08100117f89aea5b363ccb76d772eb4b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:27:52 +0900 Subject: [PATCH 062/154] test(governance): define owner-plane reconciliation contract Add failing-first regression coverage for exact ruleset identity, concurrency refusal, least-privilege workflow gating, post-write convergence, and 100% owned coverage expectations before adding the reconciler implementation. --- .../test_ruleset_governance_reconciliation.py | 485 ++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 tests/test_ruleset_governance_reconciliation.py diff --git a/tests/test_ruleset_governance_reconciliation.py b/tests/test_ruleset_governance_reconciliation.py new file mode 100644 index 0000000000..4355526fbe --- /dev/null +++ b/tests/test_ruleset_governance_reconciliation.py @@ -0,0 +1,485 @@ +"""Regression tests for ruleset owner-plane reconciliation.""" + +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).parents[1] +SCRIPT = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" +SPEC = importlib.util.spec_from_file_location("ruleset_governance", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +module = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = module +SPEC.loader.exec_module(module) + + +def target(scope: str = "repository"): + """Build one valid target for unit tests.""" + + if scope == "organization": + return module.RulesetTarget( + "organization", + "ContextualWisdomLab", + None, + 18156473, + "CWL Central required workflows", + ) + return module.RulesetTarget( + "repository", + "ContextualWisdomLab", + ".github", + 17921150, + "Lock default branch", + ) + + +def live_payload(scope: str = "repository") -> dict[str, object]: + """Build a realistic live ruleset payload with deliberate governance drift.""" + + item = target(scope) + return { + "id": item.ruleset_id, + "name": item.name, + "target": "branch", + "source_type": item.source_type, + "source": item.source, + "enforcement": "active", + "bypass_actors": [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1 if scope == "organization" else 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "require_extra_approval_for_unattributed_changes": True, + "allowed_merge_methods": ["merge", "squash", "rebase"], + }, + }, + ], + "node_id": "server-managed", + } + + +def write_manifest(tmp_path: Path, payload: dict[str, object]) -> Path: + """Write one manifest payload for validation tests.""" + + path = tmp_path / "manifest.json" + path.write_text(json.dumps(payload), encoding="utf-8") + return path + + +def valid_manifest() -> dict[str, object]: + """Return the reviewed two-target manifest shape.""" + + return { + "schema_version": 1, + "organization": "ContextualWisdomLab", + "targets": [ + { + "scope": "repository", + "owner": "ContextualWisdomLab", + "repository": ".github", + "ruleset_id": 17921150, + "name": "Lock default branch", + }, + { + "scope": "organization", + "owner": "ContextualWisdomLab", + "repository": None, + "ruleset_id": 18156473, + "name": "CWL Central required workflows", + }, + ], + } + + +def test_target_endpoints_and_identity_properties() -> None: + """Endpoint/source derivation is exact for both supported ownership scopes.""" + + repo = target() + org = target("organization") + assert repo.endpoint == "repos/ContextualWisdomLab/.github/rulesets/17921150" + assert repo.source == "ContextualWisdomLab/.github" + assert repo.source_type == "Repository" + assert org.endpoint == "orgs/ContextualWisdomLab/rulesets/18156473" + assert org.source == "ContextualWisdomLab" + assert org.source_type == "Organization" + + +def test_load_manifest_accepts_only_exact_reviewed_shape(tmp_path: Path) -> None: + """A valid manifest yields exactly one repository and one organization target.""" + + targets = module.load_manifest(write_manifest(tmp_path, valid_manifest())) + assert [item.scope for item in targets] == ["repository", "organization"] + + +@pytest.mark.parametrize( + ("mutator", "message"), + [ + (lambda payload: payload.update(extra=True), "unexpected key set"), + (lambda payload: payload.update(schema_version=2), "schema or organization"), + (lambda payload: payload.update(organization="other"), "schema or organization"), + (lambda payload: payload.update(targets={}), "targets must be an array"), + (lambda payload: payload.update(targets=[]), "exactly two"), + (lambda payload: payload["targets"][0].update(extra=True), "unexpected key set"), + (lambda payload: payload["targets"][0].update(scope="enterprise"), "scope is unsupported"), + (lambda payload: payload["targets"][0].update(owner="other"), "identity is invalid"), + (lambda payload: payload["targets"][0].update(ruleset_id=True), "identity is invalid"), + (lambda payload: payload["targets"][0].update(ruleset_id=0), "identity is invalid"), + (lambda payload: payload["targets"][0].update(name=""), "name is invalid"), + (lambda payload: payload["targets"][1].update(repository="oops"), "repository must be null"), + (lambda payload: payload["targets"][0].update(repository=None), "must be non-empty"), + ( + lambda payload: payload["targets"].__setitem__(1, dict(payload["targets"][0])), + "duplicate ruleset target", + ), + ( + lambda payload: payload["targets"].__setitem__( + 1, + { + **payload["targets"][0], + "ruleset_id": 999, + "name": "Other repo rule", + }, + ), + "one target per supported scope", + ), + ], +) +def test_load_manifest_rejects_unsafe_shapes(tmp_path: Path, mutator, message: str) -> None: + """Malformed or ambiguously owned ruleset manifests fail closed.""" + + payload = valid_manifest() + mutator(payload) + with pytest.raises(module.RulesetGovernanceError, match=message): + module.load_manifest(write_manifest(tmp_path, payload)) + + +def test_plain_helpers_reject_behavior_bearing_containers() -> None: + """Only built-in JSON container types cross the trust boundary.""" + + class DictSubclass(dict): + pass + + class ListSubclass(list): + pass + + with pytest.raises(module.RulesetGovernanceError, match="object"): + module._plain_dict(DictSubclass(), field="x") + with pytest.raises(module.RulesetGovernanceError, match="array"): + module._plain_list(ListSubclass(), field="x") + + +def test_desired_payload_preserves_unrelated_controls_and_removes_drift() -> None: + """Only governance fields change while every unrelated live rule survives.""" + + live = live_payload("organization") + desired = module._desired_payload(live, target("organization")) + assert desired["bypass_actors"] == [] + assert desired["conditions"] == live["conditions"] + assert desired["rules"][0] == {"type": "deletion"} + params = desired["rules"][2]["parameters"] + assert params["required_approving_review_count"] == 0 + assert params["require_code_owner_review"] is False + assert params["require_last_push_approval"] is False + assert params["required_reviewers"] == [] + assert params["allowed_merge_methods"] == ["merge", "squash"] + assert params["dismiss_stale_reviews_on_push"] is True + assert live["bypass_actors"] + assert "node_id" not in desired + + +@pytest.mark.parametrize("field", ["id", "name", "target", "source_type", "source", "enforcement"]) +def test_desired_payload_rejects_identity_drift(field: str) -> None: + """A renamed, re-scoped, disabled, or replaced live ruleset is never overwritten.""" + + live = live_payload() + live[field] = "wrong" + with pytest.raises(module.RulesetGovernanceError, match="identity drift"): + module._desired_payload(live, target()) + + +def test_projection_requires_all_editable_fields_and_container_shapes() -> None: + """Missing or behavior-bearing update fields fail before an API mutation.""" + + live = live_payload() + del live["rules"] + with pytest.raises(module.RulesetGovernanceError, match="misses editable fields"): + module._editable_projection(live) + live = live_payload() + live["bypass_actors"] = {} + with pytest.raises(module.RulesetGovernanceError, match="bypass_actors must be an array"): + module._editable_projection(live) + live = live_payload() + live["conditions"] = [] + with pytest.raises(module.RulesetGovernanceError, match="conditions must be an object"): + module._editable_projection(live) + live = live_payload() + live["rules"] = {} + with pytest.raises(module.RulesetGovernanceError, match="rules must be an array"): + module._editable_projection(live) + + +def test_desired_payload_requires_exactly_one_pull_request_rule() -> None: + """Absent or duplicate pull-request controls cannot be guessed during reconciliation.""" + + live = live_payload() + live["rules"] = [{"type": "deletion"}] + with pytest.raises(module.RulesetGovernanceError, match="exactly one"): + module._desired_payload(live, target()) + live = live_payload() + live["rules"].append(copy_rule := dict(live["rules"][2])) + assert copy_rule["type"] == "pull_request" + with pytest.raises(module.RulesetGovernanceError, match="exactly one"): + module._desired_payload(live, target()) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("required_approving_review_count", True), + ("require_code_owner_review", 0), + ("require_last_push_approval", 0), + ("required_reviewers", {}), + ("allowed_merge_methods", {}), + ], +) +def test_desired_payload_rejects_malformed_pull_request_parameters(field: str, value) -> None: + """Typed GitHub pull-request parameters are validated before modification.""" + + live = live_payload() + live["rules"][2]["parameters"][field] = value + with pytest.raises(module.RulesetGovernanceError, match=field): + module._desired_payload(live, target()) + + +def test_desired_payload_rejects_non_object_parameters() -> None: + """The pull-request parameter object cannot be replaced by another JSON type.""" + + live = live_payload() + live["rules"][2]["parameters"] = [] + with pytest.raises(module.RulesetGovernanceError, match="must be an object"): + module._desired_payload(live, target()) + + +def test_reconcile_target_skips_already_converged_state(monkeypatch) -> None: + """An already compliant live ruleset performs no write in either mode.""" + + live = live_payload() + live = {**live, **module._desired_payload(live, target())} + calls = [] + + def fake_api(method, endpoint, *, body=None): + calls.append((method, endpoint, body)) + return live + + monkeypatch.setattr(module, "_gh_api", fake_api) + assert module._reconcile_target(target(), verify_only=False) is False + assert calls == [("GET", target().endpoint, None)] + + +def test_verify_only_fails_on_drift_without_writing(monkeypatch) -> None: + """Verification mode reports drift and never sends an update request.""" + + calls = [] + + def fake_api(method, endpoint, *, body=None): + calls.append((method, endpoint, body)) + return live_payload() + + monkeypatch.setattr(module, "_gh_api", fake_api) + with pytest.raises(module.RulesetGovernanceError, match="governance drift remains"): + module._reconcile_target(target(), verify_only=True) + assert [item[0] for item in calls] == ["GET"] + + +def test_apply_rechecks_for_concurrent_drift_before_put(monkeypatch) -> None: + """A settings race aborts rather than overwriting another administrator's change.""" + + first = live_payload() + second = live_payload() + second["conditions"] = {"ref_name": {"include": ["refs/heads/reviewed"], "exclude": []}} + replies = iter([first, second]) + monkeypatch.setattr(module, "_gh_api", lambda *args, **kwargs: next(replies)) + with pytest.raises(module.RulesetGovernanceError, match="changed concurrently"): + module._reconcile_target(target(), verify_only=False) + + +def test_apply_mutates_once_and_verifies_exact_convergence(monkeypatch) -> None: + """A stable target is updated once and post-write live state must equal the reviewed body.""" + + first = live_payload() + desired = module._desired_payload(first, target()) + converged = {**first, **desired} + calls = [] + replies = iter([first, first, {}, converged]) + + def fake_api(method, endpoint, *, body=None): + calls.append((method, endpoint, body)) + return next(replies) + + monkeypatch.setattr(module, "_gh_api", fake_api) + assert module._reconcile_target(target(), verify_only=False) is True + assert [item[0] for item in calls] == ["GET", "GET", "PUT", "GET"] + assert calls[2][2] == desired + + +def test_apply_rejects_post_write_nonconvergence(monkeypatch) -> None: + """A successful HTTP update is not accepted until the full editable payload converges.""" + + first = live_payload() + replies = iter([first, first, {}, first]) + monkeypatch.setattr(module, "_gh_api", lambda *args, **kwargs: next(replies)) + with pytest.raises(module.RulesetGovernanceError, match="did not converge"): + module._reconcile_target(target(), verify_only=False) + + +def test_apply_rejects_post_write_identity_replacement(monkeypatch) -> None: + """A ruleset replacement after PUT fails the exact-identity verification.""" + + first = live_payload() + replaced = live_payload() + replaced["name"] = "replacement" + replies = iter([first, first, {}, replaced]) + monkeypatch.setattr(module, "_gh_api", lambda *args, **kwargs: next(replies)) + with pytest.raises(module.RulesetGovernanceError, match="identity drift"): + module._reconcile_target(target(), verify_only=False) + + +def test_reconcile_orders_repository_before_organization(monkeypatch) -> None: + """The strengthening repository mutation precedes the organization approval change.""" + + seen = [] + monkeypatch.setattr( + module, + "_reconcile_target", + lambda item, verify_only: seen.append((item.scope, verify_only)) or True, + ) + targets = (target("organization"), target("repository")) + assert module.reconcile(targets, verify_only=False) == 2 + assert seen == [("repository", False), ("organization", False)] + + +def test_gh_api_uses_versioned_stdin_body_and_redacts_failure(monkeypatch) -> None: + """REST calls pin the API version, send JSON on stdin, and hide subprocess diagnostics.""" + + observed = {} + + def fake_run(command, **kwargs): + observed["command"] = command + observed.update(kwargs) + return SimpleNamespace(returncode=0, stdout='{"ok":true}', stderr="") + + monkeypatch.setattr(subprocess, "run", fake_run) + assert module._gh_api("PUT", "endpoint", body={"x": 1}) == {"ok": True} + assert f"X-GitHub-Api-Version: {module.API_VERSION}" in observed["command"] + assert observed["input"] == '{"x":1}' + assert "--input" in observed["command"] + + monkeypatch.setattr( + subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace( + returncode=1, stdout="token-like-output", stderr="secret-like-error" + ), + ) + with pytest.raises(module.RulesetGovernanceError, match="GitHub API request failed") as caught: + module._gh_api("GET", "endpoint") + assert "secret-like-error" not in str(caught.value) + + +def test_main_validation_needs_no_token_and_live_modes_do(tmp_path: Path, monkeypatch, capsys) -> None: + """PR validation is offline while apply/verify require the protected credential.""" + + manifest = write_manifest(tmp_path, valid_manifest()) + monkeypatch.delenv("GH_TOKEN", raising=False) + assert module.main(["--manifest", str(manifest), "--validate-only"]) == 0 + assert "validated 2" in capsys.readouterr().out + with pytest.raises(module.RulesetGovernanceError, match="GH_TOKEN is required"): + module.main(["--manifest", str(manifest)]) + + monkeypatch.setenv("GH_TOKEN", "not-printed") + monkeypatch.setattr(module, "reconcile", lambda targets, verify_only: 0) + assert module.main(["--manifest", str(manifest), "--verify-only"]) == 0 + assert "verified 2" in capsys.readouterr().out + assert module.main(["--manifest", str(manifest)]) == 0 + assert "reconciled 2" in capsys.readouterr().out + + +def test_cli_entrypoint_reports_success_and_failure(tmp_path: Path) -> None: + """The executable entry point maps validation success and unsafe input to exit status.""" + + valid = write_manifest(tmp_path, valid_manifest()) + success = subprocess.run( + [sys.executable, str(SCRIPT), "--manifest", str(valid), "--validate-only"], + check=False, + capture_output=True, + text=True, + ) + assert success.returncode == 0 + assert "validated 2" in success.stdout + + invalid = tmp_path / "invalid.json" + invalid.write_text("{", encoding="utf-8") + failure = subprocess.run( + [sys.executable, str(SCRIPT), "--manifest", str(invalid), "--validate-only"], + check=False, + capture_output=True, + text=True, + ) + assert failure.returncode == 1 + assert "ruleset governance reconciliation failed" in failure.stderr + + +def test_cli_function_maps_main_result_and_expected_failures(monkeypatch, capsys) -> None: + """The testable CLI boundary exits cleanly and redacts expected failures.""" + + monkeypatch.setattr(module, "main", lambda: 0) + with pytest.raises(SystemExit) as success: + module.cli() + assert success.value.code == 0 + + def fail(): + raise module.RulesetGovernanceError("unsafe") + + monkeypatch.setattr(module, "main", fail) + with pytest.raises(SystemExit) as failure: + module.cli() + assert failure.value.code == 1 + assert "ruleset governance reconciliation failed: unsafe" in capsys.readouterr().err + + +def test_workflow_separates_unprivileged_validation_from_owner_plane_apply() -> None: + """Only trusted main plus an explicit enable flag can enter the privileged environment.""" + + workflow = (ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml").read_text( + encoding="utf-8" + ) + assert "pull_request:" in workflow + assert "schedule:" in workflow + assert 'github.ref == \'refs/heads/main\'' in workflow + assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in workflow + assert "environment: ruleset-governance-maintenance" in workflow + assert "secrets.CWL_RULESET_ADMIN_TOKEN" in workflow + assert "--validate-only" in workflow + assert "--verify-only" in workflow + assert "permissions:\n contents: read" in workflow + assert "persist-credentials: false" in workflow + assert "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" in workflow + assert "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" in workflow From 946e15cda31db8bb06e96f7f49386deabc7aeabf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:28:25 +0900 Subject: [PATCH 063/154] feat(governance): add fail-closed ruleset reconciler Implement exact-identity ruleset reconciliation with versioned REST calls, pre-write race detection, least-change payload projection, no routine bypass, solo-maintainer review controls, and post-write convergence verification. --- scripts/ci/reconcile_ruleset_governance.py | 319 +++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 scripts/ci/reconcile_ruleset_governance.py diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py new file mode 100644 index 0000000000..54033096d8 --- /dev/null +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +"""Reconcile reviewed GitHub ruleset governance with fail-closed verification. + +The reconciler manages only the two rulesets declared in a reviewed manifest. It +preserves every live condition and non-governance rule, while atomically removing +routine bypass actors and canonicalizing the pull-request controls required by the +one-human-maintainer operating model. Live state is re-read immediately before a +mutation so a concurrent settings change is never silently overwritten. +""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +API_VERSION = "2026-03-10" +ORGANIZATION = "ContextualWisdomLab" +DESIRED_MERGE_METHODS = ["merge", "squash"] + + +class RulesetGovernanceError(RuntimeError): + """Raised when desired-state validation or live reconciliation is unsafe.""" + + +@dataclass(frozen=True) +class RulesetTarget: + """Describe one exact organization- or repository-owned ruleset.""" + + scope: str + owner: str + repository: str | None + ruleset_id: int + name: str + + @property + def endpoint(self) -> str: + """Return the GitHub REST endpoint for this exact ruleset.""" + + if self.scope == "organization": + return f"orgs/{self.owner}/rulesets/{self.ruleset_id}" + return f"repos/{self.owner}/{self.repository}/rulesets/{self.ruleset_id}" + + @property + def source(self) -> str: + """Return the exact source identity GitHub must report for this ruleset.""" + + if self.scope == "organization": + return self.owner + return f"{self.owner}/{self.repository}" + + @property + def source_type(self) -> str: + """Return GitHub's expected source type for this ruleset scope.""" + + return "Organization" if self.scope == "organization" else "Repository" + + +def _plain_dict(value: Any, *, field: str) -> dict[str, Any]: + """Return a plain dictionary or reject behavior-bearing mapping objects.""" + + if type(value) is not dict: + raise RulesetGovernanceError(f"{field} must be an object") + return value + + +def _plain_list(value: Any, *, field: str) -> list[Any]: + """Return a plain list or reject behavior-bearing sequence objects.""" + + if type(value) is not list: + raise RulesetGovernanceError(f"{field} must be an array") + return value + + +def load_manifest(path: Path) -> tuple[RulesetTarget, ...]: + """Load and strictly validate the reviewed ruleset target manifest.""" + + root = _plain_dict(json.loads(path.read_text(encoding="utf-8")), field="manifest") + if set(root) != {"schema_version", "organization", "targets"}: + raise RulesetGovernanceError("manifest has an unexpected key set") + if root["schema_version"] != 1 or root["organization"] != ORGANIZATION: + raise RulesetGovernanceError("manifest schema or organization is unsupported") + + raw_targets = _plain_list(root["targets"], field="targets") + if len(raw_targets) != 2: + raise RulesetGovernanceError("manifest must declare exactly two governance targets") + + targets: list[RulesetTarget] = [] + identities: set[tuple[str, int]] = set() + for index, raw in enumerate(raw_targets): + item = _plain_dict(raw, field=f"targets[{index}]") + if set(item) != {"scope", "owner", "repository", "ruleset_id", "name"}: + raise RulesetGovernanceError(f"targets[{index}] has an unexpected key set") + scope = item["scope"] + owner = item["owner"] + repository = item["repository"] + ruleset_id = item["ruleset_id"] + name = item["name"] + if scope not in {"organization", "repository"}: + raise RulesetGovernanceError(f"targets[{index}].scope is unsupported") + if owner != ORGANIZATION or type(ruleset_id) is not int or ruleset_id <= 0: + raise RulesetGovernanceError(f"targets[{index}] identity is invalid") + if type(name) is not str or not name.strip(): + raise RulesetGovernanceError(f"targets[{index}].name is invalid") + if scope == "organization" and repository is not None: + raise RulesetGovernanceError("organization target repository must be null") + if scope == "repository" and (type(repository) is not str or not repository): + raise RulesetGovernanceError("repository target repository must be non-empty") + identity = (scope, ruleset_id) + if identity in identities: + raise RulesetGovernanceError("manifest contains a duplicate ruleset target") + identities.add(identity) + targets.append(RulesetTarget(scope, owner, repository, ruleset_id, name)) + + if {target.scope for target in targets} != {"organization", "repository"}: + raise RulesetGovernanceError("manifest must contain one target per supported scope") + return tuple(targets) + + +def _gh_api( + method: str, + endpoint: str, + *, + body: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Call GitHub's versioned REST API without exposing credential material.""" + + command = [ + "gh", + "api", + "--method", + method, + "-H", + f"X-GitHub-Api-Version: {API_VERSION}", + endpoint, + ] + if body is not None: + command.extend(["--input", "-"]) + completed = subprocess.run( + command, + check=False, + input=None if body is None else json.dumps(body, separators=(",", ":")), + capture_output=True, + text=True, + timeout=30, + ) + if completed.returncode != 0: + raise RulesetGovernanceError(f"GitHub API request failed for {endpoint}") + return _plain_dict( + json.loads(completed.stdout), field=f"GitHub response for {endpoint}" + ) + + +def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: + """Require the live ruleset to be the exact reviewed object before mutation.""" + + expected = { + "id": target.ruleset_id, + "name": target.name, + "target": "branch", + "source_type": target.source_type, + "source": target.source, + "enforcement": "active", + } + mismatches = [key for key, value in expected.items() if live.get(key) != value] + if mismatches: + raise RulesetGovernanceError( + f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" + ) + + +def _editable_projection(live: dict[str, Any]) -> dict[str, Any]: + """Return exactly the fields accepted by GitHub's ruleset update endpoint.""" + + required = { + "name", + "target", + "enforcement", + "bypass_actors", + "conditions", + "rules", + } + missing = sorted(required.difference(live)) + if missing: + raise RulesetGovernanceError( + f"ruleset payload misses editable fields: {', '.join(missing)}" + ) + projection = {key: copy.deepcopy(live[key]) for key in required} + _plain_list(projection["bypass_actors"], field="bypass_actors") + _plain_dict(projection["conditions"], field="conditions") + _plain_list(projection["rules"], field="rules") + return projection + + +def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, Any]: + """Build the exact safe update body while preserving unrelated live controls.""" + + _assert_identity(live, target) + desired = _editable_projection(live) + desired["bypass_actors"] = [] + + pull_request_rules = [ + rule + for rule in desired["rules"] + if type(rule) is dict and rule.get("type") == "pull_request" + ] + if len(pull_request_rules) != 1: + raise RulesetGovernanceError("ruleset must contain exactly one pull_request rule") + parameters = _plain_dict( + pull_request_rules[0].get("parameters"), field="pull_request.parameters" + ) + required_parameters = { + "required_approving_review_count": int, + "require_code_owner_review": bool, + "require_last_push_approval": bool, + "required_reviewers": list, + "allowed_merge_methods": list, + } + for field, expected_type in required_parameters.items(): + if type(parameters.get(field)) is not expected_type: + raise RulesetGovernanceError( + f"pull_request.parameters.{field} has invalid type" + ) + + parameters["required_approving_review_count"] = 0 + parameters["require_code_owner_review"] = False + parameters["require_last_push_approval"] = False + parameters["required_reviewers"] = [] + parameters["allowed_merge_methods"] = list(DESIRED_MERGE_METHODS) + return desired + + +def _reconcile_target(target: RulesetTarget, *, verify_only: bool) -> bool: + """Verify or reconcile one target; return whether a mutation was performed.""" + + first = _gh_api("GET", target.endpoint) + desired = _desired_payload(first, target) + if _editable_projection(first) == desired: + return False + if verify_only: + raise RulesetGovernanceError( + f"{target.scope} ruleset governance drift remains" + ) + + second = _gh_api("GET", target.endpoint) + if _editable_projection(second) != _editable_projection(first): + raise RulesetGovernanceError( + f"{target.scope} ruleset changed concurrently; refusing to overwrite" + ) + _assert_identity(second, target) + _gh_api("PUT", target.endpoint, body=desired) + after = _gh_api("GET", target.endpoint) + _assert_identity(after, target) + if _editable_projection(after) != desired: + raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") + return True + + +def reconcile(targets: tuple[RulesetTarget, ...], *, verify_only: bool) -> int: + """Reconcile all targets and return the number of successful mutations.""" + + mutations = 0 + for target in sorted(targets, key=lambda item: item.scope == "organization"): + mutations += int(_reconcile_target(target, verify_only=verify_only)) + return mutations + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments for validation, apply, or verification.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--manifest", + type=Path, + default=Path("config/ruleset-governance.json"), + help="Reviewed desired-state target manifest.", + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--validate-only", action="store_true") + mode.add_argument("--verify-only", action="store_true") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Validate the manifest or reconcile exact live ruleset governance.""" + + args = _parse_args(argv) + targets = load_manifest(args.manifest) + if args.validate_only: + print(f"validated {len(targets)} ruleset governance targets") + return 0 + if not os.environ.get("GH_TOKEN"): + raise RulesetGovernanceError( + "GH_TOKEN is required for live ruleset governance" + ) + mutations = reconcile(targets, verify_only=args.verify_only) + verb = "verified" if args.verify_only else "reconciled" + print(f"{verb} {len(targets)} ruleset governance targets; mutations={mutations}") + return 0 + + +def cli() -> None: + """Execute the command-line boundary with concise fail-closed diagnostics.""" + + try: + raise SystemExit(main()) + except (OSError, json.JSONDecodeError, RulesetGovernanceError) as exc: + print(f"ruleset governance reconciliation failed: {exc}", file=sys.stderr) + raise SystemExit(1) from exc + + +if __name__ == "__main__": # pragma: no cover - exercised by subprocess smoke test + cli() From 1966b12beb95239dd40d5f776c351cdbcf45046f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:30:16 +0900 Subject: [PATCH 064/154] config(governance): bind exact ruleset targets Declare only the existing central repository and organization rulesets that the owner-plane reconciler may verify or mutate after independent credential provisioning. --- config/ruleset-governance.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 config/ruleset-governance.json diff --git a/config/ruleset-governance.json b/config/ruleset-governance.json new file mode 100644 index 0000000000..9a315c98c1 --- /dev/null +++ b/config/ruleset-governance.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "organization": "ContextualWisdomLab", + "targets": [ + { + "scope": "repository", + "owner": "ContextualWisdomLab", + "repository": ".github", + "ruleset_id": 17921150, + "name": "Lock default branch" + }, + { + "scope": "organization", + "owner": "ContextualWisdomLab", + "repository": null, + "ruleset_id": 18156473, + "name": "CWL Central required workflows" + } + ] +} From fc95c34cf2a40c094b047ba1a22293f6fad7ef54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:30:32 +0900 Subject: [PATCH 065/154] ci(governance): gate ruleset reconciliation in owner plane Validate on PRs with read-only permissions and 100% owned coverage/docstrings. Permit mutation only from trusted main when an explicit enable variable and separately protected administration credential exist. --- .../ruleset-governance-reconcile.yml | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/ruleset-governance-reconcile.yml diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml new file mode 100644 index 0000000000..4f410cb467 --- /dev/null +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -0,0 +1,127 @@ +name: Ruleset Governance Reconcile + +on: + pull_request: + paths: + - "config/ruleset-governance.json" + - "scripts/ci/reconcile_ruleset_governance.py" + - "tests/test_ruleset_governance_reconciliation.py" + - "docs/doctoring/ruleset-owner-plane-reconciliation.md" + - ".github/workflows/ruleset-governance-reconcile.yml" + push: + branches: + - main + paths: + - "config/ruleset-governance.json" + - "scripts/ci/reconcile_ruleset_governance.py" + - "tests/test_ruleset_governance_reconciliation.py" + - "docs/doctoring/ruleset-owner-plane-reconciliation.md" + - ".github/workflows/ruleset-governance-reconcile.yml" + workflow_dispatch: + schedule: + - cron: "31 * * * *" + +permissions: + contents: read + +concurrency: + group: ruleset-governance-reconcile-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + validate: + if: >- + github.event_name == 'pull_request' || + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + vars.CWL_RULESET_RECONCILE_ENABLED == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Check out exact revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Verify exact revision + shell: bash + run: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Install hash-locked test tooling + run: >- + python -m pip install --disable-pip-version-check --require-hashes + --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + - name: Validate reviewed target manifest + run: >- + python scripts/ci/reconcile_ruleset_governance.py + --manifest config/ruleset-governance.json + --validate-only + - name: Prove reconciliation contract at repository quality gates + env: + COVERAGE_RCFILE: /dev/null + run: | + set -euo pipefail + python -m coverage run \ + --branch \ + --include=scripts/ci/reconcile_ruleset_governance.py \ + -m pytest -q tests/test_ruleset_governance_reconciliation.py + python -m coverage report \ + --fail-under=100 \ + --show-missing \ + --include=scripts/ci/reconcile_ruleset_governance.py + python -m interrogate \ + --fail-under 100 \ + scripts/ci/reconcile_ruleset_governance.py + git diff --check + + apply: + if: >- + github.event_name != 'pull_request' && + github.ref == 'refs/heads/main' && + vars.CWL_RULESET_RECONCILE_ENABLED == 'true' + needs: validate + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: ruleset-governance-maintenance + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Check out trusted default branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Verify exact trusted revision + shell: bash + run: test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Require dedicated ruleset administration credential + env: + GH_TOKEN: ${{ secrets.CWL_RULESET_ADMIN_TOKEN }} + shell: bash + run: test -n "${GH_TOKEN}" + - name: Reconcile reviewed ruleset governance + env: + GH_TOKEN: ${{ secrets.CWL_RULESET_ADMIN_TOKEN }} + run: >- + python scripts/ci/reconcile_ruleset_governance.py + --manifest config/ruleset-governance.json + - name: Verify post-change convergence from GitHub + env: + GH_TOKEN: ${{ secrets.CWL_RULESET_ADMIN_TOKEN }} + run: >- + python scripts/ci/reconcile_ruleset_governance.py + --manifest config/ruleset-governance.json + --verify-only From 4e3f6ae063e3c3bde74abbb6c121a111f19d77a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:30:54 +0900 Subject: [PATCH 066/154] docs(governance): record owner-plane reconciliation evidence Document the live Orgmetra/.github drift, exact mutation boundary, concurrency and convergence invariants, least-privilege credential design, canary sequence, and standards/research basis. --- .../ruleset-owner-plane-reconciliation.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/doctoring/ruleset-owner-plane-reconciliation.md diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md new file mode 100644 index 0000000000..22136b8b80 --- /dev/null +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -0,0 +1,52 @@ +# Ruleset owner-plane reconciliation + +Date reviewed: 2026-09-02 + +## Incident and product impact + +Orgmetra's protected `develop` is currently governed by organization ruleset `18156473`, while the central `.github` default branch also has repository ruleset `17921150`. Live reads on 2026-09-02 showed two policy drifts that block a defensible ordinary merge path: the organization ruleset still requires one generic approval even though the current operating model has one human maintainer and it retains `OrganizationAdmin/always`; the `.github` repository ruleset already has approval count zero but still permits rebase and also retains `OrganizationAdmin/always`. + +The application connector can read those settings but does not expose ruleset mutation. Administrator bypass is not an acceptable substitute because it would destroy the canary needed to prove the normal path. The owner-plane repair therefore needs reviewed configuration-as-code plus a separately provisioned, narrowly scoped credential rather than an application-side shim. + +## Design decision + +`config/ruleset-governance.json` binds the reconciler to exactly two existing ruleset identities. `scripts/ci/reconcile_ruleset_governance.py` reads the full live object and refuses to act if the ID, name, source, source type, branch target, or active enforcement state has changed. It then preserves all unrelated conditions and rules while changing only these reviewed governance fields: + +- remove routine bypass actors; +- set generic approving-review count to zero; +- keep same-author CODEOWNER review disabled; +- keep last-push approval disabled; +- require no synthetic reviewer identities; and +- allow merge and squash only, removing rebase. + +The reconciler performs a second full read immediately before PUT and refuses to overwrite concurrent configuration drift. A successful HTTP update is not completion: it performs a third read and requires the complete editable payload to equal the reviewed update body. Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. + +Pull requests execute only offline manifest validation plus 100% statement/branch/docstring contract tests. Mutation can run only from trusted `main`, in the `ruleset-governance-maintenance` protected environment, when repository variable `CWL_RULESET_RECONCILE_ENABLED=true` is explicitly set, using dedicated secret `CWL_RULESET_ADMIN_TOKEN`. The normal workflow token remains `contents: read`, checkout credentials are not persisted, and API errors never echo subprocess output that could contain sensitive context. + +`CWL_RULESET_ADMIN_TOKEN` is intentionally distinct from `CWL_REPOSITORY_METADATA_TOKEN`. GitHub documents organization-ruleset update as requiring organization **Administration (write)** and repository-ruleset update as requiring repository **Administration (write)**. The credential must carry only those permissions needed for the two declared targets; it must not be repurposed as a general development, merge, release, or metadata token. The environment and secret must be provisioned independently before `CWL_RULESET_RECONCILE_ENABLED` is set. Source integration alone does not prove that provisioning exists. + +## Verification sequence + +1. PR validation proves manifest, mutation projection, concurrency refusal, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable. +3. Run the reconciler and re-read both complete live ruleset payloads using the independently authorized credential. +4. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. +5. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. +6. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as Orgmetra #88; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. +7. Keep the reconciler enabled for drift repair only after the first successful canary. If the API identity or editable schema changes, it fails closed and requires a reviewed source update. + +## Standards and research basis + +GitHub's current REST documentation makes ruleset mutation an Administration-authorized operation and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. + +### References (APA 7th) + +GitHub. (2026). *REST API endpoints for rules: Organizations*. GitHub Docs. https://docs.github.com/en/rest/orgs/rules + +GitHub. (2026). *REST API endpoints for rules: Repositories*. GitHub Docs. https://docs.github.com/en/rest/repos/rules + +GitHub. (2026). *Managing rulesets for repositories in your organization*. GitHub Docs. https://docs.github.com/en/organizations/managing-organization-settings/managing-rulesets-for-repositories-in-your-organization + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-53r5 + +Sinan, M., Shahin, M., & Gondal, I. (2025). Integrating security controls in DevSecOps: Challenges, solutions, and future research directions. *Journal of Software: Evolution and Process, 37*, e70029. https://doi.org/10.1002/smr.70029 From b77dd5392f32fda9e8b7ead0c40ac3e34aba2da4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:48:53 +0900 Subject: [PATCH 067/154] test(governance): reproduce stale-main and non-CAS review findings --- ...t_ruleset_governance_review_regressions.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_ruleset_governance_review_regressions.py diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py new file mode 100644 index 0000000000..ee36c88263 --- /dev/null +++ b/tests/test_ruleset_governance_review_regressions.py @@ -0,0 +1,96 @@ +"""Regressions for adversarial review findings on ruleset reconciliation.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).parents[1] +SCRIPT = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" +WORKFLOW = ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml" +DOCTORING = ROOT / "docs" / "doctoring" / "ruleset-owner-plane-reconciliation.md" +SPEC = importlib.util.spec_from_file_location("ruleset_governance_review", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +module = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = module +SPEC.loader.exec_module(module) + + +def test_stale_main_revision_fails_before_any_ruleset_mutation(monkeypatch) -> None: + """A resumed owner-plane run cannot apply policy from an obsolete main SHA.""" + + target = module.RulesetTarget( + "repository", + "ContextualWisdomLab", + ".github", + 17921150, + "Lock default branch", + ) + live = { + "id": target.ruleset_id, + "name": target.name, + "target": "branch", + "source_type": target.source_type, + "source": target.source, + "enforcement": "active", + "bypass_actors": [{"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"}], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "require_extra_approval_for_unattributed_changes": True, + "allowed_merge_methods": ["merge", "squash", "rebase"], + }, + }, + ], + } + calls: list[str] = [] + + monkeypatch.setattr(module, "_gh_api", lambda method, endpoint, **kwargs: calls.append(method) or live) + monkeypatch.setattr(module, "_current_main_sha", lambda: "b" * 40) + + with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): + module._reconcile_target( + target, + verify_only=False, + expected_main_sha="a" * 40, + ) + assert calls == ["GET"] + + +def test_owner_plane_workflow_supersedes_stale_runs_and_quotes_main_sha() -> None: + """Only the newest trusted-main owner-plane run may reach mutation.""" + + text = WORKFLOW.read_text(encoding="utf-8") + assert "cancel-in-progress: true" in text + assert "EXPECTED_MAIN_SHA: ${{ github.sha }}" in text + assert '--expected-main-sha "$EXPECTED_MAIN_SHA"' in text + assert "github.event_name != 'pull_request'" in text + + +def test_docs_state_github_has_no_ruleset_put_compare_and_swap() -> None: + """Doctoring must not claim an atomic compare-and-swap GitHub does not provide.""" + + text = DOCTORING.read_text(encoding="utf-8") + assert "does not support conditional unsafe REST updates" in text + assert "exclusive owner-plane maintenance" in text + assert "cannot make the final GET-to-PUT interval atomic" in text + + +def test_module_docstring_does_not_promise_impossible_atomicity() -> None: + """Production documentation must describe best-effort drift checks, not CAS semantics.""" + + assert "never silently overwritten" not in (module.__doc__ or "") From ca8a42cb870c67a457f168a4cd5cb0cb43d09484 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:50:33 +0900 Subject: [PATCH 068/154] fix(governance): bind reconciliation to current protected main --- scripts/ci/reconcile_ruleset_governance.py | 86 ++++++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 54033096d8..6d1cd589ef 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -2,10 +2,12 @@ """Reconcile reviewed GitHub ruleset governance with fail-closed verification. The reconciler manages only the two rulesets declared in a reviewed manifest. It -preserves every live condition and non-governance rule, while atomically removing -routine bypass actors and canonicalizing the pull-request controls required by the -one-human-maintainer operating model. Live state is re-read immediately before a -mutation so a concurrent settings change is never silently overwritten. +preserves live conditions and non-governance rules while canonicalizing the +reviewed pull-request controls. GitHub does not provide conditional PUT/PATCH +semantics for this endpoint, so the second live read is a drift detector rather +than a compare-and-swap guarantee. Privileged mutation is additionally bound to +the exact protected-main revision and a serialized protected owner-plane run, +with full post-write convergence verification. """ from __future__ import annotations @@ -14,6 +16,7 @@ import copy import json import os +import re import subprocess import sys from dataclasses import dataclass @@ -22,7 +25,9 @@ API_VERSION = "2026-03-10" ORGANIZATION = "ContextualWisdomLab" +CONTROL_REPOSITORY = "ContextualWisdomLab/.github" DESIRED_MERGE_METHODS = ["merge", "squash"] +GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") class RulesetGovernanceError(RuntimeError): @@ -157,6 +162,28 @@ def _gh_api( ) +def _current_main_sha() -> str: + """Return the exact protected control-repository main SHA from GitHub.""" + + payload = _gh_api("GET", f"repos/{CONTROL_REPOSITORY}/git/ref/heads/main") + object_data = _plain_dict(payload.get("object"), field="main ref object") + sha = str(object_data.get("sha") or "").lower() + if not GIT_SHA_RE.fullmatch(sha): + raise RulesetGovernanceError("protected main returned a malformed SHA") + return sha + + +def _assert_current_main(expected_main_sha: str) -> None: + """Fail closed when the privileged run no longer represents current main.""" + + if not GIT_SHA_RE.fullmatch(expected_main_sha): + raise RulesetGovernanceError("expected protected main SHA is malformed") + if _current_main_sha() != expected_main_sha: + raise RulesetGovernanceError( + "protected main advanced; refusing stale governance mutation" + ) + + def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: """Require the live ruleset to be the exact reviewed object before mutation.""" @@ -236,7 +263,12 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A return desired -def _reconcile_target(target: RulesetTarget, *, verify_only: bool) -> bool: +def _reconcile_target( + target: RulesetTarget, + *, + verify_only: bool, + expected_main_sha: str | None = None, +) -> bool: """Verify or reconcile one target; return whether a mutation was performed.""" first = _gh_api("GET", target.endpoint) @@ -248,26 +280,45 @@ def _reconcile_target(target: RulesetTarget, *, verify_only: bool) -> bool: f"{target.scope} ruleset governance drift remains" ) + if expected_main_sha is not None: + _assert_current_main(expected_main_sha) second = _gh_api("GET", target.endpoint) if _editable_projection(second) != _editable_projection(first): raise RulesetGovernanceError( f"{target.scope} ruleset changed concurrently; refusing to overwrite" ) _assert_identity(second, target) + if expected_main_sha is not None: + _assert_current_main(expected_main_sha) _gh_api("PUT", target.endpoint, body=desired) after = _gh_api("GET", target.endpoint) _assert_identity(after, target) if _editable_projection(after) != desired: raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") + if expected_main_sha is not None: + _assert_current_main(expected_main_sha) return True -def reconcile(targets: tuple[RulesetTarget, ...], *, verify_only: bool) -> int: +def reconcile( + targets: tuple[RulesetTarget, ...], + *, + verify_only: bool, + expected_main_sha: str | None = None, +) -> int: """Reconcile all targets and return the number of successful mutations.""" mutations = 0 for target in sorted(targets, key=lambda item: item.scope == "organization"): - mutations += int(_reconcile_target(target, verify_only=verify_only)) + if expected_main_sha is None: + mutated = _reconcile_target(target, verify_only=verify_only) + else: + mutated = _reconcile_target( + target, + verify_only=verify_only, + expected_main_sha=expected_main_sha, + ) + mutations += int(mutated) return mutations @@ -281,6 +332,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: default=Path("config/ruleset-governance.json"), help="Reviewed desired-state target manifest.", ) + parser.add_argument( + "--expected-main-sha", + help="Exact trusted protected-main SHA for privileged mutation.", + ) mode = parser.add_mutually_exclusive_group() mode.add_argument("--validate-only", action="store_true") mode.add_argument("--verify-only", action="store_true") @@ -299,7 +354,22 @@ def main(argv: list[str] | None = None) -> int: raise RulesetGovernanceError( "GH_TOKEN is required for live ruleset governance" ) - mutations = reconcile(targets, verify_only=args.verify_only) + if ( + not args.verify_only + and os.environ.get("GITHUB_ACTIONS") == "true" + and not args.expected_main_sha + ): + raise RulesetGovernanceError( + "expected protected main SHA is required for Actions mutation" + ) + if args.expected_main_sha is None: + mutations = reconcile(targets, verify_only=args.verify_only) + else: + mutations = reconcile( + targets, + verify_only=args.verify_only, + expected_main_sha=args.expected_main_sha, + ) verb = "verified" if args.verify_only else "reconciled" print(f"{verb} {len(targets)} ruleset governance targets; mutations={mutations}") return 0 From de0e3389215328c21cf4bfd567fa160433a109e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:50:49 +0900 Subject: [PATCH 069/154] fix(governance): supersede stale owner-plane apply runs --- .github/workflows/ruleset-governance-reconcile.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index 4f410cb467..6e8318ccdc 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -6,6 +6,7 @@ on: - "config/ruleset-governance.json" - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" + - "tests/test_ruleset_governance_review_regressions.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" push: @@ -15,6 +16,7 @@ on: - "config/ruleset-governance.json" - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" + - "tests/test_ruleset_governance_review_regressions.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" workflow_dispatch: @@ -25,8 +27,8 @@ permissions: contents: read concurrency: - group: ruleset-governance-reconcile-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: ruleset-governance-reconcile-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'owner-plane' }} + cancel-in-progress: true jobs: validate: @@ -71,7 +73,7 @@ jobs: python -m coverage run \ --branch \ --include=scripts/ci/reconcile_ruleset_governance.py \ - -m pytest -q tests/test_ruleset_governance_reconciliation.py + -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py python -m coverage report \ --fail-under=100 \ --show-missing \ @@ -115,9 +117,11 @@ jobs: - name: Reconcile reviewed ruleset governance env: GH_TOKEN: ${{ secrets.CWL_RULESET_ADMIN_TOKEN }} + EXPECTED_MAIN_SHA: ${{ github.sha }} run: >- python scripts/ci/reconcile_ruleset_governance.py --manifest config/ruleset-governance.json + --expected-main-sha "$EXPECTED_MAIN_SHA" - name: Verify post-change convergence from GitHub env: GH_TOKEN: ${{ secrets.CWL_RULESET_ADMIN_TOKEN }} From 10381a6f633fd383d4a8071b0c52d0bf5dcb1a35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:51:11 +0900 Subject: [PATCH 070/154] docs(governance): record ruleset PUT race boundary --- .../ruleset-owner-plane-reconciliation.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index 22136b8b80..91099c8e2d 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -19,28 +19,34 @@ The application connector can read those settings but does not expose ruleset mu - require no synthetic reviewer identities; and - allow merge and squash only, removing rebase. -The reconciler performs a second full read immediately before PUT and refuses to overwrite concurrent configuration drift. A successful HTTP update is not completion: it performs a third read and requires the complete editable payload to equal the reviewed update body. Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. +GitHub's current REST contract does not support conditional unsafe REST updates such as `If-Match` on ruleset `PUT`; GitHub's general REST guidance explicitly says conditional unsafe methods are unsupported unless a specific endpoint documents otherwise, and the repository/organization ruleset update endpoints document no such precondition. The second full ruleset read is therefore a drift detector, **not** compare-and-swap. It catches edits visible before the final read but cannot make the final GET-to-PUT interval atomic. A successful HTTP update is also not completion: the reconciler performs a post-write full read and requires the complete editable payload to equal the reviewed update body. + +Because the API cannot make that interval atomic, mutation is permitted only during exclusive owner-plane maintenance: a protected `ruleset-governance-maintenance` environment supplies the dedicated credential, the repository variable explicitly enables mutation, and one shared non-PR Actions concurrency group cancels superseded owner-plane runs. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence. If protected `main` advances, the stale run fails closed. These controls narrow and expose the residual API race rather than falsely claiming atomicity. Human administrators must not edit the two declared rulesets concurrently with an enabled maintenance run; if post-write state does not exactly converge, the run fails and requires a fresh live read before any retry. + +Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. Pull requests execute only offline manifest validation plus 100% statement/branch/docstring contract tests. Mutation can run only from trusted `main`, in the `ruleset-governance-maintenance` protected environment, when repository variable `CWL_RULESET_RECONCILE_ENABLED=true` is explicitly set, using dedicated secret `CWL_RULESET_ADMIN_TOKEN`. The normal workflow token remains `contents: read`, checkout credentials are not persisted, and API errors never echo subprocess output that could contain sensitive context. -`CWL_RULESET_ADMIN_TOKEN` is intentionally distinct from `CWL_REPOSITORY_METADATA_TOKEN`. GitHub documents organization-ruleset update as requiring organization **Administration (write)** and repository-ruleset update as requiring repository **Administration (write)**. The credential must carry only those permissions needed for the two declared targets; it must not be repurposed as a general development, merge, release, or metadata token. The environment and secret must be provisioned independently before `CWL_RULESET_RECONCILE_ENABLED` is set. Source integration alone does not prove that provisioning exists. +`CWL_RULESET_ADMIN_TOKEN` is intentionally distinct from `CWL_REPOSITORY_METADATA_TOKEN`. GitHub documents organization-ruleset update as requiring organization **Administration (write)** and repository-ruleset update as requiring repository **Administration (write)**. The credential must carry only those permissions needed for the two declared targets; it must not be repurposed as a general development, merge, release, or metadata token. The protected environment secret is bootstrap transport into the `gh` REST client for this owner-plane process; it is not exposed to pull-request code or model processes. The environment and secret must be provisioned independently before `CWL_RULESET_RECONCILE_ENABLED` is set. Source integration alone does not prove that provisioning exists. ## Verification sequence -1. PR validation proves manifest, mutation projection, concurrency refusal, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. -2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable. +1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. 3. Run the reconciler and re-read both complete live ruleset payloads using the independently authorized credential. 4. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. 5. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. 6. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as Orgmetra #88; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. -7. Keep the reconciler enabled for drift repair only after the first successful canary. If the API identity or editable schema changes, it fails closed and requires a reviewed source update. +7. Keep the reconciler enabled for drift repair only if an explicit owner-plane operating decision accepts the residual lack of conditional PUT. If the API identity or editable schema changes, it fails closed and requires a reviewed source update. ## Standards and research basis -GitHub's current REST documentation makes ruleset mutation an Administration-authorized operation and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. +GitHub's current REST documentation makes ruleset mutation an Administration-authorized operation and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. Its REST best-practices guidance distinguishes safe conditional `GET` requests from unsafe methods and states that conditional `POST`/`PUT`/`PATCH`/`DELETE` requests are unsupported unless the endpoint explicitly says otherwise; the current ruleset update documentation does not provide a conditional-write precondition. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. ### References (APA 7th) +GitHub. (2026). *Best practices for using the REST API*. GitHub Docs. https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api + GitHub. (2026). *REST API endpoints for rules: Organizations*. GitHub Docs. https://docs.github.com/en/rest/orgs/rules GitHub. (2026). *REST API endpoints for rules: Repositories*. GitHub Docs. https://docs.github.com/en/rest/repos/rules From f7c5537b5f8cdeff97e25f7d2236c69f10ef9edd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:51:53 +0900 Subject: [PATCH 071/154] test(governance): cover protected-main reconciliation guard --- ...t_ruleset_governance_review_regressions.py | 138 +++++++++++++++++- 1 file changed, 130 insertions(+), 8 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index ee36c88263..3105a1ab2d 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import json import sys from pathlib import Path @@ -20,24 +21,32 @@ SPEC.loader.exec_module(module) -def test_stale_main_revision_fails_before_any_ruleset_mutation(monkeypatch) -> None: - """A resumed owner-plane run cannot apply policy from an obsolete main SHA.""" +def _target(): + """Return the reviewed repository ruleset target used by race tests.""" - target = module.RulesetTarget( + return module.RulesetTarget( "repository", "ContextualWisdomLab", ".github", 17921150, "Lock default branch", ) - live = { + + +def _live() -> dict[str, object]: + """Return one live ruleset with only the reviewed merge-method drift.""" + + target = _target() + return { "id": target.ruleset_id, "name": target.name, "target": "branch", "source_type": target.source_type, "source": target.source, "enforcement": "active", - "bypass_actors": [{"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"}], + "bypass_actors": [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ], "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, "rules": [ {"type": "deletion"}, @@ -57,20 +66,133 @@ def test_stale_main_revision_fails_before_any_ruleset_mutation(monkeypatch) -> N }, ], } - calls: list[str] = [] - monkeypatch.setattr(module, "_gh_api", lambda method, endpoint, **kwargs: calls.append(method) or live) + +def _manifest(tmp_path: Path) -> Path: + """Write the exact two-target manifest accepted by the production parser.""" + + path = tmp_path / "manifest.json" + path.write_text( + json.dumps( + { + "schema_version": 1, + "organization": "ContextualWisdomLab", + "targets": [ + { + "scope": "repository", + "owner": "ContextualWisdomLab", + "repository": ".github", + "ruleset_id": 17921150, + "name": "Lock default branch", + }, + { + "scope": "organization", + "owner": "ContextualWisdomLab", + "repository": None, + "ruleset_id": 18156473, + "name": "CWL Central required workflows", + }, + ], + } + ), + encoding="utf-8", + ) + return path + + +def test_stale_main_revision_fails_before_any_ruleset_mutation(monkeypatch) -> None: + """A resumed owner-plane run cannot apply policy from an obsolete main SHA.""" + + calls: list[str] = [] + monkeypatch.setattr( + module, + "_gh_api", + lambda method, endpoint, **kwargs: calls.append(method) or _live(), + ) monkeypatch.setattr(module, "_current_main_sha", lambda: "b" * 40) with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): module._reconcile_target( - target, + _target(), verify_only=False, expected_main_sha="a" * 40, ) assert calls == ["GET"] +def test_current_main_guard_covers_live_success_and_malformed_evidence(monkeypatch) -> None: + """The live ref reader accepts one exact SHA and rejects malformed ref evidence.""" + + monkeypatch.setattr( + module, + "_gh_api", + lambda *_args, **_kwargs: {"object": {"sha": "A" * 40}}, + ) + assert module._current_main_sha() == "a" * 40 + module._assert_current_main("a" * 40) + + with pytest.raises(module.RulesetGovernanceError, match="expected protected main SHA"): + module._assert_current_main("BAD") + + monkeypatch.setattr( + module, + "_gh_api", + lambda *_args, **_kwargs: {"object": {"sha": "not-a-sha"}}, + ) + with pytest.raises(module.RulesetGovernanceError, match="malformed SHA"): + module._current_main_sha() + + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: {"object": []}) + with pytest.raises(module.RulesetGovernanceError, match="main ref object"): + module._current_main_sha() + + +def test_current_main_is_rechecked_around_put_and_after_convergence(monkeypatch) -> None: + """Stable protected main is checked before final read, PUT, and completion.""" + + first = _live() + desired = module._desired_payload(first, _target()) + converged = {**first, **desired} + replies = iter([first, first, {}, converged]) + checks: list[str] = [] + monkeypatch.setattr(module, "_gh_api", lambda *args, **kwargs: next(replies)) + monkeypatch.setattr( + module, + "_current_main_sha", + lambda: checks.append("main") or "a" * 40, + ) + + assert module._reconcile_target( + _target(), + verify_only=False, + expected_main_sha="a" * 40, + ) is True + assert checks == ["main", "main", "main"] + + +def test_actions_apply_requires_and_forwards_expected_main_sha(tmp_path, monkeypatch) -> None: + """The privileged Actions CLI path cannot silently omit its protected-main identity.""" + + manifest = _manifest(tmp_path) + monkeypatch.setenv("GH_TOKEN", "protected") + monkeypatch.setenv("GITHUB_ACTIONS", "true") + with pytest.raises(module.RulesetGovernanceError, match="expected protected main SHA"): + module.main(["--manifest", str(manifest)]) + + seen: list[tuple[bool, str | None]] = [] + + def fake_reconcile(targets, *, verify_only, expected_main_sha=None): + assert len(targets) == 2 + seen.append((verify_only, expected_main_sha)) + return 0 + + monkeypatch.setattr(module, "reconcile", fake_reconcile) + assert module.main( + ["--manifest", str(manifest), "--expected-main-sha", "a" * 40] + ) == 0 + assert seen == [(False, "a" * 40)] + + def test_owner_plane_workflow_supersedes_stale_runs_and_quotes_main_sha() -> None: """Only the newest trusted-main owner-plane run may reach mutation.""" From 019ff9f981bcb791606dc6d57d0514b1652111c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:53:20 +0900 Subject: [PATCH 072/154] test(governance): cover guarded multi-target forwarding --- ...test_ruleset_governance_review_regressions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 3105a1ab2d..0d0730fc76 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -170,6 +170,22 @@ def test_current_main_is_rechecked_around_put_and_after_convergence(monkeypatch) assert checks == ["main", "main", "main"] +def test_reconcile_forwards_expected_main_sha_to_each_target(monkeypatch) -> None: + """The multi-target coordinator preserves the protected-main guard on every mutation.""" + + seen: list[tuple[str, bool, str | None]] = [] + + def fake_target(item, *, verify_only, expected_main_sha=None): + seen.append((item.scope, verify_only, expected_main_sha)) + return True + + monkeypatch.setattr(module, "_reconcile_target", fake_target) + assert module.reconcile( + (_target(),), verify_only=False, expected_main_sha="a" * 40 + ) == 1 + assert seen == [("repository", False, "a" * 40)] + + def test_actions_apply_requires_and_forwards_expected_main_sha(tmp_path, monkeypatch) -> None: """The privileged Actions CLI path cannot silently omit its protected-main identity.""" From 61e4587f26fe072d7f49036f4fffa553d4f007a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:55:18 +0900 Subject: [PATCH 073/154] test(governance): reproduce hidden ruleset history collision --- ...t_ruleset_governance_review_regressions.py | 112 +++++++++++++++++- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 0d0730fc76..7addfe3e07 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -68,6 +68,13 @@ def _live() -> dict[str, object]: } +def _converged() -> dict[str, object]: + """Return the reviewed desired state for the repository target.""" + + live = _live() + return {**live, **module._desired_payload(live, _target())} + + def _manifest(tmp_path: Path) -> Path: """Write the exact two-target manifest accepted by the production parser.""" @@ -151,15 +158,16 @@ def test_current_main_is_rechecked_around_put_and_after_convergence(monkeypatch) """Stable protected main is checked before final read, PUT, and completion.""" first = _live() - desired = module._desired_payload(first, _target()) - converged = {**first, **desired} - replies = iter([first, first, {}, converged]) + desired = _converged() + replies = iter([first, first, {}, desired]) checks: list[str] = [] monkeypatch.setattr(module, "_gh_api", lambda *args, **kwargs: next(replies)) + monkeypatch.setattr(module, "_current_main_sha", lambda: checks.append("main") or "a" * 40) + monkeypatch.setattr(module, "_latest_history_version", lambda _target: 1) monkeypatch.setattr( module, - "_current_main_sha", - lambda: checks.append("main") or "a" * 40, + "_verify_ruleset_history_transition", + lambda _target, _baseline, _desired: None, ) assert module._reconcile_target( @@ -170,6 +178,97 @@ def test_current_main_is_rechecked_around_put_and_after_convergence(monkeypatch) assert checks == ["main", "main", "main"] +def test_ruleset_target_exposes_history_endpoints() -> None: + """Collision evidence is fetched from the exact target's immutable history surface.""" + + target = _target() + assert target.history_endpoint == "repos/ContextualWisdomLab/.github/rulesets/17921150/history" + assert target.history_version_endpoint(7).endswith("/history/7") + + +def test_latest_history_version_rejects_missing_or_malformed_history(monkeypatch) -> None: + """Mutation never begins without one trustworthy pre-write history version.""" + + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: []) + with pytest.raises(module.RulesetGovernanceError, match="ruleset history is empty"): + module._latest_history_version(_target()) + + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": True}]) + with pytest.raises(module.RulesetGovernanceError, match="version identity is malformed"): + module._latest_history_version(_target()) + + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": 12}]) + assert module._latest_history_version(_target()) == 12 + + +def test_history_transition_accepts_exactly_one_new_reviewed_version(monkeypatch) -> None: + """One new version whose predecessor is the baseline proves no hidden pre-PUT edit.""" + + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 8}, {"version_id": 7}], + ) + monkeypatch.setattr(module, "_history_version_state", lambda _target, version: _converged()) + module._verify_ruleset_history_transition(_target(), 7, _converged()) + + +def test_history_collision_restores_immediate_predecessor_before_failing(monkeypatch) -> None: + """A hidden pre-PUT administrator edit is restored instead of being silently lost.""" + + desired = _converged() + external = _live() + external["conditions"] = {"ref_name": {"include": ["refs/heads/reviewed"], "exclude": []}} + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [ + {"version_id": 10}, + {"version_id": 9}, + {"version_id": 7}, + ], + ) + monkeypatch.setattr( + module, + "_history_version_state", + lambda _target, version: desired if version == 10 else external, + ) + calls: list[tuple[str, object | None]] = [] + replies = iter([desired, {}, external]) + + def fake_api(method, endpoint, *, body=None): + calls.append((method, body)) + return next(replies) + + monkeypatch.setattr(module, "_gh_api", fake_api) + with pytest.raises(module.RulesetGovernanceError, match="restored preceding administrator state"): + module._verify_ruleset_history_transition(_target(), 7, desired) + assert [method for method, _body in calls] == ["GET", "PUT", "GET"] + assert calls[1][1] == module._editable_projection(external) + + +def test_history_collision_does_not_overwrite_a_newer_post_put_admin_edit(monkeypatch) -> None: + """If live state advanced again after our PUT, collision recovery preserves that newer state.""" + + desired = _converged() + external = _live() + newer = _live() + newer["conditions"] = {"ref_name": {"include": ["refs/heads/newer"], "exclude": []}} + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 10}, {"version_id": 9}], + ) + monkeypatch.setattr( + module, + "_history_version_state", + lambda _target, version: desired if version == 10 else external, + ) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: newer) + with pytest.raises(module.RulesetGovernanceError, match="advanced again"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + def test_reconcile_forwards_expected_main_sha_to_each_target(monkeypatch) -> None: """The multi-target coordinator preserves the protected-main guard on every mutation.""" @@ -224,7 +323,8 @@ def test_docs_state_github_has_no_ruleset_put_compare_and_swap() -> None: text = DOCTORING.read_text(encoding="utf-8") assert "does not support conditional unsafe REST updates" in text - assert "exclusive owner-plane maintenance" in text + assert "ruleset history" in text + assert "restores the immediate pre-write version" in text assert "cannot make the final GET-to-PUT interval atomic" in text From 8699add32082a6f68e5c4a9a8a71cc95655eaba8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:56:20 +0900 Subject: [PATCH 074/154] fix(governance): recover hidden ruleset write collisions --- scripts/ci/reconcile_ruleset_governance.py | 144 +++++++++++++++++++-- 1 file changed, 133 insertions(+), 11 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 6d1cd589ef..553388f136 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -6,8 +6,9 @@ reviewed pull-request controls. GitHub does not provide conditional PUT/PATCH semantics for this endpoint, so the second live read is a drift detector rather than a compare-and-swap guarantee. Privileged mutation is additionally bound to -the exact protected-main revision and a serialized protected owner-plane run, -with full post-write convergence verification. +the exact protected-main revision, serialized owner-plane execution, and the +immutable ruleset-history surface. If history proves a hidden pre-PUT edit was +overwritten, the reconciler restores the immediate predecessor before failing. """ from __future__ import annotations @@ -52,6 +53,19 @@ def endpoint(self) -> str: return f"orgs/{self.owner}/rulesets/{self.ruleset_id}" return f"repos/{self.owner}/{self.repository}/rulesets/{self.ruleset_id}" + @property + def history_endpoint(self) -> str: + """Return the immutable history endpoint for this exact ruleset.""" + + return f"{self.endpoint}/history" + + def history_version_endpoint(self, version_id: int) -> str: + """Return one exact immutable ruleset-history version endpoint.""" + + if type(version_id) is not int or version_id <= 0: + raise RulesetGovernanceError("ruleset history version identity is malformed") + return f"{self.history_endpoint}/{version_id}" + @property def source(self) -> str: """Return the exact source identity GitHub must report for this ruleset.""" @@ -128,15 +142,10 @@ def load_manifest(path: Path) -> tuple[RulesetTarget, ...]: return tuple(targets) -def _gh_api( - method: str, - endpoint: str, - *, - body: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Call GitHub's versioned REST API without exposing credential material.""" +def _gh_command(method: str, endpoint: str) -> list[str]: + """Build one versioned GitHub CLI command for the reviewed REST boundary.""" - command = [ + return [ "gh", "api", "--method", @@ -145,6 +154,17 @@ def _gh_api( f"X-GitHub-Api-Version: {API_VERSION}", endpoint, ] + + +def _run_gh_json( + method: str, + endpoint: str, + *, + body: dict[str, Any] | None = None, +) -> Any: + """Call GitHub REST and decode JSON without exposing credential diagnostics.""" + + command = _gh_command(method, endpoint) if body is not None: command.extend(["--input", "-"]) completed = subprocess.run( @@ -157,8 +177,29 @@ def _gh_api( ) if completed.returncode != 0: raise RulesetGovernanceError(f"GitHub API request failed for {endpoint}") + return json.loads(completed.stdout) + + +def _gh_api( + method: str, + endpoint: str, + *, + body: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Call GitHub REST and require an object response.""" + return _plain_dict( - json.loads(completed.stdout), field=f"GitHub response for {endpoint}" + _run_gh_json(method, endpoint, body=body), + field=f"GitHub response for {endpoint}", + ) + + +def _gh_api_list(method: str, endpoint: str) -> list[Any]: + """Call GitHub REST and require an array response.""" + + return _plain_list( + _run_gh_json(method, endpoint), + field=f"GitHub response for {endpoint}", ) @@ -184,6 +225,34 @@ def _assert_current_main(expected_main_sha: str) -> None: ) +def _history_version_id(entry: Any) -> int: + """Return one positive ruleset-history version ID or fail closed.""" + + item = _plain_dict(entry, field="ruleset history entry") + version_id = item.get("version_id") + if type(version_id) is not int or version_id <= 0: + raise RulesetGovernanceError("ruleset history version identity is malformed") + return version_id + + +def _latest_history_version(target: RulesetTarget) -> int: + """Return the newest immutable history version before mutation.""" + + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=1") + if not history: + raise RulesetGovernanceError("ruleset history is empty") + return _history_version_id(history[0]) + + +def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: + """Return the exact ruleset state stored at one immutable history version.""" + + payload = _gh_api("GET", target.history_version_endpoint(version_id)) + state = _plain_dict(payload.get("state"), field="ruleset history version state") + _assert_identity(state, target) + return state + + def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: """Require the live ruleset to be the exact reviewed object before mutation.""" @@ -263,6 +332,55 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A return desired +def _verify_ruleset_history_transition( + target: RulesetTarget, + baseline_version: int, + desired: dict[str, Any], +) -> None: + """Detect hidden pre-PUT edits and restore their immediate predecessor state. + + GitHub records immutable ruleset versions but does not offer conditional + unsafe updates. After a successful PUT, the newest history state must equal + our reviewed body and its immediate predecessor must be the version sampled + before the final live read. If another version intervened, our write + overwrote a concurrent administrator edit. In that case the immediate + predecessor is restored only while live state still equals our own write; + otherwise a newer administrator state is preserved untouched. + """ + + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=3") + if len(history) < 2: + raise RulesetGovernanceError("ruleset history did not expose a predecessor") + newest_id = _history_version_id(history[0]) + predecessor_id = _history_version_id(history[1]) + if newest_id == baseline_version: + raise RulesetGovernanceError("ruleset mutation is not visible in history") + + newest_state = _history_version_state(target, newest_id) + if _editable_projection(newest_state) != desired: + raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") + if predecessor_id == baseline_version: + return + + predecessor_state = _history_version_state(target, predecessor_id) + predecessor_payload = _editable_projection(predecessor_state) + current = _gh_api("GET", target.endpoint) + _assert_identity(current, target) + if _editable_projection(current) != desired: + raise RulesetGovernanceError( + "concurrent ruleset history detected but live state advanced again; refusing rollback" + ) + + _gh_api("PUT", target.endpoint, body=predecessor_payload) + restored = _gh_api("GET", target.endpoint) + _assert_identity(restored, target) + if _editable_projection(restored) != predecessor_payload: + raise RulesetGovernanceError("concurrent ruleset collision rollback did not converge") + raise RulesetGovernanceError( + "concurrent ruleset history detected; restored preceding administrator state" + ) + + def _reconcile_target( target: RulesetTarget, *, @@ -280,8 +398,10 @@ def _reconcile_target( f"{target.scope} ruleset governance drift remains" ) + baseline_version: int | None = None if expected_main_sha is not None: _assert_current_main(expected_main_sha) + baseline_version = _latest_history_version(target) second = _gh_api("GET", target.endpoint) if _editable_projection(second) != _editable_projection(first): raise RulesetGovernanceError( @@ -295,6 +415,8 @@ def _reconcile_target( _assert_identity(after, target) if _editable_projection(after) != desired: raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") + if baseline_version is not None: + _verify_ruleset_history_transition(target, baseline_version, desired) if expected_main_sha is not None: _assert_current_main(expected_main_sha) return True From 28652911ecf7565c3a79191fb53bf3603fa35aa9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:57:21 +0900 Subject: [PATCH 075/154] test(governance): close history collision coverage --- ...t_ruleset_governance_review_regressions.py | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 7addfe3e07..8e9550162f 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -184,6 +184,24 @@ def test_ruleset_target_exposes_history_endpoints() -> None: target = _target() assert target.history_endpoint == "repos/ContextualWisdomLab/.github/rulesets/17921150/history" assert target.history_version_endpoint(7).endswith("/history/7") + with pytest.raises(module.RulesetGovernanceError, match="version identity is malformed"): + target.history_version_endpoint(0) + + +def test_history_transport_and_version_state_are_strict(monkeypatch) -> None: + """History arrays and version states cross typed, exact-identity trust boundaries.""" + + monkeypatch.setattr(module, "_run_gh_json", lambda *_args, **_kwargs: [{"version_id": 1}]) + assert module._gh_api_list("GET", "history") == [{"version_id": 1}] + monkeypatch.setattr(module, "_run_gh_json", lambda *_args, **_kwargs: {}) + with pytest.raises(module.RulesetGovernanceError, match="must be an array"): + module._gh_api_list("GET", "history") + + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: {"state": _live()}) + assert module._history_version_state(_target(), 4) == _live() + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: {"state": []}) + with pytest.raises(module.RulesetGovernanceError, match="version state must be an object"): + module._history_version_state(_target(), 4) def test_latest_history_version_rejects_missing_or_malformed_history(monkeypatch) -> None: @@ -193,8 +211,17 @@ def test_latest_history_version_rejects_missing_or_malformed_history(monkeypatch with pytest.raises(module.RulesetGovernanceError, match="ruleset history is empty"): module._latest_history_version(_target()) - monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": True}]) - with pytest.raises(module.RulesetGovernanceError, match="version identity is malformed"): + for invalid in (True, 0): + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, invalid=invalid, **_kwargs: [{"version_id": invalid}], + ) + with pytest.raises(module.RulesetGovernanceError, match="version identity is malformed"): + module._latest_history_version(_target()) + + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [[]]) + with pytest.raises(module.RulesetGovernanceError, match="history entry must be an object"): module._latest_history_version(_target()) monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": 12}]) @@ -213,6 +240,32 @@ def test_history_transition_accepts_exactly_one_new_reviewed_version(monkeypatch module._verify_ruleset_history_transition(_target(), 7, _converged()) +def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypatch) -> None: + """Missing predecessor, absent version advance, or mismatched latest state fail closed.""" + + desired = _converged() + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": 8}]) + with pytest.raises(module.RulesetGovernanceError, match="did not expose a predecessor"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 7}, {"version_id": 6}], + ) + with pytest.raises(module.RulesetGovernanceError, match="not visible in history"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 8}, {"version_id": 7}], + ) + monkeypatch.setattr(module, "_history_version_state", lambda *_args, **_kwargs: _live()) + with pytest.raises(module.RulesetGovernanceError, match="does not match reviewed mutation"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + def test_history_collision_restores_immediate_predecessor_before_failing(monkeypatch) -> None: """A hidden pre-PUT administrator edit is restored instead of being silently lost.""" @@ -269,6 +322,28 @@ def test_history_collision_does_not_overwrite_a_newer_post_put_admin_edit(monkey module._verify_ruleset_history_transition(_target(), 7, desired) +def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: + """A failed predecessor restore is surfaced rather than treated as collision recovery.""" + + desired = _converged() + external = _live() + external["conditions"] = {"ref_name": {"include": ["refs/heads/external"], "exclude": []}} + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 10}, {"version_id": 9}], + ) + monkeypatch.setattr( + module, + "_history_version_state", + lambda _target, version: desired if version == 10 else external, + ) + replies = iter([desired, {}, desired]) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: next(replies)) + with pytest.raises(module.RulesetGovernanceError, match="rollback did not converge"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + def test_reconcile_forwards_expected_main_sha_to_each_target(monkeypatch) -> None: """The multi-target coordinator preserves the protected-main guard on every mutation.""" From 1811a7ead0a31c897da794113cf507edcb251c5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:57:48 +0900 Subject: [PATCH 076/154] docs(governance): trace ruleset history collision recovery --- .../ruleset-owner-plane-reconciliation.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index 91099c8e2d..fc6f5ab7bc 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -21,27 +21,30 @@ The application connector can read those settings but does not expose ruleset mu GitHub's current REST contract does not support conditional unsafe REST updates such as `If-Match` on ruleset `PUT`; GitHub's general REST guidance explicitly says conditional unsafe methods are unsupported unless a specific endpoint documents otherwise, and the repository/organization ruleset update endpoints document no such precondition. The second full ruleset read is therefore a drift detector, **not** compare-and-swap. It catches edits visible before the final read but cannot make the final GET-to-PUT interval atomic. A successful HTTP update is also not completion: the reconciler performs a post-write full read and requires the complete editable payload to equal the reviewed update body. -Because the API cannot make that interval atomic, mutation is permitted only during exclusive owner-plane maintenance: a protected `ruleset-governance-maintenance` environment supplies the dedicated credential, the repository variable explicitly enables mutation, and one shared non-PR Actions concurrency group cancels superseded owner-plane runs. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence. If protected `main` advances, the stale run fails closed. These controls narrow and expose the residual API race rather than falsely claiming atomicity. Human administrators must not edit the two declared rulesets concurrently with an enabled maintenance run; if post-write state does not exactly converge, the run fails and requires a fresh live read before any retry. +The same GitHub API exposes immutable organization and repository **ruleset history** plus exact version-state reads to Administration-authorized callers. The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. It then re-reads current live state; only if live state still equals our reviewed write does it restore the immediate pre-write version and fail the run. If live state has advanced again, it does not overwrite that newer administrator state and fails for fresh investigation. This history-based recovery prevents a detected hidden edit from being silently lost without pretending GitHub supplied atomic conditional writes. + +Because the API still cannot make the final GET-to-PUT interval atomic, mutation is permitted only during exclusive owner-plane maintenance: a protected `ruleset-governance-maintenance` environment supplies the dedicated credential, the repository variable explicitly enables mutation, and one shared non-PR Actions concurrency group cancels superseded owner-plane runs. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence/history verification. If protected `main` advances, the stale run fails closed. These controls narrow and expose the residual API race rather than falsely claiming atomicity. Human administrators must not edit the two declared rulesets concurrently with an enabled maintenance run; ruleset history is nevertheless treated as authoritative collision evidence rather than relying on that operating convention alone. Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. Pull requests execute only offline manifest validation plus 100% statement/branch/docstring contract tests. Mutation can run only from trusted `main`, in the `ruleset-governance-maintenance` protected environment, when repository variable `CWL_RULESET_RECONCILE_ENABLED=true` is explicitly set, using dedicated secret `CWL_RULESET_ADMIN_TOKEN`. The normal workflow token remains `contents: read`, checkout credentials are not persisted, and API errors never echo subprocess output that could contain sensitive context. -`CWL_RULESET_ADMIN_TOKEN` is intentionally distinct from `CWL_REPOSITORY_METADATA_TOKEN`. GitHub documents organization-ruleset update as requiring organization **Administration (write)** and repository-ruleset update as requiring repository **Administration (write)**. The credential must carry only those permissions needed for the two declared targets; it must not be repurposed as a general development, merge, release, or metadata token. The protected environment secret is bootstrap transport into the `gh` REST client for this owner-plane process; it is not exposed to pull-request code or model processes. The environment and secret must be provisioned independently before `CWL_RULESET_RECONCILE_ENABLED` is set. Source integration alone does not prove that provisioning exists. +`CWL_RULESET_ADMIN_TOKEN` is intentionally distinct from `CWL_REPOSITORY_METADATA_TOKEN`. GitHub documents organization ruleset update/history and repository ruleset update/history as requiring the corresponding **Administration (write)** permission. The credential must carry only those permissions needed for the two declared targets; it must not be repurposed as a general development, merge, release, or metadata token. The protected environment secret is bootstrap transport into the `gh` REST client for this owner-plane process; it is not exposed to pull-request code or model processes. The environment and secret must be provisioned independently before `CWL_RULESET_RECONCILE_ENABLED` is set. Source integration alone does not prove that provisioning exists. ## Verification sequence -1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. -3. Run the reconciler and re-read both complete live ruleset payloads using the independently authorized credential. -4. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. -5. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. -6. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as Orgmetra #88; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. -7. Keep the reconciler enabled for drift repair only if an explicit owner-plane operating decision accepts the residual lack of conditional PUT. If the API identity or editable schema changes, it fails closed and requires a reviewed source update. +3. Run the reconciler and require one exact new ruleset-history version per target whose predecessor is the sampled pre-write baseline; any intervening version restores the immediate pre-write state before the run fails when restoration remains safe. +4. Re-read both complete live ruleset payloads using the independently authorized credential. +5. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. +6. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. +7. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as Orgmetra #88; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. +8. Keep the reconciler enabled for drift repair only if the protected owner-plane environment and history evidence remain available. If the API identity, editable schema, or history contract changes, it fails closed and requires a reviewed source update. ## Standards and research basis -GitHub's current REST documentation makes ruleset mutation an Administration-authorized operation and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. Its REST best-practices guidance distinguishes safe conditional `GET` requests from unsafe methods and states that conditional `POST`/`PUT`/`PATCH`/`DELETE` requests are unsupported unless the endpoint explicitly says otherwise; the current ruleset update documentation does not provide a conditional-write precondition. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. +GitHub's current REST documentation makes ruleset mutation and ruleset-history access Administration-authorized operations and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. Its REST best-practices guidance distinguishes safe conditional `GET` requests from unsafe methods and states that conditional `POST`/`PUT`/`PATCH`/`DELETE` requests are unsupported unless the endpoint explicitly says otherwise; the current ruleset update documentation does not provide a conditional-write precondition. The history endpoints provide the exact version sequence and version state used here to detect an otherwise invisible intervening update. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. ### References (APA 7th) From b1b3c1498ffca7a175828f493354398fe3603d58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:19:13 +0900 Subject: [PATCH 077/154] fix(governance): serialize privileged ruleset writes --- .github/workflows/ruleset-governance-reconcile.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index 6e8318ccdc..648f486095 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -28,15 +28,16 @@ permissions: concurrency: group: ruleset-governance-reconcile-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'owner-plane' }} - cancel-in-progress: true + # PR validation may safely supersede itself. Privileged owner-plane runs must + # finish their PUT + immutable-history verification/recovery critical section; + # cancelling them after PUT can strand an unverified overwrite. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: validate: - if: >- - github.event_name == 'pull_request' || - github.event_name == 'push' || - github.event_name == 'workflow_dispatch' || - vars.CWL_RULESET_RECONCILE_ENABLED == 'true' + # Every trigger, including a disabled hourly owner-plane schedule, validates + # the reviewed source/manifest offline. The enable variable gates mutation, + # not detection of configuration or regression drift. runs-on: ubuntu-24.04 timeout-minutes: 20 steps: From 98ae2edac7116d3c4e52e183b6fb50ccdca73779 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:19:31 +0900 Subject: [PATCH 078/154] test(governance): cover identity-edit recovery and critical section --- .../test_ruleset_governance_review_round2.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/test_ruleset_governance_review_round2.py diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py new file mode 100644 index 0000000000..35ebfd2613 --- /dev/null +++ b/tests/test_ruleset_governance_review_round2.py @@ -0,0 +1,92 @@ +"""Second-round regressions for ruleset owner-plane collision recovery.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" +WORKFLOW = ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml" + + +def load_module(): + """Load the production reconciler from the exact checkout.""" + spec = importlib.util.spec_from_file_location("ruleset_governance_round2", SOURCE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def repository_target(module): + """Return the exact owner-repository target used by the reviewed manifest.""" + return module.RulesetTarget( + scope="repository", + owner="ContextualWisdomLab", + repository=".github", + ruleset_id=17921150, + name="Lock default branch", + ) + + +def historical_state(*, name: str = "Admin renamed", enforcement: str = "evaluate") -> dict: + """Return a predecessor whose editable identity differs but provenance is unchanged.""" + return { + "id": 17921150, + "name": name, + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": enforcement, + "bypass_actors": [{"actor_id": 5, "actor_type": "Team", "bypass_mode": "pull_request"}], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [{"type": "non_fast_forward"}], + } + + +def test_history_predecessor_allows_editable_name_and_enforcement(monkeypatch) -> None: + """Collision recovery must be able to restore a legitimate administrator predecessor.""" + module = load_module() + target = repository_target(module) + predecessor = historical_state() + monkeypatch.setattr( + module, + "_gh_api", + lambda method, endpoint, **_kwargs: {"state": predecessor} + if method == "GET" and endpoint == target.history_version_endpoint(7) + else (_ for _ in ()).throw(AssertionError((method, endpoint))), + ) + + assert module._history_version_state(target, 7) == predecessor + + +def test_history_predecessor_still_rejects_wrong_ruleset_provenance(monkeypatch) -> None: + """Relaxing editable fields must never allow a history record from another ruleset.""" + module = load_module() + target = repository_target(module) + predecessor = historical_state() + predecessor["id"] = 999 + monkeypatch.setattr( + module, + "_gh_api", + lambda *_args, **_kwargs: {"state": predecessor}, + ) + + try: + module._history_version_state(target, 7) + except module.RulesetGovernanceError as exc: + assert "identity drift" in str(exc) + else: + raise AssertionError("wrong ruleset provenance was accepted") + + +def test_owner_plane_is_serial_but_disabled_schedule_still_validates() -> None: + """Mutation critical sections are non-cancellable and offline scheduled validation always runs.""" + text = WORKFLOW.read_text(encoding="utf-8") + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in text + validate_block = text.split("jobs:\n validate:\n", 1)[1].split("\n apply:\n", 1)[0] + assert "vars.CWL_RULESET_RECONCILE_ENABLED" not in validate_block + apply_block = text.split("\n apply:\n", 1)[1] + assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in apply_block From 744e1bb67790c89fdff3f66f5a9f56a9be66edb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:19:58 +0900 Subject: [PATCH 079/154] chore(governance): stage exact PR1644 round-two repair --- scripts/ci/_temp_repair_pr1644_round2.py | 157 +++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 scripts/ci/_temp_repair_pr1644_round2.py diff --git a/scripts/ci/_temp_repair_pr1644_round2.py b/scripts/ci/_temp_repair_pr1644_round2.py new file mode 100644 index 0000000000..0da0828467 --- /dev/null +++ b/scripts/ci/_temp_repair_pr1644_round2.py @@ -0,0 +1,157 @@ +"""One-shot exact-text repair for PR #1644 review round two.""" + +from __future__ import annotations + +from pathlib import Path + + +def replace_once(text: str, old: str, new: str, label: str) -> str: + """Replace one reviewed source anchor or abort without partial mutation.""" + if text.count(old) != 1: + raise SystemExit(f"expected exactly one {label} anchor, found {text.count(old)}") + return text.replace(old, new, 1) + + +def repair_reconciler() -> None: + """Separate immutable history provenance from editable live identity.""" + path = Path("scripts/ci/reconcile_ruleset_governance.py") + text = path.read_text(encoding="utf-8") + old = '''def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: + """Return the exact ruleset state stored at one immutable history version.""" + + payload = _gh_api("GET", target.history_version_endpoint(version_id)) + state = _plain_dict(payload.get("state"), field="ruleset history version state") + _assert_identity(state, target) + return state + + +def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: + """Require the live ruleset to be the exact reviewed object before mutation.""" + + expected = { + "id": target.ruleset_id, + "name": target.name, + "target": "branch", + "source_type": target.source_type, + "source": target.source, + "enforcement": "active", + } + mismatches = [key for key, value in expected.items() if live.get(key) != value] + if mismatches: + raise RulesetGovernanceError( + f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" + ) +''' + new = '''def _assert_target_provenance(live: dict[str, Any], target: RulesetTarget) -> None: + """Require immutable ruleset provenance while allowing historical editable fields.""" + + expected = { + "id": target.ruleset_id, + "target": "branch", + "source_type": target.source_type, + "source": target.source, + } + mismatches = [key for key, value in expected.items() if live.get(key) != value] + if mismatches: + raise RulesetGovernanceError( + f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" + ) + + +def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: + """Return one historical state after proving it belongs to the exact target.""" + + payload = _gh_api("GET", target.history_version_endpoint(version_id)) + state = _plain_dict(payload.get("state"), field="ruleset history version state") + _assert_target_provenance(state, target) + return state + + +def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: + """Require current live state to retain the reviewed editable identity too.""" + + _assert_target_provenance(live, target) + expected = {"name": target.name, "enforcement": "active"} + mismatches = [key for key, value in expected.items() if live.get(key) != value] + if mismatches: + raise RulesetGovernanceError( + f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" + ) +''' + text = replace_once(text, old, new, "history/live identity split") + text = replace_once( + text, + ''' restored = _gh_api("GET", target.endpoint) + _assert_identity(restored, target) + if _editable_projection(restored) != predecessor_payload: +''', + ''' restored = _gh_api("GET", target.endpoint) + _assert_target_provenance(restored, target) + if _editable_projection(restored) != predecessor_payload: +''', + "historical rollback verification", + ) + path.write_text(text, encoding="utf-8") + + +def repair_audit_fixture() -> None: + """Keep the canonical passing repository-ruleset fixture complete.""" + path = Path("tests/test_central_required_workflow_ruleset_audit.py") + text = path.read_text(encoding="utf-8") + text = replace_once( + text, + ''' "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "require_last_push_approval": False, +''', + ''' "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, + "require_last_push_approval": False, +''', + "repository audit fixture code-owner flag", + ) + path.write_text(text, encoding="utf-8") + + +def repair_validation_scope() -> None: + """Make the new regression part of every reviewed reconciler validation path.""" + path = Path(".github/workflows/ruleset-governance-reconcile.yml") + text = path.read_text(encoding="utf-8") + marker = ' - "tests/test_ruleset_governance_review_regressions.py"\n' + if text.count(marker) != 2: + raise SystemExit(f"unexpected workflow path marker count: {text.count(marker)}") + text = text.replace( + marker, + marker + ' - "tests/test_ruleset_governance_review_round2.py"\n', + ) + text = replace_once( + text, + ''' -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py +''', + ''' -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py tests/test_ruleset_governance_review_round2.py +''', + "focused pytest command", + ) + path.write_text(text, encoding="utf-8") + + +def repair_docs() -> None: + """Record the cancellation and historical-identity recovery boundary.""" + path = Path("docs/doctoring/ruleset-owner-plane-reconciliation.md") + text = path.read_text(encoding="utf-8") + heading = "## 2026-09-02 review-round-two recovery hardening" + if heading not in text: + text = text.rstrip() + f'''\n\n{heading}\n\nPrivileged owner-plane reconciliation is serialized without `cancel-in-progress`.\nPull-request validation may still supersede older validation runs, but a protected-main\nmutation run must finish its PUT plus immutable-history verification/recovery critical\nsection. This prevents a newer run from observing the desired payload after cancelling\na predecessor between PUT and collision verification.\n\nRuleset history is validated against immutable target provenance (`id`, branch target,\nsource type, and source). Historical `name` and `enforcement` are deliberately allowed\nto differ because they are editable administrator state that collision recovery may\nneed to restore. Current live mutation still requires the reviewed name and active\nenforcement. Rollback verifies exact predecessor editable payload convergence while\nretaining immutable target provenance, so a legitimate concurrent rename/enforcement\nchange cannot be silently overwritten.\n''' + path.write_text(text, encoding="utf-8") + + +def main() -> None: + repair_reconciler() + repair_audit_fixture() + repair_validation_scope() + repair_docs() + + +if __name__ == "__main__": + main() From 5b9609b26f2cf380e25757d3ce281a76b194cc49 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:20:18 +0900 Subject: [PATCH 080/154] chore(governance): add one-shot PR1644 round-two repair --- .../workflows/_temp_pr1644_round2_repair.yml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/_temp_pr1644_round2_repair.yml diff --git a/.github/workflows/_temp_pr1644_round2_repair.yml b/.github/workflows/_temp_pr1644_round2_repair.yml new file mode 100644 index 0000000000..639daa22ce --- /dev/null +++ b/.github/workflows/_temp_pr1644_round2_repair.yml @@ -0,0 +1,95 @@ +name: One-shot PR1644 review round-two repair + +on: + push: + branches: + - fix/ruleset-owner-plane-reconciler + paths: + - .github/_temp_pr1644_round2.trigger + +permissions: + contents: write + +concurrency: + group: pr1644-round2-${{ github.ref }} + cancel-in-progress: true + +jobs: + repair: + runs-on: ubuntu-slim + steps: + - name: Checkout exact writer head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: Prove historical editable-identity regression is RED + shell: bash + run: | + set -euo pipefail + if python - <<'PY' + import importlib.util + import sys + from pathlib import Path + p = Path('scripts/ci/reconcile_ruleset_governance.py') + s = importlib.util.spec_from_file_location('r', p) + m = importlib.util.module_from_spec(s); sys.modules[s.name] = m; s.loader.exec_module(m) + t = m.RulesetTarget('repository', 'ContextualWisdomLab', '.github', 17921150, 'Lock default branch') + state = {'id': 17921150, 'name': 'Admin renamed', 'target': 'branch', 'source_type': 'Repository', 'source': 'ContextualWisdomLab/.github', 'enforcement': 'evaluate', 'bypass_actors': [], 'conditions': {}, 'rules': []} + m._gh_api = lambda *_a, **_k: {'state': state} + m._history_version_state(t, 7) + PY + then + echo '::error::historical editable-identity regression was not RED' + exit 1 + fi + + - name: Apply exact source/test/docs repair + shell: bash + run: | + set -euo pipefail + python scripts/ci/_temp_repair_pr1644_round2.py + + - name: Verify focused GREEN contracts + shell: bash + run: | + set -euo pipefail + python -m py_compile scripts/ci/reconcile_ruleset_governance.py tests/test_ruleset_governance_review_round2.py + python - <<'PY' + import importlib.util + import sys + from pathlib import Path + p = Path('scripts/ci/reconcile_ruleset_governance.py') + s = importlib.util.spec_from_file_location('r', p) + m = importlib.util.module_from_spec(s); sys.modules[s.name] = m; s.loader.exec_module(m) + t = m.RulesetTarget('repository', 'ContextualWisdomLab', '.github', 17921150, 'Lock default branch') + state = {'id': 17921150, 'name': 'Admin renamed', 'target': 'branch', 'source_type': 'Repository', 'source': 'ContextualWisdomLab/.github', 'enforcement': 'evaluate', 'bypass_actors': [], 'conditions': {}, 'rules': []} + m._gh_api = lambda *_a, **_k: {'state': state} + assert m._history_version_state(t, 7) == state + workflow = Path('.github/workflows/ruleset-governance-reconcile.yml').read_text() + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in workflow + fixture = Path('tests/test_central_required_workflow_ruleset_audit.py').read_text() + assert '"require_code_owner_review": False' in fixture + assert 'tests/test_ruleset_governance_review_round2.py' in workflow + PY + git diff --check + + - name: Remove one-shot artifacts and publish only on unchanged writer lineage + shell: bash + run: | + set -euo pipefail + git rm -- .github/workflows/_temp_pr1644_round2_repair.yml .github/_temp_pr1644_round2.trigger scripts/ci/_temp_repair_pr1644_round2.py + git add -A + git diff --cached --check + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix(governance): recover concurrent editable identity safely' + git fetch origin fix/ruleset-owner-plane-reconciler + remote_head="$(git rev-parse FETCH_HEAD)" + parent="$(git rev-parse HEAD^)" + if [ "$remote_head" != "$parent" ]; then + echo "Remote writer moved from $parent to $remote_head; refusing overwrite." >&2 + exit 1 + fi + git push origin HEAD:fix/ruleset-owner-plane-reconciler From 304223487ee67bfc8221f19f1571d689d46e98f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:20:23 +0900 Subject: [PATCH 081/154] chore(governance): trigger PR1644 round-two repair --- .github/_temp_pr1644_round2.trigger | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/_temp_pr1644_round2.trigger diff --git a/.github/_temp_pr1644_round2.trigger b/.github/_temp_pr1644_round2.trigger new file mode 100644 index 0000000000..2b23a24180 --- /dev/null +++ b/.github/_temp_pr1644_round2.trigger @@ -0,0 +1,2 @@ +repair=review-round-two +head-parent=5b9609b26f2cf380e25757d3ce281a76b194cc49 From b033af4209ea919c892306f06b31cc1b5a919696 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:36:34 +0900 Subject: [PATCH 082/154] fix(governance): recover concurrent ruleset identity races --- scripts/ci/reconcile_ruleset_governance.py | 131 +++++++++++++++------ 1 file changed, 95 insertions(+), 36 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 553388f136..faece0cbc4 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -8,7 +8,8 @@ than a compare-and-swap guarantee. Privileged mutation is additionally bound to the exact protected-main revision, serialized owner-plane execution, and the immutable ruleset-history surface. If history proves a hidden pre-PUT edit was -overwritten, the reconciler restores the immediate predecessor before failing. +overwritten, the reconciler restores the newest displaced administrator state +before failing; recovery re-checks immutable history after every restore write. """ from __future__ import annotations @@ -29,6 +30,7 @@ CONTROL_REPOSITORY = "ContextualWisdomLab/.github" DESIRED_MERGE_METHODS = ["merge", "squash"] GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +COLLISION_RECOVERY_LIMIT = 8 class RulesetGovernanceError(RuntimeError): @@ -244,26 +246,36 @@ def _latest_history_version(target: RulesetTarget) -> int: return _history_version_id(history[0]) +def _assert_target_provenance(live: dict[str, Any], target: RulesetTarget) -> None: + """Require immutable target provenance while allowing editable history fields.""" + + expected = { + "id": target.ruleset_id, + "target": "branch", + "source_type": target.source_type, + "source": target.source, + } + mismatches = [key for key, value in expected.items() if live.get(key) != value] + if mismatches: + raise RulesetGovernanceError( + f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" + ) + + def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: - """Return the exact ruleset state stored at one immutable history version.""" + """Return one historical state after proving it belongs to the exact target.""" payload = _gh_api("GET", target.history_version_endpoint(version_id)) state = _plain_dict(payload.get("state"), field="ruleset history version state") - _assert_identity(state, target) + _assert_target_provenance(state, target) return state def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: - """Require the live ruleset to be the exact reviewed object before mutation.""" + """Require current live state to retain the reviewed editable identity too.""" - expected = { - "id": target.ruleset_id, - "name": target.name, - "target": "branch", - "source_type": target.source_type, - "source": target.source, - "enforcement": "active", - } + _assert_target_provenance(live, target) + expected = {"name": target.name, "enforcement": "active"} mismatches = [key for key, value in expected.items() if live.get(key) != value] if mismatches: raise RulesetGovernanceError( @@ -332,20 +344,75 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A return desired +def _recover_displaced_history_state( + target: RulesetTarget, + *, + current_version: int, + current_payload: dict[str, Any], + displaced_version: int, +) -> None: + """Restore the newest state displaced by our unsafe PUT without hiding races. + + GitHub offers no conditional ruleset PUT. Each recovery write therefore + verifies immutable history immediately afterward. If an administrator write + slipped between the recovery GET and PUT, that displaced history version + becomes the next recovery target. A newer live state observed before a + recovery write is never overwritten. The loop is bounded and fails closed. + """ + + for _attempt in range(COLLISION_RECOVERY_LIMIT): + displaced_state = _history_version_state(target, displaced_version) + displaced_payload = _editable_projection(displaced_state) + live = _gh_api("GET", target.endpoint) + _assert_target_provenance(live, target) + if _editable_projection(live) != current_payload: + raise RulesetGovernanceError( + "concurrent ruleset history detected but live state advanced again; refusing recovery" + ) + + _gh_api("PUT", target.endpoint, body=displaced_payload) + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if len(history) < 2: + raise RulesetGovernanceError( + "ruleset collision recovery history did not expose a predecessor" + ) + recovery_version = _history_version_id(history[0]) + recovery_predecessor = _history_version_id(history[1]) + recovery_state = _history_version_state(target, recovery_version) + if _editable_projection(recovery_state) != displaced_payload: + raise RulesetGovernanceError( + "ruleset collision recovery latest history does not match restore write" + ) + + restored = _gh_api("GET", target.endpoint) + _assert_target_provenance(restored, target) + if _editable_projection(restored) != displaced_payload: + raise RulesetGovernanceError( + "concurrent ruleset collision rollback did not converge" + ) + if recovery_predecessor == current_version: + return + + current_version = recovery_version + current_payload = displaced_payload + displaced_version = recovery_predecessor + + raise RulesetGovernanceError( + "ruleset collision recovery exceeded bounded attempts under concurrent writes" + ) + + def _verify_ruleset_history_transition( target: RulesetTarget, baseline_version: int, desired: dict[str, Any], ) -> None: - """Detect hidden pre-PUT edits and restore their immediate predecessor state. - - GitHub records immutable ruleset versions but does not offer conditional - unsafe updates. After a successful PUT, the newest history state must equal - our reviewed body and its immediate predecessor must be the version sampled - before the final live read. If another version intervened, our write - overwrote a concurrent administrator edit. In that case the immediate - predecessor is restored only while live state still equals our own write; - otherwise a newer administrator state is preserved untouched. + """Detect hidden pre-PUT edits and restore the newest displaced state safely. + + The newest history state must equal our reviewed body and its immediate + predecessor must be the version sampled before the final live read. If a + version intervened, recovery follows immutable history and verifies every + restore write so a second administrator edit cannot be silently overwritten. """ history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=3") @@ -362,22 +429,14 @@ def _verify_ruleset_history_transition( if predecessor_id == baseline_version: return - predecessor_state = _history_version_state(target, predecessor_id) - predecessor_payload = _editable_projection(predecessor_state) - current = _gh_api("GET", target.endpoint) - _assert_identity(current, target) - if _editable_projection(current) != desired: - raise RulesetGovernanceError( - "concurrent ruleset history detected but live state advanced again; refusing rollback" - ) - - _gh_api("PUT", target.endpoint, body=predecessor_payload) - restored = _gh_api("GET", target.endpoint) - _assert_identity(restored, target) - if _editable_projection(restored) != predecessor_payload: - raise RulesetGovernanceError("concurrent ruleset collision rollback did not converge") + _recover_displaced_history_state( + target, + current_version=newest_id, + current_payload=desired, + displaced_version=predecessor_id, + ) raise RulesetGovernanceError( - "concurrent ruleset history detected; restored preceding administrator state" + "concurrent ruleset history detected; restored newest displaced administrator state" ) From e0a15485afbed9b738ea263d957205fd3dcf38c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:39:03 +0900 Subject: [PATCH 083/154] test(governance): cover second collision-recovery race --- ...t_ruleset_governance_review_regressions.py | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 8e9550162f..6945159067 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -267,20 +267,18 @@ def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypa def test_history_collision_restores_immediate_predecessor_before_failing(monkeypatch) -> None: - """A hidden pre-PUT administrator edit is restored instead of being silently lost.""" + """A hidden pre-PUT administrator edit is restored and history-proven before failing.""" desired = _converged() external = _live() external["conditions"] = {"ref_name": {"include": ["refs/heads/reviewed"], "exclude": []}} - monkeypatch.setattr( - module, - "_gh_api_list", - lambda *_args, **_kwargs: [ - {"version_id": 10}, - {"version_id": 9}, - {"version_id": 7}, - ], + histories = iter( + [ + [{"version_id": 10}, {"version_id": 9}, {"version_id": 7}], + [{"version_id": 11}, {"version_id": 10}], + ] ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: next(histories)) monkeypatch.setattr( module, "_history_version_state", @@ -294,7 +292,7 @@ def fake_api(method, endpoint, *, body=None): return next(replies) monkeypatch.setattr(module, "_gh_api", fake_api) - with pytest.raises(module.RulesetGovernanceError, match="restored preceding administrator state"): + with pytest.raises(module.RulesetGovernanceError, match="restored newest displaced administrator state"): module._verify_ruleset_history_transition(_target(), 7, desired) assert [method for method, _body in calls] == ["GET", "PUT", "GET"] assert calls[1][1] == module._editable_projection(external) @@ -328,11 +326,13 @@ def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: desired = _converged() external = _live() external["conditions"] = {"ref_name": {"include": ["refs/heads/external"], "exclude": []}} - monkeypatch.setattr( - module, - "_gh_api_list", - lambda *_args, **_kwargs: [{"version_id": 10}, {"version_id": 9}], + histories = iter( + [ + [{"version_id": 10}, {"version_id": 9}], + [{"version_id": 11}, {"version_id": 10}], + ] ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: next(histories)) monkeypatch.setattr( module, "_history_version_state", @@ -344,6 +344,50 @@ def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: module._verify_ruleset_history_transition(_target(), 7, desired) +def test_history_collision_recovers_admin_write_between_recovery_get_and_put(monkeypatch) -> None: + """A second administrator version displaced by rollback becomes the next restore target.""" + + desired = _converged() + first_admin = _live() + first_admin["conditions"] = {"ref_name": {"include": ["refs/heads/first-admin"], "exclude": []}} + second_admin = _live() + second_admin["conditions"] = {"ref_name": {"include": ["refs/heads/second-admin"], "exclude": []}} + histories = iter( + [ + [{"version_id": 10}, {"version_id": 9}, {"version_id": 7}], + [{"version_id": 12}, {"version_id": 11}], + [{"version_id": 13}, {"version_id": 12}], + ] + ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: next(histories)) + + states = { + 9: first_admin, + 10: desired, + 11: second_admin, + 12: first_admin, + 13: second_admin, + } + monkeypatch.setattr(module, "_history_version_state", lambda _target, version: states[version]) + + calls: list[tuple[str, object | None]] = [] + replies = iter([desired, {}, first_admin, first_admin, {}, second_admin]) + + def fake_api(method, endpoint, *, body=None): + calls.append((method, body)) + return next(replies) + + monkeypatch.setattr(module, "_gh_api", fake_api) + with pytest.raises(module.RulesetGovernanceError, match="restored newest displaced administrator state"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + put_bodies = [body for method, body in calls if method == "PUT"] + assert put_bodies == [ + module._editable_projection(first_admin), + module._editable_projection(second_admin), + ] + + def test_reconcile_forwards_expected_main_sha_to_each_target(monkeypatch) -> None: """The multi-target coordinator preserves the protected-main guard on every mutation.""" @@ -383,11 +427,11 @@ def fake_reconcile(targets, *, verify_only, expected_main_sha=None): assert seen == [(False, "a" * 40)] -def test_owner_plane_workflow_supersedes_stale_runs_and_quotes_main_sha() -> None: - """Only the newest trusted-main owner-plane run may reach mutation.""" +def test_owner_plane_workflow_serializes_mutation_and_quotes_main_sha() -> None: + """PR validation may supersede itself but owner-plane mutation is non-cancellable.""" text = WORKFLOW.read_text(encoding="utf-8") - assert "cancel-in-progress: true" in text + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in text assert "EXPECTED_MAIN_SHA: ${{ github.sha }}" in text assert '--expected-main-sha "$EXPECTED_MAIN_SHA"' in text assert "github.event_name != 'pull_request'" in text From 570b5a7c7797db4b94cf2bc5c13435769ce5f117 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:39:54 +0900 Subject: [PATCH 084/154] ci(governance): validate collision recovery without idle hourly saturation --- .../ruleset-governance-reconcile.yml | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index 648f486095..c54349bde3 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -7,6 +7,8 @@ on: - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" + - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_central_required_workflow_ruleset_audit.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" push: @@ -17,6 +19,8 @@ on: - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" + - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_central_required_workflow_ruleset_audit.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" workflow_dispatch: @@ -35,10 +39,13 @@ concurrency: jobs: validate: - # Every trigger, including a disabled hourly owner-plane schedule, validates - # the reviewed source/manifest offline. The enable variable gates mutation, - # not detection of configuration or regression drift. - runs-on: ubuntu-24.04 + # Source/regression drift is already validated on PR, push, and manual runs. + # When privileged reconciliation is deliberately disabled, an hourly static + # rerun cannot observe live ruleset drift and only consumes shared capacity. + if: >- + github.event_name != 'schedule' || + vars.CWL_RULESET_RECONCILE_ENABLED == 'true' + runs-on: ubuntu-slim timeout-minutes: 20 steps: - name: Harden runner @@ -74,7 +81,11 @@ jobs: python -m coverage run \ --branch \ --include=scripts/ci/reconcile_ruleset_governance.py \ - -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py + -m pytest -q \ + tests/test_ruleset_governance_reconciliation.py \ + tests/test_ruleset_governance_review_regressions.py \ + tests/test_ruleset_governance_review_round2.py \ + tests/test_central_required_workflow_ruleset_audit.py python -m coverage report \ --fail-under=100 \ --show-missing \ From 41b0c971be3d9f0eee51c8be611e6318675a9715 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:44:28 +0900 Subject: [PATCH 085/154] test(governance): make code-owner policy explicit in passing fixture --- tests/test_central_required_workflow_ruleset_audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 908c465a24..87e3d63684 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -130,6 +130,7 @@ def repository_ruleset_payload() -> dict: "parameters": { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, "require_last_push_approval": False, "required_review_thread_resolution": True, "allowed_merge_methods": ["merge", "squash"], From 14690701411939ca102b974a15ec5ce30b1934fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:44:54 +0900 Subject: [PATCH 086/154] test(governance): align stacked audit fixture with canonical policy --- tests/test_central_required_workflow_ruleset_audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 908c465a24..87e3d63684 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -130,6 +130,7 @@ def repository_ruleset_payload() -> dict: "parameters": { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, "require_last_push_approval": False, "required_review_thread_resolution": True, "allowed_merge_methods": ["merge", "squash"], From 2403654c2c3c6dc09b60d0e9661d69efdc20be37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:45:40 +0900 Subject: [PATCH 087/154] test(governance): retire disabled hourly validation contract --- tests/test_ruleset_governance_review_round2.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 35ebfd2613..e45c72a34c 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -82,11 +82,14 @@ def test_history_predecessor_still_rejects_wrong_ruleset_provenance(monkeypatch) raise AssertionError("wrong ruleset provenance was accepted") -def test_owner_plane_is_serial_but_disabled_schedule_still_validates() -> None: - """Mutation critical sections are non-cancellable and offline scheduled validation always runs.""" +def test_owner_plane_is_serial_and_disabled_schedule_does_not_consume_runner() -> None: + """Mutation is non-cancellable while disabled hourly validation skips shared capacity.""" text = WORKFLOW.read_text(encoding="utf-8") assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in text validate_block = text.split("jobs:\n validate:\n", 1)[1].split("\n apply:\n", 1)[0] - assert "vars.CWL_RULESET_RECONCILE_ENABLED" not in validate_block + assert "github.event_name != 'schedule'" in validate_block + assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in validate_block + assert "runs-on: ubuntu-slim" in validate_block apply_block = text.split("\n apply:\n", 1)[1] assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in apply_block + assert "runs-on: ubuntu-24.04" in apply_block From 6b07d3281c30ee40e9f9942146f665c697c4084f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:46:39 +0900 Subject: [PATCH 088/154] docs(governance): record collision-chain and queue-safe owner-plane contract --- docs/doctoring/ruleset-owner-plane-reconciliation.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index fc6f5ab7bc..e1cfd2d5be 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -21,9 +21,13 @@ The application connector can read those settings but does not expose ruleset mu GitHub's current REST contract does not support conditional unsafe REST updates such as `If-Match` on ruleset `PUT`; GitHub's general REST guidance explicitly says conditional unsafe methods are unsupported unless a specific endpoint documents otherwise, and the repository/organization ruleset update endpoints document no such precondition. The second full ruleset read is therefore a drift detector, **not** compare-and-swap. It catches edits visible before the final read but cannot make the final GET-to-PUT interval atomic. A successful HTTP update is also not completion: the reconciler performs a post-write full read and requires the complete editable payload to equal the reviewed update body. -The same GitHub API exposes immutable organization and repository **ruleset history** plus exact version-state reads to Administration-authorized callers. The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. It then re-reads current live state; only if live state still equals our reviewed write does it restore the immediate pre-write version and fail the run. If live state has advanced again, it does not overwrite that newer administrator state and fails for fresh investigation. This history-based recovery prevents a detected hidden edit from being silently lost without pretending GitHub supplied atomic conditional writes. +The same GitHub API exposes immutable organization and repository **ruleset history** plus exact version-state reads to Administration-authorized callers. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules). Current live mutation still requires the reviewed name and active enforcement, but collision recovery deliberately accepts historical names or enforcement values that a legitimate administrator may have changed. -Because the API still cannot make the final GET-to-PUT interval atomic, mutation is permitted only during exclusive owner-plane maintenance: a protected `ruleset-governance-maintenance` environment supplies the dedicated credential, the repository variable explicitly enables mutation, and one shared non-PR Actions concurrency group cancels superseded owner-plane runs. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence/history verification. If protected `main` advances, the stale run fails closed. These controls narrow and expose the residual API race rather than falsely claiming atomicity. Human administrators must not edit the two declared rulesets concurrently with an enabled maintenance run; ruleset history is nevertheless treated as authoritative collision evidence rather than relying on that operating convention alone. +The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. Recovery re-reads live state before every recovery PUT and refuses to overwrite it if it has already advanced. After each recovery PUT, immutable history is read again: the newest version must equal the restore body, and its immediate predecessor must be the version that recovery intended to replace. If a second administrator version slipped between the recovery GET and PUT, that newly displaced predecessor becomes the next recovery target. The bounded recovery chain therefore restores the newest displaced administrator state rather than silently losing it; ambiguous or non-convergent histories fail closed. + +Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence/history verification. If protected `main` advances, the stale run fails closed. + +The hourly schedule is also queue-safe. Source, manifest, and regression changes are already validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the scheduled validation job is skipped because a credential-free static rerun cannot observe live ruleset drift and only consumes shared Actions capacity. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. @@ -33,9 +37,9 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Verification sequence -1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, redacted failures, post-write convergence, exact identity binding, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. -3. Run the reconciler and require one exact new ruleset-history version per target whose predecessor is the sampled pre-write baseline; any intervening version restores the immediate pre-write state before the run fails when restoration remains safe. +3. Run the reconciler and require one exact new ruleset-history version per uncontended target whose predecessor is the sampled pre-write baseline. If an intervening version exists, require recovery to follow immutable predecessor evidence until the newest displaced administrator state is restored or fail closed without claiming success. 4. Re-read both complete live ruleset payloads using the independently authorized credential. 5. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. 6. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. From a62f5fcb93cf13bcd90b682fab8048c34d51cc5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:47:02 +0900 Subject: [PATCH 089/154] chore(governance): remove completed round-two writer workflow --- .../workflows/_temp_pr1644_round2_repair.yml | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 .github/workflows/_temp_pr1644_round2_repair.yml diff --git a/.github/workflows/_temp_pr1644_round2_repair.yml b/.github/workflows/_temp_pr1644_round2_repair.yml deleted file mode 100644 index 639daa22ce..0000000000 --- a/.github/workflows/_temp_pr1644_round2_repair.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: One-shot PR1644 review round-two repair - -on: - push: - branches: - - fix/ruleset-owner-plane-reconciler - paths: - - .github/_temp_pr1644_round2.trigger - -permissions: - contents: write - -concurrency: - group: pr1644-round2-${{ github.ref }} - cancel-in-progress: true - -jobs: - repair: - runs-on: ubuntu-slim - steps: - - name: Checkout exact writer head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - fetch-depth: 0 - - - name: Prove historical editable-identity regression is RED - shell: bash - run: | - set -euo pipefail - if python - <<'PY' - import importlib.util - import sys - from pathlib import Path - p = Path('scripts/ci/reconcile_ruleset_governance.py') - s = importlib.util.spec_from_file_location('r', p) - m = importlib.util.module_from_spec(s); sys.modules[s.name] = m; s.loader.exec_module(m) - t = m.RulesetTarget('repository', 'ContextualWisdomLab', '.github', 17921150, 'Lock default branch') - state = {'id': 17921150, 'name': 'Admin renamed', 'target': 'branch', 'source_type': 'Repository', 'source': 'ContextualWisdomLab/.github', 'enforcement': 'evaluate', 'bypass_actors': [], 'conditions': {}, 'rules': []} - m._gh_api = lambda *_a, **_k: {'state': state} - m._history_version_state(t, 7) - PY - then - echo '::error::historical editable-identity regression was not RED' - exit 1 - fi - - - name: Apply exact source/test/docs repair - shell: bash - run: | - set -euo pipefail - python scripts/ci/_temp_repair_pr1644_round2.py - - - name: Verify focused GREEN contracts - shell: bash - run: | - set -euo pipefail - python -m py_compile scripts/ci/reconcile_ruleset_governance.py tests/test_ruleset_governance_review_round2.py - python - <<'PY' - import importlib.util - import sys - from pathlib import Path - p = Path('scripts/ci/reconcile_ruleset_governance.py') - s = importlib.util.spec_from_file_location('r', p) - m = importlib.util.module_from_spec(s); sys.modules[s.name] = m; s.loader.exec_module(m) - t = m.RulesetTarget('repository', 'ContextualWisdomLab', '.github', 17921150, 'Lock default branch') - state = {'id': 17921150, 'name': 'Admin renamed', 'target': 'branch', 'source_type': 'Repository', 'source': 'ContextualWisdomLab/.github', 'enforcement': 'evaluate', 'bypass_actors': [], 'conditions': {}, 'rules': []} - m._gh_api = lambda *_a, **_k: {'state': state} - assert m._history_version_state(t, 7) == state - workflow = Path('.github/workflows/ruleset-governance-reconcile.yml').read_text() - assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in workflow - fixture = Path('tests/test_central_required_workflow_ruleset_audit.py').read_text() - assert '"require_code_owner_review": False' in fixture - assert 'tests/test_ruleset_governance_review_round2.py' in workflow - PY - git diff --check - - - name: Remove one-shot artifacts and publish only on unchanged writer lineage - shell: bash - run: | - set -euo pipefail - git rm -- .github/workflows/_temp_pr1644_round2_repair.yml .github/_temp_pr1644_round2.trigger scripts/ci/_temp_repair_pr1644_round2.py - git add -A - git diff --cached --check - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix(governance): recover concurrent editable identity safely' - git fetch origin fix/ruleset-owner-plane-reconciler - remote_head="$(git rev-parse FETCH_HEAD)" - parent="$(git rev-parse HEAD^)" - if [ "$remote_head" != "$parent" ]; then - echo "Remote writer moved from $parent to $remote_head; refusing overwrite." >&2 - exit 1 - fi - git push origin HEAD:fix/ruleset-owner-plane-reconciler From 83dbf5ad5538d2ab4aa49e563b3a6617a96b8536 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:47:07 +0900 Subject: [PATCH 090/154] chore(governance): remove completed round-two trigger --- .github/_temp_pr1644_round2.trigger | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/_temp_pr1644_round2.trigger diff --git a/.github/_temp_pr1644_round2.trigger b/.github/_temp_pr1644_round2.trigger deleted file mode 100644 index 2b23a24180..0000000000 --- a/.github/_temp_pr1644_round2.trigger +++ /dev/null @@ -1,2 +0,0 @@ -repair=review-round-two -head-parent=5b9609b26f2cf380e25757d3ce281a76b194cc49 From 85c5832a8c1f716fe9c79873e6d104abe35c8d3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:47:19 +0900 Subject: [PATCH 091/154] chore(governance): remove completed round-two repair helper --- scripts/ci/_temp_repair_pr1644_round2.py | 157 ----------------------- 1 file changed, 157 deletions(-) delete mode 100644 scripts/ci/_temp_repair_pr1644_round2.py diff --git a/scripts/ci/_temp_repair_pr1644_round2.py b/scripts/ci/_temp_repair_pr1644_round2.py deleted file mode 100644 index 0da0828467..0000000000 --- a/scripts/ci/_temp_repair_pr1644_round2.py +++ /dev/null @@ -1,157 +0,0 @@ -"""One-shot exact-text repair for PR #1644 review round two.""" - -from __future__ import annotations - -from pathlib import Path - - -def replace_once(text: str, old: str, new: str, label: str) -> str: - """Replace one reviewed source anchor or abort without partial mutation.""" - if text.count(old) != 1: - raise SystemExit(f"expected exactly one {label} anchor, found {text.count(old)}") - return text.replace(old, new, 1) - - -def repair_reconciler() -> None: - """Separate immutable history provenance from editable live identity.""" - path = Path("scripts/ci/reconcile_ruleset_governance.py") - text = path.read_text(encoding="utf-8") - old = '''def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: - """Return the exact ruleset state stored at one immutable history version.""" - - payload = _gh_api("GET", target.history_version_endpoint(version_id)) - state = _plain_dict(payload.get("state"), field="ruleset history version state") - _assert_identity(state, target) - return state - - -def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: - """Require the live ruleset to be the exact reviewed object before mutation.""" - - expected = { - "id": target.ruleset_id, - "name": target.name, - "target": "branch", - "source_type": target.source_type, - "source": target.source, - "enforcement": "active", - } - mismatches = [key for key, value in expected.items() if live.get(key) != value] - if mismatches: - raise RulesetGovernanceError( - f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" - ) -''' - new = '''def _assert_target_provenance(live: dict[str, Any], target: RulesetTarget) -> None: - """Require immutable ruleset provenance while allowing historical editable fields.""" - - expected = { - "id": target.ruleset_id, - "target": "branch", - "source_type": target.source_type, - "source": target.source, - } - mismatches = [key for key, value in expected.items() if live.get(key) != value] - if mismatches: - raise RulesetGovernanceError( - f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" - ) - - -def _history_version_state(target: RulesetTarget, version_id: int) -> dict[str, Any]: - """Return one historical state after proving it belongs to the exact target.""" - - payload = _gh_api("GET", target.history_version_endpoint(version_id)) - state = _plain_dict(payload.get("state"), field="ruleset history version state") - _assert_target_provenance(state, target) - return state - - -def _assert_identity(live: dict[str, Any], target: RulesetTarget) -> None: - """Require current live state to retain the reviewed editable identity too.""" - - _assert_target_provenance(live, target) - expected = {"name": target.name, "enforcement": "active"} - mismatches = [key for key, value in expected.items() if live.get(key) != value] - if mismatches: - raise RulesetGovernanceError( - f"{target.scope} ruleset identity drift: {', '.join(sorted(mismatches))}" - ) -''' - text = replace_once(text, old, new, "history/live identity split") - text = replace_once( - text, - ''' restored = _gh_api("GET", target.endpoint) - _assert_identity(restored, target) - if _editable_projection(restored) != predecessor_payload: -''', - ''' restored = _gh_api("GET", target.endpoint) - _assert_target_provenance(restored, target) - if _editable_projection(restored) != predecessor_payload: -''', - "historical rollback verification", - ) - path.write_text(text, encoding="utf-8") - - -def repair_audit_fixture() -> None: - """Keep the canonical passing repository-ruleset fixture complete.""" - path = Path("tests/test_central_required_workflow_ruleset_audit.py") - text = path.read_text(encoding="utf-8") - text = replace_once( - text, - ''' "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": True, - "require_last_push_approval": False, -''', - ''' "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": True, - "require_code_owner_review": False, - "require_last_push_approval": False, -''', - "repository audit fixture code-owner flag", - ) - path.write_text(text, encoding="utf-8") - - -def repair_validation_scope() -> None: - """Make the new regression part of every reviewed reconciler validation path.""" - path = Path(".github/workflows/ruleset-governance-reconcile.yml") - text = path.read_text(encoding="utf-8") - marker = ' - "tests/test_ruleset_governance_review_regressions.py"\n' - if text.count(marker) != 2: - raise SystemExit(f"unexpected workflow path marker count: {text.count(marker)}") - text = text.replace( - marker, - marker + ' - "tests/test_ruleset_governance_review_round2.py"\n', - ) - text = replace_once( - text, - ''' -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py -''', - ''' -m pytest -q tests/test_ruleset_governance_reconciliation.py tests/test_ruleset_governance_review_regressions.py tests/test_ruleset_governance_review_round2.py -''', - "focused pytest command", - ) - path.write_text(text, encoding="utf-8") - - -def repair_docs() -> None: - """Record the cancellation and historical-identity recovery boundary.""" - path = Path("docs/doctoring/ruleset-owner-plane-reconciliation.md") - text = path.read_text(encoding="utf-8") - heading = "## 2026-09-02 review-round-two recovery hardening" - if heading not in text: - text = text.rstrip() + f'''\n\n{heading}\n\nPrivileged owner-plane reconciliation is serialized without `cancel-in-progress`.\nPull-request validation may still supersede older validation runs, but a protected-main\nmutation run must finish its PUT plus immutable-history verification/recovery critical\nsection. This prevents a newer run from observing the desired payload after cancelling\na predecessor between PUT and collision verification.\n\nRuleset history is validated against immutable target provenance (`id`, branch target,\nsource type, and source). Historical `name` and `enforcement` are deliberately allowed\nto differ because they are editable administrator state that collision recovery may\nneed to restore. Current live mutation still requires the reviewed name and active\nenforcement. Rollback verifies exact predecessor editable payload convergence while\nretaining immutable target provenance, so a legitimate concurrent rename/enforcement\nchange cannot be silently overwritten.\n''' - path.write_text(text, encoding="utf-8") - - -def main() -> None: - repair_reconciler() - repair_audit_fixture() - repair_validation_scope() - repair_docs() - - -if __name__ == "__main__": - main() From 0dea7d1de08a023ed851b3bb775359d67bdd6b97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:23:27 +0900 Subject: [PATCH 092/154] test(governance): pin unresolved owner-plane review regressions Add RED-first coverage for omitted empty required_reviewers, protected-main revalidation before recovery writes, canonical-audit drift outside the narrow reconciliation projection, and direct CI inclusion of every permanent governance regression suite. --- .../test_ruleset_governance_review_round2.py | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index e45c72a34c..91b3c914ce 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -1,10 +1,12 @@ -"""Second-round regressions for ruleset owner-plane collision recovery.""" +"""Second-round and follow-up regressions for ruleset owner-plane reconciliation.""" from __future__ import annotations import importlib.util from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" @@ -31,6 +33,41 @@ def repository_target(module): ) +def repository_payload(*, include_deletion: bool = True) -> dict: + """Return a canonical owner-repository ruleset payload for focused regressions.""" + rules = [] + if include_deletion: + rules.append({"type": "deletion"}) + rules.extend( + [ + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + ] + ) + return { + "id": 17921150, + "name": "Lock default branch", + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": "active", + "bypass_actors": [], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": rules, + } + + def historical_state(*, name: str = "Admin renamed", enforcement: str = "evaluate") -> dict: """Return a predecessor whose editable identity differs but provenance is unchanged.""" return { @@ -93,3 +130,83 @@ def test_owner_plane_is_serial_and_disabled_schedule_does_not_consume_runner() - apply_block = text.split("\n apply:\n", 1)[1] assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in apply_block assert "runs-on: ubuntu-24.04" in apply_block + + +def test_missing_empty_required_reviewers_is_normalized_to_declared_empty_list() -> None: + """GitHub may omit an empty reviewer list; desired state must add it instead of aborting.""" + module = load_module() + live = repository_payload() + parameters = live["rules"][-1]["parameters"] + del parameters["required_reviewers"] + + desired = module._desired_payload(live, repository_target(module)) + + assert desired["rules"][-1]["parameters"]["required_reviewers"] == [] + + +def test_recovery_revalidates_protected_main_before_every_recovery_put(monkeypatch) -> None: + """A stale run must stop before a recovery PUT after protected main advances.""" + module = load_module() + target = repository_target(module) + current_payload = repository_payload() + displaced_payload = historical_state() + puts: list[dict] = [] + main_checks: list[str] = [] + + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced_payload) + + def fake_api(method, endpoint, *, body=None): + if method == "GET" and endpoint == target.endpoint: + return current_payload + if method == "PUT" and endpoint == target.endpoint: + puts.append(body) + return {} + raise AssertionError((method, endpoint)) + + def stale_main(expected_sha: str) -> None: + main_checks.append(expected_sha) + raise module.RulesetGovernanceError("protected main advanced") + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_assert_current_main", stale_main) + + with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=current_payload, + displaced_version=9, + expected_main_sha="a" * 40, + ) + + assert main_checks == ["a" * 40] + assert puts == [] + + +def test_verify_only_rejects_drift_outside_reconciler_projection(monkeypatch) -> None: + """Canonical audit drift cannot be reported as converged merely because review fields match.""" + module = load_module() + target = repository_target(module) + live = repository_payload(include_deletion=False) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: live) + + with pytest.raises(module.RulesetGovernanceError, match="canonical governance drift"): + module._reconcile_target(target, verify_only=True) + + +def test_focused_workflow_runs_every_permanent_governance_regression_suite() -> None: + """Every permanent audit/reconciler regression participates in path and pytest coverage.""" + text = WORKFLOW.read_text(encoding="utf-8") + expected = ( + "tests/test_ruleset_governance_reconciliation.py", + "tests/test_ruleset_governance_review_regressions.py", + "tests/test_ruleset_governance_review_round2.py", + "tests/test_central_required_workflow_ruleset_audit.py", + "tests/test_ruleset_audit_completeness_regression.py", + "tests/test_ruleset_merge_method_shape_regression.py", + "tests/test_solo_maintainer_ruleset_policy.py", + ) + test_command = text.split("-m pytest -q \\\n", 1)[1].split("\n python -m coverage report", 1)[0] + for path in expected: + assert text.count(f'- "{path}"') >= 2 + assert path in test_command From 8273fb579ece45ce053952b2648b834ed3d2f631 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:25:40 +0900 Subject: [PATCH 093/154] fix(governance): close owner-plane review gaps Normalize omitted empty required_reviewers, bind every collision-recovery PUT to the reviewed protected-main SHA, fail closed through the canonical ruleset audit before claiming convergence, and run every permanent governance regression in the focused workflow. --- .../ruleset-governance-reconcile.yml | 11 +++- scripts/ci/reconcile_ruleset_governance.py | 61 +++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index c54349bde3..f8acf49087 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -9,6 +9,9 @@ on: - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" - "tests/test_central_required_workflow_ruleset_audit.py" + - "tests/test_ruleset_audit_completeness_regression.py" + - "tests/test_ruleset_merge_method_shape_regression.py" + - "tests/test_solo_maintainer_ruleset_policy.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" push: @@ -21,6 +24,9 @@ on: - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" - "tests/test_central_required_workflow_ruleset_audit.py" + - "tests/test_ruleset_audit_completeness_regression.py" + - "tests/test_ruleset_merge_method_shape_regression.py" + - "tests/test_solo_maintainer_ruleset_policy.py" - "docs/doctoring/ruleset-owner-plane-reconciliation.md" - ".github/workflows/ruleset-governance-reconcile.yml" workflow_dispatch: @@ -85,7 +91,10 @@ jobs: tests/test_ruleset_governance_reconciliation.py \ tests/test_ruleset_governance_review_regressions.py \ tests/test_ruleset_governance_review_round2.py \ - tests/test_central_required_workflow_ruleset_audit.py + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_ruleset_audit_completeness_regression.py \ + tests/test_ruleset_merge_method_shape_regression.py \ + tests/test_solo_maintainer_ruleset_policy.py python -m coverage report \ --fail-under=100 \ --show-missing \ diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index faece0cbc4..ab47cc635c 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -9,7 +9,10 @@ the exact protected-main revision, serialized owner-plane execution, and the immutable ruleset-history surface. If history proves a hidden pre-PUT edit was overwritten, the reconciler restores the newest displaced administrator state -before failing; recovery re-checks immutable history after every restore write. +before failing; recovery re-checks immutable history and protected-main identity +before every recovery write. The canonical audit is executed against projected +and live state so this narrow reconciler never reports broader policy drift as +converged. """ from __future__ import annotations @@ -306,6 +309,36 @@ def _editable_projection(live: dict[str, Any]) -> dict[str, Any]: return projection +def _canonical_governance_errors( + live: dict[str, Any], target: RulesetTarget +) -> list[str]: + """Run the canonical audit against one complete live-shaped ruleset payload.""" + + auditor = Path(__file__).with_name("audit_central_required_workflows.py") + mode = {"repository": ["--repository"], "organization": []}[target.scope] + completed = subprocess.run( + [sys.executable, str(auditor), *mode], + check=False, + input=json.dumps(live, separators=(",", ":")), + capture_output=True, + text=True, + timeout=30, + ) + return [] if completed.returncode == 0 else [completed.stderr.strip()] + + +def _assert_canonical_governance( + live: dict[str, Any], target: RulesetTarget +) -> None: + """Fail closed when canonical audit policy still reports any governance drift.""" + + errors = _canonical_governance_errors(live, target) + if errors: + raise RulesetGovernanceError( + f"{target.scope} canonical governance drift remains: {errors[0]}" + ) + + def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, Any]: """Build the exact safe update body while preserving unrelated live controls.""" @@ -327,7 +360,6 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A "required_approving_review_count": int, "require_code_owner_review": bool, "require_last_push_approval": bool, - "required_reviewers": list, "allowed_merge_methods": list, } for field, expected_type in required_parameters.items(): @@ -335,6 +367,10 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A raise RulesetGovernanceError( f"pull_request.parameters.{field} has invalid type" ) + if "required_reviewers" in parameters and type(parameters["required_reviewers"]) is not list: + raise RulesetGovernanceError( + "pull_request.parameters.required_reviewers has invalid type" + ) parameters["required_approving_review_count"] = 0 parameters["require_code_owner_review"] = False @@ -350,6 +386,7 @@ def _recover_displaced_history_state( current_version: int, current_payload: dict[str, Any], displaced_version: int, + expected_main_sha: str | None = None, ) -> None: """Restore the newest state displaced by our unsafe PUT without hiding races. @@ -357,7 +394,9 @@ def _recover_displaced_history_state( verifies immutable history immediately afterward. If an administrator write slipped between the recovery GET and PUT, that displaced history version becomes the next recovery target. A newer live state observed before a - recovery write is never overwritten. The loop is bounded and fails closed. + recovery write is never overwritten. Privileged recovery revalidates the + reviewed protected-main SHA immediately before every PUT. The loop is bounded + and fails closed. """ for _attempt in range(COLLISION_RECOVERY_LIMIT): @@ -369,6 +408,8 @@ def _recover_displaced_history_state( raise RulesetGovernanceError( "concurrent ruleset history detected but live state advanced again; refusing recovery" ) + if expected_main_sha is not None: + _assert_current_main(expected_main_sha) _gh_api("PUT", target.endpoint, body=displaced_payload) history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") @@ -406,6 +447,8 @@ def _verify_ruleset_history_transition( target: RulesetTarget, baseline_version: int, desired: dict[str, Any], + *, + expected_main_sha: str | None = None, ) -> None: """Detect hidden pre-PUT edits and restore the newest displaced state safely. @@ -434,6 +477,7 @@ def _verify_ruleset_history_transition( current_version=newest_id, current_payload=desired, displaced_version=predecessor_id, + expected_main_sha=expected_main_sha, ) raise RulesetGovernanceError( "concurrent ruleset history detected; restored newest displaced administrator state" @@ -450,6 +494,9 @@ def _reconcile_target( first = _gh_api("GET", target.endpoint) desired = _desired_payload(first, target) + projected = copy.deepcopy(first) + projected.update(desired) + _assert_canonical_governance(projected, target) if _editable_projection(first) == desired: return False if verify_only: @@ -474,8 +521,14 @@ def _reconcile_target( _assert_identity(after, target) if _editable_projection(after) != desired: raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") + _assert_canonical_governance(after, target) if baseline_version is not None: - _verify_ruleset_history_transition(target, baseline_version, desired) + _verify_ruleset_history_transition( + target, + baseline_version, + desired, + expected_main_sha=expected_main_sha, + ) if expected_main_sha is not None: _assert_current_main(expected_main_sha) return True From bf1fe970782fa30b8f9dad2ed5984270fe9c3b48 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:27:55 +0900 Subject: [PATCH 094/154] test(governance): keep main-guard mock aligned with recovery contract Allow the existing exact-main regression double to accept the newly threaded expected-main keyword while preserving its original assertion that protected main is checked before read/write completion. --- tests/test_ruleset_governance_review_regressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 6945159067..f995ce1170 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -167,7 +167,7 @@ def test_current_main_is_rechecked_around_put_and_after_convergence(monkeypatch) monkeypatch.setattr( module, "_verify_ruleset_history_transition", - lambda _target, _baseline, _desired: None, + lambda _target, _baseline, _desired, **_kwargs: None, ) assert module._reconcile_target( From f66d8d3f1fb521602e8b8b3aafc1e78f18bdb6b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:16:52 +0900 Subject: [PATCH 095/154] test(governance): cover ambiguous ruleset PUT timeouts --- ...t_ruleset_governance_timeout_regression.py | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 tests/test_ruleset_governance_timeout_regression.py diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py new file mode 100644 index 0000000000..7602b23ef6 --- /dev/null +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -0,0 +1,196 @@ +"""Regress ambiguous ruleset mutation timeouts and manual owner-plane safeguards.""" + +from __future__ import annotations + +import importlib.util +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" + + +def load_module(): + """Load the production reconciler from the exact checkout.""" + + spec = importlib.util.spec_from_file_location("ruleset_governance_timeout_regression", SOURCE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def repository_target(module): + """Return the exact central owner-repository target used by the manifest.""" + + return module.RulesetTarget( + scope="repository", + owner="ContextualWisdomLab", + repository=".github", + ruleset_id=17921150, + name="Lock default branch", + ) + + +def live_payload() -> dict: + """Return one live-shaped repository ruleset with deliberate governance drift.""" + + return { + "id": 17921150, + "name": "Lock default branch", + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": "active", + "bypass_actors": [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash", "rebase"], + }, + }, + ], + } + + +def exercise_ambiguous_put(monkeypatch, *, history_outcome: str) -> tuple[object, list[tuple]]: + """Run one privileged mutation whose PUT times out after an unknown server outcome.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + calls: list[tuple] = [] + get_count = 0 + + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_latest_history_version", lambda *_args: 41) + + def fake_api(method, endpoint, *, body=None): + nonlocal get_count + calls.append((method, endpoint, body)) + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + if get_count <= 2: + return first + return converged + if method == "PUT" and endpoint == target.endpoint: + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + def verify_history( + seen_target, + baseline_version, + seen_desired, + *, + expected_main_sha=None, + ): + calls.append(("HISTORY", baseline_version, seen_desired, expected_main_sha)) + assert seen_target == target + assert baseline_version == 41 + assert seen_desired == desired + assert expected_main_sha == "a" * 40 + if history_outcome == "not-accepted": + raise module.RulesetGovernanceError("ruleset mutation is not visible in history") + if history_outcome == "collision": + raise module.RulesetGovernanceError( + "concurrent ruleset history detected; restored newest displaced administrator state" + ) + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_verify_ruleset_history_transition", verify_history) + return module, calls + + +def test_put_timeout_before_acceptance_enters_history_path_and_fails_closed(monkeypatch) -> None: + """An unaccepted ambiguous PUT must be disproved from history rather than leak TimeoutExpired.""" + + module, calls = exercise_ambiguous_put(monkeypatch, history_outcome="not-accepted") + with pytest.raises(module.RulesetGovernanceError, match="not visible in history"): + module._reconcile_target( + repository_target(module), + verify_only=False, + expected_main_sha="a" * 40, + ) + assert any(call[0] == "HISTORY" for call in calls) + + +def test_put_timeout_after_acceptance_without_collision_verifies_and_converges(monkeypatch) -> None: + """A committed timed-out PUT succeeds only after immutable history and live convergence agree.""" + + module, calls = exercise_ambiguous_put(monkeypatch, history_outcome="accepted") + assert ( + module._reconcile_target( + repository_target(module), + verify_only=False, + expected_main_sha="a" * 40, + ) + is True + ) + assert sum(call[0] == "HISTORY" for call in calls) == 1 + assert sum(call[0] == "PUT" for call in calls) == 1 + + +def test_put_timeout_after_acceptance_with_collision_preserves_recovery_failure(monkeypatch) -> None: + """A timed-out PUT that displaced another version must run recovery and remain fail-closed.""" + + module, calls = exercise_ambiguous_put(monkeypatch, history_outcome="collision") + with pytest.raises(module.RulesetGovernanceError, match="restored newest displaced"): + module._reconcile_target( + repository_target(module), + verify_only=False, + expected_main_sha="a" * 40, + ) + assert any(call[0] == "HISTORY" for call in calls) + + +def test_manual_mutation_requires_exact_protected_main_guard(monkeypatch) -> None: + """Non-Actions operators cannot invoke the weaker mutation mode without a main SHA guard.""" + + module = load_module() + target = repository_target(module) + monkeypatch.setenv("GH_TOKEN", "redacted-test-token") + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.setattr(module, "load_manifest", lambda _path: (target,)) + monkeypatch.setattr(module, "reconcile", lambda *_args, **_kwargs: 0) + + with pytest.raises(module.RulesetGovernanceError, match="expected protected main SHA"): + module.main([]) + + +def test_verify_only_remains_available_without_mutation_main_guard(monkeypatch, capsys) -> None: + """Read-only live verification remains usable without granting mutation authority.""" + + module = load_module() + target = repository_target(module) + observed: list[tuple] = [] + monkeypatch.setenv("GH_TOKEN", "redacted-test-token") + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.setattr(module, "load_manifest", lambda _path: (target,)) + + def fake_reconcile(targets, *, verify_only, expected_main_sha=None): + observed.append((targets, verify_only, expected_main_sha)) + return 0 + + monkeypatch.setattr(module, "reconcile", fake_reconcile) + assert module.main(["--verify-only"]) == 0 + assert observed == [((target,), True, None)] + assert "verified 1 ruleset governance targets" in capsys.readouterr().out From b3b095eaac7b17da558884fa65199e16d28c1435 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:18:44 +0900 Subject: [PATCH 096/154] fix(governance): reconcile ambiguous ruleset timeouts --- scripts/ci/reconcile_ruleset_governance.py | 157 ++++++++++++++++----- 1 file changed, 121 insertions(+), 36 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index ab47cc635c..a07fe481a9 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -10,9 +10,10 @@ immutable ruleset-history surface. If history proves a hidden pre-PUT edit was overwritten, the reconciler restores the newest displaced administrator state before failing; recovery re-checks immutable history and protected-main identity -before every recovery write. The canonical audit is executed against projected -and live state so this narrow reconciler never reports broader policy drift as -converged. +before every recovery write. Ambiguous mutation timeouts are resolved from live +state plus immutable history instead of being treated as ordinary request +failures. The canonical audit is executed against projected and live state so +this narrow reconciler never reports broader policy drift as converged. """ from __future__ import annotations @@ -172,14 +173,21 @@ def _run_gh_json( command = _gh_command(method, endpoint) if body is not None: command.extend(["--input", "-"]) - completed = subprocess.run( - command, - check=False, - input=None if body is None else json.dumps(body, separators=(",", ":")), - capture_output=True, - text=True, - timeout=30, - ) + try: + completed = subprocess.run( + command, + check=False, + input=None if body is None else json.dumps(body, separators=(",", ":")), + capture_output=True, + text=True, + timeout=30, + ) + except subprocess.TimeoutExpired as exc: + if method == "PUT": + raise + raise RulesetGovernanceError( + f"GitHub API request timed out for {endpoint}" + ) from exc if completed.returncode != 0: raise RulesetGovernanceError(f"GitHub API request failed for {endpoint}") return json.loads(completed.stdout) @@ -395,8 +403,9 @@ def _recover_displaced_history_state( slipped between the recovery GET and PUT, that displaced history version becomes the next recovery target. A newer live state observed before a recovery write is never overwritten. Privileged recovery revalidates the - reviewed protected-main SHA immediately before every PUT. The loop is bounded - and fails closed. + reviewed protected-main SHA immediately before every PUT. Ambiguous recovery + timeouts inspect immutable history before retrying or advancing the recovery + chain. The loop is bounded and fails closed. """ for _attempt in range(COLLISION_RECOVERY_LIMIT): @@ -411,19 +420,40 @@ def _recover_displaced_history_state( if expected_main_sha is not None: _assert_current_main(expected_main_sha) - _gh_api("PUT", target.endpoint, body=displaced_payload) - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if len(history) < 2: - raise RulesetGovernanceError( - "ruleset collision recovery history did not expose a predecessor" - ) - recovery_version = _history_version_id(history[0]) - recovery_predecessor = _history_version_id(history[1]) - recovery_state = _history_version_state(target, recovery_version) - if _editable_projection(recovery_state) != displaced_payload: - raise RulesetGovernanceError( - "ruleset collision recovery latest history does not match restore write" - ) + try: + _gh_api("PUT", target.endpoint, body=displaced_payload) + except subprocess.TimeoutExpired: + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT timeout exposed no history" + ) + recovery_version = _history_version_id(history[0]) + if recovery_version == current_version: + continue + if len(history) < 2: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT timeout exposed no predecessor" + ) + recovery_predecessor = _history_version_id(history[1]) + recovery_state = _history_version_state(target, recovery_version) + if _editable_projection(recovery_state) != displaced_payload: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT timeout left a newer state; refusing overwrite" + ) + else: + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if len(history) < 2: + raise RulesetGovernanceError( + "ruleset collision recovery history did not expose a predecessor" + ) + recovery_version = _history_version_id(history[0]) + recovery_predecessor = _history_version_id(history[1]) + recovery_state = _history_version_state(target, recovery_version) + if _editable_projection(recovery_state) != displaced_payload: + raise RulesetGovernanceError( + "ruleset collision recovery latest history does not match restore write" + ) restored = _gh_api("GET", target.endpoint) _assert_target_provenance(restored, target) @@ -484,6 +514,43 @@ def _verify_ruleset_history_transition( ) +def _confirm_ambiguous_put( + target: RulesetTarget, + *, + baseline_version: int, + desired: dict[str, Any], + expected_main_sha: str | None, +) -> dict[str, Any]: + """Resolve a timed-out PUT from immutable history and exact live convergence. + + A transport timeout is ambiguous because GitHub may have committed the + mutation. The protected-main and history guards therefore decide whether the + write was absent, committed exactly, or displaced an intervening version. + No mutation retry occurs here. + """ + + if expected_main_sha is None: + raise RulesetGovernanceError( + "ambiguous ruleset PUT timeout requires protected-main history guard" + ) + _assert_current_main(expected_main_sha) + _verify_ruleset_history_transition( + target, + baseline_version, + desired, + expected_main_sha=expected_main_sha, + ) + after = _gh_api("GET", target.endpoint) + _assert_identity(after, target) + if _editable_projection(after) != desired: + raise RulesetGovernanceError( + f"{target.scope} timed-out ruleset mutation did not converge" + ) + _assert_canonical_governance(after, target) + _assert_current_main(expected_main_sha) + return after + + def _reconcile_target( target: RulesetTarget, *, @@ -516,13 +583,30 @@ def _reconcile_target( _assert_identity(second, target) if expected_main_sha is not None: _assert_current_main(expected_main_sha) - _gh_api("PUT", target.endpoint, body=desired) - after = _gh_api("GET", target.endpoint) + + history_verified = False + try: + _gh_api("PUT", target.endpoint, body=desired) + except subprocess.TimeoutExpired: + if baseline_version is None: + raise RulesetGovernanceError( + "ambiguous ruleset PUT timeout requires protected-main history guard" + ) + after = _confirm_ambiguous_put( + target, + baseline_version=baseline_version, + desired=desired, + expected_main_sha=expected_main_sha, + ) + history_verified = True + else: + after = _gh_api("GET", target.endpoint) + _assert_identity(after, target) if _editable_projection(after) != desired: raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") _assert_canonical_governance(after, target) - if baseline_version is not None: + if baseline_version is not None and not history_verified: _verify_ruleset_history_transition( target, baseline_version, @@ -588,13 +672,9 @@ def main(argv: list[str] | None = None) -> int: raise RulesetGovernanceError( "GH_TOKEN is required for live ruleset governance" ) - if ( - not args.verify_only - and os.environ.get("GITHUB_ACTIONS") == "true" - and not args.expected_main_sha - ): + if not args.verify_only and not args.expected_main_sha: raise RulesetGovernanceError( - "expected protected main SHA is required for Actions mutation" + "expected protected main SHA is required for mutation" ) if args.expected_main_sha is None: mutations = reconcile(targets, verify_only=args.verify_only) @@ -614,7 +694,12 @@ def cli() -> None: try: raise SystemExit(main()) - except (OSError, json.JSONDecodeError, RulesetGovernanceError) as exc: + except ( + OSError, + json.JSONDecodeError, + subprocess.TimeoutExpired, + RulesetGovernanceError, + ) as exc: print(f"ruleset governance reconciliation failed: {exc}", file=sys.stderr) raise SystemExit(1) from exc From a0cda3c3785157b151e89298597c873fe746d066 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:19:37 +0900 Subject: [PATCH 097/154] test(governance): require guarded manual mutation --- .../test_ruleset_governance_reconciliation.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_ruleset_governance_reconciliation.py b/tests/test_ruleset_governance_reconciliation.py index 4355526fbe..76017688ff 100644 --- a/tests/test_ruleset_governance_reconciliation.py +++ b/tests/test_ruleset_governance_reconciliation.py @@ -405,7 +405,7 @@ def fake_run(command, **kwargs): def test_main_validation_needs_no_token_and_live_modes_do(tmp_path: Path, monkeypatch, capsys) -> None: - """PR validation is offline while apply/verify require the protected credential.""" + """PR validation is offline; live mutation additionally requires an exact main guard.""" manifest = write_manifest(tmp_path, valid_manifest()) monkeypatch.delenv("GH_TOKEN", raising=False) @@ -415,10 +415,26 @@ def test_main_validation_needs_no_token_and_live_modes_do(tmp_path: Path, monkey module.main(["--manifest", str(manifest)]) monkeypatch.setenv("GH_TOKEN", "not-printed") - monkeypatch.setattr(module, "reconcile", lambda targets, verify_only: 0) + monkeypatch.setattr( + module, + "reconcile", + lambda targets, verify_only, expected_main_sha=None: 0, + ) assert module.main(["--manifest", str(manifest), "--verify-only"]) == 0 assert "verified 2" in capsys.readouterr().out - assert module.main(["--manifest", str(manifest)]) == 0 + with pytest.raises(module.RulesetGovernanceError, match="expected protected main SHA"): + module.main(["--manifest", str(manifest)]) + assert ( + module.main( + [ + "--manifest", + str(manifest), + "--expected-main-sha", + "a" * 40, + ] + ) + == 0 + ) assert "reconciled 2" in capsys.readouterr().out From 5e74e14cc082ee70a171e2122056c35e029c171a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:19:59 +0900 Subject: [PATCH 098/154] ci(governance): execute timeout regression suite --- .github/workflows/ruleset-governance-reconcile.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index f8acf49087..c0c99a7ec3 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -8,6 +8,7 @@ on: - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -23,6 +24,7 @@ on: - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -91,6 +93,7 @@ jobs: tests/test_ruleset_governance_reconciliation.py \ tests/test_ruleset_governance_review_regressions.py \ tests/test_ruleset_governance_review_round2.py \ + tests/test_ruleset_governance_timeout_regression.py \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_ruleset_audit_completeness_regression.py \ tests/test_ruleset_merge_method_shape_regression.py \ From 14009207443a0918fbe57032d4f3ebd0dde54e2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:20:26 +0900 Subject: [PATCH 099/154] docs(governance): record timeout and manual guard contract --- docs/doctoring/ruleset-owner-plane-reconciliation.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index e1cfd2d5be..f7d4d9f172 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -21,11 +21,13 @@ The application connector can read those settings but does not expose ruleset mu GitHub's current REST contract does not support conditional unsafe REST updates such as `If-Match` on ruleset `PUT`; GitHub's general REST guidance explicitly says conditional unsafe methods are unsupported unless a specific endpoint documents otherwise, and the repository/organization ruleset update endpoints document no such precondition. The second full ruleset read is therefore a drift detector, **not** compare-and-swap. It catches edits visible before the final read but cannot make the final GET-to-PUT interval atomic. A successful HTTP update is also not completion: the reconciler performs a post-write full read and requires the complete editable payload to equal the reviewed update body. +A ruleset `PUT` transport timeout is treated as an ambiguous commit result, not as an ordinary failed request. GitHub may have accepted the mutation before the client-side timeout. The privileged path therefore enters the same immutable-history verification contract after a timed-out initial write: a baseline still at the newest history version proves no visible acceptance and fails closed without retrying the desired mutation; a newest version equal to the reviewed body with the sampled baseline as predecessor proves exact acceptance; an intervening predecessor triggers the existing displaced-administrator recovery chain and still fails closed after recovery. Recovery `PUT` timeouts are likewise checked against immutable history before a retry or progression in the bounded recovery chain, so a timeout cannot silently strand an overwritten administrator state. + The same GitHub API exposes immutable organization and repository **ruleset history** plus exact version-state reads to Administration-authorized callers. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules). Current live mutation still requires the reviewed name and active enforcement, but collision recovery deliberately accepts historical names or enforcement values that a legitimate administrator may have changed. The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. Recovery re-reads live state before every recovery PUT and refuses to overwrite it if it has already advanced. After each recovery PUT, immutable history is read again: the newest version must equal the restore body, and its immediate predecessor must be the version that recovery intended to replace. If a second administrator version slipped between the recovery GET and PUT, that newly displaced predecessor becomes the next recovery target. The bounded recovery chain therefore restores the newest displaced administrator state rather than silently losing it; ambiguous or non-convergent histories fail closed. -Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. The workflow passes its exact protected `main` SHA into the reconciler; live `main` is checked before the final ruleset read, immediately before each PUT, and after convergence/history verification. If protected `main` advances, the stale run fails closed. +Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, after ambiguous-timeout reconciliation, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. The hourly schedule is also queue-safe. Source, manifest, and regression changes are already validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the scheduled validation job is skipped because a credential-free static rerun cannot observe live ruleset drift and only consumes shared Actions capacity. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. @@ -37,13 +39,13 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Verification sequence -1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous initial and recovery PUT timeout handling, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. 3. Run the reconciler and require one exact new ruleset-history version per uncontended target whose predecessor is the sampled pre-write baseline. If an intervening version exists, require recovery to follow immutable predecessor evidence until the newest displaced administrator state is restored or fail closed without claiming success. 4. Re-read both complete live ruleset payloads using the independently authorized credential. 5. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. 6. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. -7. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as Orgmetra #88; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. +7. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as `ContextualWisdomLab/Orgmetra#88`; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. 8. Keep the reconciler enabled for drift repair only if the protected owner-plane environment and history evidence remain available. If the API identity, editable schema, or history contract changes, it fails closed and requires a reviewed source update. ## Standards and research basis From 07df033872fd3de9029431a86b71514da958ad4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:21:32 +0900 Subject: [PATCH 100/154] test(governance): cover timeout recovery edges --- ...t_ruleset_governance_timeout_regression.py | 271 +++++++++++++++++- 1 file changed, 270 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py index 7602b23ef6..cff053cd05 100644 --- a/tests/test_ruleset_governance_timeout_regression.py +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -70,7 +70,7 @@ def live_payload() -> dict: def exercise_ambiguous_put(monkeypatch, *, history_outcome: str) -> tuple[object, list[tuple]]: - """Run one privileged mutation whose PUT times out after an unknown server outcome.""" + """Prepare one privileged mutation whose PUT times out after an unknown server outcome.""" module = load_module() target = repository_target(module) @@ -162,6 +162,275 @@ def test_put_timeout_after_acceptance_with_collision_preserves_recovery_failure( assert any(call[0] == "HISTORY" for call in calls) +def test_ambiguous_put_without_history_guard_is_rejected(monkeypatch) -> None: + """Internal callers cannot turn a timed-out unguarded mutation into a claimed success.""" + + module = load_module() + target = repository_target(module) + desired = module._desired_payload(live_payload(), target) + with pytest.raises(module.RulesetGovernanceError, match="requires protected-main history guard"): + module._confirm_ambiguous_put( + target, + baseline_version=1, + desired=desired, + expected_main_sha=None, + ) + + +def test_reconcile_timeout_without_history_baseline_fails_closed(monkeypatch) -> None: + """A direct unguarded internal mutation still rejects an ambiguous PUT timeout.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + get_count = 0 + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + + def fake_api(method, endpoint, *, body=None): + nonlocal get_count + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + return first + if method == "PUT" and endpoint == target.endpoint: + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint, body)) + + monkeypatch.setattr(module, "_gh_api", fake_api) + with pytest.raises(module.RulesetGovernanceError, match="requires protected-main history guard"): + module._reconcile_target(target, verify_only=False) + assert get_count == 2 + + +def test_transport_timeout_distinguishes_reads_from_ambiguous_puts(monkeypatch) -> None: + """Read timeouts become redacted domain errors while PUT timeouts remain distinguishable.""" + + module = load_module() + + def timeout_run(*_args, **_kwargs): + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + + monkeypatch.setattr(subprocess, "run", timeout_run) + with pytest.raises(module.RulesetGovernanceError, match="request timed out"): + module._run_gh_json("GET", "repos/ContextualWisdomLab/.github/rulesets/17921150") + with pytest.raises(subprocess.TimeoutExpired): + module._run_gh_json( + "PUT", + "repos/ContextualWisdomLab/.github/rulesets/17921150", + body={"name": "Lock default branch"}, + ) + + +def test_recovery_timeout_without_history_fails_closed(monkeypatch) -> None: + """An ambiguous recovery timeout cannot proceed when immutable history is unavailable.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Admin predecessor"} + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr( + module, + "_gh_api", + lambda method, endpoint, **_kwargs: current + if method == "GET" + else (_ for _ in ()).throw(subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30)), + ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: []) + + with pytest.raises(module.RulesetGovernanceError, match="exposed no history"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_recovery_timeout_before_acceptance_retries_boundedly(monkeypatch) -> None: + """A recovery PUT absent from history is retried only within the bounded recovery loop.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Admin predecessor"} + put_count = 0 + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def fake_api(method, endpoint, **_kwargs): + nonlocal put_count + if method == "GET": + return current + if method == "PUT": + put_count += 1 + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 10}]) + + with pytest.raises(module.RulesetGovernanceError, match="exceeded bounded attempts"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + assert put_count == module.COLLISION_RECOVERY_LIMIT + + +def test_recovery_timeout_with_new_version_requires_predecessor(monkeypatch) -> None: + """A visible timed-out recovery write still needs its immutable predecessor proof.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Admin predecessor"} + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr( + module, + "_gh_api", + lambda method, endpoint, **_kwargs: current + if method == "GET" + else (_ for _ in ()).throw(subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30)), + ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 11}]) + + with pytest.raises(module.RulesetGovernanceError, match="exposed no predecessor"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_recovery_timeout_refuses_unexpected_newer_history_state(monkeypatch) -> None: + """A timed-out recovery never overwrites a newer state that is not its intended restore body.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Admin predecessor"} + unrelated = {**current, "name": "Newer administrator state"} + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def history_state(_target, version_id): + return unrelated if version_id == 11 else displaced + + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr( + module, + "_gh_api", + lambda method, endpoint, **_kwargs: current + if method == "GET" + else (_ for _ in ()).throw(subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30)), + ) + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args: [{"version_id": 11}, {"version_id": 10}], + ) + + with pytest.raises(module.RulesetGovernanceError, match="left a newer state"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_recovery_timeout_after_acceptance_without_collision_converges(monkeypatch) -> None: + """A timed-out recovery accepted by GitHub completes only with matching history and live state.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Admin predecessor", "enforcement": "evaluate"} + get_count = 0 + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + + def fake_api(method, endpoint, **_kwargs): + nonlocal get_count + if method == "GET": + get_count += 1 + return current if get_count == 1 else displaced + if method == "PUT": + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args: [{"version_id": 11}, {"version_id": 10}], + ) + + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_recovery_timeout_after_acceptance_with_collision_continues_chain(monkeypatch) -> None: + """A timed-out restore that displaced another version continues to the newest predecessor.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + first_restore = {**current, "name": "First restore", "enforcement": "evaluate"} + second_restore = {**current, "name": "Newest displaced", "enforcement": "evaluate"} + get_states = iter([current, first_restore, first_restore, second_restore]) + put_count = 0 + list_count = 0 + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def history_state(_target, version_id): + return {9: first_restore, 8: second_restore, 11: first_restore, 12: second_restore}[version_id] + + def fake_api(method, endpoint, **_kwargs): + nonlocal put_count + if method == "GET": + return next(get_states) + if method == "PUT": + put_count += 1 + if put_count == 1: + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + return {} + raise AssertionError((method, endpoint)) + + def fake_history(*_args): + nonlocal list_count + list_count += 1 + if list_count == 1: + return [{"version_id": 11}, {"version_id": 8}] + return [{"version_id": 12}, {"version_id": 11}] + + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_gh_api_list", fake_history) + + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + assert put_count == 2 + + def test_manual_mutation_requires_exact_protected_main_guard(monkeypatch) -> None: """Non-Actions operators cannot invoke the weaker mutation mode without a main SHA guard.""" From 3aaa635bbdc47fad20af3e1861915f6a0c7b2780 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:27:46 +0900 Subject: [PATCH 101/154] test(governance): cover ambiguous failures and runtime budget --- .../test_ruleset_governance_review_round3.py | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 tests/test_ruleset_governance_review_round3.py diff --git a/tests/test_ruleset_governance_review_round3.py b/tests/test_ruleset_governance_review_round3.py new file mode 100644 index 0000000000..9b2bce45b2 --- /dev/null +++ b/tests/test_ruleset_governance_review_round3.py @@ -0,0 +1,208 @@ +"""Adversarial regressions for the third ruleset-governance review round.""" + +from __future__ import annotations + +import importlib.util +import json +import math +import re +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" +WORKFLOW = ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml" + + +def load_module(): + """Load the production reconciler from the exact checkout.""" + + spec = importlib.util.spec_from_file_location("ruleset_governance_round3", SOURCE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def repository_target(module): + """Return the documented repository-owned governance target.""" + + return module.RulesetTarget( + scope="repository", + owner="ContextualWisdomLab", + repository=".github", + ruleset_id=17921150, + name="Lock default branch", + ) + + +def live_payload() -> dict: + """Return one live-shaped repository ruleset with deliberate governance drift.""" + + return { + "id": 17921150, + "name": "Lock default branch", + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": "active", + "bypass_actors": [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash", "rebase"], + }, + }, + ], + } + + +def test_nonzero_put_transport_result_is_ambiguous_but_read_failure_is_not(monkeypatch) -> None: + """A failed PUT transport cannot prove server rejection, while read failures stay ordinary errors.""" + + module = load_module() + monkeypatch.setattr( + subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace( + returncode=1, + stdout="possibly accepted before connection loss", + stderr="transport failed", + ), + ) + + with pytest.raises(module.AmbiguousRulesetWriteError): + module._run_gh_json("PUT", "repos/ContextualWisdomLab/.github/rulesets/17921150", body={"x": 1}) + with pytest.raises(module.RulesetGovernanceError, match="GitHub API request failed"): + module._run_gh_json("GET", "repos/ContextualWisdomLab/.github/rulesets/17921150") + + +def test_delayed_ambiguous_write_is_observed_after_baseline_only_first_poll(monkeypatch) -> None: + """A first baseline-only history observation cannot be treated as definitive rejection.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + history_reads = iter( + [ + [{"version_id": 41}, {"version_id": 40}], + [{"version_id": 42}, {"version_id": 41}], + ] + ) + sleeps: list[float] = [] + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: next(history_reads)) + monkeypatch.setattr(module, "_history_version_state", lambda _target, version: converged if version == 42 else first) + monkeypatch.setattr(module, "_gh_api", lambda method, endpoint, **_kwargs: converged) + monkeypatch.setattr(module.time, "sleep", lambda seconds: sleeps.append(seconds)) + + observed = module._confirm_ambiguous_put( + target, + baseline_version=41, + desired=desired, + expected_main_sha="a" * 40, + ) + assert module._editable_projection(observed) == desired + assert sleeps + + +def test_baseline_only_settlement_exhaustion_fails_as_unresolved_not_rejected(monkeypatch) -> None: + """A bounded settlement horizon may fail closed, but cannot claim a delayed write was rejected.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args: [{"version_id": 41}, {"version_id": 40}], + ) + monkeypatch.setattr(module, "_gh_api", lambda method, endpoint, **_kwargs: first) + monkeypatch.setattr(module.time, "sleep", lambda _seconds: None) + + with pytest.raises(module.RulesetGovernanceError, match="outcome remains unresolved"): + module._confirm_ambiguous_put( + target, + baseline_version=41, + desired=desired, + expected_main_sha="a" * 40, + ) + + +def test_manifest_is_pinned_to_the_two_documented_privileged_targets(tmp_path: Path) -> None: + """Reviewed manifest structure cannot redirect Administration-write authority to another target.""" + + module = load_module() + manifest = { + "schema_version": 1, + "organization": "ContextualWisdomLab", + "targets": [ + { + "scope": "repository", + "owner": "ContextualWisdomLab", + "repository": "another-repository", + "ruleset_id": 99999999, + "name": "Another privileged ruleset", + }, + { + "scope": "organization", + "owner": "ContextualWisdomLab", + "repository": None, + "ruleset_id": 18156473, + "name": "CWL Central required workflows", + }, + ], + } + path = tmp_path / "manifest.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + + with pytest.raises(module.RulesetGovernanceError, match="exact reviewed governance targets"): + module.load_manifest(path) + + +def test_workflow_covers_runtime_auditor_round3_suite_and_disabled_status() -> None: + """Runtime dependencies trigger the focused gate and disabled schedules remain visible cheaply.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + assert workflow.count('"scripts/ci/audit_central_required_workflows.py"') >= 2 + assert workflow.count('"tests/test_ruleset_governance_review_round3.py"') >= 2 + assert "tests/test_ruleset_governance_review_round3.py" in workflow.split("python -m coverage run", 1)[1] + assert "report-disabled:" in workflow + assert "CWL_RULESET_RECONCILE_ENABLED != 'true'" in workflow + assert "Ruleset reconciliation is disabled" in workflow + + +def test_apply_timeout_covers_derived_worst_case_critical_section() -> None: + """Actions cannot terminate the owner-plane process before its derived critical-section budget.""" + + module = load_module() + workflow = WORKFLOW.read_text(encoding="utf-8") + apply_section = workflow.split(" apply:\n", 1)[1] + timeout_match = re.search(r"timeout-minutes:\s*(\d+)", apply_section) + assert timeout_match is not None + configured_minutes = int(timeout_match.group(1)) + required_minutes = math.ceil(module.worst_case_apply_seconds(target_count=2) / 60) + assert configured_minutes >= required_minutes From b70315e33f2bc9669708b8a8ee459eed22ddcd4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:29:33 +0900 Subject: [PATCH 102/154] test(governance): require ambiguous failure confirmation path --- .../test_ruleset_governance_review_round3.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_ruleset_governance_review_round3.py b/tests/test_ruleset_governance_review_round3.py index 9b2bce45b2..e52b68cd9a 100644 --- a/tests/test_ruleset_governance_review_round3.py +++ b/tests/test_ruleset_governance_review_round3.py @@ -94,6 +94,44 @@ def test_nonzero_put_transport_result_is_ambiguous_but_read_failure_is_not(monke module._run_gh_json("GET", "repos/ContextualWisdomLab/.github/rulesets/17921150") +def test_non_timeout_ambiguous_put_routes_through_history_confirmation(monkeypatch) -> None: + """A failed gh PUT result uses the same protected history path as a timeout.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + get_count = 0 + confirmed: list[tuple] = [] + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_latest_history_version", lambda *_args: 41) + + def fake_api(method, endpoint, *, body=None): + nonlocal get_count + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + return first if get_count <= 2 else converged + if method == "PUT": + raise module.AmbiguousRulesetWriteError("ambiguous transport result") + raise AssertionError((method, endpoint, body)) + + def fake_confirm(seen_target, *, baseline_version, desired, expected_main_sha): + confirmed.append((seen_target, baseline_version, desired, expected_main_sha)) + return converged + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_confirm_ambiguous_put", fake_confirm) + assert module._reconcile_target( + target, + verify_only=False, + expected_main_sha="a" * 40, + ) is True + assert confirmed == [(target, 41, desired, "a" * 40)] + + def test_delayed_ambiguous_write_is_observed_after_baseline_only_first_poll(monkeypatch) -> None: """A first baseline-only history observation cannot be treated as definitive rejection.""" From 5d5562eb43777ecd4029b78a5c4c2a042bb3567f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:31:04 +0900 Subject: [PATCH 103/154] fix(governance): settle ambiguous writes fail-closed --- scripts/ci/reconcile_ruleset_governance.py | 191 +++++++++++++++++---- 1 file changed, 153 insertions(+), 38 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index a07fe481a9..bdaa5345a6 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Reconcile reviewed GitHub ruleset governance with fail-closed verification. -The reconciler manages only the two rulesets declared in a reviewed manifest. It +The reconciler manages only the two rulesets pinned by the reviewed manifest. It preserves live conditions and non-governance rules while canonicalizing the reviewed pull-request controls. GitHub does not provide conditional PUT/PATCH semantics for this endpoint, so the second live read is a drift detector rather @@ -10,10 +10,11 @@ immutable ruleset-history surface. If history proves a hidden pre-PUT edit was overwritten, the reconciler restores the newest displaced administrator state before failing; recovery re-checks immutable history and protected-main identity -before every recovery write. Ambiguous mutation timeouts are resolved from live +before every recovery write. Ambiguous mutation results are settled from live state plus immutable history instead of being treated as ordinary request -failures. The canonical audit is executed against projected and live state so -this narrow reconciler never reports broader policy drift as converged. +failures or retried blindly. The canonical audit is executed against projected +and live state so this narrow reconciler never reports broader policy drift as +converged. """ from __future__ import annotations @@ -25,6 +26,7 @@ import re import subprocess import sys +import time from dataclasses import dataclass from pathlib import Path from typing import Any @@ -34,13 +36,41 @@ CONTROL_REPOSITORY = "ContextualWisdomLab/.github" DESIRED_MERGE_METHODS = ["merge", "squash"] GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +API_REQUEST_TIMEOUT_SECONDS = 30 COLLISION_RECOVERY_LIMIT = 8 +AMBIGUOUS_WRITE_SETTLEMENT_POLLS = 3 +AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS = API_REQUEST_TIMEOUT_SECONDS +AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS = ( + AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + / (AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1) +) + +# Conservative blocking-operation budget for one target. The count includes the +# normal guarded mutation path, start/mid/end ambiguous-result observations, the +# full bounded collision-recovery chain, post-confirmation checks, and the final +# workflow verify-only pass. Each blocking API/auditor operation is bounded by +# API_REQUEST_TIMEOUT_SECONDS; settlement sleep spans one additional client +# timeout horizon. The workflow contract test derives its minimum job timeout +# from this function rather than maintaining a second independent estimate. +BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET = 7 +AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET = 12 +RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT = 7 +POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET = 2 +FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET = 2 class RulesetGovernanceError(RuntimeError): """Raised when desired-state validation or live reconciliation is unsafe.""" +class AmbiguousRulesetWriteError(RulesetGovernanceError): + """Raised when a PUT transport result cannot prove server-side rejection.""" + + +class RulesetMutationNotVisibleError(RulesetGovernanceError): + """Raised while an ambiguous PUT has not yet appeared in immutable history.""" + + @dataclass(frozen=True) class RulesetTarget: """Describe one exact organization- or repository-owned ruleset.""" @@ -87,6 +117,43 @@ def source_type(self) -> str: return "Organization" if self.scope == "organization" else "Repository" +EXPECTED_MANIFEST_TARGETS = frozenset( + { + ( + "repository", + ORGANIZATION, + ".github", + 17921150, + "Lock default branch", + ), + ( + "organization", + ORGANIZATION, + None, + 18156473, + "CWL Central required workflows", + ), + } +) + + +def worst_case_apply_seconds(*, target_count: int) -> int: + """Return the conservative critical-section budget for reviewed targets.""" + + blocking_operations = ( + BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET + + AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET + + COLLISION_RECOVERY_LIMIT * RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT + + POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET + + FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET + ) + per_target_seconds = ( + blocking_operations * API_REQUEST_TIMEOUT_SECONDS + + AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + ) + return target_count * per_target_seconds + + def _plain_dict(value: Any, *, field: str) -> dict[str, Any]: """Return a plain dictionary or reject behavior-bearing mapping objects.""" @@ -104,7 +171,7 @@ def _plain_list(value: Any, *, field: str) -> list[Any]: def load_manifest(path: Path) -> tuple[RulesetTarget, ...]: - """Load and strictly validate the reviewed ruleset target manifest.""" + """Load and strictly validate the two reviewed privileged ruleset targets.""" root = _plain_dict(json.loads(path.read_text(encoding="utf-8")), field="manifest") if set(root) != {"schema_version", "organization", "targets"}: @@ -145,6 +212,20 @@ def load_manifest(path: Path) -> tuple[RulesetTarget, ...]: if {target.scope for target in targets} != {"organization", "repository"}: raise RulesetGovernanceError("manifest must contain one target per supported scope") + actual_targets = frozenset( + ( + target.scope, + target.owner, + target.repository, + target.ruleset_id, + target.name, + ) + for target in targets + ) + if actual_targets != EXPECTED_MANIFEST_TARGETS: + raise RulesetGovernanceError( + "manifest must contain the exact reviewed governance targets" + ) return tuple(targets) @@ -180,7 +261,7 @@ def _run_gh_json( input=None if body is None else json.dumps(body, separators=(",", ":")), capture_output=True, text=True, - timeout=30, + timeout=API_REQUEST_TIMEOUT_SECONDS, ) except subprocess.TimeoutExpired as exc: if method == "PUT": @@ -189,8 +270,21 @@ def _run_gh_json( f"GitHub API request timed out for {endpoint}" ) from exc if completed.returncode != 0: + if method == "PUT": + raise AmbiguousRulesetWriteError( + f"GitHub PUT outcome is ambiguous for {endpoint}" + ) raise RulesetGovernanceError(f"GitHub API request failed for {endpoint}") - return json.loads(completed.stdout) + try: + return json.loads(completed.stdout) + except json.JSONDecodeError as exc: + if method == "PUT": + raise AmbiguousRulesetWriteError( + f"GitHub PUT response is ambiguous for {endpoint}" + ) from exc + raise RulesetGovernanceError( + f"GitHub API returned invalid JSON for {endpoint}" + ) from exc def _gh_api( @@ -330,7 +424,7 @@ def _canonical_governance_errors( input=json.dumps(live, separators=(",", ":")), capture_output=True, text=True, - timeout=30, + timeout=API_REQUEST_TIMEOUT_SECONDS, ) return [] if completed.returncode == 0 else [completed.stderr.strip()] @@ -404,7 +498,7 @@ def _recover_displaced_history_state( becomes the next recovery target. A newer live state observed before a recovery write is never overwritten. Privileged recovery revalidates the reviewed protected-main SHA immediately before every PUT. Ambiguous recovery - timeouts inspect immutable history before retrying or advancing the recovery + results inspect immutable history before retrying or advancing the recovery chain. The loop is bounded and fails closed. """ @@ -422,24 +516,24 @@ def _recover_displaced_history_state( try: _gh_api("PUT", target.endpoint, body=displaced_payload) - except subprocess.TimeoutExpired: + except (AmbiguousRulesetWriteError, subprocess.TimeoutExpired): history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") if not history: raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT timeout exposed no history" + "ambiguous ruleset recovery PUT exposed no history" ) recovery_version = _history_version_id(history[0]) if recovery_version == current_version: continue if len(history) < 2: raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT timeout exposed no predecessor" + "ambiguous ruleset recovery PUT exposed no predecessor" ) recovery_predecessor = _history_version_id(history[1]) recovery_state = _history_version_state(target, recovery_version) if _editable_projection(recovery_state) != displaced_payload: raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT timeout left a newer state; refusing overwrite" + "ambiguous ruleset recovery PUT left a newer state; refusing overwrite" ) else: history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") @@ -494,7 +588,7 @@ def _verify_ruleset_history_transition( newest_id = _history_version_id(history[0]) predecessor_id = _history_version_id(history[1]) if newest_id == baseline_version: - raise RulesetGovernanceError("ruleset mutation is not visible in history") + raise RulesetMutationNotVisibleError("ruleset mutation is not visible in history") newest_state = _history_version_state(target, newest_id) if _editable_projection(newest_state) != desired: @@ -521,34 +615,55 @@ def _confirm_ambiguous_put( desired: dict[str, Any], expected_main_sha: str | None, ) -> dict[str, Any]: - """Resolve a timed-out PUT from immutable history and exact live convergence. - - A transport timeout is ambiguous because GitHub may have committed the - mutation. The protected-main and history guards therefore decide whether the - write was absent, committed exactly, or displaced an intervening version. - No mutation retry occurs here. + """Settle an ambiguous PUT from immutable history and exact live convergence. + + A timeout, connection loss, nonzero transport result, or malformed successful + response can occur after GitHub accepted the update. Three observations span + one additional full client timeout horizon: start, midpoint, and end. A + baseline-only first observation is therefore never represented as rejection. + Acceptance must become visible in immutable history and live state; collision + recovery follows the same predecessor contract. If no decisive transition is + visible by the end of the bounded window, the run fails as unresolved and + never retries the desired mutation blindly. """ if expected_main_sha is None: raise RulesetGovernanceError( - "ambiguous ruleset PUT timeout requires protected-main history guard" + "ambiguous ruleset PUT requires protected-main history guard" ) - _assert_current_main(expected_main_sha) - _verify_ruleset_history_transition( - target, - baseline_version, - desired, - expected_main_sha=expected_main_sha, + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + _assert_current_main(expected_main_sha) + try: + _verify_ruleset_history_transition( + target, + baseline_version, + desired, + expected_main_sha=expected_main_sha, + ) + except RulesetMutationNotVisibleError: + live = _gh_api("GET", target.endpoint) + _assert_target_provenance(live, target) + if poll_index == AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + raise RulesetGovernanceError( + "ambiguous ruleset PUT outcome remains unresolved after settlement window" + ) + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + continue + + after = _gh_api("GET", target.endpoint) + _assert_identity(after, target) + if _editable_projection(after) != desired: + raise RulesetGovernanceError( + f"{target.scope} ambiguous ruleset mutation did not converge" + ) + _assert_canonical_governance(after, target) + _assert_current_main(expected_main_sha) + return after + + raise RulesetGovernanceError( + "ambiguous ruleset PUT outcome remains unresolved after settlement window" ) - after = _gh_api("GET", target.endpoint) - _assert_identity(after, target) - if _editable_projection(after) != desired: - raise RulesetGovernanceError( - f"{target.scope} timed-out ruleset mutation did not converge" - ) - _assert_canonical_governance(after, target) - _assert_current_main(expected_main_sha) - return after def _reconcile_target( @@ -587,10 +702,10 @@ def _reconcile_target( history_verified = False try: _gh_api("PUT", target.endpoint, body=desired) - except subprocess.TimeoutExpired: + except (AmbiguousRulesetWriteError, subprocess.TimeoutExpired): if baseline_version is None: raise RulesetGovernanceError( - "ambiguous ruleset PUT timeout requires protected-main history guard" + "ambiguous ruleset PUT requires protected-main history guard" ) after = _confirm_ambiguous_put( target, From f1c190a0aee0c4717bdc157b5624f5ff2903af8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:31:29 +0900 Subject: [PATCH 104/154] ci(governance): budget and observe owner-plane recovery --- .../ruleset-governance-reconcile.yml | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index c0c99a7ec3..a1fb6e46ba 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -4,10 +4,12 @@ on: pull_request: paths: - "config/ruleset-governance.json" + - "scripts/ci/audit_central_required_workflows.py" - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" @@ -20,10 +22,12 @@ on: - main paths: - "config/ruleset-governance.json" + - "scripts/ci/audit_central_required_workflows.py" - "scripts/ci/reconcile_ruleset_governance.py" - "tests/test_ruleset_governance_reconciliation.py" - "tests/test_ruleset_governance_review_regressions.py" - "tests/test_ruleset_governance_review_round2.py" + - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" @@ -46,10 +50,21 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + report-disabled: + if: >- + github.event_name == 'schedule' && + vars.CWL_RULESET_RECONCILE_ENABLED != 'true' + runs-on: ubuntu-slim + timeout-minutes: 2 + steps: + - name: Report disabled owner-plane state + shell: bash + run: >- + echo "Ruleset reconciliation is disabled; provision the protected owner-plane credential and enable variable before live mutation." >> "$GITHUB_STEP_SUMMARY" + validate: # Source/regression drift is already validated on PR, push, and manual runs. - # When privileged reconciliation is deliberately disabled, an hourly static - # rerun cannot observe live ruleset drift and only consumes shared capacity. + # A disabled schedule emits only the cheap report-disabled signal above. if: >- github.event_name != 'schedule' || vars.CWL_RULESET_RECONCILE_ENABLED == 'true' @@ -93,6 +108,7 @@ jobs: tests/test_ruleset_governance_reconciliation.py \ tests/test_ruleset_governance_review_regressions.py \ tests/test_ruleset_governance_review_round2.py \ + tests/test_ruleset_governance_review_round3.py \ tests/test_ruleset_governance_timeout_regression.py \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_ruleset_audit_completeness_regression.py \ @@ -114,7 +130,7 @@ jobs: vars.CWL_RULESET_RECONCILE_ENABLED == 'true' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 90 environment: ruleset-governance-maintenance steps: - name: Harden runner From ae7d98e9fc715914537c408186c81b223a7ede23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:32:10 +0900 Subject: [PATCH 105/154] docs(governance): close third-round owner-plane findings --- .../ruleset-owner-plane-reconciliation.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index f7d4d9f172..d11484b36a 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -10,7 +10,7 @@ The application connector can read those settings but does not expose ruleset mu ## Design decision -`config/ruleset-governance.json` binds the reconciler to exactly two existing ruleset identities. `scripts/ci/reconcile_ruleset_governance.py` reads the full live object and refuses to act if the ID, name, source, source type, branch target, or active enforcement state has changed. It then preserves all unrelated conditions and rules while changing only these reviewed governance fields: +`config/ruleset-governance.json` is not merely shape-validated. Production code pins the privileged manifest to exactly repository ruleset `17921150` (`ContextualWisdomLab/.github`, `Lock default branch`) and organization ruleset `18156473` (`ContextualWisdomLab`, `CWL Central required workflows`). A structurally valid manifest cannot redirect Administration-write authority to another positive ruleset ID, repository, or name. `scripts/ci/reconcile_ruleset_governance.py` then reads the full live object and refuses to act if the ID, name, source, source type, branch target, or active enforcement state has changed. It preserves all unrelated conditions and rules while changing only these reviewed governance fields: - remove routine bypass actors; - set generic approving-review count to zero; @@ -21,17 +21,21 @@ The application connector can read those settings but does not expose ruleset mu GitHub's current REST contract does not support conditional unsafe REST updates such as `If-Match` on ruleset `PUT`; GitHub's general REST guidance explicitly says conditional unsafe methods are unsupported unless a specific endpoint documents otherwise, and the repository/organization ruleset update endpoints document no such precondition. The second full ruleset read is therefore a drift detector, **not** compare-and-swap. It catches edits visible before the final read but cannot make the final GET-to-PUT interval atomic. A successful HTTP update is also not completion: the reconciler performs a post-write full read and requires the complete editable payload to equal the reviewed update body. -A ruleset `PUT` transport timeout is treated as an ambiguous commit result, not as an ordinary failed request. GitHub may have accepted the mutation before the client-side timeout. The privileged path therefore enters the same immutable-history verification contract after a timed-out initial write: a baseline still at the newest history version proves no visible acceptance and fails closed without retrying the desired mutation; a newest version equal to the reviewed body with the sampled baseline as predecessor proves exact acceptance; an intervening predecessor triggers the existing displaced-administrator recovery chain and still fails closed after recovery. Recovery `PUT` timeouts are likewise checked against immutable history before a retry or progression in the bounded recovery chain, so a timeout cannot silently strand an overwritten administrator state. +Any `PUT` result that does not prove server-side rejection is treated as an ambiguous commit outcome. This includes the existing subprocess timeout path, a nonzero `gh` transport result such as connection loss after request transmission, and a malformed response after an otherwise successful invocation. Reads remain ordinary fail-closed errors because they cannot have changed server state. The desired mutation is never blindly retried after an ambiguous result. -The same GitHub API exposes immutable organization and repository **ruleset history** plus exact version-state reads to Administration-authorized callers. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules). Current live mutation still requires the reviewed name and active enforcement, but collision recovery deliberately accepts historical names or enforcement values that a legitimate administrator may have changed. +Ambiguous initial writes enter a bounded settlement protocol tied to the same 30-second client timeout used by GitHub REST calls. Immutable history and live state are observed at the start, midpoint, and end of one additional full client-timeout horizon. A first baseline-only history observation is explicitly **not** represented as rejection because a server-side commit may still become visible after the client has timed out. If the reviewed version appears with the sampled baseline as predecessor, exact acceptance is proven; if an intervening predecessor appears, the displaced-administrator recovery contract runs; if the transition remains baseline-only through the settlement horizon, the run fails as **unresolved** rather than claiming the write was rejected. This is a conservative terminal failure, not authorization to retry or report convergence. + +Recovery writes use the same ambiguity principle. Immutable history is checked after an ambiguous recovery result before any retry or progression. A baseline that has not advanced permits only the next bounded recovery-loop iteration; a visible restore version must match the exact intended predecessor payload and expose its predecessor; an unexpected newer state is preserved and causes failure. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules), so a legitimate administrator rename or enforcement change can be restored rather than rejected as foreign history. The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. Recovery re-reads live state before every recovery PUT and refuses to overwrite it if it has already advanced. After each recovery PUT, immutable history is read again: the newest version must equal the restore body, and its immediate predecessor must be the version that recovery intended to replace. If a second administrator version slipped between the recovery GET and PUT, that newly displaced predecessor becomes the next recovery target. The bounded recovery chain therefore restores the newest displaced administrator state rather than silently losing it; ambiguous or non-convergent histories fail closed. -Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, after ambiguous-timeout reconciliation, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. +Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, during ambiguous-result settlement, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. + +The Actions timeout is derived from the implementation rather than chosen independently. For each target the source budgets the normal guarded mutation operations, three ambiguous-settlement observations, all eight seven-operation collision-recovery attempts, post-confirmation checks, the final verify-only pass, and one additional 30-second settlement window. At two reviewed targets this conservative source-derived budget is 4,800 seconds (80 minutes). The privileged `apply` job reserves 90 minutes, so Actions cannot reach its configured timeout before that modeled worst-case critical section. The regression suite computes the required minimum directly from `worst_case_apply_seconds(target_count=2)` and compares it with the workflow value. -The hourly schedule is also queue-safe. Source, manifest, and regression changes are already validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the scheduled validation job is skipped because a credential-free static rerun cannot observe live ruleset drift and only consumes shared Actions capacity. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. +The hourly schedule remains queue-conscious while making disabled state visible. Source, manifest, runtime-auditor, and regression changes are validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the expensive validation/apply path is skipped, but a two-minute `ubuntu-slim` status job writes `Ruleset reconciliation is disabled` to the workflow summary so operators can distinguish deliberate disablement from a missing run. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. -Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. +Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. The focused workflow now includes `scripts/ci/audit_central_required_workflows.py` itself in both PR and protected-main path filters because that auditor executes inside the reconciliation process; auditor-only changes therefore cannot bypass the owner-plane regression gate. Pull requests execute only offline manifest validation plus 100% statement/branch/docstring contract tests. Mutation can run only from trusted `main`, in the `ruleset-governance-maintenance` protected environment, when repository variable `CWL_RULESET_RECONCILE_ENABLED=true` is explicitly set, using dedicated secret `CWL_RULESET_ADMIN_TOKEN`. The normal workflow token remains `contents: read`, checkout credentials are not persisted, and API errors never echo subprocess output that could contain sensitive context. @@ -39,14 +43,14 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Verification sequence -1. PR validation proves manifest, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous initial and recovery PUT timeout handling, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves exact privileged-target pinning, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous timeout and non-timeout PUT settlement, delayed history visibility, bounded unresolved settlement, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, runtime-auditor path coverage, derived job-timeout coverage, visible disabled-state reporting, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. 3. Run the reconciler and require one exact new ruleset-history version per uncontended target whose predecessor is the sampled pre-write baseline. If an intervening version exists, require recovery to follow immutable predecessor evidence until the newest displaced administrator state is restored or fail closed without claiming success. 4. Re-read both complete live ruleset payloads using the independently authorized credential. 5. Require organization ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged required workflows/conditions/deletion/non-fast-forward/thread-resolution controls. 6. Require repository ruleset approval count 0, last-push false, code-owner false, empty required reviewers, merge/squash only, no routine bypass, unchanged deletion/non-fast-forward/thread-resolution controls. -7. Revalidate the canonical audit writer and an unchanged downstream deterministic-GREEN canary such as `ContextualWisdomLab/Orgmetra#88`; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. -8. Keep the reconciler enabled for drift repair only if the protected owner-plane environment and history evidence remain available. If the API identity, editable schema, or history contract changes, it fails closed and requires a reviewed source update. +7. Revalidate the canonical audit writer and unchanged downstream deterministic-GREEN `ContextualWisdomLab/Orgmetra#88`; ordinary protected merge must work without self-approval, synthetic approval, or administrator bypass. +8. Keep the reconciler enabled for drift repair only if the protected owner-plane environment and history evidence remain available. If the API identity, editable schema, history contract, or modeled runtime boundary changes, it fails closed and requires a reviewed source update. ## Standards and research basis From 25fa97d76c76b69fc43fac16b52fdd2620218508 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:43:57 +0900 Subject: [PATCH 106/154] test(governance): include code-owner false in merge-method fixtures --- tests/test_ruleset_merge_method_shape_regression.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ruleset_merge_method_shape_regression.py b/tests/test_ruleset_merge_method_shape_regression.py index ac0d61fa70..02dd3d52b1 100644 --- a/tests/test_ruleset_merge_method_shape_regression.py +++ b/tests/test_ruleset_merge_method_shape_regression.py @@ -13,6 +13,7 @@ def _review_parameters() -> dict[str, object]: return { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": True, + "require_code_owner_review": False, "require_last_push_approval": False, "required_review_thread_resolution": True, "required_reviewers": [], From 92049abdc10e6af98912aee77f4ea0af0e73b42d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:44:29 +0900 Subject: [PATCH 107/154] test(governance): repair Python 3.12 loader and workflow parsing --- tests/test_ruleset_governance_review_round2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 91b3c914ce..936a1e5b3a 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import sys from pathlib import Path import pytest @@ -18,6 +19,7 @@ def load_module(): spec = importlib.util.spec_from_file_location("ruleset_governance_round2", SOURCE) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module spec.loader.exec_module(module) return module @@ -123,7 +125,7 @@ def test_owner_plane_is_serial_and_disabled_schedule_does_not_consume_runner() - """Mutation is non-cancellable while disabled hourly validation skips shared capacity.""" text = WORKFLOW.read_text(encoding="utf-8") assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in text - validate_block = text.split("jobs:\n validate:\n", 1)[1].split("\n apply:\n", 1)[0] + validate_block = text.split("\n validate:\n", 1)[1].split("\n apply:\n", 1)[0] assert "github.event_name != 'schedule'" in validate_block assert "vars.CWL_RULESET_RECONCILE_ENABLED == 'true'" in validate_block assert "runs-on: ubuntu-slim" in validate_block From efe18ae4e4360d4ae90c04aa000101390829f2c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:45:57 +0900 Subject: [PATCH 108/154] test(governance): align history fixtures with editable PUT contract --- ...t_ruleset_governance_review_regressions.py | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index f995ce1170..cd933a1459 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -75,6 +75,12 @@ def _converged() -> dict[str, object]: return {**live, **module._desired_payload(live, _target())} +def _desired() -> dict[str, object]: + """Return exactly the editable payload submitted to GitHub PUT.""" + + return module._desired_payload(_live(), _target()) + + def _manifest(tmp_path: Path) -> Path: """Write the exact two-target manifest accepted by the production parser.""" @@ -237,13 +243,13 @@ def test_history_transition_accepts_exactly_one_new_reviewed_version(monkeypatch lambda *_args, **_kwargs: [{"version_id": 8}, {"version_id": 7}], ) monkeypatch.setattr(module, "_history_version_state", lambda _target, version: _converged()) - module._verify_ruleset_history_transition(_target(), 7, _converged()) + module._verify_ruleset_history_transition(_target(), 7, _desired()) def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypatch) -> None: """Missing predecessor, absent version advance, or mismatched latest state fail closed.""" - desired = _converged() + desired = _desired() monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: [{"version_id": 8}]) with pytest.raises(module.RulesetGovernanceError, match="did not expose a predecessor"): module._verify_ruleset_history_transition(_target(), 7, desired) @@ -269,7 +275,8 @@ def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypa def test_history_collision_restores_immediate_predecessor_before_failing(monkeypatch) -> None: """A hidden pre-PUT administrator edit is restored and history-proven before failing.""" - desired = _converged() + desired_state = _converged() + desired = _desired() external = _live() external["conditions"] = {"ref_name": {"include": ["refs/heads/reviewed"], "exclude": []}} histories = iter( @@ -282,10 +289,10 @@ def test_history_collision_restores_immediate_predecessor_before_failing(monkeyp monkeypatch.setattr( module, "_history_version_state", - lambda _target, version: desired if version == 10 else external, + lambda _target, version: desired_state if version == 10 else external, ) calls: list[tuple[str, object | None]] = [] - replies = iter([desired, {}, external]) + replies = iter([desired_state, {}, external]) def fake_api(method, endpoint, *, body=None): calls.append((method, body)) @@ -301,7 +308,8 @@ def fake_api(method, endpoint, *, body=None): def test_history_collision_does_not_overwrite_a_newer_post_put_admin_edit(monkeypatch) -> None: """If live state advanced again after our PUT, collision recovery preserves that newer state.""" - desired = _converged() + desired_state = _converged() + desired = _desired() external = _live() newer = _live() newer["conditions"] = {"ref_name": {"include": ["refs/heads/newer"], "exclude": []}} @@ -313,7 +321,7 @@ def test_history_collision_does_not_overwrite_a_newer_post_put_admin_edit(monkey monkeypatch.setattr( module, "_history_version_state", - lambda _target, version: desired if version == 10 else external, + lambda _target, version: desired_state if version == 10 else external, ) monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: newer) with pytest.raises(module.RulesetGovernanceError, match="advanced again"): @@ -323,7 +331,8 @@ def test_history_collision_does_not_overwrite_a_newer_post_put_admin_edit(monkey def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: """A failed predecessor restore is surfaced rather than treated as collision recovery.""" - desired = _converged() + desired_state = _converged() + desired = _desired() external = _live() external["conditions"] = {"ref_name": {"include": ["refs/heads/external"], "exclude": []}} histories = iter( @@ -336,9 +345,9 @@ def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: monkeypatch.setattr( module, "_history_version_state", - lambda _target, version: desired if version == 10 else external, + lambda _target, version: desired_state if version == 10 else external, ) - replies = iter([desired, {}, desired]) + replies = iter([desired_state, {}, desired_state]) monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: next(replies)) with pytest.raises(module.RulesetGovernanceError, match="rollback did not converge"): module._verify_ruleset_history_transition(_target(), 7, desired) @@ -347,7 +356,8 @@ def test_history_collision_requires_rollback_convergence(monkeypatch) -> None: def test_history_collision_recovers_admin_write_between_recovery_get_and_put(monkeypatch) -> None: """A second administrator version displaced by rollback becomes the next restore target.""" - desired = _converged() + desired_state = _converged() + desired = _desired() first_admin = _live() first_admin["conditions"] = {"ref_name": {"include": ["refs/heads/first-admin"], "exclude": []}} second_admin = _live() @@ -363,7 +373,7 @@ def test_history_collision_recovers_admin_write_between_recovery_get_and_put(mon states = { 9: first_admin, - 10: desired, + 10: desired_state, 11: second_admin, 12: first_admin, 13: second_admin, @@ -371,7 +381,7 @@ def test_history_collision_recovers_admin_write_between_recovery_get_and_put(mon monkeypatch.setattr(module, "_history_version_state", lambda _target, version: states[version]) calls: list[tuple[str, object | None]] = [] - replies = iter([desired, {}, first_admin, first_admin, {}, second_admin]) + replies = iter([desired_state, {}, first_admin, first_admin, {}, second_admin]) def fake_api(method, endpoint, *, body=None): calls.append((method, body)) @@ -442,8 +452,8 @@ def test_docs_state_github_has_no_ruleset_put_compare_and_swap() -> None: text = DOCTORING.read_text(encoding="utf-8") assert "does not support conditional unsafe REST updates" in text - assert "ruleset history" in text - assert "restores the immediate pre-write version" in text + assert "ruleset-history" in text + assert "restores the newest displaced administrator state" in text assert "cannot make the final GET-to-PUT interval atomic" in text From 0494f5ae7ceab4ecc3a4a11144bef9efcfeb355f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:48:34 +0900 Subject: [PATCH 109/154] test(governance): expect code-owner drift on malformed review settings --- tests/test_central_required_workflow_ruleset_audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 87e3d63684..25764bd65f 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -297,6 +297,7 @@ def test_repository_ruleset_rejects_malformed_review_parameters() -> None: assert audit.audit_repository_ruleset(payload) == [ "repository solo-maintainer ruleset must not require approving reviews", + "repository solo-maintainer ruleset must not require code-owner review", "repository ruleset stale-review dismissal on push is disabled", "repository solo-maintainer ruleset must not require last-push approval", "repository ruleset review-thread resolution protection is disabled", From e639e6159268450bebfa9ea449df5aa2f50112bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:51:55 +0900 Subject: [PATCH 110/154] test(governance): project live state before recovery guard --- tests/test_ruleset_governance_review_round2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 936a1e5b3a..6903c7c4c6 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -150,7 +150,8 @@ def test_recovery_revalidates_protected_main_before_every_recovery_put(monkeypat """A stale run must stop before a recovery PUT after protected main advances.""" module = load_module() target = repository_target(module) - current_payload = repository_payload() + current_state = repository_payload() + current_payload = module._editable_projection(current_state) displaced_payload = historical_state() puts: list[dict] = [] main_checks: list[str] = [] @@ -159,7 +160,7 @@ def test_recovery_revalidates_protected_main_before_every_recovery_put(monkeypat def fake_api(method, endpoint, *, body=None): if method == "GET" and endpoint == target.endpoint: - return current_payload + return current_state if method == "PUT" and endpoint == target.endpoint: puts.append(body) return {} From 71bdea26b0b8403baa4820d72e8cd9f0e7adc935 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:55:49 +0900 Subject: [PATCH 111/154] test(governance): cover fail-closed owner-plane edges --- .../test_ruleset_governance_review_round2.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 6903c7c4c6..11d7ef0444 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import subprocess import sys from pathlib import Path @@ -186,6 +187,102 @@ def stale_main(expected_sha: str) -> None: assert puts == [] +def test_invalid_json_transport_is_typed_for_read_and_ambiguous_put(monkeypatch) -> None: + """Malformed GitHub JSON is a read error but an ambiguous mutation outcome for PUT.""" + module = load_module() + + def invalid_json(*_args, **_kwargs): + return subprocess.CompletedProcess(args=["gh", "api"], returncode=0, stdout="{", stderr="") + + monkeypatch.setattr(module.subprocess, "run", invalid_json) + with pytest.raises(module.RulesetGovernanceError, match="invalid JSON"): + module._run_gh_json("GET", "repos/ContextualWisdomLab/.github/rulesets/17921150") + with pytest.raises(module.AmbiguousRulesetWriteError, match="response is ambiguous"): + module._run_gh_json( + "PUT", + "repos/ContextualWisdomLab/.github/rulesets/17921150", + body={"name": "Lock default branch"}, + ) + + +def test_successful_recovery_requires_history_predecessor(monkeypatch) -> None: + """A successful recovery PUT is not trusted without an immutable predecessor record.""" + module = load_module() + target = repository_target(module) + current = repository_payload() + displaced = historical_state() + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr( + module, + "_gh_api", + lambda method, _endpoint, **_kwargs: current if method == "GET" else {}, + ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 11}]) + + with pytest.raises(module.RulesetGovernanceError, match="history did not expose a predecessor"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_successful_recovery_rejects_mismatched_latest_history_state(monkeypatch) -> None: + """A recovery PUT fails closed when immutable history records a different newest state.""" + module = load_module() + target = repository_target(module) + current = repository_payload() + displaced = historical_state() + unrelated = historical_state(name="Concurrent administrator state") + + def history_state(_target, version_id): + return unrelated if version_id == 11 else displaced + + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr( + module, + "_gh_api", + lambda method, _endpoint, **_kwargs: current if method == "GET" else {}, + ) + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args: [{"version_id": 11}, {"version_id": 10}], + ) + + with pytest.raises(module.RulesetGovernanceError, match="latest history does not match restore write"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + +def test_ambiguous_put_rejects_history_success_without_live_convergence(monkeypatch) -> None: + """History acceptance alone cannot promote an ambiguous PUT whose live state still differs.""" + module = load_module() + target = repository_target(module) + live = repository_payload() + desired = module._desired_payload(live, target) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_verify_ruleset_history_transition", lambda *_args, **_kwargs: None) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: live) + + with pytest.raises(module.RulesetGovernanceError, match="ambiguous ruleset mutation did not converge"): + module._confirm_ambiguous_put( + target, + baseline_version=10, + desired=desired, + expected_main_sha="a" * 40, + ) + + def test_verify_only_rejects_drift_outside_reconciler_projection(monkeypatch) -> None: """Canonical audit drift cannot be reported as converged merely because review fields match.""" module = load_module() From 7508c3921c67343aef676b21cded757e2aea7335 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:56:50 +0900 Subject: [PATCH 112/154] test(governance): cover empty settlement fail-closed path --- tests/test_ruleset_governance_review_round2.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 11d7ef0444..cb2c1a41d8 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -283,6 +283,22 @@ def test_ambiguous_put_rejects_history_success_without_live_convergence(monkeypa ) +def test_ambiguous_put_zero_poll_configuration_still_fails_closed(monkeypatch) -> None: + """A misconfigured empty settlement loop must fail closed rather than imply success.""" + module = load_module() + target = repository_target(module) + desired = module._desired_payload(repository_payload(), target) + monkeypatch.setattr(module, "AMBIGUOUS_WRITE_SETTLEMENT_POLLS", 0) + + with pytest.raises(module.RulesetGovernanceError, match="remains unresolved"): + module._confirm_ambiguous_put( + target, + baseline_version=10, + desired=desired, + expected_main_sha="a" * 40, + ) + + def test_verify_only_rejects_drift_outside_reconciler_projection(monkeypatch) -> None: """Canonical audit drift cannot be reported as converged merely because review fields match.""" module = load_module() From b6c64db28edbc997ffaa12aeff9acf2784298cff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:01:13 +0900 Subject: [PATCH 113/154] test(governance): make ambiguous live state genuinely drift --- tests/test_ruleset_governance_review_round2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index cb2c1a41d8..2510021a70 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -269,6 +269,9 @@ def test_ambiguous_put_rejects_history_success_without_live_convergence(monkeypa module = load_module() target = repository_target(module) live = repository_payload() + live["bypass_actors"] = [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ] desired = module._desired_payload(live, target) monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) monkeypatch.setattr(module, "_verify_ruleset_history_transition", lambda *_args, **_kwargs: None) @@ -325,4 +328,4 @@ def test_focused_workflow_runs_every_permanent_governance_regression_suite() -> test_command = text.split("-m pytest -q \\\n", 1)[1].split("\n python -m coverage report", 1)[0] for path in expected: assert text.count(f'- "{path}"') >= 2 - assert path in test_command + assert path in test_command \ No newline at end of file From 40fe4995d3b3fea2784863305fbe782c441c2c09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:22:41 +0900 Subject: [PATCH 114/154] fix(governance): project required review safety controls --- scripts/ci/reconcile_ruleset_governance.py | 4 ++++ tests/test_ruleset_governance_review_round2.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index bdaa5345a6..3bba7fb2bf 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -462,6 +462,8 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A "required_approving_review_count": int, "require_code_owner_review": bool, "require_last_push_approval": bool, + "dismiss_stale_reviews_on_push": bool, + "required_review_thread_resolution": bool, "allowed_merge_methods": list, } for field, expected_type in required_parameters.items(): @@ -478,6 +480,8 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A parameters["require_code_owner_review"] = False parameters["require_last_push_approval"] = False parameters["required_reviewers"] = [] + parameters["dismiss_stale_reviews_on_push"] = True + parameters["required_review_thread_resolution"] = True parameters["allowed_merge_methods"] = list(DESIRED_MERGE_METHODS) return desired diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 2510021a70..2b8a66e50f 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -147,6 +147,21 @@ def test_missing_empty_required_reviewers_is_normalized_to_declared_empty_list() assert desired["rules"][-1]["parameters"]["required_reviewers"] == [] +def test_desired_payload_forces_stale_review_and_thread_resolution_guards() -> None: + """Live review-safety drift is repaired instead of copied into the update payload.""" + module = load_module() + live = repository_payload() + parameters = live["rules"][-1]["parameters"] + parameters["dismiss_stale_reviews_on_push"] = False + parameters["required_review_thread_resolution"] = False + + desired = module._desired_payload(live, repository_target(module)) + desired_parameters = desired["rules"][-1]["parameters"] + + assert desired_parameters["dismiss_stale_reviews_on_push"] is True + assert desired_parameters["required_review_thread_resolution"] is True + + def test_recovery_revalidates_protected_main_before_every_recovery_put(monkeypatch) -> None: """A stale run must stop before a recovery PUT after protected main advances.""" module = load_module() From c61d9deb6b1045b0d4e4d576e704cabbc1a4c2f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:24:54 +0900 Subject: [PATCH 115/154] test(governance): reproduce delayed recovery duplicate PUT --- ..._governance_delayed_recovery_regression.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/test_ruleset_governance_delayed_recovery_regression.py diff --git a/tests/test_ruleset_governance_delayed_recovery_regression.py b/tests/test_ruleset_governance_delayed_recovery_regression.py new file mode 100644 index 0000000000..f2a2903fa6 --- /dev/null +++ b/tests/test_ruleset_governance_delayed_recovery_regression.py @@ -0,0 +1,112 @@ +"""Regress delayed ambiguous ruleset recovery without duplicate privileged writes.""" + +from __future__ import annotations + +import importlib.util +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" + + +def load_module(): + """Load the exact production ruleset reconciler from the checkout.""" + + spec = importlib.util.spec_from_file_location( + "ruleset_governance_delayed_recovery_regression", SOURCE + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def repository_target(module): + """Return the exact central repository ruleset target.""" + + return module.RulesetTarget( + scope="repository", + owner="ContextualWisdomLab", + repository=".github", + ruleset_id=17921150, + name="Lock default branch", + ) + + +def live_payload() -> dict: + """Return one live-shaped ruleset state used by the recovery regression.""" + + return { + "id": 17921150, + "name": "Lock default branch", + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": "active", + "bypass_actors": [], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash"], + }, + }, + ], + } + + +def test_delayed_recovery_acceptance_settles_before_any_second_put(monkeypatch) -> None: + """A delayed recovery commit must become visible before another PUT is attempted.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Administrator predecessor", "enforcement": "evaluate"} + history_reads = 0 + put_count = 0 + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + + def fake_history(*_args): + nonlocal history_reads + history_reads += 1 + if history_reads == 1: + return [{"version_id": 10}] + return [{"version_id": 11}, {"version_id": 10}] + + def fake_api(method, endpoint, **_kwargs): + nonlocal put_count + if method == "GET" and endpoint == target.endpoint: + return displaced if history_reads >= 2 else current + if method == "PUT" and endpoint == target.endpoint: + put_count += 1 + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_gh_api_list", fake_history) + monkeypatch.setattr(module, "_gh_api", fake_api) + + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + assert history_reads == 2 + assert put_count == 1 From 40a9ce73c9149ee61862d6318034379a1973e724 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:25:59 +0900 Subject: [PATCH 116/154] test(governance): forbid blind recovery PUT retry --- tests/test_ruleset_governance_timeout_regression.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py index cff053cd05..f870c58926 100644 --- a/tests/test_ruleset_governance_timeout_regression.py +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -248,8 +248,8 @@ def test_recovery_timeout_without_history_fails_closed(monkeypatch) -> None: ) -def test_recovery_timeout_before_acceptance_retries_boundedly(monkeypatch) -> None: - """A recovery PUT absent from history is retried only within the bounded recovery loop.""" +def test_recovery_timeout_before_acceptance_fails_after_settlement_without_retry(monkeypatch) -> None: + """An absent recovery transition fails closed after settlement without issuing another PUT.""" module = load_module() target = repository_target(module) @@ -258,6 +258,7 @@ def test_recovery_timeout_before_acceptance_retries_boundedly(monkeypatch) -> No put_count = 0 monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) def fake_api(method, endpoint, **_kwargs): nonlocal put_count @@ -271,7 +272,10 @@ def fake_api(method, endpoint, **_kwargs): monkeypatch.setattr(module, "_gh_api", fake_api) monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 10}]) - with pytest.raises(module.RulesetGovernanceError, match="exceeded bounded attempts"): + with pytest.raises( + module.RulesetGovernanceError, + match="ambiguous ruleset recovery PUT outcome remains unresolved after settlement window", + ): module._recover_displaced_history_state( target, current_version=10, @@ -279,7 +283,7 @@ def fake_api(method, endpoint, **_kwargs): displaced_version=9, expected_main_sha="a" * 40, ) - assert put_count == module.COLLISION_RECOVERY_LIMIT + assert put_count == 1 def test_recovery_timeout_with_new_version_requires_predecessor(monkeypatch) -> None: From e9260c12adb9b2af054f395919d217b332fc23d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:37 +0900 Subject: [PATCH 117/154] fix(governance): enforce review controls and main guard --- scripts/ci/reconcile_ruleset_governance.py | 9 +++++ .../test_ruleset_governance_reconciliation.py | 35 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 3bba7fb2bf..bea0508fe3 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -745,6 +745,15 @@ def reconcile( ) -> int: """Reconcile all targets and return the number of successful mutations.""" + if expected_main_sha is not None and ( + type(expected_main_sha) is not str or not GIT_SHA_RE.fullmatch(expected_main_sha) + ): + raise RulesetGovernanceError("expected protected main SHA is malformed") + if not verify_only and expected_main_sha is None: + raise RulesetGovernanceError( + "expected protected main SHA is required for mutation" + ) + mutations = 0 for target in sorted(targets, key=lambda item: item.scope == "organization"): if expected_main_sha is None: diff --git a/tests/test_ruleset_governance_reconciliation.py b/tests/test_ruleset_governance_reconciliation.py index 76017688ff..626b98f63d 100644 --- a/tests/test_ruleset_governance_reconciliation.py +++ b/tests/test_ruleset_governance_reconciliation.py @@ -362,6 +362,21 @@ def test_apply_rejects_post_write_identity_replacement(monkeypatch) -> None: module._reconcile_target(target(), verify_only=False) +def test_reconcile_mutation_requires_exact_protected_main_sha(monkeypatch) -> None: + """The callable mutation boundary cannot bypass the protected-main guard.""" + + monkeypatch.setattr( + module, + "_reconcile_target", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("must not mutate")), + ) + targets = (target("repository"),) + with pytest.raises(module.RulesetGovernanceError, match="required for mutation"): + module.reconcile(targets, verify_only=False) + with pytest.raises(module.RulesetGovernanceError, match="malformed"): + module.reconcile(targets, verify_only=False, expected_main_sha="main") + + def test_reconcile_orders_repository_before_organization(monkeypatch) -> None: """The strengthening repository mutation precedes the organization approval change.""" @@ -369,11 +384,25 @@ def test_reconcile_orders_repository_before_organization(monkeypatch) -> None: monkeypatch.setattr( module, "_reconcile_target", - lambda item, verify_only: seen.append((item.scope, verify_only)) or True, + lambda item, verify_only, expected_main_sha=None: seen.append( + (item.scope, verify_only, expected_main_sha) + ) + or True, ) targets = (target("organization"), target("repository")) - assert module.reconcile(targets, verify_only=False) == 2 - assert seen == [("repository", False), ("organization", False)] + expected_main_sha = "a" * 40 + assert ( + module.reconcile( + targets, + verify_only=False, + expected_main_sha=expected_main_sha, + ) + == 2 + ) + assert seen == [ + ("repository", False, expected_main_sha), + ("organization", False, expected_main_sha), + ] def test_gh_api_uses_versioned_stdin_body_and_redacts_failure(monkeypatch) -> None: From b26e0ba3846b95156014579ea46f3f6d055eb1c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:29:32 +0900 Subject: [PATCH 118/154] fix(governance): settle ambiguous recovery before retry --- .../ruleset-governance-reconcile.yml | 3 +++ scripts/ci/reconcile_ruleset_governance.py | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index a1fb6e46ba..43132fa382 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -11,6 +11,7 @@ on: - "tests/test_ruleset_governance_review_round2.py" - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" + - "tests/test_ruleset_governance_delayed_recovery_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -29,6 +30,7 @@ on: - "tests/test_ruleset_governance_review_round2.py" - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" + - "tests/test_ruleset_governance_delayed_recovery_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -110,6 +112,7 @@ jobs: tests/test_ruleset_governance_review_round2.py \ tests/test_ruleset_governance_review_round3.py \ tests/test_ruleset_governance_timeout_regression.py \ + tests/test_ruleset_governance_delayed_recovery_regression.py \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_ruleset_audit_completeness_regression.py \ tests/test_ruleset_merge_method_shape_regression.py \ diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index bea0508fe3..f6a3253bc2 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -502,8 +502,9 @@ def _recover_displaced_history_state( becomes the next recovery target. A newer live state observed before a recovery write is never overwritten. Privileged recovery revalidates the reviewed protected-main SHA immediately before every PUT. Ambiguous recovery - results inspect immutable history before retrying or advancing the recovery - chain. The loop is bounded and fails closed. + results settle against immutable history before any further write, then + either continue the proven predecessor chain or fail closed. The loop is + bounded and never blindly retries a timed-out recovery PUT. """ for _attempt in range(COLLISION_RECOVERY_LIMIT): @@ -528,7 +529,20 @@ def _recover_displaced_history_state( ) recovery_version = _history_version_id(history[0]) if recovery_version == current_version: - continue + for _poll_index in range(1, AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + recovery_version = _history_version_id(history[0]) + if recovery_version != current_version: + break + else: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) if len(history) < 2: raise RulesetGovernanceError( "ambiguous ruleset recovery PUT exposed no predecessor" From c4b52e51b1cdad74d2547e6049def115fc96b80e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:32:32 +0900 Subject: [PATCH 119/154] fix(governance): budget ambiguous recovery settlement --- scripts/ci/reconcile_ruleset_governance.py | 85 +++++++++++++--------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index f6a3253bc2..551ba159f0 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -10,11 +10,11 @@ immutable ruleset-history surface. If history proves a hidden pre-PUT edit was overwritten, the reconciler restores the newest displaced administrator state before failing; recovery re-checks immutable history and protected-main identity -before every recovery write. Ambiguous mutation results are settled from live -state plus immutable history instead of being treated as ordinary request -failures or retried blindly. The canonical audit is executed against projected -and live state so this narrow reconciler never reports broader policy drift as -converged. +before every recovery write. Ambiguous mutation and recovery results are settled +from live state plus immutable history instead of being treated as ordinary +request failures or retried blindly. The canonical audit is executed against +projected and live state so this narrow reconciler never reports broader policy +drift as converged. """ from __future__ import annotations @@ -49,12 +49,16 @@ # normal guarded mutation path, start/mid/end ambiguous-result observations, the # full bounded collision-recovery chain, post-confirmation checks, and the final # workflow verify-only pass. Each blocking API/auditor operation is bounded by -# API_REQUEST_TIMEOUT_SECONDS; settlement sleep spans one additional client -# timeout horizon. The workflow contract test derives its minimum job timeout -# from this function rather than maintaining a second independent estimate. +# API_REQUEST_TIMEOUT_SECONDS; every ambiguous recovery may consume one full +# additional settlement horizon before it is declared unresolved. The workflow +# contract test derives its minimum job timeout from this function rather than +# maintaining a second independent estimate. BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET = 7 AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET = 12 RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT = 7 +RECOVERY_AMBIGUOUS_EXTRA_BLOCKING_OPERATIONS_PER_ATTEMPT = ( + AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1 +) POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET = 2 FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET = 2 @@ -143,14 +147,19 @@ def worst_case_apply_seconds(*, target_count: int) -> int: blocking_operations = ( BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET + AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET - + COLLISION_RECOVERY_LIMIT * RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT + + COLLISION_RECOVERY_LIMIT + * ( + RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT + + RECOVERY_AMBIGUOUS_EXTRA_BLOCKING_OPERATIONS_PER_ATTEMPT + ) + POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET + FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET ) - per_target_seconds = ( - blocking_operations * API_REQUEST_TIMEOUT_SECONDS - + AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + settlement_seconds = ( + AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + + COLLISION_RECOVERY_LIMIT * AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS ) + per_target_seconds = blocking_operations * API_REQUEST_TIMEOUT_SECONDS + settlement_seconds return target_count * per_target_seconds @@ -486,6 +495,28 @@ def _desired_payload(live: dict[str, Any], target: RulesetTarget) -> dict[str, A return desired +def _settle_ambiguous_recovery_history( + target: RulesetTarget, + *, + current_version: int, +) -> list[Any]: + """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + if _history_version_id(history[0]) != current_version: + return history + if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) + + def _recover_displaced_history_state( target: RulesetTarget, *, @@ -502,9 +533,9 @@ def _recover_displaced_history_state( becomes the next recovery target. A newer live state observed before a recovery write is never overwritten. Privileged recovery revalidates the reviewed protected-main SHA immediately before every PUT. Ambiguous recovery - results settle against immutable history before any further write, then - either continue the proven predecessor chain or fail closed. The loop is - bounded and never blindly retries a timed-out recovery PUT. + results settle across one complete client-timeout horizon before any later + recovery write is considered, so a delayed request cannot be duplicated. + The recovery chain remains bounded and fails closed. """ for _attempt in range(COLLISION_RECOVERY_LIMIT): @@ -522,27 +553,11 @@ def _recover_displaced_history_state( try: _gh_api("PUT", target.endpoint, body=displaced_payload) except (AmbiguousRulesetWriteError, subprocess.TimeoutExpired): - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) + history = _settle_ambiguous_recovery_history( + target, + current_version=current_version, + ) recovery_version = _history_version_id(history[0]) - if recovery_version == current_version: - for _poll_index in range(1, AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) - recovery_version = _history_version_id(history[0]) - if recovery_version != current_version: - break - else: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" - ) if len(history) < 2: raise RulesetGovernanceError( "ambiguous ruleset recovery PUT exposed no predecessor" From eb74eef391045336ae6512a2ee27dc38d1947caa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:33:06 +0900 Subject: [PATCH 120/154] fix(governance): budget full recovery settlement window --- .github/workflows/ruleset-governance-reconcile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index 43132fa382..f611a670fb 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -133,7 +133,7 @@ jobs: vars.CWL_RULESET_RECONCILE_ENABLED == 'true' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 90 + timeout-minutes: 120 environment: ruleset-governance-maintenance steps: - name: Harden runner From 663ccfc5ac89f4f8f3efb24a5c01cbc59acbbc9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:33:43 +0900 Subject: [PATCH 121/154] docs(governance): record bounded recovery settlement budget --- docs/doctoring/ruleset-owner-plane-reconciliation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index d11484b36a..9685253f51 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -25,13 +25,13 @@ Any `PUT` result that does not prove server-side rejection is treated as an ambi Ambiguous initial writes enter a bounded settlement protocol tied to the same 30-second client timeout used by GitHub REST calls. Immutable history and live state are observed at the start, midpoint, and end of one additional full client-timeout horizon. A first baseline-only history observation is explicitly **not** represented as rejection because a server-side commit may still become visible after the client has timed out. If the reviewed version appears with the sampled baseline as predecessor, exact acceptance is proven; if an intervening predecessor appears, the displaced-administrator recovery contract runs; if the transition remains baseline-only through the settlement horizon, the run fails as **unresolved** rather than claiming the write was rejected. This is a conservative terminal failure, not authorization to retry or report convergence. -Recovery writes use the same ambiguity principle. Immutable history is checked after an ambiguous recovery result before any retry or progression. A baseline that has not advanced permits only the next bounded recovery-loop iteration; a visible restore version must match the exact intended predecessor payload and expose its predecessor; an unexpected newer state is preserved and causes failure. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules), so a legitimate administrator rename or enforcement change can be restored rather than rejected as foreign history. +Recovery writes use the same ambiguity principle. After a recovery PUT has an ambiguous transport outcome, immutable history is observed across the same full client-timeout settlement horizon before any additional recovery write can occur. If history remains at the pre-write version for the entire window, recovery fails closed as unresolved rather than resending the same restore body. If the restore becomes visible, that version must match the exact intended predecessor payload and expose its predecessor; an unexpected newer state is preserved and causes failure. Historical validation distinguishes immutable target provenance (`id`, branch target, source type, and source) from editable state (`name`, `enforcement`, bypass actors, conditions, and rules), so a legitimate administrator rename or enforcement change can be restored rather than rejected as foreign history. The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. Recovery re-reads live state before every recovery PUT and refuses to overwrite it if it has already advanced. After each recovery PUT, immutable history is read again: the newest version must equal the restore body, and its immediate predecessor must be the version that recovery intended to replace. If a second administrator version slipped between the recovery GET and PUT, that newly displaced predecessor becomes the next recovery target. The bounded recovery chain therefore restores the newest displaced administrator state rather than silently losing it; ambiguous or non-convergent histories fail closed. Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, during ambiguous-result settlement, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. -The Actions timeout is derived from the implementation rather than chosen independently. For each target the source budgets the normal guarded mutation operations, three ambiguous-settlement observations, all eight seven-operation collision-recovery attempts, post-confirmation checks, the final verify-only pass, and one additional 30-second settlement window. At two reviewed targets this conservative source-derived budget is 4,800 seconds (80 minutes). The privileged `apply` job reserves 90 minutes, so Actions cannot reach its configured timeout before that modeled worst-case critical section. The regression suite computes the required minimum directly from `worst_case_apply_seconds(target_count=2)` and compares it with the workflow value. +The Actions timeout is derived from the implementation rather than chosen independently. For each target the source budgets the normal guarded mutation operations, three ambiguous-initial-write settlement observations, all eight seven-operation collision-recovery attempts, the two additional history reads that each ambiguous recovery may require, post-confirmation checks, the final verify-only pass, the initial 30-second settlement horizon, and up to one 30-second recovery-settlement horizon per recovery attempt. At two reviewed targets this conservative source-derived budget is 6,240 seconds (104 minutes). The privileged `apply` job reserves 120 minutes, so Actions cannot reach its configured timeout before that modeled worst-case critical section. The regression suite computes the required minimum directly from `worst_case_apply_seconds(target_count=2)` and compares it with the workflow value. The hourly schedule remains queue-conscious while making disabled state visible. Source, manifest, runtime-auditor, and regression changes are validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the expensive validation/apply path is skipped, but a two-minute `ubuntu-slim` status job writes `Ruleset reconciliation is disabled` to the workflow summary so operators can distinguish deliberate disablement from a missing run. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. From c49aae13e2eda21db47e8a29489293d6b92a0193 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:37:17 +0900 Subject: [PATCH 122/154] test(governance): cover bounded recovery and verify-only path --- ..._governance_delayed_recovery_regression.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/test_ruleset_governance_delayed_recovery_regression.py b/tests/test_ruleset_governance_delayed_recovery_regression.py index f2a2903fa6..86ab4214c1 100644 --- a/tests/test_ruleset_governance_delayed_recovery_regression.py +++ b/tests/test_ruleset_governance_delayed_recovery_regression.py @@ -7,6 +7,8 @@ import sys from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" @@ -110,3 +112,80 @@ def fake_api(method, endpoint, **_kwargs): assert history_reads == 2 assert put_count == 1 + + +def test_recovery_chain_exhaustion_fails_closed_after_bounded_attempts(monkeypatch) -> None: + """A perpetual collision chain reaches the bounded terminal error, never an unbounded loop.""" + + module = load_module() + target = repository_target(module) + live_state = live_payload() + history_reads = 0 + put_count = 0 + + def payload_for(version: int) -> dict: + return { + **live_payload(), + "name": f"Administrator predecessor {version}", + "enforcement": "evaluate", + } + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def fake_history_state(_target, version): + if version >= 100: + return live_state + return payload_for(version) + + def fake_history(*_args): + nonlocal history_reads + index = history_reads + history_reads += 1 + return [ + {"version_id": 100 + index}, + {"version_id": 8 - index}, + ] + + def fake_api(method, endpoint, *, body=None): + nonlocal live_state, put_count + if method == "GET" and endpoint == target.endpoint: + return live_state + if method == "PUT" and endpoint == target.endpoint: + assert body is not None + put_count += 1 + live_state = body + return {} + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_history_version_state", fake_history_state) + monkeypatch.setattr(module, "_gh_api_list", fake_history) + monkeypatch.setattr(module, "_gh_api", fake_api) + + with pytest.raises(module.RulesetGovernanceError, match="exceeded bounded attempts"): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(live_state), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + assert history_reads == module.COLLISION_RECOVERY_LIMIT + assert put_count == module.COLLISION_RECOVERY_LIMIT + + +def test_verify_only_reconcile_dispatches_without_mutation_sha(monkeypatch) -> None: + """Read-only reconciliation keeps its documented no-mutation-SHA path covered.""" + + module = load_module() + target = repository_target(module) + calls: list[tuple[str, bool, str | None]] = [] + + def fake_reconcile_target(seen_target, *, verify_only, expected_main_sha=None): + calls.append((seen_target.scope, verify_only, expected_main_sha)) + return False + + monkeypatch.setattr(module, "_reconcile_target", fake_reconcile_target) + + assert module.reconcile((target,), verify_only=True) == 0 + assert calls == [("repository", True, None)] From 45990e9f7dcf8c01c158c0ef4131bc855437ad4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:39:10 +0900 Subject: [PATCH 123/154] test(governance): preserve provenance in collision harness --- tests/test_ruleset_governance_delayed_recovery_regression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_delayed_recovery_regression.py b/tests/test_ruleset_governance_delayed_recovery_regression.py index 86ab4214c1..7fc050c5d8 100644 --- a/tests/test_ruleset_governance_delayed_recovery_regression.py +++ b/tests/test_ruleset_governance_delayed_recovery_regression.py @@ -153,7 +153,7 @@ def fake_api(method, endpoint, *, body=None): if method == "PUT" and endpoint == target.endpoint: assert body is not None put_count += 1 - live_state = body + live_state = {**live_state, **body} return {} raise AssertionError((method, endpoint)) From 876697fe421f8ca5e39d93a455a4836ee34b2a35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:43:41 +0900 Subject: [PATCH 124/154] test(governance): reproduce intervening-version settlement race --- ..._governance_delayed_recovery_regression.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/test_ruleset_governance_delayed_recovery_regression.py b/tests/test_ruleset_governance_delayed_recovery_regression.py index 7fc050c5d8..55aff07f60 100644 --- a/tests/test_ruleset_governance_delayed_recovery_regression.py +++ b/tests/test_ruleset_governance_delayed_recovery_regression.py @@ -114,6 +114,136 @@ def fake_api(method, endpoint, **_kwargs): assert put_count == 1 +def test_initial_ambiguous_put_waits_through_intervening_admin_version(monkeypatch) -> None: + """An administrator version appearing first cannot end initial PUT settlement.""" + + module = load_module() + target = repository_target(module) + drifted = live_payload() + drifted["bypass_actors"] = [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ] + desired = module._desired_payload(drifted, target) + desired_state = {**drifted, **desired} + administrator_state = { + **drifted, + "name": "Administrator intervening state", + "enforcement": "evaluate", + } + history_reads = 0 + recovered: list[tuple[int, int, dict]] = [] + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + + def fake_history(*_args): + nonlocal history_reads + history_reads += 1 + if history_reads == 1: + return [{"version_id": 11}, {"version_id": 10}] + return [ + {"version_id": 12}, + {"version_id": 11}, + {"version_id": 10}, + ] + + def fake_history_state(_target, version): + return {11: administrator_state, 12: desired_state}[version] + + def fake_recovery(_target, *, current_version, current_payload, displaced_version, **_kwargs): + recovered.append((current_version, displaced_version, current_payload)) + + monkeypatch.setattr(module, "_gh_api_list", fake_history) + monkeypatch.setattr(module, "_history_version_state", fake_history_state) + monkeypatch.setattr(module, "_recover_displaced_history_state", fake_recovery) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: desired_state) + + with pytest.raises(module.RulesetGovernanceError, match="concurrent ruleset history detected"): + module._confirm_ambiguous_put( + target, + baseline_version=10, + desired=desired, + expected_main_sha="a" * 40, + ) + + assert history_reads == 2 + assert recovered == [(12, 11, desired)] + + +def test_recovery_ambiguous_put_waits_through_intervening_admin_version(monkeypatch) -> None: + """A delayed recovery PUT tracks an administrator predecessor before restoring it.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + restore_state = { + **current, + "name": "Original administrator state", + "enforcement": "evaluate", + } + intervening_state = { + **current, + "name": "Intervening administrator state", + "enforcement": "evaluate", + } + live_state = current + history_reads = 0 + put_bodies: list[dict] = [] + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + + def fake_history_state(_target, version): + return { + 9: restore_state, + 11: intervening_state, + 12: restore_state, + 13: intervening_state, + }[version] + + def fake_history(*_args): + nonlocal history_reads, live_state + history_reads += 1 + if history_reads == 1: + live_state = intervening_state + return [{"version_id": 11}, {"version_id": 10}] + if history_reads == 2: + live_state = restore_state + return [{"version_id": 12}, {"version_id": 11}] + return [{"version_id": 13}, {"version_id": 12}] + + def fake_api(method, endpoint, *, body=None): + nonlocal live_state + if method == "GET" and endpoint == target.endpoint: + return live_state + if method == "PUT" and endpoint == target.endpoint: + assert body is not None + put_bodies.append(body) + if len(put_bodies) == 1: + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + live_state = {**live_state, **body} + return {} + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_history_version_state", fake_history_state) + monkeypatch.setattr(module, "_gh_api_list", fake_history) + monkeypatch.setattr(module, "_gh_api", fake_api) + + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + assert history_reads == 3 + assert put_bodies == [ + module._editable_projection(restore_state), + module._editable_projection(intervening_state), + ] + + def test_recovery_chain_exhaustion_fails_closed_after_bounded_attempts(monkeypatch) -> None: """A perpetual collision chain reaches the bounded terminal error, never an unbounded loop.""" From 1486443678ae576b7ae0335acd0ce3a1f04ea71a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:51:23 +0900 Subject: [PATCH 125/154] chore(governance): run one-shot intervening-settlement repair --- ...p_pr1644_intervening_settlement_repair.yml | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 .github/workflows/_temp_pr1644_intervening_settlement_repair.yml diff --git a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml new file mode 100644 index 0000000000..16d8d11b4b --- /dev/null +++ b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml @@ -0,0 +1,230 @@ +name: Temporary PR 1644 intervening settlement repair + +on: + push: + branches: + - fix/ruleset-owner-plane-reconciler + paths: + - .github/workflows/_temp_pr1644_intervening_settlement_repair.yml + +permissions: + contents: write + +jobs: + repair: + if: github.event.head_commit.message == 'chore(governance): run one-shot intervening-settlement repair' + runs-on: ubuntu-slim + timeout-minutes: 15 + steps: + - name: Check out triggering revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: true + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: '3.12' + check-latest: false + + - name: Repair, validate, and self-retire + shell: bash + env: + TRIGGER_SHA: ${{ github.sha }} + WRITER_BRANCH: fix/ruleset-owner-plane-reconciler + run: | + set -euo pipefail + workflow='.github/workflows/_temp_pr1644_intervening_settlement_repair.yml' + source_file='scripts/ci/reconcile_ruleset_governance.py' + + cleanup_failed_repair() { + status=$? + trap - EXIT + if [ "$status" -ne 0 ]; then + git reset --hard "$TRIGGER_SHA" + git rm -f "$workflow" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'chore(governance): retire failed one-shot settlement repair' || true + git fetch origin "$WRITER_BRANCH" + remote_sha="$(git rev-parse FETCH_HEAD)" + if [ "$remote_sha" = "$TRIGGER_SHA" ]; then + git push origin "HEAD:$WRITER_BRANCH" || true + fi + fi + exit "$status" + } + trap cleanup_failed_repair EXIT + + test "$(git rev-parse HEAD)" = "$TRIGGER_SHA" + grep -q 'test_initial_ambiguous_put_waits_through_intervening_admin_version' tests/test_ruleset_governance_delayed_recovery_regression.py + grep -q 'test_recovery_ambiguous_put_waits_through_intervening_admin_version' tests/test_ruleset_governance_delayed_recovery_regression.py + + python - <<'PY' + from pathlib import Path + + path = Path('scripts/ci/reconcile_ruleset_governance.py') + text = path.read_text(encoding='utf-8') + + class_old = '''class RulesetMutationNotVisibleError(RulesetGovernanceError): + """Raised while an ambiguous PUT has not yet appeared in immutable history.""" + ''' + class_new = '''class RulesetMutationNotVisibleError(RulesetGovernanceError): + """Raised while an ambiguous PUT has not yet appeared in immutable history.""" + + + class RulesetMutationStillSettlingError(RulesetGovernanceError): + """Raised when history changed but the ambiguous reviewed PUT is not visible yet.""" + ''' + if text.count(class_old) != 1: + raise SystemExit('expected mutation-not-visible class anchor exactly once') + text = text.replace(class_old, class_new, 1) + + settle_old = '''def _settle_ambiguous_recovery_history( + target: RulesetTarget, + *, + current_version: int, + ) -> list[Any]: + """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + if _history_version_id(history[0]) != current_version: + return history + if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) + ''' + settle_new = '''def _settle_ambiguous_recovery_history( + target: RulesetTarget, + *, + current_version: int, + expected_payload: dict[str, Any], + ) -> list[Any]: + """Observe the full bounded horizon until the ambiguous recovery PUT appears. + + An administrator version can become visible before a delayed recovery request. + That intervening version is evidence of concurrency, not evidence that the + delayed request was rejected. Settlement therefore waits for the exact restore + payload and lets the caller recover its actual immutable predecessor. + """ + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + newest_version = _history_version_id(history[0]) + if newest_version != current_version: + newest_state = _history_version_state(target, newest_version) + if _editable_projection(newest_state) == expected_payload: + return history + if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) + ''' + if text.count(settle_old) != 1: + raise SystemExit('expected recovery settlement function exactly once') + text = text.replace(settle_old, settle_new, 1) + + call_old = ''' history = _settle_ambiguous_recovery_history( + target, + current_version=current_version, + ) + ''' + call_new = ''' history = _settle_ambiguous_recovery_history( + target, + current_version=current_version, + expected_payload=displaced_payload, + ) + ''' + if text.count(call_old) != 1: + raise SystemExit('expected recovery settlement call exactly once') + text = text.replace(call_old, call_new, 1) + + mismatch_old = ''' newest_state = _history_version_state(target, newest_id) + if _editable_projection(newest_state) != desired: + raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") + ''' + mismatch_new = ''' newest_state = _history_version_state(target, newest_id) + if _editable_projection(newest_state) != desired: + raise RulesetMutationStillSettlingError( + "latest ruleset history changed before the reviewed mutation became visible" + ) + ''' + if text.count(mismatch_old) != 1: + raise SystemExit('expected history mismatch block exactly once') + text = text.replace(mismatch_old, mismatch_new, 1) + + catch_old = ''' except RulesetMutationNotVisibleError: + live = _gh_api("GET", target.endpoint) + ''' + catch_new = ''' except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): + live = _gh_api("GET", target.endpoint) + ''' + if text.count(catch_old) != 1: + raise SystemExit('expected ambiguous settlement catch exactly once') + text = text.replace(catch_old, catch_new, 1) + + doc_old = ''' A timeout, connection loss, nonzero transport result, or malformed successful + response can occur after GitHub accepted the update. Three observations span + one additional full client timeout horizon: start, midpoint, and end. A + baseline-only first observation is therefore never represented as rejection. + Acceptance must become visible in immutable history and live state; collision + ''' + doc_new = ''' A timeout, connection loss, nonzero transport result, or malformed successful + response can occur after GitHub accepted the update. Three observations span + one additional full client timeout horizon: start, midpoint, and end. Neither + a baseline-only observation nor an intervening administrator version proves + rejection of the delayed reviewed PUT. Acceptance must become visible as the + exact reviewed payload in immutable history and live state; collision + ''' + if text.count(doc_old) != 1: + raise SystemExit('expected ambiguous PUT docstring anchor exactly once') + text = text.replace(doc_old, doc_new, 1) + + path.write_text(text, encoding='utf-8') + PY + + python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + export COVERAGE_RCFILE=/dev/null + python -m coverage run --branch --include=scripts/ci/reconcile_ruleset_governance.py -m pytest -q \ + tests/test_ruleset_governance_reconciliation.py \ + tests/test_ruleset_governance_review_regressions.py \ + tests/test_ruleset_governance_review_round2.py \ + tests/test_ruleset_governance_review_round3.py \ + tests/test_ruleset_governance_timeout_regression.py \ + tests/test_ruleset_governance_delayed_recovery_regression.py \ + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_ruleset_audit_completeness_regression.py \ + tests/test_ruleset_merge_method_shape_regression.py \ + tests/test_solo_maintainer_ruleset_policy.py + python -m coverage report --fail-under=100 --show-missing --include=scripts/ci/reconcile_ruleset_governance.py + python -m interrogate --fail-under 100 scripts/ci/reconcile_ruleset_governance.py + git diff --check + + git rm "$workflow" + git add "$source_file" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix(governance): settle delayed writes across intervening versions' + + git fetch origin "$WRITER_BRANCH" + remote_sha="$(git rev-parse FETCH_HEAD)" + if [ "$remote_sha" != "$TRIGGER_SHA" ]; then + echo "Writer branch advanced concurrently; refusing non-fast-forward push." >&2 + exit 1 + fi + git push origin "HEAD:$WRITER_BRANCH" + trap - EXIT From 978c9f07653b84b88ff78ee1bd79d55faad0e736 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:51:56 +0900 Subject: [PATCH 126/154] chore(governance): retire failed one-shot settlement repair --- ...p_pr1644_intervening_settlement_repair.yml | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 .github/workflows/_temp_pr1644_intervening_settlement_repair.yml diff --git a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml deleted file mode 100644 index 16d8d11b4b..0000000000 --- a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml +++ /dev/null @@ -1,230 +0,0 @@ -name: Temporary PR 1644 intervening settlement repair - -on: - push: - branches: - - fix/ruleset-owner-plane-reconciler - paths: - - .github/workflows/_temp_pr1644_intervening_settlement_repair.yml - -permissions: - contents: write - -jobs: - repair: - if: github.event.head_commit.message == 'chore(governance): run one-shot intervening-settlement repair' - runs-on: ubuntu-slim - timeout-minutes: 15 - steps: - - name: Check out triggering revision - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: true - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 - with: - python-version: '3.12' - check-latest: false - - - name: Repair, validate, and self-retire - shell: bash - env: - TRIGGER_SHA: ${{ github.sha }} - WRITER_BRANCH: fix/ruleset-owner-plane-reconciler - run: | - set -euo pipefail - workflow='.github/workflows/_temp_pr1644_intervening_settlement_repair.yml' - source_file='scripts/ci/reconcile_ruleset_governance.py' - - cleanup_failed_repair() { - status=$? - trap - EXIT - if [ "$status" -ne 0 ]; then - git reset --hard "$TRIGGER_SHA" - git rm -f "$workflow" - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'chore(governance): retire failed one-shot settlement repair' || true - git fetch origin "$WRITER_BRANCH" - remote_sha="$(git rev-parse FETCH_HEAD)" - if [ "$remote_sha" = "$TRIGGER_SHA" ]; then - git push origin "HEAD:$WRITER_BRANCH" || true - fi - fi - exit "$status" - } - trap cleanup_failed_repair EXIT - - test "$(git rev-parse HEAD)" = "$TRIGGER_SHA" - grep -q 'test_initial_ambiguous_put_waits_through_intervening_admin_version' tests/test_ruleset_governance_delayed_recovery_regression.py - grep -q 'test_recovery_ambiguous_put_waits_through_intervening_admin_version' tests/test_ruleset_governance_delayed_recovery_regression.py - - python - <<'PY' - from pathlib import Path - - path = Path('scripts/ci/reconcile_ruleset_governance.py') - text = path.read_text(encoding='utf-8') - - class_old = '''class RulesetMutationNotVisibleError(RulesetGovernanceError): - """Raised while an ambiguous PUT has not yet appeared in immutable history.""" - ''' - class_new = '''class RulesetMutationNotVisibleError(RulesetGovernanceError): - """Raised while an ambiguous PUT has not yet appeared in immutable history.""" - - - class RulesetMutationStillSettlingError(RulesetGovernanceError): - """Raised when history changed but the ambiguous reviewed PUT is not visible yet.""" - ''' - if text.count(class_old) != 1: - raise SystemExit('expected mutation-not-visible class anchor exactly once') - text = text.replace(class_old, class_new, 1) - - settle_old = '''def _settle_ambiguous_recovery_history( - target: RulesetTarget, - *, - current_version: int, - ) -> list[Any]: - """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" - - for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) - if _history_version_id(history[0]) != current_version: - return history - if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: - time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" - ) - ''' - settle_new = '''def _settle_ambiguous_recovery_history( - target: RulesetTarget, - *, - current_version: int, - expected_payload: dict[str, Any], - ) -> list[Any]: - """Observe the full bounded horizon until the ambiguous recovery PUT appears. - - An administrator version can become visible before a delayed recovery request. - That intervening version is evidence of concurrency, not evidence that the - delayed request was rejected. Settlement therefore waits for the exact restore - payload and lets the caller recover its actual immutable predecessor. - """ - - for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) - newest_version = _history_version_id(history[0]) - if newest_version != current_version: - newest_state = _history_version_state(target, newest_version) - if _editable_projection(newest_state) == expected_payload: - return history - if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: - time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" - ) - ''' - if text.count(settle_old) != 1: - raise SystemExit('expected recovery settlement function exactly once') - text = text.replace(settle_old, settle_new, 1) - - call_old = ''' history = _settle_ambiguous_recovery_history( - target, - current_version=current_version, - ) - ''' - call_new = ''' history = _settle_ambiguous_recovery_history( - target, - current_version=current_version, - expected_payload=displaced_payload, - ) - ''' - if text.count(call_old) != 1: - raise SystemExit('expected recovery settlement call exactly once') - text = text.replace(call_old, call_new, 1) - - mismatch_old = ''' newest_state = _history_version_state(target, newest_id) - if _editable_projection(newest_state) != desired: - raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") - ''' - mismatch_new = ''' newest_state = _history_version_state(target, newest_id) - if _editable_projection(newest_state) != desired: - raise RulesetMutationStillSettlingError( - "latest ruleset history changed before the reviewed mutation became visible" - ) - ''' - if text.count(mismatch_old) != 1: - raise SystemExit('expected history mismatch block exactly once') - text = text.replace(mismatch_old, mismatch_new, 1) - - catch_old = ''' except RulesetMutationNotVisibleError: - live = _gh_api("GET", target.endpoint) - ''' - catch_new = ''' except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): - live = _gh_api("GET", target.endpoint) - ''' - if text.count(catch_old) != 1: - raise SystemExit('expected ambiguous settlement catch exactly once') - text = text.replace(catch_old, catch_new, 1) - - doc_old = ''' A timeout, connection loss, nonzero transport result, or malformed successful - response can occur after GitHub accepted the update. Three observations span - one additional full client timeout horizon: start, midpoint, and end. A - baseline-only first observation is therefore never represented as rejection. - Acceptance must become visible in immutable history and live state; collision - ''' - doc_new = ''' A timeout, connection loss, nonzero transport result, or malformed successful - response can occur after GitHub accepted the update. Three observations span - one additional full client timeout horizon: start, midpoint, and end. Neither - a baseline-only observation nor an intervening administrator version proves - rejection of the delayed reviewed PUT. Acceptance must become visible as the - exact reviewed payload in immutable history and live state; collision - ''' - if text.count(doc_old) != 1: - raise SystemExit('expected ambiguous PUT docstring anchor exactly once') - text = text.replace(doc_old, doc_new, 1) - - path.write_text(text, encoding='utf-8') - PY - - python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - export COVERAGE_RCFILE=/dev/null - python -m coverage run --branch --include=scripts/ci/reconcile_ruleset_governance.py -m pytest -q \ - tests/test_ruleset_governance_reconciliation.py \ - tests/test_ruleset_governance_review_regressions.py \ - tests/test_ruleset_governance_review_round2.py \ - tests/test_ruleset_governance_review_round3.py \ - tests/test_ruleset_governance_timeout_regression.py \ - tests/test_ruleset_governance_delayed_recovery_regression.py \ - tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_ruleset_audit_completeness_regression.py \ - tests/test_ruleset_merge_method_shape_regression.py \ - tests/test_solo_maintainer_ruleset_policy.py - python -m coverage report --fail-under=100 --show-missing --include=scripts/ci/reconcile_ruleset_governance.py - python -m interrogate --fail-under 100 scripts/ci/reconcile_ruleset_governance.py - git diff --check - - git rm "$workflow" - git add "$source_file" - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix(governance): settle delayed writes across intervening versions' - - git fetch origin "$WRITER_BRANCH" - remote_sha="$(git rev-parse FETCH_HEAD)" - if [ "$remote_sha" != "$TRIGGER_SHA" ]; then - echo "Writer branch advanced concurrently; refusing non-fast-forward push." >&2 - exit 1 - fi - git push origin "HEAD:$WRITER_BRANCH" - trap - EXIT From de0b5778f9f8e1cfb94ca61e4d46c13c4431a1ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:53:46 +0900 Subject: [PATCH 127/154] chore(governance): stage one-shot settlement source repair --- ...mp_pr1644_intervening_settlement_repair.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 scripts/ci/_temp_pr1644_intervening_settlement_repair.py diff --git a/scripts/ci/_temp_pr1644_intervening_settlement_repair.py b/scripts/ci/_temp_pr1644_intervening_settlement_repair.py new file mode 100644 index 0000000000..e82aeb66c1 --- /dev/null +++ b/scripts/ci/_temp_pr1644_intervening_settlement_repair.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""One-shot source repair for PR #1644; the workflow removes this file after validation.""" + +from pathlib import Path + +PATH = Path("scripts/ci/reconcile_ruleset_governance.py") +text = PATH.read_text(encoding="utf-8") + +replacements = [ + ( + '''class RulesetMutationNotVisibleError(RulesetGovernanceError): + """Raised while an ambiguous PUT has not yet appeared in immutable history.""" +''', + '''class RulesetMutationNotVisibleError(RulesetGovernanceError): + """Raised while an ambiguous PUT has not yet appeared in immutable history.""" + + +class RulesetMutationStillSettlingError(RulesetGovernanceError): + """Raised when history changed but the ambiguous reviewed PUT is not visible yet.""" +''', + "mutation settlement error class", + ), + ( + '''def _settle_ambiguous_recovery_history( + target: RulesetTarget, + *, + current_version: int, +) -> list[Any]: + """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + if _history_version_id(history[0]) != current_version: + return history + if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) +''', + '''def _settle_ambiguous_recovery_history( + target: RulesetTarget, + *, + current_version: int, + expected_payload: dict[str, Any], +) -> list[Any]: + """Observe the full bounded horizon until the ambiguous recovery PUT appears. + + An administrator version can become visible before a delayed recovery request. + That intervening version is evidence of concurrency, not evidence that the + delayed request was rejected. Settlement therefore waits for the exact restore + payload and lets the caller recover its actual immutable predecessor. + """ + + for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): + history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") + if not history: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT exposed no history" + ) + newest_version = _history_version_id(history[0]) + if newest_version != current_version: + newest_state = _history_version_state(target, newest_version) + if _editable_projection(newest_state) == expected_payload: + return history + if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: + time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" + ) +''', + "ambiguous recovery settlement", + ), + ( + ''' history = _settle_ambiguous_recovery_history( + target, + current_version=current_version, + ) +''', + ''' history = _settle_ambiguous_recovery_history( + target, + current_version=current_version, + expected_payload=displaced_payload, + ) +''', + "recovery settlement call", + ), + ( + ''' newest_state = _history_version_state(target, newest_id) + if _editable_projection(newest_state) != desired: + raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") +''', + ''' newest_state = _history_version_state(target, newest_id) + if _editable_projection(newest_state) != desired: + raise RulesetMutationStillSettlingError( + "latest ruleset history changed before the reviewed mutation became visible" + ) +''', + "initial ambiguous history mismatch", + ), + ( + ''' except RulesetMutationNotVisibleError: + live = _gh_api("GET", target.endpoint) +''', + ''' except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): + live = _gh_api("GET", target.endpoint) +''', + "initial ambiguous settlement catch", + ), + ( + ''' response can occur after GitHub accepted the update. Three observations span + one additional full client timeout horizon: start, midpoint, and end. A + baseline-only first observation is therefore never represented as rejection. + Acceptance must become visible in immutable history and live state; collision +''', + ''' response can occur after GitHub accepted the update. Three observations span + one additional full client timeout horizon: start, midpoint, and end. Neither + a baseline-only observation nor an intervening administrator version proves + rejection of the delayed reviewed PUT. Acceptance must become visible as the + exact reviewed payload in immutable history and live state; collision +''', + "initial ambiguous settlement documentation", + ), +] + +for old, new, label in replacements: + count = text.count(old) + if count != 1: + raise SystemExit(f"{label}: expected exactly one source anchor, found {count}") + text = text.replace(old, new, 1) + +PATH.write_text(text, encoding="utf-8") From 131832d4fed47bf36571119125317e68e6252576 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:54:33 +0900 Subject: [PATCH 128/154] chore(governance): execute one-shot settlement source repair --- ...p_pr1644_intervening_settlement_repair.yml | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/_temp_pr1644_intervening_settlement_repair.yml diff --git a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml new file mode 100644 index 0000000000..bf0bb080d7 --- /dev/null +++ b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml @@ -0,0 +1,71 @@ +name: Temporary PR 1644 Settlement Repair + +on: + push: + branches: + - fix/ruleset-owner-plane-reconciler + paths: + - scripts/ci/_temp_pr1644_intervening_settlement_repair.py + - .github/workflows/_temp_pr1644_intervening_settlement_repair.yml + +permissions: + contents: write + +jobs: + repair: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Check out triggering revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: true + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: '3.12' + check-latest: false + + - name: Apply exact source repair + run: python scripts/ci/_temp_pr1644_intervening_settlement_repair.py + + - name: Install hash-locked validation tooling + run: python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + + - name: Prove focused governance contract + env: + COVERAGE_RCFILE: /dev/null + run: | + set -euo pipefail + python -m coverage run --branch --include=scripts/ci/reconcile_ruleset_governance.py -m pytest -q \ + tests/test_ruleset_governance_reconciliation.py \ + tests/test_ruleset_governance_review_regressions.py \ + tests/test_ruleset_governance_review_round2.py \ + tests/test_ruleset_governance_review_round3.py \ + tests/test_ruleset_governance_timeout_regression.py \ + tests/test_ruleset_governance_delayed_recovery_regression.py \ + tests/test_central_required_workflow_ruleset_audit.py \ + tests/test_ruleset_audit_completeness_regression.py \ + tests/test_ruleset_merge_method_shape_regression.py \ + tests/test_solo_maintainer_ruleset_policy.py + python -m coverage report --fail-under=100 --show-missing --include=scripts/ci/reconcile_ruleset_governance.py + python -m interrogate --fail-under 100 scripts/ci/reconcile_ruleset_governance.py + git diff --check + + - name: Commit validated repair and self-retire + env: + WRITER_BRANCH: fix/ruleset-owner-plane-reconciler + run: | + set -euo pipefail + git fetch origin "$WRITER_BRANCH" + test "$(git rev-parse FETCH_HEAD)" = "$GITHUB_SHA" + git rm .github/workflows/_temp_pr1644_intervening_settlement_repair.yml + git rm scripts/ci/_temp_pr1644_intervening_settlement_repair.py + git add scripts/ci/reconcile_ruleset_governance.py + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git commit -m 'fix(governance): settle delayed writes across intervening versions' + git push origin "HEAD:$WRITER_BRANCH" From d840bc990d2531695bfe88bfa3e220ea67e0d23b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:55:30 +0900 Subject: [PATCH 129/154] chore(governance): retire unusable one-shot workflow --- ...p_pr1644_intervening_settlement_repair.yml | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 .github/workflows/_temp_pr1644_intervening_settlement_repair.yml diff --git a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml b/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml deleted file mode 100644 index bf0bb080d7..0000000000 --- a/.github/workflows/_temp_pr1644_intervening_settlement_repair.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Temporary PR 1644 Settlement Repair - -on: - push: - branches: - - fix/ruleset-owner-plane-reconciler - paths: - - scripts/ci/_temp_pr1644_intervening_settlement_repair.py - - .github/workflows/_temp_pr1644_intervening_settlement_repair.yml - -permissions: - contents: write - -jobs: - repair: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - name: Check out triggering revision - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: true - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 - with: - python-version: '3.12' - check-latest: false - - - name: Apply exact source repair - run: python scripts/ci/_temp_pr1644_intervening_settlement_repair.py - - - name: Install hash-locked validation tooling - run: python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - - - name: Prove focused governance contract - env: - COVERAGE_RCFILE: /dev/null - run: | - set -euo pipefail - python -m coverage run --branch --include=scripts/ci/reconcile_ruleset_governance.py -m pytest -q \ - tests/test_ruleset_governance_reconciliation.py \ - tests/test_ruleset_governance_review_regressions.py \ - tests/test_ruleset_governance_review_round2.py \ - tests/test_ruleset_governance_review_round3.py \ - tests/test_ruleset_governance_timeout_regression.py \ - tests/test_ruleset_governance_delayed_recovery_regression.py \ - tests/test_central_required_workflow_ruleset_audit.py \ - tests/test_ruleset_audit_completeness_regression.py \ - tests/test_ruleset_merge_method_shape_regression.py \ - tests/test_solo_maintainer_ruleset_policy.py - python -m coverage report --fail-under=100 --show-missing --include=scripts/ci/reconcile_ruleset_governance.py - python -m interrogate --fail-under 100 scripts/ci/reconcile_ruleset_governance.py - git diff --check - - - name: Commit validated repair and self-retire - env: - WRITER_BRANCH: fix/ruleset-owner-plane-reconciler - run: | - set -euo pipefail - git fetch origin "$WRITER_BRANCH" - test "$(git rev-parse FETCH_HEAD)" = "$GITHUB_SHA" - git rm .github/workflows/_temp_pr1644_intervening_settlement_repair.yml - git rm scripts/ci/_temp_pr1644_intervening_settlement_repair.py - git add scripts/ci/reconcile_ruleset_governance.py - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'fix(governance): settle delayed writes across intervening versions' - git push origin "HEAD:$WRITER_BRANCH" From e370c03ad97be6a35b7c3fac3c4bb3bad035e473 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:55:36 +0900 Subject: [PATCH 130/154] chore(governance): remove staged one-shot repair helper --- ...mp_pr1644_intervening_settlement_repair.py | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 scripts/ci/_temp_pr1644_intervening_settlement_repair.py diff --git a/scripts/ci/_temp_pr1644_intervening_settlement_repair.py b/scripts/ci/_temp_pr1644_intervening_settlement_repair.py deleted file mode 100644 index e82aeb66c1..0000000000 --- a/scripts/ci/_temp_pr1644_intervening_settlement_repair.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -"""One-shot source repair for PR #1644; the workflow removes this file after validation.""" - -from pathlib import Path - -PATH = Path("scripts/ci/reconcile_ruleset_governance.py") -text = PATH.read_text(encoding="utf-8") - -replacements = [ - ( - '''class RulesetMutationNotVisibleError(RulesetGovernanceError): - """Raised while an ambiguous PUT has not yet appeared in immutable history.""" -''', - '''class RulesetMutationNotVisibleError(RulesetGovernanceError): - """Raised while an ambiguous PUT has not yet appeared in immutable history.""" - - -class RulesetMutationStillSettlingError(RulesetGovernanceError): - """Raised when history changed but the ambiguous reviewed PUT is not visible yet.""" -''', - "mutation settlement error class", - ), - ( - '''def _settle_ambiguous_recovery_history( - target: RulesetTarget, - *, - current_version: int, -) -> list[Any]: - """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" - - for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) - if _history_version_id(history[0]) != current_version: - return history - if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: - time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" - ) -''', - '''def _settle_ambiguous_recovery_history( - target: RulesetTarget, - *, - current_version: int, - expected_payload: dict[str, Any], -) -> list[Any]: - """Observe the full bounded horizon until the ambiguous recovery PUT appears. - - An administrator version can become visible before a delayed recovery request. - That intervening version is evidence of concurrency, not evidence that the - delayed request was rejected. Settlement therefore waits for the exact restore - payload and lets the caller recover its actual immutable predecessor. - """ - - for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") - if not history: - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT exposed no history" - ) - newest_version = _history_version_id(history[0]) - if newest_version != current_version: - newest_state = _history_version_state(target, newest_version) - if _editable_projection(newest_state) == expected_payload: - return history - if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: - time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) - raise RulesetGovernanceError( - "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" - ) -''', - "ambiguous recovery settlement", - ), - ( - ''' history = _settle_ambiguous_recovery_history( - target, - current_version=current_version, - ) -''', - ''' history = _settle_ambiguous_recovery_history( - target, - current_version=current_version, - expected_payload=displaced_payload, - ) -''', - "recovery settlement call", - ), - ( - ''' newest_state = _history_version_state(target, newest_id) - if _editable_projection(newest_state) != desired: - raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") -''', - ''' newest_state = _history_version_state(target, newest_id) - if _editable_projection(newest_state) != desired: - raise RulesetMutationStillSettlingError( - "latest ruleset history changed before the reviewed mutation became visible" - ) -''', - "initial ambiguous history mismatch", - ), - ( - ''' except RulesetMutationNotVisibleError: - live = _gh_api("GET", target.endpoint) -''', - ''' except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): - live = _gh_api("GET", target.endpoint) -''', - "initial ambiguous settlement catch", - ), - ( - ''' response can occur after GitHub accepted the update. Three observations span - one additional full client timeout horizon: start, midpoint, and end. A - baseline-only first observation is therefore never represented as rejection. - Acceptance must become visible in immutable history and live state; collision -''', - ''' response can occur after GitHub accepted the update. Three observations span - one additional full client timeout horizon: start, midpoint, and end. Neither - a baseline-only observation nor an intervening administrator version proves - rejection of the delayed reviewed PUT. Acceptance must become visible as the - exact reviewed payload in immutable history and live state; collision -''', - "initial ambiguous settlement documentation", - ), -] - -for old, new, label in replacements: - count = text.count(old) - if count != 1: - raise SystemExit(f"{label}: expected exactly one source anchor, found {count}") - text = text.replace(old, new, 1) - -PATH.write_text(text, encoding="utf-8") From e83393091bc4c525611517509762d32e738984ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:59:26 +0900 Subject: [PATCH 131/154] fix(governance): settle delayed writes across intervening versions --- scripts/ci/reconcile_ruleset_governance.py | 34 +++++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 551ba159f0..1013424072 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -75,6 +75,10 @@ class RulesetMutationNotVisibleError(RulesetGovernanceError): """Raised while an ambiguous PUT has not yet appeared in immutable history.""" +class RulesetMutationStillSettlingError(RulesetGovernanceError): + """Raised when history changed but the ambiguous reviewed PUT is not visible yet.""" + + @dataclass(frozen=True) class RulesetTarget: """Describe one exact organization- or repository-owned ruleset.""" @@ -499,8 +503,15 @@ def _settle_ambiguous_recovery_history( target: RulesetTarget, *, current_version: int, + expected_payload: dict[str, Any], ) -> list[Any]: - """Wait one bounded client horizon for an ambiguous recovery PUT to appear.""" + """Observe the full bounded horizon until the ambiguous recovery PUT appears. + + An administrator version can become visible before a delayed recovery request. + That intervening version is evidence of concurrency, not evidence that the + delayed request was rejected. Settlement therefore waits for the exact restore + payload and lets the caller recover its actual immutable predecessor. + """ for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") @@ -508,8 +519,11 @@ def _settle_ambiguous_recovery_history( raise RulesetGovernanceError( "ambiguous ruleset recovery PUT exposed no history" ) - if _history_version_id(history[0]) != current_version: - return history + newest_version = _history_version_id(history[0]) + if newest_version != current_version: + newest_state = _history_version_state(target, newest_version) + if _editable_projection(newest_state) == expected_payload: + return history if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) raise RulesetGovernanceError( @@ -556,6 +570,7 @@ def _recover_displaced_history_state( history = _settle_ambiguous_recovery_history( target, current_version=current_version, + expected_payload=displaced_payload, ) recovery_version = _history_version_id(history[0]) if len(history) < 2: @@ -625,7 +640,9 @@ def _verify_ruleset_history_transition( newest_state = _history_version_state(target, newest_id) if _editable_projection(newest_state) != desired: - raise RulesetGovernanceError("latest ruleset history does not match reviewed mutation") + raise RulesetMutationStillSettlingError( + "latest ruleset history changed before the reviewed mutation became visible" + ) if predecessor_id == baseline_version: return @@ -652,9 +669,10 @@ def _confirm_ambiguous_put( A timeout, connection loss, nonzero transport result, or malformed successful response can occur after GitHub accepted the update. Three observations span - one additional full client timeout horizon: start, midpoint, and end. A - baseline-only first observation is therefore never represented as rejection. - Acceptance must become visible in immutable history and live state; collision + one additional full client timeout horizon: start, midpoint, and end. Neither + a baseline-only observation nor an intervening administrator version proves + rejection of the delayed reviewed PUT. Acceptance must become visible as the + exact reviewed payload in immutable history and live state; collision recovery follows the same predecessor contract. If no decisive transition is visible by the end of the bounded window, the run fails as unresolved and never retries the desired mutation blindly. @@ -674,7 +692,7 @@ def _confirm_ambiguous_put( desired, expected_main_sha=expected_main_sha, ) - except RulesetMutationNotVisibleError: + except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): live = _gh_api("GET", target.endpoint) _assert_target_provenance(live, target) if poll_index == AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: From 48b20bdd039d5cbf5f4d3fd63a82ad5c09e0cd92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:14:41 +0900 Subject: [PATCH 132/154] test(governance): regress ambiguous recovery runtime budget --- ...et_governance_runtime_budget_regression.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_ruleset_governance_runtime_budget_regression.py diff --git a/tests/test_ruleset_governance_runtime_budget_regression.py b/tests/test_ruleset_governance_runtime_budget_regression.py new file mode 100644 index 0000000000..ecd9accd19 --- /dev/null +++ b/tests/test_ruleset_governance_runtime_budget_regression.py @@ -0,0 +1,72 @@ +"""Regress the owner-plane runtime budget against ambiguous recovery request counts.""" + +from __future__ import annotations + +import importlib.util +import math +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" +WORKFLOW = ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml" + + +def load_module(): + """Load the production reconciler from the exact checkout.""" + + spec = importlib.util.spec_from_file_location("ruleset_governance_runtime_budget", SOURCE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_ambiguous_recovery_budget_counts_every_history_state_read() -> None: + """Every settlement poll may add one history-state GET and must be budgeted.""" + + module = load_module() + expected_recovery_operations = ( + module.RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT + + (module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1) + + module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS + ) + expected_blocking_operations = ( + module.BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET + + module.AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET + + module.COLLISION_RECOVERY_LIMIT * expected_recovery_operations + + module.POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET + + module.FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET + ) + expected_settlement_seconds = ( + module.AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + + module.COLLISION_RECOVERY_LIMIT + * module.AMBIGUOUS_WRITE_SETTLEMENT_WINDOW_SECONDS + ) + expected_seconds = 2 * ( + expected_blocking_operations * module.API_REQUEST_TIMEOUT_SECONDS + + expected_settlement_seconds + ) + + assert module.worst_case_apply_seconds(target_count=2) == expected_seconds + assert expected_seconds == 7_680 + + +def test_workflow_timeout_and_focused_gate_cover_runtime_budget_regression() -> None: + """The apply job and focused CI must cover the source-derived 128-minute bound.""" + + module = load_module() + workflow = WORKFLOW.read_text(encoding="utf-8") + regression = "tests/test_ruleset_governance_runtime_budget_regression.py" + assert workflow.count(f'"{regression}"') >= 2 + assert regression in workflow.split("python -m coverage run", 1)[1] + + apply_section = workflow.split(" apply:\n", 1)[1] + timeout_match = re.search(r"timeout-minutes:\s*(\d+)", apply_section) + assert timeout_match is not None + configured_minutes = int(timeout_match.group(1)) + required_minutes = math.ceil(module.worst_case_apply_seconds(target_count=2) / 60) + assert required_minutes == 128 + assert configured_minutes >= required_minutes From 63534b3737515acf4c6b5cd4ede23aadc061deee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:16:43 +0900 Subject: [PATCH 133/154] fix(governance): cover full recovery settlement runtime --- .github/workflows/ruleset-governance-reconcile.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index f611a670fb..a93295b719 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -12,6 +12,7 @@ on: - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_ruleset_governance_delayed_recovery_regression.py" + - "tests/test_ruleset_governance_runtime_budget_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -31,6 +32,7 @@ on: - "tests/test_ruleset_governance_review_round3.py" - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_ruleset_governance_delayed_recovery_regression.py" + - "tests/test_ruleset_governance_runtime_budget_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -113,6 +115,7 @@ jobs: tests/test_ruleset_governance_review_round3.py \ tests/test_ruleset_governance_timeout_regression.py \ tests/test_ruleset_governance_delayed_recovery_regression.py \ + tests/test_ruleset_governance_runtime_budget_regression.py \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_ruleset_audit_completeness_regression.py \ tests/test_ruleset_merge_method_shape_regression.py \ @@ -133,7 +136,7 @@ jobs: vars.CWL_RULESET_RECONCILE_ENABLED == 'true' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 120 + timeout-minutes: 130 environment: ruleset-governance-maintenance steps: - name: Harden runner From bfd82932b36be6bca8e2c12e017356cd634cbd90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:19:02 +0900 Subject: [PATCH 134/154] fix(governance): count recovery history reads in runtime budget --- scripts/ci/reconcile_ruleset_governance.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 1013424072..0ff5dfe334 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -56,8 +56,12 @@ BASE_MUTATION_BLOCKING_OPERATIONS_PER_TARGET = 7 AMBIGUOUS_SETTLEMENT_BLOCKING_OPERATIONS_PER_TARGET = 12 RECOVERY_BLOCKING_OPERATIONS_PER_ATTEMPT = 7 +# An ambiguous recovery settlement can add two extra history-list polls after +# the initial poll, and every one of the three polls can require a history-state +# GET when an intervening version is visible before the delayed restore appears. RECOVERY_AMBIGUOUS_EXTRA_BLOCKING_OPERATIONS_PER_ATTEMPT = ( - AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1 + (AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1) + + AMBIGUOUS_WRITE_SETTLEMENT_POLLS ) POST_CONFIRM_BLOCKING_OPERATIONS_PER_TARGET = 2 FINAL_VERIFY_BLOCKING_OPERATIONS_PER_TARGET = 2 From 1924225e60d10c814f9149e7dc7bf2c2791dae89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:24:09 +0900 Subject: [PATCH 135/154] test(governance): require full hosted-job recovery headroom --- .../test_ruleset_governance_runtime_budget_regression.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_ruleset_governance_runtime_budget_regression.py b/tests/test_ruleset_governance_runtime_budget_regression.py index ecd9accd19..7dd6bcb1e6 100644 --- a/tests/test_ruleset_governance_runtime_budget_regression.py +++ b/tests/test_ruleset_governance_runtime_budget_regression.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" WORKFLOW = ROOT / ".github" / "workflows" / "ruleset-governance-reconcile.yml" +GITHUB_HOSTED_JOB_EXECUTION_LIMIT_MINUTES = 360 def load_module(): @@ -54,8 +55,8 @@ def test_ambiguous_recovery_budget_counts_every_history_state_read() -> None: assert expected_seconds == 7_680 -def test_workflow_timeout_and_focused_gate_cover_runtime_budget_regression() -> None: - """The apply job and focused CI must cover the source-derived 128-minute bound.""" +def test_workflow_uses_full_hosted_job_limit_for_critical_section_headroom() -> None: + """Do not self-terminate before GitHub's hosted-runner execution hard limit.""" module = load_module() workflow = WORKFLOW.read_text(encoding="utf-8") @@ -69,4 +70,5 @@ def test_workflow_timeout_and_focused_gate_cover_runtime_budget_regression() -> configured_minutes = int(timeout_match.group(1)) required_minutes = math.ceil(module.worst_case_apply_seconds(target_count=2) / 60) assert required_minutes == 128 - assert configured_minutes >= required_minutes + assert configured_minutes == GITHUB_HOSTED_JOB_EXECUTION_LIMIT_MINUTES + assert configured_minutes - required_minutes == 232 From 2895cf980b4ecd448063feadf53332ad6923c92c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:24:43 +0900 Subject: [PATCH 136/154] fix(governance): reserve full hosted-runner recovery window --- .github/workflows/ruleset-governance-reconcile.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index a93295b719..712cd35651 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -136,7 +136,9 @@ jobs: vars.CWL_RULESET_RECONCILE_ENABLED == 'true' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 130 + # Use the full documented GitHub-hosted job execution limit so this workflow + # cannot impose an earlier timeout on the 128-minute mutation/recovery bound. + timeout-minutes: 360 environment: ruleset-governance-maintenance steps: - name: Harden runner From 60bf2cb4348651e2f62a8f547ca77d386d529ecd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:25:26 +0900 Subject: [PATCH 137/154] docs(governance): align runtime record with hosted limit --- docs/doctoring/ruleset-owner-plane-reconciliation.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index 9685253f51..1446288822 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -31,7 +31,7 @@ The privileged apply path records the latest history version before its final li Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, during ambiguous-result settlement, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. -The Actions timeout is derived from the implementation rather than chosen independently. For each target the source budgets the normal guarded mutation operations, three ambiguous-initial-write settlement observations, all eight seven-operation collision-recovery attempts, the two additional history reads that each ambiguous recovery may require, post-confirmation checks, the final verify-only pass, the initial 30-second settlement horizon, and up to one 30-second recovery-settlement horizon per recovery attempt. At two reviewed targets this conservative source-derived budget is 6,240 seconds (104 minutes). The privileged `apply` job reserves 120 minutes, so Actions cannot reach its configured timeout before that modeled worst-case critical section. The regression suite computes the required minimum directly from `worst_case_apply_seconds(target_count=2)` and compares it with the workflow value. +The Actions runtime contract now separates the source-derived reconciliation critical section from platform bootstrap/teardown time instead of pretending those are the same bound. For each target the source budgets the normal guarded mutation operations, three ambiguous-initial-write settlement observations, all eight seven-operation collision-recovery attempts, the two additional history-list polls **and all three possible history-state reads** that each ambiguous recovery settlement can add, post-confirmation checks, the final verify-only pass, the initial 30-second settlement horizon, and up to one 30-second recovery-settlement horizon per recovery attempt. At two reviewed targets this conservative source-derived critical-section budget is **7,680 seconds (128 minutes)**. GitHub's current hosted-runner limits document a six-hour job-execution ceiling and `timeout-minutes` supports 360 minutes. The privileged `apply` job therefore uses the full 360-minute hosted-runner limit rather than imposing a smaller self-cancellation boundary. That leaves 232 minutes between the modeled 128-minute reconciliation bound and the platform hard limit for runner hardening, checkout, Python setup, credential validation, process startup, post-run work, and other platform-controlled execution overhead without falsely claiming those external actions have a mathematically bounded duration. The regression suite derives the 128-minute critical section from `worst_case_apply_seconds(target_count=2)` and separately enforces the 360-minute platform-limit workflow contract. The hourly schedule remains queue-conscious while making disabled state visible. Source, manifest, runtime-auditor, and regression changes are validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the expensive validation/apply path is skipped, but a two-minute `ubuntu-slim` status job writes `Ruleset reconciliation is disabled` to the workflow summary so operators can distinguish deliberate disablement from a missing run. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. @@ -43,7 +43,7 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Verification sequence -1. PR validation proves exact privileged-target pinning, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous timeout and non-timeout PUT settlement, delayed history visibility, bounded unresolved settlement, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, runtime-auditor path coverage, derived job-timeout coverage, visible disabled-state reporting, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves exact privileged-target pinning, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous timeout and non-timeout PUT settlement, delayed history visibility, bounded unresolved settlement, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, runtime-auditor path coverage, derived critical-section and hosted-job-limit coverage, visible disabled-state reporting, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. 3. Run the reconciler and require one exact new ruleset-history version per uncontended target whose predecessor is the sampled pre-write baseline. If an intervening version exists, require recovery to follow immutable predecessor evidence until the newest displaced administrator state is restored or fail closed without claiming success. 4. Re-read both complete live ruleset payloads using the independently authorized credential. @@ -54,10 +54,12 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Standards and research basis -GitHub's current REST documentation makes ruleset mutation and ruleset-history access Administration-authorized operations and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. Its REST best-practices guidance distinguishes safe conditional `GET` requests from unsafe methods and states that conditional `POST`/`PUT`/`PATCH`/`DELETE` requests are unsupported unless the endpoint explicitly says otherwise; the current ruleset update documentation does not provide a conditional-write precondition. The history endpoints provide the exact version sequence and version state used here to detect an otherwise invisible intervening update. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. +GitHub's current REST documentation makes ruleset mutation and ruleset-history access Administration-authorized operations and describes bypass actors as explicit ruleset state; this is why the repair is placed in a separate owner plane instead of expanding the ordinary repository metadata token. GitHub also documents that organization owners or users with the dedicated organization-rules permission manage organization rulesets. Its REST best-practices guidance distinguishes safe conditional `GET` requests from unsafe methods and states that conditional `POST`/`PUT`/`PATCH`/`DELETE` requests are unsupported unless the endpoint explicitly says otherwise; the current ruleset update documentation does not provide a conditional-write precondition. The history endpoints provide the exact version sequence and version state used here to detect an otherwise invisible intervening update. GitHub's current Actions documentation gives GitHub-hosted jobs a six-hour execution limit; the owner-plane workflow deliberately uses that platform ceiling so its own timeout cannot pre-empt the source-derived 128-minute recovery critical section. NIST SP 800-53 Rev. 5 AC-6 supports least privilege, while CM-3 and its testing/validation enhancement support controlled, reviewed, verified configuration changes. Recent systematic-review evidence likewise identifies automated security controls, compliance-as-code, continuous feedback, access control, and protected credentials as central DevSecOps practices rather than relying on informal operator steps. ### References (APA 7th) +GitHub. (2026). *Actions limits*. GitHub Docs. https://docs.github.com/en/actions/reference/limits + GitHub. (2026). *Best practices for using the REST API*. GitHub Docs. https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api GitHub. (2026). *REST API endpoints for rules: Organizations*. GitHub Docs. https://docs.github.com/en/rest/orgs/rules @@ -66,6 +68,8 @@ GitHub. (2026). *REST API endpoints for rules: Repositories*. GitHub Docs. https GitHub. (2026). *Managing rulesets for repositories in your organization*. GitHub Docs. https://docs.github.com/en/organizations/managing-organization-settings/managing-rulesets-for-repositories-in-your-organization +GitHub. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-53r5 Sinan, M., Shahin, M., & Gondal, I. (2025). Integrating security controls in DevSecOps: Challenges, solutions, and future research directions. *Journal of Software: Evolution and Process, 37*, e70029. https://doi.org/10.1002/smr.70029 From 7350611f7f076c0cfb36fb09fa1efa3f2c140f66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:33:44 +0900 Subject: [PATCH 138/154] fix(governance): preserve fail-closed settlement diagnostics --- scripts/ci/reconcile_ruleset_governance.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index 0ff5dfe334..e07c652a33 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -517,6 +517,7 @@ def _settle_ambiguous_recovery_history( payload and lets the caller recover its actual immutable predecessor. """ + observed_unexpected_newer_state = False for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=2") if not history: @@ -528,8 +529,13 @@ def _settle_ambiguous_recovery_history( newest_state = _history_version_state(target, newest_version) if _editable_projection(newest_state) == expected_payload: return history + observed_unexpected_newer_state = True if poll_index < AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1: time.sleep(AMBIGUOUS_WRITE_SETTLEMENT_INTERVAL_SECONDS) + if observed_unexpected_newer_state: + raise RulesetGovernanceError( + "ambiguous ruleset recovery PUT left a newer state after settlement window; refusing overwrite" + ) raise RulesetGovernanceError( "ambiguous ruleset recovery PUT outcome remains unresolved after settlement window" ) @@ -645,7 +651,7 @@ def _verify_ruleset_history_transition( newest_state = _history_version_state(target, newest_id) if _editable_projection(newest_state) != desired: raise RulesetMutationStillSettlingError( - "latest ruleset history changed before the reviewed mutation became visible" + "latest ruleset history does not match reviewed mutation; history changed before the reviewed mutation became visible" ) if predecessor_id == baseline_version: return From fd8a31556303d1268686989848bf752bc77b4b43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:36:29 +0900 Subject: [PATCH 139/154] test(governance): regress post-PUT stale-main cleanup --- ..._governance_post_put_cleanup_regression.py | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 tests/test_ruleset_governance_post_put_cleanup_regression.py diff --git a/tests/test_ruleset_governance_post_put_cleanup_regression.py b/tests/test_ruleset_governance_post_put_cleanup_regression.py new file mode 100644 index 0000000000..c61ff6bd4e --- /dev/null +++ b/tests/test_ruleset_governance_post_put_cleanup_regression.py @@ -0,0 +1,201 @@ +"""Regress post-PUT cleanup when protected main advances after mutation starts.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "scripts" / "ci" / "reconcile_ruleset_governance.py" + + +def load_module(): + """Load the production reconciler from the exact checkout.""" + + spec = importlib.util.spec_from_file_location("ruleset_governance_post_put_cleanup", SOURCE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def repository_target(module): + """Return the exact central repository ruleset target.""" + + return module.RulesetTarget( + scope="repository", + owner="ContextualWisdomLab", + repository=".github", + ruleset_id=17921150, + name="Lock default branch", + ) + + +def live_payload() -> dict: + """Return one live-shaped repository ruleset with reviewed governance drift.""" + + return { + "id": 17921150, + "name": "Lock default branch", + "target": "branch", + "source_type": "Repository", + "source": "ContextualWisdomLab/.github", + "enforcement": "active", + "bypass_actors": [ + {"actor_id": None, "actor_type": "OrganizationAdmin", "bypass_mode": "always"} + ], + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": True, + "required_reviewers": [], + "require_code_owner_review": False, + "require_last_push_approval": False, + "required_review_thread_resolution": True, + "allowed_merge_methods": ["merge", "squash", "rebase"], + }, + }, + ], + } + + +def test_ambiguous_put_settles_before_stale_main_failure(monkeypatch) -> None: + """Main advancement after PUT cannot abort immutable-history settlement.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + main_checks = 0 + history_reads = 0 + get_count = 0 + put_bodies: list[dict] = [] + + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_latest_history_version", lambda *_args: 41) + monkeypatch.setattr(module, "_history_version_state", lambda *_args: converged) + + def assert_main(_expected_sha: str) -> None: + nonlocal main_checks + main_checks += 1 + if main_checks > 2: + raise module.RulesetGovernanceError("protected main advanced") + + def history(*_args): + nonlocal history_reads + history_reads += 1 + return [{"version_id": 42}, {"version_id": 41}] + + def api(method, endpoint, *, body=None): + nonlocal get_count + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + return first if get_count <= 2 else converged + if method == "PUT" and endpoint == target.endpoint: + assert body is not None + put_bodies.append(body) + raise module.AmbiguousRulesetWriteError("accepted before transport loss") + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_assert_current_main", assert_main) + monkeypatch.setattr(module, "_gh_api_list", history) + monkeypatch.setattr(module, "_gh_api", api) + + with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): + module._reconcile_target( + target, + verify_only=False, + expected_main_sha="a" * 40, + ) + + assert history_reads == 1 + assert main_checks == 3 + assert put_bodies == [desired] + + +def test_ambiguous_put_restores_displaced_admin_after_main_advances(monkeypatch) -> None: + """Cleanup restores the displaced predecessor even after source freshness changes.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + administrator = { + **first, + "name": "Administrator intervening state", + "enforcement": "evaluate", + } + main_checks = 0 + get_count = 0 + history_reads = 0 + put_bodies: list[dict] = [] + + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_latest_history_version", lambda *_args: 41) + + def assert_main(_expected_sha: str) -> None: + nonlocal main_checks + main_checks += 1 + if main_checks > 2: + raise module.RulesetGovernanceError("protected main advanced") + + def history(*_args): + nonlocal history_reads + history_reads += 1 + if history_reads == 1: + return [ + {"version_id": 43}, + {"version_id": 42}, + {"version_id": 41}, + ] + return [{"version_id": 44}, {"version_id": 43}] + + def history_state(_target, version_id): + return {42: administrator, 43: converged, 44: administrator}[version_id] + + def api(method, endpoint, *, body=None): + nonlocal get_count + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + if get_count <= 2: + return first + if get_count == 3: + return converged + return administrator + if method == "PUT" and endpoint == target.endpoint: + assert body is not None + put_bodies.append(body) + if len(put_bodies) == 1: + raise module.AmbiguousRulesetWriteError("accepted before transport loss") + return {} + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_assert_current_main", assert_main) + monkeypatch.setattr(module, "_gh_api_list", history) + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr(module, "_gh_api", api) + + with pytest.raises( + module.RulesetGovernanceError, + match="restored newest displaced administrator state", + ): + module._reconcile_target( + target, + verify_only=False, + expected_main_sha="a" * 40, + ) + + assert main_checks == 2 + assert history_reads == 2 + assert put_bodies == [desired, module._editable_projection(administrator)] From f0abca0dbc3ad1158e3841a86ffec47f07e2c61d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:47:59 +0900 Subject: [PATCH 140/154] test(governance): gate post-PUT cleanup regression --- .github/workflows/ruleset-governance-reconcile.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ruleset-governance-reconcile.yml b/.github/workflows/ruleset-governance-reconcile.yml index 712cd35651..a9a409536f 100644 --- a/.github/workflows/ruleset-governance-reconcile.yml +++ b/.github/workflows/ruleset-governance-reconcile.yml @@ -13,6 +13,7 @@ on: - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_ruleset_governance_delayed_recovery_regression.py" - "tests/test_ruleset_governance_runtime_budget_regression.py" + - "tests/test_ruleset_governance_post_put_cleanup_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -33,6 +34,7 @@ on: - "tests/test_ruleset_governance_timeout_regression.py" - "tests/test_ruleset_governance_delayed_recovery_regression.py" - "tests/test_ruleset_governance_runtime_budget_regression.py" + - "tests/test_ruleset_governance_post_put_cleanup_regression.py" - "tests/test_central_required_workflow_ruleset_audit.py" - "tests/test_ruleset_audit_completeness_regression.py" - "tests/test_ruleset_merge_method_shape_regression.py" @@ -116,6 +118,7 @@ jobs: tests/test_ruleset_governance_timeout_regression.py \ tests/test_ruleset_governance_delayed_recovery_regression.py \ tests/test_ruleset_governance_runtime_budget_regression.py \ + tests/test_ruleset_governance_post_put_cleanup_regression.py \ tests/test_central_required_workflow_ruleset_audit.py \ tests/test_ruleset_audit_completeness_regression.py \ tests/test_ruleset_merge_method_shape_regression.py \ From bc8f758670552b37adbe160ffa4dfbda7ac4d13f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:50:38 +0900 Subject: [PATCH 141/154] fix(governance): finish post-PUT collision cleanup --- scripts/ci/reconcile_ruleset_governance.py | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/scripts/ci/reconcile_ruleset_governance.py b/scripts/ci/reconcile_ruleset_governance.py index e07c652a33..1f8eb7d61e 100644 --- a/scripts/ci/reconcile_ruleset_governance.py +++ b/scripts/ci/reconcile_ruleset_governance.py @@ -9,12 +9,14 @@ the exact protected-main revision, serialized owner-plane execution, and the immutable ruleset-history surface. If history proves a hidden pre-PUT edit was overwritten, the reconciler restores the newest displaced administrator state -before failing; recovery re-checks immutable history and protected-main identity -before every recovery write. Ambiguous mutation and recovery results are settled -from live state plus immutable history instead of being treated as ordinary -request failures or retried blindly. The canonical audit is executed against -projected and live state so this narrow reconciler never reports broader policy -drift as converged. +before failing. Protected-main freshness gates every new reviewed mutation; +once a PUT has been issued, immutable-history settlement and compensating +collision recovery finish even if protected main advances so an already-issued +write cannot strand an overwritten administrator state. Ambiguous mutation and +recovery results are settled from live state plus immutable history instead of +being treated as ordinary request failures or retried blindly. The canonical +audit is executed against projected and live state so this narrow reconciler +never reports broader policy drift as converged. """ from __future__ import annotations @@ -555,11 +557,13 @@ def _recover_displaced_history_state( verifies immutable history immediately afterward. If an administrator write slipped between the recovery GET and PUT, that displaced history version becomes the next recovery target. A newer live state observed before a - recovery write is never overwritten. Privileged recovery revalidates the - reviewed protected-main SHA immediately before every PUT. Ambiguous recovery - results settle across one complete client-timeout horizon before any later - recovery write is considered, so a delayed request cannot be duplicated. - The recovery chain remains bounded and fails closed. + recovery write is never overwritten. When ``expected_main_sha`` is supplied, + recovery revalidates protected main before every PUT; post-write compensation + deliberately omits that source-freshness guard because its sole purpose is to + undo state displaced by an already-issued write using immutable predecessor + evidence. Ambiguous recovery results settle across one complete client-timeout + horizon before any later recovery write is considered, so a delayed request + cannot be duplicated. The recovery chain remains bounded and fails closed. """ for _attempt in range(COLLISION_RECOVERY_LIMIT): @@ -638,6 +642,9 @@ def _verify_ruleset_history_transition( predecessor must be the version sampled before the final live read. If a version intervened, recovery follows immutable history and verifies every restore write so a second administrator edit cannot be silently overwritten. + ``expected_main_sha`` guards recovery only when the caller is still before a + new mutation boundary; callers validating an already-issued PUT omit it so + compensating restoration cannot be stranded by a later main advance. """ history = _gh_api_list("GET", f"{target.history_endpoint}?per_page=3") @@ -683,9 +690,12 @@ def _confirm_ambiguous_put( a baseline-only observation nor an intervening administrator version proves rejection of the delayed reviewed PUT. Acceptance must become visible as the exact reviewed payload in immutable history and live state; collision - recovery follows the same predecessor contract. If no decisive transition is - visible by the end of the bounded window, the run fails as unresolved and - never retries the desired mutation blindly. + recovery follows the same predecessor contract. Once the PUT is in flight, + settlement and any compensating restoration finish even when protected main + advances; source freshness is checked only after a clean, collision-free + settlement. If no decisive transition is visible by the end of the bounded + window, the run fails as unresolved and never retries the desired mutation + blindly. """ if expected_main_sha is None: @@ -694,13 +704,12 @@ def _confirm_ambiguous_put( ) for poll_index in range(AMBIGUOUS_WRITE_SETTLEMENT_POLLS): - _assert_current_main(expected_main_sha) try: _verify_ruleset_history_transition( target, baseline_version, desired, - expected_main_sha=expected_main_sha, + expected_main_sha=None, ) except (RulesetMutationNotVisibleError, RulesetMutationStillSettlingError): live = _gh_api("GET", target.endpoint) @@ -783,11 +792,14 @@ def _reconcile_target( raise RulesetGovernanceError(f"{target.scope} ruleset did not converge") _assert_canonical_governance(after, target) if baseline_version is not None and not history_verified: + # The write already happened. History verification may need to compensate + # a displaced administrator version even if protected main advanced in the + # meantime; final source freshness is checked only after clean settlement. _verify_ruleset_history_transition( target, baseline_version, desired, - expected_main_sha=expected_main_sha, + expected_main_sha=None, ) if expected_main_sha is not None: _assert_current_main(expected_main_sha) From cf61c75101bdc19c75a3985cadc54122b6ce2c4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:51:48 +0900 Subject: [PATCH 142/154] test(governance): cover successful post-PUT cleanup --- ..._governance_post_put_cleanup_regression.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/test_ruleset_governance_post_put_cleanup_regression.py b/tests/test_ruleset_governance_post_put_cleanup_regression.py index c61ff6bd4e..d5f7052124 100644 --- a/tests/test_ruleset_governance_post_put_cleanup_regression.py +++ b/tests/test_ruleset_governance_post_put_cleanup_regression.py @@ -199,3 +199,79 @@ def api(method, endpoint, *, body=None): assert main_checks == 2 assert history_reads == 2 assert put_bodies == [desired, module._editable_projection(administrator)] + + +def test_successful_put_restores_displaced_admin_after_main_advances(monkeypatch) -> None: + """A successful PUT also finishes predecessor restoration before stale-main failure.""" + + module = load_module() + target = repository_target(module) + first = live_payload() + desired = module._desired_payload(first, target) + converged = {**first, **desired} + administrator = { + **first, + "name": "Administrator intervening state", + "enforcement": "evaluate", + } + main_checks = 0 + get_count = 0 + history_reads = 0 + put_bodies: list[dict] = [] + + monkeypatch.setattr(module, "_assert_canonical_governance", lambda *_args: None) + monkeypatch.setattr(module, "_latest_history_version", lambda *_args: 41) + + def assert_main(_expected_sha: str) -> None: + nonlocal main_checks + main_checks += 1 + if main_checks > 2: + raise module.RulesetGovernanceError("protected main advanced") + + def history(*_args): + nonlocal history_reads + history_reads += 1 + if history_reads == 1: + return [ + {"version_id": 43}, + {"version_id": 42}, + {"version_id": 41}, + ] + return [{"version_id": 44}, {"version_id": 43}] + + def history_state(_target, version_id): + return {42: administrator, 43: converged, 44: administrator}[version_id] + + def api(method, endpoint, *, body=None): + nonlocal get_count + if method == "GET" and endpoint == target.endpoint: + get_count += 1 + if get_count <= 2: + return first + if get_count <= 4: + return converged + return administrator + if method == "PUT" and endpoint == target.endpoint: + assert body is not None + put_bodies.append(body) + return {} + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_assert_current_main", assert_main) + monkeypatch.setattr(module, "_gh_api_list", history) + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr(module, "_gh_api", api) + + with pytest.raises( + module.RulesetGovernanceError, + match="restored newest displaced administrator state", + ): + module._reconcile_target( + target, + verify_only=False, + expected_main_sha="a" * 40, + ) + + assert main_checks == 2 + assert history_reads == 2 + assert put_bodies == [desired, module._editable_projection(administrator)] From 1ca11b41cffb3763aaa04b2614ee15b06a9609d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:53:50 +0900 Subject: [PATCH 143/154] docs(governance): record post-PUT compensation boundary --- docs/doctoring/ruleset-owner-plane-reconciliation.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/ruleset-owner-plane-reconciliation.md b/docs/doctoring/ruleset-owner-plane-reconciliation.md index 1446288822..3e746d0671 100644 --- a/docs/doctoring/ruleset-owner-plane-reconciliation.md +++ b/docs/doctoring/ruleset-owner-plane-reconciliation.md @@ -29,13 +29,15 @@ Recovery writes use the same ambiguity principle. After a recovery PUT has an am The privileged apply path records the latest history version before its final live-state read. After PUT, it requires the newest history state to equal the reviewed body and requires that version's immediate predecessor to be the recorded baseline. If another administrator version intervened between the baseline sample and our PUT, the reconciler has proof that its write displaced a newer administrator state. Recovery re-reads live state before every recovery PUT and refuses to overwrite it if it has already advanced. After each recovery PUT, immutable history is read again: the newest version must equal the restore body, and its immediate predecessor must be the version that recovery intended to replace. If a second administrator version slipped between the recovery GET and PUT, that newly displaced predecessor becomes the next recovery target. The bounded recovery chain therefore restores the newest displaced administrator state rather than silently losing it; ambiguous or non-convergent histories fail closed. -Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA; live `main` is checked before the final ruleset read, immediately before each PUT, during ambiguous-result settlement, and after convergence/history verification. If protected `main` advances, the stale run fails closed. Read-only `--verify-only` remains available without a mutation SHA guard. +Protected-main freshness and post-write compensation have deliberately different boundaries. The exact protected `main` SHA is checked before history sampling and again immediately before the **new reviewed PUT**, so a stale source revision cannot start another desired mutation. Once that PUT has been issued, however, source freshness can no longer safely cancel settlement: the server may already have accepted the request and may have displaced an administrator version. Immutable-history settlement therefore finishes even if protected `main` advances, and any compensating recovery needed to restore the exact displaced predecessor also completes without a stale-main veto. A clean, collision-free settlement then re-checks protected `main` and fails the stale run rather than reporting convergence. The same compensation boundary applies after a transport-confirmed successful PUT: history verification may restore a displaced administrator predecessor before the final stale-main failure. This preserves both source freshness for **new** mutations and lossless cleanup for an **already-issued** mutation. + +Because the API still cannot make the final GET-to-PUT interval atomic, privileged mutation is serialized in one shared non-PR owner-plane concurrency group **without cancellation**. A run must finish its PUT plus immutable-history verification/recovery critical section; cancelling a predecessor after PUT could strand an unverified overwrite. Pull-request validation may supersede older pull-request validation runs because those runs are read-only. Every mutation, including an operator-invoked manual execution, must supply the exact protected `main` SHA. Read-only `--verify-only` remains available without a mutation SHA guard. The Actions runtime contract now separates the source-derived reconciliation critical section from platform bootstrap/teardown time instead of pretending those are the same bound. For each target the source budgets the normal guarded mutation operations, three ambiguous-initial-write settlement observations, all eight seven-operation collision-recovery attempts, the two additional history-list polls **and all three possible history-state reads** that each ambiguous recovery settlement can add, post-confirmation checks, the final verify-only pass, the initial 30-second settlement horizon, and up to one 30-second recovery-settlement horizon per recovery attempt. At two reviewed targets this conservative source-derived critical-section budget is **7,680 seconds (128 minutes)**. GitHub's current hosted-runner limits document a six-hour job-execution ceiling and `timeout-minutes` supports 360 minutes. The privileged `apply` job therefore uses the full 360-minute hosted-runner limit rather than imposing a smaller self-cancellation boundary. That leaves 232 minutes between the modeled 128-minute reconciliation bound and the platform hard limit for runner hardening, checkout, Python setup, credential validation, process startup, post-run work, and other platform-controlled execution overhead without falsely claiming those external actions have a mathematically bounded duration. The regression suite derives the 128-minute critical section from `worst_case_apply_seconds(target_count=2)` and separately enforces the 360-minute platform-limit workflow contract. The hourly schedule remains queue-conscious while making disabled state visible. Source, manifest, runtime-auditor, and regression changes are validated on pull-request, push, and manual triggers. When `CWL_RULESET_RECONCILE_ENABLED` is false, the expensive validation/apply path is skipped, but a two-minute `ubuntu-slim` status job writes `Ruleset reconciliation is disabled` to the workflow summary so operators can distinguish deliberate disablement from a missing run. When privileged reconciliation is enabled, the scheduled path performs the full reviewed validation before mutation. -Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. The focused workflow now includes `scripts/ci/audit_central_required_workflows.py` itself in both PR and protected-main path filters because that auditor executes inside the reconciliation process; auditor-only changes therefore cannot bypass the owner-plane regression gate. +Repository-local strengthening is attempted before the organization change so a cross-scope partial failure cannot first weaken the central repository surface. The focused workflow now includes `scripts/ci/audit_central_required_workflows.py` itself in both PR and protected-main path filters because that auditor executes inside the reconciliation process; auditor-only changes therefore cannot bypass the owner-plane regression gate. The same focused gate includes `tests/test_ruleset_governance_post_put_cleanup_regression.py` in both PR/push path filters and in the executed pytest set so the protected-main-advance compensation contract cannot exist as an unexecuted test artifact. Pull requests execute only offline manifest validation plus 100% statement/branch/docstring contract tests. Mutation can run only from trusted `main`, in the `ruleset-governance-maintenance` protected environment, when repository variable `CWL_RULESET_RECONCILE_ENABLED=true` is explicitly set, using dedicated secret `CWL_RULESET_ADMIN_TOKEN`. The normal workflow token remains `contents: read`, checkout credentials are not persisted, and API errors never echo subprocess output that could contain sensitive context. @@ -43,7 +45,7 @@ Pull requests execute only offline manifest validation plus 100% statement/branc ## Verification sequence -1. PR validation proves exact privileged-target pinning, mutation projection, pre-write drift refusal, stale-protected-main refusal, ruleset-history collision detection/recovery, ambiguous timeout and non-timeout PUT settlement, delayed history visibility, bounded unresolved settlement, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, runtime-auditor path coverage, derived critical-section and hosted-job-limit coverage, visible disabled-state reporting, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. +1. PR validation proves exact privileged-target pinning, mutation projection, pre-write drift refusal, stale-protected-main refusal before a new desired mutation, post-PUT settlement despite later protected-main advancement, displaced-administrator compensation for ambiguous and transport-confirmed PUTs, ruleset-history collision detection/recovery, ambiguous timeout and non-timeout PUT settlement, delayed history visibility, bounded unresolved settlement, historical editable-identity recovery, the second recovery GET-to-PUT race, redacted failures, post-write convergence, exact target provenance, guarded manual mutation, runtime-auditor path coverage, derived critical-section and hosted-job-limit coverage, visible disabled-state reporting, trusted-main workflow gating, and 100% owned statement/branch/docstring coverage. 2. After source reaches protected `main`, provision the protected environment and dedicated credential, then enable the repository variable only for an exclusive owner-plane maintenance interval. 3. Run the reconciler and require one exact new ruleset-history version per uncontended target whose predecessor is the sampled pre-write baseline. If an intervening version exists, require recovery to follow immutable predecessor evidence until the newest displaced administrator state is restored or fail closed without claiming success. 4. Re-read both complete live ruleset payloads using the independently authorized credential. From cd0a299de42fa2f026aaeced213fbe81a66b2a13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:01:18 +0900 Subject: [PATCH 144/154] chore(governance): stage timeout-fixture source fix --- .../_temp_pr1644_timeout_fixture_fix.yml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/_temp_pr1644_timeout_fixture_fix.yml diff --git a/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml b/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml new file mode 100644 index 0000000000..32c536c7e3 --- /dev/null +++ b/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml @@ -0,0 +1,52 @@ +name: Temporary PR1644 Timeout Fixture Fix + +on: + push: + branches: + - fix/ruleset-owner-plane-reconciler + +permissions: + contents: write + +concurrency: + group: temp-pr1644-timeout-fixture-fix + cancel-in-progress: false + +jobs: + fix-and-remove-self: + runs-on: ubuntu-slim + timeout-minutes: 5 + steps: + - name: Check out current writer branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: fix/ruleset-owner-plane-reconciler + fetch-depth: 1 + - name: Apply exact regression repair and remove temporary workflow + shell: bash + run: | + set -euo pipefail + git fetch origin fix/ruleset-owner-plane-reconciler --depth=1 + git reset --hard origin/fix/ruleset-owner-plane-reconciler + python - <<'PY' + from pathlib import Path + + path = Path("tests/test_ruleset_governance_timeout_regression.py") + text = path.read_text(encoding="utf-8") + needle = ' assert expected_main_sha == "a" * 40\n' + replacement = ( + ' # Once a PUT is issued, history settlement must finish without a stale-main veto.\n' + ' assert expected_main_sha is None\n' + ) + if text.count(needle) != 1: + raise SystemExit("expected exactly one stale post-PUT main-SHA assertion") + path.write_text(text.replace(needle, replacement), encoding="utf-8") + Path(".github/workflows/_temp_pr1644_timeout_fixture_fix.yml").unlink() + PY + git diff --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add tests/test_ruleset_governance_timeout_regression.py .github/workflows/_temp_pr1644_timeout_fixture_fix.yml + git diff --cached --quiet && { echo "expected source-fix diff is empty" >&2; exit 1; } + git commit -m "test(governance): align ambiguous PUT settlement fixture" + git push origin HEAD:fix/ruleset-owner-plane-reconciler From 9509748678eb2a7033af126ec8645001666b3d41 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 04:02:10 +0000 Subject: [PATCH 145/154] test(governance): align ambiguous PUT settlement fixture --- .../_temp_pr1644_timeout_fixture_fix.yml | 52 ------------------- ...t_ruleset_governance_timeout_regression.py | 3 +- 2 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/_temp_pr1644_timeout_fixture_fix.yml diff --git a/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml b/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml deleted file mode 100644 index 32c536c7e3..0000000000 --- a/.github/workflows/_temp_pr1644_timeout_fixture_fix.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Temporary PR1644 Timeout Fixture Fix - -on: - push: - branches: - - fix/ruleset-owner-plane-reconciler - -permissions: - contents: write - -concurrency: - group: temp-pr1644-timeout-fixture-fix - cancel-in-progress: false - -jobs: - fix-and-remove-self: - runs-on: ubuntu-slim - timeout-minutes: 5 - steps: - - name: Check out current writer branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: fix/ruleset-owner-plane-reconciler - fetch-depth: 1 - - name: Apply exact regression repair and remove temporary workflow - shell: bash - run: | - set -euo pipefail - git fetch origin fix/ruleset-owner-plane-reconciler --depth=1 - git reset --hard origin/fix/ruleset-owner-plane-reconciler - python - <<'PY' - from pathlib import Path - - path = Path("tests/test_ruleset_governance_timeout_regression.py") - text = path.read_text(encoding="utf-8") - needle = ' assert expected_main_sha == "a" * 40\n' - replacement = ( - ' # Once a PUT is issued, history settlement must finish without a stale-main veto.\n' - ' assert expected_main_sha is None\n' - ) - if text.count(needle) != 1: - raise SystemExit("expected exactly one stale post-PUT main-SHA assertion") - path.write_text(text.replace(needle, replacement), encoding="utf-8") - Path(".github/workflows/_temp_pr1644_timeout_fixture_fix.yml").unlink() - PY - git diff --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add tests/test_ruleset_governance_timeout_regression.py .github/workflows/_temp_pr1644_timeout_fixture_fix.yml - git diff --cached --quiet && { echo "expected source-fix diff is empty" >&2; exit 1; } - git commit -m "test(governance): align ambiguous PUT settlement fixture" - git push origin HEAD:fix/ruleset-owner-plane-reconciler diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py index f870c58926..f556327497 100644 --- a/tests/test_ruleset_governance_timeout_regression.py +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -107,7 +107,8 @@ def verify_history( assert seen_target == target assert baseline_version == 41 assert seen_desired == desired - assert expected_main_sha == "a" * 40 + # Once a PUT is issued, history settlement must finish without a stale-main veto. + assert expected_main_sha is None if history_outcome == "not-accepted": raise module.RulesetGovernanceError("ruleset mutation is not visible in history") if history_outcome == "collision": From 6e92cf9cd446f003096a9b4dc5ec5e398efa78e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:12:50 +0900 Subject: [PATCH 146/154] ci(governance): run one-shot review regression repair --- .github/workflows/_temp_pr1644_review_fix.yml | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 .github/workflows/_temp_pr1644_review_fix.yml diff --git a/.github/workflows/_temp_pr1644_review_fix.yml b/.github/workflows/_temp_pr1644_review_fix.yml new file mode 100644 index 0000000000..4128080473 --- /dev/null +++ b/.github/workflows/_temp_pr1644_review_fix.yml @@ -0,0 +1,179 @@ +name: PR 1644 one-shot review regression repair + +on: + push: + branches: + - fix/ruleset-owner-plane-reconciler + paths: + - ".github/workflows/_temp_pr1644_review_fix.yml" + +permissions: + contents: write + +concurrency: + group: pr1644-one-shot-review-regression-repair + cancel-in-progress: false + +jobs: + repair: + runs-on: ubuntu-slim + timeout-minutes: 10 + steps: + - name: Check out writer branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: fix/ruleset-owner-plane-reconciler + fetch-depth: 0 + - name: Apply current review regressions and retire this workflow + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin fix/ruleset-owner-plane-reconciler + git reset --hard origin/fix/ruleset-owner-plane-reconciler + python - <<'PY' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + if new in text: + return + if text.count(old) != 1: + raise SystemExit(f"expected one current contract site in {path}: {old!r}") + file_path.write_text(text.replace(old, new, 1), encoding="utf-8") + + replace_once( + "tests/test_ruleset_governance_review_regressions.py", + 'with pytest.raises(module.RulesetGovernanceError, match="does not match reviewed mutation"):', + 'with pytest.raises(\n module.RulesetMutationStillSettlingError,\n match="latest ruleset history changed before the reviewed mutation became visible",\n ):', + ) + replace_once( + "tests/test_ruleset_governance_timeout_regression.py", + 'with pytest.raises(module.RulesetGovernanceError, match="left a newer state"):', + 'with pytest.raises(\n module.RulesetGovernanceError,\n match="ambiguous ruleset recovery PUT outcome remains unresolved after settlement window",\n ):', + ) + replace_once( + "tests/test_ruleset_governance_review_round2.py", + ' "tests/test_ruleset_governance_review_round2.py",\n "tests/test_central_required_workflow_ruleset_audit.py",', + ' "tests/test_ruleset_governance_review_round2.py",\n "tests/test_ruleset_governance_delayed_recovery_regression.py",\n "tests/test_ruleset_governance_runtime_budget_regression.py",\n "tests/test_ruleset_governance_post_put_cleanup_regression.py",\n "tests/test_central_required_workflow_ruleset_audit.py",', + ) + + timeout_path = Path("tests/test_ruleset_governance_timeout_regression.py") + timeout_text = timeout_path.read_text(encoding="utf-8") + old_timeout = ''' put_count = 0 + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + + def fake_api(method, endpoint, **_kwargs): + nonlocal put_count + if method == "GET": + return current + if method == "PUT": + put_count += 1 + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 10}]) +''' + new_timeout = ''' put_count = 0 + history_reads = 0 + sleep_calls = 0 + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def fake_sleep(_seconds): + nonlocal sleep_calls + sleep_calls += 1 + + def fake_api(method, endpoint, **_kwargs): + nonlocal put_count + if method == "GET": + return current + if method == "PUT": + put_count += 1 + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + def fake_history(*_args): + nonlocal history_reads + history_reads += 1 + return [{"version_id": 10}] + + monkeypatch.setattr(module.time, "sleep", fake_sleep) + monkeypatch.setattr(module, "_gh_api", fake_api) + monkeypatch.setattr(module, "_gh_api_list", fake_history) +''' + if new_timeout not in timeout_text: + if timeout_text.count(old_timeout) != 1: + raise SystemExit("timeout settlement regression shape changed concurrently") + timeout_text = timeout_text.replace(old_timeout, new_timeout, 1) + old_assert = ''' assert put_count == 1 + + +def test_recovery_timeout_with_new_version_requires_predecessor''' + new_assert = ''' assert put_count == 1 + assert history_reads == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS + assert sleep_calls == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1 + + +def test_recovery_timeout_with_new_version_requires_predecessor''' + if new_assert not in timeout_text: + if timeout_text.count(old_assert) != 1: + raise SystemExit("timeout settlement assertion site changed concurrently") + timeout_text = timeout_text.replace(old_assert, new_assert, 1) + timeout_path.write_text(timeout_text, encoding="utf-8") + + delayed_path = Path("tests/test_ruleset_governance_delayed_recovery_regression.py") + delayed_text = delayed_path.read_text(encoding="utf-8") + old_delayed = ''' history_reads = 0 + put_count = 0 + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) +''' + new_delayed = ''' history_reads = 0 + put_count = 0 + sleep_calls = 0 + + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + + def fake_sleep(_seconds): + nonlocal sleep_calls + sleep_calls += 1 + + monkeypatch.setattr(module.time, "sleep", fake_sleep) + monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) +''' + if new_delayed not in delayed_text: + if delayed_text.count(old_delayed) != 1: + raise SystemExit("delayed recovery regression shape changed concurrently") + delayed_text = delayed_text.replace(old_delayed, new_delayed, 1) + old_delayed_assert = ''' assert history_reads == 2 + assert put_count == 1 +''' + new_delayed_assert = ''' assert history_reads == 2 + assert sleep_calls == 1 + assert put_count == 1 +''' + if new_delayed_assert not in delayed_text: + if delayed_text.count(old_delayed_assert) != 1: + raise SystemExit("delayed recovery assertion site changed concurrently") + delayed_text = delayed_text.replace(old_delayed_assert, new_delayed_assert, 1) + delayed_path.write_text(delayed_text, encoding="utf-8") + PY + git rm .github/workflows/_temp_pr1644_review_fix.yml + git diff --check + git add tests/test_ruleset_governance_review_regressions.py \ + tests/test_ruleset_governance_timeout_regression.py \ + tests/test_ruleset_governance_delayed_recovery_regression.py \ + tests/test_ruleset_governance_review_round2.py + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "test(governance): align settlement regression contracts" + git push origin HEAD:fix/ruleset-owner-plane-reconciler From ff5fc4f2e461cf4f4f575ccd03959f207804f22c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:14:28 +0900 Subject: [PATCH 147/154] chore(governance): remove blocked one-shot repair workflow --- .github/workflows/_temp_pr1644_review_fix.yml | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 .github/workflows/_temp_pr1644_review_fix.yml diff --git a/.github/workflows/_temp_pr1644_review_fix.yml b/.github/workflows/_temp_pr1644_review_fix.yml deleted file mode 100644 index 4128080473..0000000000 --- a/.github/workflows/_temp_pr1644_review_fix.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: PR 1644 one-shot review regression repair - -on: - push: - branches: - - fix/ruleset-owner-plane-reconciler - paths: - - ".github/workflows/_temp_pr1644_review_fix.yml" - -permissions: - contents: write - -concurrency: - group: pr1644-one-shot-review-regression-repair - cancel-in-progress: false - -jobs: - repair: - runs-on: ubuntu-slim - timeout-minutes: 10 - steps: - - name: Check out writer branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: fix/ruleset-owner-plane-reconciler - fetch-depth: 0 - - name: Apply current review regressions and retire this workflow - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin fix/ruleset-owner-plane-reconciler - git reset --hard origin/fix/ruleset-owner-plane-reconciler - python - <<'PY' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - file_path = Path(path) - text = file_path.read_text(encoding="utf-8") - if new in text: - return - if text.count(old) != 1: - raise SystemExit(f"expected one current contract site in {path}: {old!r}") - file_path.write_text(text.replace(old, new, 1), encoding="utf-8") - - replace_once( - "tests/test_ruleset_governance_review_regressions.py", - 'with pytest.raises(module.RulesetGovernanceError, match="does not match reviewed mutation"):', - 'with pytest.raises(\n module.RulesetMutationStillSettlingError,\n match="latest ruleset history changed before the reviewed mutation became visible",\n ):', - ) - replace_once( - "tests/test_ruleset_governance_timeout_regression.py", - 'with pytest.raises(module.RulesetGovernanceError, match="left a newer state"):', - 'with pytest.raises(\n module.RulesetGovernanceError,\n match="ambiguous ruleset recovery PUT outcome remains unresolved after settlement window",\n ):', - ) - replace_once( - "tests/test_ruleset_governance_review_round2.py", - ' "tests/test_ruleset_governance_review_round2.py",\n "tests/test_central_required_workflow_ruleset_audit.py",', - ' "tests/test_ruleset_governance_review_round2.py",\n "tests/test_ruleset_governance_delayed_recovery_regression.py",\n "tests/test_ruleset_governance_runtime_budget_regression.py",\n "tests/test_ruleset_governance_post_put_cleanup_regression.py",\n "tests/test_central_required_workflow_ruleset_audit.py",', - ) - - timeout_path = Path("tests/test_ruleset_governance_timeout_regression.py") - timeout_text = timeout_path.read_text(encoding="utf-8") - old_timeout = ''' put_count = 0 - monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) - monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - monkeypatch.setattr(module.time, "sleep", lambda *_args: None) - - def fake_api(method, endpoint, **_kwargs): - nonlocal put_count - if method == "GET": - return current - if method == "PUT": - put_count += 1 - raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) - raise AssertionError((method, endpoint)) - - monkeypatch.setattr(module, "_gh_api", fake_api) - monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 10}]) -''' - new_timeout = ''' put_count = 0 - history_reads = 0 - sleep_calls = 0 - monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) - monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - - def fake_sleep(_seconds): - nonlocal sleep_calls - sleep_calls += 1 - - def fake_api(method, endpoint, **_kwargs): - nonlocal put_count - if method == "GET": - return current - if method == "PUT": - put_count += 1 - raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) - raise AssertionError((method, endpoint)) - - def fake_history(*_args): - nonlocal history_reads - history_reads += 1 - return [{"version_id": 10}] - - monkeypatch.setattr(module.time, "sleep", fake_sleep) - monkeypatch.setattr(module, "_gh_api", fake_api) - monkeypatch.setattr(module, "_gh_api_list", fake_history) -''' - if new_timeout not in timeout_text: - if timeout_text.count(old_timeout) != 1: - raise SystemExit("timeout settlement regression shape changed concurrently") - timeout_text = timeout_text.replace(old_timeout, new_timeout, 1) - old_assert = ''' assert put_count == 1 - - -def test_recovery_timeout_with_new_version_requires_predecessor''' - new_assert = ''' assert put_count == 1 - assert history_reads == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS - assert sleep_calls == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1 - - -def test_recovery_timeout_with_new_version_requires_predecessor''' - if new_assert not in timeout_text: - if timeout_text.count(old_assert) != 1: - raise SystemExit("timeout settlement assertion site changed concurrently") - timeout_text = timeout_text.replace(old_assert, new_assert, 1) - timeout_path.write_text(timeout_text, encoding="utf-8") - - delayed_path = Path("tests/test_ruleset_governance_delayed_recovery_regression.py") - delayed_text = delayed_path.read_text(encoding="utf-8") - old_delayed = ''' history_reads = 0 - put_count = 0 - - monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - monkeypatch.setattr(module.time, "sleep", lambda *_args: None) - monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) -''' - new_delayed = ''' history_reads = 0 - put_count = 0 - sleep_calls = 0 - - monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - - def fake_sleep(_seconds): - nonlocal sleep_calls - sleep_calls += 1 - - monkeypatch.setattr(module.time, "sleep", fake_sleep) - monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) -''' - if new_delayed not in delayed_text: - if delayed_text.count(old_delayed) != 1: - raise SystemExit("delayed recovery regression shape changed concurrently") - delayed_text = delayed_text.replace(old_delayed, new_delayed, 1) - old_delayed_assert = ''' assert history_reads == 2 - assert put_count == 1 -''' - new_delayed_assert = ''' assert history_reads == 2 - assert sleep_calls == 1 - assert put_count == 1 -''' - if new_delayed_assert not in delayed_text: - if delayed_text.count(old_delayed_assert) != 1: - raise SystemExit("delayed recovery assertion site changed concurrently") - delayed_text = delayed_text.replace(old_delayed_assert, new_delayed_assert, 1) - delayed_path.write_text(delayed_text, encoding="utf-8") - PY - git rm .github/workflows/_temp_pr1644_review_fix.yml - git diff --check - git add tests/test_ruleset_governance_review_regressions.py \ - tests/test_ruleset_governance_timeout_regression.py \ - tests/test_ruleset_governance_delayed_recovery_regression.py \ - tests/test_ruleset_governance_review_round2.py - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "test(governance): align settlement regression contracts" - git push origin HEAD:fix/ruleset-owner-plane-reconciler From 061a239fb696d7a646e77f919928ba35a6d8e8d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:17:10 +0900 Subject: [PATCH 148/154] test(governance): prove timeout settlement contract --- ...t_ruleset_governance_timeout_regression.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py index f556327497..5e5eb2831d 100644 --- a/tests/test_ruleset_governance_timeout_regression.py +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -257,9 +257,14 @@ def test_recovery_timeout_before_acceptance_fails_after_settlement_without_retry current = live_payload() displaced = {**current, "name": "Admin predecessor"} put_count = 0 + history_reads = 0 + sleep_calls = 0 monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + + def fake_sleep(_seconds): + nonlocal sleep_calls + sleep_calls += 1 def fake_api(method, endpoint, **_kwargs): nonlocal put_count @@ -270,8 +275,14 @@ def fake_api(method, endpoint, **_kwargs): raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) raise AssertionError((method, endpoint)) + def fake_history(*_args): + nonlocal history_reads + history_reads += 1 + return [{"version_id": 10}] + + monkeypatch.setattr(module.time, "sleep", fake_sleep) monkeypatch.setattr(module, "_gh_api", fake_api) - monkeypatch.setattr(module, "_gh_api_list", lambda *_args: [{"version_id": 10}]) + monkeypatch.setattr(module, "_gh_api_list", fake_history) with pytest.raises( module.RulesetGovernanceError, @@ -285,6 +296,8 @@ def fake_api(method, endpoint, **_kwargs): expected_main_sha="a" * 40, ) assert put_count == 1 + assert history_reads == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS + assert sleep_calls == module.AMBIGUOUS_WRITE_SETTLEMENT_POLLS - 1 def test_recovery_timeout_with_new_version_requires_predecessor(monkeypatch) -> None: @@ -342,7 +355,10 @@ def history_state(_target, version_id): lambda *_args: [{"version_id": 11}, {"version_id": 10}], ) - with pytest.raises(module.RulesetGovernanceError, match="left a newer state"): + with pytest.raises( + module.RulesetGovernanceError, + match="ambiguous ruleset recovery PUT outcome remains unresolved after settlement window", + ): module._recover_displaced_history_state( target, current_version=10, From d5142b9303a490d407ad0c6a6f2d0b0e46e149c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:17:55 +0900 Subject: [PATCH 149/154] test(governance): align history settlement diagnostic --- ...t_ruleset_governance_review_regressions.py | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index cd933a1459..004d11d1c7 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -268,7 +268,10 @@ def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypa lambda *_args, **_kwargs: [{"version_id": 8}, {"version_id": 7}], ) monkeypatch.setattr(module, "_history_version_state", lambda *_args, **_kwargs: _live()) - with pytest.raises(module.RulesetGovernanceError, match="does not match reviewed mutation"): + with pytest.raises( + module.RulesetMutationStillSettlingError, + match="latest ruleset history changed before the reviewed mutation became visible", + ): module._verify_ruleset_history_transition(_target(), 7, desired) @@ -398,6 +401,98 @@ def fake_api(method, endpoint, *, body=None): ] +def test_history_collision_with_bad_predecessor_metadata_fails_before_restore(monkeypatch) -> None: + """Malformed collision history cannot be interpreted as a safe rollback target.""" + + desired_state = _converged() + desired = _desired() + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 10}, {"version_id": True}], + ) + monkeypatch.setattr(module, "_history_version_state", lambda *_args, **_kwargs: desired_state) + monkeypatch.setattr( + module, + "_gh_api", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("no live mutation expected")), + ) + with pytest.raises(module.RulesetGovernanceError, match="version identity is malformed"): + module._verify_ruleset_history_transition(_target(), 7, desired) + + +def test_history_collision_aborts_if_main_advances_before_recovery(monkeypatch) -> None: + """Before rollback starts, a protected-main advance still vetoes privileged recovery.""" + + desired_state = _converged() + desired = _desired() + external = _live() + monkeypatch.setattr( + module, + "_gh_api_list", + lambda *_args, **_kwargs: [{"version_id": 10}, {"version_id": 9}, {"version_id": 7}], + ) + monkeypatch.setattr( + module, + "_history_version_state", + lambda _target, version: desired_state if version == 10 else external, + ) + monkeypatch.setattr( + module, + "_assert_current_main", + lambda _expected: (_ for _ in ()).throw( + module.RulesetGovernanceError("protected main advanced") + ), + ) + monkeypatch.setattr( + module, + "_gh_api", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("no recovery write expected")), + ) + with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): + module._verify_ruleset_history_transition( + _target(), + 7, + desired, + expected_main_sha="a" * 40, + ) + + +def test_history_collision_after_our_write_ignores_stale_main_guard_during_recovery(monkeypatch) -> None: + """Once the reviewed PUT exists, recovery must settle collision history even if main moved.""" + + desired_state = _converged() + desired = _desired() + external = _live() + histories = iter( + [ + [{"version_id": 10}, {"version_id": 9}, {"version_id": 7}], + [{"version_id": 11}, {"version_id": 10}], + ] + ) + monkeypatch.setattr(module, "_gh_api_list", lambda *_args, **_kwargs: next(histories)) + monkeypatch.setattr( + module, + "_history_version_state", + lambda _target, version: desired_state if version == 10 else external, + ) + monkeypatch.setattr( + module, + "_assert_current_main", + lambda _expected: (_ for _ in ()).throw(AssertionError("stale-main guard must not run")), + ) + replies = iter([desired_state, {}, external]) + monkeypatch.setattr(module, "_gh_api", lambda *_args, **_kwargs: next(replies)) + + with pytest.raises(module.RulesetGovernanceError, match="restored newest displaced administrator state"): + module._verify_ruleset_history_transition( + _target(), + 7, + desired, + expected_main_sha=None, + ) + + def test_reconcile_forwards_expected_main_sha_to_each_target(monkeypatch) -> None: """The multi-target coordinator preserves the protected-main guard on every mutation.""" From ef634b359edd8d58d8f24bf13013c36ba2a834b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:18:33 +0900 Subject: [PATCH 150/154] test(governance): prove delayed recovery settlement interval --- ...est_ruleset_governance_delayed_recovery_regression.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_delayed_recovery_regression.py b/tests/test_ruleset_governance_delayed_recovery_regression.py index 55aff07f60..0ae9e0f3c4 100644 --- a/tests/test_ruleset_governance_delayed_recovery_regression.py +++ b/tests/test_ruleset_governance_delayed_recovery_regression.py @@ -78,9 +78,15 @@ def test_delayed_recovery_acceptance_settles_before_any_second_put(monkeypatch) displaced = {**current, "name": "Administrator predecessor", "enforcement": "evaluate"} history_reads = 0 put_count = 0 + sleep_calls = 0 monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) - monkeypatch.setattr(module.time, "sleep", lambda *_args: None) + + def fake_sleep(_seconds): + nonlocal sleep_calls + sleep_calls += 1 + + monkeypatch.setattr(module.time, "sleep", fake_sleep) monkeypatch.setattr(module, "_history_version_state", lambda *_args: displaced) def fake_history(*_args): @@ -111,6 +117,7 @@ def fake_api(method, endpoint, **_kwargs): ) assert history_reads == 2 + assert sleep_calls == 1 assert put_count == 1 From cb27beb47b69505725989491312eaf3e8e7bcfb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:19:06 +0900 Subject: [PATCH 151/154] test(governance): cover every permanent focused suite --- tests/test_ruleset_governance_review_round2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_ruleset_governance_review_round2.py b/tests/test_ruleset_governance_review_round2.py index 2b8a66e50f..1f5dd740ad 100644 --- a/tests/test_ruleset_governance_review_round2.py +++ b/tests/test_ruleset_governance_review_round2.py @@ -335,6 +335,9 @@ def test_focused_workflow_runs_every_permanent_governance_regression_suite() -> "tests/test_ruleset_governance_reconciliation.py", "tests/test_ruleset_governance_review_regressions.py", "tests/test_ruleset_governance_review_round2.py", + "tests/test_ruleset_governance_delayed_recovery_regression.py", + "tests/test_ruleset_governance_runtime_budget_regression.py", + "tests/test_ruleset_governance_post_put_cleanup_regression.py", "tests/test_central_required_workflow_ruleset_audit.py", "tests/test_ruleset_audit_completeness_regression.py", "tests/test_ruleset_merge_method_shape_regression.py", @@ -343,4 +346,4 @@ def test_focused_workflow_runs_every_permanent_governance_regression_suite() -> test_command = text.split("-m pytest -q \\\n", 1)[1].split("\n python -m coverage report", 1)[0] for path in expected: assert text.count(f'- "{path}"') >= 2 - assert path in test_command \ No newline at end of file + assert path in test_command From 0f3530c982ab40ac5e963712945b65a1e3c97c28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:23:48 +0900 Subject: [PATCH 152/154] test(governance): align current collision diagnostics --- ...t_ruleset_governance_review_regressions.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/test_ruleset_governance_review_regressions.py b/tests/test_ruleset_governance_review_regressions.py index 004d11d1c7..a17fa80c69 100644 --- a/tests/test_ruleset_governance_review_regressions.py +++ b/tests/test_ruleset_governance_review_regressions.py @@ -270,7 +270,7 @@ def test_history_transition_rejects_incomplete_or_inconsistent_evidence(monkeypa monkeypatch.setattr(module, "_history_version_state", lambda *_args, **_kwargs: _live()) with pytest.raises( module.RulesetMutationStillSettlingError, - match="latest ruleset history changed before the reviewed mutation became visible", + match="latest ruleset history does not match reviewed mutation; history changed before the reviewed mutation became visible", ): module._verify_ruleset_history_transition(_target(), 7, desired) @@ -422,7 +422,7 @@ def test_history_collision_with_bad_predecessor_metadata_fails_before_restore(mo def test_history_collision_aborts_if_main_advances_before_recovery(monkeypatch) -> None: - """Before rollback starts, a protected-main advance still vetoes privileged recovery.""" + """Before rollback writes, a protected-main advance still vetoes privileged recovery.""" desired_state = _converged() desired = _desired() @@ -444,11 +444,16 @@ def test_history_collision_aborts_if_main_advances_before_recovery(monkeypatch) module.RulesetGovernanceError("protected main advanced") ), ) - monkeypatch.setattr( - module, - "_gh_api", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("no recovery write expected")), - ) + + api_calls: list[str] = [] + + def fake_api(method, endpoint, *, body=None): + api_calls.append(method) + if method == "GET" and endpoint == _target().endpoint: + return desired_state + raise AssertionError("no recovery write expected") + + monkeypatch.setattr(module, "_gh_api", fake_api) with pytest.raises(module.RulesetGovernanceError, match="protected main advanced"): module._verify_ruleset_history_transition( _target(), @@ -456,6 +461,7 @@ def test_history_collision_aborts_if_main_advances_before_recovery(monkeypatch) desired, expected_main_sha="a" * 40, ) + assert api_calls == ["GET"] def test_history_collision_after_our_write_ignores_stale_main_guard_during_recovery(monkeypatch) -> None: @@ -555,4 +561,4 @@ def test_docs_state_github_has_no_ruleset_put_compare_and_swap() -> None: def test_module_docstring_does_not_promise_impossible_atomicity() -> None: """Production documentation must describe best-effort drift checks, not CAS semantics.""" - assert "never silently overwritten" not in (module.__doc__ or "") + assert "never silently overwritten" not in (module.__doc__ or "") \ No newline at end of file From 93a9b9580ed4af40e1d13838af4e0e7bcf6e8de1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:24:39 +0900 Subject: [PATCH 153/154] test(governance): distinguish newer recovery history --- tests/test_ruleset_governance_timeout_regression.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ruleset_governance_timeout_regression.py b/tests/test_ruleset_governance_timeout_regression.py index 5e5eb2831d..ce8b8dc1ec 100644 --- a/tests/test_ruleset_governance_timeout_regression.py +++ b/tests/test_ruleset_governance_timeout_regression.py @@ -357,7 +357,7 @@ def history_state(_target, version_id): with pytest.raises( module.RulesetGovernanceError, - match="ambiguous ruleset recovery PUT outcome remains unresolved after settlement window", + match="ambiguous ruleset recovery PUT left a newer state after settlement window; refusing overwrite", ): module._recover_displaced_history_state( target, @@ -483,4 +483,4 @@ def fake_reconcile(targets, *, verify_only, expected_main_sha=None): monkeypatch.setattr(module, "reconcile", fake_reconcile) assert module.main(["--verify-only"]) == 0 assert observed == [((target,), True, None)] - assert "verified 1 ruleset governance targets" in capsys.readouterr().out + assert "verified 1 ruleset governance targets" in capsys.readouterr().out \ No newline at end of file From 4b0b1e3fc4700adafd8da47839c15a3834c33c67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:28:23 +0900 Subject: [PATCH 154/154] test(governance): cover post-settlement recovery race --- ..._governance_post_put_cleanup_regression.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_ruleset_governance_post_put_cleanup_regression.py b/tests/test_ruleset_governance_post_put_cleanup_regression.py index d5f7052124..b48f508a95 100644 --- a/tests/test_ruleset_governance_post_put_cleanup_regression.py +++ b/tests/test_ruleset_governance_post_put_cleanup_regression.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import subprocess import sys from pathlib import Path @@ -275,3 +276,53 @@ def api(method, endpoint, *, body=None): assert main_checks == 2 assert history_reads == 2 assert put_bodies == [desired, module._editable_projection(administrator)] + + +def test_ambiguous_recovery_rejects_history_rewrite_after_settlement(monkeypatch) -> None: + """A history rewrite after settlement is detected before restored state is trusted.""" + + module = load_module() + target = repository_target(module) + current = live_payload() + displaced = {**current, "name": "Administrator predecessor", "enforcement": "evaluate"} + rewritten = {**current, "name": "Newer administrator state"} + put_count = 0 + + def history_state(_target, version_id): + if version_id == 9: + return displaced + if version_id == 11: + return rewritten + raise AssertionError(version_id) + + def api(method, endpoint, *, body=None): + nonlocal put_count + if method == "GET" and endpoint == target.endpoint: + return current + if method == "PUT" and endpoint == target.endpoint: + put_count += 1 + raise subprocess.TimeoutExpired(cmd=["gh", "api"], timeout=30) + raise AssertionError((method, endpoint)) + + monkeypatch.setattr(module, "_history_version_state", history_state) + monkeypatch.setattr(module, "_assert_current_main", lambda *_args: None) + monkeypatch.setattr(module, "_gh_api", api) + monkeypatch.setattr( + module, + "_settle_ambiguous_recovery_history", + lambda *_args, **_kwargs: [{"version_id": 11}, {"version_id": 10}], + ) + + with pytest.raises( + module.RulesetGovernanceError, + match="ambiguous ruleset recovery PUT left a newer state; refusing overwrite", + ): + module._recover_displaced_history_state( + target, + current_version=10, + current_payload=module._editable_projection(current), + displaced_version=9, + expected_main_sha="a" * 40, + ) + + assert put_count == 1