diff --git a/.github/workflows/disksage-hourly-review-repair.yml b/.github/workflows/disksage-hourly-review-repair.yml index d1868bc20..8624fcd8a 100644 --- a/.github/workflows/disksage-hourly-review-repair.yml +++ b/.github/workflows/disksage-hourly-review-repair.yml @@ -13,6 +13,7 @@ concurrency: permissions: contents: read + id-token: write jobs: dispatch-review-repair: diff --git a/docs/doctoring/disksage-hourly-review-caller.md b/docs/doctoring/disksage-hourly-review-caller.md index 2e30aee8d..5d77dc42d 100644 --- a/docs/doctoring/disksage-hourly-review-caller.md +++ b/docs/doctoring/disksage-hourly-review-caller.md @@ -61,7 +61,10 @@ 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 grants `contents: read` plus `id-token: write`. The +latter is limited to the central scheduler's OpenCode OIDC token exchange and +does not grant repository content, issue, pull-request, status, approval, merge, +or release write authority. The caller maps only the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler credentials and does not use `secrets: inherit`. @@ -88,8 +91,9 @@ neutral-required, stale-head, or synthetic-merge evidence is not success. ## Verification and rollback 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 +budget, two-hour retry floor, non-cancelling single-flight policy, the +least-privilege workflow token (`contents: read` plus OIDC-only +`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..bce9e4081 100644 --- a/tests/test_disksage_hourly_review_caller.py +++ b/tests/test_disksage_hourly_review_caller.py @@ -28,12 +28,12 @@ def test_disksage_caller_is_hourly_bounded_and_non_cancelling() -> None: assert 'retry_hours: "2"' in caller -def test_disksage_caller_preserves_credentials_and_read_only_token_scope() -> None: - """The queue scanner maps established credentials without exposing model secrets.""" +def test_disksage_caller_preserves_credentials_and_oidc_token_scope() -> None: + """The caller grants only read access plus the scheduler's OIDC exchange scope.""" caller = _read(CALLER) workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - assert "\npermissions:\n contents: read\n" in workflow_scope + assert "\npermissions:\n contents: read\n id-token: write\n" in workflow_scope assert "\n permissions:\n" not 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 @@ -62,6 +62,8 @@ def test_disksage_caller_doctoring_records_rca_feasibility_and_latency() -> None "NVIDIA_NIM_API_KEY", "COPILOT_GITHUB_TOKEN", "ContextualWisdomLab/disksage", + "`id-token: write`", + "OIDC token exchange", "APA 7th references", ): assert phrase in doctoring