diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index f98528186..fbeee49c2 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -6,6 +6,7 @@ on: - .github/workflows/pr-review-fix-scheduler.yml - .github/workflows/pr-review-autofix.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py @@ -20,12 +21,14 @@ on: - tests/test_pr_review_autofix_nvidia_nim_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/hourly-nvidia-nim-autofix.md push: paths: - .github/workflows/pr-review-fix-scheduler.yml - .github/workflows/pr-review-autofix.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py @@ -40,6 +43,7 @@ on: - tests/test_pr_review_autofix_nvidia_nim_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/hourly-nvidia-nim-autofix.md permissions: diff --git a/.github/workflows/inkspan-hourly-review-repair.yml b/.github/workflows/inkspan-hourly-review-repair.yml new file mode 100644 index 000000000..7ce6a1a98 --- /dev/null +++ b/.github/workflows/inkspan-hourly-review-repair.yml @@ -0,0 +1,26 @@ +name: Inkspan Hourly Review Repair + +on: + schedule: + # Offset from other product heartbeats to reduce shared-runner congestion. + - cron: "37 * * * *" + +concurrency: + group: inkspan-hourly-review-repair + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dispatch-review-repair: + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/inkspan + base_branch: main + max_prs: "50" + max_dispatches: "1" + retry_hours: "1" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 348819332..9a640c9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ Semantic Versioning where the repository publishes a release. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. - Added a dedicated Clearfolio hourly caller that invokes the product-neutral central scheduler with the exact repository, protected base branch, one-dispatch budget, one-hour retry floor, single-flight concurrency, and only the established scheduler credentials. +- Added a dedicated Inkspan hourly caller at minute 37 with exact repository/base targeting, its own single-flight group, the same one-dispatch and one-hour retry bounds, explicit scheduler credentials, and no model-secret exposure. ### Changed - 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. +- Run the bounded Inkspan review-feedback repair caller at minute 37 of every hour so its queue cannot cancel or congest the Clearfolio product heartbeat. - Use NVIDIA NIM `mistralai/mistral-nemotron` for scheduled repair and `nvidia/nemotron-3-nano-30b-a3b` for bounded helper work instead of GitHub Models in the write-capable autofix worker. - Apply one NUL-delimited exact-path and complete pre/post-worktree verification contract to both ordinary review repair and merge-conflict repair rather than relying on a visible post-model diff for the ordinary path. @@ -25,7 +27,7 @@ Semantic Versioning where the repository publishes a release. - Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. - Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. - Bind reusable scheduler implementation to the validated called-workflow repository, SHA, ref, and file path, and verify the checked-out commit before executing privileged scheduler logic. -- Removed the ambiguous central-repository schedule fallback that could scan `.github` instead of Clearfolio when no external variable was configured; the active product caller now names Clearfolio explicitly while the reusable engine retains caller and dispatch overrides. +- Removed the ambiguous central-repository schedule fallback that could scan `.github` instead of Clearfolio when no external variable was configured; active product callers now name their exact repositories while the reusable engine retains caller and dispatch overrides. - Corrected the conflict-ordering regression contract to select the conflict-specific snapshot and verification after the ordinary path adopted the same trusted helper. ### Security @@ -33,13 +35,13 @@ Semantic Versioning where the repository publishes a release. - Snapshot the complete pre-model worktree for ordinary and conflict repair and reject every model-caused created, deleted, modified, mode-changed, retargeted, ignored, dangling, directory-backed, external-link, metadata-race, or out-of-scope path before staging or push. - Add ignored-path inventory through Git's tracked, other, and `--others --ignored --exclude-standard` views so model-created caches, credentials, or build output cannot evade comparison merely because ordinary Git publication omits them. - Deny `.git` and `.git/*` in both OpenCode permission maps, disable repository hooks for privileged commit and push through `core.hooksPath=/dev/null`, and push only to an explicit revalidated repository URL so model-mutable Git metadata cannot control publication. -- Keep the Clearfolio caller and reusable scheduler read-only at workflow and job scope; authorize cross-repository inspection, acknowledgement, dispatch, and branch updates only through the explicitly mapped `PR_REVIEW_MERGE_TOKEN` or `OPENCODE_APPROVE_TOKEN`, with no `github.token` mutation fallback. +- Keep the Clearfolio and Inkspan callers plus reusable scheduler read-only at workflow and job scope; authorize cross-repository inspection, acknowledgement, dispatch, and branch updates only through the explicitly mapped `PR_REVIEW_MERGE_TOKEN` or `OPENCODE_APPROVE_TOKEN`, with no `github.token` mutation fallback. - Bind `NVIDIA_NIM_API_KEY` only to the two OpenCode model execution steps, fail closed when the secret is absent, and remove GitHub and Actions OIDC credentials from both model subprocesses. - Deny unnecessary non-file OpenCode interactions and preserve the independent read-only reviewer workflow and its credential/model-pool contract byte-for-byte. - Pin the repository-dispatch autofix helper checkout to the exact workflow-run SHA rather than a moving default branch. -- Pass only `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` from the Clearfolio schedule caller; do not use `secrets: inherit` and do not expose the NVIDIA model credential to the queue-scanning workflow. +- Pass only `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` from each product schedule caller; do not use `secrets: inherit` and do not expose the NVIDIA model credential to queue-scanning workflows. ### Documentation -- Added operator and APA 7 doctoring records for the hourly cadence, immutable source identity, NVIDIA NIM provider and secret boundary, model-process credential isolation, modular MSA ownership, product-specific caller activation, verification contract, and rollback. +- Added operator and APA 7 doctoring records for the hourly cadence, immutable source identity, NVIDIA NIM provider and secret boundary, model-process credential isolation, modular MSA ownership, Clearfolio and Inkspan caller activation, verification contracts, and rollback. - Documented the ordinary and conflict repair write-scope parity, ignored-path and symlink inventory, Git-control-file denial, hook suppression, explicit push destination, RED/GREEN evidence, operator response, and local-versus-protected evidence boundary. diff --git a/docs/doctoring/inkspan-hourly-review-caller.md b/docs/doctoring/inkspan-hourly-review-caller.md new file mode 100644 index 000000000..7c873b6de --- /dev/null +++ b/docs/doctoring/inkspan-hourly-review-caller.md @@ -0,0 +1,144 @@ +# Inkspan Hourly Review-Repair Caller Boundary + +## Decision + +Inkspan's one-hour review → repair → revalidation support heartbeat is owned by +a dedicated central caller workflow, +`.github/workflows/inkspan-hourly-review-repair.yml`. The product-neutral engine +remains `.github/workflows/pr-review-fix-scheduler.yml`; it contains no Inkspan +repository literal and no product-specific schedule. + +This split preserves both deployment forms required by CWL. Inkspan remains a +standalone product repository, while the central `.github` control plane owns +shared repair orchestration that naruon and other products may reuse without +copying privileged workflow logic. A scheduled workflow executes in the +repository that contains it, so the caller must name `ContextualWisdomLab/inkspan` +explicitly rather than relying on the central repository as an implicit target. + +## Product caller + +The Inkspan caller runs at minute 37 of every hour. The offset separates its +heartbeat from the Clearfolio caller and reduces avoidable shared-runner bursts. +It invokes the local reusable workflow with explicit, reviewable values: + +```yaml +target_repository: ContextualWisdomLab/inkspan +base_branch: main +max_prs: "50" +max_dispatches: "1" +retry_hours: "1" +``` + +The caller and reusable engine both use `cancel-in-progress: true`. Queue +inspection is therefore single-flight at both the product and engine boundary. +One invocation may dispatch at most one autofix, and the same exact PR head is +not retried more often than once per hour. + +## Modular MSA contract + +The caller contains product identity and cadence only. The reusable scheduler +continues to own PR inventory, exact-head retry bookkeeping, dispatch bounds, +and the handoff to the separately reviewed repair plane. Inkspan does not copy +OpenCode configuration, reviewer identities, model credentials, merge policy, +or branch-update logic into its product repository. + +This architecture allows Inkspan to run independently while remaining suitable +for naruon `compose` and `ui.panel` integration. Product integration changes do +not alter the scheduler's security or credential boundary, and scheduler +changes do not add a runtime dependency to Inkspan. + +## Credential and privilege boundary + +The caller passes exactly the two established optional scheduler credentials: + +- `PR_REVIEW_MERGE_TOKEN`; +- `OPENCODE_APPROVE_TOKEN`. + +It does not use `secrets: inherit`. It does not receive +`NVIDIA_NIM_API_KEY`, because queue inspection and workflow dispatch are not +model execution. The NVIDIA credential remains scoped only to the two model +execution steps in the separately reviewed `PR Review Autofix` workflow. +`COPILOT_GITHUB_TOKEN` and GitHub Models are not introduced. + +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 inspection, acknowledgement, workflow +dispatch, and branch updates use only the explicitly mapped scheduler +credentials exposed as `GH_TOKEN`. The scheduler contains no `github.token` +mutation fallback. Missing credentials therefore fail closed rather than +silently widening ambient workflow authority. + +The repair worker cannot approve a PR, merge a PR, publish a release, alter a +reviewer credential chain, weaken branch protection, or convert a failed or +missing check into success. + +## Privacy, CSAP, and SOC 2 evidence + +The caller contains only product identity, branch identity, bounded numeric +limits, and secret references. It does not ingest business payloads, comment +bodies, model output, or personal records. This uses separation and data +minimization rather than masking operational identifiers that the scheduler +needs to function. Read-only workflow-token permissions, explicit credential +mapping, single-flight execution, exact target selection, bounded dispatch, and +durable GitHub audit records provide reviewable evidence for least privilege, +change management, logical access, and monitoring controls. + +## Failure behavior + +Missing scheduler credentials cause target inspection or dispatch to fail +closed; they do not redirect work to `.github`. A missing NVIDIA credential +later stops the repair worker before model execution. Neither case weakens +independent review, required checks, unresolved-thread policy, or branch +protection. + +Scheduled workflows become active only from the protected default branch. This +caller is not production automation while its stacked pull request or its +prerequisite scheduler PR remains unmerged. + +## Verification contract + +Permanent tests require all of the following: + +1. the Inkspan caller contains the exact hourly cron; +2. the caller invokes the local reusable scheduler; +3. `ContextualWisdomLab/inkspan` and protected `main` are explicit; +4. dispatch and same-head retry bounds remain one; +5. caller concurrency remains single-flight and distinct from Clearfolio; +6. only the two established scheduler secrets cross the caller boundary; +7. `secrets: inherit`, `COPILOT_GITHUB_TOKEN`, direct NVIDIA credential binding, + approval, merge, release, and protection mutation are absent; +8. caller and reusable scheduler retain read-only workflow-token permissions, + no job-level write elevation, and no `github.token` mutation fallback; and +9. the focused quality workflow tracks both the caller and this doctoring file. + +Repository acceptance still requires exact-current-head workflow, security, +supply-chain, automated-review, independent-review, unresolved-thread, and +branch-protection evidence. + +## Rollback + +Rollback removes only the Inkspan caller, its static contract, doctoring, and +changelog entry. It leaves the reusable scheduler, Clearfolio caller, OpenCode +repair workflow, Noema/OpenCode reviewer identities, and credential chains +unchanged. Rollback must not replace the explicit target with the central +repository fallback, copy privileged scheduler implementation into Inkspan, +add `secrets: inherit`, introduce a `github.token` mutation fallback, or elevate +the workflow-generated token. + +## References (APA 7th edition) + +GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. Retrieved +August 6, 2026, from +https://docs.github.com/en/enterprise-cloud@latest/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule + +GitHub, Inc. (n.d.-b). *Reusing workflows*. GitHub Docs. Retrieved August 6, +2026, from +https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/reuse-automations/reuse-workflows + +GitHub, Inc. (n.d.-c). *Workflow syntax for GitHub Actions: Jobs..secrets*. +GitHub Docs. Retrieved August 6, 2026, from +https://docs.github.com/en/enterprise-cloud@latest/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idsecrets + +GitHub, Inc. (n.d.-d). *Workflow syntax for GitHub Actions: Permissions*. +GitHub Docs. Retrieved August 6, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions diff --git a/tests/test_pr_review_fix_hourly_contract.py b/tests/test_pr_review_fix_hourly_contract.py index ad1e7a117..63d0efb84 100644 --- a/tests/test_pr_review_fix_hourly_contract.py +++ b/tests/test_pr_review_fix_hourly_contract.py @@ -7,6 +7,7 @@ _REUSABLE_WORKFLOW = Path(".github/workflows/pr-review-fix-scheduler.yml") _CLEARFOLIO_CALLER = Path(".github/workflows/clearfolio-hourly-review-repair.yml") +_INKSPAN_CALLER = Path(".github/workflows/inkspan-hourly-review-repair.yml") _CONTRACT_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") @@ -15,11 +16,29 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") +def _assert_caller_keeps_github_token_read_only(path: Path) -> None: + """Require one product caller to use only explicit scheduler credentials.""" + text = _read(path) + workflow_scope, jobs_scope = text.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + for permission in ( + "actions: write", + "issues: write", + "contents: write", + "pull-requests: write", + "statuses: write", + ): + assert permission not in text + assert "\n permissions:\n" not in jobs_scope + + def test_clearfolio_caller_runs_once_each_hour() -> None: """Clearfolio receives the requested hourly bounded repair heartbeat.""" text = _read(_CLEARFOLIO_CALLER) assert 'cron: "23 * * * *"' in text + assert "workflow_dispatch:" not in text assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in text assert "target_repository: ContextualWisdomLab/clearfolio" in text assert "base_branch: main" in text @@ -29,25 +48,46 @@ def test_clearfolio_caller_runs_once_each_hour() -> None: assert "NVIDIA_NIM_API_KEY" not in text +def test_inkspan_caller_runs_once_each_hour() -> None: + """Inkspan receives an offset hourly bounded repair heartbeat.""" + text = _read(_INKSPAN_CALLER) + + assert 'cron: "37 * * * *"' in text + assert "workflow_dispatch:" not in text + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in text + assert "target_repository: ContextualWisdomLab/inkspan" in text + assert "base_branch: main" in text + assert 'max_dispatches: "1"' in text + assert 'retry_hours: "1"' in text + assert "COPILOT_GITHUB_TOKEN" not in text + assert "NVIDIA_NIM_API_KEY" not in text + + +def test_product_callers_have_distinct_single_flight_groups() -> None: + """Concurrent product heartbeats cannot cancel a sibling product run.""" + clearfolio = _read(_CLEARFOLIO_CALLER) + inkspan = _read(_INKSPAN_CALLER) + + assert "group: clearfolio-hourly-review-repair" in clearfolio + assert "group: inkspan-hourly-review-repair" in inkspan + assert "group: clearfolio-hourly-review-repair" not in inkspan + assert "group: inkspan-hourly-review-repair" not in clearfolio + assert "cancel-in-progress: true" in clearfolio + assert "cancel-in-progress: true" in inkspan + + def test_clearfolio_caller_keeps_github_token_read_only() -> None: - """The hourly caller delegates with explicit secrets and no token elevation.""" - text = _read(_CLEARFOLIO_CALLER) - workflow_scope, jobs_scope = text.split("\njobs:\n", maxsplit=1) + """Clearfolio delegates with explicit secrets and no token elevation.""" + _assert_caller_keeps_github_token_read_only(_CLEARFOLIO_CALLER) - assert "\npermissions:\n contents: read\n" in workflow_scope - for permission in ( - "actions: write", - "issues: write", - "contents: write", - "pull-requests: write", - "statuses: write", - ): - assert permission not in text - assert "\n permissions:\n" not in jobs_scope + +def test_inkspan_caller_keeps_github_token_read_only() -> None: + """Inkspan delegates with explicit secrets and no token elevation.""" + _assert_caller_keeps_github_token_read_only(_INKSPAN_CALLER) def test_reusable_scheduler_has_no_product_specific_timer() -> None: - """The shared scheduler stays modular while the caller owns product cadence.""" + """The shared scheduler stays modular while callers own product cadence.""" text = _read(_REUSABLE_WORKFLOW) target_expression = ( "github.event.client_payload.target_repository || " @@ -59,18 +99,26 @@ def test_reusable_scheduler_has_no_product_specific_timer() -> None: assert "\n schedule:\n" not in text assert text.count(target_expression) == 2 assert "ContextualWisdomLab/clearfolio" not in text + assert "ContextualWisdomLab/inkspan" not in text def test_reusable_scheduler_declares_only_required_caller_secrets() -> None: - """The caller passes only established credentials without token fallback.""" + """Each caller passes only established credentials without token fallback.""" reusable = _read(_REUSABLE_WORKFLOW) - caller = _read(_CLEARFOLIO_CALLER) assert "PR_REVIEW_MERGE_TOKEN:" in reusable assert "OPENCODE_APPROVE_TOKEN:" in reusable - 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 + for caller_path in (_CLEARFOLIO_CALLER, _INKSPAN_CALLER): + caller = _read(caller_path) + 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 assert ( "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " "secrets.OPENCODE_APPROVE_TOKEN }}" @@ -112,7 +160,6 @@ def test_review_fix_scheduler_retries_same_head_after_one_hour() -> None: def test_review_fix_scheduler_remains_bounded_and_single_flight() -> None: """Higher cadence never expands mutation volume or parallel execution.""" reusable = _read(_REUSABLE_WORKFLOW) - caller = _read(_CLEARFOLIO_CALLER) dispatch_block = reusable.split("max_dispatches:", maxsplit=1)[1].split( "target_repository:", maxsplit=1 @@ -120,11 +167,14 @@ def test_review_fix_scheduler_remains_bounded_and_single_flight() -> None: assert 'default: "1"' in dispatch_block assert "cancel-in-progress: true" in reusable assert "MAX_DISPATCHES" in reusable - assert "cancel-in-progress: true" in caller + assert "cancel-in-progress: true" in _read(_CLEARFOLIO_CALLER) + assert "cancel-in-progress: true" in _read(_INKSPAN_CALLER) -def test_contract_workflow_tracks_the_product_caller() -> None: - """Changes to the active Clearfolio caller always rerun the focused gate.""" +def test_contract_workflow_tracks_product_callers() -> None: + """Changes to either active product caller rerun the focused gate.""" text = _read(_CONTRACT_WORKFLOW) assert text.count(".github/workflows/clearfolio-hourly-review-repair.yml") == 2 + assert text.count(".github/workflows/inkspan-hourly-review-repair.yml") == 2 + assert text.count("docs/doctoring/inkspan-hourly-review-caller.md") == 2