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/CHANGELOG.md b/CHANGELOG.md index 8ab82ccea..38d06ea41 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). diff --git a/docs/doctoring/clearfolio-hourly-review-caller.md b/docs/doctoring/clearfolio-hourly-review-caller.md index 239fdbd3e..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 @@ -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 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/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]] = [] 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()) 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..f3a9173ad 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 ""