From 7f9f9f0606ac5c88df3857eb5e5367d5bdbad420 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:31:43 +0900 Subject: [PATCH 01/11] fix: grant hourly callers reusable workflow OIDC scope --- .github/workflows/clearfolio-hourly-review-repair.yml | 5 +++++ .github/workflows/disksage-hourly-review-repair.yml | 5 +++++ docs/doctoring/clearfolio-hourly-review-caller.md | 11 +++++++---- docs/doctoring/disksage-hourly-review-caller.md | 6 ++++-- tests/test_disksage_hourly_review_caller.py | 3 ++- tests/test_pr_review_fix_hourly_contract.py | 5 +++-- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/workflows/clearfolio-hourly-review-repair.yml b/.github/workflows/clearfolio-hourly-review-repair.yml index e8d2991fa..7b7af7184 100644 --- a/.github/workflows/clearfolio-hourly-review-repair.yml +++ b/.github/workflows/clearfolio-hourly-review-repair.yml @@ -14,6 +14,11 @@ permissions: jobs: dispatch-review-repair: + # The reusable scheduler exchanges a GitHub OIDC token for its established + # OpenCode app credential; reusable workflows cannot elevate this permission. + permissions: + contents: read + id-token: write uses: ./.github/workflows/pr-review-fix-scheduler.yml with: target_repository: ContextualWisdomLab/clearfolio diff --git a/.github/workflows/disksage-hourly-review-repair.yml b/.github/workflows/disksage-hourly-review-repair.yml index d1868bc20..a16219e65 100644 --- a/.github/workflows/disksage-hourly-review-repair.yml +++ b/.github/workflows/disksage-hourly-review-repair.yml @@ -16,6 +16,11 @@ permissions: jobs: dispatch-review-repair: + # The reusable scheduler exchanges a GitHub OIDC token for its established + # OpenCode app credential; reusable workflows cannot elevate this permission. + permissions: + contents: read + id-token: write uses: ./.github/workflows/pr-review-fix-scheduler.yml with: target_repository: ContextualWisdomLab/disksage diff --git a/docs/doctoring/clearfolio-hourly-review-caller.md b/docs/doctoring/clearfolio-hourly-review-caller.md index 239fdbd3e..6532308f3 100644 --- a/docs/doctoring/clearfolio-hourly-review-caller.md +++ b/docs/doctoring/clearfolio-hourly-review-caller.md @@ -65,9 +65,11 @@ It does not use `secrets: inherit`. It does not receive execution. The NVIDIA credential is bound only inside the separately reviewed `PR Review Autofix` workflow's two OpenCode execution steps. -Both the caller and reusable scheduler keep the workflow-generated -`GITHUB_TOKEN` read-only with only `contents: read`; neither declares job-level -write elevation. Cross-repository PR inspection, acknowledgement, workflow +The caller keeps the workflow-generated `GITHUB_TOKEN` read-only with +`contents: read` and grants only job-scoped `id-token: write` so the reusable +scheduler can exchange the signed GitHub OIDC token for its established +OpenCode app credential. The caller does not grant repository, pull-request, or +workflow write permissions. Cross-repository PR inspection, acknowledgement, workflow dispatch, and branch updates are authorized only through the explicitly mapped `PR_REVIEW_MERGE_TOKEN` or `OPENCODE_APPROVE_TOKEN`, exposed to the scheduler as `GH_TOKEN`. The scheduler has no `github.token` fallback. Missing credentials @@ -104,7 +106,8 @@ Permanent tests require all of the following: binding are absent from the caller; 9. the focused exact-head contract workflow reruns whenever the caller changes; 10. the caller and reusable scheduler retain read-only workflow-token - permissions, declare no job-level write elevation, and contain no + permissions, the caller declares only job-scoped `id-token: write` for the + OIDC exchange, and contain no `github.token` mutation fallback. Repository acceptance still requires current-head workflow, security, diff --git a/docs/doctoring/disksage-hourly-review-caller.md b/docs/doctoring/disksage-hourly-review-caller.md index 2e30aee8d..b47539c2d 100644 --- a/docs/doctoring/disksage-hourly-review-caller.md +++ b/docs/doctoring/disksage-hourly-review-caller.md @@ -61,7 +61,9 @@ controls every mutation and merge decision. ## Credential and model boundary -The queue-scanning caller has only `contents: read`. It maps only the established +The queue-scanning caller keeps the workflow token at `contents: read` and grants +only job-scoped `id-token: write`, which is required for the reusable scheduler's +OIDC-to-OpenCode app-token exchange. It maps only the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler credentials and does not use `secrets: inherit`. @@ -89,7 +91,7 @@ neutral-required, stale-head, or synthetic-merge evidence is not success. Repository contracts require the exact cron, target repository, one-dispatch budget, two-hour retry floor, non-cancelling single-flight policy, read-only -workflow token, explicit secret mapping, and absence of both +workflow token plus job-scoped `id-token: write`, explicit secret mapping, and absence of both `NVIDIA_NIM_API_KEY` and `COPILOT_GITHUB_TOKEN` from the caller. Rollback is a reviewed source change. Do not disable exact-head binding, reduce diff --git a/tests/test_disksage_hourly_review_caller.py b/tests/test_disksage_hourly_review_caller.py index bee0d859b..61f80e18f 100644 --- a/tests/test_disksage_hourly_review_caller.py +++ b/tests/test_disksage_hourly_review_caller.py @@ -34,7 +34,7 @@ def test_disksage_caller_preserves_credentials_and_read_only_token_scope() -> No workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n" not in jobs_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller assert "secrets: inherit" not in caller @@ -48,6 +48,7 @@ def test_disksage_caller_preserves_credentials_and_read_only_token_scope() -> No "statuses: write", ): assert forbidden not in caller + assert "id-token: write" in caller def test_disksage_caller_doctoring_records_rca_feasibility_and_latency() -> None: diff --git a/tests/test_pr_review_fix_hourly_contract.py b/tests/test_pr_review_fix_hourly_contract.py index 072ba4d8b..a2b1531c0 100644 --- a/tests/test_pr_review_fix_hourly_contract.py +++ b/tests/test_pr_review_fix_hourly_contract.py @@ -64,11 +64,12 @@ def test_clearfolio_caller_runs_once_each_hour() -> None: def test_clearfolio_caller_keeps_github_token_read_only() -> None: - """The hourly caller delegates with explicit secrets and no token elevation.""" + """The hourly caller delegates with explicit secrets and only OIDC elevation.""" text = _read(_CLEARFOLIO_CALLER) workflow_scope, jobs_scope = text.split("\njobs:\n", maxsplit=1) assert "\npermissions:\n contents: read\n" in workflow_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope for permission in ( "actions: write", "issues: write", @@ -77,7 +78,7 @@ def test_clearfolio_caller_keeps_github_token_read_only() -> None: "statuses: write", ): assert permission not in text - assert "\n permissions:\n" not in jobs_scope + assert "id-token: write" in text def test_reusable_scheduler_has_no_product_specific_timer() -> None: From 07cdefca207e8bc09e714e33740a47809cb5d9a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:01:42 +0900 Subject: [PATCH 02/11] docs: complete coordinator client docstring --- scripts/ci/organization_commercial_readiness_loop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..a4d7fa983 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize the client with one bounded GitHub credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 82cd117d279a9b870f185b136984d82bb3ac5236 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:32:32 +0900 Subject: [PATCH 03/11] test(ci): close hourly contract docstring gap --- tests/test_pr_review_fix_hourly_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_pr_review_fix_hourly_contract.py b/tests/test_pr_review_fix_hourly_contract.py index a2b1531c0..f3a9173ad 100644 --- a/tests/test_pr_review_fix_hourly_contract.py +++ b/tests/test_pr_review_fix_hourly_contract.py @@ -324,6 +324,8 @@ def test_rca_dispatch_carries_an_explicit_worker_mode(monkeypatch) -> None: captured: dict[str, str | None] = {} def fake_run(args: list[str], *, stdin: str | None = None) -> str: + """Capture the dispatch payload without invoking GitHub.""" + del args captured["stdin"] = stdin return "" From f05c9b218d4032ed11f9f393f013f316b7967c70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:35:27 +0900 Subject: [PATCH 04/11] docs(test): complete coordinator fixture docstrings --- organization_commercial_readiness_fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index d86596196..4b7ac7452 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -90,6 +90,7 @@ def __init__( repositories: list[dict[str, Any]], snapshots: dict[str, list[RepositorySnapshot | Exception]], ) -> None: + """Initialize deterministic repository and snapshot responses.""" self.repositories = repositories self.snapshots = snapshots self.dispatched_repairs: list[tuple[str, str]] = [] From 97b14e800a45cd823db2a8253abef38d9ddc15c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:55:27 +0900 Subject: [PATCH 05/11] fix(ci): refresh pip audit lock --- 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 ade197a49..0ae099d8f 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 1e3b6e72f83dcedb3ebc622a6434e5327539016e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:56:12 +0900 Subject: [PATCH 06/11] docs: record pip audit lock refresh --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42afe76a..8c3e5017b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Refresh the hash-pinned `pip` used by the Python security audit from 26.1.2 to + 26.2.1 so the audit no longer installs the vulnerable PYSEC-2026-3721 release. + - 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. - 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). From cb1ed720d84b052cc16013ddcd8813cf255f024a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:05:29 +0900 Subject: [PATCH 07/11] chore(ci): keep OIDC repair scope focused --- 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 0ae099d8f..ade197a49 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 7c131ad9971ebede1ae0b4c9fbc9e66b301aa0a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:19:07 +0900 Subject: [PATCH 08/11] fix(ci): retain secure pip audit lock --- 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 ade197a49..0ae099d8f 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 6c6b74054134d95ca302bdf0aa2107a231aaa228 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:57:51 +0900 Subject: [PATCH 09/11] docs: correct Clearfolio concurrency contract --- docs/doctoring/clearfolio-hourly-review-caller.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/clearfolio-hourly-review-caller.md b/docs/doctoring/clearfolio-hourly-review-caller.md index 6532308f3..1bde6f85c 100644 --- a/docs/doctoring/clearfolio-hourly-review-caller.md +++ b/docs/doctoring/clearfolio-hourly-review-caller.md @@ -29,10 +29,12 @@ max_dispatches: "1" retry_hours: "1" ``` -The caller and reusable engine both use `cancel-in-progress: true`. This keeps -queue inspection single-flight at the product and engine boundaries. At most one -autofix dispatch is issued during an invocation, and the same exact PR head is -not retried more than once per hour. +The caller uses `cancel-in-progress: false`, while the reusable engine applies +its own event-specific concurrency policy. This preserves each hourly handoff +long enough for the reusable scheduler to inspect the queue, while the engine +still prevents duplicate work for the same target event. At most one autofix +dispatch is issued during an invocation, and the same exact PR head is not +retried more than once per hour. ## Modular MSA contract From f2f79b95de9a1a588e20524050ee65469350a694 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:14:07 +0900 Subject: [PATCH 10/11] test(ci): align scheduler contract assertion --- scripts/ci/test_strix_quick_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8b..ccf883d1e 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,7 +1506,7 @@ 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" "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' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" 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" From 89cc59b0fc3f753e3e6b0c998b5dfc407ba92c29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:44:24 +0900 Subject: [PATCH 11/11] fix(ci): align scheduler cancellation contract --- scripts/ci/test_strix_quick_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ccf883d1e..989afc4f4 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1507,7 +1507,7 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { 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' && 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' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + 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 orphan workflow-run scans instead of accumulating merge/update attempts" 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"