diff --git a/.github/workflows/clearfolio-hourly-review-repair.yml b/.github/workflows/clearfolio-hourly-review-repair.yml index e8d2991fac..7b7af7184d 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 00106b2e0b..a16219e658 100644 --- a/.github/workflows/disksage-hourly-review-repair.yml +++ b/.github/workflows/disksage-hourly-review-repair.yml @@ -16,6 +16,8 @@ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc84661ed6..cdba81f112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -215,6 +215,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. + - Web verification now checks services through local readiness addresses only. Start the backend and frontend on this computer and use their local health URLs when running the check. diff --git a/docs/doctoring/clearfolio-hourly-review-caller.md b/docs/doctoring/clearfolio-hourly-review-caller.md index 239fdbd3ee..1bde6f85cd 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 @@ -65,9 +67,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 +108,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 2e30aee8d1..b47539c2d3 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 5ad14b2488..61f80e18f7 100644 --- a/tests/test_disksage_hourly_review_caller.py +++ b/tests/test_disksage_hourly_review_caller.py @@ -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 072ba4d8b3..f3a9173ad8 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: @@ -323,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 ""