From e3d770704e37314d64e289499ebd0f975dcf9b18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:26:03 +0900 Subject: [PATCH 001/152] docs: design hourly OpenCode maintenance agent --- ...8-04-hourly-opencode-maintenance-design.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md new file mode 100644 index 00000000..ce1fd156 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -0,0 +1,78 @@ +# Hourly OpenCode Maintenance Agent Design + +## Purpose + +mightyETL needs a scheduled development loop that can inspect the current repository state, remediate one bounded item, and open or update a pull request without weakening the independent review and merge gates. The scheduled agent must use OpenCode with the repository secret `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot and must not change the credentials or configuration of the existing review agent. + +## Decision + +Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Keep `.github/workflows/hourly-pr-disposition.yml` unchanged as the deterministic, fail-closed merge-disposition boundary. + +The maintenance workflow will: + +- run at minute 43 of every hour and on manual dispatch; +- use an immutable full-length SHA for `actions/checkout` and disable persisted checkout credentials; +- install the exact OpenCode package version `opencode-ai@1.18.13` rather than invoking a mutable `@latest` action; +- map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; +- select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; +- use the repository-scoped `GITHUB_TOKEN`, without OpenCode OIDC exchange, and grant only the write permissions needed to create branches, pull requests, and issues plus read access to checks and statuses; +- disable public session sharing; +- cap each run with workflow and process timeouts; +- run from the protected default branch and never from pull-request code. + +## Agent authority boundary + +The prompt is part of the security boundary. The OpenCode agent may inspect, test, edit, commit, push a feature branch, update one existing pull request, or open one pull request. It must not: + +- approve or merge a pull request; +- push directly to `develop` or `main`; +- bypass checks, branch protection, security gates, or independent review; +- alter review-agent workflows, review-agent secret names, `CODEOWNERS`, branch protection, or repository secrets; +- modify its own workflow or other `.github/workflows/**` files unless an open issue explicitly carries the `automation-maintenance` label; +- expose secret values, payloads, raw principals, raw idempotency keys, or internal exception details; +- create a second development pull request while another development pull request is open. + +The deterministic hourly disposition workflow remains responsible for exact-head merge eligibility. Branch protection remains authoritative even if the maintenance agent proposes a change. + +## Data and control flow + +1. GitHub starts the scheduled workflow from `develop`. +2. Checkout reads the default-branch source with credentials persistence disabled. +3. npm installs the pinned OpenCode CLI and verifies its exact version. +4. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, and `NVIDIA_API_KEY` alias. +5. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. +6. The agent leaves work in a feature branch and pull request. It does not approve or merge. +7. Existing CI, security, review, and the deterministic disposition workflow evaluate the exact head independently. + +## Failure behavior + +- A missing `NVIDIA_NIM_API_KEY`, unavailable NVIDIA endpoint, OpenCode installation mismatch, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- No fallback provider or Copilot credential is configured. +- Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. +- A timed-out process receives `TERM` before the workflow hard timeout. +- The workflow does not publish a release or merge partial work. + +## Verification + +A repository test will parse the workflow as text and fail unless it proves all of the following: + +- hourly off-peak schedule, serialized concurrency, and bounded timeout; +- full-SHA checkout pinning and disabled credential persistence; +- exact OpenCode version pin and no `@latest` use; +- exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; +- no Copilot, Anthropic, or OpenAI credential path; +- private session setting and direct `GITHUB_TOKEN` mode; +- least-privilege permissions without `id-token`; +- prompt prohibitions against approval, merge, protected-branch pushes, review-agent key changes, workflow self-modification, and duplicate pull requests. + +## References + +Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ + +Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ + +GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication + +GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows + +NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis \ No newline at end of file From 6e9219d00f44df7519cf6c18e19901528f535d90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:26:43 +0900 Subject: [PATCH 002/152] docs: plan hourly OpenCode maintenance agent --- ...-08-04-hourly-opencode-maintenance-plan.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md new file mode 100644 index 00000000..d38837cf --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -0,0 +1,164 @@ +# Hourly OpenCode Maintenance Agent Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. + +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. + +**Tech Stack:** GitHub Actions, OpenCode 1.18.13, NVIDIA NIM, bash, Maven, JUnit 5. + +## Global Constraints + +- Keep `.github/workflows/hourly-pr-disposition.yml` and review-agent key configuration unchanged. +- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it to OpenCode as `NVIDIA_API_KEY`. +- Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. +- Pin third-party workflow sources and executable package versions immutably. +- The scheduled agent may open or update a pull request, but may never approve, merge, bypass protection, or push to `develop` or `main`. +- Preserve standalone operation and modular CWL service compatibility. + +--- + +### Task 1: Add the fail-closed workflow contract test + +**Files:** +- Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` + +**Interfaces:** +- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`. +- Produces: a text-level security and configuration contract for `.github/workflows/hourly-opencode-maintenance.yml`. + +- [ ] **Step 1: Write the failing test** + +Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, timeout, immutable checkout SHA, `persist-credentials: false`, exact `opencode-ai@1.18.13` installation, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, least-privilege permissions, no `id-token`, and prompt prohibitions. + +- [ ] **Step 2: Run the focused test to verify RED** + +Run: + +```bash +./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test +``` + +Expected: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. + +- [ ] **Step 3: Commit the failing contract** + +```bash +git add etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +git commit -m "test(ci): define hourly OpenCode maintenance contract" +``` + +### Task 2: Implement the pinned NVIDIA OpenCode workflow + +**Files:** +- Create: `.github/workflows/hourly-opencode-maintenance.yml` + +**Interfaces:** +- Consumes: repository secret `NVIDIA_NIM_API_KEY`, built-in `GITHUB_TOKEN`, OpenCode CLI 1.18.13. +- Produces: one serialized scheduled maintenance run that can prepare a feature-branch pull request but cannot approve or merge. + +- [ ] **Step 1: Add the minimal workflow** + +Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, minimal permissions, a full-SHA checkout with disabled persisted credentials, exact npm installation, exact version verification, and `timeout --signal=TERM 45m opencode github run`. + +Set: + +```yaml +GITHUB_TOKEN: ${{ github.token }} +NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} +MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct +AGENT: build +SHARE: "false" +USE_GITHUB_TOKEN: "true" +``` + +The prompt must encode every authority boundary from the design and must instruct the agent to inspect all exact current PR heads before selecting work. + +- [ ] **Step 2: Run the focused test to verify GREEN** + +```bash +./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test +``` + +Expected: PASS. + +- [ ] **Step 3: Run workflow and documentation contract tests** + +```bash +./mvnw -pl etl-service -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test +``` + +Expected: PASS with no skipped tests. + +- [ ] **Step 4: Commit the workflow** + +```bash +git add .github/workflows/hourly-opencode-maintenance.yml +git commit -m "ci: schedule NVIDIA OpenCode maintenance agent" +``` + +### Task 3: Document operations and release notes + +**Files:** +- Create: `docs/operations/hourly-opencode-maintenance.md` +- Modify: `CHANGELOG.md` + +**Interfaces:** +- Consumes: behavior and constraints from the workflow. +- Produces: beginner-readable activation, failure, rollback, security, and evidence documentation. + +- [ ] **Step 1: Add the operator document** + +Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. + +- [ ] **Step 2: Update `CHANGELOG.md`** + +Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. + +- [ ] **Step 3: Run the full reactor tests** + +```bash +./mvnw -B test +``` + +Expected: all modules build successfully; no project test is skipped. + +- [ ] **Step 4: Commit documentation** + +```bash +git add docs/operations/hourly-opencode-maintenance.md CHANGELOG.md +git commit -m "docs(ci): document OpenCode maintenance operations" +``` + +### Task 4: Verify and open the protected workflow-change pull request + +**Files:** +- No additional source files. + +**Interfaces:** +- Consumes: completed branch and all exact-head test results. +- Produces: a ready-for-review pull request targeting `develop`. + +- [ ] **Step 1: Verify exact branch head and diff** + +```bash +git status --short +git rev-parse HEAD +git diff develop...HEAD --check +./mvnw -B test +``` + +Expected: clean tree, no whitespace errors, successful build. + +- [ ] **Step 2: Open the pull request** + +Open a pull request titled `ci: schedule NVIDIA OpenCode maintenance agent`, explain the credential isolation and authority boundary, and apply `automerge-workflow` because the deterministic disposition workflow requires explicit approval for workflow changes. + +- [ ] **Step 3: Request independent review and verify exact-head checks** + +Request CodeRabbit and any configured independent reviewer. Treat queued, pending, skipped-required, stale-head, or cancelled checks as not passing. Resolve only addressed current threads. + +- [ ] **Step 4: Merge only after every repository gate passes** + +Do not self-approve. Merge only when branch protection, independent approval, security gates, repository policy, exact-head checks, and workflow-change policy are all satisfied. \ No newline at end of file From ec45a1bdd3509836745318ecd55c3bcc61ffe688 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:27:35 +0900 Subject: [PATCH 003/152] test(ci): define hourly OpenCode maintenance contract --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java new file mode 100644 index 00000000..2a2d5d03 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -0,0 +1,177 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the credential, authority, and supply-chain boundaries of the scheduled OpenCode + * maintenance workflow. + * + *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a + * reviewer or merger. These tests make that separation visible to beginners and prevent a + * later workflow edit from silently adding a fallback provider, mutable tool version, elevated + * token permission, protected-branch push, or self-approval path.

+ */ +class HourlyOpenCodeMaintenanceWorkflowTest { + + private static final Pattern SECRET_REFERENCE = Pattern.compile( + "\\$\\{\\{\\s*secrets\\.([A-Z0-9_]+)\\s*}}" + ); + + private static String workflow; + + /** + * Reads the workflow once after first producing an ordinary assertion failure when the + * production workflow has not yet been implemented. + * + * @throws IOException when the workflow exists but cannot be read as UTF-8 text + */ + @BeforeAll + static void readWorkflow() throws IOException { + Path workflowPath = projectRoot().resolve( + ".github/workflows/hourly-opencode-maintenance.yml" + ); + assertTrue( + Files.exists(workflowPath), + "The hourly OpenCode maintenance workflow must exist" + ); + workflow = Files.readString(workflowPath, StandardCharsets.UTF_8); + } + + /** + * Verifies that runs are offset from the top of the hour, serialized, and time bounded. + */ + @Test + void schedulesOneBoundedNonOverlappingRunPerHour() { + assertTrue(workflow.contains("cron: \"43 * * * *\"")); + assertTrue(workflow.contains("group: hourly-opencode-maintenance")); + assertTrue(workflow.contains("cancel-in-progress: false")); + assertTrue(workflow.contains("timeout-minutes: 50")); + assertTrue(workflow.contains("timeout --signal=TERM 45m opencode github run")); + } + + /** + * Verifies that repository source and the OpenCode executable are pinned without retaining + * checkout credentials that a generated process could reuse implicitly. + */ + @Test + void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { + assertTrue(workflow.contains( + "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" + )); + assertTrue(workflow.contains("fetch-depth: 1")); + assertTrue(workflow.contains("persist-credentials: false")); + assertTrue(workflow.contains("npm install --global opencode-ai@1.18.13")); + assertTrue(workflow.contains("test \"$(opencode --version)\" = \"1.18.13\"")); + assertFalse(workflow.contains("opencode-ai@latest")); + assertFalse(workflow.contains("anomalyco/opencode/github@")); + } + + /** + * Verifies that the repository's NVIDIA NIM secret is the only model credential and is + * mapped to the environment variable documented by OpenCode's NVIDIA provider. + */ + @Test + void usesOnlyTheNvidiaNimCredentialAndExplicitModel() { + assertEquals(Set.of("NVIDIA_NIM_API_KEY"), referencedSecrets()); + assertTrue(workflow.contains( + "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" + )); + assertTrue(workflow.contains( + "MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct" + )); + assertTrue(workflow.contains("SHARE: \"false\"")); + assertTrue(workflow.contains("USE_GITHUB_TOKEN: \"true\"")); + + String lowerCaseWorkflow = workflow.toLowerCase(Locale.ROOT); + assertFalse(lowerCaseWorkflow.contains("copilot")); + assertFalse(workflow.contains("ANTHROPIC_API_KEY")); + assertFalse(workflow.contains("OPENAI_API_KEY")); + } + + /** + * Verifies least-privilege repository access and rejects an unnecessary OIDC token path. + */ + @Test + void grantsOnlyRepositoryMaintenancePermissions() { + assertTrue(workflow.contains("contents: write")); + assertTrue(workflow.contains("pull-requests: write")); + assertTrue(workflow.contains("issues: write")); + assertTrue(workflow.contains("checks: read")); + assertTrue(workflow.contains("statuses: read")); + assertFalse(workflow.contains("id-token:")); + assertFalse(workflow.contains("actions: write")); + assertFalse(workflow.contains("security-events: write")); + } + + /** + * Verifies that the model prompt preserves independent review and deterministic merge + * authority instead of granting the development agent governance powers. + */ + @Test + void promptForbidsReviewMergeAndProtectedBranchBypass() { + assertTrue(workflow.contains("Start every run by inspecting every open pull request")); + assertTrue(workflow.contains("exact current head")); + assertTrue(workflow.contains("Never approve or merge a pull request")); + assertTrue(workflow.contains("Never push directly to develop or main")); + assertTrue(workflow.contains("Do not bypass branch protection")); + assertTrue(workflow.contains("Do not alter the existing review agent")); + assertTrue(workflow.contains("Do not change any review-agent secret name")); + assertTrue(workflow.contains("Do not modify .github/workflows/")); + assertTrue(workflow.contains("automation-maintenance")); + assertTrue(workflow.contains("Do not create a second development pull request")); + assertTrue(workflow.contains("Do not print, echo, summarize, or expose secret values")); + } + + /** + * Extracts every repository-secret name referenced by the workflow. + * + * @return immutable set of referenced GitHub Actions secret identifiers + */ + private static Set referencedSecrets() { + Matcher matcher = SECRET_REFERENCE.matcher(workflow); + Set secretNames = new java.util.HashSet<>(); + while (matcher.find()) { + secretNames.add(matcher.group(1)); + } + return Set.copyOf(secretNames); + } + + /** + * Finds the reactor root from either root or module-local Maven execution. + * + * @return absolute path that contains the root Maven project + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 856667e5f0c528c9a32ace9146a965f5ff296cc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:31:38 +0900 Subject: [PATCH 004/152] ci: schedule NVIDIA OpenCode maintenance agent --- .../workflows/hourly-opencode-maintenance.yml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/hourly-opencode-maintenance.yml diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml new file mode 100644 index 00000000..90797d4a --- /dev/null +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -0,0 +1,70 @@ +name: Hourly OpenCode maintenance + +on: + schedule: + - cron: "43 * * * *" + workflow_dispatch: + +concurrency: + group: hourly-opencode-maintenance + cancel-in-progress: false + +permissions: + actions: read + checks: read + contents: write + issues: write + pull-requests: write + security-events: read + statuses: read + +jobs: + maintain-repository: + runs-on: ubuntu-latest + timeout-minutes: 50 + steps: + - name: Checkout protected default-branch source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install pinned OpenCode CLI + shell: bash + run: | + set -euo pipefail + npm install --global opencode-ai@1.18.13 --no-audit --no-fund + test "$(opencode --version)" = "1.18.13" + + - name: Run bounded NVIDIA OpenCode maintenance + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct + AGENT: build + SHARE: "false" + USE_GITHUB_TOKEN: "true" + PROMPT: | + Maintain ContextualWisdomLab/mightyETL toward defensible commercial and acquisition readiness. + + Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only threads that the current exact head actually addresses. + + If a development pull request is open, work only on the dependency-eligible exact current head of that pull request. Do not create a second development pull request while another development pull request is open. Implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and update the existing feature branch and pull request. Close only clearly superseded duplicate pull requests with a precise reason. + + If no development pull request is open, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. Prefer issue #120 while its durable lease-fenced worker gap remains open and dependency-ready. + + Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services. Database objects must contain at least two descriptive words and use snake_case by default. Public production APIs require complete beginner-readable documentation. Added production statements and branches require deterministic coverage. Use current authoritative standards, primary technical documentation, and peer-reviewed evidence where material, with APA 7th references in repository documentation. + + Never approve or merge a pull request. Never push directly to develop or main. Do not bypass branch protection, independent approval, security gates, repository policy, required checks, or test coverage. Do not publish a release unless a separate release-authorized workflow and every acceptance gate explicitly permit it. + + Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect, print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, prepare a pull request and leave approval and merge to an independent gate. + + Use the repository-scoped GitHub token only for the minimum branch, pull-request, issue, check, status, and security-read operations required by this run. Keep the working tree and pull-request description truthful about tests, skipped coverage, remaining risks, and release readiness. Leave one reviewable feature branch and pull request; never represent pending external gates as completed. + run: | + set -euo pipefail + if [[ -z "${NVIDIA_API_KEY}" ]]; then + echo "NVIDIA_NIM_API_KEY is required for scheduled OpenCode maintenance" >&2 + exit 1 + fi + timeout --signal=TERM 45m opencode github run From 995bfe3de04bf56a8bee5fd0544ef5a77839f933 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:34:04 +0900 Subject: [PATCH 005/152] fix(ci): make secret handling boundary explicit --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 90797d4a..748528d0 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -58,7 +58,7 @@ jobs: Never approve or merge a pull request. Never push directly to develop or main. Do not bypass branch protection, independent approval, security gates, repository policy, required checks, or test coverage. Do not publish a release unless a separate release-authorized workflow and every acceptance gate explicitly permit it. - Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect, print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, prepare a pull request and leave approval and merge to an independent gate. + Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect secret values. Do not print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, prepare a pull request and leave approval and merge to an independent gate. Use the repository-scoped GitHub token only for the minimum branch, pull-request, issue, check, status, and security-read operations required by this run. Keep the working tree and pull-request description truthful about tests, skipped coverage, remaining risks, and release readiness. Leave one reviewable feature branch and pull request; never represent pending external gates as completed. run: | From ff5065e925994cd84e2b62c10ed9aa1e0541fdfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:36:35 +0900 Subject: [PATCH 006/152] docs(ci): document OpenCode maintenance operations --- .../operations/hourly-opencode-maintenance.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/operations/hourly-opencode-maintenance.md diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md new file mode 100644 index 00000000..9adba60a --- /dev/null +++ b/docs/operations/hourly-opencode-maintenance.md @@ -0,0 +1,131 @@ +# Hourly OpenCode maintenance + +## Purpose + +`.github/workflows/hourly-opencode-maintenance.yml` runs a bounded development agent at minute 43 of every hour, in UTC, and on manual dispatch. The agent uses OpenCode 1.18.13 with NVIDIA NIM to inspect the repository, repair one existing development pull request, or prepare one bounded buyer-visible improvement when no development pull request is open. + +This workflow is intentionally separate from both code review and merge disposition. It does not replace independent review, GitHub branch protection, required checks, GitHub Advanced Security, Dependabot, CodeRabbit, or `.github/workflows/hourly-pr-disposition.yml`. + +## Required repository secret + +Create or retain exactly this GitHub Actions repository secret: + +```text +NVIDIA_NIM_API_KEY +``` + +The workflow exposes that value only to the OpenCode process as the provider variable documented by OpenCode: + +```text +NVIDIA_API_KEY +``` + +The secret name used by the existing review agent is not changed. The scheduled workflow has no fallback credential for GitHub Copilot, Anthropic, OpenAI, or another model provider. A missing or empty `NVIDIA_NIM_API_KEY` fails the run before the agent starts. + +Never place the key in repository variables, source files, workflow output, issue comments, pull-request descriptions, step summaries, command arguments, or diagnostic logs. + +## Pinned execution contract + +| Control | Pinned value | +| --- | --- | +| Schedule | `43 * * * *` | +| OpenCode package | `opencode-ai@1.18.13` | +| OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | +| Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | +| OpenCode process timeout | 45 minutes, terminated with `TERM` | +| GitHub job timeout | 50 minutes | +| Session sharing | disabled | +| Overlapping runs | disabled; an active run is not cancelled | + +The workflow installs the exact npm package version and verifies `opencode --version` before use. It does not invoke the mutable OpenCode GitHub Action tag or a floating `latest` package version. Checkout credentials are not persisted in the working tree. + +## Repository permissions + +The job receives only these explicit `GITHUB_TOKEN` permissions: + +- `actions: read` +- `checks: read` +- `contents: write` +- `issues: write` +- `pull-requests: write` +- `security-events: read` +- `statuses: read` + +There is no `id-token` permission and no permission to write Actions or security events. `contents: write` is needed to prepare a feature branch; repository branch protection remains authoritative for protected branches. + +## Authority boundaries + +The scheduled development agent starts by inspecting every open pull request and its exact current head. It may update one dependency-eligible development pull request or, when none exists, create one feature branch and pull request for one bounded vertical slice. + +The agent must not: + +- approve or merge a pull request; +- push directly to `develop` or `main`; +- treat queued, pending, skipped-required, cancelled, stale-head, absent, or failed checks as passing; +- bypass independent review, branch protection, repository policy, security gates, or coverage requirements; +- change the existing review agent, its provider, its workflow, its credential flow, or any review-agent secret name; +- modify `.github/workflows/**` or `CODEOWNERS` unless an open issue with the `automation-maintenance` label authorizes that exact change; +- inspect or disclose secret values; +- create a second development pull request while another development pull request is open; +- publish a release unless a separate release-authorized workflow and all release acceptance gates permit it. + +`.github/workflows/hourly-pr-disposition.yml` remains the deterministic exact-head merge boundary. It evaluates review state, unresolved threads, named checks, status contexts, labels, mergeability, and expected head SHA independently from the development agent. + +## Normal run sequence + +1. GitHub starts the workflow from the default branch. +2. The workflow checks out a shallow copy with persisted credentials disabled. +3. It installs and verifies OpenCode 1.18.13. +4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY`. +5. OpenCode inspects all current pull requests before selecting any work. +6. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. +7. Independent review and repository checks evaluate the exact new head. +8. The separate disposition workflow may merge only after every gate passes. + +## Failure handling + +| Failure | Expected result | Operator action | +| --- | --- | --- | +| Secret missing or empty | Job fails before OpenCode starts | Restore `NVIDIA_NIM_API_KEY`; never add a fallback key | +| Exact OpenCode version unavailable or mismatched | Installation step fails | Investigate npm availability and supply-chain status before changing the pin | +| NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | +| Process exceeds 45 minutes | `timeout` sends `TERM` and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | +| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior | +| Tests or security checks fail | Pull request remains unmergeable | Fix the current exact head; never weaken the gate | +| Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | +| Another hourly run starts while one is active | New run waits because concurrency is serialized | No action unless the prior run is stuck | + +The workflow must not claim success for partial work. A failed run may leave a feature branch or pull request for inspection, but it cannot approve or merge it. + +## Rollback and disablement + +This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract test, this runbook, the design and plan documents, and the corresponding `CHANGELOG.md` entry through a reviewed pull request. + +Do not delete or rename `NVIDIA_NIM_API_KEY` when it is also used by other approved workflows. Disabling this workflow does not require changing the review agent or its credential scheme. + +## Verification checklist + +Before merging a workflow change, verify the exact current head has: + +- successful Windows, Ubuntu, and macOS CI; +- successful dependency review, SBOM, Semgrep, Trivy, OSV, and Scorecard gates required by repository policy; +- no unresolved current review thread; +- independent approval; +- the `automerge-workflow` label required by the deterministic disposition workflow; +- no new secret reference other than `NVIDIA_NIM_API_KEY`; +- no mutable OpenCode package or action reference; +- no review-agent credential or workflow change. + +## References + +Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ + +Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ + +GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication + +GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows + +GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions + +NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis \ No newline at end of file From 7e5e047f37f8821acc4d6308576decc2db626725 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:38:03 +0900 Subject: [PATCH 007/152] docs(ci): record NVIDIA OpenCode maintenance loop --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b08b8c..5108da6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. - Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`. - ETL problem-details client and operator contract: `docs/api/problem-details.md`. From 844642cd331f5a1a7278a02612dd30dab4491d27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:52:10 +0900 Subject: [PATCH 008/152] test(ci): require direct-token git bootstrap --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 2a2d5d03..93bfef36 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -81,6 +81,40 @@ void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { assertFalse(workflow.contains("anomalyco/opencode/github@")); } + /** + * Verifies that direct-token mode can actually create commits and push a feature branch. + * + *

OpenCode 1.18.13 intentionally skips its internal Git credential and author setup when + * {@code USE_GITHUB_TOKEN=true}. Because checkout credentials remain disabled, the workflow + * must install a local, short-lived authorization header and local author identity before + * starting OpenCode, then remove the authorization header even when the process fails or + * times out.

+ */ + @Test + void bootstrapsAndRemovesDirectTokenGitCredentials() { + assertTrue(workflow.contains( + "git_config_key=\"http.https://github.com/.extraheader\"" + )); + assertTrue(workflow.contains("cleanup_git_credentials()")); + assertTrue(workflow.contains("trap cleanup_git_credentials EXIT")); + assertTrue(workflow.contains( + "printf 'x-access-token:%s' \"${GITHUB_TOKEN}\"" + )); + assertTrue(workflow.contains( + "git config --local \"${git_config_key}\" " + + "\"AUTHORIZATION: basic ${github_basic_auth}\"" + )); + assertTrue(workflow.contains( + "git config --local user.name \"opencode-agent[bot]\"" + )); + assertTrue(workflow.contains( + "git config --local user.email " + + "\"opencode-agent[bot]@users.noreply.github.com\"" + )); + assertTrue(workflow.contains("unset github_basic_auth")); + assertFalse(workflow.contains("AGENT: build")); + } + /** * Verifies that the repository's NVIDIA NIM secret is the only model credential and is * mapped to the environment variable documented by OpenCode's NVIDIA provider. From c7b168d829dded0d5a9f86f6d331661206d110ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:52:51 +0900 Subject: [PATCH 009/152] fix(ci): bootstrap OpenCode direct-token git access --- .../workflows/hourly-opencode-maintenance.yml | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 748528d0..4f40cd34 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -42,7 +42,6 @@ jobs: GITHUB_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct - AGENT: build SHARE: "false" USE_GITHUB_TOKEN: "true" PROMPT: | @@ -67,4 +66,26 @@ jobs: echo "NVIDIA_NIM_API_KEY is required for scheduled OpenCode maintenance" >&2 exit 1 fi + if [[ -z "${GITHUB_TOKEN}" ]]; then + echo "The repository-scoped GITHUB_TOKEN is required for scheduled maintenance" >&2 + exit 1 + fi + + # OpenCode 1.18.13 skips its internal Git credential and author setup in + # USE_GITHUB_TOKEN mode. Bootstrap a repository-local header explicitly while + # preserving checkout's persist-credentials: false supply-chain boundary. + git_config_key="http.https://github.com/.extraheader" + cleanup_git_credentials() { + git config --local --unset-all "${git_config_key}" >/dev/null 2>&1 || true + } + trap cleanup_git_credentials EXIT + cleanup_git_credentials + github_basic_auth="$( + printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 | tr -d '\r\n' + )" + git config --local "${git_config_key}" "AUTHORIZATION: basic ${github_basic_auth}" + unset github_basic_auth + git config --local user.name "opencode-agent[bot]" + git config --local user.email "opencode-agent[bot]@users.noreply.github.com" + timeout --signal=TERM 45m opencode github run From 603a5ade1acf72f974e6e2194058bade7de78cbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:54:30 +0900 Subject: [PATCH 010/152] docs(ci): explain direct-token Git bootstrap --- .../operations/hourly-opencode-maintenance.md | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 9adba60a..be2efec3 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -31,6 +31,7 @@ Never place the key in repository variables, source files, workflow output, issu | Schedule | `43 * * * *` | | OpenCode package | `opencode-ai@1.18.13` | | OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | +| OpenCode agent | Repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | | OpenCode process timeout | 45 minutes, terminated with `TERM` | | GitHub job timeout | 50 minutes | @@ -39,6 +40,23 @@ Never place the key in repository variables, source files, workflow output, issu The workflow installs the exact npm package version and verifies `opencode --version` before use. It does not invoke the mutable OpenCode GitHub Action tag or a floating `latest` package version. Checkout credentials are not persisted in the working tree. +OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It deliberately omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. + +## Direct-token Git bootstrap + +The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal `configureGit` function. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS authorization header. + +Before starting OpenCode, the workflow therefore: + +1. fails closed when `GITHUB_TOKEN` is absent; +2. removes any pre-existing repository-local GitHub authorization header; +3. derives a Basic authorization header in memory from `x-access-token:${GITHUB_TOKEN}` without printing it; +4. stores that header only in the checked-out repository's local Git configuration; +5. sets the local commit author to `opencode-agent[bot]`; +6. installs an `EXIT` trap that removes the authorization header after success, failure, or timeout. + +The local author identity contains no credential. The authorization header is short-lived on the ephemeral runner and is not committed. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No OIDC or fallback model credential is introduced. + ## Repository permissions The job receives only these explicit `GITHUB_TOKEN` permissions: @@ -76,21 +94,25 @@ The agent must not: 1. GitHub starts the workflow from the default branch. 2. The workflow checks out a shallow copy with persisted credentials disabled. 3. It installs and verifies OpenCode 1.18.13. -4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY`. -5. OpenCode inspects all current pull requests before selecting any work. -6. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. -7. Independent review and repository checks evaluate the exact new head. -8. The separate disposition workflow may merge only after every gate passes. +4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that `GITHUB_TOKEN` is present. +5. It installs the repository-local Git author and short-lived authorization header required by OpenCode's direct-token path. +6. OpenCode inspects all current pull requests before selecting any work. +7. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. +8. The shell `EXIT` trap removes the local Git authorization header. +9. Independent review and repository checks evaluate the exact new head. +10. The separate disposition workflow may merge only after every gate passes. ## Failure handling | Failure | Expected result | Operator action | | --- | --- | --- | | Secret missing or empty | Job fails before OpenCode starts | Restore `NVIDIA_NIM_API_KEY`; never add a fallback key | +| Repository token missing | Job fails before Git bootstrap | Restore normal GitHub Actions token availability; do not add a personal token | +| Git bootstrap fails | Job fails before OpenCode starts | Inspect local Git configuration commands and retain `persist-credentials: false` | | Exact OpenCode version unavailable or mismatched | Installation step fails | Investigate npm availability and supply-chain status before changing the pin | | NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | -| Process exceeds 45 minutes | `timeout` sends `TERM` and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | -| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior | +| Process exceeds 45 minutes | `timeout` sends `TERM`, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | +| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior and verify the ephemeral runner was destroyed | | Tests or security checks fail | Pull request remains unmergeable | Fix the current exact head; never weaken the gate | | Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | | Another hourly run starts while one is active | New run waits because concurrency is serialized | No action unless the prior run is stuck | @@ -114,10 +136,14 @@ Before merging a workflow change, verify the exact current head has: - the `automerge-workflow` label required by the deterministic disposition workflow; - no new secret reference other than `NVIDIA_NIM_API_KEY`; - no mutable OpenCode package or action reference; +- `persist-credentials: false` plus explicit local direct-token Git bootstrap and `EXIT` cleanup; +- no ineffective `AGENT` environment claim for raw OpenCode 1.18.13; - no review-agent credential or workflow change. ## References +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts + Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ @@ -128,4 +154,4 @@ GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs. GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions -NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis \ No newline at end of file +NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis From b19d032a87f4d03253896dcc23a5d24f69d9e101 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:55:46 +0900 Subject: [PATCH 011/152] docs(ci): align plan with direct-token behavior --- ...-08-04-hourly-opencode-maintenance-plan.md | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index d38837cf..8b4e46e6 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,7 +4,7 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local, short-lived Git authorization header because OpenCode 1.18.13 skips its own Git setup in that mode. **Tech Stack:** GitHub Actions, OpenCode 1.18.13, NVIDIA NIM, bash, Maven, JUnit 5. @@ -14,6 +14,8 @@ - Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it to OpenCode as `NVIDIA_API_KEY`. - Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. - Pin third-party workflow sources and executable package versions immutably. +- Keep checkout credential persistence disabled; bootstrap only a repository-local direct-token authorization header and remove it through an `EXIT` trap. +- Do not claim that raw OpenCode 1.18.13 consumes an `AGENT` environment variable; it uses repository `default_agent` configuration or its `build` fallback. - The scheduled agent may open or update a pull request, but may never approve, merge, bypass protection, or push to `develop` or `main`. - Preserve standalone operation and modular CWL service compatibility. @@ -25,13 +27,15 @@ - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` **Interfaces:** -- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`. +- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest` and the pinned OpenCode 1.18.13 GitHub-handler behavior. - Produces: a text-level security and configuration contract for `.github/workflows/hourly-opencode-maintenance.yml`. - [ ] **Step 1: Write the failing test** Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, timeout, immutable checkout SHA, `persist-credentials: false`, exact `opencode-ai@1.18.13` installation, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, least-privilege permissions, no `id-token`, and prompt prohibitions. +Add a focused direct-token contract that requires a local GitHub authorization header, local bot author identity, credential cleanup through an `EXIT` trap, and removal of the ineffective `AGENT: build` environment claim. + - [ ] **Step 2: Run the focused test to verify RED** Run: @@ -40,13 +44,16 @@ Run: ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test ``` -Expected: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. +Expected initially: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. + +After discovering the direct-token gap, expected regression RED: FAIL because the workflow has `persist-credentials: false` and `USE_GITHUB_TOKEN=true` without the local authorization header, author identity, and cleanup contract required for OpenCode 1.18.13 infrastructure-managed commits and pushes. - [ ] **Step 3: Commit the failing contract** ```bash git add etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java git commit -m "test(ci): define hourly OpenCode maintenance contract" +git commit -am "test(ci): require direct-token git bootstrap" ``` ### Task 2: Implement the pinned NVIDIA OpenCode workflow @@ -62,20 +69,34 @@ git commit -m "test(ci): define hourly OpenCode maintenance contract" Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, minimal permissions, a full-SHA checkout with disabled persisted credentials, exact npm installation, exact version verification, and `timeout --signal=TERM 45m opencode github run`. -Set: +Set only the OpenCode environment variables that raw OpenCode 1.18.13 consumes: ```yaml GITHUB_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct -AGENT: build SHARE: "false" USE_GITHUB_TOKEN: "true" +PROMPT: | + ... ``` The prompt must encode every authority boundary from the design and must instruct the agent to inspect all exact current PR heads before selecting work. -- [ ] **Step 2: Run the focused test to verify GREEN** +- [ ] **Step 2: Bootstrap direct-token Git access without persisted checkout credentials** + +Before starting OpenCode: + +1. fail closed if `GITHUB_TOKEN` is empty; +2. define the repository-local key `http.https://github.com/.extraheader`; +3. install an `EXIT` trap that removes that header; +4. create a Basic authorization value from `x-access-token:${GITHUB_TOKEN}` without printing it; +5. set the header and immediately unset the temporary shell variable; +6. configure repository-local `user.name` and `user.email` for `opencode-agent[bot]`. + +Do not persist checkout credentials, add a personal token, enable OIDC, or store the authorization value in tracked files. + +- [ ] **Step 3: Run the focused test to verify GREEN** ```bash ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test @@ -83,7 +104,7 @@ The prompt must encode every authority boundary from the design and must instruc Expected: PASS. -- [ ] **Step 3: Run workflow and documentation contract tests** +- [ ] **Step 4: Run workflow and documentation contract tests** ```bash ./mvnw -pl etl-service -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test @@ -91,11 +112,12 @@ Expected: PASS. Expected: PASS with no skipped tests. -- [ ] **Step 4: Commit the workflow** +- [ ] **Step 5: Commit the workflow** ```bash git add .github/workflows/hourly-opencode-maintenance.yml git commit -m "ci: schedule NVIDIA OpenCode maintenance agent" +git commit -am "fix(ci): bootstrap OpenCode direct-token git access" ``` ### Task 3: Document operations and release notes @@ -103,18 +125,19 @@ git commit -m "ci: schedule NVIDIA OpenCode maintenance agent" **Files:** - Create: `docs/operations/hourly-opencode-maintenance.md` - Modify: `CHANGELOG.md` +- Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` **Interfaces:** -- Consumes: behavior and constraints from the workflow. -- Produces: beginner-readable activation, failure, rollback, security, and evidence documentation. +- Consumes: behavior and constraints from the workflow and OpenCode 1.18.13 primary source. +- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, security, and evidence documentation. - [ ] **Step 1: Add the operator document** -Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. +Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, direct-token Git bootstrap, credential cleanup, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. -- [ ] **Step 2: Update `CHANGELOG.md`** +- [ ] **Step 2: Update the design and `CHANGELOG.md`** -Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. +Record why direct-token mode requires an explicit local Git bootstrap while checkout credential persistence remains disabled. Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. - [ ] **Step 3: Run the full reactor tests** @@ -127,7 +150,10 @@ Expected: all modules build successfully; no project test is skipped. - [ ] **Step 4: Commit documentation** ```bash -git add docs/operations/hourly-opencode-maintenance.md CHANGELOG.md +git add docs/operations/hourly-opencode-maintenance.md \ + docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md \ + docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md \ + CHANGELOG.md git commit -m "docs(ci): document OpenCode maintenance operations" ``` @@ -161,4 +187,4 @@ Request CodeRabbit and any configured independent reviewer. Treat queued, pendin - [ ] **Step 4: Merge only after every repository gate passes** -Do not self-approve. Merge only when branch protection, independent approval, security gates, repository policy, exact-head checks, and workflow-change policy are all satisfied. \ No newline at end of file +Do not self-approve. Merge only when branch protection, independent approval, security gates, repository policy, exact-head checks, and workflow-change policy are all satisfied. From a616fbeedce9cdecd9a98153aec5f1fc29c37955 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:56:30 +0900 Subject: [PATCH 012/152] docs(ci): harden direct-token credential lifecycle --- ...8-04-hourly-opencode-maintenance-design.md | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index ce1fd156..042eccca 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -16,10 +16,28 @@ The maintenance workflow will: - map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; - select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; - use the repository-scoped `GITHUB_TOKEN`, without OpenCode OIDC exchange, and grant only the write permissions needed to create branches, pull requests, and issues plus read access to checks and statuses; +- bootstrap a short-lived repository-local Git authorization header and bot author identity because OpenCode 1.18.13 skips internal Git setup when direct `GITHUB_TOKEN` mode is selected; +- remove the local authorization header through an `EXIT` trap after success, failure, or process timeout; +- omit the ineffective `AGENT` environment variable because raw OpenCode 1.18.13 uses repository `default_agent` configuration or its `build` fallback; - disable public session sharing; - cap each run with workflow and process timeouts; - run from the protected default branch and never from pull-request code. +## Direct-token credential lifecycle + +OpenCode 1.18.13 reads `USE_GITHUB_TOKEN`, uses `GITHUB_TOKEN` for GitHub API access, and then deliberately bypasses its `configureGit` function. Its scheduled-event path later invokes ordinary `git commit` and `git push` commands to publish the generated branch before creating a pull request. With `persist-credentials: false`, those commands otherwise have neither a commit author nor HTTPS push authorization. + +The workflow resolves this version-specific gap without weakening checkout isolation: + +1. checkout still persists no credential; +2. the run step fails closed if either `NVIDIA_API_KEY` or `GITHUB_TOKEN` is empty; +3. a Basic GitHub authorization header is derived in memory from the repository token and stored only in the checkout's local Git configuration; +4. local `user.name` and `user.email` identify infrastructure-created commits as `opencode-agent[bot]`; +5. an `EXIT` trap removes the authorization header even when OpenCode fails or the process timeout returns non-zero; +6. no personal token, OIDC permission, model fallback, tracked credential file, or review-agent secret is introduced. + +The agent process already requires `GITHUB_TOKEN` for GitHub API calls, so this bootstrap does not expand token scope. It makes the granted `contents: write` capability operational while keeping the credential bounded to the ephemeral runner and repository-local Git configuration. + ## Agent authority boundary The prompt is part of the security boundary. The OpenCode agent may inspect, test, edit, commit, push a feature branch, update one existing pull request, or open one pull request. It must not: @@ -39,17 +57,19 @@ The deterministic hourly disposition workflow remains responsible for exact-head 1. GitHub starts the scheduled workflow from `develop`. 2. Checkout reads the default-branch source with credentials persistence disabled. 3. npm installs the pinned OpenCode CLI and verifies its exact version. -4. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, and `NVIDIA_API_KEY` alias. -5. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. -6. The agent leaves work in a feature branch and pull request. It does not approve or merge. -7. Existing CI, security, review, and the deterministic disposition workflow evaluate the exact head independently. +4. The shell validates both required tokens, installs the repository-local Git author and short-lived authorization header, and registers credential cleanup. +5. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, and `NVIDIA_API_KEY` alias. +6. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. +7. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. +8. The shell removes the repository-local authorization header. +9. Existing CI, security, review, and the deterministic disposition workflow evaluate the exact head independently. ## Failure behavior -- A missing `NVIDIA_NIM_API_KEY`, unavailable NVIDIA endpoint, OpenCode installation mismatch, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- A missing `NVIDIA_NIM_API_KEY`, missing `GITHUB_TOKEN`, unavailable NVIDIA endpoint, OpenCode installation mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. -- A timed-out process receives `TERM` before the workflow hard timeout. +- A timed-out process receives `TERM` before the workflow hard timeout, and the shell cleanup trap removes the local Git authorization header. - The workflow does not publish a release or merge partial work. ## Verification @@ -62,11 +82,14 @@ A repository test will parse the workflow as text and fail unless it proves all - exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; - no Copilot, Anthropic, or OpenAI credential path; - private session setting and direct `GITHUB_TOKEN` mode; +- local direct-token Git authorization, bot author identity, `EXIT` cleanup, and no ineffective `AGENT: build` claim; - least-privilege permissions without `id-token`; - prompt prohibitions against approval, merge, protected-branch pushes, review-agent key changes, workflow self-modification, and duplicate pull requests. ## References +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts + Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ @@ -75,4 +98,6 @@ GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows -NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis \ No newline at end of file +GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions + +NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis From d23fefc3bc8fbc8b8a065e46ea0b33d3cd9add97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:57:46 +0900 Subject: [PATCH 013/152] docs(changelog): record direct-token Git fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5108da6e..8682c9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The hourly OpenCode direct-`GITHUB_TOKEN` path now bootstraps a repository-local bot author and short-lived GitHub authorization header, removes the header through an `EXIT` trap, retains `persist-credentials: false`, and no longer claims that raw OpenCode 1.18.13 consumes `AGENT: build`. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. - `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata. From 83b56c194447fda9df215486f66df17b573deab5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:04:18 +0900 Subject: [PATCH 014/152] test(ci): require forced OpenCode termination --- .../HourlyOpenCodeMaintenanceWorkflowTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 93bfef36..71d00ea9 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -53,7 +53,8 @@ static void readWorkflow() throws IOException { } /** - * Verifies that runs are offset from the top of the hour, serialized, and time bounded. + * Verifies that runs are offset from the top of the hour, serialized, and time bounded, + * including forced termination when an agent ignores the graceful termination signal. */ @Test void schedulesOneBoundedNonOverlappingRunPerHour() { @@ -61,7 +62,9 @@ void schedulesOneBoundedNonOverlappingRunPerHour() { assertTrue(workflow.contains("group: hourly-opencode-maintenance")); assertTrue(workflow.contains("cancel-in-progress: false")); assertTrue(workflow.contains("timeout-minutes: 50")); - assertTrue(workflow.contains("timeout --signal=TERM 45m opencode github run")); + assertTrue(workflow.contains( + "timeout --signal=TERM --kill-after=30s 45m opencode github run" + )); } /** From 613a753e8c30ba63ccf404c77d353e1d3f21e44c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:04:55 +0900 Subject: [PATCH 015/152] fix(ci): force termination after OpenCode timeout --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 4f40cd34..c03eca82 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -88,4 +88,4 @@ jobs: git config --local user.name "opencode-agent[bot]" git config --local user.email "opencode-agent[bot]@users.noreply.github.com" - timeout --signal=TERM 45m opencode github run + timeout --signal=TERM --kill-after=30s 45m opencode github run From b18c8597dffd3c4d5857a4b7dd71222d8c11353e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:05:44 +0900 Subject: [PATCH 016/152] docs(ci): document forced OpenCode termination --- docs/operations/hourly-opencode-maintenance.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index be2efec3..a19e8078 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -33,7 +33,7 @@ Never place the key in repository variables, source files, workflow output, issu | OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | | OpenCode agent | Repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | -| OpenCode process timeout | 45 minutes, terminated with `TERM` | +| OpenCode process timeout | `TERM` after 45 minutes, then `KILL` after a 30-second grace period | | GitHub job timeout | 50 minutes | | Session sharing | disabled | | Overlapping runs | disabled; an active run is not cancelled | @@ -53,7 +53,7 @@ Before starting OpenCode, the workflow therefore: 3. derives a Basic authorization header in memory from `x-access-token:${GITHUB_TOKEN}` without printing it; 4. stores that header only in the checked-out repository's local Git configuration; 5. sets the local commit author to `opencode-agent[bot]`; -6. installs an `EXIT` trap that removes the authorization header after success, failure, or timeout. +6. installs an `EXIT` trap that removes the authorization header after success, failure, graceful timeout, or forced process termination. The local author identity contains no credential. The authorization header is short-lived on the ephemeral runner and is not committed. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No OIDC or fallback model credential is introduced. @@ -111,7 +111,7 @@ The agent must not: | Git bootstrap fails | Job fails before OpenCode starts | Inspect local Git configuration commands and retain `persist-credentials: false` | | Exact OpenCode version unavailable or mismatched | Installation step fails | Investigate npm availability and supply-chain status before changing the pin | | NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | -| Process exceeds 45 minutes | `timeout` sends `TERM`, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | +| Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | | GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior and verify the ephemeral runner was destroyed | | Tests or security checks fail | Pull request remains unmergeable | Fix the current exact head; never weaken the gate | | Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | @@ -137,6 +137,7 @@ Before merging a workflow change, verify the exact current head has: - no new secret reference other than `NVIDIA_NIM_API_KEY`; - no mutable OpenCode package or action reference; - `persist-credentials: false` plus explicit local direct-token Git bootstrap and `EXIT` cleanup; +- bounded `TERM` timeout with deterministic `KILL` escalation; - no ineffective `AGENT` environment claim for raw OpenCode 1.18.13; - no review-agent credential or workflow change. From ba845b82ed3b1f05b992c71c188b3cdf991e3cf8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:06:24 +0900 Subject: [PATCH 017/152] docs(ci): specify forced timeout escalation --- .../2026-08-04-hourly-opencode-maintenance-design.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index 042eccca..8ddaaf10 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -20,7 +20,7 @@ The maintenance workflow will: - remove the local authorization header through an `EXIT` trap after success, failure, or process timeout; - omit the ineffective `AGENT` environment variable because raw OpenCode 1.18.13 uses repository `default_agent` configuration or its `build` fallback; - disable public session sharing; -- cap each run with workflow and process timeouts; +- cap each run with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute workflow timeout; - run from the protected default branch and never from pull-request code. ## Direct-token credential lifecycle @@ -33,7 +33,7 @@ The workflow resolves this version-specific gap without weakening checkout isola 2. the run step fails closed if either `NVIDIA_API_KEY` or `GITHUB_TOKEN` is empty; 3. a Basic GitHub authorization header is derived in memory from the repository token and stored only in the checkout's local Git configuration; 4. local `user.name` and `user.email` identify infrastructure-created commits as `opencode-agent[bot]`; -5. an `EXIT` trap removes the authorization header even when OpenCode fails or the process timeout returns non-zero; +5. an `EXIT` trap removes the authorization header even when OpenCode fails, exits after `TERM`, or is forcibly ended after the grace period; 6. no personal token, OIDC permission, model fallback, tracked credential file, or review-agent secret is introduced. The agent process already requires `GITHUB_TOKEN` for GitHub API calls, so this bootstrap does not expand token scope. It makes the granted `contents: write` capability operational while keeping the credential bounded to the ephemeral runner and repository-local Git configuration. @@ -69,14 +69,14 @@ The deterministic hourly disposition workflow remains responsible for exact-head - A missing `NVIDIA_NIM_API_KEY`, missing `GITHUB_TOKEN`, unavailable NVIDIA endpoint, OpenCode installation mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. -- A timed-out process receives `TERM` before the workflow hard timeout, and the shell cleanup trap removes the local Git authorization header. +- At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. - The workflow does not publish a release or merge partial work. ## Verification A repository test will parse the workflow as text and fail unless it proves all of the following: -- hourly off-peak schedule, serialized concurrency, and bounded timeout; +- hourly off-peak schedule, serialized concurrency, bounded graceful timeout, and forced termination; - full-SHA checkout pinning and disabled credential persistence; - exact OpenCode version pin and no `@latest` use; - exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; From cc661d361db82a0ef82b392c32868d3b45d48ad1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:07:13 +0900 Subject: [PATCH 018/152] docs(ci): align plan with timeout escalation --- ...-08-04-hourly-opencode-maintenance-plan.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index 8b4e46e6..f8c52162 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,9 +4,9 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local, short-lived Git authorization header because OpenCode 1.18.13 skips its own Git setup in that mode. +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local, short-lived Git authorization header because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. -**Tech Stack:** GitHub Actions, OpenCode 1.18.13, NVIDIA NIM, bash, Maven, JUnit 5. +**Tech Stack:** GitHub Actions, OpenCode 1.18.13, NVIDIA NIM, GNU Coreutils `timeout`, bash, Maven, JUnit 5. ## Global Constraints @@ -15,6 +15,7 @@ - Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. - Pin third-party workflow sources and executable package versions immutably. - Keep checkout credential persistence disabled; bootstrap only a repository-local direct-token authorization header and remove it through an `EXIT` trap. +- Bound OpenCode with `TERM` after 45 minutes, force `KILL` after a 30-second grace period, and retain a 50-minute GitHub job timeout. - Do not claim that raw OpenCode 1.18.13 consumes an `AGENT` environment variable; it uses repository `default_agent` configuration or its `build` fallback. - The scheduled agent may open or update a pull request, but may never approve, merge, bypass protection, or push to `develop` or `main`. - Preserve standalone operation and modular CWL service compatibility. @@ -32,7 +33,7 @@ - [ ] **Step 1: Write the failing test** -Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, timeout, immutable checkout SHA, `persist-credentials: false`, exact `opencode-ai@1.18.13` installation, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, least-privilege permissions, no `id-token`, and prompt prohibitions. +Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, workflow timeout, graceful process timeout with deterministic forced termination, immutable checkout SHA, `persist-credentials: false`, exact `opencode-ai@1.18.13` installation, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, least-privilege permissions, no `id-token`, and prompt prohibitions. Add a focused direct-token contract that requires a local GitHub authorization header, local bot author identity, credential cleanup through an `EXIT` trap, and removal of the ineffective `AGENT: build` environment claim. @@ -48,12 +49,15 @@ Expected initially: FAIL at the explicit existence assertion because `.github/wo After discovering the direct-token gap, expected regression RED: FAIL because the workflow has `persist-credentials: false` and `USE_GITHUB_TOKEN=true` without the local authorization header, author identity, and cleanup contract required for OpenCode 1.18.13 infrastructure-managed commits and pushes. +After reviewing timeout behavior, expected regression RED: FAIL until the 45-minute `TERM` timeout includes a bounded 30-second `KILL` escalation. + - [ ] **Step 3: Commit the failing contract** ```bash git add etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java git commit -m "test(ci): define hourly OpenCode maintenance contract" git commit -am "test(ci): require direct-token git bootstrap" +git commit -am "test(ci): require forced OpenCode termination" ``` ### Task 2: Implement the pinned NVIDIA OpenCode workflow @@ -67,7 +71,7 @@ git commit -am "test(ci): require direct-token git bootstrap" - [ ] **Step 1: Add the minimal workflow** -Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, minimal permissions, a full-SHA checkout with disabled persisted credentials, exact npm installation, exact version verification, and `timeout --signal=TERM 45m opencode github run`. +Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, minimal permissions, a full-SHA checkout with disabled persisted credentials, exact npm installation, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. Set only the OpenCode environment variables that raw OpenCode 1.18.13 consumes: @@ -118,6 +122,7 @@ Expected: PASS with no skipped tests. git add .github/workflows/hourly-opencode-maintenance.yml git commit -m "ci: schedule NVIDIA OpenCode maintenance agent" git commit -am "fix(ci): bootstrap OpenCode direct-token git access" +git commit -am "fix(ci): force termination after OpenCode timeout" ``` ### Task 3: Document operations and release notes @@ -129,15 +134,15 @@ git commit -am "fix(ci): bootstrap OpenCode direct-token git access" **Interfaces:** - Consumes: behavior and constraints from the workflow and OpenCode 1.18.13 primary source. -- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, security, and evidence documentation. +- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, timeout-escalation, security, and evidence documentation. - [ ] **Step 1: Add the operator document** -Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, direct-token Git bootstrap, credential cleanup, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. +Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, direct-token Git bootstrap, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. - [ ] **Step 2: Update the design and `CHANGELOG.md`** -Record why direct-token mode requires an explicit local Git bootstrap while checkout credential persistence remains disabled. Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. +Record why direct-token mode requires an explicit local Git bootstrap while checkout credential persistence remains disabled, and why a force-kill grace period is needed before the workflow hard timeout. Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. - [ ] **Step 3: Run the full reactor tests** From 209ace00a093e4059ec3786d736f399cf0497b17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:08:57 +0900 Subject: [PATCH 019/152] docs(ci): cite GNU timeout behavior --- docs/operations/hourly-opencode-maintenance.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index a19e8078..da3caa85 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -149,6 +149,8 @@ Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ +Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html + GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows From 0f341d86cd6379013f328aaafd64ddaa828d859b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:09:37 +0900 Subject: [PATCH 020/152] docs(ci): cite GNU timeout design basis --- .../specs/2026-08-04-hourly-opencode-maintenance-design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index 8ddaaf10..69bb6243 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -69,7 +69,7 @@ The deterministic hourly disposition workflow remains responsible for exact-head - A missing `NVIDIA_NIM_API_KEY`, missing `GITHUB_TOKEN`, unavailable NVIDIA endpoint, OpenCode installation mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. -- At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. +- At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, GNU `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. - The workflow does not publish a release or merge partial work. ## Verification @@ -94,6 +94,8 @@ Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ +Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html + GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows From 9437c997a6edb025497b23f915bb188ab5757f17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:13:30 +0900 Subject: [PATCH 021/152] test(ci): require independent exact-head approval --- .../HourlyPrDispositionWorkflowTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyPrDispositionWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyPrDispositionWorkflowTest.java index b81fb429..d320f0f6 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyPrDispositionWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyPrDispositionWorkflowTest.java @@ -44,6 +44,19 @@ void requiresLatestDecisiveReviewState() { assertTrue(workflow.contains("latest decisive review state includes requested changes")); } + /** + * Prevents unattended merge when nobody other than the pull-request author has approved the + * exact current head. An approval anchored to an older commit is stale evidence and must not + * authorize a newer head. + */ + @Test + void requiresIndependentApprovalForTheExactCurrentHead() { + assertTrue(workflow.contains("independent_exact_head_approvals")); + assertTrue(workflow.contains(".user.login != $author")); + assertTrue(workflow.contains(".commit_id == $head_sha")); + assertTrue(workflow.contains("independent exact-head approval is absent")); + } + @Test void requiresResolvedCurrentReviewThreads() { assertTrue(workflow.contains("reviewThreads(first: 100, after: $endCursor)")); From 5c9f349a132f61e979d45f422616ebb4ce1ef3fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:14:27 +0900 Subject: [PATCH 022/152] fix(ci): require independent exact-head approval --- .github/workflows/hourly-pr-disposition.yml | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hourly-pr-disposition.yml b/.github/workflows/hourly-pr-disposition.yml index 020d6b9a..652f370b 100644 --- a/.github/workflows/hourly-pr-disposition.yml +++ b/.github/workflows/hourly-pr-disposition.yml @@ -80,19 +80,36 @@ jobs: fi reviews=$(gh api --paginate "/repos/${repo}/pulls/${number}/reviews?per_page=100") - blocking_reviews=$(jq -s ' + decisive_reviews=$(jq -s ' add | map(select(.state == "APPROVED" or .state == "CHANGES_REQUESTED")) | group_by(.user.login) | map(max_by(.submitted_at // "")) - | map(select(.state == "CHANGES_REQUESTED")) - | length ' <<<"${reviews}") + blocking_reviews=$(jq ' + map(select(.state == "CHANGES_REQUESTED")) + | length + ' <<<"${decisive_reviews}") if [[ "${blocking_reviews}" -gt 0 ]]; then skip "latest decisive review state includes requested changes" continue fi + independent_exact_head_approvals=$(jq \ + --arg author "${author}" \ + --arg head_sha "${head_sha}" ' + map(select( + .state == "APPROVED" + and .user.login != $author + and .commit_id == $head_sha + )) + | length + ' <<<"${decisive_reviews}") + if [[ "${independent_exact_head_approvals}" -eq 0 ]]; then + skip "independent exact-head approval is absent" + continue + fi + review_threads=$(gh api graphql --paginate \ -F owner="${owner}" \ -F name="${name}" \ From 671cf683da7e84752c49570a48673ab61be7a9ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:15:01 +0900 Subject: [PATCH 023/152] docs(ci): define exact-head approval gate --- docs/hourly-pr-disposition.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/hourly-pr-disposition.md b/docs/hourly-pr-disposition.md index 4c8964be..27a9c561 100644 --- a/docs/hourly-pr-disposition.md +++ b/docs/hourly-pr-disposition.md @@ -13,13 +13,20 @@ A pull request is merged only when all of the following hold: 3. No `do-not-merge`, `manual-merge`, `security-review`, or `breaking-change` label is present. 4. Workflow changes are excluded unless a maintainer explicitly applies `automerge-workflow`. 5. For each reviewer, the most recent decisive review (`APPROVED` or `CHANGES_REQUESTED`) is evaluated; comment-only reviews cannot erase an outstanding change request. -6. Every current, non-outdated review thread is resolved. -7. Every named required CI, dependency, SBOM, SAST, and security check has completed with `success`; a skipped required check is not sufficient. -8. No other reported check has failed or remains pending, commit status contexts are successful, and GitHub reports the pull request as cleanly mergeable. -9. The merge request includes the expected head SHA, preventing a time-of-check/time-of-use merge after the branch moves. +6. At least one reviewer other than the pull-request author has approved the exact current head SHA. An approval attached to an older commit is stale and cannot authorize a newer head. +7. Every current, non-outdated review thread is resolved. +8. Every named required CI, dependency, SBOM, SAST, and security check has completed with `success`; a skipped required check is not sufficient. +9. No other reported check has failed or remains pending, commit status contexts are successful, and GitHub reports the pull request as cleanly mergeable. +10. The merge request includes the expected head SHA, preventing a time-of-check/time-of-use merge after the branch moves. Eligible pull requests are squash-merged. GitHub branch protection remains authoritative and can still reject a merge. A rejection is recorded for that pull request without aborting disposition of the remaining queue. +## Review evidence + +The approval gate is intentionally stricter than merely checking that nobody requested changes. The workflow groups decisive reviews by reviewer, retains each reviewer's latest decisive state, blocks any outstanding `CHANGES_REQUESTED` state, and then requires a non-author `APPROVED` review whose `commit_id` exactly equals the current pull-request head SHA. + +Pushing another commit invalidates the unattended-merge evidence until an independent reviewer approves that new exact head. Comment-only reviews and successful status contexts do not substitute for approval. + ## Security properties - The scheduled workflow runs from the protected default branch, not from untrusted pull-request code. @@ -27,6 +34,7 @@ Eligible pull requests are squash-merged. GitHub branch protection remains autho - It uses the repository-scoped `GITHUB_TOKEN` with only `contents`, `pull-requests`, `checks`, and `statuses` permissions. - External forks and untrusted authors are never merged unattended. - Changes to workflow files require a separate explicit label and therefore remain manual by default. +- Independent approval must be anchored to the exact current head, preventing stale review evidence from authorizing later code. - GraphQL review-thread pagination prevents unresolved comments beyond the first page from being ignored. - The expected head SHA prevents a branch update from being merged under stale check results. From 87fe9464dbf5f3ffc7dd5c809c672a3f93a5cfe3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:16:41 +0900 Subject: [PATCH 024/152] docs(changelog): record exact-head approval gate --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8682c9b4..bf8330e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode direct-`GITHUB_TOKEN` path now bootstraps a repository-local bot author and short-lived GitHub authorization header, removes the header through an `EXIT` trap, retains `persist-credentials: false`, and no longer claims that raw OpenCode 1.18.13 consumes `AGENT: build`. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. From 5ec2c2208f7a6adf306f15c651708f1c820dcf05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:50:15 +0900 Subject: [PATCH 025/152] test(ci): require checksum-pinned OpenCode install --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 71d00ea9..9d0144e9 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -68,8 +68,9 @@ void schedulesOneBoundedNonOverlappingRunPerHour() { } /** - * Verifies that repository source and the OpenCode executable are pinned without retaining - * checkout credentials that a generated process could reuse implicitly. + * Verifies that repository source and the OpenCode executable are pinned by immutable + * content identifiers without retaining checkout credentials that a generated process could + * reuse implicitly. */ @Test void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { @@ -78,8 +79,19 @@ void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { )); assertTrue(workflow.contains("fetch-depth: 1")); assertTrue(workflow.contains("persist-credentials: false")); - assertTrue(workflow.contains("npm install --global opencode-ai@1.18.13")); - assertTrue(workflow.contains("test \"$(opencode --version)\" = \"1.18.13\"")); + assertTrue(workflow.contains( + "https://github.com/anomalyco/opencode/releases/download/" + + "v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" + )); + assertTrue(workflow.contains( + "OPENCODE_SHA256: \"8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937\"" + )); + assertTrue(workflow.contains("sha256sum --check --strict")); + assertTrue(workflow.contains("tar --extract --gzip")); + assertTrue(workflow.contains( + "test \"$(\"${install_dir}/opencode\" --version)\" = \"${OPENCODE_VERSION}\"" + )); + assertFalse(workflow.contains("npm install")); assertFalse(workflow.contains("opencode-ai@latest")); assertFalse(workflow.contains("anomalyco/opencode/github@")); } @@ -141,15 +153,23 @@ void usesOnlyTheNvidiaNimCredentialAndExplicitModel() { } /** - * Verifies least-privilege repository access and rejects an unnecessary OIDC token path. + * Verifies least-privilege repository access, scopes write authority to the sole maintenance + * job instead of every future job, and rejects an unnecessary OIDC token path. */ @Test void grantsOnlyRepositoryMaintenancePermissions() { - assertTrue(workflow.contains("contents: write")); - assertTrue(workflow.contains("pull-requests: write")); - assertTrue(workflow.contains("issues: write")); - assertTrue(workflow.contains("checks: read")); - assertTrue(workflow.contains("statuses: read")); + assertTrue(workflow.contains("permissions:\n contents: read\n\njobs:")); + assertTrue(workflow.contains( + "maintain-repository:\n" + + " permissions:\n" + + " actions: read\n" + + " checks: read\n" + + " contents: write\n" + + " issues: write\n" + + " pull-requests: write\n" + + " security-events: read\n" + + " statuses: read" + )); assertFalse(workflow.contains("id-token:")); assertFalse(workflow.contains("actions: write")); assertFalse(workflow.contains("security-events: write")); From 0ac43ab96d17305b79cb29b0d3726c244461e47c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:52:36 +0900 Subject: [PATCH 026/152] test(ci): require local GitHub CLI credential helper --- .../HourlyOpenCodeMaintenanceWorkflowTest.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 9d0144e9..e45c9706 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -101,23 +101,25 @@ void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { * *

OpenCode 1.18.13 intentionally skips its internal Git credential and author setup when * {@code USE_GITHUB_TOKEN=true}. Because checkout credentials remain disabled, the workflow - * must install a local, short-lived authorization header and local author identity before - * starting OpenCode, then remove the authorization header even when the process fails or - * times out.

+ * must install a repository-local GitHub CLI credential helper and local author identity + * before starting OpenCode, then remove the helper even when the process fails or times out. + * The helper reads the short-lived token from {@code GH_TOKEN}; no encoded token is written + * to Git configuration.

*/ @Test void bootstrapsAndRemovesDirectTokenGitCredentials() { + assertTrue(workflow.contains("GH_TOKEN: ${{ github.token }}")); assertTrue(workflow.contains( - "git_config_key=\"http.https://github.com/.extraheader\"" + "git_credential_key=\"credential.https://github.com.helper\"" )); assertTrue(workflow.contains("cleanup_git_credentials()")); assertTrue(workflow.contains("trap cleanup_git_credentials EXIT")); assertTrue(workflow.contains( - "printf 'x-access-token:%s' \"${GITHUB_TOKEN}\"" + "git config --local --add \"${git_credential_key}\" \"\"" )); assertTrue(workflow.contains( - "git config --local \"${git_config_key}\" " - + "\"AUTHORIZATION: basic ${github_basic_auth}\"" + "git config --local --add \"${git_credential_key}\" " + + "\"!gh auth git-credential\"" )); assertTrue(workflow.contains( "git config --local user.name \"opencode-agent[bot]\"" @@ -126,7 +128,7 @@ void bootstrapsAndRemovesDirectTokenGitCredentials() { "git config --local user.email " + "\"opencode-agent[bot]@users.noreply.github.com\"" )); - assertTrue(workflow.contains("unset github_basic_auth")); + assertFalse(workflow.contains("AUTHORIZATION: basic")); assertFalse(workflow.contains("AGENT: build")); } From 5ac043ee70c1ec91714177d9781493c2b29d9d2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:53:12 +0900 Subject: [PATCH 027/152] fix(ci): close Scorecard supply-chain findings --- .../workflows/hourly-opencode-maintenance.yml | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index c03eca82..f9d0e2aa 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -10,16 +10,18 @@ concurrency: cancel-in-progress: false permissions: - actions: read - checks: read - contents: write - issues: write - pull-requests: write - security-events: read - statuses: read + contents: read jobs: maintain-repository: + permissions: + actions: read + checks: read + contents: write + issues: write + pull-requests: write + security-events: read + statuses: read runs-on: ubuntu-latest timeout-minutes: 50 steps: @@ -29,17 +31,41 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Install pinned OpenCode CLI + - name: Install checksum-pinned OpenCode CLI shell: bash + env: + OPENCODE_VERSION: "1.18.13" + OPENCODE_SHA256: "8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937" run: | set -euo pipefail - npm install --global opencode-ai@1.18.13 --no-audit --no-fund - test "$(opencode --version)" = "1.18.13" + archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz" + install_dir="${RUNNER_TEMP}/opencode-bin" + + command -v curl >/dev/null + command -v rg >/dev/null + command -v sha256sum >/dev/null + command -v tar >/dev/null + + curl --fail --location --proto '=https' --tlsv1.2 \ + --output "${archive}" \ + "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" + printf '%s %s\n' "${OPENCODE_SHA256}" "${archive}" \ + | sha256sum --check --strict + + rm -rf "${install_dir}" + mkdir -p "${install_dir}" + tar --extract --gzip --no-same-owner --no-same-permissions \ + --file "${archive}" --directory "${install_dir}" + test -f "${install_dir}/opencode" + chmod 0755 "${install_dir}/opencode" + test "$("${install_dir}/opencode" --version)" = "${OPENCODE_VERSION}" + printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" - name: Run bounded NVIDIA OpenCode maintenance shell: bash env: GITHUB_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct SHARE: "false" @@ -66,25 +92,22 @@ jobs: echo "NVIDIA_NIM_API_KEY is required for scheduled OpenCode maintenance" >&2 exit 1 fi - if [[ -z "${GITHUB_TOKEN}" ]]; then - echo "The repository-scoped GITHUB_TOKEN is required for scheduled maintenance" >&2 + if [[ -z "${GITHUB_TOKEN}" || -z "${GH_TOKEN}" ]]; then + echo "The repository-scoped GitHub token is required for scheduled maintenance" >&2 exit 1 fi # OpenCode 1.18.13 skips its internal Git credential and author setup in - # USE_GITHUB_TOKEN mode. Bootstrap a repository-local header explicitly while + # USE_GITHUB_TOKEN mode. Install a repository-local GitHub CLI helper while # preserving checkout's persist-credentials: false supply-chain boundary. - git_config_key="http.https://github.com/.extraheader" + git_credential_key="credential.https://github.com.helper" cleanup_git_credentials() { - git config --local --unset-all "${git_config_key}" >/dev/null 2>&1 || true + git config --local --unset-all "${git_credential_key}" >/dev/null 2>&1 || true } trap cleanup_git_credentials EXIT cleanup_git_credentials - github_basic_auth="$( - printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 | tr -d '\r\n' - )" - git config --local "${git_config_key}" "AUTHORIZATION: basic ${github_basic_auth}" - unset github_basic_auth + git config --local --add "${git_credential_key}" "" + git config --local --add "${git_credential_key}" "!gh auth git-credential" git config --local user.name "opencode-agent[bot]" git config --local user.email "opencode-agent[bot]@users.noreply.github.com" From a9a996733ac438a22035f8aa6f71a5669512396d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:54:14 +0900 Subject: [PATCH 028/152] docs(ci): document checksum and permission boundaries --- .../operations/hourly-opencode-maintenance.md | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index da3caa85..71ed20ae 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -29,7 +29,8 @@ Never place the key in repository variables, source files, workflow output, issu | Control | Pinned value | | --- | --- | | Schedule | `43 * * * *` | -| OpenCode package | `opencode-ai@1.18.13` | +| OpenCode release | `v1.18.13` immutable GitHub release | +| Linux x64 archive SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | | OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | | OpenCode agent | Repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | @@ -38,28 +39,30 @@ Never place the key in repository variables, source files, workflow output, issu | Session sharing | disabled | | Overlapping runs | disabled; an active run is not cancelled | -The workflow installs the exact npm package version and verifies `opencode --version` before use. It does not invoke the mutable OpenCode GitHub Action tag or a floating `latest` package version. Checkout credentials are not persisted in the working tree. +The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset, verifies the authoritative SHA-256 published by the upstream release process before extraction, and then verifies `opencode --version`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It deliberately omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. ## Direct-token Git bootstrap -The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal `configureGit` function. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS authorization header. +The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal `configureGit` function. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS credential helper. Before starting OpenCode, the workflow therefore: -1. fails closed when `GITHUB_TOKEN` is absent; -2. removes any pre-existing repository-local GitHub authorization header; -3. derives a Basic authorization header in memory from `x-access-token:${GITHUB_TOKEN}` without printing it; -4. stores that header only in the checked-out repository's local Git configuration; +1. fails closed when either `GITHUB_TOKEN` or its GitHub CLI alias `GH_TOKEN` is absent; +2. removes any pre-existing repository-local GitHub credential-helper entry; +3. resets inherited helpers for `https://github.com` with an empty local helper entry; +4. adds the repository-local `!gh auth git-credential` helper, which reads the short-lived token from `GH_TOKEN` at credential-request time; 5. sets the local commit author to `opencode-agent[bot]`; -6. installs an `EXIT` trap that removes the authorization header after success, failure, graceful timeout, or forced process termination. +6. installs an `EXIT` trap that removes the local helper after success, failure, graceful timeout, or forced process termination. -The local author identity contains no credential. The authorization header is short-lived on the ephemeral runner and is not committed. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No OIDC or fallback model credential is introduced. +No encoded or plaintext token is written to Git configuration. The local author identity contains no credential. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No personal token, OIDC path, fallback model credential, or tracked credential file is introduced. ## Repository permissions -The job receives only these explicit `GITHUB_TOKEN` permissions: +The workflow-level default is only `contents: read`. Write authority is scoped to the sole `maintain-repository` job so a future job cannot inherit repository write access accidentally. + +That job receives only these explicit `GITHUB_TOKEN` permissions: - `actions: read` - `checks: read` @@ -93,12 +96,12 @@ The agent must not: 1. GitHub starts the workflow from the default branch. 2. The workflow checks out a shallow copy with persisted credentials disabled. -3. It installs and verifies OpenCode 1.18.13. -4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that `GITHUB_TOKEN` is present. -5. It installs the repository-local Git author and short-lived authorization header required by OpenCode's direct-token path. +3. It downloads the OpenCode 1.18.13 Linux archive, verifies the pinned SHA-256, extracts the executable, and verifies its version. +4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. +5. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. 6. OpenCode inspects all current pull requests before selecting any work. 7. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. -8. The shell `EXIT` trap removes the local Git authorization header. +8. The shell `EXIT` trap removes the local Git credential helper. 9. Independent review and repository checks evaluate the exact new head. 10. The separate disposition workflow may merge only after every gate passes. @@ -108,8 +111,10 @@ The agent must not: | --- | --- | --- | | Secret missing or empty | Job fails before OpenCode starts | Restore `NVIDIA_NIM_API_KEY`; never add a fallback key | | Repository token missing | Job fails before Git bootstrap | Restore normal GitHub Actions token availability; do not add a personal token | -| Git bootstrap fails | Job fails before OpenCode starts | Inspect local Git configuration commands and retain `persist-credentials: false` | -| Exact OpenCode version unavailable or mismatched | Installation step fails | Investigate npm availability and supply-chain status before changing the pin | +| GitHub CLI or Git bootstrap fails | Job fails before OpenCode starts or push fails visibly | Retain `persist-credentials: false`; verify the runner-provided `gh` executable and local helper entries | +| Release archive unavailable | Installation step fails before extraction | Verify the immutable upstream release exists; do not substitute a floating version | +| Archive checksum mismatches | Installation fails closed before extraction | Treat as a supply-chain incident; compare the upstream immutable release and generated tap checksum before changing any pin | +| OpenCode version mismatches | Installation step fails | Investigate the verified archive contents; do not bypass the version assertion | | NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | | Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | | GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior and verify the ephemeral runner was destroyed | @@ -135,8 +140,10 @@ Before merging a workflow change, verify the exact current head has: - independent approval; - the `automerge-workflow` label required by the deterministic disposition workflow; - no new secret reference other than `NVIDIA_NIM_API_KEY`; -- no mutable OpenCode package or action reference; -- `persist-credentials: false` plus explicit local direct-token Git bootstrap and `EXIT` cleanup; +- a full-SHA checkout pin and checksum-pinned immutable OpenCode release asset; +- no npm install command, floating package tag, or mutable OpenCode action reference; +- workflow-level read-only permission plus explicit job-scoped write permissions; +- `persist-credentials: false` plus the local GitHub CLI credential helper and `EXIT` cleanup; - bounded `TERM` timeout with deterministic `KILL` escalation; - no ineffective `AGENT` environment claim for raw OpenCode 1.18.13; - no review-agent credential or workflow change. @@ -145,6 +152,10 @@ Before merging a workflow change, verify the exact current head has: Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 + +Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb + Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ @@ -153,7 +164,9 @@ Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GN GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication -GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential + +GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From 1ce9a27e5d2a2ef559add3f6dbb9222bf983c622 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:55:13 +0900 Subject: [PATCH 029/152] docs(ci): align OpenCode security design --- ...8-04-hourly-opencode-maintenance-design.md | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index 69bb6243..bd93f6c1 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -6,37 +6,59 @@ mightyETL needs a scheduled development loop that can inspect the current reposi ## Decision -Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Keep `.github/workflows/hourly-pr-disposition.yml` unchanged as the deterministic, fail-closed merge-disposition boundary. +Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic, fail-closed merge-disposition boundary and require that boundary to recognize only a non-author approval anchored to the exact current head. The maintenance workflow will: - run at minute 43 of every hour and on manual dispatch; - use an immutable full-length SHA for `actions/checkout` and disable persisted checkout credentials; -- install the exact OpenCode package version `opencode-ai@1.18.13` rather than invoking a mutable `@latest` action; +- download the immutable OpenCode `v1.18.13` Linux x64 release archive and verify SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction; +- avoid npm install commands, floating package tags, and mutable OpenCode action references; - map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; - select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; -- use the repository-scoped `GITHUB_TOKEN`, without OpenCode OIDC exchange, and grant only the write permissions needed to create branches, pull requests, and issues plus read access to checks and statuses; -- bootstrap a short-lived repository-local Git authorization header and bot author identity because OpenCode 1.18.13 skips internal Git setup when direct `GITHUB_TOKEN` mode is selected; -- remove the local authorization header through an `EXIT` trap after success, failure, or process timeout; +- keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job; +- use the repository-scoped GitHub token, without OpenCode OIDC exchange, and grant only the write permissions needed to create branches, pull requests, and issues plus read access to checks, statuses, workflow outcomes, and security findings; +- bootstrap a repository-local GitHub CLI credential helper and bot author identity because OpenCode 1.18.13 skips internal Git setup when direct `GITHUB_TOKEN` mode is selected; +- remove the local helper through an `EXIT` trap after success, failure, or process timeout; - omit the ineffective `AGENT` environment variable because raw OpenCode 1.18.13 uses repository `default_agent` configuration or its `build` fallback; - disable public session sharing; - cap each run with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute workflow timeout; - run from the protected default branch and never from pull-request code. +## Supply-chain boundary + +An exact npm version is not a content identity and was reported by GitHub Advanced Security's Scorecard integration as an unpinned command dependency. The workflow therefore consumes the upstream immutable release asset directly. + +The installation step: + +1. downloads only over HTTPS with redirect failure handling and a TLS 1.2 minimum; +2. verifies the archive against the SHA-256 published by the upstream release process and generated Homebrew tap; +3. refuses extraction on any mismatch; +4. extracts without preserving archive ownership or permissions; +5. applies executable mode only to the expected `opencode` file; +6. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. + +This closes the mutable npm-command finding without adding a package-manager bootstrap, lockfile-generation network step, or floating release reference. + ## Direct-token credential lifecycle -OpenCode 1.18.13 reads `USE_GITHUB_TOKEN`, uses `GITHUB_TOKEN` for GitHub API access, and then deliberately bypasses its `configureGit` function. Its scheduled-event path later invokes ordinary `git commit` and `git push` commands to publish the generated branch before creating a pull request. With `persist-credentials: false`, those commands otherwise have neither a commit author nor HTTPS push authorization. +OpenCode 1.18.13 reads `USE_GITHUB_TOKEN`, uses `GITHUB_TOKEN` for GitHub API access, and then deliberately bypasses its `configureGit` function. Its scheduled-event path later invokes ordinary `git commit` and `git push` commands to publish the generated branch before creating a pull request. With `persist-credentials: false`, those commands otherwise have neither a commit author nor a credential source. The workflow resolves this version-specific gap without weakening checkout isolation: 1. checkout still persists no credential; -2. the run step fails closed if either `NVIDIA_API_KEY` or `GITHUB_TOKEN` is empty; -3. a Basic GitHub authorization header is derived in memory from the repository token and stored only in the checkout's local Git configuration; -4. local `user.name` and `user.email` identify infrastructure-created commits as `opencode-agent[bot]`; -5. an `EXIT` trap removes the authorization header even when OpenCode fails, exits after `TERM`, or is forcibly ended after the grace period; -6. no personal token, OIDC permission, model fallback, tracked credential file, or review-agent secret is introduced. +2. the run step fails closed if `NVIDIA_API_KEY`, `GITHUB_TOKEN`, or `GH_TOKEN` is empty; +3. inherited GitHub credential helpers are reset for the repository-local scope; +4. `!gh auth git-credential` is installed only as a repository-local helper and reads the ephemeral token from `GH_TOKEN` when Git requests credentials; +5. local `user.name` and `user.email` identify infrastructure-created commits as `opencode-agent[bot]`; +6. an `EXIT` trap removes the local helper even when OpenCode fails, exits after `TERM`, or is forcibly ended after the grace period; +7. no encoded token, personal token, OIDC permission, model fallback, tracked credential file, or review-agent secret is introduced. -The agent process already requires `GITHUB_TOKEN` for GitHub API calls, so this bootstrap does not expand token scope. It makes the granted `contents: write` capability operational while keeping the credential bounded to the ephemeral runner and repository-local Git configuration. +The agent process already requires the repository token for GitHub API calls, so this bootstrap does not expand token scope. It makes the granted `contents: write` capability operational while keeping credential resolution bounded to the ephemeral runner and repository-local Git configuration. + +## Permission inheritance boundary + +GitHub applies workflow-level permissions to every job unless a job provides its own permission map. A top-level `contents: write` grant therefore creates avoidable future-job inheritance. The workflow now sets only top-level `contents: read` and gives the sole `maintain-repository` job its explicit read/write map. This preserves current functionality while preventing a future observation or reporting job from silently inheriting write authority. ## Agent authority boundary @@ -56,17 +78,18 @@ The deterministic hourly disposition workflow remains responsible for exact-head 1. GitHub starts the scheduled workflow from `develop`. 2. Checkout reads the default-branch source with credentials persistence disabled. -3. npm installs the pinned OpenCode CLI and verifies its exact version. -4. The shell validates both required tokens, installs the repository-local Git author and short-lived authorization header, and registers credential cleanup. -5. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, and `NVIDIA_API_KEY` alias. +3. The installer downloads the immutable OpenCode archive, verifies the pinned SHA-256, extracts it safely, and verifies the exact version. +4. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. +5. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. 6. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. 7. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. -8. The shell removes the repository-local authorization header. -9. Existing CI, security, review, and the deterministic disposition workflow evaluate the exact head independently. +8. The shell removes the repository-local credential helper. +9. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. ## Failure behavior -- A missing `NVIDIA_NIM_API_KEY`, missing `GITHUB_TOKEN`, unavailable NVIDIA endpoint, OpenCode installation mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- The archive is never extracted after a checksum mismatch. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. - At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, GNU `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. @@ -74,22 +97,26 @@ The deterministic hourly disposition workflow remains responsible for exact-head ## Verification -A repository test will parse the workflow as text and fail unless it proves all of the following: +A repository test parses the workflow as text and fails unless it proves all of the following: - hourly off-peak schedule, serialized concurrency, bounded graceful timeout, and forced termination; - full-SHA checkout pinning and disabled credential persistence; -- exact OpenCode version pin and no `@latest` use; +- immutable OpenCode release URL, exact SHA-256 verification, exact version verification, and no npm install command; - exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; - no Copilot, Anthropic, or OpenAI credential path; -- private session setting and direct `GITHUB_TOKEN` mode; -- local direct-token Git authorization, bot author identity, `EXIT` cleanup, and no ineffective `AGENT: build` claim; -- least-privilege permissions without `id-token`; +- private session setting and direct GitHub token mode; +- repository-local GitHub CLI credential helper, bot author identity, `EXIT` cleanup, no persisted encoded authorization header, and no ineffective `AGENT: build` claim; +- read-only workflow default plus job-scoped least-privilege permissions without `id-token`; - prompt prohibitions against approval, merge, protected-branch pushes, review-agent key changes, workflow self-modification, and duplicate pull requests. ## References Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 + +Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb + Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ @@ -98,7 +125,9 @@ Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GN GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication -GitHub, Inc. (2026). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential + +GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From 38a85be6f5421de255e8c6b3ba6d99a53e7b031f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:57:05 +0900 Subject: [PATCH 030/152] docs(ci): update checksum-pinned implementation plan --- ...-08-04-hourly-opencode-maintenance-plan.md | 113 +++++++++--------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index f8c52162..3619d3a3 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,17 +4,20 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and exact OpenCode version, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local, short-lived Git authorization header because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. -**Tech Stack:** GitHub Actions, OpenCode 1.18.13, NVIDIA NIM, GNU Coreutils `timeout`, bash, Maven, JUnit 5. +**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, GitHub CLI credential helper, GNU Coreutils `sha256sum` and `timeout`, bash, Maven, JUnit 5. ## Global Constraints -- Keep `.github/workflows/hourly-pr-disposition.yml` and review-agent key configuration unchanged. +- Keep the review-agent key configuration unchanged and preserve `.github/workflows/hourly-pr-disposition.yml` as the independent deterministic merge boundary. - Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it to OpenCode as `NVIDIA_API_KEY`. - Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. -- Pin third-party workflow sources and executable package versions immutably. -- Keep checkout credential persistence disabled; bootstrap only a repository-local direct-token authorization header and remove it through an `EXIT` trap. +- Pin third-party workflow sources and executable content immutably. +- Download OpenCode only from the immutable `v1.18.13` release asset and verify Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. +- Do not use npm install commands, floating package tags, or mutable OpenCode action references. +- Keep checkout credential persistence disabled; bootstrap only a repository-local GitHub CLI credential helper and remove it through an `EXIT` trap. +- Keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job. - Bound OpenCode with `TERM` after 45 minutes, force `KILL` after a 30-second grace period, and retain a 50-minute GitHub job timeout. - Do not claim that raw OpenCode 1.18.13 consumes an `AGENT` environment variable; it uses repository `default_agent` configuration or its `build` fallback. - The scheduled agent may open or update a pull request, but may never approve, merge, bypass protection, or push to `develop` or `main`. @@ -28,16 +31,16 @@ - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` **Interfaces:** -- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest` and the pinned OpenCode 1.18.13 GitHub-handler behavior. +- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`, OpenCode 1.18.13 release metadata, and the pinned GitHub-handler behavior. - Produces: a text-level security and configuration contract for `.github/workflows/hourly-opencode-maintenance.yml`. -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** -Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, workflow timeout, graceful process timeout with deterministic forced termination, immutable checkout SHA, `persist-credentials: false`, exact `opencode-ai@1.18.13` installation, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, least-privilege permissions, no `id-token`, and prompt prohibitions. +Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, workflow timeout, graceful process timeout with deterministic forced termination, immutable checkout SHA, `persist-credentials: false`, immutable release URL, exact SHA-256 verification, exact OpenCode version verification, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, job-scoped least-privilege permissions, no `id-token`, and prompt prohibitions. -Add a focused direct-token contract that requires a local GitHub authorization header, local bot author identity, credential cleanup through an `EXIT` trap, and removal of the ineffective `AGENT: build` environment claim. +Add a focused direct-token contract that requires a repository-local GitHub CLI credential helper, local bot author identity, helper cleanup through an `EXIT` trap, no persisted encoded authorization header, and removal of the ineffective `AGENT: build` environment claim. -- [ ] **Step 2: Run the focused test to verify RED** +- [x] **Step 2: Run the focused test to verify RED** Run: @@ -45,38 +48,43 @@ Run: ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test ``` -Expected initially: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. +Expected initial RED: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. -After discovering the direct-token gap, expected regression RED: FAIL because the workflow has `persist-credentials: false` and `USE_GITHUB_TOKEN=true` without the local authorization header, author identity, and cleanup contract required for OpenCode 1.18.13 infrastructure-managed commits and pushes. +Expected supply-chain regression RED: FAIL while the workflow uses `npm install --global opencode-ai@1.18.13` instead of a checksum-verified immutable release asset. -After reviewing timeout behavior, expected regression RED: FAIL until the 45-minute `TERM` timeout includes a bounded 30-second `KILL` escalation. +Expected permission regression RED: FAIL while repository write permission is granted at workflow scope instead of only to the maintenance job. -- [ ] **Step 3: Commit the failing contract** +Expected direct-token regression RED: FAIL while checkout credentials are disabled and OpenCode direct-token mode has no repository-local credential helper and bot author. + +- [x] **Step 3: Commit the failing contracts** ```bash git add etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java git commit -m "test(ci): define hourly OpenCode maintenance contract" git commit -am "test(ci): require direct-token git bootstrap" git commit -am "test(ci): require forced OpenCode termination" +git commit -am "test(ci): require checksum-pinned OpenCode install" +git commit -am "test(ci): require local GitHub CLI credential helper" ``` -### Task 2: Implement the pinned NVIDIA OpenCode workflow +### Task 2: Implement the checksum-pinned NVIDIA OpenCode workflow **Files:** - Create: `.github/workflows/hourly-opencode-maintenance.yml` **Interfaces:** -- Consumes: repository secret `NVIDIA_NIM_API_KEY`, built-in `GITHUB_TOKEN`, OpenCode CLI 1.18.13. +- Consumes: repository secret `NVIDIA_NIM_API_KEY`, built-in `GITHUB_TOKEN`, immutable OpenCode v1.18.13 Linux x64 release asset. - Produces: one serialized scheduled maintenance run that can prepare a feature-branch pull request but cannot approve or merge. -- [ ] **Step 1: Add the minimal workflow** +- [x] **Step 1: Add the minimal workflow** -Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, minimal permissions, a full-SHA checkout with disabled persisted credentials, exact npm installation, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. +Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, workflow-level `contents: read`, job-scoped maintenance permissions, a full-SHA checkout with disabled persisted credentials, immutable release download, exact SHA-256 verification, safe extraction, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. -Set only the OpenCode environment variables that raw OpenCode 1.18.13 consumes: +Set only the model and GitHub environment variables required by the direct-token path: ```yaml GITHUB_TOKEN: ${{ github.token }} +GH_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct SHARE: "false" @@ -85,22 +93,35 @@ PROMPT: | ... ``` -The prompt must encode every authority boundary from the design and must instruct the agent to inspect all exact current PR heads before selecting work. +The prompt must encode every authority boundary from the design and instruct the agent to inspect all exact current PR heads before selecting work. + +- [x] **Step 2: Install OpenCode from immutable content** + +Before extraction: + +1. require `curl`, `sha256sum`, `tar`, and the `ripgrep` dependency supplied by the runner; +2. download `opencode-linux-x64.tar.gz` from release `v1.18.13` over HTTPS; +3. validate SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` with `sha256sum --check --strict`; +4. extract with ownership and archived permission restoration disabled; +5. apply executable mode only to the expected binary; +6. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. -- [ ] **Step 2: Bootstrap direct-token Git access without persisted checkout credentials** +Do not fall back to npm, a floating release, or a mutable action reference after any failure. + +- [x] **Step 3: Bootstrap direct-token Git access without persisted checkout credentials** Before starting OpenCode: -1. fail closed if `GITHUB_TOKEN` is empty; -2. define the repository-local key `http.https://github.com/.extraheader`; -3. install an `EXIT` trap that removes that header; -4. create a Basic authorization value from `x-access-token:${GITHUB_TOKEN}` without printing it; -5. set the header and immediately unset the temporary shell variable; +1. fail closed if `GITHUB_TOKEN` or `GH_TOKEN` is empty; +2. define the repository-local key `credential.https://github.com.helper`; +3. install an `EXIT` trap that removes that key; +4. reset inherited helper resolution locally with an empty helper entry; +5. add `!gh auth git-credential`, which obtains the ephemeral token from `GH_TOKEN` only when Git asks for credentials; 6. configure repository-local `user.name` and `user.email` for `opencode-agent[bot]`. -Do not persist checkout credentials, add a personal token, enable OIDC, or store the authorization value in tracked files. +Do not persist checkout credentials, encode the token into Git configuration, add a personal token, enable OIDC, or store credential material in tracked files. -- [ ] **Step 3: Run the focused test to verify GREEN** +- [ ] **Step 4: Run the focused test to verify GREEN** ```bash ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test @@ -108,7 +129,7 @@ Do not persist checkout credentials, add a personal token, enable OIDC, or store Expected: PASS. -- [ ] **Step 4: Run workflow and documentation contract tests** +- [ ] **Step 5: Run workflow and documentation contract tests** ```bash ./mvnw -pl etl-service -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test @@ -116,33 +137,25 @@ Expected: PASS. Expected: PASS with no skipped tests. -- [ ] **Step 5: Commit the workflow** - -```bash -git add .github/workflows/hourly-opencode-maintenance.yml -git commit -m "ci: schedule NVIDIA OpenCode maintenance agent" -git commit -am "fix(ci): bootstrap OpenCode direct-token git access" -git commit -am "fix(ci): force termination after OpenCode timeout" -``` - ### Task 3: Document operations and release notes **Files:** - Create: `docs/operations/hourly-opencode-maintenance.md` - Modify: `CHANGELOG.md` - Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` +- Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` **Interfaces:** -- Consumes: behavior and constraints from the workflow and OpenCode 1.18.13 primary source. -- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, timeout-escalation, security, and evidence documentation. +- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, upstream checksum evidence, GitHub workflow permission semantics, and GitHub CLI credential behavior. +- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, timeout-escalation, security, and evidence documentation. -- [ ] **Step 1: Add the operator document** +- [x] **Step 1: Add the operator document** -Document the secret name, provider alias, exact model and version, schedule, permissions, branch/PR lifecycle, direct-token Git bootstrap, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, and GitHub primary documentation. +Document the secret name, provider alias, exact model and version, release asset and checksum, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, and GNU primary documentation. -- [ ] **Step 2: Update the design and `CHANGELOG.md`** +- [x] **Step 2: Update the design and `CHANGELOG.md`** -Record why direct-token mode requires an explicit local Git bootstrap while checkout credential persistence remains disabled, and why a force-kill grace period is needed before the workflow hard timeout. Add an Unreleased entry describing the separate pinned OpenCode/NVIDIA maintenance workflow and its review-agent/merge-agent isolation. +Record why direct-token mode requires an explicit repository-local credential helper while checkout credential persistence remains disabled, why immutable archive checksum validation replaces npm installation, why write permissions are job-scoped, and why a force-kill grace period is needed before the workflow hard timeout. Add an Unreleased entry describing the separate OpenCode/NVIDIA workflow and its review-agent/merge-agent isolation. - [ ] **Step 3: Run the full reactor tests** @@ -152,16 +165,6 @@ Record why direct-token mode requires an explicit local Git bootstrap while chec Expected: all modules build successfully; no project test is skipped. -- [ ] **Step 4: Commit documentation** - -```bash -git add docs/operations/hourly-opencode-maintenance.md \ - docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md \ - docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md \ - CHANGELOG.md -git commit -m "docs(ci): document OpenCode maintenance operations" -``` - ### Task 4: Verify and open the protected workflow-change pull request **Files:** @@ -182,7 +185,7 @@ git diff develop...HEAD --check Expected: clean tree, no whitespace errors, successful build. -- [ ] **Step 2: Open the pull request** +- [x] **Step 2: Open the pull request** Open a pull request titled `ci: schedule NVIDIA OpenCode maintenance agent`, explain the credential isolation and authority boundary, and apply `automerge-workflow` because the deterministic disposition workflow requires explicit approval for workflow changes. From c1a8e79b2fe2c010901c31d50f5f19f5b1a38ebf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 22:58:11 +0900 Subject: [PATCH 031/152] docs(changelog): record OpenCode supply-chain hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8330e4..a8b2b8a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. -- The hourly OpenCode direct-`GITHUB_TOKEN` path now bootstraps a repository-local bot author and short-lived GitHub authorization header, removes the header through an `EXIT` trap, retains `persist-credentials: false`, and no longer claims that raw OpenCode 1.18.13 consumes `AGENT: build`. +- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. - `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata. From bdf94358173388443f02926358decfc0f2b5748a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 23:02:09 +0900 Subject: [PATCH 032/152] fix(test): normalize workflow line endings --- .../HourlyOpenCodeMaintenanceWorkflowTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index e45c9706..ed747ee5 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -36,7 +36,8 @@ class HourlyOpenCodeMaintenanceWorkflowTest { /** * Reads the workflow once after first producing an ordinary assertion failure when the - * production workflow has not yet been implemented. + * production workflow has not yet been implemented. Line endings are normalized so the same + * structural contracts run deterministically on Windows, Linux, and macOS checkouts. * * @throws IOException when the workflow exists but cannot be read as UTF-8 text */ @@ -49,7 +50,8 @@ static void readWorkflow() throws IOException { Files.exists(workflowPath), "The hourly OpenCode maintenance workflow must exist" ); - workflow = Files.readString(workflowPath, StandardCharsets.UTF_8); + workflow = Files.readString(workflowPath, StandardCharsets.UTF_8) + .replace("\r\n", "\n"); } /** From 432f8ac5f4d14ec08285e1578d1f680fa563e3fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 23:06:47 +0900 Subject: [PATCH 033/152] test(security): require patched Jackson BOM --- .../JacksonSecurityVersionTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java new file mode 100644 index 00000000..8d53a624 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java @@ -0,0 +1,82 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Prevents the Maven dependency graph from returning to Jackson Databind versions affected by + * the 2026 creator-property authorization-bypass advisories. + * + *

Spring Boot exposes {@code jackson-bom.version} as the supported Maven override for the + * complete, mutually compatible Jackson component set. Pinning only {@code jackson-databind} + * could create an unsupported mix of core, annotations, datatype, and module artifacts, so this + * contract requires the patched FasterXML BOM instead.

+ */ +class JacksonSecurityVersionTest { + + private static final String PATCHED_JACKSON_BOM_VERSION = "2.21.5"; + + /** + * Requires the root Maven project to override Spring Boot's Jackson BOM with the first 2.21 + * patch release that fixes CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w. + * + * @throws Exception when the root Maven model cannot be parsed securely + */ + @Test + void usesPatchedCompatibleJacksonBom() throws Exception { + Path rootPom = projectRoot().resolve("pom.xml"); + assertTrue(Files.exists(rootPom), "The root Maven POM must exist"); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + + Document document = factory.newDocumentBuilder().parse(rootPom.toFile()); + Element version = (Element) document.getElementsByTagName("jackson-bom.version").item(0); + assertNotNull( + version, + "The root POM must declare jackson-bom.version so every Jackson module is aligned" + ); + assertEquals(PATCHED_JACKSON_BOM_VERSION, version.getTextContent().trim()); + } + + /** + * Finds the repository root from either root or module-local Maven execution. + * + * @return absolute repository root containing the root Maven project + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From b1beef868f31e1782c7dfd309a9cbc1998d6f935 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 23:07:22 +0900 Subject: [PATCH 034/152] fix(security): align Jackson to patched BOM --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 7c0ee7c2..94bda043 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ 25 3.5.16 + 2.21.5 2025.0.3 42.7.12 3.3.16 From af707e927d5e30f4c1509554fd2b48f5e5f859c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 23:08:25 +0900 Subject: [PATCH 035/152] docs(changelog): record Jackson advisory remediation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b2b8a6..86c4082b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. +- The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. - `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata. From 751eedb852eca1165a5b936296255fc608494dad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:28:41 +0900 Subject: [PATCH 036/152] test(ci): require exact OpenCode archive membership --- .../HourlyOpenCodeArchiveValidationTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java new file mode 100644 index 00000000..5dd731fe --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java @@ -0,0 +1,83 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards archive-member validation before the scheduled workflow extracts the OpenCode binary. + * + *

A checksum proves that downloaded bytes match the reviewed release asset, but it does not + * independently constrain where archive members would be written. The workflow therefore accepts + * only the documented one-file release shape: one regular member named {@code opencode}. This + * contract prevents future tool-pin changes from silently broadening extraction to directories, + * symbolic links, absolute paths, parent-directory paths, or unexpected additional files.

+ */ +class HourlyOpenCodeArchiveValidationTest { + + private static String workflow; + + /** + * Reads the workflow as normalized UTF-8 text for deterministic assertions on every CI + * operating system. + * + * @throws IOException when the workflow cannot be read + */ + @BeforeAll + static void readWorkflow() throws IOException { + Path workflowPath = projectRoot().resolve( + ".github/workflows/hourly-opencode-maintenance.yml" + ); + workflow = Files.readString(workflowPath, StandardCharsets.UTF_8) + .replace("\r\n", "\n"); + } + + /** + * Requires exact archive membership to be checked before extraction begins. + */ + @Test + void validatesOneExpectedArchiveMemberBeforeExtraction() { + String validation = "mapfile -t archive_members < <(tar --list --gzip --file \"${archive}\")"; + String exactShape = "[[ \"${#archive_members[@]}\" -ne 1 " + + "|| \"${archive_members[0]}\" != \"opencode\" ]]"; + String extraction = "tar --extract --gzip --no-same-owner --no-same-permissions"; + + assertTrue(workflow.contains(validation)); + assertTrue(workflow.contains(exactShape)); + assertTrue(workflow.contains("OpenCode archive contains unexpected members")); + assertTrue(workflow.contains(extraction)); + assertTrue(workflow.indexOf(validation) < workflow.indexOf(extraction)); + assertTrue(workflow.indexOf(exactShape) < workflow.indexOf(extraction)); + } + + /** + * Finds the repository root from either root-level or module-local Maven execution. + * + * @return absolute repository root containing the Maven reactor + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 13c3ab9d53776e2dc3a9aa064d1f49245743d1bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:31:44 +0900 Subject: [PATCH 037/152] fix(ci): validate OpenCode archive members --- .github/workflows/hourly-opencode-maintenance.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index f9d0e2aa..a55529fa 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -42,6 +42,7 @@ jobs: install_dir="${RUNNER_TEMP}/opencode-bin" command -v curl >/dev/null + command -v install >/dev/null command -v rg >/dev/null command -v sha256sum >/dev/null command -v tar >/dev/null @@ -52,11 +53,19 @@ jobs: printf '%s %s\n' "${OPENCODE_SHA256}" "${archive}" \ | sha256sum --check --strict + mapfile -t archive_members < <(tar --list --gzip --file "${archive}") + if [[ "${#archive_members[@]}" -ne 1 || "${archive_members[0]}" != "opencode" ]]; then + echo "OpenCode archive contains unexpected members" >&2 + exit 1 + fi + rm -rf "${install_dir}" - mkdir -p "${install_dir}" + install -d -m 0700 "${install_dir}" tar --extract --gzip --no-same-owner --no-same-permissions \ + --no-overwrite-dir --keep-old-files \ --file "${archive}" --directory "${install_dir}" test -f "${install_dir}/opencode" + test ! -L "${install_dir}/opencode" chmod 0755 "${install_dir}/opencode" test "$("${install_dir}/opencode" --version)" = "${OPENCODE_VERSION}" printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" From f4b2a1ccdb66b82dceb6f4fdbb4c62c1414fecfe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:32:39 +0900 Subject: [PATCH 038/152] docs(ci): explain archive extraction boundary --- .../operations/hourly-opencode-maintenance.md | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 71ed20ae..a66334c8 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -31,6 +31,7 @@ Never place the key in repository variables, source files, workflow output, issu | Schedule | `43 * * * *` | | OpenCode release | `v1.18.13` immutable GitHub release | | Linux x64 archive SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | +| Expected archive shape | exactly one root member named `opencode` | | OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | | OpenCode agent | Repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | @@ -39,7 +40,9 @@ Never place the key in repository variables, source files, workflow output, issu | Session sharing | disabled | | Overlapping runs | disabled; an active run is not cancelled | -The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset, verifies the authoritative SHA-256 published by the upstream release process before extraction, and then verifies `opencode --version`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. +The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies the authoritative SHA-256 published by the upstream release process. Before extraction it lists the archive and requires exactly one member named `opencode`. It then creates a fresh mode-`0700` installation directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. + +Checksum validation and member validation are separate controls. The checksum binds the bytes to the reviewed release asset; the member check constrains the extraction shape if a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory and post-extraction type checks provide additional containment. OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It deliberately omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. @@ -96,14 +99,15 @@ The agent must not: 1. GitHub starts the workflow from the default branch. 2. The workflow checks out a shallow copy with persisted credentials disabled. -3. It downloads the OpenCode 1.18.13 Linux archive, verifies the pinned SHA-256, extracts the executable, and verifies its version. -4. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. -5. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. -6. OpenCode inspects all current pull requests before selecting any work. -7. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. -8. The shell `EXIT` trap removes the local Git credential helper. -9. Independent review and repository checks evaluate the exact new head. -10. The separate disposition workflow may merge only after every gate passes. +3. It downloads the OpenCode 1.18.13 Linux archive and verifies the pinned SHA-256. +4. It requires exactly one archive member named `opencode`, extracts into a fresh private directory without archived ownership or permissions, and verifies a regular non-symbolic-link executable with the exact version. +5. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. +6. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. +7. OpenCode inspects all current pull requests before selecting any work. +8. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. +9. The shell `EXIT` trap removes the local Git credential helper. +10. Independent review and repository checks evaluate the exact new head. +11. The separate disposition workflow may merge only after every gate passes. ## Failure handling @@ -114,6 +118,8 @@ The agent must not: | GitHub CLI or Git bootstrap fails | Job fails before OpenCode starts or push fails visibly | Retain `persist-credentials: false`; verify the runner-provided `gh` executable and local helper entries | | Release archive unavailable | Installation step fails before extraction | Verify the immutable upstream release exists; do not substitute a floating version | | Archive checksum mismatches | Installation fails closed before extraction | Treat as a supply-chain incident; compare the upstream immutable release and generated tap checksum before changing any pin | +| Archive has an unexpected member set | Installation fails closed before extraction | Treat the packaging change as a supply-chain review event; inspect the exact immutable asset before updating the member contract | +| Extracted object is absent, non-regular, or a symbolic link | Installation fails before execution | Treat as a supply-chain incident; do not relax the file-type checks | | OpenCode version mismatches | Installation step fails | Investigate the verified archive contents; do not bypass the version assertion | | NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | | Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | @@ -126,7 +132,7 @@ The workflow must not claim success for partial work. A failed run may leave a f ## Rollback and disablement -This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract test, this runbook, the design and plan documents, and the corresponding `CHANGELOG.md` entry through a reviewed pull request. +This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, the doctoring evidence, and the corresponding `CHANGELOG.md` entry through a reviewed pull request. Do not delete or rename `NVIDIA_NIM_API_KEY` when it is also used by other approved workflows. Disabling this workflow does not require changing the review agent or its credential scheme. @@ -141,6 +147,8 @@ Before merging a workflow change, verify the exact current head has: - the `automerge-workflow` label required by the deterministic disposition workflow; - no new secret reference other than `NVIDIA_NIM_API_KEY`; - a full-SHA checkout pin and checksum-pinned immutable OpenCode release asset; +- exactly one expected archive member validated before extraction; +- a fresh mode-`0700` extraction directory, overwrite refusal, and regular non-symbolic-link executable validation; - no npm install command, floating package tag, or mutable OpenCode action reference; - workflow-level read-only permission plus explicit job-scoped write permissions; - `persist-credentials: false` plus the local GitHub CLI credential helper and `EXIT` cleanup; @@ -160,6 +168,8 @@ Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ +Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html + Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication From 1ccc8c9726391f88dca8d5a42baa6564f2b7f227 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:33:26 +0900 Subject: [PATCH 039/152] docs(ci): design fail-closed archive extraction --- ...8-04-hourly-opencode-maintenance-design.md | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index bd93f6c1..e8425fd5 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -13,6 +13,7 @@ The maintenance workflow will: - run at minute 43 of every hour and on manual dispatch; - use an immutable full-length SHA for `actions/checkout` and disable persisted checkout credentials; - download the immutable OpenCode `v1.18.13` Linux x64 release archive and verify SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction; +- require the verified archive to contain exactly one root member named `opencode`, extract it into a fresh mode-`0700` directory, and reject non-regular or symbolic-link output; - avoid npm install commands, floating package tags, and mutable OpenCode action references; - map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; - select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; @@ -33,12 +34,17 @@ The installation step: 1. downloads only over HTTPS with redirect failure handling and a TLS 1.2 minimum; 2. verifies the archive against the SHA-256 published by the upstream release process and generated Homebrew tap; -3. refuses extraction on any mismatch; -4. extracts without preserving archive ownership or permissions; -5. applies executable mode only to the expected `opencode` file; -6. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. +3. lists the verified archive and requires exactly one member named `opencode` before extraction; +4. refuses extraction on a checksum or member-shape mismatch; +5. creates a fresh mode-`0700` extraction directory and refuses overwrites; +6. extracts without preserving archive ownership or permissions; +7. requires the extracted object to be a regular file and not a symbolic link; +8. applies executable mode only to the expected `opencode` file; +9. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. -This closes the mutable npm-command finding without adding a package-manager bootstrap, lockfile-generation network step, or floating release reference. +The checksum and archive-member controls serve different purposes. SHA-256 binds the downloaded bytes to the reviewed upstream release. Exact member validation and private-directory extraction constrain filesystem effects when a future pin is changed incorrectly or upstream packaging changes. Post-extraction file-type checks prevent a symbolic-link or special-file output from being executed. + +This closes the mutable npm-command finding without adding a package-manager bootstrap, lockfile-generation network step, floating release reference, or unconstrained archive extraction. ## Direct-token credential lifecycle @@ -78,18 +84,20 @@ The deterministic hourly disposition workflow remains responsible for exact-head 1. GitHub starts the scheduled workflow from `develop`. 2. Checkout reads the default-branch source with credentials persistence disabled. -3. The installer downloads the immutable OpenCode archive, verifies the pinned SHA-256, extracts it safely, and verifies the exact version. -4. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. -5. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. -6. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. -7. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. -8. The shell removes the repository-local credential helper. -9. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. +3. The installer downloads the immutable OpenCode archive and verifies the pinned SHA-256. +4. The installer validates the one-member archive shape, extracts into a fresh private directory without archived ownership or permissions, and verifies a regular non-symbolic-link executable at the exact version. +5. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. +6. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. +7. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. +8. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. +9. The shell removes the repository-local credential helper. +10. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. ## Failure behavior -- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. -- The archive is never extracted after a checksum mismatch. +- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, archive-member mismatch, extracted-file type mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- The archive is never extracted after a checksum or member-shape mismatch. +- Extraction occurs only in a newly recreated mode-`0700` directory and refuses overwrites. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. - At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, GNU `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. @@ -97,11 +105,12 @@ The deterministic hourly disposition workflow remains responsible for exact-head ## Verification -A repository test parses the workflow as text and fails unless it proves all of the following: +Repository tests parse the workflow as text and fail unless they prove all of the following: - hourly off-peak schedule, serialized concurrency, bounded graceful timeout, and forced termination; - full-SHA checkout pinning and disabled credential persistence; - immutable OpenCode release URL, exact SHA-256 verification, exact version verification, and no npm install command; +- exact one-member archive validation before extraction, private extraction directory creation, overwrite refusal, and regular non-symbolic-link output checks; - exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; - no Copilot, Anthropic, or OpenAI credential path; - private session setting and direct GitHub token mode; @@ -121,6 +130,8 @@ Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ +Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html + Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication From 368ceacded27d05711dfa8fdc0aea3cced8b51a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:34:24 +0900 Subject: [PATCH 040/152] docs(ci): plan archive-member validation --- ...-08-04-hourly-opencode-maintenance-plan.md | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index 3619d3a3..e59b3247 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,9 +4,9 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Before extraction, the installer accepts only the reviewed one-member archive shape and uses a fresh private directory plus file-type checks. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. -**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, GitHub CLI credential helper, GNU Coreutils `sha256sum` and `timeout`, bash, Maven, JUnit 5. +**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, GitHub CLI credential helper, GNU Coreutils `sha256sum` and `timeout`, GNU tar, bash, Maven, JUnit 5. ## Global Constraints @@ -15,6 +15,7 @@ - Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. - Pin third-party workflow sources and executable content immutably. - Download OpenCode only from the immutable `v1.18.13` release asset and verify Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. +- Require exactly one archive member named `opencode` before extraction; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output. - Do not use npm install commands, floating package tags, or mutable OpenCode action references. - Keep checkout credential persistence disabled; bootstrap only a repository-local GitHub CLI credential helper and remove it through an `EXIT` trap. - Keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job. @@ -25,22 +26,23 @@ --- -### Task 1: Add the fail-closed workflow contract test +### Task 1: Add fail-closed workflow contract tests **Files:** - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` +- Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java` **Interfaces:** -- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`, OpenCode 1.18.13 release metadata, and the pinned GitHub-handler behavior. -- Produces: a text-level security and configuration contract for `.github/workflows/hourly-opencode-maintenance.yml`. +- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`, OpenCode 1.18.13 release metadata, the pinned GitHub-handler behavior, upstream release packaging, and GNU tar extraction guidance. +- Produces: text-level security, credential, archive, and configuration contracts for `.github/workflows/hourly-opencode-maintenance.yml`. -- [x] **Step 1: Write the failing test** +- [x] **Step 1: Write the initial failing workflow test** Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, workflow timeout, graceful process timeout with deterministic forced termination, immutable checkout SHA, `persist-credentials: false`, immutable release URL, exact SHA-256 verification, exact OpenCode version verification, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, job-scoped least-privilege permissions, no `id-token`, and prompt prohibitions. Add a focused direct-token contract that requires a repository-local GitHub CLI credential helper, local bot author identity, helper cleanup through an `EXIT` trap, no persisted encoded authorization header, and removal of the ineffective `AGENT: build` environment claim. -- [x] **Step 2: Run the focused test to verify RED** +- [x] **Step 2: Run the initial focused test to verify RED** Run: @@ -48,25 +50,28 @@ Run: ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test ``` -Expected initial RED: FAIL at the explicit existence assertion because `.github/workflows/hourly-opencode-maintenance.yml` does not exist. +Observed initial RED: the explicit existence assertion failed because `.github/workflows/hourly-opencode-maintenance.yml` did not exist. -Expected supply-chain regression RED: FAIL while the workflow uses `npm install --global opencode-ai@1.18.13` instead of a checksum-verified immutable release asset. +Observed subsequent RED states covered the mutable npm command, workflow-scoped write permission, missing direct-token Git bootstrap, and prompt wording boundary before each corresponding production change. -Expected permission regression RED: FAIL while repository write permission is granted at workflow scope instead of only to the maintenance job. +- [x] **Step 3: Write the archive-member regression test** -Expected direct-token regression RED: FAIL while checkout credentials are disabled and OpenCode direct-token mode has no repository-local credential helper and bot author. +Create `HourlyOpenCodeArchiveValidationTest` with complete beginner-readable Javadoc. Require the workflow to list the verified archive, accept exactly one member named `opencode`, emit a stable failure message for any other shape, and perform this validation before extraction. -- [x] **Step 3: Commit the failing contracts** +- [x] **Step 4: Run the archive test to verify RED** + +Run: ```bash -git add etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java -git commit -m "test(ci): define hourly OpenCode maintenance contract" -git commit -am "test(ci): require direct-token git bootstrap" -git commit -am "test(ci): require forced OpenCode termination" -git commit -am "test(ci): require checksum-pinned OpenCode install" -git commit -am "test(ci): require local GitHub CLI credential helper" +./mvnw -pl etl-service -Dtest=HourlyOpenCodeArchiveValidationTest test ``` +Observed on the pull-request CI merge ref for the test-only commit: `Tests run: 284, Failures: 1, Errors: 0`. The sole failure was `HourlyOpenCodeArchiveValidationTest.validatesOneExpectedArchiveMemberBeforeExtraction`, proving that the existing checksum-only installer did not satisfy the archive-member boundary. + +- [x] **Step 5: Commit the failing contracts** + +The workflow contracts were committed before their production fixes, including the dedicated archive-member regression contract. + ### Task 2: Implement the checksum-pinned NVIDIA OpenCode workflow **Files:** @@ -78,7 +83,7 @@ git commit -am "test(ci): require local GitHub CLI credential helper" - [x] **Step 1: Add the minimal workflow** -Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, workflow-level `contents: read`, job-scoped maintenance permissions, a full-SHA checkout with disabled persisted credentials, immutable release download, exact SHA-256 verification, safe extraction, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. +Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, workflow-level `contents: read`, job-scoped maintenance permissions, a full-SHA checkout with disabled persisted credentials, immutable release download, exact SHA-256 verification, constrained extraction, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. Set only the model and GitHub environment variables required by the direct-token path: @@ -95,18 +100,21 @@ PROMPT: | The prompt must encode every authority boundary from the design and instruct the agent to inspect all exact current PR heads before selecting work. -- [x] **Step 2: Install OpenCode from immutable content** +- [x] **Step 2: Install OpenCode from immutable and constrained content** Before extraction: -1. require `curl`, `sha256sum`, `tar`, and the `ripgrep` dependency supplied by the runner; +1. require `curl`, `install`, `sha256sum`, `tar`, and the `ripgrep` dependency supplied by the runner; 2. download `opencode-linux-x64.tar.gz` from release `v1.18.13` over HTTPS; 3. validate SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` with `sha256sum --check --strict`; -4. extract with ownership and archived permission restoration disabled; -5. apply executable mode only to the expected binary; -6. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. +4. list the archive and require exactly one member named `opencode`; +5. recreate the extraction directory with mode `0700`; +6. extract with ownership and archived permission restoration disabled and overwrite refusal enabled; +7. require the resulting `opencode` path to be a regular file and not a symbolic link; +8. apply executable mode only to the expected binary; +9. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. -Do not fall back to npm, a floating release, or a mutable action reference after any failure. +Do not fall back to npm, a floating release, a mutable action reference, or a broader archive-member allowlist after any failure. - [x] **Step 3: Bootstrap direct-token Git access without persisted checkout credentials** @@ -121,10 +129,11 @@ Before starting OpenCode: Do not persist checkout credentials, encode the token into Git configuration, add a personal token, enable OIDC, or store credential material in tracked files. -- [ ] **Step 4: Run the focused test to verify GREEN** +- [ ] **Step 4: Run focused tests to verify GREEN** ```bash -./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test +./mvnw -pl etl-service \ + -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest' test ``` Expected: PASS. @@ -132,30 +141,32 @@ Expected: PASS. - [ ] **Step 5: Run workflow and documentation contract tests** ```bash -./mvnw -pl etl-service -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test +./mvnw -pl etl-service \ + -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test ``` -Expected: PASS with no skipped tests. +Expected: PASS with no skipped project tests. -### Task 3: Document operations and release notes +### Task 3: Document operations, evidence, and release notes **Files:** - Create: `docs/operations/hourly-opencode-maintenance.md` +- Create: `docs/doctoring/opencode-archive-extraction-evidence.md` - Modify: `CHANGELOG.md` - Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` - Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` **Interfaces:** -- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, upstream checksum evidence, GitHub workflow permission semantics, and GitHub CLI credential behavior. -- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, timeout-escalation, security, and evidence documentation. +- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, upstream checksum evidence, GNU tar security guidance, GitHub workflow permission semantics, and GitHub CLI credential behavior. +- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, archive-member, timeout-escalation, security, and evidence documentation. -- [x] **Step 1: Add the operator document** +- [x] **Step 1: Add and update operator documentation** -Document the secret name, provider alias, exact model and version, release asset and checksum, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, and GNU primary documentation. +Document the secret name, provider alias, exact model and version, release asset and checksum, exact archive shape, private extraction directory, file-type checks, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, GNU tar, and GNU Coreutils primary documentation. -- [x] **Step 2: Update the design and `CHANGELOG.md`** +- [ ] **Step 2: Add doctoring evidence and update `CHANGELOG.md`** -Record why direct-token mode requires an explicit repository-local credential helper while checkout credential persistence remains disabled, why immutable archive checksum validation replaces npm installation, why write permissions are job-scoped, and why a force-kill grace period is needed before the workflow hard timeout. Add an Unreleased entry describing the separate OpenCode/NVIDIA workflow and its review-agent/merge-agent isolation. +Record the threat, evidence chain, exact fail-closed controls, RED/GREEN verification, rollback implications, and APA 7 references in `docs/doctoring/opencode-archive-extraction-evidence.md`. Update the Unreleased entry to state that the installer validates exactly one expected archive member before extraction and rejects symbolic-link or non-regular output. - [ ] **Step 3: Run the full reactor tests** From 862bad4001662264aac4391ace19d7ffe0bfaa1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:34:55 +0900 Subject: [PATCH 041/152] docs(doctoring): record archive extraction evidence --- .../opencode-archive-extraction-evidence.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/doctoring/opencode-archive-extraction-evidence.md diff --git a/docs/doctoring/opencode-archive-extraction-evidence.md b/docs/doctoring/opencode-archive-extraction-evidence.md new file mode 100644 index 00000000..929dab4f --- /dev/null +++ b/docs/doctoring/opencode-archive-extraction-evidence.md @@ -0,0 +1,52 @@ +# OpenCode archive extraction evidence + +## Scope + +This evidence note covers only the installation boundary in `.github/workflows/hourly-opencode-maintenance.yml`. It does not authorize pull-request approval, merge, protected-branch writes, release publication, or changes to the independent review agent. + +The scheduled workflow downloads the immutable OpenCode `v1.18.13` Linux x64 release archive. A pinned SHA-256 verifies that the downloaded bytes match the reviewed upstream artifact, but checksum verification alone does not constrain the filesystem shape that an archive extractor would process. This note records the additional fail-closed archive-member and extracted-file controls. + +## Threat statement + +Archive extraction is a filesystem write operation. Unexpected archive members can broaden that write beyond the intended executable, and link or special-file members can change the meaning of the extracted path. GNU tar's security guidance therefore treats archive member names, extraction location, ownership, permissions, links, and overwrite behavior as security-relevant controls. + +For this immutable OpenCode release, the reviewed upstream packaging contract installs a single root executable named `opencode`. mightyETL intentionally does not generalize that shape into a reusable archive installer. A changed member set is a supply-chain review event and fails the job. + +## Evidence chain + +1. OpenCode's upstream release process builds `opencode-linux-x64.tar.gz`, computes its SHA-256, and generates package-manager metadata that installs the root `opencode` executable. +2. The mightyETL workflow pins release `v1.18.13` and SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`. +3. The workflow validates the SHA-256 before listing or extracting the archive. +4. The workflow lists the verified archive and requires exactly one member named `opencode`. +5. The workflow recreates a private mode-`0700` installation directory and enables overwrite refusal. +6. Extraction does not restore archived ownership or permissions. +7. The extracted `opencode` path must be a regular file and must not be a symbolic link before executable mode is applied. +8. The executable must report exactly version `1.18.13` before its directory is added to `GITHUB_PATH`. + +These controls are deliberately cumulative. A checksum mismatch, unexpected member name or count, extraction failure, missing file, non-regular file, symbolic link, or version mismatch terminates the installation before OpenCode runs. + +## Test-first evidence + +`HourlyOpenCodeArchiveValidationTest` was added before the workflow member check. On the pull-request CI merge ref for test-only commit `751eedb852eca1165a5b936296255fc608494dad`, the Maven reactor reported 284 tests, one failure, and zero errors. The sole failure was `validatesOneExpectedArchiveMemberBeforeExtraction`, demonstrating that the prior checksum-only workflow did not satisfy the new extraction contract. + +The production workflow then added exact member validation before its existing extraction command, a fresh private directory, overwrite refusal, and regular non-symbolic-link output checks. The test remains a deterministic cross-platform repository contract; the actual scheduled installer executes only on the declared Ubuntu runner. + +This document is design and verification evidence, not a substitute for exact-head CI, security checks, independent review, or branch protection. Any later commit makes earlier exact-head evidence stale. + +## Operational response + +Treat any archive checksum, member-set, extracted-file type, or version mismatch as a supply-chain incident. Do not broaden the member allowlist, remove the symbolic-link check, disable overwrite refusal, or change the checksum merely to restore a green workflow. Compare the exact immutable upstream release, upstream publishing source, and generated checksum metadata before proposing a reviewed pin change. + +Rollback consists of disabling the scheduled workflow or reverting the workflow, tests, operations documentation, design, implementation plan, doctoring evidence, and CHANGELOG entry through an independently reviewed pull request. Rollback must not rename or remove `NVIDIA_NIM_API_KEY` when another approved workflow also uses it, and must not alter the review-agent credential scheme. + +## References + +Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/script/publish.ts + +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 + +Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb + +Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html + +GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From f3da591148f09c970d16a87555d83241344e1f37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:35:50 +0900 Subject: [PATCH 042/152] docs(changelog): record archive extraction hardening --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c4082b..4d86082c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. -- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. +- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one expected archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. +- Supply-chain doctoring evidence for checksum binding, exact archive-member validation, private extraction, file-type checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. - Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`. - ETL problem-details client and operator contract: `docs/api/problem-details.md`. @@ -253,5 +254,5 @@ This changelog will be updated: --- **Changelog Version**: 1.0 -**Last Updated**: 2026-08-04 +**Last Updated**: 2026-08-05 **Maintained By**: Development Team \ No newline at end of file From 7b82a40b12c46aed869aeec7b387a161a7b33896 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:43:11 +0900 Subject: [PATCH 043/152] test(ci): require regular OpenCode archive member --- .../HourlyOpenCodeArchiveValidationTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java index 5dd731fe..3cd9fcf8 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java @@ -18,7 +18,7 @@ * independently constrain where archive members would be written. The workflow therefore accepts * only the documented one-file release shape: one regular member named {@code opencode}. This * contract prevents future tool-pin changes from silently broadening extraction to directories, - * symbolic links, absolute paths, parent-directory paths, or unexpected additional files.

+ * links, device nodes, absolute paths, parent-directory paths, or unexpected additional files.

*/ class HourlyOpenCodeArchiveValidationTest { @@ -57,6 +57,28 @@ void validatesOneExpectedArchiveMemberBeforeExtraction() { assertTrue(workflow.indexOf(exactShape) < workflow.indexOf(extraction)); } + /** + * Requires the sole member to be a regular-file entry before tar writes to the filesystem. + * + *

Post-extraction symbolic-link checks remain useful defense in depth, but they cannot by + * themselves distinguish an ordinary archived file from every link-oriented archive entry. + * GNU tar's verbose listing begins each member with its type character, so the Ubuntu-only + * installer must require {@code -} for a regular file before extraction.

+ */ + @Test + void validatesRegularFileEntryTypeBeforeExtraction() { + String metadata = "archive_entry_metadata=\"$(LC_ALL=C tar --list --verbose " + + "--numeric-owner --gzip --file \"${archive}\")\""; + String regularType = "[[ \"${archive_entry_metadata:0:1}\" != \"-\" ]]"; + String extraction = "tar --extract --gzip --no-same-owner --no-same-permissions"; + + assertTrue(workflow.contains(metadata)); + assertTrue(workflow.contains(regularType)); + assertTrue(workflow.contains("OpenCode archive member is not a regular file")); + assertTrue(workflow.indexOf(metadata) < workflow.indexOf(extraction)); + assertTrue(workflow.indexOf(regularType) < workflow.indexOf(extraction)); + } + /** * Finds the repository root from either root-level or module-local Maven execution. * From 31dc60e5e9d708bb8d78caed6a7bcea3f14070a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:45:54 +0900 Subject: [PATCH 044/152] fix(ci): reject non-regular archive entries --- .github/workflows/hourly-opencode-maintenance.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index a55529fa..ca9aab44 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -59,6 +59,12 @@ jobs: exit 1 fi + archive_entry_metadata="$(LC_ALL=C tar --list --verbose --numeric-owner --gzip --file "${archive}")" + if [[ "${archive_entry_metadata:0:1}" != "-" ]]; then + echo "OpenCode archive member is not a regular file" >&2 + exit 1 + fi + rm -rf "${install_dir}" install -d -m 0700 "${install_dir}" tar --extract --gzip --no-same-owner --no-same-permissions \ From 7a363049823f560abda7b3f059be2489b01537c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:46:34 +0900 Subject: [PATCH 045/152] docs(doctoring): record regular-entry validation --- .../opencode-archive-extraction-evidence.md | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/docs/doctoring/opencode-archive-extraction-evidence.md b/docs/doctoring/opencode-archive-extraction-evidence.md index 929dab4f..74d27903 100644 --- a/docs/doctoring/opencode-archive-extraction-evidence.md +++ b/docs/doctoring/opencode-archive-extraction-evidence.md @@ -4,38 +4,42 @@ This evidence note covers only the installation boundary in `.github/workflows/hourly-opencode-maintenance.yml`. It does not authorize pull-request approval, merge, protected-branch writes, release publication, or changes to the independent review agent. -The scheduled workflow downloads the immutable OpenCode `v1.18.13` Linux x64 release archive. A pinned SHA-256 verifies that the downloaded bytes match the reviewed upstream artifact, but checksum verification alone does not constrain the filesystem shape that an archive extractor would process. This note records the additional fail-closed archive-member and extracted-file controls. +The scheduled workflow downloads the immutable OpenCode `v1.18.13` Linux x64 release archive. GitHub marks that release immutable, and the workflow pins the Linux x64 asset's SHA-256. Checksum verification binds downloaded bytes to the reviewed asset, but it does not independently constrain the filesystem shape or entry type that an archive extractor would process. This note records the additional fail-closed archive-member, pre-extraction entry-type, extraction-directory, and post-extraction file controls. ## Threat statement -Archive extraction is a filesystem write operation. Unexpected archive members can broaden that write beyond the intended executable, and link or special-file members can change the meaning of the extracted path. GNU tar's security guidance therefore treats archive member names, extraction location, ownership, permissions, links, and overwrite behavior as security-relevant controls. +Archive extraction is a filesystem write operation. Unexpected archive members can broaden that write beyond the intended executable. Symbolic links, hard links, directories, device nodes, and other special entries can change the meaning or destination of extracted paths. GNU tar's security guidance therefore treats archive member names, extraction location, ownership, permissions, links, and overwrite behavior as security-relevant controls. -For this immutable OpenCode release, the reviewed upstream packaging contract installs a single root executable named `opencode`. mightyETL intentionally does not generalize that shape into a reusable archive installer. A changed member set is a supply-chain review event and fails the job. +For this immutable OpenCode release, upstream publishing source and release packaging identify a single root executable named `opencode`. mightyETL intentionally does not generalize that shape into a reusable archive installer. Any changed member count, name, or entry type is a supply-chain review event and fails the job. ## Evidence chain -1. OpenCode's upstream release process builds `opencode-linux-x64.tar.gz`, computes its SHA-256, and generates package-manager metadata that installs the root `opencode` executable. -2. The mightyETL workflow pins release `v1.18.13` and SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`. -3. The workflow validates the SHA-256 before listing or extracting the archive. -4. The workflow lists the verified archive and requires exactly one member named `opencode`. -5. The workflow recreates a private mode-`0700` installation directory and enables overwrite refusal. -6. Extraction does not restore archived ownership or permissions. -7. The extracted `opencode` path must be a regular file and must not be a symbolic link before executable mode is applied. -8. The executable must report exactly version `1.18.13` before its directory is added to `GITHUB_PATH`. +1. GitHub marks OpenCode release `v1.18.13` immutable and identifies asset `501285078` as `opencode-linux-x64.tar.gz`. +2. OpenCode's upstream release process builds the Linux x64 archive and computes release digests. +3. The mightyETL workflow pins release `v1.18.13` and SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`. +4. The workflow validates the SHA-256 before listing or extracting the archive. +5. The workflow lists the verified archive and requires exactly one member named `opencode`. +6. With `LC_ALL=C`, the workflow obtains GNU tar's verbose metadata for that sole member and requires the first type character to be `-`, denoting a regular-file archive entry. Link, directory, and special-file entries are rejected before extraction. +7. The workflow recreates a private mode-`0700` installation directory and enables overwrite refusal. +8. Extraction does not restore archived ownership or permissions. +9. The extracted `opencode` path must be a regular file and must not be a symbolic link before executable mode is applied. +10. The executable must report exactly version `1.18.13` before its directory is added to `GITHUB_PATH`. -These controls are deliberately cumulative. A checksum mismatch, unexpected member name or count, extraction failure, missing file, non-regular file, symbolic link, or version mismatch terminates the installation before OpenCode runs. +These controls are deliberately cumulative. A checksum mismatch, unexpected member name or count, non-regular archive entry, extraction failure, missing file, non-regular extracted file, symbolic link, or version mismatch terminates installation before OpenCode runs. ## Test-first evidence -`HourlyOpenCodeArchiveValidationTest` was added before the workflow member check. On the pull-request CI merge ref for test-only commit `751eedb852eca1165a5b936296255fc608494dad`, the Maven reactor reported 284 tests, one failure, and zero errors. The sole failure was `validatesOneExpectedArchiveMemberBeforeExtraction`, demonstrating that the prior checksum-only workflow did not satisfy the new extraction contract. +`HourlyOpenCodeArchiveValidationTest` was first added before the workflow member check. On the pull-request CI merge ref for test-only commit `751eedb852eca1165a5b936296255fc608494dad`, the Maven reactor reported 284 tests, one failure, and zero errors. The sole failure was `validatesOneExpectedArchiveMemberBeforeExtraction`, demonstrating that the prior checksum-only workflow did not satisfy the member-shape contract. -The production workflow then added exact member validation before its existing extraction command, a fresh private directory, overwrite refusal, and regular non-symbolic-link output checks. The test remains a deterministic cross-platform repository contract; the actual scheduled installer executes only on the declared Ubuntu runner. +After exact member validation was added, the test was extended before production code to require a regular-file archive entry. On the pull-request CI merge ref for test-only commit `7b82a40b12c46aed869aeec7b387a161a7b33896`, GitHub Actions run `30964191079` reported 285 tests, one failure, zero errors, and zero skipped project tests on Ubuntu. The sole failure was `validatesRegularFileEntryTypeBeforeExtraction`, demonstrating that name and count validation alone did not reject hard-link or other non-regular archive entries before extraction. + +The production workflow then added locale-stable verbose metadata inspection and a regular-entry type check before its existing extraction command. Exact member validation, a fresh private directory, overwrite refusal, disabled ownership and archived-permission restoration, and post-extraction regular non-symbolic-link checks remain defense in depth. The tests are deterministic cross-platform repository contracts; the actual scheduled installer executes only on the declared Ubuntu runner. This document is design and verification evidence, not a substitute for exact-head CI, security checks, independent review, or branch protection. Any later commit makes earlier exact-head evidence stale. ## Operational response -Treat any archive checksum, member-set, extracted-file type, or version mismatch as a supply-chain incident. Do not broaden the member allowlist, remove the symbolic-link check, disable overwrite refusal, or change the checksum merely to restore a green workflow. Compare the exact immutable upstream release, upstream publishing source, and generated checksum metadata before proposing a reviewed pin change. +Treat any archive checksum, member-set, archive-entry type, extracted-file type, or version mismatch as a supply-chain incident. Do not broaden the member allowlist, permit link or special-file entries, remove post-extraction file checks, disable overwrite refusal, or change the checksum merely to restore a green workflow. Compare the exact immutable upstream release record, release-asset metadata, and upstream publishing source before proposing a reviewed pin change. Rollback consists of disabling the scheduled workflow or reverting the workflow, tests, operations documentation, design, implementation plan, doctoring evidence, and CHANGELOG entry through an independently reviewed pull request. Rollback must not rename or remove `NVIDIA_NIM_API_KEY` when another approved workflow also uses it, and must not alter the review-agent credential scheme. @@ -45,8 +49,8 @@ Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. htt Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 -Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb - Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html +GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 + GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From 46fdace878dd459cb2a9a3ce70289e43a0b01627 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:47:32 +0900 Subject: [PATCH 046/152] docs(ci): document archive entry-type gate --- .../operations/hourly-opencode-maintenance.md | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index a66334c8..aeb09930 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -29,20 +29,21 @@ Never place the key in repository variables, source files, workflow output, issu | Control | Pinned value | | --- | --- | | Schedule | `43 * * * *` | -| OpenCode release | `v1.18.13` immutable GitHub release | +| OpenCode release | immutable GitHub release `v1.18.13` | +| Linux x64 release asset | asset `501285078`, `opencode-linux-x64.tar.gz` | | Linux x64 archive SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | -| Expected archive shape | exactly one root member named `opencode` | +| Expected archive shape | exactly one root regular-file entry named `opencode` | | OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | -| OpenCode agent | Repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | +| OpenCode agent | repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | | OpenCode process timeout | `TERM` after 45 minutes, then `KILL` after a 30-second grace period | | GitHub job timeout | 50 minutes | | Session sharing | disabled | | Overlapping runs | disabled; an active run is not cancelled | -The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies the authoritative SHA-256 published by the upstream release process. Before extraction it lists the archive and requires exactly one member named `opencode`. It then creates a fresh mode-`0700` installation directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. +The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies its pinned SHA-256. Before extraction it lists the archive and requires exactly one member named `opencode`. It then reads the sole member's locale-stable GNU tar verbose metadata and requires the regular-file type character `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries before the filesystem is modified. It creates a fresh mode-`0700` installation directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. -Checksum validation and member validation are separate controls. The checksum binds the bytes to the reviewed release asset; the member check constrains the extraction shape if a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory and post-extraction type checks provide additional containment. +Checksum validation, member validation, entry-type validation, and post-extraction validation are separate controls. The checksum binds the bytes to the reviewed immutable release asset. Member-name and type checks constrain extraction when a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory, overwrite refusal, and post-extraction type checks provide further containment. OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It deliberately omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. @@ -99,15 +100,16 @@ The agent must not: 1. GitHub starts the workflow from the default branch. 2. The workflow checks out a shallow copy with persisted credentials disabled. -3. It downloads the OpenCode 1.18.13 Linux archive and verifies the pinned SHA-256. -4. It requires exactly one archive member named `opencode`, extracts into a fresh private directory without archived ownership or permissions, and verifies a regular non-symbolic-link executable with the exact version. -5. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. -6. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. -7. OpenCode inspects all current pull requests before selecting any work. -8. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. -9. The shell `EXIT` trap removes the local Git credential helper. -10. Independent review and repository checks evaluate the exact new head. -11. The separate disposition workflow may merge only after every gate passes. +3. It downloads the OpenCode 1.18.13 Linux x64 archive and verifies the pinned SHA-256. +4. It requires exactly one archive member named `opencode` and confirms through locale-stable verbose metadata that the entry is a regular file before extraction. +5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable with the exact version. +6. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. +7. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. +8. OpenCode inspects all current pull requests before selecting any work. +9. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. +10. The shell `EXIT` trap removes the local Git credential helper. +11. Independent review and repository checks evaluate the exact new head. +12. The separate disposition workflow may merge only after every gate passes. ## Failure handling @@ -117,9 +119,10 @@ The agent must not: | Repository token missing | Job fails before Git bootstrap | Restore normal GitHub Actions token availability; do not add a personal token | | GitHub CLI or Git bootstrap fails | Job fails before OpenCode starts or push fails visibly | Retain `persist-credentials: false`; verify the runner-provided `gh` executable and local helper entries | | Release archive unavailable | Installation step fails before extraction | Verify the immutable upstream release exists; do not substitute a floating version | -| Archive checksum mismatches | Installation fails closed before extraction | Treat as a supply-chain incident; compare the upstream immutable release and generated tap checksum before changing any pin | -| Archive has an unexpected member set | Installation fails closed before extraction | Treat the packaging change as a supply-chain review event; inspect the exact immutable asset before updating the member contract | -| Extracted object is absent, non-regular, or a symbolic link | Installation fails before execution | Treat as a supply-chain incident; do not relax the file-type checks | +| Archive checksum mismatches | Installation fails closed before extraction | Treat as a supply-chain incident; compare the immutable release record, asset metadata, and upstream publishing source before changing any pin | +| Archive has an unexpected member name or count | Installation fails closed before extraction | Treat the packaging change as a supply-chain review event; inspect the exact immutable asset before updating the member contract | +| Archive member is not a regular-file entry | Installation fails closed before extraction | Treat link, directory, or special-entry packaging as a supply-chain incident; do not relax the type gate | +| Extracted object is absent, non-regular, or a symbolic link | Installation fails before execution | Treat as a supply-chain incident; do not relax post-extraction checks | | OpenCode version mismatches | Installation step fails | Investigate the verified archive contents; do not bypass the version assertion | | NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | | Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | @@ -143,11 +146,12 @@ Before merging a workflow change, verify the exact current head has: - successful Windows, Ubuntu, and macOS CI; - successful dependency review, SBOM, Semgrep, Trivy, OSV, and Scorecard gates required by repository policy; - no unresolved current review thread; -- independent approval; +- independent approval anchored to the exact current head; - the `automerge-workflow` label required by the deterministic disposition workflow; - no new secret reference other than `NVIDIA_NIM_API_KEY`; - a full-SHA checkout pin and checksum-pinned immutable OpenCode release asset; - exactly one expected archive member validated before extraction; +- a pre-extraction regular-file entry-type check under `LC_ALL=C`; - a fresh mode-`0700` extraction directory, overwrite refusal, and regular non-symbolic-link executable validation; - no npm install command, floating package tag, or mutable OpenCode action reference; - workflow-level read-only permission plus explicit job-scoped write permissions; @@ -160,9 +164,9 @@ Before merging a workflow change, verify the exact current head has: Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 +Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/script/publish.ts -Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ @@ -176,6 +180,8 @@ GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential +GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 + GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From cc7766e635f474f40e31efaa024c75fbdc8a4882 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:48:14 +0900 Subject: [PATCH 047/152] docs(ci): design regular-entry validation --- ...8-04-hourly-opencode-maintenance-design.md | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index e8425fd5..a006aa41 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -13,7 +13,8 @@ The maintenance workflow will: - run at minute 43 of every hour and on manual dispatch; - use an immutable full-length SHA for `actions/checkout` and disable persisted checkout credentials; - download the immutable OpenCode `v1.18.13` Linux x64 release archive and verify SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction; -- require the verified archive to contain exactly one root member named `opencode`, extract it into a fresh mode-`0700` directory, and reject non-regular or symbolic-link output; +- require the verified archive to contain exactly one root member named `opencode` and require its GNU tar entry type to be a regular file before extraction; +- extract into a fresh mode-`0700` directory with overwrite refusal, then reject non-regular or symbolic-link output; - avoid npm install commands, floating package tags, and mutable OpenCode action references; - map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; - select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; @@ -28,21 +29,22 @@ The maintenance workflow will: ## Supply-chain boundary -An exact npm version is not a content identity and was reported by GitHub Advanced Security's Scorecard integration as an unpinned command dependency. The workflow therefore consumes the upstream immutable release asset directly. +An exact npm version is not a content identity and was reported by GitHub Advanced Security's Scorecard integration as an unpinned command dependency. The workflow therefore consumes the immutable upstream release asset directly. The installation step: 1. downloads only over HTTPS with redirect failure handling and a TLS 1.2 minimum; -2. verifies the archive against the SHA-256 published by the upstream release process and generated Homebrew tap; +2. verifies the archive against the pinned SHA-256 associated with the immutable GitHub release asset; 3. lists the verified archive and requires exactly one member named `opencode` before extraction; -4. refuses extraction on a checksum or member-shape mismatch; -5. creates a fresh mode-`0700` extraction directory and refuses overwrites; -6. extracts without preserving archive ownership or permissions; -7. requires the extracted object to be a regular file and not a symbolic link; -8. applies executable mode only to the expected `opencode` file; -9. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. +4. obtains locale-stable GNU tar verbose metadata and requires the sole member's type character to be `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries; +5. refuses extraction on a checksum, member-shape, or entry-type mismatch; +6. creates a fresh mode-`0700` extraction directory and refuses overwrites; +7. extracts without preserving archive ownership or permissions; +8. requires the extracted object to be a regular file and not a symbolic link; +9. applies executable mode only to the expected `opencode` file; +10. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. -The checksum and archive-member controls serve different purposes. SHA-256 binds the downloaded bytes to the reviewed upstream release. Exact member validation and private-directory extraction constrain filesystem effects when a future pin is changed incorrectly or upstream packaging changes. Post-extraction file-type checks prevent a symbolic-link or special-file output from being executed. +The checksum and archive controls serve different purposes. SHA-256 binds the downloaded bytes to the reviewed immutable upstream asset. Exact name and regular-entry validation constrain filesystem effects when a future pin is changed incorrectly or upstream packaging changes. Private-directory extraction and post-extraction file-type checks provide defense in depth. This closes the mutable npm-command finding without adding a package-manager bootstrap, lockfile-generation network step, floating release reference, or unconstrained archive extraction. @@ -64,7 +66,7 @@ The agent process already requires the repository token for GitHub API calls, so ## Permission inheritance boundary -GitHub applies workflow-level permissions to every job unless a job provides its own permission map. A top-level `contents: write` grant therefore creates avoidable future-job inheritance. The workflow now sets only top-level `contents: read` and gives the sole `maintain-repository` job its explicit read/write map. This preserves current functionality while preventing a future observation or reporting job from silently inheriting write authority. +GitHub applies workflow-level permissions to every job unless a job provides its own permission map. A top-level `contents: write` grant therefore creates avoidable future-job inheritance. The workflow sets only top-level `contents: read` and gives the sole `maintain-repository` job its explicit read/write map. This preserves current functionality while preventing a future observation or reporting job from silently inheriting write authority. ## Agent authority boundary @@ -85,18 +87,19 @@ The deterministic hourly disposition workflow remains responsible for exact-head 1. GitHub starts the scheduled workflow from `develop`. 2. Checkout reads the default-branch source with credentials persistence disabled. 3. The installer downloads the immutable OpenCode archive and verifies the pinned SHA-256. -4. The installer validates the one-member archive shape, extracts into a fresh private directory without archived ownership or permissions, and verifies a regular non-symbolic-link executable at the exact version. -5. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. -6. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. -7. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. -8. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. -9. The shell removes the repository-local credential helper. -10. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. +4. The installer validates the one-member archive shape and regular-file entry type before extraction. +5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable at the exact version. +6. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. +7. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. +8. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. +9. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. +10. The shell removes the repository-local credential helper. +11. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. ## Failure behavior -- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, archive-member mismatch, extracted-file type mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. -- The archive is never extracted after a checksum or member-shape mismatch. +- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, archive-member mismatch, non-regular archive entry, extracted-file type mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. +- The archive is never extracted after a checksum, member-shape, or entry-type mismatch. - Extraction occurs only in a newly recreated mode-`0700` directory and refuses overwrites. - No fallback provider or Copilot credential is configured. - Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. @@ -110,7 +113,8 @@ Repository tests parse the workflow as text and fail unless they prove all of th - hourly off-peak schedule, serialized concurrency, bounded graceful timeout, and forced termination; - full-SHA checkout pinning and disabled credential persistence; - immutable OpenCode release URL, exact SHA-256 verification, exact version verification, and no npm install command; -- exact one-member archive validation before extraction, private extraction directory creation, overwrite refusal, and regular non-symbolic-link output checks; +- exact one-member archive validation and regular-file entry-type validation before extraction; +- private extraction directory creation, overwrite refusal, and regular non-symbolic-link output checks; - exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; - no Copilot, Anthropic, or OpenAI credential path; - private session setting and direct GitHub token mode; @@ -122,9 +126,9 @@ Repository tests parse the workflow as text and fail unless they prove all of th Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 +Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/script/publish.ts -Anomaly. (2026). *OpenCode Homebrew formula* [Source code]. GitHub. https://github.com/anomalyco/homebrew-tap/blob/master/opencode.rb +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ @@ -138,6 +142,8 @@ GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential +GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 + GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From 982a6bc35635d36a4f020be57c998804fc4b5bcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:49:07 +0900 Subject: [PATCH 048/152] docs(ci): align archive entry-type plan --- ...-08-04-hourly-opencode-maintenance-plan.md | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index e59b3247..aac31224 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,7 +4,7 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Before extraction, the installer accepts only the reviewed one-member archive shape and uses a fresh private directory plus file-type checks. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. +**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Before extraction, the installer accepts only the reviewed single-member regular-file archive shape and uses a fresh private directory plus post-extraction file-type checks. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. **Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, GitHub CLI credential helper, GNU Coreutils `sha256sum` and `timeout`, GNU tar, bash, Maven, JUnit 5. @@ -15,7 +15,7 @@ - Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. - Pin third-party workflow sources and executable content immutably. - Download OpenCode only from the immutable `v1.18.13` release asset and verify Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. -- Require exactly one archive member named `opencode` before extraction; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output. +- Require exactly one archive member named `opencode` and require GNU tar's regular-file entry type before extraction; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output afterward. - Do not use npm install commands, floating package tags, or mutable OpenCode action references. - Keep checkout credential persistence disabled; bootstrap only a repository-local GitHub CLI credential helper and remove it through an `EXIT` trap. - Keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job. @@ -54,11 +54,11 @@ Observed initial RED: the explicit existence assertion failed because `.github/w Observed subsequent RED states covered the mutable npm command, workflow-scoped write permission, missing direct-token Git bootstrap, and prompt wording boundary before each corresponding production change. -- [x] **Step 3: Write the archive-member regression test** +- [x] **Step 3: Write archive regression tests** -Create `HourlyOpenCodeArchiveValidationTest` with complete beginner-readable Javadoc. Require the workflow to list the verified archive, accept exactly one member named `opencode`, emit a stable failure message for any other shape, and perform this validation before extraction. +Create `HourlyOpenCodeArchiveValidationTest` with complete beginner-readable Javadoc. Require the workflow to list the verified archive, accept exactly one member named `opencode`, obtain locale-stable GNU tar verbose metadata, require the regular-file type character `-`, emit stable failure messages, and perform all checks before extraction. -- [x] **Step 4: Run the archive test to verify RED** +- [x] **Step 4: Run archive tests to verify RED** Run: @@ -66,11 +66,13 @@ Run: ./mvnw -pl etl-service -Dtest=HourlyOpenCodeArchiveValidationTest test ``` -Observed on the pull-request CI merge ref for the test-only commit: `Tests run: 284, Failures: 1, Errors: 0`. The sole failure was `HourlyOpenCodeArchiveValidationTest.validatesOneExpectedArchiveMemberBeforeExtraction`, proving that the existing checksum-only installer did not satisfy the archive-member boundary. +Observed first RED on the pull-request CI merge ref for test-only commit `751eedb852eca1165a5b936296255fc608494dad`: 284 tests, one failure, zero errors. The sole failure was `validatesOneExpectedArchiveMemberBeforeExtraction`, proving that the checksum-only installer did not satisfy the member-shape boundary. + +Observed second RED on the pull-request CI merge ref for test-only commit `7b82a40b12c46aed869aeec7b387a161a7b33896`: 285 tests, one failure, zero errors, zero skipped project tests. The sole failure was `validatesRegularFileEntryTypeBeforeExtraction`, proving that name and count validation did not reject hard-link or other non-regular archive entries before extraction. - [x] **Step 5: Commit the failing contracts** -The workflow contracts were committed before their production fixes, including the dedicated archive-member regression contract. +The workflow contracts were committed before their production fixes, including the dedicated member-shape and regular-entry regression contracts. ### Task 2: Implement the checksum-pinned NVIDIA OpenCode workflow @@ -108,13 +110,14 @@ Before extraction: 2. download `opencode-linux-x64.tar.gz` from release `v1.18.13` over HTTPS; 3. validate SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` with `sha256sum --check --strict`; 4. list the archive and require exactly one member named `opencode`; -5. recreate the extraction directory with mode `0700`; -6. extract with ownership and archived permission restoration disabled and overwrite refusal enabled; -7. require the resulting `opencode` path to be a regular file and not a symbolic link; -8. apply executable mode only to the expected binary; -9. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. +5. under `LC_ALL=C`, obtain GNU tar verbose metadata and require its first type character to be `-` for a regular-file entry; +6. recreate the extraction directory with mode `0700`; +7. extract with ownership and archived permission restoration disabled and overwrite refusal enabled; +8. require the resulting `opencode` path to be a regular file and not a symbolic link; +9. apply executable mode only to the expected binary; +10. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. -Do not fall back to npm, a floating release, a mutable action reference, or a broader archive-member allowlist after any failure. +Do not fall back to npm, a floating release, a mutable action reference, a broader archive-member allowlist, or link/special-file entry support after any failure. - [x] **Step 3: Bootstrap direct-token Git access without persisted checkout credentials** @@ -157,16 +160,16 @@ Expected: PASS with no skipped project tests. - Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` **Interfaces:** -- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, upstream checksum evidence, GNU tar security guidance, GitHub workflow permission semantics, and GitHub CLI credential behavior. -- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, archive-member, timeout-escalation, security, and evidence documentation. +- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, release-asset metadata, GNU tar security guidance, GitHub workflow permission semantics, and GitHub CLI credential behavior. +- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, archive-member, archive-entry, timeout-escalation, security, and evidence documentation. - [x] **Step 1: Add and update operator documentation** -Document the secret name, provider alias, exact model and version, release asset and checksum, exact archive shape, private extraction directory, file-type checks, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, GNU tar, and GNU Coreutils primary documentation. +Document the secret name, provider alias, exact model and version, release asset and checksum, exact archive name and regular-file type, private extraction directory, file-type checks, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, GNU tar, and GNU Coreutils primary documentation. -- [ ] **Step 2: Add doctoring evidence and update `CHANGELOG.md`** +- [x] **Step 2: Add doctoring evidence and update `CHANGELOG.md`** -Record the threat, evidence chain, exact fail-closed controls, RED/GREEN verification, rollback implications, and APA 7 references in `docs/doctoring/opencode-archive-extraction-evidence.md`. Update the Unreleased entry to state that the installer validates exactly one expected archive member before extraction and rejects symbolic-link or non-regular output. +Record the threat, evidence chain, exact fail-closed controls, both RED cycles, rollback implications, and APA 7 references in `docs/doctoring/opencode-archive-extraction-evidence.md`. Update the Unreleased entry to state that the installer validates exactly one expected regular-file archive member before extraction and rejects symbolic-link or non-regular output. - [ ] **Step 3: Run the full reactor tests** @@ -176,14 +179,14 @@ Record the threat, evidence chain, exact fail-closed controls, RED/GREEN verific Expected: all modules build successfully; no project test is skipped. -### Task 4: Verify and open the protected workflow-change pull request +### Task 4: Verify and integrate the protected workflow-change pull request **Files:** - No additional source files. **Interfaces:** - Consumes: completed branch and all exact-head test results. -- Produces: a ready-for-review pull request targeting `develop`. +- Produces: a ready-for-review pull request targeting `develop` and a guarded merge only after every gate passes. - [ ] **Step 1: Verify exact branch head and diff** @@ -198,7 +201,7 @@ Expected: clean tree, no whitespace errors, successful build. - [x] **Step 2: Open the pull request** -Open a pull request titled `ci: schedule NVIDIA OpenCode maintenance agent`, explain the credential isolation and authority boundary, and apply `automerge-workflow` because the deterministic disposition workflow requires explicit approval for workflow changes. +Open a pull request titled `ci: schedule NVIDIA OpenCode maintenance agent`, explain credential and supply-chain isolation, and apply `automerge-workflow` because the deterministic disposition workflow requires explicit approval for workflow changes. - [ ] **Step 3: Request independent review and verify exact-head checks** From 42eb7d7ac8bc3912e3a50f98b427b712f78b2b9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:53:17 +0900 Subject: [PATCH 049/152] test(ci): require available free NVIDIA model --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index ed747ee5..227435e8 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -24,7 +24,7 @@ *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a * reviewer or merger. These tests make that separation visible to beginners and prevent a * later workflow edit from silently adding a fallback provider, mutable tool version, elevated - * token permission, protected-branch push, or self-approval path.

+ * token permission, protected-branch push, self-approval path, or deprecated hosted model.

*/ class HourlyOpenCodeMaintenanceWorkflowTest { @@ -139,14 +139,11 @@ void bootstrapsAndRemovesDirectTokenGitCredentials() { * mapped to the environment variable documented by OpenCode's NVIDIA provider. */ @Test - void usesOnlyTheNvidiaNimCredentialAndExplicitModel() { + void usesOnlyTheNvidiaNimCredential() { assertEquals(Set.of("NVIDIA_NIM_API_KEY"), referencedSecrets()); assertTrue(workflow.contains( "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" )); - assertTrue(workflow.contains( - "MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct" - )); assertTrue(workflow.contains("SHARE: \"false\"")); assertTrue(workflow.contains("USE_GITHUB_TOKEN: \"true\"")); @@ -156,6 +153,21 @@ void usesOnlyTheNvidiaNimCredentialAndExplicitModel() { assertFalse(workflow.contains("OPENAI_API_KEY")); } + /** + * Requires a currently available free NVIDIA endpoint suited to repository-scale coding. + * + *

The previous Qwen3 Coder trial endpoint was deprecated by NVIDIA. A scheduled agent + * that pins a deprecated hosted endpoint is not durable automation even when its workflow + * and credential handling are otherwise correct. DeepSeek V4 Pro is explicitly exposed by + * NVIDIA as a free endpoint with one-million-token context, agentic tool use, and software- + * engineering support, so the workflow pins that provider/model identifier.

+ */ + @Test + void usesCurrentFreeAgenticCodingModel() { + assertTrue(workflow.contains("MODEL: nvidia/deepseek-ai/deepseek-v4-pro")); + assertFalse(workflow.contains("qwen/qwen3-coder-480b-a35b-instruct")); + } + /** * Verifies least-privilege repository access, scopes write authority to the sole maintenance * job instead of every future job, and rejects an unnecessary OIDC token path. From 90c15026be8bd71ade359336b4ce835bac5dc3fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:54:45 +0900 Subject: [PATCH 050/152] fix(ci): select available free NVIDIA model --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index ca9aab44..688a0948 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -82,7 +82,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct + MODEL: nvidia/deepseek-ai/deepseek-v4-pro SHARE: "false" USE_GITHUB_TOKEN: "true" PROMPT: | From f0c0211c5782c96dc9125a147daa74fac7369e27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:55:24 +0900 Subject: [PATCH 051/152] docs(doctoring): record NVIDIA model selection evidence --- ...vidia-opencode-model-selection-evidence.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/doctoring/nvidia-opencode-model-selection-evidence.md diff --git a/docs/doctoring/nvidia-opencode-model-selection-evidence.md b/docs/doctoring/nvidia-opencode-model-selection-evidence.md new file mode 100644 index 00000000..dac82bfa --- /dev/null +++ b/docs/doctoring/nvidia-opencode-model-selection-evidence.md @@ -0,0 +1,57 @@ +# NVIDIA OpenCode model selection evidence + +## Scope + +This evidence note governs the hosted NVIDIA model identifier used by `.github/workflows/hourly-opencode-maintenance.yml`. It does not change the existing review agent, its provider, its credential names, or its approval authority. It also does not introduce a non-NVIDIA credential or model fallback. + +## Problem statement + +The workflow originally selected `nvidia/qwen/qwen3-coder-480b-a35b-instruct`. NVIDIA's current model catalog marks that model's free endpoint as deprecated. The partner endpoint remains available, but a repository workflow that relies only on `NVIDIA_NIM_API_KEY` must not assume a separately contracted partner endpoint. A deprecated free endpoint is therefore an operational reliability defect: the scheduler may be syntactically correct yet fail before it can inspect or improve the repository. + +## Decision + +Select `nvidia/deepseek-ai/deepseek-v4-pro` for the scheduled OpenCode development agent. + +NVIDIA's current primary documentation identifies `deepseek-ai/deepseek-v4-pro` as: + +- available through a free endpoint; +- suitable for coding, agentic AI, tool use, software engineering, and enterprise assistants; +- capable of structured output and function or tool calling; +- able to accept up to one million tokens of context; +- licensed for commercial and non-commercial use under the NVIDIA Open Model Agreement and the model's MIT terms. + +The NVIDIA model card reports stronger maximum-reasoning results than DeepSeek V4 Flash on the listed software-engineering and terminal-agent benchmarks, including SWE Verified, SWE Pro, SWE Multilingual, and Terminal Bench 2.0. mightyETL does not claim those benchmark values as its own performance and does not claim that OpenCode automatically selects NVIDIA's maximum-reasoning mode. The comparison is used only as current primary-source evidence that the Pro endpoint is a defensible high-capability free model for repository-scale agentic coding. + +## Cost and reliability boundary + +The selected endpoint is currently marked free by NVIDIA. That status is operational metadata, not a permanent contractual guarantee. The workflow therefore pins one explicit model identifier and its contract test rejects the known deprecated Qwen3 Coder identifier. It does not silently route to a paid partner endpoint or another provider. + +A future endpoint deprecation must be handled through the same test-first process: + +1. confirm status in NVIDIA's current primary model catalog and API reference; +2. add or update a failing contract test for the replacement identifier; +3. select a currently available NVIDIA free endpoint suited to coding and tool use; +4. update this evidence note, operations documentation, design, implementation plan, and `CHANGELOG.md`; +5. require exact-head CI, security checks, and independent approval before merge. + +The scheduled agent intentionally has no automatic model fallback. Running a second model after a partially completed agent session could create non-deterministic workspace state, duplicate branches, or conflicting pull requests. A model rejection therefore fails the run visibly and leaves repository state for the next reviewed maintenance change. + +## Test-first evidence + +`HourlyOpenCodeMaintenanceWorkflowTest.usesCurrentFreeAgenticCodingModel` was committed before the workflow model was changed. On the pull-request CI merge ref for test-only commit `42eb7d7ac8bc3912e3a50f98b427b712f78b2b9b`, GitHub Actions run `30964719079` reported 286 tests, one failure, zero errors, and zero skipped project tests on Ubuntu. The sole failure was the new model-availability contract because the workflow still contained the deprecated Qwen3 Coder identifier. + +The production workflow then replaced only the model identifier with `nvidia/deepseek-ai/deepseek-v4-pro`. The NVIDIA credential alias, OpenCode installation, permissions, branch authority, review-agent boundary, and merge protections remain unchanged. + +This note records design evidence rather than exact-head completion. Any later commit makes earlier CI and review evidence stale. + +## Operational response + +If NVIDIA rejects the model identifier, reports endpoint deprecation, or removes free access, do not add GitHub Copilot, Anthropic, OpenAI, or a partner-endpoint credential as an emergency fallback. Disable the scheduled workflow if necessary, preserve any open feature branch or pull request, and prepare a bounded reviewed model-selection change using current NVIDIA primary documentation. + +## References + +NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro + +NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro + +NVIDIA Corporation. (2026). *Qwen3-Coder-480B-A35B-Instruct*. NVIDIA NIM API catalog. https://build.nvidia.com/qwen/qwen3-coder-480b-a35b-instruct From 5747bf8201a1f6a3e32f607d798ac34977931b57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:57:50 +0900 Subject: [PATCH 052/152] docs(ci): select available NVIDIA coding model --- .../operations/hourly-opencode-maintenance.md | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index aeb09930..291eeec9 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -4,7 +4,7 @@ `.github/workflows/hourly-opencode-maintenance.yml` runs a bounded development agent at minute 43 of every hour, in UTC, and on manual dispatch. The agent uses OpenCode 1.18.13 with NVIDIA NIM to inspect the repository, repair one existing development pull request, or prepare one bounded buyer-visible improvement when no development pull request is open. -This workflow is intentionally separate from both code review and merge disposition. It does not replace independent review, GitHub branch protection, required checks, GitHub Advanced Security, Dependabot, CodeRabbit, or `.github/workflows/hourly-pr-disposition.yml`. +This workflow is intentionally separate from code review and merge disposition. It does not replace independent review, GitHub branch protection, required checks, GitHub Advanced Security, Dependabot, CodeRabbit, or `.github/workflows/hourly-pr-disposition.yml`. ## Required repository secret @@ -20,7 +20,7 @@ The workflow exposes that value only to the OpenCode process as the provider var NVIDIA_API_KEY ``` -The secret name used by the existing review agent is not changed. The scheduled workflow has no fallback credential for GitHub Copilot, Anthropic, OpenAI, or another model provider. A missing or empty `NVIDIA_NIM_API_KEY` fails the run before the agent starts. +The secret name used by the existing review agent is not changed. The scheduled workflow has no fallback credential for GitHub Copilot, Anthropic, OpenAI, a partner-only NVIDIA endpoint, or another model provider. A missing or empty `NVIDIA_NIM_API_KEY` fails the run before the agent starts. Never place the key in repository variables, source files, workflow output, issue comments, pull-request descriptions, step summaries, command arguments, or diagnostic logs. @@ -33,7 +33,7 @@ Never place the key in repository variables, source files, workflow output, issu | Linux x64 release asset | asset `501285078`, `opencode-linux-x64.tar.gz` | | Linux x64 archive SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | | Expected archive shape | exactly one root regular-file entry named `opencode` | -| OpenCode provider/model | `nvidia/qwen/qwen3-coder-480b-a35b-instruct` | +| OpenCode provider/model | `nvidia/deepseek-ai/deepseek-v4-pro` | | OpenCode agent | repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | | Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | | OpenCode process timeout | `TERM` after 45 minutes, then `KILL` after a 30-second grace period | @@ -41,15 +41,27 @@ Never place the key in repository variables, source files, workflow output, issu | Session sharing | disabled | | Overlapping runs | disabled; an active run is not cancelled | -The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies its pinned SHA-256. Before extraction it lists the archive and requires exactly one member named `opencode`. It then reads the sole member's locale-stable GNU tar verbose metadata and requires the regular-file type character `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries before the filesystem is modified. It creates a fresh mode-`0700` installation directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. +## Model selection boundary -Checksum validation, member validation, entry-type validation, and post-extraction validation are separate controls. The checksum binds the bytes to the reviewed immutable release asset. Member-name and type checks constrain extraction when a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory, overwrite refusal, and post-extraction type checks provide further containment. +The workflow uses `deepseek-ai/deepseek-v4-pro` because NVIDIA's current primary model catalog exposes it through a free endpoint and documents coding, agentic tool use, structured output, function calling, software-engineering use cases, and up to one million tokens of context. The previously selected Qwen3 Coder free endpoint is currently marked deprecated; relying on that endpoint would make the scheduled loop operationally brittle even when its workflow syntax and credentials were correct. -OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It deliberately omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. +The workflow deliberately has no automatic model fallback. A second model invocation after a partially completed agent session could continue from non-deterministic workspace state, create duplicate branches, or produce conflicting pull requests. If the selected endpoint becomes unavailable, the run fails visibly. A replacement requires current NVIDIA primary-source research, a failing contract test, updated doctoring evidence, exact-head checks, and independent review. + +Current selection evidence and its limits are recorded in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. The repository does not claim NVIDIA benchmark results as mightyETL performance and does not claim that raw OpenCode automatically selects a maximum-reasoning variant. + +## Supply-chain installation boundary + +The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies its pinned SHA-256. Before extraction it lists the archive and requires exactly one member named `opencode`. It then reads the sole member's locale-stable GNU tar verbose metadata and requires the regular-file type character `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries before the filesystem is modified. + +The installer creates a fresh mode-`0700` directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. + +Checksum validation, member validation, entry-type validation, and post-extraction validation are separate controls. The checksum binds the bytes to the reviewed immutable release asset. Member-name and type checks constrain extraction when a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory, overwrite refusal, and post-extraction type checks provide further containment. Detailed evidence is in `docs/doctoring/opencode-archive-extraction-evidence.md`. + +OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. ## Direct-token Git bootstrap -The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal `configureGit` function. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS credential helper. +The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal Git configuration. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS credential helper. Before starting OpenCode, the workflow therefore: @@ -76,7 +88,7 @@ That job receives only these explicit `GITHUB_TOKEN` permissions: - `security-events: read` - `statuses: read` -There is no `id-token` permission and no permission to write Actions or security events. `contents: write` is needed to prepare a feature branch; repository branch protection remains authoritative for protected branches. +There is no `id-token` permission and no permission to write Actions or security events. `contents: write` is required to prepare a feature branch; repository branch protection remains authoritative for protected branches. ## Authority boundaries @@ -88,13 +100,13 @@ The agent must not: - push directly to `develop` or `main`; - treat queued, pending, skipped-required, cancelled, stale-head, absent, or failed checks as passing; - bypass independent review, branch protection, repository policy, security gates, or coverage requirements; -- change the existing review agent, its provider, its workflow, its credential flow, or any review-agent secret name; +- change the existing review agent, its provider, workflow, credential flow, or any review-agent secret name; - modify `.github/workflows/**` or `CODEOWNERS` unless an open issue with the `automation-maintenance` label authorizes that exact change; - inspect or disclose secret values; - create a second development pull request while another development pull request is open; - publish a release unless a separate release-authorized workflow and all release acceptance gates permit it. -`.github/workflows/hourly-pr-disposition.yml` remains the deterministic exact-head merge boundary. It evaluates review state, unresolved threads, named checks, status contexts, labels, mergeability, and expected head SHA independently from the development agent. +`.github/workflows/hourly-pr-disposition.yml` remains the deterministic exact-head merge boundary. It independently evaluates review state, unresolved threads, named checks, status contexts, labels, mergeability, and expected head SHA. ## Normal run sequence @@ -105,8 +117,8 @@ The agent must not: 5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable with the exact version. 6. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. 7. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. -8. OpenCode inspects all current pull requests before selecting any work. -9. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves a feature branch and pull request. +8. OpenCode calls the explicit `nvidia/deepseek-ai/deepseek-v4-pro` endpoint and inspects all current pull requests before selecting work. +9. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves one feature branch and pull request. 10. The shell `EXIT` trap removes the local Git credential helper. 11. Independent review and repository checks evaluate the exact new head. 12. The separate disposition workflow may merge only after every gate passes. @@ -124,10 +136,10 @@ The agent must not: | Archive member is not a regular-file entry | Installation fails closed before extraction | Treat link, directory, or special-entry packaging as a supply-chain incident; do not relax the type gate | | Extracted object is absent, non-regular, or a symbolic link | Installation fails before execution | Treat as a supply-chain incident; do not relax post-extraction checks | | OpenCode version mismatches | Installation step fails | Investigate the verified archive contents; do not bypass the version assertion | -| NVIDIA API unavailable or model rejected | OpenCode step fails | Check NVIDIA service health and model availability; retain the current PR state | -| Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect the incomplete feature branch or PR; reduce slice size if needed | -| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate runner or process shutdown behavior and verify the ephemeral runner was destroyed | -| Tests or security checks fail | Pull request remains unmergeable | Fix the current exact head; never weaken the gate | +| NVIDIA endpoint is unavailable, deprecated, or rejects the model | OpenCode step fails without fallback | Confirm current NVIDIA catalog status; prepare a test-first reviewed model-selection change or disable the scheduler | +| Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect incomplete branch or PR state; reduce slice size if needed | +| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate shutdown behavior and verify the ephemeral runner was destroyed | +| Tests or security checks fail | Pull request remains unmergeable | Fix the exact current head; never weaken the gate | | Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | | Another hourly run starts while one is active | New run waits because concurrency is serialized | No action unless the prior run is stuck | @@ -135,7 +147,7 @@ The workflow must not claim success for partial work. A failed run may leave a f ## Rollback and disablement -This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, the doctoring evidence, and the corresponding `CHANGELOG.md` entry through a reviewed pull request. +This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, both doctoring evidence notes, and the corresponding `CHANGELOG.md` entries through a reviewed pull request. Do not delete or rename `NVIDIA_NIM_API_KEY` when it is also used by other approved workflows. Disabling this workflow does not require changing the review agent or its credential scheme. @@ -149,9 +161,10 @@ Before merging a workflow change, verify the exact current head has: - independent approval anchored to the exact current head; - the `automerge-workflow` label required by the deterministic disposition workflow; - no new secret reference other than `NVIDIA_NIM_API_KEY`; +- a current non-deprecated NVIDIA free endpoint suited to coding and tool use; +- no automatic provider or model fallback after a partially completed session; - a full-SHA checkout pin and checksum-pinned immutable OpenCode release asset; -- exactly one expected archive member validated before extraction; -- a pre-extraction regular-file entry-type check under `LC_ALL=C`; +- exactly one expected archive member and a pre-extraction regular-file entry-type check under `LC_ALL=C`; - a fresh mode-`0700` extraction directory, overwrite refusal, and regular non-symbolic-link executable validation; - no npm install command, floating package tag, or mutable OpenCode action reference; - workflow-level read-only permission plus explicit job-scoped write permissions; @@ -186,4 +199,8 @@ GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https:// GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions -NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis +NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro + +NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro + +NVIDIA Corporation. (2026). *Qwen3-Coder-480B-A35B-Instruct*. NVIDIA NIM API catalog. https://build.nvidia.com/qwen/qwen3-coder-480b-a35b-instruct From ca1dc7fe056cee4e5efd3914a042af0dd13421b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:59:08 +0900 Subject: [PATCH 053/152] docs(ci): design current NVIDIA model selection --- ...8-04-hourly-opencode-maintenance-design.md | 172 ++++++++---------- 1 file changed, 80 insertions(+), 92 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index a006aa41..5e50dc54 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -2,125 +2,119 @@ ## Purpose -mightyETL needs a scheduled development loop that can inspect the current repository state, remediate one bounded item, and open or update a pull request without weakening the independent review and merge gates. The scheduled agent must use OpenCode with the repository secret `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot and must not change the credentials or configuration of the existing review agent. +mightyETL needs a scheduled development loop that can inspect current repository state, remediate one bounded item, and open or update a pull request without weakening independent review and merge gates. The scheduled agent must use OpenCode with `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot or change the existing review agent's credentials, provider, workflow, or authority. ## Decision -Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic, fail-closed merge-disposition boundary and require that boundary to recognize only a non-author approval anchored to the exact current head. +Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic, fail-closed merge boundary and require a non-author approval anchored to the exact current head. The maintenance workflow will: - run at minute 43 of every hour and on manual dispatch; -- use an immutable full-length SHA for `actions/checkout` and disable persisted checkout credentials; -- download the immutable OpenCode `v1.18.13` Linux x64 release archive and verify SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction; -- require the verified archive to contain exactly one root member named `opencode` and require its GNU tar entry type to be a regular file before extraction; -- extract into a fresh mode-`0700` directory with overwrite refusal, then reject non-regular or symbolic-link output; -- avoid npm install commands, floating package tags, and mutable OpenCode action references; -- map `${{ secrets.NVIDIA_NIM_API_KEY }}` only to OpenCode's documented `NVIDIA_API_KEY` environment variable; -- select `nvidia/qwen/qwen3-coder-480b-a35b-instruct` explicitly; -- keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job; -- use the repository-scoped GitHub token, without OpenCode OIDC exchange, and grant only the write permissions needed to create branches, pull requests, and issues plus read access to checks, statuses, workflow outcomes, and security findings; -- bootstrap a repository-local GitHub CLI credential helper and bot author identity because OpenCode 1.18.13 skips internal Git setup when direct `GITHUB_TOKEN` mode is selected; -- remove the local helper through an `EXIT` trap after success, failure, or process timeout; -- omit the ineffective `AGENT` environment variable because raw OpenCode 1.18.13 uses repository `default_agent` configuration or its `build` fallback; +- pin `actions/checkout` by full SHA with persisted credentials disabled; +- install OpenCode 1.18.13 from an immutable release asset verified by SHA-256; +- accept exactly one regular archive member named `opencode` before extraction; +- extract into a fresh mode-`0700` directory with ownership, archived permissions, and overwrites disabled; +- reject non-regular or symbolic-link output and verify the exact executable version; +- map only `${{ secrets.NVIDIA_NIM_API_KEY }}` to `NVIDIA_API_KEY`; +- select `nvidia/deepseek-ai/deepseek-v4-pro`, a current NVIDIA free endpoint documented for coding, agentic tool use, function calling, and long-context software-engineering work; +- configure no automatic model or provider fallback; +- keep workflow-level permissions read-only and scope required write permissions to the sole maintenance job; +- use the repository-scoped GitHub token without OpenCode OIDC exchange; +- bootstrap a repository-local GitHub CLI credential helper and bot author because OpenCode 1.18.13 skips internal Git setup in direct-token mode; +- remove the helper through an `EXIT` trap; +- omit the ineffective `AGENT` environment variable, allowing repository `default_agent` or OpenCode's `build` fallback; - disable public session sharing; -- cap each run with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute workflow timeout; -- run from the protected default branch and never from pull-request code. +- cap each run with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute job timeout; +- run from protected default-branch source, never pull-request code. + +## Model selection boundary + +The previous Qwen3 Coder free endpoint is marked deprecated in NVIDIA's current catalog. A deprecated hosted endpoint makes the scheduler unreliable even if its workflow, credentials, and Git behavior are correct. DeepSeek V4 Pro is selected because NVIDIA currently exposes it through a free endpoint and documents coding, agentic AI, tool use, structured output, function calling, software-engineering use cases, and up to one million tokens of context. + +The model identifier is explicit and test-guarded. The workflow has no automatic fallback because invoking a second model after a partially completed session could operate on non-deterministic workspace state, create duplicate branches, or generate conflicting pull requests. Endpoint rejection fails visibly and requires a separate test-first model-selection change. The repository does not claim NVIDIA benchmarks as mightyETL performance and does not claim that raw OpenCode selects a maximum-reasoning variant. ## Supply-chain boundary -An exact npm version is not a content identity and was reported by GitHub Advanced Security's Scorecard integration as an unpinned command dependency. The workflow therefore consumes the immutable upstream release asset directly. +An exact npm version is not a content identity. The workflow therefore consumes the immutable upstream release asset directly. The installation step: -1. downloads only over HTTPS with redirect failure handling and a TLS 1.2 minimum; -2. verifies the archive against the pinned SHA-256 associated with the immutable GitHub release asset; -3. lists the verified archive and requires exactly one member named `opencode` before extraction; -4. obtains locale-stable GNU tar verbose metadata and requires the sole member's type character to be `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries; -5. refuses extraction on a checksum, member-shape, or entry-type mismatch; -6. creates a fresh mode-`0700` extraction directory and refuses overwrites; -7. extracts without preserving archive ownership or permissions; -8. requires the extracted object to be a regular file and not a symbolic link; -9. applies executable mode only to the expected `opencode` file; -10. verifies the binary reports exactly `1.18.13` before adding its directory to `GITHUB_PATH`. +1. downloads only over HTTPS with failure handling and TLS 1.2 minimum; +2. verifies SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`; +3. requires exactly one archive member named `opencode`; +4. under `LC_ALL=C`, requires GNU tar's regular-file type character `-` before extraction; +5. refuses extraction on checksum, member-shape, or entry-type mismatch; +6. recreates a private mode-`0700` directory and refuses overwrites; +7. extracts without restoring archive ownership or permissions; +8. requires a regular non-symbolic-link output file; +9. applies executable mode only to that file; +10. verifies version `1.18.13` before adding it to `GITHUB_PATH`. -The checksum and archive controls serve different purposes. SHA-256 binds the downloaded bytes to the reviewed immutable upstream asset. Exact name and regular-entry validation constrain filesystem effects when a future pin is changed incorrectly or upstream packaging changes. Private-directory extraction and post-extraction file-type checks provide defense in depth. - -This closes the mutable npm-command finding without adding a package-manager bootstrap, lockfile-generation network step, floating release reference, or unconstrained archive extraction. +Checksum binding, pre-extraction name and type checks, private extraction, overwrite refusal, and post-extraction file checks are cumulative controls. They close the mutable npm-command finding without a package-manager bootstrap, floating release, mutable action reference, or unconstrained extraction. ## Direct-token credential lifecycle -OpenCode 1.18.13 reads `USE_GITHUB_TOKEN`, uses `GITHUB_TOKEN` for GitHub API access, and then deliberately bypasses its `configureGit` function. Its scheduled-event path later invokes ordinary `git commit` and `git push` commands to publish the generated branch before creating a pull request. With `persist-credentials: false`, those commands otherwise have neither a commit author nor a credential source. +OpenCode 1.18.13 uses `GITHUB_TOKEN` for GitHub API access when `USE_GITHUB_TOKEN=true` and skips its internal `configureGit` function. Its scheduled-event path later uses ordinary `git commit` and `git push`. With `persist-credentials: false`, those commands otherwise lack both author identity and HTTPS credentials. -The workflow resolves this version-specific gap without weakening checkout isolation: +The workflow therefore: -1. checkout still persists no credential; -2. the run step fails closed if `NVIDIA_API_KEY`, `GITHUB_TOKEN`, or `GH_TOKEN` is empty; -3. inherited GitHub credential helpers are reset for the repository-local scope; -4. `!gh auth git-credential` is installed only as a repository-local helper and reads the ephemeral token from `GH_TOKEN` when Git requests credentials; -5. local `user.name` and `user.email` identify infrastructure-created commits as `opencode-agent[bot]`; -6. an `EXIT` trap removes the local helper even when OpenCode fails, exits after `TERM`, or is forcibly ended after the grace period; -7. no encoded token, personal token, OIDC permission, model fallback, tracked credential file, or review-agent secret is introduced. +1. fails closed if `NVIDIA_API_KEY`, `GITHUB_TOKEN`, or `GH_TOKEN` is empty; +2. resets inherited GitHub credential helpers in repository-local configuration; +3. installs `!gh auth git-credential`, which reads the ephemeral token from `GH_TOKEN` only when Git requests credentials; +4. sets local `user.name` and `user.email` to `opencode-agent[bot]`; +5. removes the helper through an `EXIT` trap after success, failure, timeout, or forced termination. -The agent process already requires the repository token for GitHub API calls, so this bootstrap does not expand token scope. It makes the granted `contents: write` capability operational while keeping credential resolution bounded to the ephemeral runner and repository-local Git configuration. +No encoded token, personal token, OIDC permission, tracked credential file, alternate model credential, or review-agent secret is introduced. ## Permission inheritance boundary -GitHub applies workflow-level permissions to every job unless a job provides its own permission map. A top-level `contents: write` grant therefore creates avoidable future-job inheritance. The workflow sets only top-level `contents: read` and gives the sole `maintain-repository` job its explicit read/write map. This preserves current functionality while preventing a future observation or reporting job from silently inheriting write authority. +GitHub applies workflow-level permissions to jobs unless a job provides its own map. The workflow sets only top-level `contents: read` and gives the sole `maintain-repository` job the minimum explicit map needed to inspect checks and security state, create a feature branch, update issues, and create or update a pull request. `contents: write` is necessary for the bounded branch operation; branch protection remains authoritative for `develop` and `main`. ## Agent authority boundary -The prompt is part of the security boundary. The OpenCode agent may inspect, test, edit, commit, push a feature branch, update one existing pull request, or open one pull request. It must not: +The prompt is part of the security boundary. The agent may inspect, test, edit, commit, push one feature branch, update one development pull request, or open one pull request. It must not: - approve or merge a pull request; - push directly to `develop` or `main`; -- bypass checks, branch protection, security gates, or independent review; -- alter review-agent workflows, review-agent secret names, `CODEOWNERS`, branch protection, or repository secrets; -- modify its own workflow or other `.github/workflows/**` files unless an open issue explicitly carries the `automation-maintenance` label; -- expose secret values, payloads, raw principals, raw idempotency keys, or internal exception details; -- create a second development pull request while another development pull request is open. - -The deterministic hourly disposition workflow remains responsible for exact-head merge eligibility. Branch protection remains authoritative even if the maintenance agent proposes a change. - -## Data and control flow - -1. GitHub starts the scheduled workflow from `develop`. -2. Checkout reads the default-branch source with credentials persistence disabled. -3. The installer downloads the immutable OpenCode archive and verifies the pinned SHA-256. -4. The installer validates the one-member archive shape and regular-file entry type before extraction. -5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable at the exact version. -6. The shell validates required tokens, installs the repository-local bot author and GitHub CLI credential helper, and registers helper cleanup. -7. `opencode github run` receives the bounded maintenance prompt, NVIDIA model selection, `GITHUB_TOKEN`, `GH_TOKEN`, and `NVIDIA_API_KEY` alias. -8. The agent inspects every open pull request first. If one exists, it works only on the dependency-eligible current head. If none exists, it selects one bounded buyer-visible gap, preferring the durable worker lifecycle tracked in issue #120. -9. OpenCode infrastructure commits and pushes the generated feature branch and opens or updates one pull request. It does not approve or merge. -10. The shell removes the repository-local credential helper. -11. Existing CI, security, independent review, and the deterministic disposition workflow evaluate the exact head independently. +- bypass checks, branch protection, security gates, coverage, or independent review; +- alter review-agent workflows, providers, credentials, secret names, `CODEOWNERS`, branch protection, or repository secrets; +- modify `.github/workflows/**` unless an issue explicitly labeled `automation-maintenance` authorizes that exact bounded change; +- expose secret values, request payloads, raw principals, raw idempotency keys, or internal exception details; +- create a second development pull request while another one is open; +- publish a release without a separate release-authorized workflow and every acceptance gate. + +The deterministic disposition workflow independently evaluates reviews, current threads, named checks, status contexts, labels, mergeability, and expected head SHA. + +## Data flow + +1. GitHub starts the workflow from `develop`. +2. Checkout reads protected default-branch source without persisting credentials. +3. The installer verifies the immutable OpenCode archive, member name, regular entry type, extraction boundary, output type, and executable version. +4. The shell validates required secrets and token aliases, installs local Git identity and helper, and registers cleanup. +5. OpenCode calls `nvidia/deepseek-ai/deepseek-v4-pro` with the bounded prompt. +6. The agent inspects all open pull requests first. It repairs one dependency-eligible exact head or, when none exists, selects one bounded buyer-visible gap, preferring issue #120 while open and ready. +7. OpenCode prepares one branch and pull request but does not approve or merge. +8. The shell removes its credential helper. +9. CI, security, independent review, branch protection, and deterministic disposition evaluate the exact head independently. ## Failure behavior -- A missing `NVIDIA_NIM_API_KEY`, missing repository token alias, unavailable NVIDIA endpoint, release download failure, checksum mismatch, archive-member mismatch, non-regular archive entry, extracted-file type mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or GitHub permission denial fails the scheduled job visibly. -- The archive is never extracted after a checksum, member-shape, or entry-type mismatch. -- Extraction occurs only in a newly recreated mode-`0700` directory and refuses overwrites. -- No fallback provider or Copilot credential is configured. -- Concurrency is serialized with `cancel-in-progress: false`, preventing overlapping maintenance runs from racing. -- At 45 minutes, the process receives `TERM`; if it remains alive after 30 seconds, GNU `timeout` sends `KILL` so the shell can complete its credential-cleanup trap before the 50-minute workflow limit. -- The workflow does not publish a release or merge partial work. +Missing credentials, deprecated or rejected model, NVIDIA outage, download failure, checksum mismatch, archive mismatch, non-regular entry, extracted-file mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or permission denial fails visibly. No provider fallback or partial-success claim is allowed. Concurrency is serialized. At 45 minutes, `TERM` is followed by `KILL` after 30 seconds if necessary; the job-level timeout remains 50 minutes. ## Verification -Repository tests parse the workflow as text and fail unless they prove all of the following: +Repository tests fail unless they prove: -- hourly off-peak schedule, serialized concurrency, bounded graceful timeout, and forced termination; -- full-SHA checkout pinning and disabled credential persistence; -- immutable OpenCode release URL, exact SHA-256 verification, exact version verification, and no npm install command; -- exact one-member archive validation and regular-file entry-type validation before extraction; -- private extraction directory creation, overwrite refusal, and regular non-symbolic-link output checks; -- exclusive use of `NVIDIA_NIM_API_KEY` through `NVIDIA_API_KEY` with the explicit NVIDIA model; -- no Copilot, Anthropic, or OpenAI credential path; -- private session setting and direct GitHub token mode; -- repository-local GitHub CLI credential helper, bot author identity, `EXIT` cleanup, no persisted encoded authorization header, and no ineffective `AGENT: build` claim; -- read-only workflow default plus job-scoped least-privilege permissions without `id-token`; -- prompt prohibitions against approval, merge, protected-branch pushes, review-agent key changes, workflow self-modification, and duplicate pull requests. +- hourly serialized scheduling and bounded forced termination; +- immutable checkout and OpenCode content pins; +- exact pre-extraction member name and regular-file type; +- private extraction, overwrite refusal, and regular non-symbolic-link output; +- exclusive use of `NVIDIA_NIM_API_KEY` and current `nvidia/deepseek-ai/deepseek-v4-pro` selection; +- rejection of the deprecated Qwen3 Coder identifier and non-NVIDIA credential paths; +- direct-token Git bootstrap and cleanup without stored encoded authorization; +- workflow-level read-only and job-scoped least privilege without OIDC; +- prompt prohibitions against approval, merge, protected-branch writes, review-agent changes, self-modification, and duplicate pull requests. ## References @@ -130,22 +124,16 @@ Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. htt Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 -Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ - -Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ - Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html -Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html - GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential -GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 - GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax -GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions +NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro + +NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro -NVIDIA Corporation. (2026). *LLM APIs*. NVIDIA API documentation. https://docs.api.nvidia.com/nim/reference/llm-apis +NVIDIA Corporation. (2026). *Qwen3-Coder-480B-A35B-Instruct*. NVIDIA NIM API catalog. https://build.nvidia.com/qwen/qwen3-coder-480b-a35b-instruct From 57d15c6670e79f517a82bb7810bedeb08f818ba2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:00:00 +0900 Subject: [PATCH 054/152] docs(ci): plan current NVIDIA model integration --- ...-08-04-hourly-opencode-maintenance-plan.md | 192 ++++++++---------- 1 file changed, 87 insertions(+), 105 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index aac31224..f62ea430 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -4,45 +4,44 @@ **Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. -**Architecture:** A new scheduled GitHub Actions workflow runs OpenCode from the protected default branch, uses a pinned NVIDIA model and checksum-verified OpenCode release, and may only prepare feature-branch pull requests. Before extraction, the installer accepts only the reviewed single-member regular-file archive shape and uses a fresh private directory plus post-extraction file-type checks. Existing review, CI, security, and hourly merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and adds a repository-local GitHub CLI credential helper because OpenCode 1.18.13 skips its own Git setup in that mode. A bounded `TERM` timeout escalates to `KILL` after 30 seconds so cleanup completes before the workflow-level timeout. +**Architecture:** A scheduled GitHub Actions workflow runs OpenCode from protected default-branch source, installs a checksum-verified immutable OpenCode release inside a constrained extraction boundary, and may prepare only one feature-branch pull request. It calls the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint without automatic fallback. Existing review, CI, security, branch protection, and merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and uses a removable repository-local GitHub CLI helper because OpenCode 1.18.13 skips its own Git setup in that mode. -**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, GitHub CLI credential helper, GNU Coreutils `sha256sum` and `timeout`, GNU tar, bash, Maven, JUnit 5. +**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, DeepSeek V4 Pro, GitHub CLI credential helper, GNU Coreutils, GNU tar, Bash, Maven, JUnit 5. ## Global Constraints -- Keep the review-agent key configuration unchanged and preserve `.github/workflows/hourly-pr-disposition.yml` as the independent deterministic merge boundary. -- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it to OpenCode as `NVIDIA_API_KEY`. -- Never configure GitHub Copilot, Anthropic, or OpenAI credentials as fallbacks. -- Pin third-party workflow sources and executable content immutably. -- Download OpenCode only from the immutable `v1.18.13` release asset and verify Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. -- Require exactly one archive member named `opencode` and require GNU tar's regular-file entry type before extraction; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output afterward. -- Do not use npm install commands, floating package tags, or mutable OpenCode action references. -- Keep checkout credential persistence disabled; bootstrap only a repository-local GitHub CLI credential helper and remove it through an `EXIT` trap. -- Keep the workflow-level token default read-only and scope required write permissions to the sole maintenance job. -- Bound OpenCode with `TERM` after 45 minutes, force `KILL` after a 30-second grace period, and retain a 50-minute GitHub job timeout. -- Do not claim that raw OpenCode 1.18.13 consumes an `AGENT` environment variable; it uses repository `default_agent` configuration or its `build` fallback. -- The scheduled agent may open or update a pull request, but may never approve, merge, bypass protection, or push to `develop` or `main`. +- Keep the review-agent provider, workflow, credential flow, and secret names unchanged. +- Preserve `.github/workflows/hourly-pr-disposition.yml` as the independent exact-head merge boundary. +- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it as `NVIDIA_API_KEY`. +- Pin `MODEL: nvidia/deepseek-ai/deepseek-v4-pro`; reject the deprecated Qwen3 Coder free-endpoint identifier. +- Do not configure GitHub Copilot, Anthropic, OpenAI, partner-only NVIDIA, or automatic model fallbacks. +- Pin executable content and third-party workflow sources immutably. +- Verify OpenCode 1.18.13 Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. +- Require exactly one regular archive entry named `opencode`; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output. +- Keep checkout credential persistence disabled; use only a repository-local GitHub CLI helper removed by an `EXIT` trap. +- Keep workflow-level permissions read-only and scope necessary writes to the sole maintenance job. +- Bound OpenCode with `TERM` after 45 minutes, `KILL` after a 30-second grace period, and a 50-minute job timeout. +- Do not claim raw OpenCode 1.18.13 consumes `AGENT`; use repository `default_agent` or its `build` fallback. +- The agent may create or update one pull request but may never approve, merge, bypass protection, or push to `develop` or `main`. - Preserve standalone operation and modular CWL service compatibility. --- -### Task 1: Add fail-closed workflow contract tests +### Task 1: Add fail-closed workflow contracts **Files:** - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java` **Interfaces:** -- Consumes: repository root discovery pattern from `HourlyPrDispositionWorkflowTest`, OpenCode 1.18.13 release metadata, the pinned GitHub-handler behavior, upstream release packaging, and GNU tar extraction guidance. -- Produces: text-level security, credential, archive, and configuration contracts for `.github/workflows/hourly-opencode-maintenance.yml`. +- Consumes: repository-root discovery, OpenCode 1.18.13 source and release metadata, NVIDIA model catalog, GNU tar guidance. +- Produces: cross-platform text contracts for schedule, credentials, model, archive, permissions, and authority. -- [x] **Step 1: Write the initial failing workflow test** +- [x] **Step 1: Write the initial missing-workflow test** -Create a JUnit 5 class that first asserts the workflow file exists, then verifies the schedule, concurrency, workflow timeout, graceful process timeout with deterministic forced termination, immutable checkout SHA, `persist-credentials: false`, immutable release URL, exact SHA-256 verification, exact OpenCode version verification, NVIDIA-only credential mapping, explicit NVIDIA Qwen3 Coder model, private sharing, direct GitHub token mode, job-scoped least-privilege permissions, no `id-token`, and prompt prohibitions. +Require the workflow to exist and encode schedule, concurrency, timeout, pinned checkout, no persisted credentials, immutable OpenCode installation, NVIDIA-only credentials, private sharing, direct GitHub token mode, least privilege, and prompt prohibitions. -Add a focused direct-token contract that requires a repository-local GitHub CLI credential helper, local bot author identity, helper cleanup through an `EXIT` trap, no persisted encoded authorization header, and removal of the ineffective `AGENT: build` environment claim. - -- [x] **Step 2: Run the initial focused test to verify RED** +- [x] **Step 2: Verify initial RED** Run: @@ -50,134 +49,113 @@ Run: ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test ``` -Observed initial RED: the explicit existence assertion failed because `.github/workflows/hourly-opencode-maintenance.yml` did not exist. +Observed: the existence assertion failed before the workflow was created. -Observed subsequent RED states covered the mutable npm command, workflow-scoped write permission, missing direct-token Git bootstrap, and prompt wording boundary before each corresponding production change. +- [x] **Step 3: Add immutable-installation and direct-token contracts** -- [x] **Step 3: Write archive regression tests** +Require exact release URL and checksum, no npm install, job-scoped writes, graceful and forced timeout, repository-local GitHub CLI helper, bot author, cleanup trap, and no encoded authorization header or ineffective `AGENT` claim. -Create `HourlyOpenCodeArchiveValidationTest` with complete beginner-readable Javadoc. Require the workflow to list the verified archive, accept exactly one member named `opencode`, obtain locale-stable GNU tar verbose metadata, require the regular-file type character `-`, emit stable failure messages, and perform all checks before extraction. +- [x] **Step 4: Add archive-member and entry-type contracts** -- [x] **Step 4: Run archive tests to verify RED** +Require exactly one member named `opencode`, locale-stable GNU tar metadata, regular-file type `-`, stable failure messages, and validation before extraction. -Run: +- [x] **Step 5: Verify archive RED cycles** -```bash -./mvnw -pl etl-service -Dtest=HourlyOpenCodeArchiveValidationTest test -``` +First test-only commit `751eedb852eca1165a5b936296255fc608494dad` produced 284 tests, one failure, zero errors; the sole failure was missing exact-member validation. -Observed first RED on the pull-request CI merge ref for test-only commit `751eedb852eca1165a5b936296255fc608494dad`: 284 tests, one failure, zero errors. The sole failure was `validatesOneExpectedArchiveMemberBeforeExtraction`, proving that the checksum-only installer did not satisfy the member-shape boundary. +Second test-only commit `7b82a40b12c46aed869aeec7b387a161a7b33896` produced 285 tests, one failure, zero errors, zero skipped project tests; the sole failure was missing regular-entry validation. -Observed second RED on the pull-request CI merge ref for test-only commit `7b82a40b12c46aed869aeec7b387a161a7b33896`: 285 tests, one failure, zero errors, zero skipped project tests. The sole failure was `validatesRegularFileEntryTypeBeforeExtraction`, proving that name and count validation did not reject hard-link or other non-regular archive entries before extraction. +- [x] **Step 6: Add current-model availability contract** -- [x] **Step 5: Commit the failing contracts** +Require: -The workflow contracts were committed before their production fixes, including the dedicated member-shape and regular-entry regression contracts. +```java +assertTrue(workflow.contains("MODEL: nvidia/deepseek-ai/deepseek-v4-pro")); +assertFalse(workflow.contains("qwen/qwen3-coder-480b-a35b-instruct")); +``` + +- [x] **Step 7: Verify model-selection RED** -### Task 2: Implement the checksum-pinned NVIDIA OpenCode workflow +Test-only commit `42eb7d7ac8bc3912e3a50f98b427b712f78b2b9b` produced 286 tests, one failure, zero errors, zero skipped project tests in Ubuntu run `30964719079`; the sole failure was `usesCurrentFreeAgenticCodingModel` because the workflow still selected the deprecated Qwen3 Coder free endpoint. + +### Task 2: Implement the bounded NVIDIA OpenCode workflow **Files:** - Create: `.github/workflows/hourly-opencode-maintenance.yml` **Interfaces:** -- Consumes: repository secret `NVIDIA_NIM_API_KEY`, built-in `GITHUB_TOKEN`, immutable OpenCode v1.18.13 Linux x64 release asset. -- Produces: one serialized scheduled maintenance run that can prepare a feature-branch pull request but cannot approve or merge. - -- [x] **Step 1: Add the minimal workflow** +- Consumes: `NVIDIA_NIM_API_KEY`, repository-scoped `GITHUB_TOKEN`, immutable OpenCode release asset. +- Produces: one serialized scheduled development session that can prepare but not approve or merge one pull request. -Configure `schedule` at `43 * * * *`, `workflow_dispatch`, serialized concurrency, `timeout-minutes: 50`, workflow-level `contents: read`, job-scoped maintenance permissions, a full-SHA checkout with disabled persisted credentials, immutable release download, exact SHA-256 verification, constrained extraction, exact version verification, and `timeout --signal=TERM --kill-after=30s 45m opencode github run`. +- [x] **Step 1: Add scheduling and authority boundary** -Set only the model and GitHub environment variables required by the direct-token path: +Configure `43 * * * *`, manual dispatch, serialized concurrency, 50-minute job timeout, protected source checkout, prompt constraints, and explicit prohibition of approval, merge, protected-branch push, review-agent modification, secret disclosure, duplicate PR creation, and release publication. -```yaml -GITHUB_TOKEN: ${{ github.token }} -GH_TOKEN: ${{ github.token }} -NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} -MODEL: nvidia/qwen/qwen3-coder-480b-a35b-instruct -SHARE: "false" -USE_GITHUB_TOKEN: "true" -PROMPT: | - ... -``` - -The prompt must encode every authority boundary from the design and instruct the agent to inspect all exact current PR heads before selecting work. +- [x] **Step 2: Add immutable OpenCode installation** -- [x] **Step 2: Install OpenCode from immutable and constrained content** +Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract into a private mode-`0700` directory with ownership, archived permissions, and overwrites disabled, reject non-regular or symbolic-link output, and verify version `1.18.13`. -Before extraction: +- [x] **Step 3: Add direct-token Git bootstrap** -1. require `curl`, `install`, `sha256sum`, `tar`, and the `ripgrep` dependency supplied by the runner; -2. download `opencode-linux-x64.tar.gz` from release `v1.18.13` over HTTPS; -3. validate SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` with `sha256sum --check --strict`; -4. list the archive and require exactly one member named `opencode`; -5. under `LC_ALL=C`, obtain GNU tar verbose metadata and require its first type character to be `-` for a regular-file entry; -6. recreate the extraction directory with mode `0700`; -7. extract with ownership and archived permission restoration disabled and overwrite refusal enabled; -8. require the resulting `opencode` path to be a regular file and not a symbolic link; -9. apply executable mode only to the expected binary; -10. verify the exact reported version before adding the temporary directory to `GITHUB_PATH`. +Fail closed on missing token aliases, reset inherited helpers locally, install `!gh auth git-credential`, set `opencode-agent[bot]` local identity, and remove the helper through an `EXIT` trap. -Do not fall back to npm, a floating release, a mutable action reference, a broader archive-member allowlist, or link/special-file entry support after any failure. +- [x] **Step 4: Select a current free NVIDIA coding endpoint** -- [x] **Step 3: Bootstrap direct-token Git access without persisted checkout credentials** +Set: -Before starting OpenCode: +```yaml +NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} +MODEL: nvidia/deepseek-ai/deepseek-v4-pro +SHARE: "false" +USE_GITHUB_TOKEN: "true" +``` -1. fail closed if `GITHUB_TOKEN` or `GH_TOKEN` is empty; -2. define the repository-local key `credential.https://github.com.helper`; -3. install an `EXIT` trap that removes that key; -4. reset inherited helper resolution locally with an empty helper entry; -5. add `!gh auth git-credential`, which obtains the ephemeral token from `GH_TOKEN` only when Git asks for credentials; -6. configure repository-local `user.name` and `user.email` for `opencode-agent[bot]`. +Do not add a provider or model fallback. Endpoint rejection must fail visibly. -Do not persist checkout credentials, encode the token into Git configuration, add a personal token, enable OIDC, or store credential material in tracked files. +- [ ] **Step 5: Verify focused GREEN on the integrated exact head** -- [ ] **Step 4: Run focused tests to verify GREEN** +Run: ```bash ./mvnw -pl etl-service \ -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest' test ``` -Expected: PASS. - -- [ ] **Step 5: Run workflow and documentation contract tests** - -```bash -./mvnw -pl etl-service \ - -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest,HourlyPrDispositionWorkflowTest,DocumentationValidationTest' test -``` - -Expected: PASS with no skipped project tests. +Expected: PASS with zero failures, errors, and skipped project tests. -### Task 3: Document operations, evidence, and release notes +### Task 3: Complete evidence and release notes **Files:** - Create: `docs/operations/hourly-opencode-maintenance.md` - Create: `docs/doctoring/opencode-archive-extraction-evidence.md` -- Modify: `CHANGELOG.md` +- Create: `docs/doctoring/nvidia-opencode-model-selection-evidence.md` - Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` - Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` +- Modify: `CHANGELOG.md` **Interfaces:** -- Consumes: behavior and constraints from the workflow, OpenCode 1.18.13 primary source, immutable release metadata, release-asset metadata, GNU tar security guidance, GitHub workflow permission semantics, and GitHub CLI credential behavior. -- Produces: beginner-readable activation, failure, rollback, credential-lifecycle, permission-inheritance, checksum, archive-member, archive-entry, timeout-escalation, security, and evidence documentation. +- Consumes: implemented workflow, TDD evidence, OpenCode source, NVIDIA current catalog/API, GitHub and GNU primary documentation. +- Produces: beginner-readable operations, rollback, model-selection, supply-chain, credential, permission, timeout, and governance evidence. -- [x] **Step 1: Add and update operator documentation** +- [x] **Step 1: Document archive and credential boundaries** -Document the secret name, provider alias, exact model and version, release asset and checksum, exact archive name and regular-file type, private extraction directory, file-type checks, schedule, job-scoped permissions, branch/PR lifecycle, direct-token GitHub CLI helper, credential cleanup, bounded `TERM`/`KILL` behavior, repository/default agent behavior, separation from review and merge agents, failure modes, rollback procedure, and APA 7 references to OpenCode, NVIDIA, GitHub, GNU tar, and GNU Coreutils primary documentation. +Document the immutable release, checksum, exact regular archive member, extraction boundary, direct-token helper lifecycle, permissions, timeout escalation, authority restrictions, failure handling, and rollback. -- [x] **Step 2: Add doctoring evidence and update `CHANGELOG.md`** +- [x] **Step 2: Document model selection** -Record the threat, evidence chain, exact fail-closed controls, both RED cycles, rollback implications, and APA 7 references in `docs/doctoring/opencode-archive-extraction-evidence.md`. Update the Unreleased entry to state that the installer validates exactly one expected regular-file archive member before extraction and rejects symbolic-link or non-regular output. +Record the deprecated Qwen3 Coder free endpoint, current DeepSeek V4 Pro free endpoint, coding and tool-use suitability, one-million-token context evidence, absence of automatic fallback, RED evidence, and APA 7 references. -- [ ] **Step 3: Run the full reactor tests** +- [ ] **Step 3: Align `CHANGELOG.md` with the exact integrated behavior** + +Replace the Qwen3 Coder model reference with `nvidia/deepseek-ai/deepseek-v4-pro`, record the deprecated-endpoint replacement, and list both doctoring evidence files. + +- [ ] **Step 4: Run full reactor verification** ```bash ./mvnw -B test ``` -Expected: all modules build successfully; no project test is skipped. +Expected: all modules succeed; no project test is skipped. ### Task 4: Verify and integrate the protected workflow-change pull request @@ -185,8 +163,8 @@ Expected: all modules build successfully; no project test is skipped. - No additional source files. **Interfaces:** -- Consumes: completed branch and all exact-head test results. -- Produces: a ready-for-review pull request targeting `develop` and a guarded merge only after every gate passes. +- Consumes: final exact branch head and all CI, security, and review evidence. +- Produces: guarded squash merge to `develop` only after every policy gate succeeds. - [ ] **Step 1: Verify exact branch head and diff** @@ -199,14 +177,18 @@ git diff develop...HEAD --check Expected: clean tree, no whitespace errors, successful build. -- [x] **Step 2: Open the pull request** +- [x] **Step 2: Open and label the pull request** + +Use title `ci: schedule NVIDIA OpenCode maintenance agent`; apply `automerge-workflow` because workflow files change, and retain `manual-merge` until an exact-head non-author approval exists. + +- [ ] **Step 3: Reinspect all feedback on the final exact head** -Open a pull request titled `ci: schedule NVIDIA OpenCode maintenance agent`, explain credential and supply-chain isolation, and apply `automerge-workflow` because the deterministic disposition workflow requires explicit approval for workflow changes. +Inspect human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Resolve only addressed current threads and distinguish stale or superseded findings. -- [ ] **Step 3: Request independent review and verify exact-head checks** +- [ ] **Step 4: Verify every final exact-head gate** -Request CodeRabbit and any configured independent reviewer. Treat queued, pending, skipped-required, stale-head, or cancelled checks as not passing. Resolve only addressed current threads. +Require successful Ubuntu, macOS, Windows, Dependency Review, SBOM, Semgrep, Trivy, OSV, Scorecard, combined status, mergeability, and no unresolved current thread. Treat pending, queued, cancelled, neutral-required, skipped-required, stale, or absent evidence as not passing. -- [ ] **Step 4: Merge only after every repository gate passes** +- [ ] **Step 5: Require independent exact-head approval and merge** -Do not self-approve. Merge only when branch protection, independent approval, security gates, repository policy, exact-head checks, and workflow-change policy are all satisfied. +Do not self-approve. After a non-author approval whose commit ID equals the current head and every exact-head gate succeeds, remove `manual-merge` and squash-merge using the expected head SHA. Otherwise keep the hold and report only the external approval or policy blocker after all autonomous remediation is exhausted. From a0ad9d0aab6eda2d7b9ac2609845f54ad121e278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:01:13 +0900 Subject: [PATCH 055/152] docs(changelog): record current NVIDIA model --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d86082c..2bba2d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. -- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one expected archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. +- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. +- The hourly OpenCode workflow now uses the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint for long-context coding and agentic tool use instead of the deprecated Qwen3 Coder free endpoint; model or endpoint rejection fails visibly without a non-NVIDIA, partner-only, or automatic fallback. - The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. @@ -25,8 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. -- Supply-chain doctoring evidence for checksum binding, exact archive-member validation, private extraction, file-type checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. +- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. +- Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. +- NVIDIA model-selection doctoring evidence for endpoint availability, deprecated-endpoint rejection, capability and context evidence, no-fallback semantics, test-first regression evidence, and replacement procedure in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. - Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`. - ETL problem-details client and operator contract: `docs/api/problem-details.md`. From d7596e4924f6c8466239490fa0615cfee4d07836 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:09:30 +0900 Subject: [PATCH 056/152] test(etl): reject empty JaCoCo coverage scopes --- .../etl/job/EtlJobCoveragePolicyTest.java | 199 +++++++++++++++--- 1 file changed, 172 insertions(+), 27 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java index 4a728d75..8e7328b6 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java @@ -1,55 +1,200 @@ package com.xtrmetl.etl.job; import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Keeps the durable-job production slice bound to an executable 100% coverage policy. + * Keeps the durable-job production slice bound to an executable, non-empty 100% coverage policy. * - *

The policy is intentionally scoped to the production classes introduced by the durable-job - * intake slice. It requires current Java-compatible JaCoCo instrumentation and zero missed - * instructions, lines, methods, or branches while the ordinary {@code mvn test} lifecycle runs.

+ *

JaCoCo's agent instrumentation filters and Maven report filters consume different name + * forms. A plugin-wide dotted include can therefore match neither compiled class-file paths nor + * the names seen by the agent, creating a report with zero analyzed classes that still satisfies + * zero-missed rules vacuously. This contract requires unrestricted test instrumentation, + * execution-specific class-file filters, and an explicit non-empty bundle check before the + * zero-missed instruction, line, method, and branch rules can pass.

*/ class EtlJobCoveragePolicyTest { + private static final Set DURABLE_JOB_CLASS_FILES = Set.of( + "com/xtrmetl/etl/job/*.class", + "com/xtrmetl/etl/controller/EtlJobController*.class" + ); + /** - * Requires the ETL module build to fail when any durable-job production path is untested. + * Requires the ETL module build to analyze at least one intended production class and fail + * when any analyzed durable-job path is untested. * * @throws IOException when the module build descriptor cannot be read + * @throws ParserConfigurationException when the JDK XML parser cannot be created + * @throws SAXException when the Maven descriptor is not well-formed XML */ @Test - void etlModuleEnforcesCompleteInstructionAndBranchCoverageForTheDurableJobSlice() - throws IOException { - String modulePom = read("etl-service/pom.xml"); - - assertTrue(modulePom.contains("jacoco-maven-plugin")); - assertTrue(modulePom.contains("0.8.15")); - assertTrue(modulePom.contains("initialize")); - assertTrue(modulePom.contains("prepare-agent")); - assertTrue(modulePom.contains("test")); - assertTrue(modulePom.contains("report")); - assertTrue(modulePom.contains("check")); - assertTrue(modulePom.contains("com.xtrmetl.etl.job.*")); - assertTrue(modulePom.contains( + void etlModuleEnforcesNonEmptyCompleteCoverageForTheDurableJobSlice() + throws IOException, ParserConfigurationException, SAXException { + Document modulePom = parseModulePom(); + Element jacocoPlugin = findPlugin(modulePom, "jacoco-maven-plugin"); + + assertEquals("0.8.15", directText(jacocoPlugin, "version")); + Element pluginConfiguration = directChild(jacocoPlugin, "configuration"); + assertTrue( + pluginConfiguration == null || directChild(pluginConfiguration, "includes") == null, + "JaCoCo includes must not be shared across agent and report goals" + ); + + Element prepareExecution = findExecution(jacocoPlugin, "prepare-durable-job-coverage"); + assertEquals("initialize", directText(prepareExecution, "phase")); + assertTrue(goalNames(prepareExecution).contains("prepare-agent")); + Element prepareConfiguration = directChild(prepareExecution, "configuration"); + assertTrue( + prepareConfiguration == null || directChild(prepareConfiguration, "includes") == null, + "The test agent must instrument all application classes; report filtering is separate" + ); + + Element reportExecution = findExecution(jacocoPlugin, "report-durable-job-coverage"); + assertEquals("test", directText(reportExecution, "phase")); + assertTrue(goalNames(reportExecution).contains("report")); + assertEquals(DURABLE_JOB_CLASS_FILES, configuredIncludes(reportExecution)); + + Element checkExecution = findExecution(jacocoPlugin, "check-durable-job-coverage"); + assertEquals("test", directText(checkExecution, "phase")); + assertTrue(goalNames(checkExecution).contains("check")); + assertEquals(DURABLE_JOB_CLASS_FILES, configuredIncludes(checkExecution)); + assertTrue(hasLimit(checkExecution, "BUNDLE", "INSTRUCTION", "TOTALCOUNT", "minimum", "1")); + + for (String counter : Set.of("INSTRUCTION", "LINE", "METHOD", "BRANCH")) { + assertTrue( + hasLimit(checkExecution, "CLASS", counter, "MISSEDCOUNT", "maximum", "0"), + () -> "Missing zero-missed class rule for " + counter + ); + } + + String serializedPom = Files.readString(projectRoot().resolve("etl-service/pom.xml")); + assertFalse(serializedPom.contains("com.xtrmetl.etl.job.*")); + assertFalse(serializedPom.contains( "com.xtrmetl.etl.controller.EtlJobController*" )); - assertTrue(modulePom.contains("INSTRUCTION")); - assertTrue(modulePom.contains("LINE")); - assertTrue(modulePom.contains("METHOD")); - assertTrue(modulePom.contains("BRANCH")); - assertTrue(modulePom.contains("MISSEDCOUNT")); - assertTrue(modulePom.contains("0")); } - private static String read(String relativePath) throws IOException { - return Files.readString(projectRoot().resolve(relativePath), StandardCharsets.UTF_8); + private static Document parseModulePom() + throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(projectRoot().resolve("etl-service/pom.xml").toFile()); + } + + private static Element findPlugin(Document document, String artifactId) { + NodeList plugins = document.getElementsByTagName("plugin"); + for (int index = 0; index < plugins.getLength(); index++) { + Element plugin = (Element) plugins.item(index); + if (artifactId.equals(directText(plugin, "artifactId"))) { + return plugin; + } + } + throw new AssertionError("Missing Maven plugin " + artifactId); + } + + private static Element findExecution(Element plugin, String executionId) { + NodeList executions = plugin.getElementsByTagName("execution"); + for (int index = 0; index < executions.getLength(); index++) { + Element execution = (Element) executions.item(index); + if (executionId.equals(directText(execution, "id"))) { + return execution; + } + } + throw new AssertionError("Missing JaCoCo execution " + executionId); + } + + private static Set goalNames(Element execution) { + Element goals = directChild(execution, "goals"); + assertNotNull(goals, "Every JaCoCo execution must declare goals"); + return directTexts(goals, "goal"); + } + + private static Set configuredIncludes(Element execution) { + Element configuration = directChild(execution, "configuration"); + assertNotNull(configuration, "Report and check executions require explicit configuration"); + Element includes = directChild(configuration, "includes"); + assertNotNull(includes, "Report and check executions require class-file include patterns"); + return directTexts(includes, "include"); + } + + private static boolean hasLimit( + Element execution, + String elementName, + String counter, + String value, + String boundName, + String boundValue + ) { + Element configuration = directChild(execution, "configuration"); + assertNotNull(configuration); + NodeList rules = configuration.getElementsByTagName("rule"); + for (int ruleIndex = 0; ruleIndex < rules.getLength(); ruleIndex++) { + Element rule = (Element) rules.item(ruleIndex); + if (!elementName.equals(directText(rule, "element"))) { + continue; + } + NodeList limits = rule.getElementsByTagName("limit"); + for (int limitIndex = 0; limitIndex < limits.getLength(); limitIndex++) { + Element limit = (Element) limits.item(limitIndex); + if (counter.equals(directText(limit, "counter")) + && value.equals(directText(limit, "value")) + && boundValue.equals(directText(limit, boundName))) { + return true; + } + } + } + return false; + } + + private static Set directTexts(Element parent, String childName) { + Set values = new HashSet<>(); + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && childName.equals(element.getTagName())) { + values.add(element.getTextContent().trim()); + } + } + return Set.copyOf(values); + } + + private static String directText(Element parent, String childName) { + Element child = directChild(parent, childName); + return child == null ? null : child.getTextContent().trim(); + } + + private static Element directChild(Element parent, String childName) { + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && childName.equals(element.getTagName())) { + return element; + } + } + return null; } private static Path projectRoot() { From 25170565cedc3178cb8bef992f94b2155bbe348a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:10:52 +0900 Subject: [PATCH 057/152] fix(etl): make JaCoCo coverage fail closed --- etl-service/pom.xml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/etl-service/pom.xml b/etl-service/pom.xml index 00695de2..38029849 100644 --- a/etl-service/pom.xml +++ b/etl-service/pom.xml @@ -98,12 +98,6 @@ org.jacoco jacoco-maven-plugin 0.8.15 - - - com.xtrmetl.etl.job.* - com.xtrmetl.etl.controller.EtlJobController* - - prepare-durable-job-coverage @@ -118,6 +112,12 @@ report + + + com/xtrmetl/etl/job/*.class + com/xtrmetl/etl/controller/EtlJobController*.class + + check-durable-job-coverage @@ -126,13 +126,23 @@ check + + com/xtrmetl/etl/job/*.class + com/xtrmetl/etl/controller/EtlJobController*.class + + + BUNDLE + + + INSTRUCTION + TOTALCOUNT + 1 + + + CLASS - - com.xtrmetl.etl.job.* - com.xtrmetl.etl.controller.EtlJobController* - INSTRUCTION From 3b25dfdbcf11c0426c6f0b72e01fc6283ac7f0af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:20:35 +0900 Subject: [PATCH 058/152] test(etl): compare coverage includes deterministically --- .../com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java index 8e7328b6..cc1b1096 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java @@ -71,12 +71,12 @@ void etlModuleEnforcesNonEmptyCompleteCoverageForTheDurableJobSlice() Element reportExecution = findExecution(jacocoPlugin, "report-durable-job-coverage"); assertEquals("test", directText(reportExecution, "phase")); assertTrue(goalNames(reportExecution).contains("report")); - assertEquals(DURABLE_JOB_CLASS_FILES, configuredIncludes(reportExecution)); + assertConfiguredIncludes(reportExecution); Element checkExecution = findExecution(jacocoPlugin, "check-durable-job-coverage"); assertEquals("test", directText(checkExecution, "phase")); assertTrue(goalNames(checkExecution).contains("check")); - assertEquals(DURABLE_JOB_CLASS_FILES, configuredIncludes(checkExecution)); + assertConfiguredIncludes(checkExecution); assertTrue(hasLimit(checkExecution, "BUNDLE", "INSTRUCTION", "TOTALCOUNT", "minimum", "1")); for (String counter : Set.of("INSTRUCTION", "LINE", "METHOD", "BRANCH")) { @@ -93,6 +93,13 @@ void etlModuleEnforcesNonEmptyCompleteCoverageForTheDurableJobSlice() )); } + private static void assertConfiguredIncludes(Element execution) { + assertEquals( + DURABLE_JOB_CLASS_FILES.stream().sorted().toList(), + configuredIncludes(execution).stream().sorted().toList() + ); + } + private static Document parseModulePom() throws ParserConfigurationException, IOException, SAXException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); From bfd511480f6a03ca415ee3b428168304a1a36e28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:25:22 +0900 Subject: [PATCH 059/152] test(etl): cover durable intake validation boundaries --- .../EtlJobServiceCoverageCompletionTest.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java new file mode 100644 index 00000000..48d755c1 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java @@ -0,0 +1,117 @@ +package com.xtrmetl.etl.job; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xtrmetl.etl.service.EtlBatchProperties; +import com.xtrmetl.etl.service.EtlRequestError; +import com.xtrmetl.etl.service.EtlRequestException; +import com.xtrmetl.etl.service.EtlRequestLock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Exercises realistic durable-intake validation boundaries that are easy to miss in ordinary + * happy-path and persistence tests. + * + *

The cases model malformed empty request bodies, visually ambiguous Unicode identifiers, + * identifiers that exceed the documented code-point bound, and separators that can alter how an + * identifier appears in logs or text tools. Every rejection must happen before transaction, lock, + * or database work so an invalid client request cannot consume shared persistence capacity.

+ */ +class EtlJobServiceCoverageCompletionTest { + + private static final String IDEMPOTENCY_KEY = "550e8400-e29b-41d4-a716-446655440000"; + private static final String PRINCIPAL_SCOPE = "tenant_alpha"; + + /** + * Verifies that an empty JSON stream is rejected as malformed rather than reaching transaction + * or persistence work. Jackson represents this input with a {@code null} parse result instead + * of a JSON null node, so it is a distinct real-world boundary from the literal text + * {@code null}. + */ + @Test + void rejectsEmptyJsonStreamBeforeTransactionOrPersistence() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + EtlJobService service = service(jdbcTemplate, requestLock); + + EtlRequestException exception = assertThrows( + EtlRequestException.class, + () -> service.submit("", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) + ); + + assertEquals(EtlRequestError.INVALID_JSON, exception.error()); + verifyNoInteractions(requestLock, jdbcTemplate); + } + + /** + * Verifies identifier rejection for Unicode boundary whitespace, format controls, line and + * paragraph separators, and the documented 256-code-point maximum. + * + * @param identifier identifier embedded in an otherwise valid one-record JSON batch + * @param scenario beginner-readable reason that the case is unsafe + */ + @ParameterizedTest(name = "{1}") + @MethodSource("unsafeIdentifiers") + void rejectsUnsafeOrOverlongIdentifiersBeforeTransactionOrPersistence( + String identifier, + String scenario + ) { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + EtlJobService service = service(jdbcTemplate, requestLock); + String payload = "[{\"id\":" + new ObjectMapper().valueToTree(identifier) + "}]"; + + EtlRequestException exception = assertThrows( + EtlRequestException.class, + () -> service.submit(payload, IDEMPOTENCY_KEY, PRINCIPAL_SCOPE), + scenario + ); + + assertEquals(EtlRequestError.INVALID_RECORD, exception.error()); + verifyNoInteractions(requestLock, jdbcTemplate); + } + + /** + * Supplies concrete identifier attacks and admission-bound violations. + * + * @return parameter stream containing identifier text and its operational threat + */ + private static Stream unsafeIdentifiers() { + return Stream.of( + Arguments.of("\u00a0record_alpha", "non-breaking boundary whitespace"), + Arguments.of("x".repeat(257), "identifier longer than 256 code points"), + Arguments.of("record\u200balpha", "zero-width format control"), + Arguments.of("record\u2028alpha", "Unicode line separator"), + Arguments.of("record\u2029alpha", "Unicode paragraph separator") + ); + } + + /** + * Creates a service whose lock and database collaborators reveal any premature side effect. + * + * @param jdbcTemplate mocked database collaborator + * @param requestLock mocked transaction-lifetime lock collaborator + * @return service configured with normal production admission limits + */ + private static EtlJobService service( + JdbcTemplate jdbcTemplate, + EtlRequestLock requestLock + ) { + return new EtlJobService( + jdbcTemplate, + new ObjectMapper(), + new EtlBatchProperties(), + requestLock + ); + } +} From 516bc5720bcaac232f793a17f6b48a5013fc85c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:28:56 +0900 Subject: [PATCH 060/152] test(etl): cover every durable intake branch --- .../etl/job/EtlJobServiceBoundaryTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java index 6ce140d6..3d524649 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java @@ -110,6 +110,10 @@ void rejectsEveryCoveredPayloadAdmissionFailureBeforePersistence() { EtlRequestError.INVALID_JSON, () -> service.submit(null, IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_JSON, + () -> service.submit("", IDEMPOTENCY_KEY, "tenant_alpha") + ); assertError( EtlRequestError.INVALID_JSON, () -> service.submit("null", IDEMPOTENCY_KEY, "tenant_alpha") @@ -134,6 +138,10 @@ void rejectsEveryCoveredPayloadAdmissionFailureBeforePersistence() { "tenant_alpha" ) ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> EtlJobService.validateRecord(null) + ); assertError( EtlRequestError.INVALID_RECORD, () -> service.submit("[null]", IDEMPOTENCY_KEY, "tenant_alpha") @@ -154,10 +162,38 @@ void rejectsEveryCoveredPayloadAdmissionFailureBeforePersistence() { EtlRequestError.INVALID_RECORD, () -> service.submit("[{\"id\":\" record_alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + "[{\"id\":\"\u00a0record_alpha\"}]", + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + "[{\"id\":\"" + "x".repeat(257) + "\"}]", + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); assertError( EtlRequestError.INVALID_RECORD, () -> service.submit("[{\"id\":\"record\\u0000alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit("[{\"id\":\"record\\u200dalpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit("[{\"id\":\"record\\u2028alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit("[{\"id\":\"record\\u2029alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") + ); assertError( EtlRequestError.INVALID_RECORD, () -> service.submit( From d4d8a9ef56c81ad9a3598072d0be14f3c33c829d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:29:50 +0900 Subject: [PATCH 061/152] refactor(etl): centralize durable SHA-256 identity --- .../com/xtrmetl/etl/job/EtlJobService.java | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobService.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobService.java index 5f9b4645..e1992d17 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobService.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobService.java @@ -9,6 +9,7 @@ import com.xtrmetl.etl.service.EtlRequestException; import com.xtrmetl.etl.service.EtlRequestLock; import com.xtrmetl.etl.service.PostgresEtlRequestLock; +import com.xtrmetl.etl.service.Sha256Digest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.lang.Nullable; @@ -17,12 +18,9 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.sql.Timestamp; import java.time.Instant; import java.util.HashSet; -import java.util.HexFormat; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -168,10 +166,12 @@ public EtlJobSubmission submit( String validatedPayload = validatePayload(requestPayload); requireActiveTransaction(); - String principalScopeHash = sha256(validatedScope); - String submissionKeyHash = sha256(validatedKey); - String submissionLockHash = sha256(principalScopeHash + ":" + submissionKeyHash); - String requestDigest = sha256(validatedPayload); + String principalScopeHash = Sha256Digest.digest(validatedScope); + String submissionKeyHash = Sha256Digest.digest(validatedKey); + String submissionLockHash = Sha256Digest.digest( + principalScopeHash + ":" + submissionKeyHash + ); + String requestDigest = Sha256Digest.digest(validatedPayload); if (!requestLock.tryLock(submissionLockHash)) { throw new EtlRequestException(EtlRequestError.JOB_SUBMISSION_IN_PROGRESS); @@ -219,7 +219,7 @@ public EtlJobSnapshot findOwned( @Nullable String principalScope ) { UUID validatedJobId = Objects.requireNonNull(jobRecordId, "jobRecordId must not be null"); - String principalScopeHash = sha256(validatePrincipalScope(principalScope)); + String principalScopeHash = Sha256Digest.digest(validatePrincipalScope(principalScope)); List jobs = jdbcTemplate.query( SELECT_OWNED_JOB_SQL, (resultSet, rowNumber) -> mapSnapshot(resultSet.getObject("job_record_id", UUID.class), @@ -310,7 +310,7 @@ private String validatePayload(@Nullable String requestPayload) { return requestPayload; } - private static void validateRecord(@Nullable JsonNode record) { + static void validateRecord(@Nullable JsonNode record) { if (record == null || !record.isObject()) { throw new EtlRequestException(EtlRequestError.INVALID_RECORD); } @@ -380,16 +380,6 @@ private static void requireActiveTransaction() { } } - private static String sha256(String value) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - byte[] digest = messageDigest.digest(value.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(digest); - } catch (NoSuchAlgorithmException exception) { - throw new IllegalStateException("SHA-256 is required by the Java platform", exception); - } - } - private record StoredJobRecord(String requestDigest, EtlJobSnapshot snapshot) { private StoredJobRecord { Objects.requireNonNull(requestDigest, "requestDigest must not be null"); From 1907a48b3ec336e7ba554c298ca6710a6d1b3211 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:30:12 +0900 Subject: [PATCH 062/152] feat(etl): centralize SHA-256 digesting --- .../com/xtrmetl/etl/service/Sha256Digest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/service/Sha256Digest.java diff --git a/etl-service/src/main/java/com/xtrmetl/etl/service/Sha256Digest.java b/etl-service/src/main/java/com/xtrmetl/etl/service/Sha256Digest.java new file mode 100644 index 00000000..4603b431 --- /dev/null +++ b/etl-service/src/main/java/com/xtrmetl/etl/service/Sha256Digest.java @@ -0,0 +1,53 @@ +package com.xtrmetl.etl.service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; + +/** + * Produces lowercase SHA-256 hexadecimal digests for durable ETL persistence identities. + * + *

SHA-256 is required by the Java platform. The defensive exception branch therefore indicates + * a broken runtime rather than invalid customer input.

+ */ +public final class Sha256Digest { + + private Sha256Digest() { + // Utility class. + } + + /** + * Hashes one UTF-8 string into lowercase 64-character SHA-256 hexadecimal text. + * + * @param value text to hash + * @return lowercase SHA-256 hexadecimal digest + * @throws NullPointerException when the value is {@code null} + * @throws IllegalStateException when the Java runtime lacks mandatory SHA-256 support + */ + public static String digest(String value) { + return digest(value, () -> MessageDigest.getInstance("SHA-256")); + } + + static String digest(String value, MessageDigestFactory messageDigestFactory) { + String requiredValue = Objects.requireNonNull(value, "value must not be null"); + MessageDigestFactory requiredFactory = Objects.requireNonNull( + messageDigestFactory, + "messageDigestFactory must not be null" + ); + try { + MessageDigest messageDigest = requiredFactory.create(); + byte[] digest = messageDigest.digest(requiredValue.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(digest); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is required by the Java platform", exception); + } + } + + @FunctionalInterface + interface MessageDigestFactory { + + MessageDigest create() throws NoSuchAlgorithmException; + } +} From 9b94b1472c0f7de435e7f53817ea5e138e003af6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:30:25 +0900 Subject: [PATCH 063/152] test(etl): verify SHA-256 digest utility --- .../xtrmetl/etl/service/Sha256DigestTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java b/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java new file mode 100644 index 00000000..55401a48 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java @@ -0,0 +1,55 @@ +package com.xtrmetl.etl.service; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies deterministic lowercase SHA-256 text identities and fail-closed runtime handling. + */ +class Sha256DigestTest { + + @Test + void producesThePublishedSha256Vector() { + assertEquals( + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + Sha256Digest.digest("abc") + ); + } + + @Test + void rejectsMissingInputOrFactory() { + assertThrows(NullPointerException.class, () -> Sha256Digest.digest(null)); + assertThrows( + NullPointerException.class, + () -> Sha256Digest.digest("abc", null) + ); + } + + @Test + void convertsMissingMandatoryAlgorithmIntoBrokenRuntimeSignal() { + NoSuchAlgorithmException missingAlgorithm = new NoSuchAlgorithmException("missing"); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> Sha256Digest.digest("abc", () -> { + throw missingAlgorithm; + }) + ); + + assertEquals("SHA-256 is required by the Java platform", exception.getMessage()); + assertInstanceOf(NoSuchAlgorithmException.class, exception.getCause()); + } + + @Test + void utilityConstructorCannotBeCalledNormallyButRemainsCovered() throws Exception { + Constructor constructor = Sha256Digest.class.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + } +} From b0a5d49b64992f07807289cd3012c399f01a1f07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:30:55 +0900 Subject: [PATCH 064/152] test(etl): include digest utility in complete coverage --- etl-service/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etl-service/pom.xml b/etl-service/pom.xml index 38029849..9e0e56b5 100644 --- a/etl-service/pom.xml +++ b/etl-service/pom.xml @@ -116,6 +116,7 @@ com/xtrmetl/etl/job/*.class com/xtrmetl/etl/controller/EtlJobController*.class + com/xtrmetl/etl/service/Sha256Digest*.class
@@ -129,6 +130,7 @@ com/xtrmetl/etl/job/*.class com/xtrmetl/etl/controller/EtlJobController*.class + com/xtrmetl/etl/service/Sha256Digest*.class From 3106dc779919c3289a70ddd99d8b57f96cb6bc45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:31:37 +0900 Subject: [PATCH 065/152] test(etl): bind digest utility to complete coverage --- .../java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java index cc1b1096..74fa5e0a 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java @@ -35,7 +35,8 @@ class EtlJobCoveragePolicyTest { private static final Set DURABLE_JOB_CLASS_FILES = Set.of( "com/xtrmetl/etl/job/*.class", - "com/xtrmetl/etl/controller/EtlJobController*.class" + "com/xtrmetl/etl/controller/EtlJobController*.class", + "com/xtrmetl/etl/service/Sha256Digest*.class" ); /** @@ -91,6 +92,9 @@ void etlModuleEnforcesNonEmptyCompleteCoverageForTheDurableJobSlice() assertFalse(serializedPom.contains( "com.xtrmetl.etl.controller.EtlJobController*" )); + assertFalse(serializedPom.contains( + "com.xtrmetl.etl.service.Sha256Digest*" + )); } private static void assertConfiguredIncludes(Element execution) { From b8fea3f25554a28f837b998bfa427003fd3e3c80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:04:38 +0900 Subject: [PATCH 066/152] test(etl): cover empty durable batch validation --- .../EtlJobServiceCoverageCompletionTest.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java index 48d755c1..aad4a3b3 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java @@ -22,10 +22,11 @@ * Exercises realistic durable-intake validation boundaries that are easy to miss in ordinary * happy-path and persistence tests. * - *

The cases model malformed empty request bodies, visually ambiguous Unicode identifiers, - * identifiers that exceed the documented code-point bound, and separators that can alter how an - * identifier appears in logs or text tools. Every rejection must happen before transaction, lock, - * or database work so an invalid client request cannot consume shared persistence capacity.

+ *

The cases model malformed empty request bodies, intentionally empty batches, visually + * ambiguous Unicode identifiers, identifiers that exceed the documented code-point bound, and + * separators that can alter how an identifier appears in logs or text tools. Every rejection must + * happen before transaction, lock, or database work so an invalid client request cannot consume + * shared persistence capacity.

*/ class EtlJobServiceCoverageCompletionTest { @@ -53,6 +54,30 @@ void rejectsEmptyJsonStreamBeforeTransactionOrPersistence() { verifyNoInteractions(requestLock, jdbcTemplate); } + /** + * Verifies that a valid empty JSON batch completes record validation and reaches the transaction + * boundary without attempting a lock or database operation. Empty batches are distinct from + * empty request bodies: they are well-formed arrays containing zero records and therefore + * exercise the no-iteration path of whole-batch prevalidation. + */ + @Test + void acceptsEmptyBatchThroughValidationBeforeRequiringTransaction() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + EtlJobService service = service(jdbcTemplate, requestLock); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> service.submit("[]", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) + ); + + assertEquals( + "Durable ETL job submission requires an active transaction", + exception.getMessage() + ); + verifyNoInteractions(requestLock, jdbcTemplate); + } + /** * Verifies identifier rejection for Unicode boundary whitespace, format controls, line and * paragraph separators, and the documented 256-code-point maximum. From cae178c2b7ba86983edbb177f1513f3903b664bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:09:48 +0900 Subject: [PATCH 067/152] ci: expose exact JaCoCo branch diagnostics --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9780487..72a6de52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,60 @@ jobs: if: runner.os == 'Windows' run: .\\mvnw.cmd -B test + - name: Report uncovered JaCoCo branches + if: failure() + shell: bash + run: | + python_command="python3" + if ! command -v "${python_command}" >/dev/null 2>&1; then + python_command="python" + fi + "${python_command}" - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ElementTree + + class_name = "com/xtrmetl/etl/job/EtlJobService" + source_name = "EtlJobService.java" + reports = sorted(Path(".").glob("**/target/site/jacoco/jacoco.xml")) + if not reports: + raise SystemExit("No JaCoCo XML report was produced") + + found_class = False + for report_path in reports: + report_root = ElementTree.parse(report_path).getroot() + for class_element in report_root.findall(f".//class[@name='{class_name}']"): + found_class = True + print(f"JaCoCo branch diagnostics from {report_path}:") + for method_element in class_element.findall("method"): + for counter in method_element.findall("counter"): + if counter.get("type") != "BRANCH": + continue + missed = int(counter.get("missed", "0")) + if missed > 0: + print( + " method=" + f"{method_element.get('name')}{method_element.get('desc')} " + f"first_line={method_element.get('line')} " + f"missed_branches={missed} " + f"covered_branches={counter.get('covered', '0')}" + ) + + for source_element in report_root.findall( + f".//sourcefile[@name='{source_name}']" + ): + for line_element in source_element.findall("line"): + missed = int(line_element.get("mb", "0")) + if missed > 0: + print( + f" source_line={line_element.get('nr')} " + f"missed_branches={missed} " + f"covered_branches={line_element.get('cb', '0')}" + ) + + if not found_class: + raise SystemExit(f"{class_name} was not present in any JaCoCo XML report") + PY + test_self_hosted: if: ${{ github.event_name == 'workflow_dispatch' && inputs.use_self_hosted == true }} runs-on: self-hosted From bd2f0d0b80fa7942579f157e867c276bb5b19930 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:12:09 +0900 Subject: [PATCH 068/152] test(etl): cover parser end-of-input branch --- .../EtlJobServiceCoverageCompletionTest.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java index aad4a3b3..442199b0 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceCoverageCompletionTest.java @@ -17,16 +17,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * Exercises realistic durable-intake validation boundaries that are easy to miss in ordinary * happy-path and persistence tests. * - *

The cases model malformed empty request bodies, intentionally empty batches, visually - * ambiguous Unicode identifiers, identifiers that exceed the documented code-point bound, and - * separators that can alter how an identifier appears in logs or text tools. Every rejection must - * happen before transaction, lock, or database work so an invalid client request cannot consume - * shared persistence capacity.

+ *

The cases model a parser that reaches end-of-input without producing a tree, intentionally + * empty batches, visually ambiguous Unicode identifiers, identifiers that exceed the documented + * code-point bound, and separators that can alter how an identifier appears in logs or text tools. + * Every rejection must happen before transaction, lock, or database work so an invalid client + * request cannot consume shared persistence capacity.

*/ class EtlJobServiceCoverageCompletionTest { @@ -34,20 +35,30 @@ class EtlJobServiceCoverageCompletionTest { private static final String PRINCIPAL_SCOPE = "tenant_alpha"; /** - * Verifies that an empty JSON stream is rejected as malformed rather than reaching transaction - * or persistence work. Jackson represents this input with a {@code null} parse result instead - * of a JSON null node, so it is a distinct real-world boundary from the literal text - * {@code null}. + * Verifies the defensive parser-end-of-input boundary independently of Jackson version-specific + * empty-string behavior. A copied application mapper is allowed to return no tree, and the + * service must classify that result as invalid JSON before transaction, lock, or database work. + * + * @throws Exception when Mockito cannot configure the checked parser method */ @Test - void rejectsEmptyJsonStreamBeforeTransactionOrPersistence() { + void rejectsParserEndOfInputWithoutAJsonTreeBeforePersistence() throws Exception { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); EtlRequestLock requestLock = mock(EtlRequestLock.class); - EtlJobService service = service(jdbcTemplate, requestLock); + ObjectMapper sourceMapper = mock(ObjectMapper.class); + ObjectMapper copiedMapper = mock(ObjectMapper.class); + when(sourceMapper.copy()).thenReturn(copiedMapper); + when(copiedMapper.readTree("parser-end-of-input")).thenReturn(null); + EtlJobService service = new EtlJobService( + jdbcTemplate, + sourceMapper, + new EtlBatchProperties(), + requestLock + ); EtlRequestException exception = assertThrows( EtlRequestException.class, - () -> service.submit("", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) + () -> service.submit("parser-end-of-input", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) ); assertEquals(EtlRequestError.INVALID_JSON, exception.error()); From 1feb650fe960a358d6ed3a7d87f98914801b5ab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:22:26 +0900 Subject: [PATCH 069/152] test(ci): require default-branch-only maintenance source --- .../HourlyOpenCodeManualRefWorkflowTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeManualRefWorkflowTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeManualRefWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeManualRefWorkflowTest.java new file mode 100644 index 00000000..02a5e136 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeManualRefWorkflowTest.java @@ -0,0 +1,81 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Prevents the scheduled maintenance workflow from executing repository-controlled code from a + * manually selected feature branch or tag. + * + *

GitHub manual workflow dispatch can select a non-default ref. The maintenance workflow has + * repository write authority and receives the NVIDIA model credential, so its trusted workflow + * source must be schedule-only and its checkout must explicitly bind to the repository default + * branch. This test makes both authority boundaries visible to beginning maintainers.

+ */ +class HourlyOpenCodeManualRefWorkflowTest { + + private static String workflow; + + /** + * Reads the workflow with normalized line endings for deterministic cross-platform checks. + * + * @throws IOException when the workflow cannot be read as UTF-8 text + */ + @BeforeAll + static void readWorkflow() throws IOException { + Path workflowPath = projectRoot().resolve( + ".github/workflows/hourly-opencode-maintenance.yml" + ); + assertTrue(Files.exists(workflowPath), "The maintenance workflow must exist"); + workflow = Files.readString(workflowPath, StandardCharsets.UTF_8) + .replace("\r\n", "\n"); + } + + /** + * Requires schedule-only invocation and an explicit protected default-branch checkout. + */ + @Test + void rejectsManualFeatureRefsAndPinsTheDefaultBranchCheckout() { + assertFalse( + workflow.contains("workflow_dispatch:"), + "Manual dispatch must not allow a feature branch or tag to supply workflow code" + ); + assertTrue( + workflow.contains("ref: ${{ github.event.repository.default_branch }}"), + "Checkout must explicitly use the protected repository default branch" + ); + } + + /** + * Finds the repository root from either root or module-local Maven execution. + * + * @return absolute repository root containing the root Maven project + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From ed302008c98c71b861e8143c03b696ff0cb9fa8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:22:40 +0900 Subject: [PATCH 070/152] test(ci): require diagnostics for all coverage targets --- .../CiCoverageDiagnosticsWorkflowTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/CiCoverageDiagnosticsWorkflowTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/CiCoverageDiagnosticsWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/CiCoverageDiagnosticsWorkflowTest.java new file mode 100644 index 00000000..67148b3d --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/CiCoverageDiagnosticsWorkflowTest.java @@ -0,0 +1,82 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the failure diagnostics for every production class governed by the strict JaCoCo policy. + * + *

The coverage gate protects more than the durable-job service. When a class fails the 100% + * statement or branch threshold, the CI log must identify missed methods and source lines for + * every configured target rather than reporting only one historical class. This test keeps the + * diagnostic vocabulary synchronized with the authoritative Maven coverage configuration.

+ */ +class CiCoverageDiagnosticsWorkflowTest { + + private static String workflow; + + /** + * Reads the CI workflow with normalized line endings for deterministic platform behavior. + * + * @throws IOException when the workflow cannot be read as UTF-8 text + */ + @BeforeAll + static void readWorkflow() throws IOException { + Path workflowPath = projectRoot().resolve(".github/workflows/ci.yml"); + assertTrue(Files.exists(workflowPath), "The CI workflow must exist"); + workflow = Files.readString(workflowPath, StandardCharsets.UTF_8) + .replace("\r\n", "\n"); + } + + /** + * Requires one iterable diagnostic map for every zero-missed production coverage target. + */ + @Test + void diagnosesEveryStrictCoverageTarget() { + assertTrue(workflow.contains( + "\"com/xtrmetl/etl/job/EtlJobService\": \"EtlJobService.java\"" + )); + assertTrue(workflow.contains( + "\"com/xtrmetl/etl/controller/EtlJobController\": " + + "\"EtlJobController.java\"" + )); + assertTrue(workflow.contains( + "\"com/xtrmetl/etl/service/Sha256Digest\": \"Sha256Digest.java\"" + )); + assertTrue(workflow.contains( + "for class_name, source_name in coverage_targets.items():" + )); + } + + /** + * Finds the repository root from either root or module-local Maven execution. + * + * @return absolute repository root containing the root Maven project + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 82de9b37ca0bf49c0a095ec449429997a9912dd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:22:55 +0900 Subject: [PATCH 071/152] test(security): verify effective Jackson BOM resolution --- .../JacksonBomResolutionTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonBomResolutionTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonBomResolutionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonBomResolutionTest.java new file mode 100644 index 00000000..a14e25a1 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonBomResolutionTest.java @@ -0,0 +1,86 @@ +package com.xtrmetl.etl.documentation; + +import com.fasterxml.jackson.databind.cfg.PackageVersion; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the patched Jackson bill of materials is both declared with effective Maven + * precedence and actually resolved on the test runtime classpath. + * + *

A property in a project that imports Spring Boot's dependency BOM does not override the + * imported BOM's internal property interpolation. The patched Jackson BOM therefore has to be + * imported explicitly before Spring Boot's BOM. The runtime assertion catches configuration that + * looks correct in source but still resolves a vulnerable Jackson Databind version.

+ */ +class JacksonBomResolutionTest { + + private static final Pattern JACKSON_VERSION_PROPERTY = Pattern.compile( + "([^<]+)" + ); + + /** + * Requires explicit Jackson BOM precedence and exact runtime resolution to the configured + * patched component line. + * + * @throws Exception when the root Maven model cannot be read as UTF-8 text + */ + @Test + void importsAndResolvesTheConfiguredPatchedJacksonBom() throws Exception { + String rootPom = Files.readString( + projectRoot().resolve("pom.xml"), + StandardCharsets.UTF_8 + ); + Matcher versionMatcher = JACKSON_VERSION_PROPERTY.matcher(rootPom); + assertTrue(versionMatcher.find(), "The root POM must declare jackson-bom.version"); + String configuredVersion = versionMatcher.group(1).trim(); + + int jacksonBom = rootPom.indexOf("jackson-bom"); + int springBootBom = rootPom.indexOf( + "spring-boot-dependencies" + ); + assertTrue(jacksonBom >= 0, "The Jackson BOM must be imported explicitly"); + assertTrue( + jacksonBom < springBootBom, + "The Jackson BOM import must precede Spring Boot dependency management" + ); + assertEquals( + configuredVersion, + PackageVersion.VERSION.toString(), + "The resolved jackson-databind version must match jackson-bom.version" + ); + } + + /** + * Finds the repository root from either root or module-local Maven execution. + * + * @return absolute repository root containing the root Maven project + * @throws IllegalStateException when no repository or Maven root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 7a682cf94ce16ca0ee05e50c853a97f49ba1a8db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:23:55 +0900 Subject: [PATCH 072/152] fix(ci): bind maintenance workflow to default branch --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 688a0948..c587b83a 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -3,7 +3,6 @@ name: Hourly OpenCode maintenance on: schedule: - cron: "43 * * * *" - workflow_dispatch: concurrency: group: hourly-opencode-maintenance @@ -28,6 +27,7 @@ jobs: - name: Checkout protected default-branch source uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: + ref: ${{ github.event.repository.default_branch }} fetch-depth: 1 persist-credentials: false From 97b4449bb1e3ab7efbf84cd788cdf6ab34dbd531 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:24:21 +0900 Subject: [PATCH 073/152] fix(ci): diagnose every strict coverage target --- .github/workflows/ci.yml | 73 ++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72a6de52..e71e0b9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,46 +58,61 @@ jobs: from pathlib import Path import xml.etree.ElementTree as ElementTree - class_name = "com/xtrmetl/etl/job/EtlJobService" - source_name = "EtlJobService.java" + coverage_targets = { + "com/xtrmetl/etl/job/EtlJobService": "EtlJobService.java", + "com/xtrmetl/etl/controller/EtlJobController": "EtlJobController.java", + "com/xtrmetl/etl/service/Sha256Digest": "Sha256Digest.java", + } reports = sorted(Path(".").glob("**/target/site/jacoco/jacoco.xml")) if not reports: raise SystemExit("No JaCoCo XML report was produced") - found_class = False + found_classes = {class_name: False for class_name in coverage_targets} for report_path in reports: report_root = ElementTree.parse(report_path).getroot() - for class_element in report_root.findall(f".//class[@name='{class_name}']"): - found_class = True - print(f"JaCoCo branch diagnostics from {report_path}:") - for method_element in class_element.findall("method"): - for counter in method_element.findall("counter"): - if counter.get("type") != "BRANCH": - continue - missed = int(counter.get("missed", "0")) + for class_name, source_name in coverage_targets.items(): + for class_element in report_root.findall( + f".//class[@name='{class_name}']" + ): + found_classes[class_name] = True + print(f"JaCoCo branch diagnostics for {class_name} from {report_path}:") + for method_element in class_element.findall("method"): + for counter in method_element.findall("counter"): + if counter.get("type") != "BRANCH": + continue + missed = int(counter.get("missed", "0")) + if missed > 0: + print( + " method=" + f"{method_element.get('name')}{method_element.get('desc')} " + f"first_line={method_element.get('line')} " + f"missed_branches={missed} " + f"covered_branches={counter.get('covered', '0')}" + ) + + for source_element in report_root.findall( + f".//sourcefile[@name='{source_name}']" + ): + for line_element in source_element.findall("line"): + missed = int(line_element.get("mb", "0")) if missed > 0: print( - " method=" - f"{method_element.get('name')}{method_element.get('desc')} " - f"first_line={method_element.get('line')} " + f" source={source_name} " + f"source_line={line_element.get('nr')} " f"missed_branches={missed} " - f"covered_branches={counter.get('covered', '0')}" + f"covered_branches={line_element.get('cb', '0')}" ) - for source_element in report_root.findall( - f".//sourcefile[@name='{source_name}']" - ): - for line_element in source_element.findall("line"): - missed = int(line_element.get("mb", "0")) - if missed > 0: - print( - f" source_line={line_element.get('nr')} " - f"missed_branches={missed} " - f"covered_branches={line_element.get('cb', '0')}" - ) - - if not found_class: - raise SystemExit(f"{class_name} was not present in any JaCoCo XML report") + missing_classes = [ + class_name + for class_name, was_found in found_classes.items() + if not was_found + ] + if missing_classes: + raise SystemExit( + "Strict coverage targets absent from JaCoCo XML: " + + ", ".join(missing_classes) + ) PY test_self_hosted: From e081acdd26d60aabb0af7529d08acaf04a1fde30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:25:43 +0900 Subject: [PATCH 074/152] fix(security): import patched Jackson BOM explicitly --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 94bda043..66720d8f 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,13 @@ + + com.fasterxml.jackson + jackson-bom + ${jackson-bom.version} + pom + import + org.springframework.boot spring-boot-dependencies From 75eafeb3e96c6659410a0801a9b80188614537a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:33:09 +0900 Subject: [PATCH 075/152] test(security): verify Jackson BOM precedence --- .../JacksonSecurityVersionTest.java | 136 ++++++++++++++++-- 1 file changed, 123 insertions(+), 13 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java index 8d53a624..2f0fd23c 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityVersionTest.java @@ -1,8 +1,11 @@ package com.xtrmetl.etl.documentation; +import com.fasterxml.jackson.databind.cfg.PackageVersion; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; @@ -18,23 +21,106 @@ * Prevents the Maven dependency graph from returning to Jackson Databind versions affected by * the 2026 creator-property authorization-bypass advisories. * - *

Spring Boot exposes {@code jackson-bom.version} as the supported Maven override for the - * complete, mutually compatible Jackson component set. Pinning only {@code jackson-databind} - * could create an unsupported mix of core, annotations, datatype, and module artifacts, so this - * contract requires the patched FasterXML BOM instead.

+ *

The root project imports the patched FasterXML BOM before Spring Boot's dependency BOM so + * Maven's first-declaration precedence keeps the complete Jackson component set on one compatible + * security line. These tests verify the configured version, the explicit import and its order, and + * the Jackson Databind version that the test runtime actually resolved.

*/ class JacksonSecurityVersionTest { private static final String PATCHED_JACKSON_BOM_VERSION = "2.21.5"; /** - * Requires the root Maven project to override Spring Boot's Jackson BOM with the first 2.21 - * patch release that fixes CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w. + * Requires the root Maven project to declare the reviewed patched Jackson BOM version. * * @throws Exception when the root Maven model cannot be parsed securely */ @Test void usesPatchedCompatibleJacksonBom() throws Exception { + Document document = rootPomDocument(); + Element version = (Element) document.getElementsByTagName("jackson-bom.version").item(0); + assertNotNull( + version, + "The root POM must declare jackson-bom.version so every Jackson module is aligned" + ); + assertEquals(PATCHED_JACKSON_BOM_VERSION, version.getTextContent().trim()); + } + + /** + * Requires the explicit Jackson BOM import to precede Spring Boot's broader dependency BOM. + * + *

Maven uses the first declaration when imported dependency-management entries overlap. + * Therefore, merely declaring {@code jackson-bom.version} is insufficient unless the Jackson + * BOM is imported explicitly before Spring Boot's BOM.

+ * + * @throws Exception when the root Maven model cannot be parsed securely + */ + @Test + void importsJacksonBomBeforeSpringBootBom() throws Exception { + Document document = rootPomDocument(); + Element dependencyManagement = (Element) document + .getElementsByTagName("dependencyManagement") + .item(0); + assertNotNull(dependencyManagement, "The root POM must declare dependencyManagement"); + Element dependencies = directChild(dependencyManagement, "dependencies"); + assertNotNull(dependencies, "dependencyManagement must contain dependencies"); + + int jacksonBomIndex = -1; + int springBootBomIndex = -1; + Element jacksonBom = null; + int dependencyIndex = 0; + NodeList children = dependencies.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (!(child instanceof Element dependency) + || !"dependency".equals(dependency.getTagName())) { + continue; + } + String groupId = childText(dependency, "groupId"); + String artifactId = childText(dependency, "artifactId"); + if ("com.fasterxml.jackson".equals(groupId) + && "jackson-bom".equals(artifactId)) { + jacksonBomIndex = dependencyIndex; + jacksonBom = dependency; + } + if ("org.springframework.boot".equals(groupId) + && "spring-boot-dependencies".equals(artifactId)) { + springBootBomIndex = dependencyIndex; + } + dependencyIndex++; + } + + assertTrue(jacksonBomIndex >= 0, "The root POM must explicitly import jackson-bom"); + assertTrue(springBootBomIndex >= 0, "The root POM must import Spring Boot dependencies"); + assertTrue( + jacksonBomIndex < springBootBomIndex, + "jackson-bom must appear before Spring Boot's BOM to retain Maven precedence" + ); + assertNotNull(jacksonBom, "The located Jackson BOM dependency must be available"); + assertEquals("${jackson-bom.version}", childText(jacksonBom, "version")); + assertEquals("pom", childText(jacksonBom, "type")); + assertEquals("import", childText(jacksonBom, "scope")); + } + + /** + * Requires the resolved Jackson Databind artifact to match the reviewed BOM security line. + */ + @Test + void resolvesPatchedJacksonDatabindVersion() { + assertEquals( + PATCHED_JACKSON_BOM_VERSION, + PackageVersion.VERSION.toString(), + "The resolved jackson-databind version must match jackson-bom.version" + ); + } + + /** + * Parses the root Maven model with external entities and external schemas disabled. + * + * @return securely parsed root Maven document + * @throws Exception when the root Maven model cannot be parsed + */ + private static Document rootPomDocument() throws Exception { Path rootPom = projectRoot().resolve("pom.xml"); assertTrue(Files.exists(rootPom), "The root Maven POM must exist"); @@ -46,14 +132,38 @@ void usesPatchedCompatibleJacksonBom() throws Exception { factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); factory.setXIncludeAware(false); factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(rootPom.toFile()); + } - Document document = factory.newDocumentBuilder().parse(rootPom.toFile()); - Element version = (Element) document.getElementsByTagName("jackson-bom.version").item(0); - assertNotNull( - version, - "The root POM must declare jackson-bom.version so every Jackson module is aligned" - ); - assertEquals(PATCHED_JACKSON_BOM_VERSION, version.getTextContent().trim()); + /** + * Finds one direct child element by its tag name. + * + * @param parent parent element to inspect + * @param tagName required child tag name + * @return matching direct child, or {@code null} when absent + */ + private static Element directChild(Element parent, String tagName) { + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && tagName.equals(element.getTagName())) { + return element; + } + } + return null; + } + + /** + * Reads and trims one required direct child value. + * + * @param parent dependency element + * @param tagName child element name + * @return trimmed child text + */ + private static String childText(Element parent, String tagName) { + Element child = directChild(parent, tagName); + assertNotNull(child, "Expected child element " + tagName); + return child.getTextContent().trim(); } /** From e9f3a0354ab41361fb657521f29a2cff85a9a962 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:34:03 +0900 Subject: [PATCH 076/152] docs: clarify OpenCode secret process scope --- docs/operations/hourly-opencode-maintenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 291eeec9..6fb70b05 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -14,7 +14,7 @@ Create or retain exactly this GitHub Actions repository secret: NVIDIA_NIM_API_KEY ``` -The workflow exposes that value only to the OpenCode process as the provider variable documented by OpenCode: +The workflow step maps that value to the provider variable documented by OpenCode. Step-level environment variables are visible to the step's Bash shell and every child process it launches, including OpenCode: ```text NVIDIA_API_KEY From 3a46868ff89a44166dee91c30b7bfa322c7cb154 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:34:50 +0900 Subject: [PATCH 077/152] docs: align OpenCode extraction plan --- .../plans/2026-08-04-hourly-opencode-maintenance-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index f62ea430..e30fa275 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -93,7 +93,7 @@ Configure `43 * * * *`, manual dispatch, serialized concurrency, 50-minute job t - [x] **Step 2: Add immutable OpenCode installation** -Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract into a private mode-`0700` directory with ownership, archived permissions, and overwrites disabled, reject non-regular or symbolic-link output, and verify version `1.18.13`. +Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract into a private mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled, reject non-regular or symbolic-link output, and verify version `1.18.13`. - [x] **Step 3: Add direct-token Git bootstrap** From 5f8fcdf125510eb1ea80131e3f01a611b3759fe2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 20:35:28 +0900 Subject: [PATCH 078/152] docs: align OpenCode extraction design --- .../specs/2026-08-04-hourly-opencode-maintenance-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index 5e50dc54..3f37e30e 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -14,7 +14,7 @@ The maintenance workflow will: - pin `actions/checkout` by full SHA with persisted credentials disabled; - install OpenCode 1.18.13 from an immutable release asset verified by SHA-256; - accept exactly one regular archive member named `opencode` before extraction; -- extract into a fresh mode-`0700` directory with ownership, archived permissions, and overwrites disabled; +- extract into a fresh mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled; - reject non-regular or symbolic-link output and verify the exact executable version; - map only `${{ secrets.NVIDIA_NIM_API_KEY }}` to `NVIDIA_API_KEY`; - select `nvidia/deepseek-ai/deepseek-v4-pro`, a current NVIDIA free endpoint documented for coding, agentic tool use, function calling, and long-context software-engineering work; From 91d86fa6f945cbd5b999543d7cbe60c3278f7314 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:45:52 +0900 Subject: [PATCH 079/152] test(ci): require exact-head checks after agent pushes --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 227435e8..678697de 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -18,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Guards the credential, authority, and supply-chain boundaries of the scheduled OpenCode - * maintenance workflow. + * Guards the credential, authority, supply-chain, and exact-head validation boundaries of the + * scheduled OpenCode maintenance workflow. * - *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a - * reviewer or merger. These tests make that separation visible to beginners and prevent a - * later workflow edit from silently adding a fallback provider, mutable tool version, elevated - * token permission, protected-branch push, self-approval path, or deprecated hosted model.

+ *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a reviewer + * or merger. These tests make that separation visible to beginners and prevent a later workflow + * edit from silently adding a fallback provider, mutable tool version, protected-branch push, + * self-approval path, workflow-code auto-approval, or unvalidated agent-generated head.

*/ class HourlyOpenCodeMaintenanceWorkflowTest { @@ -156,11 +156,9 @@ void usesOnlyTheNvidiaNimCredential() { /** * Requires a currently available free NVIDIA endpoint suited to repository-scale coding. * - *

The previous Qwen3 Coder trial endpoint was deprecated by NVIDIA. A scheduled agent - * that pins a deprecated hosted endpoint is not durable automation even when its workflow - * and credential handling are otherwise correct. DeepSeek V4 Pro is explicitly exposed by - * NVIDIA as a free endpoint with one-million-token context, agentic tool use, and software- - * engineering support, so the workflow pins that provider/model identifier.

+ *

The previous Qwen3 Coder trial endpoint was deprecated by NVIDIA. DeepSeek V4 Pro is + * exposed by NVIDIA as a free endpoint with long-context coding and tool-use capabilities, so + * the workflow pins that provider/model identifier and fails instead of silently falling back.

*/ @Test void usesCurrentFreeAgenticCodingModel() { @@ -170,15 +168,16 @@ void usesCurrentFreeAgenticCodingModel() { /** * Verifies least-privilege repository access, scopes write authority to the sole maintenance - * job instead of every future job, and rejects an unnecessary OIDC token path. + * job, and permits only the Actions write capability needed to authorize exact-head CI that + * GitHub deliberately places in approval-required state after a {@code GITHUB_TOKEN} PR update. */ @Test - void grantsOnlyRepositoryMaintenancePermissions() { + void grantsOnlyRepositoryMaintenanceAndCheckRevalidationPermissions() { assertTrue(workflow.contains("permissions:\n contents: read\n\njobs:")); assertTrue(workflow.contains( "maintain-repository:\n" + " permissions:\n" - + " actions: read\n" + + " actions: write\n" + " checks: read\n" + " contents: write\n" + " issues: write\n" @@ -187,10 +186,41 @@ void grantsOnlyRepositoryMaintenancePermissions() { + " statuses: read" )); assertFalse(workflow.contains("id-token:")); - assertFalse(workflow.contains("actions: write")); assertFalse(workflow.contains("security-events: write")); } + /** + * Requires exact-head CI authorization after the agent creates or updates a same-repository PR. + * + *

GitHub prevents ordinary events produced with {@code GITHUB_TOKEN} from recursively + * starting workflows. Current GitHub behavior creates {@code opened}, {@code synchronize}, and + * {@code reopened} PR runs in an approval-required state instead. The trusted default-branch + * scheduler must snapshot heads before the agent, detect only heads changed by this run, refuse + * workflow or CODEOWNERS changes, bind every decision to the still-current SHA, and authorize + * only those exact-head runs. This step starts validation; it does not approve or merge the PR.

+ */ + @Test + void authorizesExactHeadChecksForAgentChangedPullRequests() { + assertTrue(workflow.contains("name: Snapshot open pull-request heads")); + assertTrue(workflow.contains("open-pr-heads-before.json")); + assertTrue(workflow.contains( + "name: Authorize exact-head checks for agent-updated pull requests" + )); + assertTrue(workflow.contains("if: ${{ always() && !cancelled() }}")); + assertTrue(workflow.contains("head.repo.full_name == $repo")); + assertTrue(workflow.contains(".base.ref == \"develop\"")); + assertTrue(workflow.contains(".github/workflows/")); + assertTrue(workflow.contains("CODEOWNERS")); + assertTrue(workflow.contains("head_sha=${head_sha}")); + assertTrue(workflow.contains("event=pull_request")); + assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); + assertTrue(workflow.contains("current_head")); + assertTrue(workflow.contains("expected_head")); + assertTrue(workflow.contains("No pull-request workflow run materialized")); + assertFalse(workflow.contains("gh pr review --approve")); + assertFalse(workflow.contains("/pulls/${number}/merge")); + } + /** * Verifies that the model prompt preserves independent review and deterministic merge * authority instead of granting the development agent governance powers. From 1d3598865b01cce4ba73891d8272172872ad81cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:47:50 +0900 Subject: [PATCH 080/152] fix(ci): revalidate exact heads written by OpenCode --- .../workflows/hourly-opencode-maintenance.yml | 129 +++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index c587b83a..9780e617 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -14,7 +14,7 @@ permissions: jobs: maintain-repository: permissions: - actions: read + actions: write checks: read contents: write issues: write @@ -76,6 +76,22 @@ jobs: test "$("${install_dir}/opencode" --version)" = "${OPENCODE_VERSION}" printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" + - name: Snapshot open pull-request heads + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + snapshot="${RUNNER_TEMP}/open-pr-heads-before.json" + gh api --paginate \ + "/repos/${GITHUB_REPOSITORY}/pulls?state=open&base=develop&per_page=100" \ + | jq -s --arg repo "${GITHUB_REPOSITORY}" ' + (add // []) + | map(select(.head.repo.full_name == $repo)) + | map({key: (.number | tostring), value: .head.sha}) + | from_entries + ' > "${snapshot}" + - name: Run bounded NVIDIA OpenCode maintenance shell: bash env: @@ -127,3 +143,114 @@ jobs: git config --local user.email "opencode-agent[bot]@users.noreply.github.com" timeout --signal=TERM --kill-after=30s 45m opencode github run + + - name: Authorize exact-head checks for agent-updated pull requests + if: ${{ always() && !cancelled() }} + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + before_file="${RUNNER_TEMP}/open-pr-heads-before.json" + after_file="${RUNNER_TEMP}/open-pr-heads-after.json" + changed_file="${RUNNER_TEMP}/agent-changed-pr-heads.jsonl" + repository="${GITHUB_REPOSITORY}" + failed=0 + + if [[ ! -f "${before_file}" ]]; then + echo "Open pull-request head snapshot is unavailable" >&2 + exit 1 + fi + + gh api --paginate \ + "/repos/${repository}/pulls?state=open&base=develop&per_page=100" \ + | jq -s '(add // [])' > "${after_file}" + jq -c --arg repo "${repository}" --slurpfile before "${before_file}" ' + .[] + | select(.head.repo.full_name == $repo) + | select(.base.ref == "develop") + | (.number | tostring) as $number_key + | ($before[0][$number_key] // null) as $previous_head + | select($previous_head != .head.sha) + | { + number: .number, + head_ref: .head.ref, + head_sha: .head.sha + } + ' "${after_file}" > "${changed_file}" + + while IFS= read -r changed_pull_request; do + [[ -n "${changed_pull_request}" ]] || continue + number="$(jq -r '.number' <<<"${changed_pull_request}")" + head_ref="$(jq -r '.head_ref' <<<"${changed_pull_request}")" + expected_head="$(jq -r '.head_sha' <<<"${changed_pull_request}")" + + changed_files="$( + gh api --paginate "/repos/${repository}/pulls/${number}/files?per_page=100" \ + | jq -s '(add // []) | map(.filename)' + )" + if jq -e ' + any( + startswith(".github/workflows/") + or . == "CODEOWNERS" + or endswith("/CODEOWNERS") + ) + ' >/dev/null <<<"${changed_files}"; then + echo "PR #${number} changes workflow or CODEOWNERS policy; exact-head runs require manual authorization" >&2 + failed=1 + continue + fi + + current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" + if [[ "${current_head}" != "${expected_head}" ]]; then + echo "PR #${number} moved before workflow-run discovery" >&2 + failed=1 + continue + fi + + runs_file="${RUNNER_TEMP}/pr-${number}-exact-head-runs.json" + run_count=0 + for _ in $(seq 1 18); do + gh api --paginate \ + "/repos/${repository}/actions/runs?event=pull_request&head_sha=${expected_head}&per_page=100" \ + | jq -s '(map(.workflow_runs) | add) // []' > "${runs_file}" + run_count="$(jq 'length' "${runs_file}")" + if [[ "${run_count}" -gt 0 ]]; then + break + fi + sleep 5 + done + + if [[ "${run_count}" -eq 0 ]]; then + echo "No pull-request workflow run materialized for PR #${number} exact head ${expected_head}" >&2 + failed=1 + continue + fi + + current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" + if [[ "${current_head}" != "${expected_head}" ]]; then + echo "PR #${number} moved before workflow-run authorization" >&2 + failed=1 + continue + fi + + while IFS= read -r run_id; do + [[ -n "${run_id}" ]] || continue + gh api --method POST \ + "/repos/${repository}/actions/runs/${run_id}/approve" + done < <( + jq -r '.[] + | select( + .head_sha == "'"${expected_head}"'" + and ( + .conclusion == "action_required" + or .status == "waiting" + ) + ) + | .id' "${runs_file}" + ) + + echo "Authorized exact-head pull-request checks for PR #${number} branch ${head_ref} at ${expected_head}" + done < "${changed_file}" + + exit "${failed}" From 9b63536dac28566a26f85c022270310d2dd9a4e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:51:30 +0900 Subject: [PATCH 081/152] test(ci): harden exact-head run authorization --- .../HourlyOpenCodeMaintenanceWorkflowTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 678697de..bf7035e4 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -24,7 +24,7 @@ *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a reviewer * or merger. These tests make that separation visible to beginners and prevent a later workflow * edit from silently adding a fallback provider, mutable tool version, protected-branch push, - * self-approval path, workflow-code auto-approval, or unvalidated agent-generated head.

+ * self-approval path, policy-code auto-authorization, or unvalidated agent-generated head.

*/ class HourlyOpenCodeMaintenanceWorkflowTest { @@ -196,8 +196,9 @@ void grantsOnlyRepositoryMaintenanceAndCheckRevalidationPermissions() { * starting workflows. Current GitHub behavior creates {@code opened}, {@code synchronize}, and * {@code reopened} PR runs in an approval-required state instead. The trusted default-branch * scheduler must snapshot heads before the agent, detect only heads changed by this run, refuse - * workflow or CODEOWNERS changes, bind every decision to the still-current SHA, and authorize - * only those exact-head runs. This step starts validation; it does not approve or merge the PR.

+ * every {@code .github} or CODEOWNERS policy change, bind every decision to the still-current + * SHA, and authorize only those exact-head runs. This step starts validation; it does not + * approve or merge the pull request.

*/ @Test void authorizesExactHeadChecksForAgentChangedPullRequests() { @@ -209,10 +210,12 @@ void authorizesExactHeadChecksForAgentChangedPullRequests() { assertTrue(workflow.contains("if: ${{ always() && !cancelled() }}")); assertTrue(workflow.contains("head.repo.full_name == $repo")); assertTrue(workflow.contains(".base.ref == \"develop\"")); - assertTrue(workflow.contains(".github/workflows/")); + assertTrue(workflow.contains("startswith(\".github/\")")); assertTrue(workflow.contains("CODEOWNERS")); - assertTrue(workflow.contains("head_sha=${head_sha}")); + assertTrue(workflow.contains("head_sha=${expected_head}")); assertTrue(workflow.contains("event=pull_request")); + assertTrue(workflow.contains("--arg expected_head \"${expected_head}\"")); + assertTrue(workflow.contains(".head_sha == $expected_head")); assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); assertTrue(workflow.contains("current_head")); assertTrue(workflow.contains("expected_head")); From b4ec3ba0d0e79a49eb1df6534544a3c139c20e7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:53:13 +0900 Subject: [PATCH 082/152] fix(ci): fail closed on agent policy changes --- .github/workflows/hourly-opencode-maintenance.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 9780e617..4aff458d 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -191,12 +191,12 @@ jobs: )" if jq -e ' any( - startswith(".github/workflows/") + startswith(".github/") or . == "CODEOWNERS" or endswith("/CODEOWNERS") ) ' >/dev/null <<<"${changed_files}"; then - echo "PR #${number} changes workflow or CODEOWNERS policy; exact-head runs require manual authorization" >&2 + echo "PR #${number} changes .github or CODEOWNERS policy; exact-head runs require manual authorization" >&2 failed=1 continue fi @@ -239,15 +239,17 @@ jobs: gh api --method POST \ "/repos/${repository}/actions/runs/${run_id}/approve" done < <( - jq -r '.[] + jq -r --arg expected_head "${expected_head}" ' + .[] | select( - .head_sha == "'"${expected_head}"'" + .head_sha == $expected_head and ( .conclusion == "action_required" or .status == "waiting" ) ) - | .id' "${runs_file}" + | .id + ' "${runs_file}" ) echo "Authorized exact-head pull-request checks for PR #${number} branch ${head_ref} at ${expected_head}" From ee6c5ebf4140fe613dcb101c578fe39928c5e143 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:54:12 +0900 Subject: [PATCH 083/152] docs(ci): record exact-head check authorization evidence --- ...exact-head-check-authorization-evidence.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/doctoring/github-token-exact-head-check-authorization-evidence.md diff --git a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md new file mode 100644 index 00000000..748ccc1c --- /dev/null +++ b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md @@ -0,0 +1,112 @@ +# GitHub-token exact-head check authorization evidence + +## Decision + +The hourly development workflow uses the repository-scoped `GITHUB_TOKEN` rather than a personal +access token or GitHub App installation token. When OpenCode creates or updates a same-repository +pull request with that token, mightyETL must explicitly authorize the resulting approval-required +pull-request workflow runs before treating the agent's work as ready for review. + +Authorization is limited to starting validation for the exact current head. It is not pull-request +approval, review, merge authority, branch-protection bypass, or evidence that any check succeeded. + +## Platform behavior + +GitHub prevents most events created with `GITHUB_TOKEN` from recursively starting another workflow. +For pull requests opened, synchronized, reopened, or updated through GitHub Actions, GitHub creates +pull-request workflow runs in an approval-required state rather than granting an automated writer an +unreviewed recursive execution path. The workflow-run approval endpoint requires Actions write +permission. + +Without a bounded authorization step, a scheduled OpenCode run could push a valid fix while leaving +the new exact head without CI, SAST, SBOM, dependency, or security execution. That would break the +required review → fix → exact-head revalidation loop even though the source change itself was valid. + +## Fail-closed authorization algorithm + +The protected default-branch workflow performs the following steps: + +1. Before OpenCode starts, snapshot the exact heads of all same-repository pull requests targeting + `develop`. +2. After OpenCode exits, enumerate the same pull-request set again. +3. Select only a new pull request or a pull request whose head changed during this run. +4. Refuse automatic run authorization when the pull request changes any path below `.github/` or + any `CODEOWNERS` file. Those policy changes require explicit human authorization. +5. Read the still-current pull-request head and require it to equal the selected expected SHA. +6. Discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. +7. Fail visibly when no exact-head run materializes. +8. Re-read the pull-request head immediately before authorization and reject a moved head. +9. Approve only exact-head runs in `action_required` or `waiting` state. +10. Leave check execution, review, mergeability, branch protection, and expected-head merge + disposition to their existing independent gates. + +The post-agent step runs even when OpenCode fails, provided the GitHub job itself was not cancelled. +This covers a partial agent session that pushed a branch before later failing. If the pre-run snapshot +is absent, run discovery fails, the head moves, a policy file changed, no run appears, or GitHub +rejects authorization, the maintenance job fails instead of reporting successful revalidation. + +## Authority and credential boundary + +The workflow-level permission remains `contents: read`. Only the maintenance job receives +`actions: write`, alongside the existing minimum branch, pull-request, issue, check, status, and +security-read permissions. No personal token, GitHub App token, OIDC token, or additional repository +secret is introduced. + +`actions: write` is used solely for the workflow-run approval endpoint. The implementation contains +no pull-request review approval command and no merge API call. The OpenCode prompt continues to +forbid approval, merge, protected-branch push, branch-protection bypass, review-agent modification, +and unauthorized workflow-policy changes. + +## Test-first evidence + +`HourlyOpenCodeMaintenanceWorkflowTest` first required the following contracts before production +implemented them: + +- a before/after exact-head snapshot; +- job-scoped Actions write permission; +- same-repository and `develop` targeting; +- refusal of `.github/**` and `CODEOWNERS` changes; +- exact-head workflow-run discovery; +- two head-SHA time-of-check/time-of-use validations; +- explicit failure when no run materializes; +- authorization through the workflow-run endpoint only; +- continued absence of pull-request approval and merge operations. + +The production workflow then implemented those contracts with `gh api`, canonical JSON processing +through `jq`, and an exact SHA passed as a jq argument rather than interpolated into jq source. + +## Verification checklist + +Reviewers must verify on the exact current pull-request head that: + +- the scheduler still runs only from protected default-branch workflow source; +- the snapshot precedes the OpenCode process; +- only heads changed by that run are considered; +- `.github/**` and all `CODEOWNERS` paths are excluded from automatic authorization; +- both current-head reads equal the expected head; +- run discovery filters `event=pull_request` and the exact head SHA; +- only waiting or action-required runs reach the approval endpoint; +- absent runs and authorization failures make the job fail; +- no review approval, merge, protected-branch push, or secret fallback was added; +- every authorized run must still complete successfully before merge disposition can proceed. + +## Rollback + +If GitHub changes the approval-required run model or the endpoint becomes unavailable, disable the +hourly development workflow. Do not remove the exact-head authorization contract while leaving the +agent able to push changes with `GITHUB_TOKEN`, because that recreates unvalidated agent heads. + +A replacement based on a GitHub App may remove this step only after its installation permissions, +recursive-trigger behavior, actor identity, secret lifecycle, exact-head workflow evidence, and +independent review boundary are documented and tested through a separate pull request. + +## References — APA 7th + +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. +https://docs.github.com/en/actions/concepts/security/github_token + +GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. +https://docs.github.com/en/actions/using-workflows/triggering-a-workflow + +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. +https://docs.github.com/en/rest/actions/workflow-runs From 80dd0d0d1e1d2fc32b403748cdb495ec85d8ff8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:56:37 +0900 Subject: [PATCH 084/152] docs(ci): document agent head revalidation --- .../operations/hourly-opencode-maintenance.md | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 6fb70b05..6c5b7aed 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -2,10 +2,12 @@ ## Purpose -`.github/workflows/hourly-opencode-maintenance.yml` runs a bounded development agent at minute 43 of every hour, in UTC, and on manual dispatch. The agent uses OpenCode 1.18.13 with NVIDIA NIM to inspect the repository, repair one existing development pull request, or prepare one bounded buyer-visible improvement when no development pull request is open. +`.github/workflows/hourly-opencode-maintenance.yml` runs a bounded development agent at minute 43 of every hour, in UTC. The agent uses OpenCode 1.18.13 with NVIDIA NIM to inspect the repository, repair one existing development pull request, or prepare one bounded buyer-visible improvement when no development pull request is open. This workflow is intentionally separate from code review and merge disposition. It does not replace independent review, GitHub branch protection, required checks, GitHub Advanced Security, Dependabot, CodeRabbit, or `.github/workflows/hourly-pr-disposition.yml`. +Because the agent writes branches and pull requests with the repository-scoped `GITHUB_TOKEN`, the workflow also owns one narrowly scoped validation duty: after an agent-created or agent-updated same-repository pull request appears, it authorizes only the approval-required GitHub Actions runs for that exact current head. This authorization starts CI and security evaluation; it is not pull-request approval, merge authority, or proof that any check passed. + ## Required repository secret Create or retain exactly this GitHub Actions repository secret: @@ -74,21 +76,40 @@ Before starting OpenCode, the workflow therefore: No encoded or plaintext token is written to Git configuration. The local author identity contains no credential. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No personal token, OIDC path, fallback model credential, or tracked credential file is introduced. +## Exact-head check authorization + +GitHub prevents most ordinary events created with `GITHUB_TOKEN` from recursively starting another workflow. For pull requests created or updated from GitHub Actions, pull-request workflow runs can be created in an approval-required state. Leaving those runs unapproved would let the agent push a valid fix while the new head never receives CI, dependency, SBOM, SAST, or security evaluation. + +The workflow therefore snapshots all same-repository `develop` pull-request heads immediately before OpenCode starts. After the agent exits, including an agent failure that occurred after a push, it: + +1. enumerates the same pull-request set; +2. selects only a new head or a head changed during this run; +3. refuses automatic run authorization if the pull request changes any `.github/**` path or any `CODEOWNERS` file; +4. verifies the pull request still points to the expected exact head; +5. discovers only `pull_request` workflow runs for that SHA; +6. fails if no exact-head run materializes; +7. verifies the head a second time immediately before authorization; +8. authorizes only runs in `action_required` or `waiting` state through the workflow-run approval endpoint. + +The implementation passes the expected SHA to `jq` as data rather than interpolating it into jq source. This avoids treating an identifier as executable filter text. The step does not approve a pull request, merge a branch, relax a check, or claim successful validation. Existing review and disposition workflows remain the only review and merge authorities. + +Detailed test-first and rollback evidence is in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. + ## Repository permissions The workflow-level default is only `contents: read`. Write authority is scoped to the sole `maintain-repository` job so a future job cannot inherit repository write access accidentally. That job receives only these explicit `GITHUB_TOKEN` permissions: -- `actions: read` -- `checks: read` -- `contents: write` -- `issues: write` -- `pull-requests: write` -- `security-events: read` -- `statuses: read` +- `actions: write`, solely to authorize approval-required workflow runs for a verified exact head; +- `checks: read`; +- `contents: write`; +- `issues: write`; +- `pull-requests: write`; +- `security-events: read`; +- `statuses: read`. -There is no `id-token` permission and no permission to write Actions or security events. `contents: write` is required to prepare a feature branch; repository branch protection remains authoritative for protected branches. +There is no `id-token` permission and no permission to write security events. `contents: write` is required to prepare a feature branch; repository branch protection remains authoritative for protected branches. Actions write permission does not grant pull-request approval or merge authority, and the workflow contains no operation that performs either action. ## Authority boundaries @@ -106,22 +127,26 @@ The agent must not: - create a second development pull request while another development pull request is open; - publish a release unless a separate release-authorized workflow and all release acceptance gates permit it. +Even when an issue authorizes an automation-policy change, the post-agent step never auto-authorizes a pull request that modifies `.github/**` or `CODEOWNERS`. A human must review and authorize those runs. + `.github/workflows/hourly-pr-disposition.yml` remains the deterministic exact-head merge boundary. It independently evaluates review state, unresolved threads, named checks, status contexts, labels, mergeability, and expected head SHA. ## Normal run sequence -1. GitHub starts the workflow from the default branch. +1. GitHub starts the scheduled workflow from the protected default branch. 2. The workflow checks out a shallow copy with persisted credentials disabled. 3. It downloads the OpenCode 1.18.13 Linux x64 archive and verifies the pinned SHA-256. 4. It requires exactly one archive member named `opencode` and confirms through locale-stable verbose metadata that the entry is a regular file before extraction. 5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable with the exact version. -6. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. -7. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. -8. OpenCode calls the explicit `nvidia/deepseek-ai/deepseek-v4-pro` endpoint and inspects all current pull requests before selecting work. -9. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves one feature branch and pull request. -10. The shell `EXIT` trap removes the local Git credential helper. -11. Independent review and repository checks evaluate the exact new head. -12. The separate disposition workflow may merge only after every gate passes. +6. It snapshots current same-repository `develop` pull-request heads. +7. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. +8. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. +9. OpenCode calls the explicit `nvidia/deepseek-ai/deepseek-v4-pro` endpoint and inspects all current pull requests before selecting work. +10. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves one feature branch and pull request. +11. The shell `EXIT` trap removes the local Git credential helper. +12. The post-agent step detects heads changed by this run, refuses policy-file changes, binds decisions to the still-current SHA, and authorizes only approval-required exact-head workflow runs. +13. Independent review and repository checks evaluate the exact new head. +14. The separate disposition workflow may merge only after every gate passes. ## Failure handling @@ -139,6 +164,11 @@ The agent must not: | NVIDIA endpoint is unavailable, deprecated, or rejects the model | OpenCode step fails without fallback | Confirm current NVIDIA catalog status; prepare a test-first reviewed model-selection change or disable the scheduler | | Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect incomplete branch or PR state; reduce slice size if needed | | GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate shutdown behavior and verify the ephemeral runner was destroyed | +| Pre-run head snapshot is absent | Post-agent authorization fails closed | Investigate the GitHub API and snapshot step; never authorize from an unknown baseline | +| Agent changes `.github/**` or `CODEOWNERS` | Exact-head run authorization is refused and the job fails | Perform explicit human review and workflow-run authorization for that policy change | +| Pull-request head moves during authorization | Authorization is refused and the job fails | Re-evaluate the new exact head; never reuse the prior decision | +| No exact-head workflow run materializes | Job fails rather than claiming revalidation | Inspect GitHub event and Actions policy; do not merge the unvalidated head | +| Workflow-run approval is rejected | Job fails | Verify Actions permission and repository policy; do not add a personal token as an unreviewed workaround | | Tests or security checks fail | Pull request remains unmergeable | Fix the exact current head; never weaken the gate | | Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | | Another hourly run starts while one is active | New run waits because concurrency is serialized | No action unless the prior run is stuck | @@ -147,10 +177,12 @@ The workflow must not claim success for partial work. A failed run may leave a f ## Rollback and disablement -This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, both doctoring evidence notes, and the corresponding `CHANGELOG.md` entries through a reviewed pull request. +This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, all related doctoring evidence notes, and the corresponding `CHANGELOG.md` entries through a reviewed pull request. Do not delete or rename `NVIDIA_NIM_API_KEY` when it is also used by other approved workflows. Disabling this workflow does not require changing the review agent or its credential scheme. +Do not remove exact-head run authorization while retaining `GITHUB_TOKEN` branch writes. A replacement GitHub App design must first prove its permissions, actor identity, recursive-trigger behavior, secret lifecycle, exact-head evidence, and independent review boundary. + ## Verification checklist Before merging a workflow change, verify the exact current head has: @@ -167,10 +199,15 @@ Before merging a workflow change, verify the exact current head has: - exactly one expected archive member and a pre-extraction regular-file entry-type check under `LC_ALL=C`; - a fresh mode-`0700` extraction directory, overwrite refusal, and regular non-symbolic-link executable validation; - no npm install command, floating package tag, or mutable OpenCode action reference; -- workflow-level read-only permission plus explicit job-scoped write permissions; +- workflow-level read-only permission plus explicit job-scoped `actions: write` and repository-maintenance permissions; - `persist-credentials: false` plus the local GitHub CLI credential helper and `EXIT` cleanup; - bounded `TERM` timeout with deterministic `KILL` escalation; - no ineffective `AGENT` environment claim for raw OpenCode 1.18.13; +- a pre-agent head snapshot and post-agent changed-head selection; +- refusal of automatic run authorization for `.github/**` and `CODEOWNERS` changes; +- exact-head workflow-run discovery plus head revalidation immediately before authorization; +- visible failure when no exact-head run materializes or authorization is rejected; +- no pull-request approval or merge operation in the development workflow; - no review-agent credential or workflow change. ## References @@ -191,14 +228,20 @@ Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GN GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token + GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 -GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions +GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/triggering-a-workflow + +GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro From 9852841174dcaa1958f2ceb029d73b8adebbbc58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 21:58:54 +0900 Subject: [PATCH 085/152] docs(changelog): record exact-head check authorization --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bba2d21..94c53829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. +- The hourly OpenCode workflow now snapshots same-repository `develop` pull-request heads before the agent runs and uses job-scoped Actions write authority only to authorize approval-required workflow runs for an unchanged exact head; `.github/**` and `CODEOWNERS` changes remain human-authorized, and no review or merge authority is added. - The hourly OpenCode workflow now uses the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint for long-context coding and agentic tool use instead of the deprecated Qwen3 Coder free endpoint; model or endpoint rejection fails visibly without a non-NVIDIA, partner-only, or automatic fallback. - The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. +- Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. - Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. - NVIDIA model-selection doctoring evidence for endpoint availability, deprecated-endpoint rejection, capability and context evidence, no-fallback semantics, test-first regression evidence, and replacement procedure in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. From a8d1f6a6b9a3dbb1831a2db55857982755a23f92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:02:19 +0900 Subject: [PATCH 086/152] test(ci): isolate Actions write from the agent --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index bf7035e4..903f8b37 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -167,17 +167,19 @@ void usesCurrentFreeAgenticCodingModel() { } /** - * Verifies least-privilege repository access, scopes write authority to the sole maintenance - * job, and permits only the Actions write capability needed to authorize exact-head CI that - * GitHub deliberately places in approval-required state after a {@code GITHUB_TOKEN} PR update. + * Verifies that the OpenCode process never receives Actions write authority. + * + *

The maintenance job owns only branch and pull-request preparation permissions. A separate + * job, which never checks out or executes repository code, receives the sole occurrence of + * {@code actions: write} needed to authorize approval-required exact-head workflow runs.

*/ @Test - void grantsOnlyRepositoryMaintenanceAndCheckRevalidationPermissions() { + void isolatesActionsWriteFromTheAgentProcess() { assertTrue(workflow.contains("permissions:\n contents: read\n\njobs:")); assertTrue(workflow.contains( "maintain-repository:\n" + " permissions:\n" - + " actions: write\n" + + " actions: read\n" + " checks: read\n" + " contents: write\n" + " issues: write\n" @@ -185,6 +187,19 @@ void grantsOnlyRepositoryMaintenanceAndCheckRevalidationPermissions() { + " security-events: read\n" + " statuses: read" )); + assertTrue(workflow.contains( + "authorize-exact-head-checks:\n" + + " needs: maintain-repository\n" + + " if: ${{ always() && !cancelled() }}\n" + + " permissions:\n" + + " actions: write\n" + + " contents: read\n" + + " pull-requests: read" + )); + assertEquals(1, countOccurrences(workflow, "actions: write")); + assertEquals(1, countOccurrences(workflow, "contents: write")); + assertFalse(authorizationJob().contains("actions/checkout@")); + assertFalse(authorizationJob().contains("NVIDIA_API_KEY")); assertFalse(workflow.contains("id-token:")); assertFalse(workflow.contains("security-events: write")); } @@ -195,19 +210,20 @@ void grantsOnlyRepositoryMaintenanceAndCheckRevalidationPermissions() { *

GitHub prevents ordinary events produced with {@code GITHUB_TOKEN} from recursively * starting workflows. Current GitHub behavior creates {@code opened}, {@code synchronize}, and * {@code reopened} PR runs in an approval-required state instead. The trusted default-branch - * scheduler must snapshot heads before the agent, detect only heads changed by this run, refuse + * workflow must snapshot heads before the agent, detect only heads changed by this run, refuse * every {@code .github} or CODEOWNERS policy change, bind every decision to the still-current - * SHA, and authorize only those exact-head runs. This step starts validation; it does not - * approve or merge the pull request.

+ * SHA, and authorize only those exact-head runs in a separate non-checkout job. This starts + * validation; it does not approve or merge the pull request.

*/ @Test void authorizesExactHeadChecksForAgentChangedPullRequests() { - assertTrue(workflow.contains("name: Snapshot open pull-request heads")); + assertTrue(workflow.contains("id: snapshot_heads")); + assertTrue(workflow.contains("open_pr_heads_before: ${{ steps.snapshot_heads.outputs.open_pr_heads }}")); assertTrue(workflow.contains("open-pr-heads-before.json")); + assertTrue(workflow.contains("BEFORE_HEADS: ${{ needs.maintain-repository.outputs.open_pr_heads_before }}")); assertTrue(workflow.contains( "name: Authorize exact-head checks for agent-updated pull requests" )); - assertTrue(workflow.contains("if: ${{ always() && !cancelled() }}")); assertTrue(workflow.contains("head.repo.full_name == $repo")); assertTrue(workflow.contains(".base.ref == \"develop\"")); assertTrue(workflow.contains("startswith(\".github/\")")); @@ -243,6 +259,34 @@ void promptForbidsReviewMergeAndProtectedBranchBypass() { assertTrue(workflow.contains("Do not print, echo, summarize, or expose secret values")); } + /** + * Returns the text of the isolated workflow-run authorization job. + * + * @return workflow suffix beginning at the authorization job + */ + private static String authorizationJob() { + int jobStart = workflow.indexOf(" authorize-exact-head-checks:"); + assertTrue(jobStart >= 0, "The isolated exact-head authorization job must exist"); + return workflow.substring(jobStart); + } + + /** + * Counts non-overlapping occurrences of one literal fragment. + * + * @param text complete text to inspect + * @param fragment non-empty literal fragment + * @return number of non-overlapping occurrences + */ + private static int countOccurrences(String text, String fragment) { + int count = 0; + int cursor = 0; + while ((cursor = text.indexOf(fragment, cursor)) >= 0) { + count++; + cursor += fragment.length(); + } + return count; + } + /** * Extracts every repository-secret name referenced by the workflow. * From 1a56702395b992df1d38c882911a2058ac8c491d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:04:05 +0900 Subject: [PATCH 087/152] fix(ci): isolate check authorization from OpenCode --- .../workflows/hourly-opencode-maintenance.yml | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 4aff458d..b20081ae 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -14,13 +14,15 @@ permissions: jobs: maintain-repository: permissions: - actions: write + actions: read checks: read contents: write issues: write pull-requests: write security-events: read statuses: read + outputs: + open_pr_heads_before: ${{ steps.snapshot_heads.outputs.open_pr_heads }} runs-on: ubuntu-latest timeout-minutes: 50 steps: @@ -77,6 +79,7 @@ jobs: printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" - name: Snapshot open pull-request heads + id: snapshot_heads shell: bash env: GH_TOKEN: ${{ github.token }} @@ -91,6 +94,7 @@ jobs: | map({key: (.number | tostring), value: .head.sha}) | from_entries ' > "${snapshot}" + printf 'open_pr_heads=%s\n' "$(jq -c . "${snapshot}")" >> "${GITHUB_OUTPUT}" - name: Run bounded NVIDIA OpenCode maintenance shell: bash @@ -144,11 +148,21 @@ jobs: timeout --signal=TERM --kill-after=30s 45m opencode github run + authorize-exact-head-checks: + needs: maintain-repository + if: ${{ always() && !cancelled() }} + permissions: + actions: write + contents: read + pull-requests: read + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: - name: Authorize exact-head checks for agent-updated pull requests - if: ${{ always() && !cancelled() }} shell: bash env: GH_TOKEN: ${{ github.token }} + BEFORE_HEADS: ${{ needs.maintain-repository.outputs.open_pr_heads_before }} run: | set -euo pipefail before_file="${RUNNER_TEMP}/open-pr-heads-before.json" @@ -157,10 +171,12 @@ jobs: repository="${GITHUB_REPOSITORY}" failed=0 - if [[ ! -f "${before_file}" ]]; then + if [[ -z "${BEFORE_HEADS}" ]]; then echo "Open pull-request head snapshot is unavailable" >&2 exit 1 fi + printf '%s\n' "${BEFORE_HEADS}" \ + | jq -e 'select(type == "object")' > "${before_file}" gh api --paginate \ "/repos/${repository}/pulls?state=open&base=develop&per_page=100" \ From cc4c9e9b13a8af00a5bfe372b4ab232d71f231b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:05:18 +0900 Subject: [PATCH 088/152] docs(ci): isolate exact-head authorization authority --- ...exact-head-check-authorization-evidence.md | 102 ++++++++++++------ 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md index 748ccc1c..8739f7d5 100644 --- a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md +++ b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md @@ -9,6 +9,7 @@ pull-request workflow runs before treating the agent's work as ready for review. Authorization is limited to starting validation for the exact current head. It is not pull-request approval, review, merge authority, branch-protection bypass, or evidence that any check succeeded. +The OpenCode process itself never receives Actions write authority. ## Platform behavior @@ -22,48 +23,82 @@ Without a bounded authorization step, a scheduled OpenCode run could push a vali the new exact head without CI, SAST, SBOM, dependency, or security execution. That would break the required review → fix → exact-head revalidation loop even though the source change itself was valid. +## Split-job authority model + +The workflow separates mutable development from run authorization: + +```text +maintain-repository job + ├─ checks out protected default-branch source + ├─ installs and executes OpenCode + ├─ may prepare a feature branch and pull request + ├─ has actions: read + └─ outputs only the pre-agent pull-request head map + + job output boundary + ↓ + +authorize-exact-head-checks job + ├─ never checks out or executes repository code + ├─ receives no NVIDIA model credential + ├─ has actions: write, contents: read, pull-requests: read + ├─ compares before/after exact heads + └─ authorizes only approval-required exact-head workflow runs +``` + +This prevents generated code, repository scripts, or the OpenCode process from using Actions write +permission. The sole privileged job consumes only GitHub API metadata and a compact JSON map of pull +request numbers to commit SHAs produced before the agent starts. + ## Fail-closed authorization algorithm The protected default-branch workflow performs the following steps: 1. Before OpenCode starts, snapshot the exact heads of all same-repository pull requests targeting - `develop`. -2. After OpenCode exits, enumerate the same pull-request set again. -3. Select only a new pull request or a pull request whose head changed during this run. -4. Refuse automatic run authorization when the pull request changes any path below `.github/` or + `develop` and expose that compact object as the maintenance job's output. +2. After the maintenance job completes or fails without cancellation, start the isolated + authorization job. +3. Require the prior output to exist and parse as a JSON object. +4. Enumerate the same pull-request set again. +5. Select only a new pull request or a pull request whose head changed during this run. +6. Refuse automatic run authorization when the pull request changes any path below `.github/` or any `CODEOWNERS` file. Those policy changes require explicit human authorization. -5. Read the still-current pull-request head and require it to equal the selected expected SHA. -6. Discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. -7. Fail visibly when no exact-head run materializes. -8. Re-read the pull-request head immediately before authorization and reject a moved head. -9. Approve only exact-head runs in `action_required` or `waiting` state. -10. Leave check execution, review, mergeability, branch protection, and expected-head merge +7. Read the still-current pull-request head and require it to equal the selected expected SHA. +8. Discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. +9. Fail visibly when no exact-head run materializes. +10. Re-read the pull-request head immediately before authorization and reject a moved head. +11. Approve only exact-head runs in `action_required` or `waiting` state. +12. Leave check execution, review, mergeability, branch protection, and expected-head merge disposition to their existing independent gates. -The post-agent step runs even when OpenCode fails, provided the GitHub job itself was not cancelled. -This covers a partial agent session that pushed a branch before later failing. If the pre-run snapshot -is absent, run discovery fails, the head moves, a policy file changed, no run appears, or GitHub -rejects authorization, the maintenance job fails instead of reporting successful revalidation. +The isolated job runs even when OpenCode fails, provided the workflow was not cancelled. This covers +a partial agent session that pushed a branch before later failing. If the pre-run snapshot is absent, +run discovery fails, the head moves, a policy file changed, no run appears, or GitHub rejects +authorization, the workflow fails instead of reporting successful revalidation. ## Authority and credential boundary -The workflow-level permission remains `contents: read`. Only the maintenance job receives -`actions: write`, alongside the existing minimum branch, pull-request, issue, check, status, and -security-read permissions. No personal token, GitHub App token, OIDC token, or additional repository -secret is introduced. +The workflow-level permission remains `contents: read`. The maintenance job retains only the +minimum branch, pull-request, issue, check, status, security-read, and Actions-read permissions needed +for development. The separate authorization job receives the workflow's only `actions: write` +permission plus read-only contents and pull-request metadata access. No personal token, GitHub App +token, OIDC token, or additional repository secret is introduced. -`actions: write` is used solely for the workflow-run approval endpoint. The implementation contains -no pull-request review approval command and no merge API call. The OpenCode prompt continues to -forbid approval, merge, protected-branch push, branch-protection bypass, review-agent modification, -and unauthorized workflow-policy changes. +The authorization job never checks out the repository, never executes repository files, and never +receives `NVIDIA_API_KEY`. Its Actions write permission is used solely for the workflow-run approval +endpoint. The implementation contains no pull-request review approval command and no merge API call. +The OpenCode prompt continues to forbid approval, merge, protected-branch push, branch-protection +bypass, review-agent modification, and unauthorized workflow-policy changes. ## Test-first evidence `HourlyOpenCodeMaintenanceWorkflowTest` first required the following contracts before production implemented them: -- a before/after exact-head snapshot; -- job-scoped Actions write permission; +- a pre-agent exact-head snapshot exported as a job output; +- the absence of Actions write authority from the OpenCode job; +- exactly one isolated authorization job with Actions write permission; +- no checkout or NVIDIA credential in that authorization job; - same-repository and `develop` targeting; - refusal of `.github/**` and `CODEOWNERS` changes; - exact-head workflow-run discovery; @@ -72,21 +107,25 @@ implemented them: - authorization through the workflow-run endpoint only; - continued absence of pull-request approval and merge operations. -The production workflow then implemented those contracts with `gh api`, canonical JSON processing -through `jq`, and an exact SHA passed as a jq argument rather than interpolated into jq source. +The production workflow then implemented those contracts with job outputs, `gh api`, canonical JSON +processing through `jq`, and an exact SHA passed as a jq argument rather than interpolated into jq +source. ## Verification checklist Reviewers must verify on the exact current pull-request head that: - the scheduler still runs only from protected default-branch workflow source; -- the snapshot precedes the OpenCode process; +- the snapshot precedes the OpenCode process and is the only cross-job mutable evidence; +- the maintenance job has `actions: read`, not `actions: write`; +- the authorization job is the only job with `actions: write`; +- the authorization job has no checkout, repository-code execution, or NVIDIA credential; - only heads changed by that run are considered; - `.github/**` and all `CODEOWNERS` paths are excluded from automatic authorization; - both current-head reads equal the expected head; - run discovery filters `event=pull_request` and the exact head SHA; - only waiting or action-required runs reach the approval endpoint; -- absent runs and authorization failures make the job fail; +- absent runs and authorization failures make the workflow fail; - no review approval, merge, protected-branch push, or secret fallback was added; - every authorized run must still complete successfully before merge disposition can proceed. @@ -96,9 +135,10 @@ If GitHub changes the approval-required run model or the endpoint becomes unavai hourly development workflow. Do not remove the exact-head authorization contract while leaving the agent able to push changes with `GITHUB_TOKEN`, because that recreates unvalidated agent heads. -A replacement based on a GitHub App may remove this step only after its installation permissions, -recursive-trigger behavior, actor identity, secret lifecycle, exact-head workflow evidence, and -independent review boundary are documented and tested through a separate pull request. +Do not move `actions: write` back into the OpenCode job. A replacement based on a GitHub App may +remove the isolated authorization job only after its installation permissions, recursive-trigger +behavior, actor identity, secret lifecycle, exact-head workflow evidence, and independent review +boundary are documented and tested through a separate pull request. ## References — APA 7th From 6d2359930c967d86137d0309e77d4dd1775033e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:07:13 +0900 Subject: [PATCH 089/152] docs(ci): document split-job authorization --- .../operations/hourly-opencode-maintenance.md | 290 ++++++++---------- 1 file changed, 125 insertions(+), 165 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 6c5b7aed..1590de43 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -2,219 +2,187 @@ ## Purpose -`.github/workflows/hourly-opencode-maintenance.yml` runs a bounded development agent at minute 43 of every hour, in UTC. The agent uses OpenCode 1.18.13 with NVIDIA NIM to inspect the repository, repair one existing development pull request, or prepare one bounded buyer-visible improvement when no development pull request is open. +`.github/workflows/hourly-opencode-maintenance.yml` runs at minute 43 of every hour, in UTC. It uses a checksum-pinned OpenCode 1.18.13 executable and the repository's existing `NVIDIA_NIM_API_KEY` to repair one dependency-eligible development pull request or prepare one bounded buyer-visible product improvement. -This workflow is intentionally separate from code review and merge disposition. It does not replace independent review, GitHub branch protection, required checks, GitHub Advanced Security, Dependabot, CodeRabbit, or `.github/workflows/hourly-pr-disposition.yml`. +The workflow is separate from independent review and deterministic merge disposition. It never approves or merges a pull request, pushes to `develop` or `main`, weakens branch protection, changes the review agent's credential path, or publishes a release. -Because the agent writes branches and pull requests with the repository-scoped `GITHUB_TOKEN`, the workflow also owns one narrowly scoped validation duty: after an agent-created or agent-updated same-repository pull request appears, it authorizes only the approval-required GitHub Actions runs for that exact current head. This authorization starts CI and security evaluation; it is not pull-request approval, merge authority, or proof that any check passed. +## Required secret and model -## Required repository secret - -Create or retain exactly this GitHub Actions repository secret: +The only model secret referenced by this workflow is: ```text NVIDIA_NIM_API_KEY ``` -The workflow step maps that value to the provider variable documented by OpenCode. Step-level environment variables are visible to the step's Bash shell and every child process it launches, including OpenCode: +The OpenCode step maps it to: ```text NVIDIA_API_KEY ``` -The secret name used by the existing review agent is not changed. The scheduled workflow has no fallback credential for GitHub Copilot, Anthropic, OpenAI, a partner-only NVIDIA endpoint, or another model provider. A missing or empty `NVIDIA_NIM_API_KEY` fails the run before the agent starts. +A step-level environment variable is visible to that step's Bash shell and every child process, including OpenCode. The workflow has no GitHub Copilot, Anthropic, OpenAI, partner-only NVIDIA, or automatic model fallback. A missing secret fails before the agent starts. + +The pinned provider/model is: + +```text +nvidia/deepseek-ai/deepseek-v4-pro +``` -Never place the key in repository variables, source files, workflow output, issue comments, pull-request descriptions, step summaries, command arguments, or diagnostic logs. +Current model-selection evidence and replacement rules are recorded in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. -## Pinned execution contract +## Immutable execution contract -| Control | Pinned value | +| Control | Value | | --- | --- | | Schedule | `43 * * * *` | -| OpenCode release | immutable GitHub release `v1.18.13` | -| Linux x64 release asset | asset `501285078`, `opencode-linux-x64.tar.gz` | -| Linux x64 archive SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | -| Expected archive shape | exactly one root regular-file entry named `opencode` | -| OpenCode provider/model | `nvidia/deepseek-ai/deepseek-v4-pro` | -| OpenCode agent | repository `default_agent`, with OpenCode 1.18.13 falling back to `build` | -| Checkout action | `actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` | -| OpenCode process timeout | `TERM` after 45 minutes, then `KILL` after a 30-second grace period | -| GitHub job timeout | 50 minutes | +| OpenCode release | `v1.18.13` | +| Release asset | `opencode-linux-x64.tar.gz` | +| SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | +| Archive shape | exactly one regular-file member named `opencode` | +| Checkout | full-SHA-pinned `actions/checkout` | +| OpenCode timeout | `TERM` at 45 minutes, `KILL` after 30 seconds | +| Job timeout | 50 minutes | +| Overlap | serialized, active run not cancelled | | Session sharing | disabled | -| Overlapping runs | disabled; an active run is not cancelled | - -## Model selection boundary -The workflow uses `deepseek-ai/deepseek-v4-pro` because NVIDIA's current primary model catalog exposes it through a free endpoint and documents coding, agentic tool use, structured output, function calling, software-engineering use cases, and up to one million tokens of context. The previously selected Qwen3 Coder free endpoint is currently marked deprecated; relying on that endpoint would make the scheduled loop operationally brittle even when its workflow syntax and credentials were correct. +The installer downloads only the immutable GitHub release asset over HTTPS, verifies its checksum, requires exactly one member, requires GNU tar's regular-file type before extraction, extracts into a fresh mode-`0700` directory without restoring archive ownership or permissions, refuses overwrite, rejects symbolic links and non-regular output, and verifies the exact executable version. It does not use npm installation, a floating package version, or a mutable OpenCode action reference. Detailed evidence is in `docs/doctoring/opencode-archive-extraction-evidence.md`. -The workflow deliberately has no automatic model fallback. A second model invocation after a partially completed agent session could continue from non-deterministic workspace state, create duplicate branches, or produce conflicting pull requests. If the selected endpoint becomes unavailable, the run fails visibly. A replacement requires current NVIDIA primary-source research, a failing contract test, updated doctoring evidence, exact-head checks, and independent review. - -Current selection evidence and its limits are recorded in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. The repository does not claim NVIDIA benchmark results as mightyETL performance and does not claim that raw OpenCode automatically selects a maximum-reasoning variant. - -## Supply-chain installation boundary - -The workflow downloads the immutable `opencode-linux-x64.tar.gz` release asset and verifies its pinned SHA-256. Before extraction it lists the archive and requires exactly one member named `opencode`. It then reads the sole member's locale-stable GNU tar verbose metadata and requires the regular-file type character `-`, rejecting hard links, symbolic links, directories, device nodes, and other special entries before the filesystem is modified. +## Direct-token Git bootstrap -The installer creates a fresh mode-`0700` directory, refuses overwrites, does not restore archived ownership or permissions, and verifies that the extracted object is a regular non-symbolic-link file whose reported version is exactly `1.18.13`. It does not execute an npm install command, invoke a mutable OpenCode GitHub Action tag, or use a floating `latest` reference. Checkout credentials are not persisted in the working tree. +`USE_GITHUB_TOKEN=true` makes OpenCode use the repository-scoped token rather than an OpenCode App or OIDC token. Checkout keeps `persist-credentials: false`. Before OpenCode starts, the maintenance job: -Checksum validation, member validation, entry-type validation, and post-extraction validation are separate controls. The checksum binds the bytes to the reviewed immutable release asset. Member-name and type checks constrain extraction when a future pin is changed incorrectly or upstream packaging changes. The private empty extraction directory, overwrite refusal, and post-extraction type checks provide further containment. Detailed evidence is in `docs/doctoring/opencode-archive-extraction-evidence.md`. +1. requires `GITHUB_TOKEN` and `GH_TOKEN`; +2. removes inherited repository-local GitHub credential helpers; +3. installs a repository-local `!gh auth git-credential` helper; +4. sets the local author to `opencode-agent[bot]`; +5. registers an `EXIT` trap that removes the helper after success, failure, or timeout. -OpenCode 1.18.13's raw `opencode github run` handler does not consume an `AGENT` environment variable. It omits an explicit agent from the session request, allowing repository `default_agent` configuration or the handler's `build` fallback. The workflow therefore does not set a misleading `AGENT` variable. +No plaintext or encoded token is stored in Git configuration. No personal token or additional repository secret is introduced. -## Direct-token Git bootstrap +## Split-job permission model -The workflow sets `USE_GITHUB_TOKEN=true` so OpenCode uses the repository-scoped `GITHUB_TOKEN` directly and does not request an OpenCode App token through OIDC. In OpenCode 1.18.13, that mode also skips OpenCode's internal Git configuration. Without an explicit bootstrap, `persist-credentials: false` would leave later infrastructure-managed commits without an author and pushes without an HTTPS credential helper. +The workflow-level permission is only `contents: read`. -Before starting OpenCode, the workflow therefore: +### `maintain-repository` -1. fails closed when either `GITHUB_TOKEN` or its GitHub CLI alias `GH_TOKEN` is absent; -2. removes any pre-existing repository-local GitHub credential-helper entry; -3. resets inherited helpers for `https://github.com` with an empty local helper entry; -4. adds the repository-local `!gh auth git-credential` helper, which reads the short-lived token from `GH_TOKEN` at credential-request time; -5. sets the local commit author to `opencode-agent[bot]`; -6. installs an `EXIT` trap that removes the local helper after success, failure, graceful timeout, or forced process termination. +This job checks out and executes repository and agent code. It receives: -No encoded or plaintext token is written to Git configuration. The local author identity contains no credential. The agent still receives `GITHUB_TOKEN` because OpenCode uses it for GitHub API operations such as pull-request creation. No personal token, OIDC path, fallback model credential, or tracked credential file is introduced. +- `actions: read` +- `checks: read` +- `contents: write` +- `issues: write` +- `pull-requests: write` +- `security-events: read` +- `statuses: read` -## Exact-head check authorization +It does **not** receive Actions write permission. Therefore OpenCode, generated code, and checked-out repository scripts cannot authorize workflow runs. -GitHub prevents most ordinary events created with `GITHUB_TOKEN` from recursively starting another workflow. For pull requests created or updated from GitHub Actions, pull-request workflow runs can be created in an approval-required state. Leaving those runs unapproved would let the agent push a valid fix while the new head never receives CI, dependency, SBOM, SAST, or security evaluation. +### `authorize-exact-head-checks` -The workflow therefore snapshots all same-repository `develop` pull-request heads immediately before OpenCode starts. After the agent exits, including an agent failure that occurred after a push, it: +This separate job receives: -1. enumerates the same pull-request set; -2. selects only a new head or a head changed during this run; -3. refuses automatic run authorization if the pull request changes any `.github/**` path or any `CODEOWNERS` file; -4. verifies the pull request still points to the expected exact head; -5. discovers only `pull_request` workflow runs for that SHA; -6. fails if no exact-head run materializes; -7. verifies the head a second time immediately before authorization; -8. authorizes only runs in `action_required` or `waiting` state through the workflow-run approval endpoint. +- `actions: write` +- `contents: read` +- `pull-requests: read` -The implementation passes the expected SHA to `jq` as data rather than interpolating it into jq source. This avoids treating an identifier as executable filter text. The step does not approve a pull request, merge a branch, relax a check, or claim successful validation. Existing review and disposition workflows remain the only review and merge authorities. +It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. Its only write operation is the GitHub workflow-run approval endpoint for an approval-required run bound to a verified exact head. It contains no pull-request review approval or merge operation. -Detailed test-first and rollback evidence is in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. +## Why exact-head run authorization is required -## Repository permissions +GitHub prevents most events created with `GITHUB_TOKEN` from recursively triggering workflows. Pull-request runs created after an Actions-authored open or synchronize event can remain approval-required. Without explicit authorization, an agent could push a valid repair whose exact head never receives CI, dependency review, SBOM, SAST, or security scans. -The workflow-level default is only `contents: read`. Write authority is scoped to the sole `maintain-repository` job so a future job cannot inherit repository write access accidentally. +The maintenance job snapshots same-repository pull requests targeting `develop` before OpenCode starts and exports only the compact pull-request-number-to-head-SHA map. The isolated authorization job runs after the maintenance job succeeds or fails without cancellation and: -That job receives only these explicit `GITHUB_TOKEN` permissions: +1. requires the snapshot to exist and parse as a JSON object; +2. enumerates the same pull-request set after the agent run; +3. selects only a new pull request or a head changed by that run; +4. refuses automatic authorization for any `.github/**` or `CODEOWNERS` change; +5. verifies that the current head equals the expected SHA; +6. discovers only `pull_request` workflow runs for that exact SHA; +7. fails if no run materializes; +8. verifies the head again immediately before authorization; +9. approves only runs in `action_required` or `waiting` state. -- `actions: write`, solely to authorize approval-required workflow runs for a verified exact head; -- `checks: read`; -- `contents: write`; -- `issues: write`; -- `pull-requests: write`; -- `security-events: read`; -- `statuses: read`. +The expected SHA is passed to `jq` as data, not interpolated into jq source. Authorization only starts validation. Every check must still complete successfully, all review threads must be resolved, a non-author approval must be anchored to the same head, and branch protection and expected-head merge disposition must still permit merge. -There is no `id-token` permission and no permission to write security events. `contents: write` is required to prepare a feature branch; repository branch protection remains authoritative for protected branches. Actions write permission does not grant pull-request approval or merge authority, and the workflow contains no operation that performs either action. +Test-first, least-privilege, time-of-check/time-of-use, and rollback evidence is recorded in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. -## Authority boundaries +## Agent authority boundary -The scheduled development agent starts by inspecting every open pull request and its exact current head. It may update one dependency-eligible development pull request or, when none exists, create one feature branch and pull request for one bounded vertical slice. +The agent may inspect open pull requests and their exact heads, fix one dependency-eligible development pull request, or create one bounded product pull request when no development pull request exists. It must use current authoritative standards and primary documentation, add APA 7th references where material, preserve modular MSA operation, use descriptive multiword `snake_case` database names, add beginner-readable production documentation, maintain deterministic statement and branch coverage, update `CHANGELOG.md`, and report incomplete gates truthfully. The agent must not: - approve or merge a pull request; -- push directly to `develop` or `main`; -- treat queued, pending, skipped-required, cancelled, stale-head, absent, or failed checks as passing; -- bypass independent review, branch protection, repository policy, security gates, or coverage requirements; -- change the existing review agent, its provider, workflow, credential flow, or any review-agent secret name; -- modify `.github/workflows/**` or `CODEOWNERS` unless an open issue with the `automation-maintenance` label authorizes that exact change; +- push directly to protected branches; +- treat pending, absent, cancelled, skipped-required, stale, neutral-required, or failed checks as passing; +- bypass review, security, coverage, or branch-protection policy; - inspect or disclose secret values; -- create a second development pull request while another development pull request is open; -- publish a release unless a separate release-authorized workflow and all release acceptance gates permit it. - -Even when an issue authorizes an automation-policy change, the post-agent step never auto-authorizes a pull request that modifies `.github/**` or `CODEOWNERS`. A human must review and authorize those runs. - -`.github/workflows/hourly-pr-disposition.yml` remains the deterministic exact-head merge boundary. It independently evaluates review state, unresolved threads, named checks, status contexts, labels, mergeability, and expected head SHA. - -## Normal run sequence - -1. GitHub starts the scheduled workflow from the protected default branch. -2. The workflow checks out a shallow copy with persisted credentials disabled. -3. It downloads the OpenCode 1.18.13 Linux x64 archive and verifies the pinned SHA-256. -4. It requires exactly one archive member named `opencode` and confirms through locale-stable verbose metadata that the entry is a regular file before extraction. -5. It extracts into a fresh private directory without archived ownership or permissions, refuses overwrites, and verifies a regular non-symbolic-link executable with the exact version. -6. It snapshots current same-repository `develop` pull-request heads. -7. It checks that `NVIDIA_NIM_API_KEY` was supplied through `NVIDIA_API_KEY` and that the repository token aliases are present. -8. It installs the repository-local Git author and GitHub CLI credential helper required by OpenCode's direct-token path. -9. OpenCode calls the explicit `nvidia/deepseek-ai/deepseek-v4-pro` endpoint and inspects all current pull requests before selecting work. -10. The agent runs tests first, implements one bounded change, updates authoritative documentation and `CHANGELOG.md`, and leaves one feature branch and pull request. -11. The shell `EXIT` trap removes the local Git credential helper. -12. The post-agent step detects heads changed by this run, refuses policy-file changes, binds decisions to the still-current SHA, and authorizes only approval-required exact-head workflow runs. -13. Independent review and repository checks evaluate the exact new head. -14. The separate disposition workflow may merge only after every gate passes. +- change the existing review agent, its provider, workflow, credential flow, or secret names; +- create a second development pull request while one is dependency-eligible; +- modify workflow policy without a specifically authorized `automation-maintenance` issue; +- publish a release without a separately authorized release workflow and complete acceptance evidence. + +Even with an authorized automation issue, `.github/**` and `CODEOWNERS` changes are excluded from automatic run authorization and require human action. + +## Normal sequence + +1. GitHub starts the scheduled workflow from protected default-branch source. +2. OpenCode is installed from the immutable checksum-pinned archive. +3. The maintenance job snapshots current same-repository `develop` pull-request heads. +4. The NVIDIA and GitHub credential boundaries are validated and the removable Git helper is installed. +5. OpenCode reviews the current queue, executes one bounded test-first change, and leaves a feature branch or pull request. +6. The credential cleanup trap removes the local helper. +7. The isolated authorization job compares before and after heads. +8. Policy-changing pull requests are rejected from automatic run authorization. +9. Exact-head approval-required workflow runs are authorized after two SHA checks. +10. CI, security, coverage, independent review, and merge disposition operate separately. ## Failure handling -| Failure | Expected result | Operator action | +| Failure | Result | Required response | | --- | --- | --- | -| Secret missing or empty | Job fails before OpenCode starts | Restore `NVIDIA_NIM_API_KEY`; never add a fallback key | -| Repository token missing | Job fails before Git bootstrap | Restore normal GitHub Actions token availability; do not add a personal token | -| GitHub CLI or Git bootstrap fails | Job fails before OpenCode starts or push fails visibly | Retain `persist-credentials: false`; verify the runner-provided `gh` executable and local helper entries | -| Release archive unavailable | Installation step fails before extraction | Verify the immutable upstream release exists; do not substitute a floating version | -| Archive checksum mismatches | Installation fails closed before extraction | Treat as a supply-chain incident; compare the immutable release record, asset metadata, and upstream publishing source before changing any pin | -| Archive has an unexpected member name or count | Installation fails closed before extraction | Treat the packaging change as a supply-chain review event; inspect the exact immutable asset before updating the member contract | -| Archive member is not a regular-file entry | Installation fails closed before extraction | Treat link, directory, or special-entry packaging as a supply-chain incident; do not relax the type gate | -| Extracted object is absent, non-regular, or a symbolic link | Installation fails before execution | Treat as a supply-chain incident; do not relax post-extraction checks | -| OpenCode version mismatches | Installation step fails | Investigate the verified archive contents; do not bypass the version assertion | -| NVIDIA endpoint is unavailable, deprecated, or rejects the model | OpenCode step fails without fallback | Confirm current NVIDIA catalog status; prepare a test-first reviewed model-selection change or disable the scheduler | -| Process exceeds 45 minutes | `timeout` sends `TERM`, escalates to `KILL` after 30 seconds if necessary, the credential-cleanup trap runs, and the step fails | Inspect incomplete branch or PR state; reduce slice size if needed | -| GitHub job exceeds 50 minutes | GitHub cancels the job | Investigate shutdown behavior and verify the ephemeral runner was destroyed | -| Pre-run head snapshot is absent | Post-agent authorization fails closed | Investigate the GitHub API and snapshot step; never authorize from an unknown baseline | -| Agent changes `.github/**` or `CODEOWNERS` | Exact-head run authorization is refused and the job fails | Perform explicit human review and workflow-run authorization for that policy change | -| Pull-request head moves during authorization | Authorization is refused and the job fails | Re-evaluate the new exact head; never reuse the prior decision | -| No exact-head workflow run materializes | Job fails rather than claiming revalidation | Inspect GitHub event and Actions policy; do not merge the unvalidated head | -| Workflow-run approval is rejected | Job fails | Verify Actions permission and repository policy; do not add a personal token as an unreviewed workaround | -| Tests or security checks fail | Pull request remains unmergeable | Fix the exact current head; never weaken the gate | -| Token permission denied | Operation fails visibly | Add no permission until the exact denied operation is justified and documented | -| Another hourly run starts while one is active | New run waits because concurrency is serialized | No action unless the prior run is stuck | - -The workflow must not claim success for partial work. A failed run may leave a feature branch or pull request for inspection, but it cannot approve or merge it. - -## Rollback and disablement - -This change has no database migration and does not change runtime ETL services. To stop scheduled agent execution immediately, disable the **Hourly OpenCode maintenance** workflow in GitHub Actions. To remove it permanently, revert the workflow, its contract tests, this runbook, the design and plan documents, all related doctoring evidence notes, and the corresponding `CHANGELOG.md` entries through a reviewed pull request. - -Do not delete or rename `NVIDIA_NIM_API_KEY` when it is also used by other approved workflows. Disabling this workflow does not require changing the review agent or its credential scheme. - -Do not remove exact-head run authorization while retaining `GITHUB_TOKEN` branch writes. A replacement GitHub App design must first prove its permissions, actor identity, recursive-trigger behavior, secret lifecycle, exact-head evidence, and independent review boundary. +| NVIDIA secret missing | Fail before OpenCode | Restore `NVIDIA_NIM_API_KEY`; add no fallback | +| Repository token or Git helper unavailable | Fail visibly | Preserve `persist-credentials: false`; inspect runner tooling | +| Archive unavailable, checksum mismatch, unexpected member/type, or version mismatch | Fail before execution | Treat as supply-chain review; never relax the pin silently | +| Model endpoint unavailable or deprecated | Fail without fallback | Research a current NVIDIA endpoint and submit a reviewed test-first change | +| OpenCode exceeds 45 minutes | TERM then KILL; cleanup trap executes | Reduce slice size and inspect partial branch state | +| Pre-agent head output missing or invalid | Authorization job fails | Never authorize from an unknown baseline | +| Agent changes `.github/**` or `CODEOWNERS` | Automatic authorization refused | Require explicit human workflow-run authorization | +| Head moves during discovery or authorization | Authorization refused | Re-evaluate the new exact head | +| No exact-head run materializes | Workflow fails | Diagnose event and Actions policy; do not merge the head | +| Workflow-run approval rejected | Workflow fails | Verify repository policy and token permissions; add no personal-token workaround | +| Checks or review fail | Pull request remains blocked | Fix the exact head without weakening the gate | +| A prior hourly run remains active | New run waits | Investigate only if the prior run is stuck | + +A failed run can leave a reviewable feature branch or pull request, but it cannot claim successful validation, approval, merge, or release. + +## Rollback + +Disable **Hourly OpenCode maintenance** to stop execution immediately. Permanent rollback must revert the workflow, contract tests, operations document, doctoring evidence, plan/design documents, and `CHANGELOG.md` through a reviewed pull request. + +Do not remove exact-head authorization while retaining agent writes through `GITHUB_TOKEN`, and do not move `actions: write` into the OpenCode job. A GitHub App replacement requires separately reviewed evidence for installation permissions, recursive triggers, actor identity, secret lifecycle, exact-head validation, and independent review. ## Verification checklist -Before merging a workflow change, verify the exact current head has: - -- successful Windows, Ubuntu, and macOS CI; -- successful dependency review, SBOM, Semgrep, Trivy, OSV, and Scorecard gates required by repository policy; -- no unresolved current review thread; -- independent approval anchored to the exact current head; -- the `automerge-workflow` label required by the deterministic disposition workflow; -- no new secret reference other than `NVIDIA_NIM_API_KEY`; -- a current non-deprecated NVIDIA free endpoint suited to coding and tool use; -- no automatic provider or model fallback after a partially completed session; -- a full-SHA checkout pin and checksum-pinned immutable OpenCode release asset; -- exactly one expected archive member and a pre-extraction regular-file entry-type check under `LC_ALL=C`; -- a fresh mode-`0700` extraction directory, overwrite refusal, and regular non-symbolic-link executable validation; -- no npm install command, floating package tag, or mutable OpenCode action reference; -- workflow-level read-only permission plus explicit job-scoped `actions: write` and repository-maintenance permissions; -- `persist-credentials: false` plus the local GitHub CLI credential helper and `EXIT` cleanup; -- bounded `TERM` timeout with deterministic `KILL` escalation; -- no ineffective `AGENT` environment claim for raw OpenCode 1.18.13; -- a pre-agent head snapshot and post-agent changed-head selection; -- refusal of automatic run authorization for `.github/**` and `CODEOWNERS` changes; -- exact-head workflow-run discovery plus head revalidation immediately before authorization; -- visible failure when no exact-head run materializes or authorization is rejected; -- no pull-request approval or merge operation in the development workflow; -- no review-agent credential or workflow change. - -## References +Before merge, verify on the exact current head: -Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts +- Ubuntu, macOS, and Windows CI succeeded; +- dependency review, SBOM, Semgrep, Trivy, OSV, Scorecard, and required security gates succeeded; +- no current unresolved review thread or requested change remains; +- a non-author approval is anchored to the exact head; +- required workflow-change labels are present; +- only `NVIDIA_NIM_API_KEY` is referenced as a model secret; +- the current NVIDIA endpoint and immutable OpenCode pin remain valid; +- archive member, type, private extraction, overwrite, output-type, and version checks remain intact; +- OpenCode retains no Actions write authority; +- the isolated authorization job is the only holder of `actions: write` and performs no checkout; +- the before/after head output, `.github/**` and `CODEOWNERS` exclusion, exact-head run filter, double head check, and visible absent-run failure remain intact; +- the development workflow contains no review approval, merge, protected-branch push, fallback credential, or review-agent modification. -Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/script/publish.ts +## References — APA 7th + +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 @@ -226,24 +194,16 @@ Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/ Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html -GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication - GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential -GitHub, Inc. (2026). *OpenCode v1.18.13 Linux x64 release asset metadata* [JSON metadata]. GitHub REST API. https://api.github.com/repos/anomalyco/opencode/releases/assets/501285078 - GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/triggering-a-workflow -GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax - NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro - -NVIDIA Corporation. (2026). *Qwen3-Coder-480B-A35B-Instruct*. NVIDIA NIM API catalog. https://build.nvidia.com/qwen/qwen3-coder-480b-a35b-instruct From 41624f3bb2129d5b458b3475c808a2661853087c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:10:54 +0900 Subject: [PATCH 090/152] docs(ci): align design with split authority --- ...8-04-hourly-opencode-maintenance-design.md | 173 +++++++++++------- 1 file changed, 110 insertions(+), 63 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index 3f37e30e..e8ab9c14 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -2,105 +2,150 @@ ## Purpose -mightyETL needs a scheduled development loop that can inspect current repository state, remediate one bounded item, and open or update a pull request without weakening independent review and merge gates. The scheduled agent must use OpenCode with `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot or change the existing review agent's credentials, provider, workflow, or authority. +mightyETL needs a scheduled development loop that can inspect current repository state, remediate one bounded item, and open or update a pull request without weakening independent review, exact-head validation, or merge gates. The agent must use OpenCode with `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot or change the existing review agent's credentials, provider, workflow, or authority. ## Decision -Add a separate `.github/workflows/hourly-opencode-maintenance.yml` workflow. Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic, fail-closed merge boundary and require a non-author approval anchored to the exact current head. - -The maintenance workflow will: - -- run at minute 43 of every hour and on manual dispatch; -- pin `actions/checkout` by full SHA with persisted credentials disabled; -- install OpenCode 1.18.13 from an immutable release asset verified by SHA-256; -- accept exactly one regular archive member named `opencode` before extraction; -- extract into a fresh mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled; -- reject non-regular or symbolic-link output and verify the exact executable version; -- map only `${{ secrets.NVIDIA_NIM_API_KEY }}` to `NVIDIA_API_KEY`; -- select `nvidia/deepseek-ai/deepseek-v4-pro`, a current NVIDIA free endpoint documented for coding, agentic tool use, function calling, and long-context software-engineering work; -- configure no automatic model or provider fallback; -- keep workflow-level permissions read-only and scope required write permissions to the sole maintenance job; -- use the repository-scoped GitHub token without OpenCode OIDC exchange; -- bootstrap a repository-local GitHub CLI credential helper and bot author because OpenCode 1.18.13 skips internal Git setup in direct-token mode; -- remove the helper through an `EXIT` trap; -- omit the ineffective `AGENT` environment variable, allowing repository `default_agent` or OpenCode's `build` fallback; -- disable public session sharing; -- cap each run with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute job timeout; -- run from protected default-branch source, never pull-request code. +Add `.github/workflows/hourly-opencode-maintenance.yml` as a two-job workflow: + +```text +maintain-repository + ├─ protected default-branch checkout + ├─ pinned OpenCode + NVIDIA NIM + ├─ branch / issue / pull-request preparation + └─ actions: read + + exact pre-agent head map + ↓ + +authorize-exact-head-checks + ├─ no checkout or repository-code execution + ├─ no model credential + ├─ actions: write + └─ exact-head workflow-run authorization only +``` + +Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic fail-closed merge boundary and require a non-author approval anchored to the exact current head. + +The workflow: + +- runs only from the protected default branch at minute 43 of every hour; +- omits manual dispatch so a feature branch or tag cannot become scheduler source; +- pins `actions/checkout` by full SHA with persisted credentials disabled; +- installs OpenCode 1.18.13 from an immutable release asset verified by SHA-256; +- accepts exactly one regular archive member named `opencode` before extraction; +- extracts into a fresh mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled; +- rejects non-regular or symbolic-link output and verifies the exact executable version; +- maps only `${{ secrets.NVIDIA_NIM_API_KEY }}` to `NVIDIA_API_KEY`; +- selects `nvidia/deepseek-ai/deepseek-v4-pro` with no automatic model or provider fallback; +- uses the repository-scoped GitHub token without OpenCode OIDC exchange; +- bootstraps a repository-local GitHub CLI credential helper and bot author because OpenCode 1.18.13 skips internal Git setup in direct-token mode; +- removes the helper through an `EXIT` trap; +- omits the ineffective `AGENT` environment variable, allowing repository `default_agent` or OpenCode's `build` fallback; +- disables public session sharing; +- caps OpenCode with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute job timeout; +- exports a compact pre-agent pull-request head map to the isolated authorization job; +- authorizes only approval-required `pull_request` workflow runs for a still-current exact head; +- refuses automatic authorization for `.github/**` and `CODEOWNERS` changes; +- never approves or merges a pull request. ## Model selection boundary -The previous Qwen3 Coder free endpoint is marked deprecated in NVIDIA's current catalog. A deprecated hosted endpoint makes the scheduler unreliable even if its workflow, credentials, and Git behavior are correct. DeepSeek V4 Pro is selected because NVIDIA currently exposes it through a free endpoint and documents coding, agentic AI, tool use, structured output, function calling, software-engineering use cases, and up to one million tokens of context. +The previous Qwen3 Coder free endpoint is marked deprecated in NVIDIA's current catalog. DeepSeek V4 Pro is selected because NVIDIA currently exposes it through a free endpoint and documents coding, agentic AI, tool use, structured output, function calling, software-engineering use cases, and long context. -The model identifier is explicit and test-guarded. The workflow has no automatic fallback because invoking a second model after a partially completed session could operate on non-deterministic workspace state, create duplicate branches, or generate conflicting pull requests. Endpoint rejection fails visibly and requires a separate test-first model-selection change. The repository does not claim NVIDIA benchmarks as mightyETL performance and does not claim that raw OpenCode selects a maximum-reasoning variant. +The model identifier is explicit and test guarded. No fallback runs after a partial agent session because another model could operate on non-deterministic workspace state, create duplicate branches, or generate conflicting pull requests. Endpoint rejection fails visibly and requires a separate test-first model-selection change. The repository does not claim NVIDIA benchmarks as mightyETL performance. ## Supply-chain boundary -An exact npm version is not a content identity. The workflow therefore consumes the immutable upstream release asset directly. +An exact npm version is not a content identity. The workflow consumes the immutable upstream release asset directly. -The installation step: +The installer: 1. downloads only over HTTPS with failure handling and TLS 1.2 minimum; 2. verifies SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`; 3. requires exactly one archive member named `opencode`; 4. under `LC_ALL=C`, requires GNU tar's regular-file type character `-` before extraction; -5. refuses extraction on checksum, member-shape, or entry-type mismatch; -6. recreates a private mode-`0700` directory and refuses overwrites; -7. extracts without restoring archive ownership or permissions; -8. requires a regular non-symbolic-link output file; -9. applies executable mode only to that file; -10. verifies version `1.18.13` before adding it to `GITHUB_PATH`. +5. recreates a private mode-`0700` directory and refuses overwrites; +6. extracts without restoring archive ownership or permissions; +7. requires a regular non-symbolic-link output file; +8. applies executable mode only to that file; +9. verifies version `1.18.13` before adding it to `GITHUB_PATH`. -Checksum binding, pre-extraction name and type checks, private extraction, overwrite refusal, and post-extraction file checks are cumulative controls. They close the mutable npm-command finding without a package-manager bootstrap, floating release, mutable action reference, or unconstrained extraction. +Checksum binding, pre-extraction name and type checks, private extraction, overwrite refusal, and post-extraction checks are cumulative controls. They avoid mutable package-manager bootstrapping and unconstrained archive extraction. ## Direct-token credential lifecycle -OpenCode 1.18.13 uses `GITHUB_TOKEN` for GitHub API access when `USE_GITHUB_TOKEN=true` and skips its internal `configureGit` function. Its scheduled-event path later uses ordinary `git commit` and `git push`. With `persist-credentials: false`, those commands otherwise lack both author identity and HTTPS credentials. +OpenCode 1.18.13 uses `GITHUB_TOKEN` for GitHub API access when `USE_GITHUB_TOKEN=true` and skips its internal Git configuration. With `persist-credentials: false`, ordinary `git commit` and `git push` need explicit local author and HTTPS helper configuration. -The workflow therefore: +The maintenance job therefore: 1. fails closed if `NVIDIA_API_KEY`, `GITHUB_TOKEN`, or `GH_TOKEN` is empty; 2. resets inherited GitHub credential helpers in repository-local configuration; 3. installs `!gh auth git-credential`, which reads the ephemeral token from `GH_TOKEN` only when Git requests credentials; -4. sets local `user.name` and `user.email` to `opencode-agent[bot]`; +4. sets local author identity to `opencode-agent[bot]`; 5. removes the helper through an `EXIT` trap after success, failure, timeout, or forced termination. No encoded token, personal token, OIDC permission, tracked credential file, alternate model credential, or review-agent secret is introduced. -## Permission inheritance boundary +## Permission and code-execution boundary -GitHub applies workflow-level permissions to jobs unless a job provides its own map. The workflow sets only top-level `contents: read` and gives the sole `maintain-repository` job the minimum explicit map needed to inspect checks and security state, create a feature branch, update issues, and create or update a pull request. `contents: write` is necessary for the bounded branch operation; branch protection remains authoritative for `develop` and `main`. +The workflow-level permission is only `contents: read`. + +The `maintain-repository` job receives Actions read plus the minimum check, branch, issue, pull-request, security-read, and status-read permissions required for bounded development. It executes OpenCode and repository tests but has no Actions write authority. + +The `authorize-exact-head-checks` job receives Actions write plus read-only contents and pull-request metadata. It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. Its only write operation is authorizing an approval-required workflow run after exact-head verification. + +This separation prevents OpenCode, generated code, and checked-out repository scripts from using Actions write permission while still closing the `GITHUB_TOKEN` recursive-trigger gap. + +## Exact-head authorization algorithm + +1. Before OpenCode, snapshot all same-repository pull requests targeting `develop` as `{pull_request_number: head_sha}`. +2. Export that compact object as the maintenance job output. +3. Run the authorization job after maintenance success or failure unless the workflow was cancelled. +4. Require the output to exist and parse as a JSON object. +5. Enumerate the same pull-request set after OpenCode. +6. Select only a new pull request or a changed head. +7. Refuse automatic authorization when any `.github/**` or `CODEOWNERS` path changed. +8. Verify the still-current pull-request head equals the expected SHA. +9. Discover only `pull_request` workflow runs for that SHA. +10. Fail if no exact-head run materializes. +11. Verify the current head again immediately before authorization. +12. Authorize only `action_required` or `waiting` runs. + +Expected SHA values are passed to `jq` as data rather than interpolated into jq source. Authorization begins validation but conveys no review, merge, or success decision. ## Agent authority boundary -The prompt is part of the security boundary. The agent may inspect, test, edit, commit, push one feature branch, update one development pull request, or open one pull request. It must not: +The prompt is part of the security boundary. The agent may inspect, test, edit, commit, push one feature branch, update one dependency-eligible development pull request, or open one pull request. It must not: - approve or merge a pull request; - push directly to `develop` or `main`; - bypass checks, branch protection, security gates, coverage, or independent review; - alter review-agent workflows, providers, credentials, secret names, `CODEOWNERS`, branch protection, or repository secrets; -- modify `.github/workflows/**` unless an issue explicitly labeled `automation-maintenance` authorizes that exact bounded change; -- expose secret values, request payloads, raw principals, raw idempotency keys, or internal exception details; -- create a second development pull request while another one is open; -- publish a release without a separate release-authorized workflow and every acceptance gate. +- modify workflow policy unless a specifically labeled issue authorizes the bounded change; +- expose secret values or sensitive ETL payload information; +- create a second development pull request while another is dependency-eligible; +- publish a release without separate release authorization and every acceptance gate. The deterministic disposition workflow independently evaluates reviews, current threads, named checks, status contexts, labels, mergeability, and expected head SHA. ## Data flow -1. GitHub starts the workflow from `develop`. -2. Checkout reads protected default-branch source without persisting credentials. -3. The installer verifies the immutable OpenCode archive, member name, regular entry type, extraction boundary, output type, and executable version. -4. The shell validates required secrets and token aliases, installs local Git identity and helper, and registers cleanup. -5. OpenCode calls `nvidia/deepseek-ai/deepseek-v4-pro` with the bounded prompt. -6. The agent inspects all open pull requests first. It repairs one dependency-eligible exact head or, when none exists, selects one bounded buyer-visible gap, preferring issue #120 while open and ready. -7. OpenCode prepares one branch and pull request but does not approve or merge. -8. The shell removes its credential helper. -9. CI, security, independent review, branch protection, and deterministic disposition evaluate the exact head independently. +1. GitHub starts the schedule from the protected default branch. +2. The maintenance job checks out source without persisting credentials. +3. The installer validates and installs OpenCode. +4. The job snapshots direct `develop` pull-request heads and exports the map. +5. The shell validates model and repository credentials, installs local Git identity and helper, and registers cleanup. +6. OpenCode calls `nvidia/deepseek-ai/deepseek-v4-pro`, inspects the queue, and performs one bounded test-first slice. +7. OpenCode leaves one branch and pull request but does not approve or merge. +8. The helper cleanup trap runs. +9. The isolated authorization job compares before and after heads and rejects policy-changing pull requests. +10. It authorizes only approval-required runs for an unchanged exact head. +11. CI, security, independent review, branch protection, and deterministic disposition evaluate that exact head separately. ## Failure behavior -Missing credentials, deprecated or rejected model, NVIDIA outage, download failure, checksum mismatch, archive mismatch, non-regular entry, extracted-file mismatch, version mismatch, Git bootstrap failure, timeout, test failure, or permission denial fails visibly. No provider fallback or partial-success claim is allowed. Concurrency is serialized. At 45 minutes, `TERM` is followed by `KILL` after 30 seconds if necessary; the job-level timeout remains 50 minutes. +Missing credentials, deprecated or rejected model, NVIDIA outage, download failure, checksum mismatch, archive mismatch, non-regular entry, extracted-file mismatch, version mismatch, Git bootstrap failure, timeout, test failure, missing pre-agent output, policy-file change, head movement, absent workflow run, authorization rejection, or permission denial fails visibly. No provider fallback or partial-success claim is allowed. Concurrency is serialized. ## Verification @@ -110,30 +155,32 @@ Repository tests fail unless they prove: - immutable checkout and OpenCode content pins; - exact pre-extraction member name and regular-file type; - private extraction, overwrite refusal, and regular non-symbolic-link output; -- exclusive use of `NVIDIA_NIM_API_KEY` and current `nvidia/deepseek-ai/deepseek-v4-pro` selection; -- rejection of the deprecated Qwen3 Coder identifier and non-NVIDIA credential paths; -- direct-token Git bootstrap and cleanup without stored encoded authorization; -- workflow-level read-only and job-scoped least privilege without OIDC; +- exclusive use of `NVIDIA_NIM_API_KEY` and the selected NVIDIA model; +- direct-token Git bootstrap and cleanup without stored authorization data; +- workflow-level read-only permissions; +- Actions write absent from the OpenCode job; +- exactly one isolated non-checkout authorization job with Actions write; +- before/after exact-head evidence, `.github/**` and `CODEOWNERS` exclusion, exact-run filtering, and double SHA validation; - prompt prohibitions against approval, merge, protected-branch writes, review-agent changes, self-modification, and duplicate pull requests. -## References +## References — APA 7th Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *OpenCode release publishing script* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/script/publish.ts - Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html -GitHub, Inc. (2026). *Automatic token authentication*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs + +GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/triggering-a-workflow + GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro - -NVIDIA Corporation. (2026). *Qwen3-Coder-480B-A35B-Instruct*. NVIDIA NIM API catalog. https://build.nvidia.com/qwen/qwen3-coder-480b-a35b-instruct From aa2ac97d705839e66f64a76e5d33bc8aea0516ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:12:39 +0900 Subject: [PATCH 091/152] docs(ci): align implementation plan with exact-head revalidation --- ...-08-04-hourly-opencode-maintenance-plan.md | 138 +++++++++--------- 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index e30fa275..26978a27 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -2,27 +2,28 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY` without changing the independent review agent or deterministic merge workflow. +**Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY`, prepares one bounded development pull request, starts validation for an agent-written exact head, and never changes the independent review agent or deterministic merge authority. -**Architecture:** A scheduled GitHub Actions workflow runs OpenCode from protected default-branch source, installs a checksum-verified immutable OpenCode release inside a constrained extraction boundary, and may prepare only one feature-branch pull request. It calls the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint without automatic fallback. Existing review, CI, security, branch protection, and merge-disposition automation remain independent and authoritative. Direct `GITHUB_TOKEN` mode retains `persist-credentials: false` and uses a removable repository-local GitHub CLI helper because OpenCode 1.18.13 skips its own Git setup in that mode. +**Architecture:** A protected default-branch GitHub Actions workflow has two jobs. `maintain-repository` installs a checksum-verified immutable OpenCode release and may prepare one feature branch without Actions write permission. `authorize-exact-head-checks` never checks out repository code, receives no model secret, and uses the sole Actions write grant to authorize only approval-required pull-request workflow runs for an unchanged exact head. Existing review, security, branch protection, and merge-disposition automation remain independent and authoritative. -**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, DeepSeek V4 Pro, GitHub CLI credential helper, GNU Coreutils, GNU tar, Bash, Maven, JUnit 5. +**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, DeepSeek V4 Pro, GitHub CLI credential helper, GNU Coreutils, GNU tar, Bash, jq, Maven, JUnit 5. -## Global Constraints +## Global constraints - Keep the review-agent provider, workflow, credential flow, and secret names unchanged. - Preserve `.github/workflows/hourly-pr-disposition.yml` as the independent exact-head merge boundary. -- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the scheduled model credential and expose it as `NVIDIA_API_KEY`. -- Pin `MODEL: nvidia/deepseek-ai/deepseek-v4-pro`; reject the deprecated Qwen3 Coder free-endpoint identifier. -- Do not configure GitHub Copilot, Anthropic, OpenAI, partner-only NVIDIA, or automatic model fallbacks. +- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the model credential and expose it as `NVIDIA_API_KEY` only to the OpenCode step. +- Pin `MODEL: nvidia/deepseek-ai/deepseek-v4-pro`; reject deprecated or non-NVIDIA fallback identifiers. - Pin executable content and third-party workflow sources immutably. - Verify OpenCode 1.18.13 Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. - Require exactly one regular archive entry named `opencode`; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output. - Keep checkout credential persistence disabled; use only a repository-local GitHub CLI helper removed by an `EXIT` trap. -- Keep workflow-level permissions read-only and scope necessary writes to the sole maintenance job. +- Keep workflow-level permissions read-only. +- Keep Actions write authority out of the OpenCode job and in one isolated non-checkout job. +- Refuse automatic workflow-run authorization for `.github/**` and `CODEOWNERS` changes. +- Bind run discovery and authorization to the exact still-current head SHA. - Bound OpenCode with `TERM` after 45 minutes, `KILL` after a 30-second grace period, and a 50-minute job timeout. -- Do not claim raw OpenCode 1.18.13 consumes `AGENT`; use repository `default_agent` or its `build` fallback. -- The agent may create or update one pull request but may never approve, merge, bypass protection, or push to `develop` or `main`. +- The agent may create or update one pull request but may never approve, merge, bypass protection, publish, or push to `develop` or `main`. - Preserve standalone operation and modular CWL service compatibility. --- @@ -33,18 +34,12 @@ - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` - Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java` -**Interfaces:** -- Consumes: repository-root discovery, OpenCode 1.18.13 source and release metadata, NVIDIA model catalog, GNU tar guidance. -- Produces: cross-platform text contracts for schedule, credentials, model, archive, permissions, and authority. - - [x] **Step 1: Write the initial missing-workflow test** -Require the workflow to exist and encode schedule, concurrency, timeout, pinned checkout, no persisted credentials, immutable OpenCode installation, NVIDIA-only credentials, private sharing, direct GitHub token mode, least privilege, and prompt prohibitions. +Require workflow existence, hourly schedule, serialized concurrency, bounded timeout, immutable checkout, no persisted credentials, NVIDIA-only credentials, private sharing, and prompt prohibitions. - [x] **Step 2: Verify initial RED** -Run: - ```bash ./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test ``` @@ -53,103 +48,108 @@ Observed: the existence assertion failed before the workflow was created. - [x] **Step 3: Add immutable-installation and direct-token contracts** -Require exact release URL and checksum, no npm install, job-scoped writes, graceful and forced timeout, repository-local GitHub CLI helper, bot author, cleanup trap, and no encoded authorization header or ineffective `AGENT` claim. +Require exact release URL and checksum, no npm install, graceful and forced timeout, removable GitHub CLI helper, bot author, cleanup trap, and no encoded authorization header. - [x] **Step 4: Add archive-member and entry-type contracts** -Require exactly one member named `opencode`, locale-stable GNU tar metadata, regular-file type `-`, stable failure messages, and validation before extraction. - -- [x] **Step 5: Verify archive RED cycles** +Require one member named `opencode`, locale-stable GNU tar metadata, regular-file type `-`, validation before extraction, private directory, overwrite refusal, and post-extraction file checks. -First test-only commit `751eedb852eca1165a5b936296255fc608494dad` produced 284 tests, one failure, zero errors; the sole failure was missing exact-member validation. +- [x] **Step 5: Add current-model availability contract** -Second test-only commit `7b82a40b12c46aed869aeec7b387a161a7b33896` produced 285 tests, one failure, zero errors, zero skipped project tests; the sole failure was missing regular-entry validation. +Require `nvidia/deepseek-ai/deepseek-v4-pro` and reject the deprecated Qwen3 Coder endpoint. -- [x] **Step 6: Add current-model availability contract** +- [x] **Step 6: Add exact-head revalidation contracts** -Require: +Require a pre-agent head snapshot, isolated Actions-write job, no checkout or NVIDIA secret in that job, `.github/**` and `CODEOWNERS` exclusion, exact-run discovery, double SHA validation, absent-run failure, and no review or merge operation. -```java -assertTrue(workflow.contains("MODEL: nvidia/deepseek-ai/deepseek-v4-pro")); -assertFalse(workflow.contains("qwen/qwen3-coder-480b-a35b-instruct")); -``` - -- [x] **Step 7: Verify model-selection RED** +- [x] **Step 7: Verify RED cycles** -Test-only commit `42eb7d7ac8bc3912e3a50f98b427b712f78b2b9b` produced 286 tests, one failure, zero errors, zero skipped project tests in Ubuntu run `30964719079`; the sole failure was `usesCurrentFreeAgenticCodingModel` because the workflow still selected the deprecated Qwen3 Coder free endpoint. +The workflow-existence, archive-member, archive-type, model-selection, exact-head authorization, policy-path exclusion, and Actions-write isolation contracts were committed before their corresponding production behavior. -### Task 2: Implement the bounded NVIDIA OpenCode workflow +### Task 2: Implement the bounded NVIDIA OpenCode job **Files:** - Create: `.github/workflows/hourly-opencode-maintenance.yml` -**Interfaces:** -- Consumes: `NVIDIA_NIM_API_KEY`, repository-scoped `GITHUB_TOKEN`, immutable OpenCode release asset. -- Produces: one serialized scheduled development session that can prepare but not approve or merge one pull request. - -- [x] **Step 1: Add scheduling and authority boundary** +- [x] **Step 1: Add protected scheduling and authority boundary** -Configure `43 * * * *`, manual dispatch, serialized concurrency, 50-minute job timeout, protected source checkout, prompt constraints, and explicit prohibition of approval, merge, protected-branch push, review-agent modification, secret disclosure, duplicate PR creation, and release publication. +Configure `43 * * * *`, omit manual dispatch, serialize concurrency, set a 50-minute job timeout, checkout protected default-branch source, and prohibit approval, merge, protected-branch push, review-agent modification, secret disclosure, duplicate PR creation, and release publication. - [x] **Step 2: Add immutable OpenCode installation** -Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract into a private mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled, reject non-regular or symbolic-link output, and verify version `1.18.13`. +Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract privately without restoring archive ownership or permissions, refuse overwrite, reject non-regular or symbolic-link output, and verify exact version. - [x] **Step 3: Add direct-token Git bootstrap** Fail closed on missing token aliases, reset inherited helpers locally, install `!gh auth git-credential`, set `opencode-agent[bot]` local identity, and remove the helper through an `EXIT` trap. -- [x] **Step 4: Select a current free NVIDIA coding endpoint** +- [x] **Step 4: Select the current NVIDIA coding endpoint** -Set: +Set `NVIDIA_API_KEY`, `MODEL`, `SHARE=false`, and `USE_GITHUB_TOKEN=true` without provider or model fallback. -```yaml -NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} -MODEL: nvidia/deepseek-ai/deepseek-v4-pro -SHARE: "false" -USE_GITHUB_TOKEN: "true" -``` +- [x] **Step 5: Remove Actions write from the OpenCode job** + +Give `maintain-repository` Actions read plus only the branch, issue, PR, check, security-read, and status permissions required for bounded maintenance. + +### Task 3: Implement isolated exact-head run authorization + +**Files:** +- Modify: `.github/workflows/hourly-opencode-maintenance.yml` +- Modify: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` -Do not add a provider or model fallback. Endpoint rejection must fail visibly. +- [x] **Step 1: Export the pre-agent head map** -- [ ] **Step 5: Verify focused GREEN on the integrated exact head** +Snapshot same-repository pull requests targeting `develop` before OpenCode and expose compact JSON as a maintenance-job output. -Run: +- [x] **Step 2: Add the isolated authorization job** + +Run after maintenance success or failure unless cancelled. Give it only `actions: write`, `contents: read`, and `pull-requests: read`. Do not checkout repository code or pass the NVIDIA credential. + +- [x] **Step 3: Add policy-path and TOCTOU gates** + +Reject `.github/**` and `CODEOWNERS` changes, verify the exact head before discovery and immediately before authorization, and pass the expected SHA to jq as data. + +- [x] **Step 4: Authorize only exact approval-required runs** + +Discover `pull_request` runs by exact `head_sha`, fail when no run appears, and call the approval endpoint only for `action_required` or `waiting` runs. Do not approve or merge the pull request. + +- [ ] **Step 5: Verify focused GREEN on the final exact head** ```bash ./mvnw -pl etl-service \ -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest' test ``` -Expected: PASS with zero failures, errors, and skipped project tests. +Expected: zero failures, errors, and skipped project tests. -### Task 3: Complete evidence and release notes +### Task 4: Complete evidence and release notes **Files:** - Create: `docs/operations/hourly-opencode-maintenance.md` - Create: `docs/doctoring/opencode-archive-extraction-evidence.md` - Create: `docs/doctoring/nvidia-opencode-model-selection-evidence.md` +- Create: `docs/doctoring/github-token-exact-head-check-authorization-evidence.md` - Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` - Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` - Modify: `CHANGELOG.md` -**Interfaces:** -- Consumes: implemented workflow, TDD evidence, OpenCode source, NVIDIA current catalog/API, GitHub and GNU primary documentation. -- Produces: beginner-readable operations, rollback, model-selection, supply-chain, credential, permission, timeout, and governance evidence. - - [x] **Step 1: Document archive and credential boundaries** -Document the immutable release, checksum, exact regular archive member, extraction boundary, direct-token helper lifecycle, permissions, timeout escalation, authority restrictions, failure handling, and rollback. +Record immutable release, checksum, exact member and type, extraction controls, direct-token helper lifecycle, timeout escalation, authority restrictions, failure behavior, and rollback. - [x] **Step 2: Document model selection** -Record the deprecated Qwen3 Coder free endpoint, current DeepSeek V4 Pro free endpoint, coding and tool-use suitability, one-million-token context evidence, absence of automatic fallback, RED evidence, and APA 7 references. +Record deprecated-endpoint rejection, current NVIDIA endpoint evidence, no-fallback semantics, RED evidence, and APA 7 references. + +- [x] **Step 3: Document exact-head run authorization** -- [ ] **Step 3: Align `CHANGELOG.md` with the exact integrated behavior** +Record GitHub-token recursive-trigger behavior, split-job authority, before/after SHA evidence, policy-path exclusion, Actions-write isolation, TOCTOU validation, failure behavior, rollback, and APA 7 references. -Replace the Qwen3 Coder model reference with `nvidia/deepseek-ai/deepseek-v4-pro`, record the deprecated-endpoint replacement, and list both doctoring evidence files. +- [x] **Step 4: Align `CHANGELOG.md`** -- [ ] **Step 4: Run full reactor verification** +Record the NVIDIA model, immutable installation, isolated exact-head workflow-run authorization, and all doctoring evidence files under `Unreleased`. + +- [ ] **Step 5: Run full reactor verification** ```bash ./mvnw -B test @@ -157,15 +157,11 @@ Replace the Qwen3 Coder model reference with `nvidia/deepseek-ai/deepseek-v4-pro Expected: all modules succeed; no project test is skipped. -### Task 4: Verify and integrate the protected workflow-change pull request +### Task 5: Verify and integrate the protected workflow-change pull request **Files:** - No additional source files. -**Interfaces:** -- Consumes: final exact branch head and all CI, security, and review evidence. -- Produces: guarded squash merge to `develop` only after every policy gate succeeds. - - [ ] **Step 1: Verify exact branch head and diff** ```bash @@ -179,16 +175,16 @@ Expected: clean tree, no whitespace errors, successful build. - [x] **Step 2: Open and label the pull request** -Use title `ci: schedule NVIDIA OpenCode maintenance agent`; apply `automerge-workflow` because workflow files change, and retain `manual-merge` until an exact-head non-author approval exists. +Use title `ci: schedule NVIDIA OpenCode maintenance agent`; apply `automerge-workflow`, and retain `manual-merge` until exact-head checks and non-author approval exist. - [ ] **Step 3: Reinspect all feedback on the final exact head** -Inspect human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Resolve only addressed current threads and distinguish stale or superseded findings. +Inspect human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Resolve only findings addressed by the current head. - [ ] **Step 4: Verify every final exact-head gate** -Require successful Ubuntu, macOS, Windows, Dependency Review, SBOM, Semgrep, Trivy, OSV, Scorecard, combined status, mergeability, and no unresolved current thread. Treat pending, queued, cancelled, neutral-required, skipped-required, stale, or absent evidence as not passing. +Require successful Ubuntu, macOS, Windows, Dependency Review, SBOM, Semgrep, Trivy, OSV, Scorecard, combined status, mergeability, and zero unresolved current threads. Pending, queued, cancelled, neutral-required, skipped-required, stale, or absent evidence is not passing. - [ ] **Step 5: Require independent exact-head approval and merge** -Do not self-approve. After a non-author approval whose commit ID equals the current head and every exact-head gate succeeds, remove `manual-merge` and squash-merge using the expected head SHA. Otherwise keep the hold and report only the external approval or policy blocker after all autonomous remediation is exhausted. +Do not self-approve. After a non-author approval whose commit ID equals the current head and every exact-head gate succeeds, remove `manual-merge` and squash-merge using the expected head SHA. Otherwise retain the hold and identify the exact external gate. From 015511c5c4cf1e25480eae78b1357430c810c9ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:31:35 +0900 Subject: [PATCH 092/152] test(ci): require every exact-head workflow to materialize --- ...CodeRequiredWorkflowAuthorizationTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java new file mode 100644 index 00000000..2e54f001 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java @@ -0,0 +1,70 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards complete exact-head workflow-run materialization after an OpenCode branch update. + * + *

GitHub can create approval-required pull-request runs asynchronously. Stopping after the first + * run appears can leave later CI or security workflows waiting forever. The isolated authorization + * job must therefore continue discovering and authorizing runs until every named pull-request + * workflow required by mightyETL has materialized for the unchanged exact head.

+ */ +class HourlyOpenCodeRequiredWorkflowAuthorizationTest { + + /** + * Requires the authorization loop to wait for, and account for, every required workflow. + * + * @throws IOException when the production workflow cannot be read + */ + @Test + void waitsForEveryRequiredExactHeadWorkflow() throws IOException { + String workflow = Files.readString( + projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + + assertTrue(workflow.contains( + "required_workflow_names='[\"CI\",\"Dependency Review\"," + + "\"SBOM (CycloneDX)\",\"SAST Semgrep\",\"Security Scan\"]'" + )); + assertTrue(workflow.contains("observed_workflow_names")); + assertTrue(workflow.contains("missing_workflow_names")); + assertTrue(workflow.contains("missing_workflow_count")); + assertTrue(workflow.contains("for _ in $(seq 1 18); do")); + assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); + assertTrue(workflow.contains("All required exact-head pull-request workflows materialized")); + assertTrue(workflow.contains("Required exact-head pull-request workflows did not materialize")); + } + + /** + * Finds the repository root from root- or module-scoped Maven execution. + * + * @return absolute repository root + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From bd017ef9e81b784b15d12c6aec4dbafe9a53f9bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:34:36 +0900 Subject: [PATCH 093/152] fix(ci): authorize the complete exact-head workflow set --- .../workflows/hourly-opencode-maintenance.yml | 77 ++++++++++++------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index b20081ae..aa2a1f9c 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -132,9 +132,6 @@ jobs: exit 1 fi - # OpenCode 1.18.13 skips its internal Git credential and author setup in - # USE_GITHUB_TOKEN mode. Install a repository-local GitHub CLI helper while - # preserving checkout's persist-credentials: false supply-chain boundary. git_credential_key="credential.https://github.com.helper" cleanup_git_credentials() { git config --local --unset-all "${git_credential_key}" >/dev/null 2>&1 || true @@ -169,6 +166,7 @@ jobs: after_file="${RUNNER_TEMP}/open-pr-heads-after.json" changed_file="${RUNNER_TEMP}/agent-changed-pr-heads.jsonl" repository="${GITHUB_REPOSITORY}" + required_workflow_names='["CI","Dependency Review","SBOM (CycloneDX)","SAST Semgrep","Security Scan"]' failed=0 if [[ -z "${BEFORE_HEADS}" ]]; then @@ -225,49 +223,74 @@ jobs: fi runs_file="${RUNNER_TEMP}/pr-${number}-exact-head-runs.json" - run_count=0 + observed_workflow_names='[]' + missing_workflow_names="${required_workflow_names}" + missing_workflow_count="$(jq 'length' <<<"${missing_workflow_names}")" + for _ in $(seq 1 18); do + current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" + if [[ "${current_head}" != "${expected_head}" ]]; then + echo "PR #${number} moved while workflow runs were materializing" >&2 + failed=1 + break + fi + gh api --paginate \ "/repos/${repository}/actions/runs?event=pull_request&head_sha=${expected_head}&per_page=100" \ | jq -s '(map(.workflow_runs) | add) // []' > "${runs_file}" - run_count="$(jq 'length' "${runs_file}")" - if [[ "${run_count}" -gt 0 ]]; then + + while IFS= read -r run_id; do + [[ -n "${run_id}" ]] || continue + gh api --method POST \ + "/repos/${repository}/actions/runs/${run_id}/approve" + done < <( + jq -r --arg expected_head "${expected_head}" ' + .[] + | select( + .head_sha == $expected_head + and ( + .conclusion == "action_required" + or .status == "waiting" + ) + ) + | .id + ' "${runs_file}" + ) + + observed_workflow_names="$(jq -c '[.[].name] | unique' "${runs_file}")" + missing_workflow_names="$( + jq -cn \ + --argjson required "${required_workflow_names}" \ + --argjson observed "${observed_workflow_names}" \ + '$required - $observed' + )" + missing_workflow_count="$(jq 'length' <<<"${missing_workflow_names}")" + if [[ "${missing_workflow_count}" -eq 0 ]]; then + echo "All required exact-head pull-request workflows materialized for PR #${number} at ${expected_head}" break fi sleep 5 done - if [[ "${run_count}" -eq 0 ]]; then - echo "No pull-request workflow run materialized for PR #${number} exact head ${expected_head}" >&2 + if [[ "${failed}" -ne 0 ]]; then + continue + fi + if [[ "${missing_workflow_count}" -gt 0 ]]; then + if [[ "$(jq 'length' <<<"${observed_workflow_names}")" -eq 0 ]]; then + echo "No pull-request workflow run materialized for PR #${number} exact head ${expected_head}" >&2 + fi + echo "Required exact-head pull-request workflows did not materialize for PR #${number}: ${missing_workflow_names}" >&2 failed=1 continue fi current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" if [[ "${current_head}" != "${expected_head}" ]]; then - echo "PR #${number} moved before workflow-run authorization" >&2 + echo "PR #${number} moved before workflow-run authorization completed" >&2 failed=1 continue fi - while IFS= read -r run_id; do - [[ -n "${run_id}" ]] || continue - gh api --method POST \ - "/repos/${repository}/actions/runs/${run_id}/approve" - done < <( - jq -r --arg expected_head "${expected_head}" ' - .[] - | select( - .head_sha == $expected_head - and ( - .conclusion == "action_required" - or .status == "waiting" - ) - ) - | .id - ' "${runs_file}" - ) - echo "Authorized exact-head pull-request checks for PR #${number} branch ${head_ref} at ${expected_head}" done < "${changed_file}" From 887a48a05a22ea5cf16c55c9b9fe93cd078867cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:36:32 +0900 Subject: [PATCH 094/152] docs(ci): require complete workflow-set materialization --- ...exact-head-check-authorization-evidence.md | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md index 8739f7d5..96b6983a 100644 --- a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md +++ b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md @@ -23,6 +23,11 @@ Without a bounded authorization step, a scheduled OpenCode run could push a vali the new exact head without CI, SAST, SBOM, dependency, or security execution. That would break the required review → fix → exact-head revalidation loop even though the source change itself was valid. +A second timing problem also matters: GitHub materializes the several pull-request workflows +asynchronously. Stopping as soon as the first run appears can authorize CI while a later Security Scan +or SAST run remains approval-required indefinitely. The authorization job must therefore observe the +complete named workflow set, authorizing newly visible waiting runs on every discovery pass. + ## Split-job authority model The workflow separates mutable development from run authorization: @@ -50,6 +55,24 @@ This prevents generated code, repository scripts, or the OpenCode process from u permission. The sole privileged job consumes only GitHub API metadata and a compact JSON map of pull request numbers to commit SHAs produced before the agent starts. +## Required workflow set + +For a direct pull request to `develop`, exact-head validation is incomplete until all of these +pull-request workflows have materialized: + +```text +CI +Dependency Review +SBOM (CycloneDX) +SAST Semgrep +Security Scan +``` + +These are workflow names, not conclusions. Their presence proves only that validation was created for +the head. Every run and named check must still complete successfully through the ordinary repository +policy before merge. A workflow rename or required-workflow change must update the contract test, +this evidence document, and the authorization list in one reviewed change. + ## Fail-closed authorization algorithm The protected default-branch workflow performs the following steps: @@ -64,17 +87,22 @@ The protected default-branch workflow performs the following steps: 6. Refuse automatic run authorization when the pull request changes any path below `.github/` or any `CODEOWNERS` file. Those policy changes require explicit human authorization. 7. Read the still-current pull-request head and require it to equal the selected expected SHA. -8. Discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. -9. Fail visibly when no exact-head run materializes. -10. Re-read the pull-request head immediately before authorization and reject a moved head. -11. Approve only exact-head runs in `action_required` or `waiting` state. -12. Leave check execution, review, mergeability, branch protection, and expected-head merge +8. Repeatedly discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. +9. On each discovery pass, authorize every exact-head run still in `action_required` or `waiting` + state. +10. Compare the unique observed workflow names with the complete required workflow set. +11. Continue bounded discovery until every required workflow has materialized or the discovery + window expires. +12. Fail visibly when no run appears or any required workflow remains absent. +13. Re-read the pull-request head immediately before declaring authorization complete and reject a + moved head. +14. Leave check execution, review, mergeability, branch protection, and expected-head merge disposition to their existing independent gates. The isolated job runs even when OpenCode fails, provided the workflow was not cancelled. This covers a partial agent session that pushed a branch before later failing. If the pre-run snapshot is absent, -run discovery fails, the head moves, a policy file changed, no run appears, or GitHub rejects -authorization, the workflow fails instead of reporting successful revalidation. +run discovery fails, the head moves, a policy file changed, the named workflow set is incomplete, or +GitHub rejects authorization, the workflow fails instead of reporting successful revalidation. ## Authority and credential boundary @@ -107,9 +135,11 @@ implemented them: - authorization through the workflow-run endpoint only; - continued absence of pull-request approval and merge operations. -The production workflow then implemented those contracts with job outputs, `gh api`, canonical JSON -processing through `jq`, and an exact SHA passed as a jq argument rather than interpolated into jq -source. +`HourlyOpenCodeRequiredWorkflowAuthorizationTest` then required the complete five-workflow set, +repeated bounded discovery, repeated waiting-run authorization, observed/missing workflow evidence, +and explicit failure when the set remains incomplete. Production implemented those contracts with +job outputs, `gh api`, canonical JSON processing through `jq`, and exact SHA and workflow-name arrays +passed as data rather than interpolated into jq source. ## Verification checklist @@ -124,8 +154,9 @@ Reviewers must verify on the exact current pull-request head that: - `.github/**` and all `CODEOWNERS` paths are excluded from automatic authorization; - both current-head reads equal the expected head; - run discovery filters `event=pull_request` and the exact head SHA; -- only waiting or action-required runs reach the approval endpoint; -- absent runs and authorization failures make the workflow fail; +- waiting or action-required runs are authorized on every discovery pass; +- CI, Dependency Review, SBOM, SAST, and Security Scan all materialize before success is reported; +- absent runs, missing named workflows, and authorization failures make the workflow fail; - no review approval, merge, protected-branch push, or secret fallback was added; - every authorized run must still complete successfully before merge disposition can proceed. @@ -137,8 +168,9 @@ agent able to push changes with `GITHUB_TOKEN`, because that recreates unvalidat Do not move `actions: write` back into the OpenCode job. A replacement based on a GitHub App may remove the isolated authorization job only after its installation permissions, recursive-trigger -behavior, actor identity, secret lifecycle, exact-head workflow evidence, and independent review -boundary are documented and tested through a separate pull request. +behavior, actor identity, secret lifecycle, exact-head workflow evidence, complete required-run +materialization, and independent review boundary are documented and tested through a separate pull +request. ## References — APA 7th From 68e82f9d7e19623b6f397bd52a4e54d41a472c97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:39:33 +0900 Subject: [PATCH 095/152] docs(ci): document complete exact-head workflow authorization --- .../operations/hourly-opencode-maintenance.md | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 1590de43..2654f836 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -91,6 +91,18 @@ It never checks out or executes repository code and never receives `NVIDIA_API_K GitHub prevents most events created with `GITHUB_TOKEN` from recursively triggering workflows. Pull-request runs created after an Actions-authored open or synchronize event can remain approval-required. Without explicit authorization, an agent could push a valid repair whose exact head never receives CI, dependency review, SBOM, SAST, or security scans. +GitHub materializes those workflows asynchronously. Seeing and authorizing only the first run is not sufficient: later workflows can appear after the authorization step exits and remain waiting forever. The isolated job therefore requires this complete workflow-name set for a direct `develop` pull request: + +```text +CI +Dependency Review +SBOM (CycloneDX) +SAST Semgrep +Security Scan +``` + +These names prove only that the runs exist for the head. They do not make a run successful and do not replace the named check requirements enforced by merge disposition. + The maintenance job snapshots same-repository pull requests targeting `develop` before OpenCode starts and exports only the compact pull-request-number-to-head-SHA map. The isolated authorization job runs after the maintenance job succeeds or fails without cancellation and: 1. requires the snapshot to exist and parse as a JSON object; @@ -98,14 +110,16 @@ The maintenance job snapshots same-repository pull requests targeting `develop` 3. selects only a new pull request or a head changed by that run; 4. refuses automatic authorization for any `.github/**` or `CODEOWNERS` change; 5. verifies that the current head equals the expected SHA; -6. discovers only `pull_request` workflow runs for that exact SHA; -7. fails if no run materializes; -8. verifies the head again immediately before authorization; -9. approves only runs in `action_required` or `waiting` state. +6. repeatedly discovers only `pull_request` workflow runs for that exact SHA; +7. authorizes every visible run still in `action_required` or `waiting` state on each pass; +8. compares observed names with the complete five-workflow set; +9. continues bounded discovery until all five materialize; +10. fails if no run appears or any required workflow remains missing; +11. verifies the head again immediately before authorization is declared complete. -The expected SHA is passed to `jq` as data, not interpolated into jq source. Authorization only starts validation. Every check must still complete successfully, all review threads must be resolved, a non-author approval must be anchored to the same head, and branch protection and expected-head merge disposition must still permit merge. +The expected SHA and workflow-name arrays are passed to `jq` as data, not interpolated into jq source. Authorization only starts validation. Every check must still complete successfully, all review threads must be resolved, a non-author approval must be anchored to the same head, and branch protection and expected-head merge disposition must still permit merge. -Test-first, least-privilege, time-of-check/time-of-use, and rollback evidence is recorded in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. +Test-first, least-privilege, complete-materialization, time-of-check/time-of-use, and rollback evidence is recorded in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. ## Agent authority boundary @@ -135,7 +149,7 @@ Even with an authorized automation issue, `.github/**` and `CODEOWNERS` changes 6. The credential cleanup trap removes the local helper. 7. The isolated authorization job compares before and after heads. 8. Policy-changing pull requests are rejected from automatic run authorization. -9. Exact-head approval-required workflow runs are authorized after two SHA checks. +9. Exact-head approval-required runs are repeatedly authorized while all five required workflow names materialize. 10. CI, security, coverage, independent review, and merge disposition operate separately. ## Failure handling @@ -151,7 +165,8 @@ Even with an authorized automation issue, `.github/**` and `CODEOWNERS` changes | Agent changes `.github/**` or `CODEOWNERS` | Automatic authorization refused | Require explicit human workflow-run authorization | | Head moves during discovery or authorization | Authorization refused | Re-evaluate the new exact head | | No exact-head run materializes | Workflow fails | Diagnose event and Actions policy; do not merge the head | -| Workflow-run approval rejected | Workflow fails | Verify repository policy and token permissions; add no personal-token workaround | +| A required workflow name remains absent | Workflow fails and lists missing names | Diagnose trigger filters or renamed workflows; update the contract only through review | +| Workflow-run approval is rejected | Workflow fails | Verify repository policy and token permissions; add no personal-token workaround | | Checks or review fail | Pull request remains blocked | Fix the exact head without weakening the gate | | A prior hourly run remains active | New run waits | Investigate only if the prior run is stuck | @@ -161,7 +176,7 @@ A failed run can leave a reviewable feature branch or pull request, but it canno Disable **Hourly OpenCode maintenance** to stop execution immediately. Permanent rollback must revert the workflow, contract tests, operations document, doctoring evidence, plan/design documents, and `CHANGELOG.md` through a reviewed pull request. -Do not remove exact-head authorization while retaining agent writes through `GITHUB_TOKEN`, and do not move `actions: write` into the OpenCode job. A GitHub App replacement requires separately reviewed evidence for installation permissions, recursive triggers, actor identity, secret lifecycle, exact-head validation, and independent review. +Do not remove exact-head authorization while retaining agent writes through `GITHUB_TOKEN`, and do not move `actions: write` into the OpenCode job. A GitHub App replacement requires separately reviewed evidence for installation permissions, recursive triggers, actor identity, secret lifecycle, complete exact-head validation, and independent review. ## Verification checklist @@ -178,6 +193,7 @@ Before merge, verify on the exact current head: - OpenCode retains no Actions write authority; - the isolated authorization job is the only holder of `actions: write` and performs no checkout; - the before/after head output, `.github/**` and `CODEOWNERS` exclusion, exact-head run filter, double head check, and visible absent-run failure remain intact; +- all five required pull-request workflows must materialize before the authorization job reports completion; - the development workflow contains no review approval, merge, protected-branch push, fallback credential, or review-agent modification. ## References — APA 7th From df61d97576e5f5bd2cc863da74eb68644fc91e5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 10:45:24 +0900 Subject: [PATCH 096/152] test(ci): require agent authority separation and PR-bound checks --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 246 ++++++++---------- 1 file changed, 114 insertions(+), 132 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index 903f8b37..f5f48fa6 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -18,13 +18,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Guards the credential, authority, supply-chain, and exact-head validation boundaries of the - * scheduled OpenCode maintenance workflow. + * Guards the credential, authority, supply-chain, publication, and exact-head validation + * boundaries of the scheduled OpenCode maintenance workflow. * - *

The scheduled agent is allowed to prepare feature-branch pull requests. It is not a reviewer - * or merger. These tests make that separation visible to beginners and prevent a later workflow - * edit from silently adding a fallback provider, mutable tool version, protected-branch push, - * self-approval path, policy-code auto-authorization, or unvalidated agent-generated head.

+ *

The model may edit and push one bounded feature branch. It never receives pull-request write + * authority. A deterministic non-checkout publisher may create one draft pull request, while a + * second isolated non-checkout job may authorize only approval-required workflow runs associated + * with that exact pull request and exact head. These tests keep those authorities physically + * separated from model and repository-code execution.

*/ class HourlyOpenCodeMaintenanceWorkflowTest { @@ -35,9 +36,7 @@ class HourlyOpenCodeMaintenanceWorkflowTest { private static String workflow; /** - * Reads the workflow once after first producing an ordinary assertion failure when the - * production workflow has not yet been implemented. Line endings are normalized so the same - * structural contracts run deterministically on Windows, Linux, and macOS checkouts. + * Reads the workflow once and normalizes line endings for deterministic cross-platform tests. * * @throws IOException when the workflow exists but cannot be read as UTF-8 text */ @@ -46,18 +45,12 @@ static void readWorkflow() throws IOException { Path workflowPath = projectRoot().resolve( ".github/workflows/hourly-opencode-maintenance.yml" ); - assertTrue( - Files.exists(workflowPath), - "The hourly OpenCode maintenance workflow must exist" - ); + assertTrue(Files.exists(workflowPath), "The hourly OpenCode workflow must exist"); workflow = Files.readString(workflowPath, StandardCharsets.UTF_8) .replace("\r\n", "\n"); } - /** - * Verifies that runs are offset from the top of the hour, serialized, and time bounded, - * including forced termination when an agent ignores the graceful termination signal. - */ + /** Verifies one serialized, bounded run every hour. */ @Test void schedulesOneBoundedNonOverlappingRunPerHour() { assertTrue(workflow.contains("cron: \"43 * * * *\"")); @@ -65,15 +58,11 @@ void schedulesOneBoundedNonOverlappingRunPerHour() { assertTrue(workflow.contains("cancel-in-progress: false")); assertTrue(workflow.contains("timeout-minutes: 50")); assertTrue(workflow.contains( - "timeout --signal=TERM --kill-after=30s 45m opencode github run" + "timeout --signal=TERM --kill-after=30s 45m opencode run" )); } - /** - * Verifies that repository source and the OpenCode executable are pinned by immutable - * content identifiers without retaining checkout credentials that a generated process could - * reuse implicitly. - */ + /** Verifies immutable checkout and OpenCode installation without persisted credentials. */ @Test void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { assertTrue(workflow.contains( @@ -98,16 +87,7 @@ void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { assertFalse(workflow.contains("anomalyco/opencode/github@")); } - /** - * Verifies that direct-token mode can actually create commits and push a feature branch. - * - *

OpenCode 1.18.13 intentionally skips its internal Git credential and author setup when - * {@code USE_GITHUB_TOKEN=true}. Because checkout credentials remain disabled, the workflow - * must install a repository-local GitHub CLI credential helper and local author identity - * before starting OpenCode, then remove the helper even when the process fails or times out. - * The helper reads the short-lived token from {@code GH_TOKEN}; no encoded token is written - * to Git configuration.

- */ + /** Verifies ephemeral Git credentials for branch pushes and deterministic cleanup. */ @Test void bootstrapsAndRemovesDirectTokenGitCredentials() { assertTrue(workflow.contains("GH_TOKEN: ${{ github.token }}")); @@ -131,151 +111,158 @@ void bootstrapsAndRemovesDirectTokenGitCredentials() { + "\"opencode-agent[bot]@users.noreply.github.com\"" )); assertFalse(workflow.contains("AUTHORIZATION: basic")); - assertFalse(workflow.contains("AGENT: build")); } - /** - * Verifies that the repository's NVIDIA NIM secret is the only model credential and is - * mapped to the environment variable documented by OpenCode's NVIDIA provider. - */ + /** Verifies NVIDIA NIM is the sole model credential and plain OpenCode owns no PR lifecycle. */ @Test - void usesOnlyTheNvidiaNimCredential() { + void usesOnlyNvidiaNimWithPlainOpenCodeRun() { assertEquals(Set.of("NVIDIA_NIM_API_KEY"), referencedSecrets()); assertTrue(workflow.contains( "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" )); - assertTrue(workflow.contains("SHARE: \"false\"")); - assertTrue(workflow.contains("USE_GITHUB_TOKEN: \"true\"")); - - String lowerCaseWorkflow = workflow.toLowerCase(Locale.ROOT); - assertFalse(lowerCaseWorkflow.contains("copilot")); + assertTrue(workflow.contains("MODEL: nvidia/deepseek-ai/deepseek-v4-pro")); + assertTrue(workflow.contains("opencode run --model \"${MODEL}\" --auto")); + assertFalse(workflow.contains("opencode github run")); + assertFalse(workflow.contains("USE_GITHUB_TOKEN")); + assertFalse(workflow.toLowerCase(Locale.ROOT).contains("copilot")); assertFalse(workflow.contains("ANTHROPIC_API_KEY")); assertFalse(workflow.contains("OPENAI_API_KEY")); } /** - * Requires a currently available free NVIDIA endpoint suited to repository-scale coding. - * - *

The previous Qwen3 Coder trial endpoint was deprecated by NVIDIA. DeepSeek V4 Pro is - * exposed by NVIDIA as a free endpoint with long-context coding and tool-use capabilities, so - * the workflow pins that provider/model identifier and fails instead of silently falling back.

+ * Proves the model has read-only pull-request authority and both PR-writing jobs are + * deterministic non-checkout jobs without the NVIDIA credential. */ @Test - void usesCurrentFreeAgenticCodingModel() { - assertTrue(workflow.contains("MODEL: nvidia/deepseek-ai/deepseek-v4-pro")); - assertFalse(workflow.contains("qwen/qwen3-coder-480b-a35b-instruct")); - } + void isolatesPullRequestAndActionsWriteAuthorityFromTheAgent() { + String maintenance = maintenanceJob(); + String publisher = publicationJob(); + String authorizer = authorizationJob(); - /** - * Verifies that the OpenCode process never receives Actions write authority. - * - *

The maintenance job owns only branch and pull-request preparation permissions. A separate - * job, which never checks out or executes repository code, receives the sole occurrence of - * {@code actions: write} needed to authorize approval-required exact-head workflow runs.

- */ - @Test - void isolatesActionsWriteFromTheAgentProcess() { assertTrue(workflow.contains("permissions:\n contents: read\n\njobs:")); - assertTrue(workflow.contains( - "maintain-repository:\n" - + " permissions:\n" - + " actions: read\n" - + " checks: read\n" - + " contents: write\n" - + " issues: write\n" - + " pull-requests: write\n" - + " security-events: read\n" - + " statuses: read" - )); - assertTrue(workflow.contains( - "authorize-exact-head-checks:\n" - + " needs: maintain-repository\n" - + " if: ${{ always() && !cancelled() }}\n" - + " permissions:\n" - + " actions: write\n" - + " contents: read\n" - + " pull-requests: read" - )); + assertTrue(maintenance.contains("actions: read")); + assertTrue(maintenance.contains("contents: write")); + assertTrue(maintenance.contains("pull-requests: read")); + assertFalse(maintenance.contains("pull-requests: write")); + assertFalse(maintenance.contains("actions: write")); + + assertTrue(publisher.contains("pull-requests: write")); + assertTrue(publisher.contains("contents: read")); + assertFalse(publisher.contains("actions/checkout@")); + assertFalse(publisher.contains("NVIDIA_API_KEY")); + assertFalse(publisher.contains("/reviews")); + assertFalse(publisher.contains("/merge")); + + assertTrue(authorizer.contains("actions: write")); + assertTrue(authorizer.contains("pull-requests: read")); + assertFalse(authorizer.contains("actions/checkout@")); + assertFalse(authorizer.contains("NVIDIA_API_KEY")); + assertFalse(authorizer.contains("pull-requests: write")); + assertEquals(1, countOccurrences(workflow, "actions: write")); assertEquals(1, countOccurrences(workflow, "contents: write")); - assertFalse(authorizationJob().contains("actions/checkout@")); - assertFalse(authorizationJob().contains("NVIDIA_API_KEY")); + assertEquals(1, countOccurrences(workflow, "pull-requests: write")); assertFalse(workflow.contains("id-token:")); assertFalse(workflow.contains("security-events: write")); } - /** - * Requires exact-head CI authorization after the agent creates or updates a same-repository PR. - * - *

GitHub prevents ordinary events produced with {@code GITHUB_TOKEN} from recursively - * starting workflows. Current GitHub behavior creates {@code opened}, {@code synchronize}, and - * {@code reopened} PR runs in an approval-required state instead. The trusted default-branch - * workflow must snapshot heads before the agent, detect only heads changed by this run, refuse - * every {@code .github} or CODEOWNERS policy change, bind every decision to the still-current - * SHA, and authorize only those exact-head runs in a separate non-checkout job. This starts - * validation; it does not approve or merge the pull request.

- */ + /** Verifies one strict branch or existing PR is selected before deterministic publication. */ @Test - void authorizesExactHeadChecksForAgentChangedPullRequests() { - assertTrue(workflow.contains("id: snapshot_heads")); - assertTrue(workflow.contains("open_pr_heads_before: ${{ steps.snapshot_heads.outputs.open_pr_heads }}")); - assertTrue(workflow.contains("open-pr-heads-before.json")); - assertTrue(workflow.contains("BEFORE_HEADS: ${{ needs.maintain-repository.outputs.open_pr_heads_before }}")); + void publishesOnlyOneValidatedAgentCandidate() { + assertTrue(workflow.contains( + "automation_branch_heads_before: " + + "${{ steps.snapshot_heads.outputs.automation_branch_heads }}" + )); assertTrue(workflow.contains( - "name: Authorize exact-head checks for agent-updated pull requests" + "agent_candidate: ${{ steps.detect_candidate.outputs.agent_candidate }}" )); - assertTrue(workflow.contains("head.repo.full_name == $repo")); - assertTrue(workflow.contains(".base.ref == \"develop\"")); + assertTrue(workflow.contains("automation/opencode-")); + assertTrue(workflow.contains("Multiple agent publication candidates were detected")); + assertTrue(workflow.contains("kind: \"existing_pr\"")); + assertTrue(workflow.contains("kind: \"new_branch\"")); + assertTrue(workflow.contains("draft: true")); assertTrue(workflow.contains("startswith(\".github/\")")); assertTrue(workflow.contains("CODEOWNERS")); - assertTrue(workflow.contains("head_sha=${expected_head}")); + assertTrue(workflow.contains("Agent branch is not ahead of develop")); + } + + /** + * Requires every workflow-run decision to bind the event, exact SHA, and associated pull + * request number before the isolated job can authorize a waiting run. + */ + @Test + void authorizesOnlyRunsAssociatedWithTheExactPullRequestHead() { + assertTrue(workflow.contains("required_workflow_names=")); assertTrue(workflow.contains("event=pull_request")); - assertTrue(workflow.contains("--arg expected_head \"${expected_head}\"")); + assertTrue(workflow.contains("head_sha=${expected_head}")); + assertTrue(workflow.contains("--argjson pull_request_number \"${number}\"")); + assertTrue(workflow.contains( + "any(.pull_requests[]?; .number == $pull_request_number)" + )); assertTrue(workflow.contains(".head_sha == $expected_head")); assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); - assertTrue(workflow.contains("current_head")); - assertTrue(workflow.contains("expected_head")); - assertTrue(workflow.contains("No pull-request workflow run materialized")); + assertTrue(workflow.contains("Missing required exact-head workflows")); + assertTrue(workflow.contains("PR #${number} moved")); assertFalse(workflow.contains("gh pr review --approve")); assertFalse(workflow.contains("/pulls/${number}/merge")); } - /** - * Verifies that the model prompt preserves independent review and deterministic merge - * authority instead of granting the development agent governance powers. - */ + /** Verifies the prompt itself mirrors the hard authority boundary and bounded branch contract. */ @Test - void promptForbidsReviewMergeAndProtectedBranchBypass() { + void promptForbidsPullRequestMutationMergeAndProtectedBranchPushes() { assertTrue(workflow.contains("Start every run by inspecting every open pull request")); assertTrue(workflow.contains("exact current head")); - assertTrue(workflow.contains("Never approve or merge a pull request")); + assertTrue(workflow.contains( + "Do not create, update, approve, close, or merge a pull request directly" + )); assertTrue(workflow.contains("Never push directly to develop or main")); + assertTrue(workflow.contains("exactly one automation/opencode-")); assertTrue(workflow.contains("Do not bypass branch protection")); assertTrue(workflow.contains("Do not alter the existing review agent")); assertTrue(workflow.contains("Do not change any review-agent secret name")); assertTrue(workflow.contains("Do not modify .github/workflows/")); assertTrue(workflow.contains("automation-maintenance")); - assertTrue(workflow.contains("Do not create a second development pull request")); assertTrue(workflow.contains("Do not print, echo, summarize, or expose secret values")); } + /** @return workflow text for the OpenCode execution job only */ + private static String maintenanceJob() { + return jobSection(" maintain-repository:", " publish-agent-pull-request:"); + } + + /** @return workflow text for the deterministic draft-PR publisher only */ + private static String publicationJob() { + return jobSection(" publish-agent-pull-request:", " authorize-exact-head-checks:"); + } + + /** @return workflow text for the exact-head workflow-run authorizer */ + private static String authorizationJob() { + int start = workflow.indexOf(" authorize-exact-head-checks:"); + assertTrue(start >= 0, "The isolated exact-head authorization job must exist"); + return workflow.substring(start); + } + /** - * Returns the text of the isolated workflow-run authorization job. + * Extracts one job section between two top-level job keys. * - * @return workflow suffix beginning at the authorization job + * @param startMarker first job marker + * @param endMarker following job marker + * @return exact workflow section */ - private static String authorizationJob() { - int jobStart = workflow.indexOf(" authorize-exact-head-checks:"); - assertTrue(jobStart >= 0, "The isolated exact-head authorization job must exist"); - return workflow.substring(jobStart); + private static String jobSection(String startMarker, String endMarker) { + int start = workflow.indexOf(startMarker); + int end = workflow.indexOf(endMarker); + assertTrue(start >= 0, "Missing workflow job: " + startMarker); + assertTrue(end > start, "Invalid workflow job order for: " + startMarker); + return workflow.substring(start, end); } /** - * Counts non-overlapping occurrences of one literal fragment. + * Counts non-overlapping literal occurrences. * - * @param text complete text to inspect + * @param text complete text * @param fragment non-empty literal fragment - * @return number of non-overlapping occurrences + * @return occurrence count */ private static int countOccurrences(String text, String fragment) { int count = 0; @@ -287,11 +274,7 @@ private static int countOccurrences(String text, String fragment) { return count; } - /** - * Extracts every repository-secret name referenced by the workflow. - * - * @return immutable set of referenced GitHub Actions secret identifiers - */ + /** @return immutable set of referenced repository-secret names */ private static Set referencedSecrets() { Matcher matcher = SECRET_REFERENCE.matcher(workflow); Set secretNames = new java.util.HashSet<>(); @@ -302,10 +285,9 @@ private static Set referencedSecrets() { } /** - * Finds the reactor root from either root or module-local Maven execution. + * Finds the repository root from either reactor-root or module-local execution. * - * @return absolute path that contains the root Maven project - * @throws IllegalStateException when no repository or Maven root can be found + * @return absolute repository root */ private static Path projectRoot() { Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); From e68949999359298a468db956d673c06ce2a20a89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 10:53:44 +0900 Subject: [PATCH 097/152] fix(ci): isolate agent PR authority and bind checks to PR --- .../workflows/hourly-opencode-maintenance.yml | 491 +++++++++++++----- 1 file changed, 357 insertions(+), 134 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index aa2a1f9c..af15e8d6 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -18,11 +18,14 @@ jobs: checks: read contents: write issues: write - pull-requests: write + pull-requests: read security-events: read statuses: read outputs: open_pr_heads_before: ${{ steps.snapshot_heads.outputs.open_pr_heads }} + automation_branch_heads_before: ${{ steps.snapshot_heads.outputs.automation_branch_heads }} + develop_head_before: ${{ steps.snapshot_heads.outputs.develop_head }} + agent_candidate: ${{ steps.detect_candidate.outputs.agent_candidate }} runs-on: ubuntu-latest timeout-minutes: 50 steps: @@ -45,7 +48,7 @@ jobs: command -v curl >/dev/null command -v install >/dev/null - command -v rg >/dev/null + command -v jq >/dev/null command -v sha256sum >/dev/null command -v tar >/dev/null @@ -78,49 +81,66 @@ jobs: test "$("${install_dir}/opencode" --version)" = "${OPENCODE_VERSION}" printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" - - name: Snapshot open pull-request heads + - name: Snapshot protected branch and publication candidates id: snapshot_heads shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - snapshot="${RUNNER_TEMP}/open-pr-heads-before.json" + repository="${GITHUB_REPOSITORY}" + open_pr_file="${RUNNER_TEMP}/open-pr-heads-before.json" + automation_branch_file="${RUNNER_TEMP}/automation-branch-heads-before.json" + + develop_head="$(gh api "/repos/${repository}/git/ref/heads/develop" --jq '.object.sha')" gh api --paginate \ - "/repos/${GITHUB_REPOSITORY}/pulls?state=open&base=develop&per_page=100" \ - | jq -s --arg repo "${GITHUB_REPOSITORY}" ' + "/repos/${repository}/pulls?state=open&base=develop&per_page=100" \ + | jq -s --arg repo "${repository}" ' (add // []) | map(select(.head.repo.full_name == $repo)) | map({key: (.number | tostring), value: .head.sha}) | from_entries - ' > "${snapshot}" - printf 'open_pr_heads=%s\n' "$(jq -c . "${snapshot}")" >> "${GITHUB_OUTPUT}" + ' > "${open_pr_file}" + gh api --paginate "/repos/${repository}/branches?per_page=100" \ + | jq -s ' + (add // []) + | map(select(.name | startswith("automation/opencode-"))) + | map({key: .name, value: .commit.sha}) + | from_entries + ' > "${automation_branch_file}" + + { + printf 'develop_head=%s\n' "${develop_head}" + printf 'open_pr_heads=%s\n' "$(jq -c . "${open_pr_file}")" + printf 'automation_branch_heads=%s\n' "$(jq -c . "${automation_branch_file}")" + } >> "${GITHUB_OUTPUT}" - name: Run bounded NVIDIA OpenCode maintenance + id: run_opencode + continue-on-error: true shell: bash env: GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} MODEL: nvidia/deepseek-ai/deepseek-v4-pro - SHARE: "false" - USE_GITHUB_TOKEN: "true" + OPENCODE_DISABLE_SHARE: "true" PROMPT: | Maintain ContextualWisdomLab/mightyETL toward defensible commercial and acquisition readiness. - Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only threads that the current exact head actually addresses. + Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only source defects; the deterministic publisher and independent reviewers own pull-request lifecycle operations. - If a development pull request is open, work only on the dependency-eligible exact current head of that pull request. Do not create a second development pull request while another development pull request is open. Implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and update the existing feature branch and pull request. Close only clearly superseded duplicate pull requests with a precise reason. + When one dependency-eligible development pull request exists, check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, commit, and push only that existing branch. Do not create another branch for the same work. - If no development pull request is open, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. Prefer issue #120 while its durable lease-fenced worker gap remains open and dependency-ready. + When no development pull request exists, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Create and push exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug branch. Do not create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. - Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services. Database objects must contain at least two descriptive words and use snake_case by default. Public production APIs require complete beginner-readable documentation. Added production statements and branches require deterministic coverage. Use current authoritative standards, primary technical documentation, and peer-reviewed evidence where material, with APA 7th references in repository documentation. + Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services. Database objects must contain at least two descriptive words and use snake_case by default. Public production APIs require complete beginner-readable documentation. Added production statements and branches require deterministic 100% statement and branch coverage. Use current authoritative standards, primary technical documentation, and peer-reviewed evidence where material, with APA 7th references in repository documentation. - Never approve or merge a pull request. Never push directly to develop or main. Do not bypass branch protection, independent approval, security gates, repository policy, required checks, or test coverage. Do not publish a release unless a separate release-authorized workflow and every acceptance gate explicitly permit it. + Do not create, update, approve, close, or merge a pull request directly. Never push directly to develop or main. Do not bypass branch protection, independent approval, security gates, repository policy, required checks, or test coverage. Do not publish a release unless a separate release-authorized workflow and every acceptance gate explicitly permit it. - Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect secret values. Do not print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, prepare a pull request and leave approval and merge to an independent gate. + Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect secret values. Do not print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, leave the policy change for explicit human workflow authorization. - Use the repository-scoped GitHub token only for the minimum branch, pull-request, issue, check, status, and security-read operations required by this run. Keep the working tree and pull-request description truthful about tests, skipped coverage, remaining risks, and release readiness. Leave one reviewable feature branch and pull request; never represent pending external gates as completed. + Use the repository-scoped token only for read operations and the one permitted feature-branch push. Keep the working tree truthful about tests, skipped coverage, remaining risks, and release readiness. The deterministic non-model publisher creates a draft pull request after validating the resulting branch. run: | set -euo pipefail if [[ -z "${NVIDIA_API_KEY}" ]]; then @@ -143,155 +163,358 @@ jobs: git config --local user.name "opencode-agent[bot]" git config --local user.email "opencode-agent[bot]@users.noreply.github.com" - timeout --signal=TERM --kill-after=30s 45m opencode github run + printf '%s\n' "${PROMPT}" \ + | timeout --signal=TERM --kill-after=30s 45m opencode run --model "${MODEL}" --auto - authorize-exact-head-checks: + - name: Detect exactly one agent publication candidate + id: detect_candidate + if: ${{ always() && !cancelled() }} + shell: bash + env: + GH_TOKEN: ${{ github.token }} + BEFORE_PR_HEADS: ${{ steps.snapshot_heads.outputs.open_pr_heads }} + BEFORE_AUTOMATION_BRANCH_HEADS: ${{ steps.snapshot_heads.outputs.automation_branch_heads }} + DEVELOP_HEAD_BEFORE: ${{ steps.snapshot_heads.outputs.develop_head }} + run: | + set -euo pipefail + repository="${GITHUB_REPOSITORY}" + after_pr_file="${RUNNER_TEMP}/open-pr-heads-after.json" + after_branch_file="${RUNNER_TEMP}/automation-branch-heads-after.json" + + jq -e 'select(type == "object")' >/dev/null <<<"${BEFORE_PR_HEADS}" + jq -e 'select(type == "object")' >/dev/null <<<"${BEFORE_AUTOMATION_BRANCH_HEADS}" + current_develop="$(gh api "/repos/${repository}/git/ref/heads/develop" --jq '.object.sha')" + if [[ "${current_develop}" != "${DEVELOP_HEAD_BEFORE}" ]]; then + echo "Protected develop moved during the model job; publication evidence is indeterminate" >&2 + exit 1 + fi + + gh api --paginate "/repos/${repository}/pulls?state=open&base=develop&per_page=100" \ + | jq -s '(add // [])' > "${after_pr_file}" + gh api --paginate "/repos/${repository}/branches?per_page=100" \ + | jq -s '(add // [])' > "${after_branch_file}" + + changed_existing="$( + jq -c \ + --arg repo "${repository}" \ + --argjson before "${BEFORE_PR_HEADS}" ' + [ + .[] + | select(.head.repo.full_name == $repo and .base.ref == "develop") + | (.number | tostring) as $number_key + | select($before[$number_key] != null and $before[$number_key] != .head.sha) + | { + kind: "existing_pr", + number: .number, + head_ref: .head.ref, + head_sha: .head.sha + } + ] + ' "${after_pr_file}" + )" + changed_existing_refs="$(jq -c '[.[].head_ref]' <<<"${changed_existing}")" + changed_automation="$( + jq -c \ + --argjson before "${BEFORE_AUTOMATION_BRANCH_HEADS}" \ + --argjson existing_refs "${changed_existing_refs}" ' + [ + .[] + | select(.name | startswith("automation/opencode-")) + | select(($before[.name] // null) != .commit.sha) + | select(($existing_refs | index(.name)) == null) + | select(.name | test("^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$")) + | { + kind: "new_branch", + head_ref: .name, + head_sha: .commit.sha + } + ] + ' "${after_branch_file}" + )" + candidates="$(jq -cn --argjson a "${changed_existing}" --argjson b "${changed_automation}" '$a + $b')" + candidate_count="$(jq 'length' <<<"${candidates}")" + + if [[ "${candidate_count}" -gt 1 ]]; then + echo "Multiple agent publication candidates were detected; refusing ambiguous publication" >&2 + jq . <<<"${candidates}" >&2 + exit 1 + fi + if [[ "${candidate_count}" -eq 0 ]]; then + printf 'agent_candidate=%s\n' '{"kind":"none"}' >> "${GITHUB_OUTPUT}" + exit 0 + fi + + candidate="$(jq -c '.[0]' <<<"${candidates}")" + printf 'agent_candidate=%s\n' "${candidate}" >> "${GITHUB_OUTPUT}" + + - name: Preserve an OpenCode execution failure after candidate capture + if: ${{ steps.run_opencode.outcome == 'failure' }} + shell: bash + run: | + echo "OpenCode failed after any reviewable branch evidence was captured" >&2 + exit 1 + + publish-agent-pull-request: needs: maintain-repository - if: ${{ always() && !cancelled() }} + if: ${{ always() && !cancelled() && needs.maintain-repository.outputs.agent_candidate != '' }} permissions: - actions: write contents: read - pull-requests: read + pull-requests: write + outputs: + pr_number: ${{ steps.publish.outputs.pr_number }} + head_ref: ${{ steps.publish.outputs.head_ref }} + head_sha: ${{ steps.publish.outputs.head_sha }} runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Authorize exact-head checks for agent-updated pull requests + - name: Publish one validated draft pull request or identify the updated pull request + id: publish shell: bash env: GH_TOKEN: ${{ github.token }} - BEFORE_HEADS: ${{ needs.maintain-repository.outputs.open_pr_heads_before }} + AGENT_CANDIDATE: ${{ needs.maintain-repository.outputs.agent_candidate }} run: | set -euo pipefail - before_file="${RUNNER_TEMP}/open-pr-heads-before.json" - after_file="${RUNNER_TEMP}/open-pr-heads-after.json" - changed_file="${RUNNER_TEMP}/agent-changed-pr-heads.jsonl" repository="${GITHUB_REPOSITORY}" - required_workflow_names='["CI","Dependency Review","SBOM (CycloneDX)","SAST Semgrep","Security Scan"]' - failed=0 + owner="${repository%%/*}" + base_branch="develop" + kind="$(jq -r '.kind' <<<"${AGENT_CANDIDATE}")" - if [[ -z "${BEFORE_HEADS}" ]]; then - echo "Open pull-request head snapshot is unavailable" >&2 + if [[ "${kind}" == "none" ]]; then + exit 0 + fi + + head_ref="$(jq -r '.head_ref' <<<"${AGENT_CANDIDATE}")" + expected_head="$(jq -r '.head_sha' <<<"${AGENT_CANDIDATE}")" + if ! [[ "${expected_head}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Agent candidate has an invalid head SHA" >&2 exit 1 fi - printf '%s\n' "${BEFORE_HEADS}" \ - | jq -e 'select(type == "object")' > "${before_file}" - gh api --paginate \ - "/repos/${repository}/pulls?state=open&base=develop&per_page=100" \ - | jq -s '(add // [])' > "${after_file}" - jq -c --arg repo "${repository}" --slurpfile before "${before_file}" ' - .[] - | select(.head.repo.full_name == $repo) - | select(.base.ref == "develop") - | (.number | tostring) as $number_key - | ($before[0][$number_key] // null) as $previous_head - | select($previous_head != .head.sha) - | { - number: .number, - head_ref: .head.ref, - head_sha: .head.sha - } - ' "${after_file}" > "${changed_file}" - - while IFS= read -r changed_pull_request; do - [[ -n "${changed_pull_request}" ]] || continue - number="$(jq -r '.number' <<<"${changed_pull_request}")" - head_ref="$(jq -r '.head_ref' <<<"${changed_pull_request}")" - expected_head="$(jq -r '.head_sha' <<<"${changed_pull_request}")" - - changed_files="$( - gh api --paginate "/repos/${repository}/pulls/${number}/files?per_page=100" \ - | jq -s '(add // []) | map(.filename)' - )" + if [[ "${kind}" == "existing_pr" ]]; then + number="$(jq -r '.number' <<<"${AGENT_CANDIDATE}")" + live="$(gh api "/repos/${repository}/pulls/${number}")" + jq -e \ + --arg repo "${repository}" \ + --arg base "${base_branch}" \ + --arg head_ref "${head_ref}" \ + --arg head_sha "${expected_head}" ' + select( + .state == "open" + and .base.repo.full_name == $repo + and .head.repo.full_name == $repo + and .base.ref == $base + and .head.ref == $head_ref + and .head.sha == $head_sha + ) + ' >/dev/null <<<"${live}" + elif [[ "${kind}" == "new_branch" ]]; then + if ! [[ "${head_ref}" =~ ^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$ ]]; then + echo "Agent branch does not match the strict publication namespace" >&2 + exit 1 + fi + encoded_ref="$(jq -rn --arg value "${head_ref}" '$value | @uri')" + live_head="$(gh api "/repos/${repository}/branches/${encoded_ref}" --jq '.commit.sha')" + if [[ "${live_head}" != "${expected_head}" ]]; then + echo "Agent branch moved before deterministic publication" >&2 + exit 1 + fi + + comparison="$(gh api "/repos/${repository}/compare/${base_branch}...${expected_head}")" + ahead_by="$(jq -r '.ahead_by' <<<"${comparison}")" + if [[ "${ahead_by}" -lt 1 ]]; then + echo "Agent branch is not ahead of develop" >&2 + exit 1 + fi + file_count="$(jq '.files | length' <<<"${comparison}")" + if [[ "${file_count}" -gt 50 ]]; then + echo "Agent branch exceeds the bounded 50-file publication limit" >&2 + exit 1 + fi if jq -e ' any( + .files[].filename; startswith(".github/") or . == "CODEOWNERS" or endswith("/CODEOWNERS") ) - ' >/dev/null <<<"${changed_files}"; then - echo "PR #${number} changes .github or CODEOWNERS policy; exact-head runs require manual authorization" >&2 - failed=1 - continue + ' >/dev/null <<<"${comparison}"; then + echo "Agent branch changes .github or CODEOWNERS policy and requires manual handling" >&2 + exit 1 fi - current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" - if [[ "${current_head}" != "${expected_head}" ]]; then - echo "PR #${number} moved before workflow-run discovery" >&2 - failed=1 - continue - fi - - runs_file="${RUNNER_TEMP}/pr-${number}-exact-head-runs.json" - observed_workflow_names='[]' - missing_workflow_names="${required_workflow_names}" - missing_workflow_count="$(jq 'length' <<<"${missing_workflow_names}")" - - for _ in $(seq 1 18); do - current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" - if [[ "${current_head}" != "${expected_head}" ]]; then - echo "PR #${number} moved while workflow runs were materializing" >&2 - failed=1 - break + existing="$( + gh api --method GET "/repos/${repository}/pulls" \ + -f state=open \ + -f base="${base_branch}" \ + -f head="${owner}:${head_ref}" + )" + if [[ "$(jq 'length' <<<"${existing}")" -gt 0 ]]; then + number="$(jq -r '.[0].number' <<<"${existing}")" + else + title="$(gh api "/repos/${repository}/commits/${expected_head}" --jq '.commit.message | split("\n")[0]')" + title="${title//$'\r'/}" + if [[ -z "${title}" ]]; then + title="chore: bounded OpenCode maintenance" fi + case "${title}" in + feat:*|fix:*|docs:*|test:*|refactor:*|perf:*|ci:*|chore:*|build:*|revert:*) ;; + *) title="chore: ${title}" ;; + esac + title="${title:0:180}" + body="$(printf '%s\n' \ + "## Automated bounded development slice" \ + "" \ + "This draft pull request was published by a deterministic non-model job after OpenCode pushed exactly one validated branch." \ + "" \ + "- Exact head: \`${expected_head}\`" \ + "- Base: \`${base_branch}\`" \ + "- Model credential: repository \`NVIDIA_NIM_API_KEY\` mapped only inside the preceding OpenCode job" \ + "- Pull-request approval and merge: explicitly outside the development agent's authority" \ + "" \ + "All tests, security checks, review threads, independent exact-head approvals, documentation, and release gates remain required. Pending or absent evidence is not passing." \ + )" + payload="$(jq -cn \ + --arg title "${title}" \ + --arg head "${head_ref}" \ + --arg base "${base_branch}" \ + --arg body "${body}" \ + '{title: $title, head: $head, base: $base, body: $body, draft: true}')" + created="$(gh api --method POST "/repos/${repository}/pulls" --input - <<<"${payload}")" + number="$(jq -r '.number' <<<"${created}")" + fi + else + echo "Unknown agent candidate kind: ${kind}" >&2 + exit 1 + fi - gh api --paginate \ - "/repos/${repository}/actions/runs?event=pull_request&head_sha=${expected_head}&per_page=100" \ - | jq -s '(map(.workflow_runs) | add) // []' > "${runs_file}" + live="$(gh api "/repos/${repository}/pulls/${number}")" + live_head="$(jq -r '.head.sha' <<<"${live}")" + if [[ "${live_head}" != "${expected_head}" ]]; then + echo "Published pull request does not retain the expected exact head" >&2 + exit 1 + fi + { + printf 'pr_number=%s\n' "${number}" + printf 'head_ref=%s\n' "${head_ref}" + printf 'head_sha=%s\n' "${expected_head}" + } >> "${GITHUB_OUTPUT}" - while IFS= read -r run_id; do - [[ -n "${run_id}" ]] || continue - gh api --method POST \ - "/repos/${repository}/actions/runs/${run_id}/approve" - done < <( - jq -r --arg expected_head "${expected_head}" ' - .[] - | select( - .head_sha == $expected_head - and ( - .conclusion == "action_required" - or .status == "waiting" - ) - ) - | .id - ' "${runs_file}" + authorize-exact-head-checks: + needs: + - maintain-repository + - publish-agent-pull-request + if: ${{ always() && !cancelled() && needs.publish-agent-pull-request.result == 'success' && needs.publish-agent-pull-request.outputs.pr_number != '' }} + permissions: + actions: write + contents: read + pull-requests: read + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Authorize exact-head checks for the published or updated pull request + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ needs.publish-agent-pull-request.outputs.pr_number }} + HEAD_REF: ${{ needs.publish-agent-pull-request.outputs.head_ref }} + EXPECTED_HEAD: ${{ needs.publish-agent-pull-request.outputs.head_sha }} + run: | + set -euo pipefail + repository="${GITHUB_REPOSITORY}" + number="${PR_NUMBER}" + head_ref="${HEAD_REF}" + expected_head="${EXPECTED_HEAD}" + required_workflow_names='["CI","Dependency Review","SBOM (CycloneDX)","SAST Semgrep","Security Scan"]' + all_runs_file="${RUNNER_TEMP}/pr-${number}-all-exact-head-runs.json" + runs_file="${RUNNER_TEMP}/pr-${number}-associated-exact-head-runs.json" + + live="$(gh api "/repos/${repository}/pulls/${number}")" + jq -e \ + --arg repo "${repository}" \ + --arg head_ref "${head_ref}" \ + --arg head_sha "${expected_head}" ' + select( + .state == "open" + and .head.repo.full_name == $repo + and .base.ref == "develop" + and .head.ref == $head_ref + and .head.sha == $head_sha ) + ' >/dev/null <<<"${live}" + + changed_files="$( + gh api --paginate "/repos/${repository}/pulls/${number}/files?per_page=100" \ + | jq -s '(add // []) | map(.filename)' + )" + if jq -e ' + any( + startswith(".github/") + or . == "CODEOWNERS" + or endswith("/CODEOWNERS") + ) + ' >/dev/null <<<"${changed_files}"; then + echo "PR #${number} changes .github or CODEOWNERS policy; workflow runs require human authorization" >&2 + exit 1 + fi - observed_workflow_names="$(jq -c '[.[].name] | unique' "${runs_file}")" - missing_workflow_names="$( - jq -cn \ - --argjson required "${required_workflow_names}" \ - --argjson observed "${observed_workflow_names}" \ - '$required - $observed' - )" - missing_workflow_count="$(jq 'length' <<<"${missing_workflow_names}")" - if [[ "${missing_workflow_count}" -eq 0 ]]; then - echo "All required exact-head pull-request workflows materialized for PR #${number} at ${expected_head}" - break - fi - sleep 5 - done - - if [[ "${failed}" -ne 0 ]]; then - continue - fi - if [[ "${missing_workflow_count}" -gt 0 ]]; then - if [[ "$(jq 'length' <<<"${observed_workflow_names}")" -eq 0 ]]; then - echo "No pull-request workflow run materialized for PR #${number} exact head ${expected_head}" >&2 - fi - echo "Required exact-head pull-request workflows did not materialize for PR #${number}: ${missing_workflow_names}" >&2 - failed=1 - continue - fi - + missing_workflow_names="${required_workflow_names}" + for _ in $(seq 1 18); do current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" if [[ "${current_head}" != "${expected_head}" ]]; then - echo "PR #${number} moved before workflow-run authorization completed" >&2 - failed=1 - continue + echo "PR #${number} moved while exact-head workflow runs were materializing" >&2 + exit 1 + fi + + gh api --paginate \ + "/repos/${repository}/actions/runs?event=pull_request&head_sha=${expected_head}&per_page=100" \ + | jq -s '(map(.workflow_runs) | add) // []' > "${all_runs_file}" + jq -c \ + --arg expected_head "${expected_head}" \ + --argjson pull_request_number "${number}" ' + [ + .[] + | select(.head_sha == $expected_head) + | select(any(.pull_requests[]?; .number == $pull_request_number)) + ] + ' "${all_runs_file}" > "${runs_file}" + + while IFS= read -r run_id; do + [[ -n "${run_id}" ]] || continue + gh api --method POST "/repos/${repository}/actions/runs/${run_id}/approve" + done < <( + jq -r ' + .[] + | select( + .conclusion == "action_required" + or .status == "waiting" + ) + | .id + ' "${runs_file}" + ) + + observed_workflow_names="$(jq -c '[.[].name] | unique' "${runs_file}")" + missing_workflow_names="$( + jq -cn \ + --argjson required "${required_workflow_names}" \ + --argjson observed "${observed_workflow_names}" \ + '$required - $observed' + )" + if [[ "$(jq 'length' <<<"${missing_workflow_names}")" -eq 0 ]]; then + break fi + sleep 5 + done - echo "Authorized exact-head pull-request checks for PR #${number} branch ${head_ref} at ${expected_head}" - done < "${changed_file}" + if [[ "$(jq 'length' <<<"${missing_workflow_names}")" -gt 0 ]]; then + echo "Missing required exact-head workflows for PR #${number}: ${missing_workflow_names}" >&2 + exit 1 + fi + + current_head="$(gh api "/repos/${repository}/pulls/${number}" --jq '.head.sha')" + if [[ "${current_head}" != "${expected_head}" ]]; then + echo "PR #${number} moved before workflow-run authorization completed" >&2 + exit 1 + fi - exit "${failed}" + echo "Authorized exact-head pull-request checks for PR #${number} branch ${head_ref} at ${expected_head}" From c115a4a90423be271d72a73d5fa2a01a0e43c5d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 10:57:10 +0900 Subject: [PATCH 098/152] docs(ci): record model and PR authority separation --- .../operations/hourly-opencode-maintenance.md | 242 ++++++++---------- 1 file changed, 108 insertions(+), 134 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 2654f836..6d72a119 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -2,35 +2,35 @@ ## Purpose -`.github/workflows/hourly-opencode-maintenance.yml` runs at minute 43 of every hour, in UTC. It uses a checksum-pinned OpenCode 1.18.13 executable and the repository's existing `NVIDIA_NIM_API_KEY` to repair one dependency-eligible development pull request or prepare one bounded buyer-visible product improvement. +`.github/workflows/hourly-opencode-maintenance.yml` runs at minute 43 of every UTC hour. It uses a checksum-pinned OpenCode 1.18.13 executable and the existing `NVIDIA_NIM_API_KEY` repository secret to repair one dependency-eligible development branch or prepare one bounded buyer-visible improvement. -The workflow is separate from independent review and deterministic merge disposition. It never approves or merges a pull request, pushes to `develop` or `main`, weakens branch protection, changes the review agent's credential path, or publishes a release. +The workflow is not a reviewer or merger. Independent review, required checks, branch protection, the central CWL review workflows, and deterministic expected-head merge disposition remain authoritative. -## Required secret and model +## Credential and model boundary -The only model secret referenced by this workflow is: +The only model secret referenced by the workflow is: ```text NVIDIA_NIM_API_KEY ``` -The OpenCode step maps it to: +It is mapped only inside the model-execution step to: ```text NVIDIA_API_KEY ``` -A step-level environment variable is visible to that step's Bash shell and every child process, including OpenCode. The workflow has no GitHub Copilot, Anthropic, OpenAI, partner-only NVIDIA, or automatic model fallback. A missing secret fails before the agent starts. +The selected model is `nvidia/deepseek-ai/deepseek-v4-pro`. There is no GitHub Copilot credential, `COPILOT_GITHUB_TOKEN`, Anthropic or OpenAI key, partner-only endpoint, or automatic provider fallback. A missing NVIDIA credential fails before model execution. -The pinned provider/model is: +The workflow invokes the plain non-interactive command: ```text -nvidia/deepseek-ai/deepseek-v4-pro +opencode run --model "${MODEL}" --auto ``` -Current model-selection evidence and replacement rules are recorded in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`. +It deliberately does not invoke `opencode github run`. OpenCode's GitHub schedule handler can create a pull request itself, which would require giving the model process coarse `pull-requests: write` authority. Plain `opencode run` lets the model edit, test, commit, and push one branch while deterministic non-model jobs own publication and workflow-run authorization. -## Immutable execution contract +## Immutable OpenCode installation | Control | Value | | --- | --- | @@ -40,58 +40,81 @@ Current model-selection evidence and replacement rules are recorded in `docs/doc | SHA-256 | `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` | | Archive shape | exactly one regular-file member named `opencode` | | Checkout | full-SHA-pinned `actions/checkout` | -| OpenCode timeout | `TERM` at 45 minutes, `KILL` after 30 seconds | +| OpenCode timeout | `TERM` after 45 minutes; `KILL` after 30 seconds | | Job timeout | 50 minutes | -| Overlap | serialized, active run not cancelled | -| Session sharing | disabled | +| Overlap | serialized; an active run is not cancelled | -The installer downloads only the immutable GitHub release asset over HTTPS, verifies its checksum, requires exactly one member, requires GNU tar's regular-file type before extraction, extracts into a fresh mode-`0700` directory without restoring archive ownership or permissions, refuses overwrite, rejects symbolic links and non-regular output, and verifies the exact executable version. It does not use npm installation, a floating package version, or a mutable OpenCode action reference. Detailed evidence is in `docs/doctoring/opencode-archive-extraction-evidence.md`. +The installer verifies the immutable archive checksum, member count, member name, GNU tar regular-file type, private mode-`0700` extraction directory, overwrite refusal, non-symbolic-link output, and exact executable version before running OpenCode. Checkout keeps `persist-credentials: false`. -## Direct-token Git bootstrap +## Three-job authority separation -`USE_GITHUB_TOKEN=true` makes OpenCode use the repository-scoped token rather than an OpenCode App or OIDC token. Checkout keeps `persist-credentials: false`. Before OpenCode starts, the maintenance job: +```mermaid +flowchart LR + A[maintain-repository
model + checked-out source] -->|candidate JSON only| B[publish-agent-pull-request
no checkout, no model secret] + B -->|PR number + exact SHA| C[authorize-exact-head-checks
no checkout, no model secret] + C --> D[CI and security checks] + D --> E[independent review and merge disposition] +``` -1. requires `GITHUB_TOKEN` and `GH_TOKEN`; -2. removes inherited repository-local GitHub credential helpers; -3. installs a repository-local `!gh auth git-credential` helper; -4. sets the local author to `opencode-agent[bot]`; -5. registers an `EXIT` trap that removes the helper after success, failure, or timeout. +### `maintain-repository` -No plaintext or encoded token is stored in Git configuration. No personal token or additional repository secret is introduced. +This is the only job that executes checked-out repository code and OpenCode. Its pull-request permission is read-only: -## Split-job permission model +```text +actions: read +checks: read +contents: write +issues: write +pull-requests: read +security-events: read +statuses: read +``` -The workflow-level permission is only `contents: read`. +`contents: write` permits one feature-branch push. It does not grant pull-request review or merge endpoints. The model prompt additionally forbids direct pull-request creation, update, approval, closure, or merge, but the permission map—not the prompt—is the primary authorization boundary. -### `maintain-repository` +Before OpenCode starts, the job snapshots: -This job checks out and executes repository and agent code. It receives: +- the `develop` head; +- every same-repository open pull request targeting `develop` and its exact head; +- every existing `automation/opencode-*` branch and its exact head. -- `actions: read` -- `checks: read` -- `contents: write` -- `issues: write` -- `pull-requests: write` -- `security-events: read` -- `statuses: read` +After OpenCode exits, the job requires `develop` to remain unchanged and selects at most one candidate: -It does **not** receive Actions write permission. Therefore OpenCode, generated code, and checked-out repository scripts cannot authorize workflow runs. +- one existing pull request whose head moved; or +- one new or advanced branch matching `automation/opencode-YYYYMMDDTHHMMSSZ-short-slug`. -### `authorize-exact-head-checks` +Multiple candidates fail closed. No candidate is represented as a no-op, not as successful product development. -This separate job receives: +### `publish-agent-pull-request` + +This job has `contents: read` and the workflow's sole `pull-requests: write` grant. It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. + +For an existing pull request, it only re-reads and validates repository, base branch, head branch, state, and exact SHA. For a new branch, it additionally requires: + +- the strict `automation/opencode-*` namespace; +- the live branch SHA to equal the candidate SHA; +- at least one commit ahead of `develop`; +- no more than 50 changed files; +- no `.github/**` or `CODEOWNERS` change; +- a same-repository branch and `develop` base. + +It then creates one draft pull request through a fixed script-generated payload. Commit text may supply the title, but no untrusted source is executed. The job contains no review submission or merge endpoint. + +### `authorize-exact-head-checks` -- `actions: write` -- `contents: read` -- `pull-requests: read` +This job has `actions: write`, `contents: read`, and `pull-requests: read`. It never checks out repository code and never receives the model credential. Its only write operation is approval of GitHub Actions workflow runs that GitHub has placed in `action_required` or `waiting` state. -It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. Its only write operation is the GitHub workflow-run approval endpoint for an approval-required run bound to a verified exact head. It contains no pull-request review approval or merge operation. +Every eligible run must satisfy all of the following: -## Why exact-head run authorization is required +1. event is `pull_request`; +2. `head_sha` equals the publisher's exact SHA; +3. the run's `pull_requests` association contains the exact pull-request number; +4. the pull request still has the expected repository, base, head branch, and SHA; +5. the pull request does not change `.github/**` or `CODEOWNERS`. -GitHub prevents most events created with `GITHUB_TOKEN` from recursively triggering workflows. Pull-request runs created after an Actions-authored open or synchronize event can remain approval-required. Without explicit authorization, an agent could push a valid repair whose exact head never receives CI, dependency review, SBOM, SAST, or security scans. +The pull-request association check matters because two pull requests can reference the same commit SHA. SHA-only filtering could authorize another pull request's waiting workflow. -GitHub materializes those workflows asynchronously. Seeing and authorizing only the first run is not sufficient: later workflows can appear after the authorization step exits and remain waiting forever. The isolated job therefore requires this complete workflow-name set for a direct `develop` pull request: +The job repeatedly discovers and authorizes runs until all of the following names materialize or the bounded wait expires: ```text CI @@ -101,125 +124,76 @@ SAST Semgrep Security Scan ``` -These names prove only that the runs exist for the head. They do not make a run successful and do not replace the named check requirements enforced by merge disposition. +Run authorization starts validation only. It does not make a check successful, approve the pull request, or permit merge. -The maintenance job snapshots same-repository pull requests targeting `develop` before OpenCode starts and exports only the compact pull-request-number-to-head-SHA map. The isolated authorization job runs after the maintenance job succeeds or fails without cancellation and: +## Agent development contract -1. requires the snapshot to exist and parse as a JSON object; -2. enumerates the same pull-request set after the agent run; -3. selects only a new pull request or a head changed by that run; -4. refuses automatic authorization for any `.github/**` or `CODEOWNERS` change; -5. verifies that the current head equals the expected SHA; -6. repeatedly discovers only `pull_request` workflow runs for that exact SHA; -7. authorizes every visible run still in `action_required` or `waiting` state on each pass; -8. compares observed names with the complete five-workflow set; -9. continues bounded discovery until all five materialize; -10. fails if no run appears or any required workflow remains missing; -11. verifies the head again immediately before authorization is declared complete. +When an eligible pull request exists, OpenCode may update only that same-repository head branch. When none exists, it may create exactly one strict `automation/opencode-*` branch. It must work test-first, preserve 100% configured production statement and branch coverage, add beginner-readable public documentation, update `CHANGELOG.md`, use descriptive multi-word `snake_case` database names, and record current primary standards or peer-reviewed evidence in APA 7th form where material. -The expected SHA and workflow-name arrays are passed to `jq` as data, not interpolated into jq source. Authorization only starts validation. Every check must still complete successfully, all review threads must be resolved, a non-author approval must be anchored to the same head, and branch protection and expected-head merge disposition must still permit merge. +The model must not: -Test-first, least-privilege, complete-materialization, time-of-check/time-of-use, and rollback evidence is recorded in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. - -## Agent authority boundary - -The agent may inspect open pull requests and their exact heads, fix one dependency-eligible development pull request, or create one bounded product pull request when no development pull request exists. It must use current authoritative standards and primary documentation, add APA 7th references where material, preserve modular MSA operation, use descriptive multiword `snake_case` database names, add beginner-readable production documentation, maintain deterministic statement and branch coverage, update `CHANGELOG.md`, and report incomplete gates truthfully. +- mutate pull-request lifecycle state; +- push to `develop` or `main`; +- bypass checks, reviews, security gates, or branch protection; +- modify the existing review agent or its credential names; +- inspect or disclose secret values; +- modify `.github/**` or `CODEOWNERS` without a specifically authorized automation-maintenance issue; +- publish a release. -The agent must not: +## Failure behavior -- approve or merge a pull request; -- push directly to protected branches; -- treat pending, absent, cancelled, skipped-required, stale, neutral-required, or failed checks as passing; -- bypass review, security, coverage, or branch-protection policy; -- inspect or disclose secret values; -- change the existing review agent, its provider, workflow, credential flow, or secret names; -- create a second development pull request while one is dependency-eligible; -- modify workflow policy without a specifically authorized `automation-maintenance` issue; -- publish a release without a separately authorized release workflow and complete acceptance evidence. - -Even with an authorized automation issue, `.github/**` and `CODEOWNERS` changes are excluded from automatic run authorization and require human action. - -## Normal sequence - -1. GitHub starts the scheduled workflow from protected default-branch source. -2. OpenCode is installed from the immutable checksum-pinned archive. -3. The maintenance job snapshots current same-repository `develop` pull-request heads. -4. The NVIDIA and GitHub credential boundaries are validated and the removable Git helper is installed. -5. OpenCode reviews the current queue, executes one bounded test-first change, and leaves a feature branch or pull request. -6. The credential cleanup trap removes the local helper. -7. The isolated authorization job compares before and after heads. -8. Policy-changing pull requests are rejected from automatic run authorization. -9. Exact-head approval-required runs are repeatedly authorized while all five required workflow names materialize. -10. CI, security, coverage, independent review, and merge disposition operate separately. - -## Failure handling - -| Failure | Result | Required response | -| --- | --- | --- | -| NVIDIA secret missing | Fail before OpenCode | Restore `NVIDIA_NIM_API_KEY`; add no fallback | -| Repository token or Git helper unavailable | Fail visibly | Preserve `persist-credentials: false`; inspect runner tooling | -| Archive unavailable, checksum mismatch, unexpected member/type, or version mismatch | Fail before execution | Treat as supply-chain review; never relax the pin silently | -| Model endpoint unavailable or deprecated | Fail without fallback | Research a current NVIDIA endpoint and submit a reviewed test-first change | -| OpenCode exceeds 45 minutes | TERM then KILL; cleanup trap executes | Reduce slice size and inspect partial branch state | -| Pre-agent head output missing or invalid | Authorization job fails | Never authorize from an unknown baseline | -| Agent changes `.github/**` or `CODEOWNERS` | Automatic authorization refused | Require explicit human workflow-run authorization | -| Head moves during discovery or authorization | Authorization refused | Re-evaluate the new exact head | -| No exact-head run materializes | Workflow fails | Diagnose event and Actions policy; do not merge the head | -| A required workflow name remains absent | Workflow fails and lists missing names | Diagnose trigger filters or renamed workflows; update the contract only through review | -| Workflow-run approval is rejected | Workflow fails | Verify repository policy and token permissions; add no personal-token workaround | -| Checks or review fail | Pull request remains blocked | Fix the exact head without weakening the gate | -| A prior hourly run remains active | New run waits | Investigate only if the prior run is stuck | - -A failed run can leave a reviewable feature branch or pull request, but it cannot claim successful validation, approval, merge, or release. +| Failure | Result | +| --- | --- | +| NVIDIA credential missing | fail before model execution | +| OpenCode archive or checksum mismatch | fail before extraction or execution | +| protected `develop` head moves during the run | fail as indeterminate publication evidence | +| more than one candidate branch or PR changes | fail as ambiguous model output | +| candidate branch is not ahead of `develop` | refuse publication | +| candidate changes `.github/**` or `CODEOWNERS` | require explicit human handling | +| live PR or branch SHA differs from the captured SHA | fail closed | +| workflow run lacks exact PR association | exclude it from authorization | +| required workflow name never materializes | fail and list missing names | +| exact PR head moves during discovery | fail and require re-evaluation | +| any check or independent review fails | leave the PR unmerged | + +A failed OpenCode step is preserved as a failed job after candidate evidence is captured. A deterministic publisher may still expose a valid partial branch as a draft for review, but the workflow never calls that a successful maintenance run. ## Rollback -Disable **Hourly OpenCode maintenance** to stop execution immediately. Permanent rollback must revert the workflow, contract tests, operations document, doctoring evidence, plan/design documents, and `CHANGELOG.md` through a reviewed pull request. +Disable **Hourly OpenCode maintenance** to stop the schedule immediately. Permanent rollback must revert the workflow, contract tests, this operations document, doctoring evidence, design and plan records, and corresponding changelog material through an independently reviewed pull request. -Do not remove exact-head authorization while retaining agent writes through `GITHUB_TOKEN`, and do not move `actions: write` into the OpenCode job. A GitHub App replacement requires separately reviewed evidence for installation permissions, recursive triggers, actor identity, secret lifecycle, complete exact-head validation, and independent review. +Do not restore `pull-requests: write` to the model job. Do not remove exact pull-request association from workflow-run authorization. A replacement GitHub App or token broker requires separate evidence for endpoint-level capability, actor identity, secret lifecycle, exact-head binding, and independent review. ## Verification checklist -Before merge, verify on the exact current head: +Before merge, verify on the exact head: -- Ubuntu, macOS, and Windows CI succeeded; -- dependency review, SBOM, Semgrep, Trivy, OSV, Scorecard, and required security gates succeeded; -- no current unresolved review thread or requested change remains; -- a non-author approval is anchored to the exact head; -- required workflow-change labels are present; +- Ubuntu, macOS, and Windows CI succeed; +- dependency review, SBOM, Semgrep, Trivy, OSV, Scorecard, and required security gates succeed; +- all current review threads are resolved; +- a non-author approval is anchored to the exact current SHA; - only `NVIDIA_NIM_API_KEY` is referenced as a model secret; -- the current NVIDIA endpoint and immutable OpenCode pin remain valid; -- archive member, type, private extraction, overwrite, output-type, and version checks remain intact; -- OpenCode retains no Actions write authority; -- the isolated authorization job is the only holder of `actions: write` and performs no checkout; -- the before/after head output, `.github/**` and `CODEOWNERS` exclusion, exact-head run filter, double head check, and visible absent-run failure remain intact; -- all five required pull-request workflows must materialize before the authorization job reports completion; -- the development workflow contains no review approval, merge, protected-branch push, fallback credential, or review-agent modification. +- the immutable OpenCode archive and action pins remain unchanged; +- the model job has `pull-requests: read`, not write; +- the publisher is the only holder of `pull-requests: write` and performs no checkout; +- the authorizer is the only holder of `actions: write` and performs no checkout; +- every authorized run is associated with the exact pull-request number and SHA; +- the workflow contains no pull-request review or merge operation. ## References — APA 7th Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 - -Anomaly. (2026). *GitHub integration*. OpenCode. https://opencode.ai/docs/github/ +Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts -Anomaly. (2026). *Providers*. OpenCode. https://opencode.ai/docs/providers/ +Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html -Free Software Foundation. (2026). *timeout: Run a command with a time limit*. GNU Coreutils 9.11. https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html - GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token -GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential - GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions -GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/triggering-a-workflow - NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro - -NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro From 8d34b7c6b62acbb2d7d6f4c5ed8355e4d6263a00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 10:58:52 +0900 Subject: [PATCH 099/152] docs(doctoring): prove PR authority and run association boundaries --- ...exact-head-check-authorization-evidence.md | 280 +++++++++--------- 1 file changed, 133 insertions(+), 147 deletions(-) diff --git a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md index 96b6983a..221f4546 100644 --- a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md +++ b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md @@ -1,64 +1,105 @@ -# GitHub-token exact-head check authorization evidence +# GitHub-token publication and exact-head authorization evidence ## Decision -The hourly development workflow uses the repository-scoped `GITHUB_TOKEN` rather than a personal -access token or GitHub App installation token. When OpenCode creates or updates a same-repository -pull request with that token, mightyETL must explicitly authorize the resulting approval-required -pull-request workflow runs before treating the agent's work as ready for review. +The hourly development loop uses the repository-scoped `GITHUB_TOKEN`, but it does not expose the same GitHub authority to the model, the pull-request publisher, and the workflow-run authorizer. -Authorization is limited to starting validation for the exact current head. It is not pull-request -approval, review, merge authority, branch-protection bypass, or evidence that any check succeeded. -The OpenCode process itself never receives Actions write authority. +The deployed contract is: -## Platform behavior +```text +model execution and branch push + ≠ +draft pull-request publication + ≠ +workflow-run authorization + ≠ +independent review and merge +``` + +This separation addresses two concrete failure modes found during exact-head review: + +1. `pull-requests: write` on the OpenCode job allowed the model process to call review and merge endpoints even when the prompt prohibited it. +2. authorizing workflow runs by `head_sha` alone could authorize a run associated with another pull request that referenced the same commit. + +## Primary-source finding + +OpenCode 1.18.13 exposes two relevant execution paths. + +- `opencode github run` includes GitHub lifecycle behavior and can call the pull-request creation endpoint after committing and pushing scheduled work. +- `opencode run` is the plain non-interactive model runner and does not itself own GitHub pull-request publication. + +mightyETL therefore uses `opencode run --model ... --auto` under a read-only pull-request token. Deterministic jobs validate and publish the resulting branch separately. + +## Authority topology + +```mermaid +flowchart TB + M[maintain-repository] -->|candidate JSON| P[publish-agent-pull-request] + P -->|PR number, head ref, exact SHA| A[authorize-exact-head-checks] + A --> C[required CI and security workflows] + C --> R[independent OpenCode and Noema reviews] + R --> D[expected-head merge disposition] +``` + +### Model job + +`maintain-repository` is the only job that checks out source or runs OpenCode. It has: + +```text +actions: read +checks: read +contents: write +issues: write +pull-requests: read +security-events: read +statuses: read +``` + +The model can inspect pull requests and push one branch. It cannot create, update, approve, close, or merge a pull request through the token. The prompt prohibition remains defense in depth rather than the primary authorization boundary. + +### Deterministic publisher -GitHub prevents most events created with `GITHUB_TOKEN` from recursively starting another workflow. -For pull requests opened, synchronized, reopened, or updated through GitHub Actions, GitHub creates -pull-request workflow runs in an approval-required state rather than granting an automated writer an -unreviewed recursive execution path. The workflow-run approval endpoint requires Actions write -permission. +`publish-agent-pull-request` has: -Without a bounded authorization step, a scheduled OpenCode run could push a valid fix while leaving -the new exact head without CI, SAST, SBOM, dependency, or security execution. That would break the -required review → fix → exact-head revalidation loop even though the source change itself was valid. +```text +contents: read +pull-requests: write +``` + +It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. It accepts only one structured candidate emitted by the model job. A new branch must: -A second timing problem also matters: GitHub materializes the several pull-request workflows -asynchronously. Stopping as soon as the first run appears can authorize CI while a later Security Scan -or SAST run remains approval-required indefinitely. The authorization job must therefore observe the -complete named workflow set, authorizing newly visible waiting runs on every discovery pass. +- match `automation/opencode-YYYYMMDDTHHMMSSZ-short-slug`; +- retain the captured exact SHA; +- be ahead of `develop`; +- change at most 50 files; +- avoid `.github/**` and all `CODEOWNERS` paths. -## Split-job authority model +The publisher creates a draft with a fixed JSON payload. It contains no pull-request review or merge endpoint. -The workflow separates mutable development from run authorization: +### Exact-head run authorizer + +`authorize-exact-head-checks` has: ```text -maintain-repository job - ├─ checks out protected default-branch source - ├─ installs and executes OpenCode - ├─ may prepare a feature branch and pull request - ├─ has actions: read - └─ outputs only the pre-agent pull-request head map - - job output boundary - ↓ - -authorize-exact-head-checks job - ├─ never checks out or executes repository code - ├─ receives no NVIDIA model credential - ├─ has actions: write, contents: read, pull-requests: read - ├─ compares before/after exact heads - └─ authorizes only approval-required exact-head workflow runs +actions: write +contents: read +pull-requests: read ``` -This prevents generated code, repository scripts, or the OpenCode process from using Actions write -permission. The sole privileged job consumes only GitHub API metadata and a compact JSON map of pull -request numbers to commit SHAs produced before the agent starts. +It never checks out source and never receives the model credential. Before authorizing a run, it requires: -## Required workflow set +```text +run.event == pull_request +run.head_sha == expected_head +any(run.pull_requests; number == expected_pull_request_number) +live pull request head == expected_head +``` + +The `pull_requests` association is mandatory. A commit SHA is not a unique pull-request identity: more than one pull request can reference the same commit. -For a direct pull request to `develop`, exact-head validation is incomplete until all of these -pull-request workflows have materialized: +## Complete workflow materialization + +GitHub can materialize pull-request workflows asynchronously. The authorizer repeats bounded discovery and authorizes newly visible `action_required` or `waiting` runs on every pass. It succeeds only after this complete workflow-name set is associated with the exact pull request and exact SHA: ```text CI @@ -68,117 +109,62 @@ SAST Semgrep Security Scan ``` -These are workflow names, not conclusions. Their presence proves only that validation was created for -the head. Every run and named check must still complete successfully through the ordinary repository -policy before merge. A workflow rename or required-workflow change must update the contract test, -this evidence document, and the authorization list in one reviewed change. - -## Fail-closed authorization algorithm - -The protected default-branch workflow performs the following steps: - -1. Before OpenCode starts, snapshot the exact heads of all same-repository pull requests targeting - `develop` and expose that compact object as the maintenance job's output. -2. After the maintenance job completes or fails without cancellation, start the isolated - authorization job. -3. Require the prior output to exist and parse as a JSON object. -4. Enumerate the same pull-request set again. -5. Select only a new pull request or a pull request whose head changed during this run. -6. Refuse automatic run authorization when the pull request changes any path below `.github/` or - any `CODEOWNERS` file. Those policy changes require explicit human authorization. -7. Read the still-current pull-request head and require it to equal the selected expected SHA. -8. Repeatedly discover only `pull_request` workflow runs whose `head_sha` equals that expected SHA. -9. On each discovery pass, authorize every exact-head run still in `action_required` or `waiting` - state. -10. Compare the unique observed workflow names with the complete required workflow set. -11. Continue bounded discovery until every required workflow has materialized or the discovery - window expires. -12. Fail visibly when no run appears or any required workflow remains absent. -13. Re-read the pull-request head immediately before declaring authorization complete and reject a - moved head. -14. Leave check execution, review, mergeability, branch protection, and expected-head merge - disposition to their existing independent gates. - -The isolated job runs even when OpenCode fails, provided the workflow was not cancelled. This covers -a partial agent session that pushed a branch before later failing. If the pre-run snapshot is absent, -run discovery fails, the head moves, a policy file changed, the named workflow set is incomplete, or -GitHub rejects authorization, the workflow fails instead of reporting successful revalidation. - -## Authority and credential boundary - -The workflow-level permission remains `contents: read`. The maintenance job retains only the -minimum branch, pull-request, issue, check, status, security-read, and Actions-read permissions needed -for development. The separate authorization job receives the workflow's only `actions: write` -permission plus read-only contents and pull-request metadata access. No personal token, GitHub App -token, OIDC token, or additional repository secret is introduced. - -The authorization job never checks out the repository, never executes repository files, and never -receives `NVIDIA_API_KEY`. Its Actions write permission is used solely for the workflow-run approval -endpoint. The implementation contains no pull-request review approval command and no merge API call. -The OpenCode prompt continues to forbid approval, merge, protected-branch push, branch-protection -bypass, review-agent modification, and unauthorized workflow-policy changes. +Name presence proves only that a run was created. Every run must still complete successfully before merge. + +## Time-of-check/time-of-use controls + +The loop validates state at several points: + +1. snapshot `develop`, open pull-request heads, and prior automation branches before model execution; +2. require `develop` to remain unchanged after the model exits; +3. select at most one changed existing pull request or strict automation branch; +4. re-read the branch or pull request before draft publication; +5. re-read the pull request before workflow-run discovery; +6. re-read the exact head on every discovery pass; +7. re-read it once more before declaring authorization complete. + +A moved head, multiple candidate, invalid namespace, policy-file change, absent required workflow, or GitHub authorization rejection fails closed. ## Test-first evidence -`HourlyOpenCodeMaintenanceWorkflowTest` first required the following contracts before production -implemented them: - -- a pre-agent exact-head snapshot exported as a job output; -- the absence of Actions write authority from the OpenCode job; -- exactly one isolated authorization job with Actions write permission; -- no checkout or NVIDIA credential in that authorization job; -- same-repository and `develop` targeting; -- refusal of `.github/**` and `CODEOWNERS` changes; -- exact-head workflow-run discovery; -- two head-SHA time-of-check/time-of-use validations; -- explicit failure when no run materializes; -- authorization through the workflow-run endpoint only; -- continued absence of pull-request approval and merge operations. - -`HourlyOpenCodeRequiredWorkflowAuthorizationTest` then required the complete five-workflow set, -repeated bounded discovery, repeated waiting-run authorization, observed/missing workflow evidence, -and explicit failure when the set remains incomplete. Production implemented those contracts with -job outputs, `gh api`, canonical JSON processing through `jq`, and exact SHA and workflow-name arrays -passed as data rather than interpolated into jq source. - -## Verification checklist - -Reviewers must verify on the exact current pull-request head that: - -- the scheduler still runs only from protected default-branch workflow source; -- the snapshot precedes the OpenCode process and is the only cross-job mutable evidence; -- the maintenance job has `actions: read`, not `actions: write`; -- the authorization job is the only job with `actions: write`; -- the authorization job has no checkout, repository-code execution, or NVIDIA credential; -- only heads changed by that run are considered; -- `.github/**` and all `CODEOWNERS` paths are excluded from automatic authorization; -- both current-head reads equal the expected head; -- run discovery filters `event=pull_request` and the exact head SHA; -- waiting or action-required runs are authorized on every discovery pass; -- CI, Dependency Review, SBOM, SAST, and Security Scan all materialize before success is reported; -- absent runs, missing named workflows, and authorization failures make the workflow fail; -- no review approval, merge, protected-branch push, or secret fallback was added; -- every authorized run must still complete successfully before merge disposition can proceed. +`HourlyOpenCodeMaintenanceWorkflowTest` was changed before production to require: + +- plain `opencode run`, not the GitHub lifecycle handler; +- `pull-requests: read` on the model job; +- exactly one non-checkout publisher with `pull-requests: write`; +- exactly one non-checkout authorizer with `actions: write`; +- no NVIDIA credential in either privileged deterministic job; +- one strict publication candidate; +- draft-only deterministic publication; +- `.github/**` and `CODEOWNERS` exclusion; +- exact pull-request association for every workflow run; +- complete required-workflow materialization; +- continued absence of review and merge endpoints. + +The initial test commit intentionally made the existing workflow contract fail. Production was then changed to satisfy the new authority and association requirements. + +## Residual risks and controls + +- `contents: write` remains necessary for a branch push. Protected-branch rules remain authoritative for `develop` and `main`. +- The deterministic publisher necessarily has coarse pull-request write permission. It has no checkout, model input, or executable repository source and its script exposes only draft creation or metadata validation. +- Workflow-run approval is an Actions control, not a successful check or pull-request approval. +- A workflow or `CODEOWNERS` change always requires explicit human authorization. +- Independent exact-head approval remains mandatory after every new commit. ## Rollback -If GitHub changes the approval-required run model or the endpoint becomes unavailable, disable the -hourly development workflow. Do not remove the exact-head authorization contract while leaving the -agent able to push changes with `GITHUB_TOKEN`, because that recreates unvalidated agent heads. +Disable the hourly workflow if the platform's `GITHUB_TOKEN` recursion or workflow-run approval model changes. Do not restore pull-request write authority to the model job and do not revert to SHA-only run authorization. -Do not move `actions: write` back into the OpenCode job. A replacement based on a GitHub App may -remove the isolated authorization job only after its installation permissions, recursive-trigger -behavior, actor identity, secret lifecycle, exact-head workflow evidence, complete required-run -materialization, and independent review boundary are documented and tested through a separate pull -request. +A GitHub App or endpoint proxy may replace the publisher only after its endpoint allowlist, actor identity, installation scope, token lifetime, audit log, and exact-head behavior are independently tested and documented. ## References — APA 7th -GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. -https://docs.github.com/en/actions/concepts/security/github_token +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts + +Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts + +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token -GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. -https://docs.github.com/en/actions/using-workflows/triggering-a-workflow +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs -GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. -https://docs.github.com/en/rest/actions/workflow-runs +GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From 28b56bb81f020117589436f8e6acf9829510a3a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:00:46 +0900 Subject: [PATCH 100/152] docs(design): separate model, publisher, and run authorizer --- ...8-04-hourly-opencode-maintenance-design.md | 230 ++++++++---------- 1 file changed, 99 insertions(+), 131 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md index e8ab9c14..4fad4ee0 100644 --- a/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md +++ b/docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md @@ -2,185 +2,153 @@ ## Purpose -mightyETL needs a scheduled development loop that can inspect current repository state, remediate one bounded item, and open or update a pull request without weakening independent review, exact-head validation, or merge gates. The agent must use OpenCode with `NVIDIA_NIM_API_KEY`; it must not use GitHub Copilot or change the existing review agent's credentials, provider, workflow, or authority. +mightyETL requires an hourly development loop that can inspect the live pull-request queue, repair one dependency-eligible branch, or prepare one bounded buyer-visible improvement without granting model-generated code review, merge, or workflow-administration authority. -## Decision +The model credential is `NVIDIA_NIM_API_KEY`. GitHub Copilot credentials and changes to the existing review agents are outside this design. -Add `.github/workflows/hourly-opencode-maintenance.yml` as a two-job workflow: +## Architecture decision -```text -maintain-repository - ├─ protected default-branch checkout - ├─ pinned OpenCode + NVIDIA NIM - ├─ branch / issue / pull-request preparation - └─ actions: read - - exact pre-agent head map - ↓ - -authorize-exact-head-checks - ├─ no checkout or repository-code execution - ├─ no model credential - ├─ actions: write - └─ exact-head workflow-run authorization only +Use three physically separated GitHub Actions jobs: + +```mermaid +flowchart LR + M[maintain-repository
OpenCode + source checkout] -->|one candidate record| P[publish-agent-pull-request
no checkout] + P -->|PR number + exact SHA| A[authorize-exact-head-checks
no checkout] + A --> V[CI and security validation] + V --> R[independent review and merge disposition] ``` -Preserve `.github/workflows/hourly-pr-disposition.yml` as the deterministic fail-closed merge boundary and require a non-author approval anchored to the exact current head. +### Model execution job -The workflow: +`maintain-repository` uses plain `opencode run`, not `opencode github run`. The GitHub lifecycle handler can create pull requests and therefore requires pull-request write authority. Plain run separates model execution from PR publication. -- runs only from the protected default branch at minute 43 of every hour; -- omits manual dispatch so a feature branch or tag cannot become scheduler source; -- pins `actions/checkout` by full SHA with persisted credentials disabled; -- installs OpenCode 1.18.13 from an immutable release asset verified by SHA-256; -- accepts exactly one regular archive member named `opencode` before extraction; -- extracts into a fresh mode-`0700` directory without restoring archive ownership or permissions and with overwrites disabled; -- rejects non-regular or symbolic-link output and verifies the exact executable version; -- maps only `${{ secrets.NVIDIA_NIM_API_KEY }}` to `NVIDIA_API_KEY`; -- selects `nvidia/deepseek-ai/deepseek-v4-pro` with no automatic model or provider fallback; -- uses the repository-scoped GitHub token without OpenCode OIDC exchange; -- bootstraps a repository-local GitHub CLI credential helper and bot author because OpenCode 1.18.13 skips internal Git setup in direct-token mode; -- removes the helper through an `EXIT` trap; -- omits the ineffective `AGENT` environment variable, allowing repository `default_agent` or OpenCode's `build` fallback; -- disables public session sharing; -- caps OpenCode with a 45-minute `TERM` timeout, 30-second `KILL` escalation, and 50-minute job timeout; -- exports a compact pre-agent pull-request head map to the isolated authorization job; -- authorizes only approval-required `pull_request` workflow runs for a still-current exact head; -- refuses automatic authorization for `.github/**` and `CODEOWNERS` changes; -- never approves or merges a pull request. +The job receives: -## Model selection boundary +```text +actions: read +checks: read +contents: write +issues: write +pull-requests: read +security-events: read +statuses: read +``` -The previous Qwen3 Coder free endpoint is marked deprecated in NVIDIA's current catalog. DeepSeek V4 Pro is selected because NVIDIA currently exposes it through a free endpoint and documents coding, agentic AI, tool use, structured output, function calling, software-engineering use cases, and long context. +It may inspect pull requests and push one feature branch. It cannot create, update, approve, close, or merge a pull request with its token. -The model identifier is explicit and test guarded. No fallback runs after a partial agent session because another model could operate on non-deterministic workspace state, create duplicate branches, or generate conflicting pull requests. Endpoint rejection fails visibly and requires a separate test-first model-selection change. The repository does not claim NVIDIA benchmarks as mightyETL performance. +### Deterministic publisher -## Supply-chain boundary +`publish-agent-pull-request` has `contents: read` and `pull-requests: write`. It has no checkout, model credential, or repository-code execution. It validates one candidate and either identifies an already-open updated PR or creates one draft PR from a strict `automation/opencode-*` branch. -An exact npm version is not a content identity. The workflow consumes the immutable upstream release asset directly. +### Workflow-run authorizer -The installer: +`authorize-exact-head-checks` has `actions: write`, `contents: read`, and `pull-requests: read`. It has no checkout or model credential. It may approve only workflow runs that are: -1. downloads only over HTTPS with failure handling and TLS 1.2 minimum; -2. verifies SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`; -3. requires exactly one archive member named `opencode`; -4. under `LC_ALL=C`, requires GNU tar's regular-file type character `-` before extraction; -5. recreates a private mode-`0700` directory and refuses overwrites; -6. extracts without restoring archive ownership or permissions; -7. requires a regular non-symbolic-link output file; -8. applies executable mode only to that file; -9. verifies version `1.18.13` before adding it to `GITHUB_PATH`. +- `pull_request` events; +- bound to the exact expected SHA; +- associated with the exact expected pull-request number; +- in `action_required` or `waiting` state. -Checksum binding, pre-extraction name and type checks, private extraction, overwrite refusal, and post-extraction checks are cumulative controls. They avoid mutable package-manager bootstrapping and unconstrained archive extraction. +It does not approve a PR or merge code. -## Direct-token credential lifecycle +## Schedule and supply-chain contract -OpenCode 1.18.13 uses `GITHUB_TOKEN` for GitHub API access when `USE_GITHUB_TOKEN=true` and skips its internal Git configuration. With `persist-credentials: false`, ordinary `git commit` and `git push` need explicit local author and HTTPS helper configuration. +- cron: `43 * * * *`; +- no manual dispatch; +- serialized concurrency; +- immutable `actions/checkout` full SHA; +- `persist-credentials: false`; +- OpenCode 1.18.13 immutable Linux archive; +- SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`; +- exactly one regular archive entry named `opencode`; +- private mode-`0700` extraction without archived ownership or permissions; +- 45-minute `TERM`, 30-second `KILL` escalation, 50-minute job timeout; +- model `nvidia/deepseek-ai/deepseek-v4-pro` with no provider fallback. -The maintenance job therefore: +## Candidate-state contract -1. fails closed if `NVIDIA_API_KEY`, `GITHUB_TOKEN`, or `GH_TOKEN` is empty; -2. resets inherited GitHub credential helpers in repository-local configuration; -3. installs `!gh auth git-credential`, which reads the ephemeral token from `GH_TOKEN` only when Git requests credentials; -4. sets local author identity to `opencode-agent[bot]`; -5. removes the helper through an `EXIT` trap after success, failure, timeout, or forced termination. +Before OpenCode starts, snapshot: -No encoded token, personal token, OIDC permission, tracked credential file, alternate model credential, or review-agent secret is introduced. +1. protected `develop` head; +2. every same-repository open `develop` PR and head SHA; +3. every existing `automation/opencode-*` branch and head SHA. -## Permission and code-execution boundary +After OpenCode exits: -The workflow-level permission is only `contents: read`. +1. require `develop` to be unchanged; +2. identify existing PR heads changed during the run; +3. identify new or advanced strict automation branches; +4. exclude an automation branch already represented by the changed PR candidate; +5. require zero or one candidate; +6. fail if the output is ambiguous. -The `maintain-repository` job receives Actions read plus the minimum check, branch, issue, pull-request, security-read, and status-read permissions required for bounded development. It executes OpenCode and repository tests but has no Actions write authority. +A new branch must match: -The `authorize-exact-head-checks` job receives Actions write plus read-only contents and pull-request metadata. It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. Its only write operation is authorizing an approval-required workflow run after exact-head verification. +```text +^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$ +``` -This separation prevents OpenCode, generated code, and checked-out repository scripts from using Actions write permission while still closing the `GITHUB_TOKEN` recursive-trigger gap. +The publisher additionally requires a live matching SHA, at least one commit ahead of `develop`, at most 50 changed files, and no `.github/**` or `CODEOWNERS` path. -## Exact-head authorization algorithm +## Exact-head workflow contract -1. Before OpenCode, snapshot all same-repository pull requests targeting `develop` as `{pull_request_number: head_sha}`. -2. Export that compact object as the maintenance job output. -3. Run the authorization job after maintenance success or failure unless the workflow was cancelled. -4. Require the output to exist and parse as a JSON object. -5. Enumerate the same pull-request set after OpenCode. -6. Select only a new pull request or a changed head. -7. Refuse automatic authorization when any `.github/**` or `CODEOWNERS` path changed. -8. Verify the still-current pull-request head equals the expected SHA. -9. Discover only `pull_request` workflow runs for that SHA. -10. Fail if no exact-head run materializes. -11. Verify the current head again immediately before authorization. -12. Authorize only `action_required` or `waiting` runs. +GitHub creates pull-request workflows asynchronously. The authorizer performs bounded repeated discovery and succeeds only when all five names are associated with the exact PR number and SHA: -Expected SHA values are passed to `jq` as data rather than interpolated into jq source. Authorization begins validation but conveys no review, merge, or success decision. +```text +CI +Dependency Review +SBOM (CycloneDX) +SAST Semgrep +Security Scan +``` -## Agent authority boundary +A SHA-only filter is insufficient because multiple pull requests can reference the same commit. Every selected workflow run must satisfy: -The prompt is part of the security boundary. The agent may inspect, test, edit, commit, push one feature branch, update one dependency-eligible development pull request, or open one pull request. It must not: +```text +run.head_sha == expected_head +and any(run.pull_requests; number == expected_pull_request_number) +``` + +The live PR head is checked before discovery, on every discovery pass, and after the complete set materializes. -- approve or merge a pull request; -- push directly to `develop` or `main`; -- bypass checks, branch protection, security gates, coverage, or independent review; -- alter review-agent workflows, providers, credentials, secret names, `CODEOWNERS`, branch protection, or repository secrets; -- modify workflow policy unless a specifically labeled issue authorizes the bounded change; -- expose secret values or sensitive ETL payload information; -- create a second development pull request while another is dependency-eligible; -- publish a release without separate release authorization and every acceptance gate. +## Git credential lifecycle -The deterministic disposition workflow independently evaluates reviews, current threads, named checks, status contexts, labels, mergeability, and expected head SHA. +The model job uses an ephemeral repository-local `!gh auth git-credential` helper because checkout credentials remain disabled. It clears inherited local helpers, configures the bot author, and removes the helper through an `EXIT` trap after success, failure, or timeout. No token is written into Git configuration. -## Data flow +## Agent product contract -1. GitHub starts the schedule from the protected default branch. -2. The maintenance job checks out source without persisting credentials. -3. The installer validates and installs OpenCode. -4. The job snapshots direct `develop` pull-request heads and exports the map. -5. The shell validates model and repository credentials, installs local Git identity and helper, and registers cleanup. -6. OpenCode calls `nvidia/deepseek-ai/deepseek-v4-pro`, inspects the queue, and performs one bounded test-first slice. -7. OpenCode leaves one branch and pull request but does not approve or merge. -8. The helper cleanup trap runs. -9. The isolated authorization job compares before and after heads and rejects policy-changing pull requests. -10. It authorizes only approval-required runs for an unchanged exact head. -11. CI, security, independent review, branch protection, and deterministic disposition evaluate that exact head separately. +When a dependency-eligible PR exists, the model may update only that branch. Otherwise it may push exactly one strict automation branch. It must work test-first, preserve configured production statement and branch coverage at 100%, maintain public documentation and `CHANGELOG.md`, use descriptive multi-word `snake_case` database names, and record material primary standards or peer-reviewed evidence in APA 7th form. -## Failure behavior +The model may not mutate PR lifecycle state, protected branches, workflow policy, review-agent credentials, repository secrets, or releases. -Missing credentials, deprecated or rejected model, NVIDIA outage, download failure, checksum mismatch, archive mismatch, non-regular entry, extracted-file mismatch, version mismatch, Git bootstrap failure, timeout, test failure, missing pre-agent output, policy-file change, head movement, absent workflow run, authorization rejection, or permission denial fails visibly. No provider fallback or partial-success claim is allowed. Concurrency is serialized. +## Failure semantics + +Missing credentials, archive mismatch, model failure, protected-branch movement, multiple candidates, invalid branch namespace, policy-file changes, candidate SHA movement, absent workflow association, incomplete workflow-name materialization, or any rejected API mutation fails visibly. Partial branch evidence can be exposed only as a draft by the deterministic publisher; it is not reported as a successful run. ## Verification -Repository tests fail unless they prove: +Tests must prove: -- hourly serialized scheduling and bounded forced termination; -- immutable checkout and OpenCode content pins; -- exact pre-extraction member name and regular-file type; -- private extraction, overwrite refusal, and regular non-symbolic-link output; -- exclusive use of `NVIDIA_NIM_API_KEY` and the selected NVIDIA model; -- direct-token Git bootstrap and cleanup without stored authorization data; -- workflow-level read-only permissions; -- Actions write absent from the OpenCode job; -- exactly one isolated non-checkout authorization job with Actions write; -- before/after exact-head evidence, `.github/**` and `CODEOWNERS` exclusion, exact-run filtering, and double SHA validation; -- prompt prohibitions against approval, merge, protected-branch writes, review-agent changes, self-modification, and duplicate pull requests. +- hourly bounded execution and immutable installation; +- exclusive NVIDIA NIM model credential; +- plain OpenCode run instead of GitHub lifecycle execution; +- PR read-only authority in the model job; +- exactly one non-checkout PR publisher; +- exactly one non-checkout Actions authorizer; +- strict single-candidate publication; +- policy-file exclusion; +- exact PR-number plus SHA run association; +- complete five-workflow materialization; +- no PR review or merge endpoint in the workflow. ## References — APA 7th Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *OpenCode release v1.18.13* [Software release]. GitHub. https://github.com/anomalyco/opencode/releases/tag/v1.18.13 - -Free Software Foundation. (2023). *GNU tar 1.35: Security*. https://www.gnu.org/software/tar/manual/html_section/Security.html +Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token -GitHub, Inc. (2026). *GitHub CLI manual: gh auth git-credential*. GitHub CLI Manual. https://cli.github.com/manual/gh_auth_git-credential - GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs -GitHub, Inc. (2026). *Triggering a workflow*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/triggering-a-workflow - -GitHub, Inc. (2026). *Workflow syntax for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax - NVIDIA Corporation. (2026). *DeepSeek V4 Pro*. NVIDIA NIM API catalog. https://build.nvidia.com/deepseek-ai/deepseek-v4-pro - -NVIDIA Corporation. (2026). *DeepSeek AI / DeepSeek V4 Pro*. NVIDIA NIM API reference. https://docs.api.nvidia.com/nim/reference/deepseek-ai-deepseek-v4-pro From f3d68612764ef5511ee0897d37d014648574fa42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:02:51 +0900 Subject: [PATCH 101/152] docs(plan): complete authority-separated OpenCode workflow plan --- ...-08-04-hourly-opencode-maintenance-plan.md | 244 +++++++----------- 1 file changed, 91 insertions(+), 153 deletions(-) diff --git a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md index 26978a27..f3f17985 100644 --- a/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md +++ b/docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md @@ -1,190 +1,128 @@ # Hourly OpenCode Maintenance Agent Implementation Plan -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **Execution rule:** implement each authority boundary test-first, then verify the final exact head through repository CI, security, and independent review. -**Goal:** Add a fail-closed hourly OpenCode development agent that uses `NVIDIA_NIM_API_KEY`, prepares one bounded development pull request, starts validation for an agent-written exact head, and never changes the independent review agent or deterministic merge authority. +**Goal:** Run one bounded NVIDIA NIM-backed development loop every hour while ensuring model execution cannot create, approve, close, or merge pull requests and cannot authorize GitHub Actions runs. -**Architecture:** A protected default-branch GitHub Actions workflow has two jobs. `maintain-repository` installs a checksum-verified immutable OpenCode release and may prepare one feature branch without Actions write permission. `authorize-exact-head-checks` never checks out repository code, receives no model secret, and uses the sole Actions write grant to authorize only approval-required pull-request workflow runs for an unchanged exact head. Existing review, security, branch protection, and merge-disposition automation remain independent and authoritative. - -**Tech Stack:** GitHub Actions, OpenCode 1.18.13 immutable release archive, NVIDIA NIM, DeepSeek V4 Pro, GitHub CLI credential helper, GNU Coreutils, GNU tar, Bash, jq, Maven, JUnit 5. +**Architecture:** Three jobs separate model execution, deterministic draft-PR publication, and exact-head workflow-run authorization. Independent OpenCode/Noema review and expected-head merge disposition remain outside all three jobs. ## Global constraints -- Keep the review-agent provider, workflow, credential flow, and secret names unchanged. -- Preserve `.github/workflows/hourly-pr-disposition.yml` as the independent exact-head merge boundary. -- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` for the model credential and expose it as `NVIDIA_API_KEY` only to the OpenCode step. -- Pin `MODEL: nvidia/deepseek-ai/deepseek-v4-pro`; reject deprecated or non-NVIDIA fallback identifiers. -- Pin executable content and third-party workflow sources immutably. -- Verify OpenCode 1.18.13 Linux x64 SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937` before extraction. -- Require exactly one regular archive entry named `opencode`; use a fresh mode-`0700` directory, refuse overwrites, and reject non-regular or symbolic-link output. -- Keep checkout credential persistence disabled; use only a repository-local GitHub CLI helper removed by an `EXIT` trap. -- Keep workflow-level permissions read-only. -- Keep Actions write authority out of the OpenCode job and in one isolated non-checkout job. -- Refuse automatic workflow-run authorization for `.github/**` and `CODEOWNERS` changes. -- Bind run discovery and authorization to the exact still-current head SHA. -- Bound OpenCode with `TERM` after 45 minutes, `KILL` after a 30-second grace period, and a 50-minute job timeout. -- The agent may create or update one pull request but may never approve, merge, bypass protection, publish, or push to `develop` or `main`. -- Preserve standalone operation and modular CWL service compatibility. +- Use only `${{ secrets.NVIDIA_NIM_API_KEY }}` as the model secret. +- Do not introduce `COPILOT_GITHUB_TOKEN` or alter an existing review-agent secret. +- Invoke plain `opencode run`, not the GitHub lifecycle handler. +- Give the model job `pull-requests: read`, never write. +- Give exactly one non-checkout publisher `pull-requests: write`. +- Give exactly one non-checkout authorizer `actions: write`. +- Bind every workflow-run authorization to both exact SHA and exact PR number. +- Keep all `.github/**` and `CODEOWNERS` changes outside automatic publication and authorization. +- Preserve immutable OpenCode installation, branch protection, 100% configured production statement/branch coverage, public docstrings, APA 7th doctoring, and `CHANGELOG.md` maintenance. --- -### Task 1: Add fail-closed workflow contracts - -**Files:** -- Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` -- Create: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeArchiveValidationTest.java` - -- [x] **Step 1: Write the initial missing-workflow test** - -Require workflow existence, hourly schedule, serialized concurrency, bounded timeout, immutable checkout, no persisted credentials, NVIDIA-only credentials, private sharing, and prompt prohibitions. - -- [x] **Step 2: Verify initial RED** - -```bash -./mvnw -pl etl-service -Dtest=HourlyOpenCodeMaintenanceWorkflowTest test -``` - -Observed: the existence assertion failed before the workflow was created. - -- [x] **Step 3: Add immutable-installation and direct-token contracts** - -Require exact release URL and checksum, no npm install, graceful and forced timeout, removable GitHub CLI helper, bot author, cleanup trap, and no encoded authorization header. - -- [x] **Step 4: Add archive-member and entry-type contracts** - -Require one member named `opencode`, locale-stable GNU tar metadata, regular-file type `-`, validation before extraction, private directory, overwrite refusal, and post-extraction file checks. - -- [x] **Step 5: Add current-model availability contract** - -Require `nvidia/deepseek-ai/deepseek-v4-pro` and reject the deprecated Qwen3 Coder endpoint. - -- [x] **Step 6: Add exact-head revalidation contracts** - -Require a pre-agent head snapshot, isolated Actions-write job, no checkout or NVIDIA secret in that job, `.github/**` and `CODEOWNERS` exclusion, exact-run discovery, double SHA validation, absent-run failure, and no review or merge operation. - -- [x] **Step 7: Verify RED cycles** - -The workflow-existence, archive-member, archive-type, model-selection, exact-head authorization, policy-path exclusion, and Actions-write isolation contracts were committed before their corresponding production behavior. - -### Task 2: Implement the bounded NVIDIA OpenCode job - -**Files:** -- Create: `.github/workflows/hourly-opencode-maintenance.yml` - -- [x] **Step 1: Add protected scheduling and authority boundary** - -Configure `43 * * * *`, omit manual dispatch, serialize concurrency, set a 50-minute job timeout, checkout protected default-branch source, and prohibit approval, merge, protected-branch push, review-agent modification, secret disclosure, duplicate PR creation, and release publication. +## Task 1 — Test the authority boundaries before production changes -- [x] **Step 2: Add immutable OpenCode installation** +**File:** `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` -Download the immutable `v1.18.13` Linux x64 archive over HTTPS, validate its SHA-256, require one regular entry named `opencode`, extract privately without restoring archive ownership or permissions, refuse overwrite, reject non-regular or symbolic-link output, and verify exact version. +- [x] Require hourly serialized execution and bounded TERM/KILL handling. +- [x] Require the immutable OpenCode 1.18.13 archive, SHA-256, regular-file member, private extraction, and exact version. +- [x] Require `NVIDIA_NIM_API_KEY` as the sole model secret and `nvidia/deepseek-ai/deepseek-v4-pro` as the selected model. +- [x] Require plain `opencode run --model "${MODEL}" --auto` and reject `opencode github run`. +- [x] Require `pull-requests: read` on `maintain-repository`. +- [x] Require one non-checkout publisher with the workflow's sole `pull-requests: write` grant. +- [x] Require one non-checkout authorizer with the workflow's sole `actions: write` grant. +- [x] Require one strict existing-PR or `automation/opencode-*` candidate. +- [x] Require draft publication, policy-path exclusion, and a branch ahead of `develop`. +- [x] Require workflow-run association with both PR number and exact SHA. +- [x] Require complete CI, Dependency Review, SBOM, Semgrep, and Security Scan materialization. -- [x] **Step 3: Add direct-token Git bootstrap** +The test-only head intentionally made the preceding workflow fail before implementation. -Fail closed on missing token aliases, reset inherited helpers locally, install `!gh auth git-credential`, set `opencode-agent[bot]` local identity, and remove the helper through an `EXIT` trap. +## Task 2 — Implement the model job without PR write authority -- [x] **Step 4: Select the current NVIDIA coding endpoint** +**File:** `.github/workflows/hourly-opencode-maintenance.yml` -Set `NVIDIA_API_KEY`, `MODEL`, `SHARE=false`, and `USE_GITHUB_TOKEN=true` without provider or model fallback. +- [x] Run at `43 * * * *` only from protected default-branch source. +- [x] Keep top-level `contents: read`. +- [x] Set model-job permissions to Actions/checks/PR/security/status read, issues write, and contents write. +- [x] Install OpenCode from the pinned immutable archive. +- [x] Configure the removable repository-local `gh auth git-credential` helper and bot author. +- [x] Pipe the bounded prompt into plain `opencode run`. +- [x] Permit an existing eligible branch update or exactly one strict automation branch. +- [x] Preserve the model step's failure after capturing any reviewable branch evidence. -- [x] **Step 5: Remove Actions write from the OpenCode job** +## Task 3 — Detect exactly one candidate -Give `maintain-repository` Actions read plus only the branch, issue, PR, check, security-read, and status permissions required for bounded maintenance. +**File:** `.github/workflows/hourly-opencode-maintenance.yml` -### Task 3: Implement isolated exact-head run authorization +- [x] Snapshot `develop`, current direct PR heads, and prior automation branch heads before model execution. +- [x] Require `develop` to remain unchanged afterward. +- [x] Detect one changed existing PR head or one strict automation branch. +- [x] Exclude an automation branch already represented by the changed PR. +- [x] Fail on multiple candidates. +- [x] Emit compact candidate JSON as a job output. -**Files:** -- Modify: `.github/workflows/hourly-opencode-maintenance.yml` -- Modify: `etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java` - -- [x] **Step 1: Export the pre-agent head map** - -Snapshot same-repository pull requests targeting `develop` before OpenCode and expose compact JSON as a maintenance-job output. - -- [x] **Step 2: Add the isolated authorization job** - -Run after maintenance success or failure unless cancelled. Give it only `actions: write`, `contents: read`, and `pull-requests: read`. Do not checkout repository code or pass the NVIDIA credential. - -- [x] **Step 3: Add policy-path and TOCTOU gates** +## Task 4 — Publish deterministically without executing source -Reject `.github/**` and `CODEOWNERS` changes, verify the exact head before discovery and immediately before authorization, and pass the expected SHA to jq as data. +**File:** `.github/workflows/hourly-opencode-maintenance.yml` -- [x] **Step 4: Authorize only exact approval-required runs** +- [x] Add `publish-agent-pull-request` with `contents: read` and `pull-requests: write` only. +- [x] Do not checkout source or pass the NVIDIA credential. +- [x] Re-read existing PR repository, state, base, branch, and exact SHA. +- [x] For a new branch, require the strict namespace, live exact SHA, positive `ahead_by`, at most 50 files, and no policy path. +- [x] Create one draft PR from a fixed JSON payload. +- [x] Expose PR number, head ref, and exact SHA for the authorizer. +- [x] Contain no review or merge endpoint. -Discover `pull_request` runs by exact `head_sha`, fail when no run appears, and call the approval endpoint only for `action_required` or `waiting` runs. Do not approve or merge the pull request. +## Task 5 — Authorize only PR-associated exact-head runs -- [ ] **Step 5: Verify focused GREEN on the final exact head** +**File:** `.github/workflows/hourly-opencode-maintenance.yml` -```bash -./mvnw -pl etl-service \ - -Dtest='HourlyOpenCodeMaintenanceWorkflowTest,HourlyOpenCodeArchiveValidationTest' test -``` +- [x] Add `authorize-exact-head-checks` with `actions: write`, `contents: read`, and `pull-requests: read`. +- [x] Do not checkout source or pass the model credential. +- [x] Reject `.github/**` and `CODEOWNERS` changes. +- [x] Re-read the live PR before discovery and on every bounded pass. +- [x] Filter each run by `event=pull_request`, exact SHA, and `pull_requests[].number`. +- [x] Authorize only `action_required` or `waiting` runs. +- [x] Require all five named workflows to materialize. +- [x] Fail with missing names or any head movement. -Expected: zero failures, errors, and skipped project tests. - -### Task 4: Complete evidence and release notes +## Task 6 — Align operations, design, doctoring, and changelog evidence **Files:** -- Create: `docs/operations/hourly-opencode-maintenance.md` -- Create: `docs/doctoring/opencode-archive-extraction-evidence.md` -- Create: `docs/doctoring/nvidia-opencode-model-selection-evidence.md` -- Create: `docs/doctoring/github-token-exact-head-check-authorization-evidence.md` -- Modify: `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` -- Modify: `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` -- Modify: `CHANGELOG.md` - -- [x] **Step 1: Document archive and credential boundaries** - -Record immutable release, checksum, exact member and type, extraction controls, direct-token helper lifecycle, timeout escalation, authority restrictions, failure behavior, and rollback. - -- [x] **Step 2: Document model selection** - -Record deprecated-endpoint rejection, current NVIDIA endpoint evidence, no-fallback semantics, RED evidence, and APA 7 references. - -- [x] **Step 3: Document exact-head run authorization** - -Record GitHub-token recursive-trigger behavior, split-job authority, before/after SHA evidence, policy-path exclusion, Actions-write isolation, TOCTOU validation, failure behavior, rollback, and APA 7 references. - -- [x] **Step 4: Align `CHANGELOG.md`** - -Record the NVIDIA model, immutable installation, isolated exact-head workflow-run authorization, and all doctoring evidence files under `Unreleased`. - -- [ ] **Step 5: Run full reactor verification** - -```bash -./mvnw -B test -``` - -Expected: all modules succeed; no project test is skipped. - -### Task 5: Verify and integrate the protected workflow-change pull request - -**Files:** -- No additional source files. - -- [ ] **Step 1: Verify exact branch head and diff** - -```bash -git status --short -git rev-parse HEAD -git diff develop...HEAD --check -./mvnw -B test -``` -Expected: clean tree, no whitespace errors, successful build. +- `docs/operations/hourly-opencode-maintenance.md` +- `docs/doctoring/github-token-exact-head-check-authorization-evidence.md` +- `docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md` +- `docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md` +- `CHANGELOG.md` -- [x] **Step 2: Open and label the pull request** +- [x] Document the three-job authority topology. +- [x] Record why plain OpenCode run replaces the GitHub lifecycle handler. +- [x] Record strict candidate and draft publication controls. +- [x] Record exact PR-number plus SHA association. +- [x] Record residual coarse permissions and compensating controls. +- [x] Preserve APA 7th references to OpenCode and GitHub primary sources. +- [ ] Confirm the final root changelog wording against the final exact head before merge. -Use title `ci: schedule NVIDIA OpenCode maintenance agent`; apply `automerge-workflow`, and retain `manual-merge` until exact-head checks and non-author approval exist. +## Task 7 — Final verification and integration -- [ ] **Step 3: Reinspect all feedback on the final exact head** +- [ ] Run focused workflow contract tests on the exact final head. +- [ ] Run the complete Maven reactor with no skipped project test. +- [ ] Confirm Ubuntu, macOS, and Windows CI. +- [ ] Confirm Dependency Review and CycloneDX SBOM. +- [ ] Confirm Semgrep, Trivy, OSV, Scorecard, and all required security evidence. +- [ ] Confirm zero unresolved current review thread. +- [ ] Obtain non-author approval anchored to the exact final SHA. +- [ ] Remove `manual-merge` only immediately before an expected-head squash merge. +- [ ] Merge #121, then retarget and revalidate #122 and every successor in stack order. -Inspect human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Resolve only findings addressed by the current head. +## References — APA 7th -- [ ] **Step 4: Verify every final exact-head gate** +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Require successful Ubuntu, macOS, Windows, Dependency Review, SBOM, Semgrep, Trivy, OSV, Scorecard, combined status, mergeability, and zero unresolved current threads. Pending, queued, cancelled, neutral-required, skipped-required, stale, or absent evidence is not passing. +Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts -- [ ] **Step 5: Require independent exact-head approval and merge** +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token -Do not self-approve. After a non-author approval whose commit ID equals the current head and every exact-head gate succeeds, remove `manual-merge` and squash-merge using the expected head SHA. Otherwise retain the hold and identify the exact external gate. +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs From a5f09725b69624db45de0cffe03bce045d6794c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:04:29 +0900 Subject: [PATCH 102/152] test(ci): assert semantic workflow authorization contract --- ...lyOpenCodeRequiredWorkflowAuthorizationTest.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java index 2e54f001..342e01b1 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java @@ -21,7 +21,8 @@ class HourlyOpenCodeRequiredWorkflowAuthorizationTest { /** - * Requires the authorization loop to wait for, and account for, every required workflow. + * Requires the authorization loop to wait for, associate, and account for every required + * workflow without coupling the test to incidental shell-variable or log-message wording. * * @throws IOException when the production workflow cannot be read */ @@ -38,11 +39,15 @@ void waitsForEveryRequiredExactHeadWorkflow() throws IOException { )); assertTrue(workflow.contains("observed_workflow_names")); assertTrue(workflow.contains("missing_workflow_names")); - assertTrue(workflow.contains("missing_workflow_count")); assertTrue(workflow.contains("for _ in $(seq 1 18); do")); + assertTrue(workflow.contains(".head_sha == $expected_head")); + assertTrue(workflow.contains( + "any(.pull_requests[]?; .number == $pull_request_number)" + )); assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); - assertTrue(workflow.contains("All required exact-head pull-request workflows materialized")); - assertTrue(workflow.contains("Required exact-head pull-request workflows did not materialize")); + assertTrue(workflow.contains("jq 'length' <<<\"${missing_workflow_names}\"")); + assertTrue(workflow.contains("Missing required exact-head workflows for PR")); + assertTrue(workflow.contains("Authorized exact-head pull-request checks for PR")); } /** From 7974905d9bd44a4a24980e5473eabfb40624e8cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:08:37 +0900 Subject: [PATCH 103/152] test(ci): guard jq branch candidate scope --- .../HourlyOpenCodeCandidateSelectionTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java new file mode 100644 index 00000000..a7dcc4b5 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java @@ -0,0 +1,62 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the jq variable scope used to remove an updated PR branch from new-branch candidates. + * + *

Inside {@code $existing_refs | index(...)}, the jq input changes from the branch object to the + * reference array. Reading {@code .name} after that pipe therefore fails at runtime. Production + * must capture the branch name first and pass the scalar variable to {@code index}.

+ */ +class HourlyOpenCodeCandidateSelectionTest { + + /** + * Requires branch-name capture before indexing the existing-PR reference array. + * + * @throws IOException when the workflow cannot be read + */ + @Test + void capturesBranchNameBeforeExistingReferenceLookup() throws IOException { + String workflow = Files.readString( + projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + + assertTrue(workflow.contains(".name as $branch_name")); + assertTrue(workflow.contains("index($branch_name)")); + assertFalse(workflow.contains("index(.name)")); + } + + /** + * Finds the Maven reactor root from root-level or module-local execution. + * + * @return absolute project root + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From ed88fc8d97bb46dca7cda4c947532b688dcda08a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:11:30 +0900 Subject: [PATCH 104/152] fix(ci): capture branch name before jq array lookup --- .github/workflows/hourly-opencode-maintenance.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index af15e8d6..d7c52e51 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -221,7 +221,8 @@ jobs: .[] | select(.name | startswith("automation/opencode-")) | select(($before[.name] // null) != .commit.sha) - | select(($existing_refs | index(.name)) == null) + | .name as $branch_name + | select(($existing_refs | index($branch_name)) == null) | select(.name | test("^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$")) | { kind: "new_branch", From 75bdc199bc6e7b10635b5b3d33d608c014794635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:02:49 +0900 Subject: [PATCH 105/152] test(ci): require existing PR policy-file guard --- .../HourlyOpenCodeCandidateSelectionTest.java | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java index a7dcc4b5..784754fa 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java @@ -12,11 +12,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Guards the jq variable scope used to remove an updated PR branch from new-branch candidates. + * Guards the candidate-selection and publication boundaries used by the hourly OpenCode workflow. * *

Inside {@code $existing_refs | index(...)}, the jq input changes from the branch object to the * reference array. Reading {@code .name} after that pipe therefore fails at runtime. Production - * must capture the branch name first and pass the scalar variable to {@code index}.

+ * must capture the branch name first and pass the scalar variable to {@code index}. Updated + * existing pull requests must also be compared with their pre-agent heads so policy-file changes + * cannot bypass the new-branch publication guard.

*/ class HourlyOpenCodeCandidateSelectionTest { @@ -27,16 +29,72 @@ class HourlyOpenCodeCandidateSelectionTest { */ @Test void capturesBranchNameBeforeExistingReferenceLookup() throws IOException { - String workflow = Files.readString( - projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), - StandardCharsets.UTF_8 - ).replace("\r\n", "\n"); + String workflow = workflowText(); assertTrue(workflow.contains(".name as $branch_name")); assertTrue(workflow.contains("index($branch_name)")); assertFalse(workflow.contains("index(.name)")); } + /** + * Requires updated existing pull requests to reject agent-introduced policy-file changes. + * + *

The model job may update an already-open pull request. The deterministic publisher must + * therefore retain that pull request's pre-agent head, compare it with the candidate head, and + * apply the same {@code .github/**} and {@code CODEOWNERS} exclusion used for new branches.

+ * + * @throws IOException when the workflow cannot be read + */ + @Test + void rejectsPolicyChangesOnUpdatedExistingPullRequests() throws IOException { + String workflow = workflowText(); + String existingPrBlock = between( + workflow, + "if [[ \"${kind}\" == \"existing_pr\" ]]; then", + "elif [[ \"${kind}\" == \"new_branch\" ]]; then" + ); + + assertTrue(workflow.contains("before_head_sha: $before[$number_key]")); + assertTrue(existingPrBlock.contains( + "before_head=\"$(jq -r '.before_head_sha' <<<\"${AGENT_CANDIDATE}\")\"" + )); + assertTrue(existingPrBlock.contains( + "comparison=\"$(gh api \"/repos/${repository}/compare/${before_head}...${expected_head}\")\"" + )); + assertTrue(existingPrBlock.contains("startswith(\".github/\")")); + assertTrue(existingPrBlock.contains("or . == \"CODEOWNERS\"")); + assertTrue(existingPrBlock.contains("or endswith(\"/CODEOWNERS\")")); + } + + /** + * Reads the normalized hourly workflow text. + * + * @return workflow content with Unix line endings + * @throws IOException when the workflow cannot be read + */ + private static String workflowText() throws IOException { + return Files.readString( + projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + } + + /** + * Extracts a required text section between two unique markers. + * + * @param text complete source text + * @param startMarker inclusive section marker + * @param endMarker exclusive section marker + * @return required section text + */ + private static String between(String text, String startMarker, String endMarker) { + int start = text.indexOf(startMarker); + int end = text.indexOf(endMarker, start + startMarker.length()); + assertTrue(start >= 0, () -> "Missing start marker: " + startMarker); + assertTrue(end > start, () -> "Missing end marker after start: " + endMarker); + return text.substring(start, end); + } + /** * Finds the Maven reactor root from root-level or module-local execution. * From 91533ad0df070a9198599c553166c84a38e8dfb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:08:25 +0900 Subject: [PATCH 106/152] fix(ci): guard policy changes on updated PRs --- .../workflows/hourly-opencode-maintenance.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index d7c52e51..a0b53977 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -207,6 +207,7 @@ jobs: kind: "existing_pr", number: .number, head_ref: .head.ref, + before_head_sha: $before[$number_key], head_sha: .head.sha } ] @@ -294,6 +295,11 @@ jobs: if [[ "${kind}" == "existing_pr" ]]; then number="$(jq -r '.number' <<<"${AGENT_CANDIDATE}")" + before_head="$(jq -r '.before_head_sha' <<<"${AGENT_CANDIDATE}")" + if ! [[ "${before_head}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Updated pull request candidate has an invalid pre-agent head SHA" >&2 + exit 1 + fi live="$(gh api "/repos/${repository}/pulls/${number}")" jq -e \ --arg repo "${repository}" \ @@ -309,6 +315,32 @@ jobs: and .head.sha == $head_sha ) ' >/dev/null <<<"${live}" + + comparison="$(gh api "/repos/${repository}/compare/${before_head}...${expected_head}")" + if ! jq -e ' + .status == "ahead" + and .ahead_by >= 1 + and .behind_by == 0 + ' >/dev/null <<<"${comparison}"; then + echo "Updated pull request head is not a non-destructive descendant of its pre-agent head" >&2 + exit 1 + fi + file_count="$(jq '.files | length' <<<"${comparison}")" + if [[ "${file_count}" -gt 50 ]]; then + echo "Updated pull request exceeds the bounded 50-file publication limit" >&2 + exit 1 + fi + if jq -e ' + any( + .files[].filename; + startswith(".github/") + or . == "CODEOWNERS" + or endswith("/CODEOWNERS") + ) + ' >/dev/null <<<"${comparison}"; then + echo "Updated pull request changes .github or CODEOWNERS policy and requires manual handling" >&2 + exit 1 + fi elif [[ "${kind}" == "new_branch" ]]; then if ! [[ "${head_ref}" =~ ^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$ ]]; then echo "Agent branch does not match the strict publication namespace" >&2 From 6b698be1e90a01c236ed8423b742719c8830ae26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:09:31 +0900 Subject: [PATCH 107/152] docs(ci): document updated PR policy guard --- docs/operations/hourly-opencode-maintenance.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index 6d72a119..d0a6f0bf 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -80,7 +80,7 @@ Before OpenCode starts, the job snapshots: After OpenCode exits, the job requires `develop` to remain unchanged and selects at most one candidate: -- one existing pull request whose head moved; or +- one existing pull request whose head moved, retaining both the pre-agent and post-agent exact heads; or - one new or advanced branch matching `automation/opencode-YYYYMMDDTHHMMSSZ-short-slug`. Multiple candidates fail closed. No candidate is represented as a no-op, not as successful product development. @@ -89,7 +89,15 @@ Multiple candidates fail closed. No candidate is represented as a no-op, not as This job has `contents: read` and the workflow's sole `pull-requests: write` grant. It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. -For an existing pull request, it only re-reads and validates repository, base branch, head branch, state, and exact SHA. For a new branch, it additionally requires: +For an existing pull request, it re-reads and validates repository, base branch, head branch, state, and exact SHA. It then compares the captured pre-agent head with the post-agent head and requires all of the following: + +- the post-agent head is a non-destructive descendant with at least one new commit and no commits behind the captured head; +- no more than 50 files changed during the agent update; +- the agent-introduced range contains no `.github/**` or `CODEOWNERS` change. + +This range-specific comparison prevents an already-open pull request from bypassing the same policy-file boundary applied to a newly created automation branch. Policy files that existed before the agent run do not authorize the model to modify them during that run. + +For a new branch, the publisher requires: - the strict `automation/opencode-*` namespace; - the live branch SHA to equal the candidate SHA; @@ -148,7 +156,9 @@ The model must not: | OpenCode archive or checksum mismatch | fail before extraction or execution | | protected `develop` head moves during the run | fail as indeterminate publication evidence | | more than one candidate branch or PR changes | fail as ambiguous model output | +| updated PR head is not a non-destructive descendant of its captured head | refuse publication | | candidate branch is not ahead of `develop` | refuse publication | +| agent-introduced range exceeds 50 files | refuse publication | | candidate changes `.github/**` or `CODEOWNERS` | require explicit human handling | | live PR or branch SHA differs from the captured SHA | fail closed | | workflow run lacks exact PR association | exclude it from authorization | @@ -162,7 +172,7 @@ A failed OpenCode step is preserved as a failed job after candidate evidence is Disable **Hourly OpenCode maintenance** to stop the schedule immediately. Permanent rollback must revert the workflow, contract tests, this operations document, doctoring evidence, design and plan records, and corresponding changelog material through an independently reviewed pull request. -Do not restore `pull-requests: write` to the model job. Do not remove exact pull-request association from workflow-run authorization. A replacement GitHub App or token broker requires separate evidence for endpoint-level capability, actor identity, secret lifecycle, exact-head binding, and independent review. +Do not restore `pull-requests: write` to the model job. Do not remove exact pull-request association from workflow-run authorization. Do not remove the pre-agent versus post-agent range check from updated pull requests. A replacement GitHub App or token broker requires separate evidence for endpoint-level capability, actor identity, secret lifecycle, exact-head binding, and independent review. ## Verification checklist @@ -176,6 +186,8 @@ Before merge, verify on the exact head: - the immutable OpenCode archive and action pins remain unchanged; - the model job has `pull-requests: read`, not write; - the publisher is the only holder of `pull-requests: write` and performs no checkout; +- updated existing pull requests retain their captured pre-agent head and reject destructive ancestry, more than 50 agent-introduced files, `.github/**`, and `CODEOWNERS` changes; +- new branches reject more than 50 files, `.github/**`, and `CODEOWNERS` changes; - the authorizer is the only holder of `actions: write` and performs no checkout; - every authorized run is associated with the exact pull-request number and SHA; - the workflow contains no pull-request review or merge operation. From 2f374446b0e0dc180c53736787a2a7a9b331503f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:10:47 +0900 Subject: [PATCH 108/152] docs(changelog): record existing PR policy guard --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c53829..508e7623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - The hourly OpenCode workflow now snapshots same-repository `develop` pull-request heads before the agent runs and uses job-scoped Actions write authority only to authorize approval-required workflow runs for an unchanged exact head; `.github/**` and `CODEOWNERS` changes remain human-authorized, and no review or merge authority is added. +- Updated existing pull-request candidates now carry their captured pre-agent head into the deterministic publisher, which rejects destructive ancestry, more than 50 agent-introduced files, and any agent-introduced `.github/**` or `CODEOWNERS` change before exposing the updated pull request or authorizing checks. - The hourly OpenCode workflow now uses the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint for long-context coding and agentic tool use instead of the deprecated Qwen3 Coder free endpoint; model or endpoint rejection fails visibly without a non-NVIDIA, partner-only, or automatic fallback. - The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. @@ -136,8 +137,6 @@ Through code analysis, identified the platform as: - **Enterprise ETL and CDC Platform** - Microservices-based architecture using Spring Cloud - Real-time Change Data Capture using Debezium -- Data transformation pipelines with parallel processing -- JWT-based security with role-based access control - Event streaming via Apache Kafka - Service discovery with Netflix Eureka - Distributed tracing with Zipkin @@ -258,5 +257,5 @@ This changelog will be updated: --- **Changelog Version**: 1.0 -**Last Updated**: 2026-08-05 -**Maintained By**: Development Team \ No newline at end of file +**Last Updated**: 2026-08-06 +**Maintained By**: Development Team From 9bbd20b42967b8401776d9399cec6a5c24aa4512 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:44:54 +0900 Subject: [PATCH 109/152] test(ci): expose synthetic merge workflow evidence --- .../ExactHeadWorkflowCheckoutTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/ExactHeadWorkflowCheckoutTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ExactHeadWorkflowCheckoutTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ExactHeadWorkflowCheckoutTest.java new file mode 100644 index 00000000..4c484c18 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ExactHeadWorkflowCheckoutTest.java @@ -0,0 +1,86 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards pull-request quality evidence against GitHub's generated merge revision. + * + *

A pull-request workflow normally checks out {@code github.sha}, which is the synthetic merge + * commit rather than the contributor branch's exact current head. Branch protection may consume + * those results, but mightyETL's expected-head policy also requires direct evidence for the literal + * head SHA. Each source-executing workflow therefore binds checkout and an explicit identity + * assertion to the pull-request head, while push runs fall back to their event SHA.

+ */ +class ExactHeadWorkflowCheckoutTest { + + private static final String EXACT_SOURCE_EXPRESSION = + "${{ github.event.pull_request.head.sha || github.sha }}"; + + @Test + void continuousIntegrationChecksOutAndAssertsTheExactSourceRevision() throws IOException { + assertExactHeadCheckout(".github/workflows/ci.yml"); + } + + @Test + void sbomGenerationChecksOutAndAssertsTheExactSourceRevision() throws IOException { + assertExactHeadCheckout(".github/workflows/sbom.yml"); + } + + private static void assertExactHeadCheckout(String relativePath) throws IOException { + String workflow = Files.readString( + projectRoot().resolve(relativePath), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + + assertTrue( + workflow.contains("ref: " + EXACT_SOURCE_EXPRESSION), + relativePath + " must check out the literal pull-request head" + ); + assertTrue( + workflow.contains("persist-credentials: false"), + relativePath + " must not persist the checkout credential" + ); + assertTrue( + workflow.contains( + "test \"$(git rev-parse HEAD)\" = \"" + EXACT_SOURCE_EXPRESSION + "\"" + ), + relativePath + " must fail when the checked-out revision is not the expected head" + ); + assertFalse( + workflow.contains("ref: ${{ github.sha }}"), + relativePath + " must not bind pull-request source execution to the merge revision" + ); + } + + /** + * Finds the repository root from repository-root or module-local Maven execution. + * + * @return repository root containing the workflow files + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 4ce080abf594b3db606df1ba3de37173762f33bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:48:21 +0900 Subject: [PATCH 110/152] fix(ci): execute quality gates on exact source head --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e71e0b9d..1535498a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,17 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - name: Checkout + - name: Checkout exact source revision uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Verify exact source revision + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" - name: Set up Java uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 @@ -119,8 +128,27 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && inputs.use_self_hosted == true }} runs-on: self-hosted steps: - - name: Checkout + - name: Checkout exact source revision uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Verify exact source revision (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" + + - name: Verify exact source revision (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $actualHead = git rev-parse HEAD + if ($actualHead -ne "${{ github.event.pull_request.head.sha || github.sha }}") { + throw "Checked-out revision does not match the expected source SHA." + } - name: Set up Java uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 From 91f6dcb5c4b472760ef8c1ea67be34cb7b471353 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:49:27 +0900 Subject: [PATCH 111/152] fix(sbom): bind inventory generation to exact source head --- .github/workflows/sbom.yml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 26aefe40..f5ee166c 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -24,8 +24,17 @@ jobs: if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.use_self_hosted == false) }} runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout exact source revision uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Verify exact source revision + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" - name: Set up Java uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 @@ -49,8 +58,27 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && inputs.use_self_hosted == true }} runs-on: self-hosted steps: - - name: Checkout + - name: Checkout exact source revision uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Verify exact source revision (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" + + - name: Verify exact source revision (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $actualHead = git rev-parse HEAD + if ($actualHead -ne "${{ github.event.pull_request.head.sha || github.sha }}") { + throw "Checked-out revision does not match the expected source SHA." + } - name: Set up Java uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 From 16ebda44071c695ec6b7a20e4b2d913cfdad2c50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:50:57 +0900 Subject: [PATCH 112/152] docs(ci): record exact-head source evidence boundary --- .../exact-head-source-workflow-evidence.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/doctoring/exact-head-source-workflow-evidence.md diff --git a/docs/doctoring/exact-head-source-workflow-evidence.md b/docs/doctoring/exact-head-source-workflow-evidence.md new file mode 100644 index 00000000..ddd5f369 --- /dev/null +++ b/docs/doctoring/exact-head-source-workflow-evidence.md @@ -0,0 +1,70 @@ +# Exact-head CI and SBOM source evidence + +## Incident + +The `CI` and `SBOM (CycloneDX)` pull-request workflows previously relied on the default `actions/checkout` ref. GitHub defines `GITHUB_SHA` for a `pull_request` event as the last commit on the generated pull-request merge branch, and the default checkout therefore materializes that synthetic merge revision rather than the pull request's literal current head. + +Run `31175322160` demonstrated the mismatch on pull request #121. The macOS job checked out synthetic merge commit `95f543c80dbfc43796179c12d8ceda3196cb9eeb`, whose message merged source head `9bbd20b42967b8401776d9399cec6a5c24aa4512` into base `622e5e6c3d534f230c390f10e3832efadfc01825`. The checkout also retained its repository credential. Those results could describe merge-preview compatibility, but they were not direct execution evidence for the exact source head and could not satisfy mightyETL's expected-head policy. + +## Decision + +Both source-executing workflows now bind checkout to: + +```yaml +ref: ${{ github.event.pull_request.head.sha || github.sha }} +persist-credentials: false +``` + +For `pull_request`, the expression selects the literal contributor head. For `push` and the existing manual test entrypoint, the pull-request payload is absent and the expression selects the event SHA. Each workflow immediately compares `git rev-parse HEAD` with the same expression and fails before toolchain setup when the materialized source does not match. + +The CI matrix performs this assertion on Ubuntu, macOS, and Windows. Self-hosted execution uses Bash on Unix and PowerShell on Windows but retains the identical expected SHA. SBOM generation applies the same boundary before Maven resolves the aggregate dependency graph. + +## Test-first evidence + +Commit `9bbd20b42967b8401776d9399cec6a5c24aa4512` added only `ExactHeadWorkflowCheckoutTest`. CI run `31175322160` then failed exactly two new assertions while the established test surface otherwise ran: + +```text +ExactHeadWorkflowCheckoutTest.continuousIntegrationChecksOutAndAssertsTheExactSourceRevision +ExactHeadWorkflowCheckoutTest.sbomGenerationChecksOutAndAssertsTheExactSourceRevision +``` + +The failure log independently exposed the synthetic merge checkout and persisted credential. The production workflow changes were applied only after this RED evidence. + +The permanent contract requires: + +- the literal pull-request head expression in both workflows; +- checkout credential persistence disabled; +- an explicit post-checkout identity assertion; and +- no hard binding to `github.sha`, which denotes the merge revision on `pull_request` events. + +## Authority boundary + +This repair changes the source being measured; it does not convert a check into approval and does not weaken branch protection. + +- The workflows remain `pull_request` workflows with repository permission `contents: read` only. +- No repository, model, cloud, deployment, or signing secret is exposed to pull-request source. +- The checkout credential is removed before Maven or project code executes. +- The workflow definition still follows GitHub's pull-request event semantics; the explicit ref affects the checked-out source tree, not the event or reviewer identity. +- Dependency Review remains a distinct control that compares the event's base and head dependency changes. +- Organization SAST and Security Scan evidence remains independently required and must itself be proven against the exact source head before merge. +- A green generated-merge run from an older head, a predecessor base, a queued run, or an absent workflow is not accepted as exact-head evidence. + +This pattern would be unsafe under a privileged `pull_request_target`, `workflow_run`, or comment-triggered workflow that exposes secrets or write authority to untrusted source. mightyETL does not use those privileged event shapes for these source-executing jobs. + +## Stack and review consequence + +Every change to the root stack head invalidates downstream ancestry and all older check, review, and approval evidence. After this repair passes its current exact head, each downstream branch must be advanced non-destructively to an auditable merge commit containing the exact predecessor head, then rerun its own exact-head gates. No predecessor evidence transfers. + +## Operations and rollback + +Operators should inspect the checkout log and exact identity step whenever the event payload, checkout action, or trigger changes. A passing run must show the expected source SHA as the checked-out `HEAD` before Maven execution. + +Rollback to implicit checkout is prohibited because it restores synthetic-merge-only evidence. If direct head checkout becomes unavailable, fail the workflow and investigate event metadata, repository access, or checkout behavior. Do not substitute an older head, a generated merge revision, or a manually asserted status. + +## References — APA 7th edition + +GitHub. (2026a). *Events that trigger workflows*. GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +GitHub. (2026b). *Securely using pull_request_target*. GitHub Docs. https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target + +GitHub. (2026c). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token From b24bed946d3da2e62d89ded828d877dac2f3c65c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:54:18 +0900 Subject: [PATCH 113/152] docs(changelog): record exact-head CI and SBOM evidence --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 508e7623..5e527f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - The hourly OpenCode workflow now snapshots same-repository `develop` pull-request heads before the agent runs and uses job-scoped Actions write authority only to authorize approval-required workflow runs for an unchanged exact head; `.github/**` and `CODEOWNERS` changes remain human-authorized, and no review or merge authority is added. @@ -28,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including the observed synthetic merge checkout, cross-platform source identity assertions, least-privilege boundary, stack invalidation rule, rollback prohibition, and APA 7th GitHub references. - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. - Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. @@ -257,5 +259,5 @@ This changelog will be updated: --- **Changelog Version**: 1.0 -**Last Updated**: 2026-08-06 +**Last Updated**: 2026-08-07 **Maintained By**: Development Team From 077340a62e267f3dfbe05099b137bec57c11a5ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:15:45 +0900 Subject: [PATCH 114/152] test(ci): require exact dependency review refs --- ...DependencyReviewExactHeadWorkflowTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java new file mode 100644 index 00000000..0b3a574b --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java @@ -0,0 +1,55 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Keeps dependency-delta evidence explicitly bound to the pull request's exact base and head SHAs. + */ +class DependencyReviewExactHeadWorkflowTest { + + @Test + void dependencyReviewPassesExactEventBaseAndHeadToPinnedAction() throws IOException { + String workflow = Files.readString( + projectRoot().resolve(".github/workflows/dependency-review.yml"), + StandardCharsets.UTF_8 + ).replaceAll("\\s+", " "); + + assertTrue(workflow.contains( + "base-ref: ${{ github.event.pull_request.base.sha }}" + )); + assertTrue(workflow.contains( + "head-ref: ${{ github.event.pull_request.head.sha }}" + )); + assertTrue(workflow.contains( + "uses: actions/dependency-review-action@" + + "a1d282b36b6f3519aa1f3fc636f609c47dddb294" + )); + } + + /** @return repository root from reactor-root or module-local execution */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 0b947a691a7114a31a3ba11be50c8c3f484ac838 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:22:20 +0900 Subject: [PATCH 115/152] fix(ci): bind dependency review to exact PR refs --- .github/workflows/dependency-review.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 98509889..e517a7c4 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -18,6 +18,8 @@ jobs: - name: Dependency Review uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: + base-ref: ${{ github.event.pull_request.base.sha }} + head-ref: ${{ github.event.pull_request.head.sha }} fail-on-severity: high dependency-review-private-note: From 3185f26e06af67cc4938a4a8b442c4917355dad2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:24:41 +0900 Subject: [PATCH 116/152] docs(ci): record exact dependency review evidence --- .../exact-head-source-workflow-evidence.md | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/docs/doctoring/exact-head-source-workflow-evidence.md b/docs/doctoring/exact-head-source-workflow-evidence.md index ddd5f369..9458c195 100644 --- a/docs/doctoring/exact-head-source-workflow-evidence.md +++ b/docs/doctoring/exact-head-source-workflow-evidence.md @@ -1,12 +1,12 @@ -# Exact-head CI and SBOM source evidence +# Exact-head CI, SBOM, and Dependency Review source evidence -## Incident +## Incident: source-executing checks The `CI` and `SBOM (CycloneDX)` pull-request workflows previously relied on the default `actions/checkout` ref. GitHub defines `GITHUB_SHA` for a `pull_request` event as the last commit on the generated pull-request merge branch, and the default checkout therefore materializes that synthetic merge revision rather than the pull request's literal current head. Run `31175322160` demonstrated the mismatch on pull request #121. The macOS job checked out synthetic merge commit `95f543c80dbfc43796179c12d8ceda3196cb9eeb`, whose message merged source head `9bbd20b42967b8401776d9399cec6a5c24aa4512` into base `622e5e6c3d534f230c390f10e3832efadfc01825`. The checkout also retained its repository credential. Those results could describe merge-preview compatibility, but they were not direct execution evidence for the exact source head and could not satisfy mightyETL's expected-head policy. -## Decision +## Decision: source-executing checks Both source-executing workflows now bind checkout to: @@ -19,7 +19,7 @@ For `pull_request`, the expression selects the literal contributor head. For `pu The CI matrix performs this assertion on Ubuntu, macOS, and Windows. Self-hosted execution uses Bash on Unix and PowerShell on Windows but retains the identical expected SHA. SBOM generation applies the same boundary before Maven resolves the aggregate dependency graph. -## Test-first evidence +## Test-first evidence: source-executing checks Commit `9bbd20b42967b8401776d9399cec6a5c24aa4512` added only `ExactHeadWorkflowCheckoutTest`. CI run `31175322160` then failed exactly two new assertions while the established test surface otherwise ran: @@ -37,15 +37,40 @@ The permanent contract requires: - an explicit post-checkout identity assertion; and - no hard binding to `github.sha`, which denotes the merge revision on `pull_request` events. +## Incident and decision: dependency-delta evidence + +Dependency Review does not execute contributor source, but it evaluates the dependency delta between two revisions. Relying on an action's implicit event defaults makes that evidence less explicit than mightyETL's exact-head gate requires, especially after a stacked predecessor or head moves. + +Fail-first commit `077340a62e267f3dfbe05099b137bec57c11a5ae` added `DependencyReviewExactHeadWorkflowTest`, requiring the pinned GitHub Dependency Review Action to receive the pull-request event's exact base and head SHAs. CI run `31177454329` failed on that test while the existing workflow still supplied only `fail-on-severity`. + +Green repair `0b947a691a7114a31a3ba11be50c8c3f484ac838` changed `.github/workflows/dependency-review.yml` to pass: + +```yaml +with: + base-ref: ${{ github.event.pull_request.base.sha }} + head-ref: ${{ github.event.pull_request.head.sha }} + fail-on-severity: high +``` + +The workflow remains pinned to `actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294` (v5.0.0). The pinned action's `action.yml` declares both `base-ref` and `head-ref` as supported optional inputs, so the repair uses the action's reviewed public contract rather than undocumented behavior. + +The permanent Dependency Review contract requires: + +- explicit event base SHA passed as `base-ref`; +- explicit event head SHA passed as `head-ref`; +- an immutable action commit pin; +- fail-closed severity policy retained; and +- no reuse of a successful dependency delta after either endpoint moves. + ## Authority boundary -This repair changes the source being measured; it does not convert a check into approval and does not weaken branch protection. +These repairs change which exact revisions are measured; they do not convert checks into approval and do not weaken branch protection. - The workflows remain `pull_request` workflows with repository permission `contents: read` only. - No repository, model, cloud, deployment, or signing secret is exposed to pull-request source. -- The checkout credential is removed before Maven or project code executes. -- The workflow definition still follows GitHub's pull-request event semantics; the explicit ref affects the checked-out source tree, not the event or reviewer identity. -- Dependency Review remains a distinct control that compares the event's base and head dependency changes. +- The CI/SBOM checkout credential is removed before Maven or project code executes. +- The workflow definition still follows GitHub's pull-request event semantics; the explicit checkout ref affects the checked-out source tree, not the event or reviewer identity. +- Dependency Review receives exact event endpoint SHAs and remains a distinct dependency-delta control. - Organization SAST and Security Scan evidence remains independently required and must itself be proven against the exact source head before merge. - A green generated-merge run from an older head, a predecessor base, a queued run, or an absent workflow is not accepted as exact-head evidence. @@ -53,13 +78,17 @@ This pattern would be unsafe under a privileged `pull_request_target`, `workflow ## Stack and review consequence -Every change to the root stack head invalidates downstream ancestry and all older check, review, and approval evidence. After this repair passes its current exact head, each downstream branch must be advanced non-destructively to an auditable merge commit containing the exact predecessor head, then rerun its own exact-head gates. No predecessor evidence transfers. +Every change to the root stack head invalidates downstream ancestry and all older check, review, and approval evidence. After this repair passes its current exact head, each downstream branch must be advanced non-destructively to an auditable history containing the exact predecessor head, then rerun its own exact-head gates. No predecessor evidence transfers. + +For Dependency Review specifically, any base or head movement also invalidates the previous dependency delta even when the dependency manifest itself appears unchanged. The workflow must rerun with the new event endpoints. ## Operations and rollback -Operators should inspect the checkout log and exact identity step whenever the event payload, checkout action, or trigger changes. A passing run must show the expected source SHA as the checked-out `HEAD` before Maven execution. +Operators should inspect the checkout log and exact identity step whenever the event payload, checkout action, or trigger changes. A passing source-executing run must show the expected source SHA as the checked-out `HEAD` before Maven execution. + +Operators should inspect the Dependency Review run inputs whenever stacked ancestry changes. A passing dependency review must correspond to the event's exact current base and head SHA pair. -Rollback to implicit checkout is prohibited because it restores synthetic-merge-only evidence. If direct head checkout becomes unavailable, fail the workflow and investigate event metadata, repository access, or checkout behavior. Do not substitute an older head, a generated merge revision, or a manually asserted status. +Rollback to implicit source checkout or implicit Dependency Review endpoints is prohibited because either restores ambiguous evidence. If exact endpoint binding becomes unavailable, fail the workflow and investigate event metadata or action behavior. Do not substitute an older head, older base, generated merge revision, or manually asserted status. ## References — APA 7th edition @@ -68,3 +97,5 @@ GitHub. (2026a). *Events that trigger workflows*. GitHub Docs. https://docs.gith GitHub. (2026b). *Securely using pull_request_target*. GitHub Docs. https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target GitHub. (2026c). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token + +GitHub. (2026d). *Dependency review action* (v5.0.0, commit a1d282b36b6f3519aa1f3fc636f609c47dddb294) [GitHub Action]. GitHub. https://github.com/actions/dependency-review-action From cd706f235f9ddda4ee0d7244772453f2f5c934a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:25:43 +0900 Subject: [PATCH 117/152] test(ci): reject ignored dependency review ref overrides --- ...DependencyReviewExactHeadWorkflowTest.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java index 0b3a574b..615a9d76 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/DependencyReviewExactHeadWorkflowTest.java @@ -8,30 +8,34 @@ import java.nio.file.Path; import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Keeps dependency-delta evidence explicitly bound to the pull request's exact base and head SHAs. + * Keeps dependency review bound to GitHub pull-request event semantics and an immutable action revision. + * + *

The dependency-review action derives the pull request base and head from the event payload for + * {@code pull_request} and {@code pull_request_target}. Its {@code base-ref}/{@code head-ref} inputs + * are documented for other event types only, so supplying them here would be ignored and would create + * misleading exact-head evidence.

*/ class DependencyReviewExactHeadWorkflowTest { @Test - void dependencyReviewPassesExactEventBaseAndHeadToPinnedAction() throws IOException { + void dependencyReviewUsesPullRequestEventRefsWithPinnedAction() throws IOException { String workflow = Files.readString( projectRoot().resolve(".github/workflows/dependency-review.yml"), StandardCharsets.UTF_8 ).replaceAll("\\s+", " "); - assertTrue(workflow.contains( - "base-ref: ${{ github.event.pull_request.base.sha }}" - )); - assertTrue(workflow.contains( - "head-ref: ${{ github.event.pull_request.head.sha }}" - )); + assertTrue(workflow.contains("pull_request:")); + assertFalse(workflow.contains("base-ref:")); + assertFalse(workflow.contains("head-ref:")); assertTrue(workflow.contains( "uses: actions/dependency-review-action@" + "a1d282b36b6f3519aa1f3fc636f609c47dddb294" )); + assertTrue(workflow.contains("fail-on-severity: high")); } /** @return repository root from reactor-root or module-local execution */ From 4c30a17dbd0da2737eec92a24a7306c5c2d63b1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:25:45 +0900 Subject: [PATCH 118/152] docs(changelog): record exact dependency review binding --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e527f50..269c5889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. +- Dependency Review now passes the pull-request event's exact base and head SHAs explicitly to the immutably pinned GitHub Dependency Review Action; dependency-delta evidence is invalidated whenever either endpoint moves instead of relying on implicit event defaults. - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - The hourly OpenCode workflow now snapshots same-repository `develop` pull-request heads before the agent runs and uses job-scoped Actions write authority only to authorize approval-required workflow runs for an unchanged exact head; `.github/**` and `CODEOWNERS` changes remain human-authorized, and no review or merge authority is added. @@ -29,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including the observed synthetic merge checkout, cross-platform source identity assertions, least-privilege boundary, stack invalidation rule, rollback prohibition, and APA 7th GitHub references. +- Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including observed synthetic-merge checkout behavior, cross-platform source identity assertions, explicit Dependency Review base/head binding, least-privilege boundaries, stack invalidation rules, rollback prohibition, and APA 7th GitHub references. - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. - Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. From 3358b40adaf52cc821e3fce923ecf5af49f77f7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:26:14 +0900 Subject: [PATCH 119/152] fix(ci): rely on pull-request dependency review refs --- .github/workflows/dependency-review.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index e517a7c4..98509889 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -18,8 +18,6 @@ jobs: - name: Dependency Review uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: - base-ref: ${{ github.event.pull_request.base.sha }} - head-ref: ${{ github.event.pull_request.head.sha }} fail-on-severity: high dependency-review-private-note: From 6b6afb9635930e75ff5b35e77cc063d822551c14 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:27:03 +0900 Subject: [PATCH 120/152] docs(ci): correct dependency review endpoint evidence --- .../exact-head-source-workflow-evidence.md | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/doctoring/exact-head-source-workflow-evidence.md b/docs/doctoring/exact-head-source-workflow-evidence.md index 9458c195..474dca88 100644 --- a/docs/doctoring/exact-head-source-workflow-evidence.md +++ b/docs/doctoring/exact-head-source-workflow-evidence.md @@ -30,65 +30,65 @@ ExactHeadWorkflowCheckoutTest.sbomGenerationChecksOutAndAssertsTheExactSourceRev The failure log independently exposed the synthetic merge checkout and persisted credential. The production workflow changes were applied only after this RED evidence. -The permanent contract requires: +The permanent source-execution contract requires: - the literal pull-request head expression in both workflows; - checkout credential persistence disabled; - an explicit post-checkout identity assertion; and - no hard binding to `github.sha`, which denotes the merge revision on `pull_request` events. -## Incident and decision: dependency-delta evidence +## Dependency Review event-endpoint contract -Dependency Review does not execute contributor source, but it evaluates the dependency delta between two revisions. Relying on an action's implicit event defaults makes that evidence less explicit than mightyETL's exact-head gate requires, especially after a stacked predecessor or head moves. +Dependency Review does not execute contributor source. It asks GitHub's dependency-review service to evaluate the dependency delta represented by a pull-request event. The action's contract therefore differs from source-executing CI and SBOM checks. -Fail-first commit `077340a62e267f3dfbe05099b137bec57c11a5ae` added `DependencyReviewExactHeadWorkflowTest`, requiring the pinned GitHub Dependency Review Action to receive the pull-request event's exact base and head SHAs. CI run `31177454329` failed on that test while the existing workflow still supplied only `fail-on-severity`. +Fail-first commit `077340a62e267f3dfbe05099b137bec57c11a5ae` originally asserted that the workflow must pass the pull request's base and head SHAs through the action's `base-ref` and `head-ref` inputs. CI run `31177454329` failed that new assertion while the established workflow used only the pull-request event and `fail-on-severity`. -Green repair `0b947a691a7114a31a3ba11be50c8c3f484ac838` changed `.github/workflows/dependency-review.yml` to pass: +A subsequent repair added those inputs. Review against the current upstream `actions/dependency-review-action` v5 documentation showed that premise to be incorrect: `base-ref` and `head-ref` are supported inputs, but they are **only used for event types other than `pull_request` and `pull_request_target`**. Supplying them to this `pull_request` workflow is therefore ignored and can create false confidence that an explicit binding exists when the action is actually using the event endpoints. -```yaml -with: - base-ref: ${{ github.event.pull_request.base.sha }} - head-ref: ${{ github.event.pull_request.head.sha }} - fail-on-severity: high -``` +The corrective TDD sequence preserves both findings without rewriting history: -The workflow remains pinned to `actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294` (v5.0.0). The pinned action's `action.yml` declares both `base-ref` and `head-ref` as supported optional inputs, so the repair uses the action's reviewed public contract rather than undocumented behavior. +- `077340a62e267f3dfbe05099b137bec57c11a5ae` remains the original fail-first experiment; +- the intervening commits that added explicit `base-ref`/`head-ref` remain auditable as an incorrect repair; +- `cd706f235f9ddda4ee0d7244772453f2f5c934a5` changes the contract test first so ignored PR-event overrides are rejected; with the overrides still present, this is corrective RED evidence; +- `3358b40adaf52cc821e3fce923ecf5af49f77f7f` removes the ignored inputs and returns the workflow to the action's documented pull-request semantics. The permanent Dependency Review contract requires: -- explicit event base SHA passed as `base-ref`; -- explicit event head SHA passed as `head-ref`; -- an immutable action commit pin; -- fail-closed severity policy retained; and -- no reuse of a successful dependency delta after either endpoint moves. +- trigger through `pull_request` so the action obtains the comparison endpoints from the pull-request event; +- immutable pin `actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294` (v5.0.0); +- `fail-on-severity: high` retained; +- no `base-ref` or `head-ref` override on the pull-request workflow, because upstream documents those inputs for non-pull-request events only; and +- no reuse of a successful dependency-review run after either the pull-request head or base moves. + +This is an evidence correction, not a gate relaxation. The dependency review must still rerun for the current pull-request event after any endpoint change, and queued, pending, absent, skipped, neutral, cancelled, failed, or stale-event evidence is not accepted. ## Authority boundary -These repairs change which exact revisions are measured; they do not convert checks into approval and do not weaken branch protection. +These controls change which exact revisions are measured or how their provenance is established; they do not convert checks into approval and do not weaken branch protection. - The workflows remain `pull_request` workflows with repository permission `contents: read` only. - No repository, model, cloud, deployment, or signing secret is exposed to pull-request source. - The CI/SBOM checkout credential is removed before Maven or project code executes. -- The workflow definition still follows GitHub's pull-request event semantics; the explicit checkout ref affects the checked-out source tree, not the event or reviewer identity. -- Dependency Review receives exact event endpoint SHAs and remains a distinct dependency-delta control. +- The CI/SBOM checkout ref affects the checked-out source tree, not the event or reviewer identity. +- Dependency Review derives its base/head comparison from the pull-request event according to the pinned action's documented contract; mightyETL verifies freshness by accepting only a run associated with the unchanged current pull request rather than by supplying ignored inputs. - Organization SAST and Security Scan evidence remains independently required and must itself be proven against the exact source head before merge. - A green generated-merge run from an older head, a predecessor base, a queued run, or an absent workflow is not accepted as exact-head evidence. -This pattern would be unsafe under a privileged `pull_request_target`, `workflow_run`, or comment-triggered workflow that exposes secrets or write authority to untrusted source. mightyETL does not use those privileged event shapes for these source-executing jobs. +This source-checkout pattern would be unsafe under a privileged `pull_request_target`, `workflow_run`, or comment-triggered workflow that exposes secrets or write authority to untrusted source. mightyETL does not use those privileged event shapes for these source-executing jobs. ## Stack and review consequence Every change to the root stack head invalidates downstream ancestry and all older check, review, and approval evidence. After this repair passes its current exact head, each downstream branch must be advanced non-destructively to an auditable history containing the exact predecessor head, then rerun its own exact-head gates. No predecessor evidence transfers. -For Dependency Review specifically, any base or head movement also invalidates the previous dependency delta even when the dependency manifest itself appears unchanged. The workflow must rerun with the new event endpoints. +For Dependency Review specifically, any base or head movement invalidates the previous dependency delta even when the dependency manifest itself appears unchanged. A fresh pull-request event run must complete for the unchanged current endpoints before merge evidence is accepted. ## Operations and rollback Operators should inspect the checkout log and exact identity step whenever the event payload, checkout action, or trigger changes. A passing source-executing run must show the expected source SHA as the checked-out `HEAD` before Maven execution. -Operators should inspect the Dependency Review run inputs whenever stacked ancestry changes. A passing dependency review must correspond to the event's exact current base and head SHA pair. +For Dependency Review, operators should confirm that the run belongs to the current pull request after its latest base/head movement and that the workflow remains a `pull_request` workflow using the immutable action pin. Do not infer stronger evidence from `base-ref`/`head-ref` inputs on a pull-request event because upstream documents those inputs as unused for that event type. -Rollback to implicit source checkout or implicit Dependency Review endpoints is prohibited because either restores ambiguous evidence. If exact endpoint binding becomes unavailable, fail the workflow and investigate event metadata or action behavior. Do not substitute an older head, older base, generated merge revision, or manually asserted status. +Rollback to implicit CI/SBOM source checkout is prohibited because it restores synthetic-merge-only execution evidence. Reintroducing ignored Dependency Review endpoint overrides is also prohibited because it restores misleading evidence. If the upstream action changes its event/ref contract, fail closed, review the pinned action and primary documentation, update tests and doctoring first, and rerun the current pull request. Do not substitute an older head, older base, generated merge revision, or manually asserted status. ## References — APA 7th edition From d1cae1189870f314445e934e9969fa9a70c1b7b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 21:29:20 +0900 Subject: [PATCH 121/152] docs(changelog): correct dependency review event semantics --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 269c5889..840c89a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. -- Dependency Review now passes the pull-request event's exact base and head SHAs explicitly to the immutably pinned GitHub Dependency Review Action; dependency-delta evidence is invalidated whenever either endpoint moves instead of relying on implicit event defaults. +- Dependency Review now relies on the immutably pinned GitHub Dependency Review Action's documented `pull_request` event endpoints; ignored `base-ref`/`head-ref` overrides are prohibited on pull-request runs, and dependency-delta evidence is invalidated whenever either endpoint moves. - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. - The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`. - The hourly OpenCode workflow now snapshots same-repository `develop` pull-request heads before the agent runs and uses job-scoped Actions write authority only to authorize approval-required workflow runs for an unchanged exact head; `.github/**` and `CODEOWNERS` changes remain human-authorized, and no review or merge authority is added. @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including observed synthetic-merge checkout behavior, cross-platform source identity assertions, explicit Dependency Review base/head binding, least-privilege boundaries, stack invalidation rules, rollback prohibition, and APA 7th GitHub references. +- Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including observed synthetic-merge checkout behavior, cross-platform source identity assertions, the rejected ignored Dependency Review ref-override experiment, corrective pull-request event-endpoint semantics, least-privilege boundaries, stack invalidation rules, rollback prohibition, and APA 7th GitHub references. - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. - Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`. From 7efcca206ab2497fb0b6293e3537b8be584b667b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 04:05:51 +0900 Subject: [PATCH 122/152] test: require read-only issue permission --- ...lyOpenCodeIssuePermissionWorkflowTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeIssuePermissionWorkflowTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeIssuePermissionWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeIssuePermissionWorkflowTest.java new file mode 100644 index 00000000..eaf008ae --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeIssuePermissionWorkflowTest.java @@ -0,0 +1,64 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards least-privilege issue access for the scheduled OpenCode maintenance workflow. + * + *

The maintenance agent may inspect issues while selecting one bounded product gap, but no + * current workflow operation mutates issue state. The repository token therefore requires only + * read access to issues; retaining {@code issues: write} would add unnecessary authority to the + * model-executing job.

+ */ +class HourlyOpenCodeIssuePermissionWorkflowTest { + + /** Verifies that the model-executing job can read issues but cannot mutate them. */ + @Test + void grantsReadOnlyIssuePermissionToMaintenanceAgent() throws IOException { + String workflow = Files.readString( + projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + + int maintenanceStart = workflow.indexOf(" maintain-repository:"); + int publisherStart = workflow.indexOf(" publish-agent-pull-request:"); + assertTrue(maintenanceStart >= 0, "The maintenance job must exist"); + assertTrue(publisherStart > maintenanceStart, "The publisher must follow maintenance"); + + String maintenanceJob = workflow.substring(maintenanceStart, publisherStart); + assertTrue(maintenanceJob.contains("issues: read")); + assertFalse(maintenanceJob.contains("issues: write")); + } + + /** + * Finds the repository root from either reactor-root or module-local execution. + * + * @return absolute repository root + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 171e0a80d45a89ad9d54dace88d63190ff9dcb0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 04:07:34 +0900 Subject: [PATCH 123/152] fix: restrict maintenance issue access --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index a0b53977..dfd4b919 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -17,7 +17,7 @@ jobs: actions: read checks: read contents: write - issues: write + issues: read pull-requests: read security-events: read statuses: read From c7ef4a8a4fd8c6d4e9f8c4e8160d2539a18e10a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 04:08:45 +0900 Subject: [PATCH 124/152] docs: document read-only issue authority --- docs/operations/hourly-opencode-maintenance.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/operations/hourly-opencode-maintenance.md b/docs/operations/hourly-opencode-maintenance.md index d0a6f0bf..1fd4ba9b 100644 --- a/docs/operations/hourly-opencode-maintenance.md +++ b/docs/operations/hourly-opencode-maintenance.md @@ -64,13 +64,13 @@ This is the only job that executes checked-out repository code and OpenCode. Its actions: read checks: read contents: write -issues: write +issues: read pull-requests: read security-events: read statuses: read ``` -`contents: write` permits one feature-branch push. It does not grant pull-request review or merge endpoints. The model prompt additionally forbids direct pull-request creation, update, approval, closure, or merge, but the permission map—not the prompt—is the primary authorization boundary. +`contents: write` permits one feature-branch push. `issues: read` permits issue and roadmap inspection only; the model-executing job has no issue mutation authority. Neither permission grants pull-request review or merge endpoints. The model prompt additionally forbids direct pull-request creation, update, approval, closure, or merge, but the permission map—not the prompt—is the primary authorization boundary. Before OpenCode starts, the job snapshots: @@ -185,6 +185,7 @@ Before merge, verify on the exact head: - only `NVIDIA_NIM_API_KEY` is referenced as a model secret; - the immutable OpenCode archive and action pins remain unchanged; - the model job has `pull-requests: read`, not write; +- the model job has `issues: read`, not write; - the publisher is the only holder of `pull-requests: write` and performs no checkout; - updated existing pull requests retain their captured pre-agent head and reject destructive ancestry, more than 50 agent-introduced files, `.github/**`, and `CODEOWNERS` changes; - new branches reject more than 50 files, `.github/**`, and `CODEOWNERS` changes; From c2ecc34e923af67cfcd1df546bb37325a040dba9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 04:09:46 +0900 Subject: [PATCH 125/152] docs: record least-privilege issue access --- CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 840c89a5..fa2833cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The model-executing hourly OpenCode maintenance job now has read-only issue access; fail-first workflow-contract coverage proves `issues: write` is unnecessary while preserving issue and roadmap inspection. - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. - Dependency Review now relies on the immutably pinned GitHub Dependency Review Action's documented `pull_request` event endpoints; ignored `base-ref`/`head-ref` overrides are prohibited on pull-request runs, and dependency-delta evidence is invalidated whenever either endpoint moves. - The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge. @@ -257,8 +258,4 @@ This changelog will be updated: - When documentation is significantly updated - For each release or milestone ---- - -**Changelog Version**: 1.0 -**Last Updated**: 2026-08-07 -**Maintained By**: Development Team +--- \ No newline at end of file From 727e7cae3092d042adb63e658cba627bf77f055a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 05:11:59 +0900 Subject: [PATCH 126/152] test(ci): require isolated branch writer --- ...HourlyOpenCodeMaintenanceWorkflowTest.java | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java index f5f48fa6..8754b508 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeMaintenanceWorkflowTest.java @@ -21,11 +21,12 @@ * Guards the credential, authority, supply-chain, publication, and exact-head validation * boundaries of the scheduled OpenCode maintenance workflow. * - *

The model may edit and push one bounded feature branch. It never receives pull-request write - * authority. A deterministic non-checkout publisher may create one draft pull request, while a - * second isolated non-checkout job may authorize only approval-required workflow runs associated - * with that exact pull request and exact head. These tests keep those authorities physically - * separated from model and repository-code execution.

+ *

The model may edit and commit one bounded feature-branch candidate, but it must never + * receive repository-content write authority. An isolated deterministic branch publisher may + * transfer the exact candidate commit, a second non-checkout publisher may create one draft pull + * request, and a third isolated non-checkout job may authorize only approval-required workflow + * runs associated with that exact pull request and exact head. These tests keep every write + * authority physically separated from model and repository-code execution.

*/ class HourlyOpenCodeMaintenanceWorkflowTest { @@ -87,14 +88,19 @@ void pinsCheckoutAndOpenCodeWithoutPersistedCredentials() { assertFalse(workflow.contains("anomalyco/opencode/github@")); } - /** Verifies ephemeral Git credentials for branch pushes and deterministic cleanup. */ + /** Verifies ephemeral Git credentials exist only for deterministic branch publication. */ @Test void bootstrapsAndRemovesDirectTokenGitCredentials() { + String maintenance = maintenanceJob(); + String branchPublisher = branchPublicationJob(); + assertTrue(workflow.contains("GH_TOKEN: ${{ github.token }}")); - assertTrue(workflow.contains( + assertFalse(maintenance.contains("git_credential_key=")); + assertFalse(maintenance.contains("git push")); + assertTrue(branchPublisher.contains( "git_credential_key=\"credential.https://github.com.helper\"" )); - assertTrue(workflow.contains("cleanup_git_credentials()")); + assertTrue(branchPublisher.contains("cleanup_git_credentials()")); assertTrue(workflow.contains("trap cleanup_git_credentials EXIT")); assertTrue(workflow.contains( "git config --local --add \"${git_credential_key}\" \"\"" @@ -130,22 +136,36 @@ void usesOnlyNvidiaNimWithPlainOpenCodeRun() { } /** - * Proves the model has read-only pull-request authority and both PR-writing jobs are - * deterministic non-checkout jobs without the NVIDIA credential. + * Proves all repository writes are isolated from the model-executing job and its NVIDIA + * credential, while pull-request and Actions write authorities remain separately bounded. */ @Test void isolatesPullRequestAndActionsWriteAuthorityFromTheAgent() { String maintenance = maintenanceJob(); + String branchPublisher = branchPublicationJob(); String publisher = publicationJob(); String authorizer = authorizationJob(); assertTrue(workflow.contains("permissions:\n contents: read\n\njobs:")); assertTrue(maintenance.contains("actions: read")); - assertTrue(maintenance.contains("contents: write")); + assertTrue(maintenance.contains("contents: read")); + assertFalse(maintenance.contains("contents: write")); assertTrue(maintenance.contains("pull-requests: read")); assertFalse(maintenance.contains("pull-requests: write")); assertFalse(maintenance.contains("actions: write")); + assertTrue(branchPublisher.contains("contents: write")); + assertTrue(branchPublisher.contains("actions: read")); + assertFalse(branchPublisher.contains("NVIDIA_API_KEY")); + assertFalse(branchPublisher.contains("opencode run")); + assertFalse(branchPublisher.contains("pull-requests: write")); + assertTrue(branchPublisher.contains( + "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + )); + assertTrue(maintenance.contains( + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + )); + assertTrue(publisher.contains("pull-requests: write")); assertTrue(publisher.contains("contents: read")); assertFalse(publisher.contains("actions/checkout@")); @@ -227,7 +247,12 @@ void promptForbidsPullRequestMutationMergeAndProtectedBranchPushes() { /** @return workflow text for the OpenCode execution job only */ private static String maintenanceJob() { - return jobSection(" maintain-repository:", " publish-agent-pull-request:"); + return jobSection(" maintain-repository:", " publish-agent-branch:"); + } + + /** @return workflow text for the isolated deterministic branch publisher only */ + private static String branchPublicationJob() { + return jobSection(" publish-agent-branch:", " publish-agent-pull-request:"); } /** @return workflow text for the deterministic draft-PR publisher only */ @@ -306,4 +331,4 @@ private static Path projectRoot() { } throw new IllegalStateException("Could not find project root"); } -} +} \ No newline at end of file From 2079a018d42b11ac7c94a70d1ef6dafd4b921ff8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 05:24:53 +0900 Subject: [PATCH 127/152] fix(ci): isolate model from repository writes --- .../workflows/hourly-opencode-maintenance.yml | 443 ++++++++++++++++-- 1 file changed, 413 insertions(+), 30 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index dfd4b919..1ef5efd2 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -16,7 +16,7 @@ jobs: permissions: actions: read checks: read - contents: write + contents: read issues: read pull-requests: read security-events: read @@ -26,6 +26,7 @@ jobs: automation_branch_heads_before: ${{ steps.snapshot_heads.outputs.automation_branch_heads }} develop_head_before: ${{ steps.snapshot_heads.outputs.develop_head }} agent_candidate: ${{ steps.detect_candidate.outputs.agent_candidate }} + has_candidate: ${{ steps.detect_candidate.outputs.has_candidate }} runs-on: ubuntu-latest timeout-minutes: 50 steps: @@ -128,11 +129,11 @@ jobs: PROMPT: | Maintain ContextualWisdomLab/mightyETL toward defensible commercial and acquisition readiness. - Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only source defects; the deterministic publisher and independent reviewers own pull-request lifecycle operations. + Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only source defects; deterministic non-model jobs and independent reviewers own remote branch publication and pull-request lifecycle operations. - When one dependency-eligible development pull request exists, check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, commit, and push only that existing branch. Do not create another branch for the same work. + When one dependency-eligible development pull request exists, fetch and check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and commit only to that local branch. Do not publish the branch remotely; the isolated deterministic branch publisher owns that authority. Do not create another branch for the same work. - When no development pull request exists, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Create and push exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug branch. Do not create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. + When no development pull request exists, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Use exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug local branch and commit the bounded slice there. Do not publish the branch remotely or create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services. Database objects must contain at least two descriptive words and use snake_case by default. Public production APIs require complete beginner-readable documentation. Added production statements and branches require deterministic 100% statement and branch coverage. Use current authoritative standards, primary technical documentation, and peer-reviewed evidence where material, with APA 7th references in repository documentation. @@ -140,7 +141,7 @@ jobs: Do not alter the existing review agent, its workflow, its provider configuration, or its credential flow. Do not change any review-agent secret name. Do not inspect secret values. Do not print, echo, summarize, or expose secret values. Do not modify .github/workflows/ or CODEOWNERS unless an open issue explicitly labeled automation-maintenance authorizes that exact bounded automation change; even then, leave the policy change for explicit human workflow authorization. - Use the repository-scoped token only for read operations and the one permitted feature-branch push. Keep the working tree truthful about tests, skipped coverage, remaining risks, and release readiness. The deterministic non-model publisher creates a draft pull request after validating the resulting branch. + Use the repository-scoped token for read operations only. Keep the working tree truthful about tests, skipped coverage, remaining risks, and release readiness. Commit every intended candidate change before returning. The deterministic non-model jobs validate and publish at most one exact local candidate. run: | set -euo pipefail if [[ -z "${NVIDIA_API_KEY}" ]]; then @@ -152,14 +153,21 @@ jobs: exit 1 fi - git_credential_key="credential.https://github.com.helper" - cleanup_git_credentials() { - git config --local --unset-all "${git_credential_key}" >/dev/null 2>&1 || true + askpass_script="${RUNNER_TEMP}/opencode-git-read-askpass.sh" + cleanup_read_credentials() { + rm -f "${askpass_script}" } - trap cleanup_git_credentials EXIT - cleanup_git_credentials - git config --local --add "${git_credential_key}" "" - git config --local --add "${git_credential_key}" "!gh auth git-credential" + trap cleanup_read_credentials EXIT + cat > "${askpass_script}" <<'ASKPASS' + #!/usr/bin/env bash + case "${1:-}" in + *Username*) printf '%s\n' 'x-access-token' ;; + *) printf '%s\n' "${GITHUB_TOKEN}" ;; + esac + ASKPASS + chmod 0700 "${askpass_script}" + export GIT_ASKPASS="${askpass_script}" + export GIT_TERMINAL_PROMPT=0 git config --local user.name "opencode-agent[bot]" git config --local user.email "opencode-agent[bot]@users.noreply.github.com" @@ -180,6 +188,7 @@ jobs: repository="${GITHUB_REPOSITORY}" after_pr_file="${RUNNER_TEMP}/open-pr-heads-after.json" after_branch_file="${RUNNER_TEMP}/automation-branch-heads-after.json" + candidate_dir="${RUNNER_TEMP}/opencode-candidate" jq -e 'select(type == "object")' >/dev/null <<<"${BEFORE_PR_HEADS}" jq -e 'select(type == "object")' >/dev/null <<<"${BEFORE_AUTOMATION_BRANCH_HEADS}" @@ -224,7 +233,6 @@ jobs: | select(($before[.name] // null) != .commit.sha) | .name as $branch_name | select(($existing_refs | index($branch_name)) == null) - | select(.name | test("^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$")) | { kind: "new_branch", head_ref: .name, @@ -235,19 +243,210 @@ jobs: )" candidates="$(jq -cn --argjson a "${changed_existing}" --argjson b "${changed_automation}" '$a + $b')" candidate_count="$(jq 'length' <<<"${candidates}")" - - if [[ "${candidate_count}" -gt 1 ]]; then - echo "Multiple agent publication candidates were detected; refusing ambiguous publication" >&2 + if [[ "${candidate_count}" -gt 0 ]]; then + echo "Multiple agent publication candidates were detected remotely; refusing to race another writer" >&2 jq . <<<"${candidates}" >&2 exit 1 fi - if [[ "${candidate_count}" -eq 0 ]]; then - printf 'agent_candidate=%s\n' '{"kind":"none"}' >> "${GITHUB_OUTPUT}" - exit 0 + + after_pr_heads="$( + jq -c --arg repo "${repository}" ' + map(select(.head.repo.full_name == $repo and .base.ref == "develop")) + | map({key: (.number | tostring), value: .head.sha}) + | from_entries + ' "${after_pr_file}" + )" + if ! jq -e --argjson before "${BEFORE_PR_HEADS}" --argjson after "${after_pr_heads}" \ + '$before == $after' >/dev/null; then + echo "Open pull-request state moved during the model job; refusing stale publication" >&2 + exit 1 + fi + after_automation_heads="$( + jq -c ' + map(select(.name | startswith("automation/opencode-"))) + | map({key: .name, value: .commit.sha}) + | from_entries + ' "${after_branch_file}" + )" + if ! jq -e \ + --argjson before "${BEFORE_AUTOMATION_BRANCH_HEADS}" \ + --argjson after "${after_automation_heads}" \ + '$before == $after' >/dev/null; then + echo "Automation branch state moved during the model job; refusing stale publication" >&2 + exit 1 + fi + + if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then + echo "OpenCode left uncommitted changes; candidate publication requires an exact commit" >&2 + exit 1 + fi + branch_name="$(git symbolic-ref --quiet --short HEAD || true)" + candidate_head="$(git rev-parse HEAD)" + if [[ -z "${branch_name}" ]]; then + echo "OpenCode left a detached HEAD; refusing ambiguous publication" >&2 + exit 1 + fi + + if [[ "${branch_name}" == "develop" ]]; then + if [[ "${candidate_head}" == "${DEVELOP_HEAD_BEFORE}" ]]; then + printf 'agent_candidate=%s\n' '{"kind":"none"}' >> "${GITHUB_OUTPUT}" + printf 'has_candidate=false\n' >> "${GITHUB_OUTPUT}" + exit 0 + fi + echo "OpenCode committed on protected develop instead of a bounded candidate branch" >&2 + exit 1 fi - candidate="$(jq -c '.[0]' <<<"${candidates}")" + matching_prs="$( + jq -c \ + --arg repo "${repository}" \ + --arg branch "${branch_name}" ' + [ + .[] + | select( + .head.repo.full_name == $repo + and .base.ref == "develop" + and .head.ref == $branch + ) + ] + ' "${after_pr_file}" + )" + matching_pr_count="$(jq 'length' <<<"${matching_prs}")" + if [[ "${matching_pr_count}" -gt 1 ]]; then + echo "Multiple same-repository pull requests share the local candidate branch" >&2 + exit 1 + fi + + if [[ "${matching_pr_count}" -eq 1 ]]; then + number="$(jq -r '.[0].number' <<<"${matching_prs}")" + number_key="${number}" + before_head="$(jq -r --arg key "${number_key}" '.[$key] // empty' <<<"${BEFORE_PR_HEADS}")" + live_head="$(jq -r '.[0].head.sha' <<<"${matching_prs}")" + if [[ -z "${before_head}" || "${live_head}" != "${before_head}" ]]; then + echo "Existing pull-request head moved before local candidate validation" >&2 + exit 1 + fi + if [[ "${candidate_head}" == "${before_head}" ]]; then + printf 'agent_candidate=%s\n' '{"kind":"none"}' >> "${GITHUB_OUTPUT}" + printf 'has_candidate=false\n' >> "${GITHUB_OUTPUT}" + exit 0 + fi + predecessor_sha="${before_head}" + predecessor_ref="${branch_name}" + candidate="$( + jq -cn \ + --argjson number "${number}" \ + --arg head_ref "${branch_name}" \ + --arg before_head_sha "${before_head}" \ + --arg head_sha "${candidate_head}" \ + --arg predecessor_sha "${predecessor_sha}" \ + --arg predecessor_ref "${predecessor_ref}" \ + --arg develop_head_before "${DEVELOP_HEAD_BEFORE}" \ + '{ + kind: "existing_pr", + number: $number, + head_ref: $head_ref, + before_head_sha: $before_head_sha, + head_sha: $head_sha, + predecessor_sha: $predecessor_sha, + predecessor_ref: $predecessor_ref, + develop_head_before: $develop_head_before + }' + )" + else + if ! [[ "${branch_name}" =~ ^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$ ]]; then + echo "Local candidate branch does not match the strict automation namespace" >&2 + exit 1 + fi + if jq -e --arg branch "${branch_name}" 'has($branch)' \ + >/dev/null <<<"${BEFORE_AUTOMATION_BRANCH_HEADS}"; then + echo "OpenCode reused an existing automation branch without an associated pull request" >&2 + exit 1 + fi + if jq -e --arg branch "${branch_name}" \ + 'any(.[]; .name == $branch)' >/dev/null "${after_branch_file}"; then + echo "Agent branch already exists remotely; refusing ambiguous ownership" >&2 + exit 1 + fi + predecessor_sha="${DEVELOP_HEAD_BEFORE}" + predecessor_ref="develop" + candidate="$( + jq -cn \ + --arg head_ref "${branch_name}" \ + --arg head_sha "${candidate_head}" \ + --arg predecessor_sha "${predecessor_sha}" \ + --arg predecessor_ref "${predecessor_ref}" \ + --arg develop_head_before "${DEVELOP_HEAD_BEFORE}" \ + '{ + kind: "new_branch", + head_ref: $head_ref, + head_sha: $head_sha, + predecessor_sha: $predecessor_sha, + predecessor_ref: $predecessor_ref, + develop_head_before: $develop_head_before + }' + )" + fi + + if ! git merge-base --is-ancestor "${predecessor_sha}" "${candidate_head}"; then + echo "Local candidate is not a non-destructive descendant of its exact predecessor" >&2 + exit 1 + fi + ahead_count="$(git rev-list --count "${predecessor_sha}..${candidate_head}")" + if [[ "${ahead_count}" -lt 1 ]]; then + echo "Agent branch is not ahead of develop or its exact predecessor" >&2 + exit 1 + fi + if [[ "${ahead_count}" -gt 50 ]]; then + echo "Agent candidate exceeds the bounded 50-commit publication limit" >&2 + exit 1 + fi + if git rev-list --merges "${predecessor_sha}..${candidate_head}" | grep -q .; then + echo "Agent candidate contains a merge commit; refusing non-linear publication" >&2 + exit 1 + fi + + changed_paths_file="${RUNNER_TEMP}/opencode-candidate-paths.txt" + git log --format= --name-only "${predecessor_sha}..${candidate_head}" \ + | sed '/^$/d' | sort -u > "${changed_paths_file}" + file_count="$(wc -l < "${changed_paths_file}")" + if [[ "${file_count}" -gt 50 ]]; then + echo "Agent branch exceeds the bounded 50-file publication limit" >&2 + exit 1 + fi + if grep -Eq '(^\.github/|(^|/)CODEOWNERS$)' "${changed_paths_file}"; then + echo "Agent branch changes .github or CODEOWNERS policy and requires manual handling" >&2 + exit 1 + fi + git diff --check "${predecessor_sha}" "${candidate_head}" + + rm -rf "${candidate_dir}" + install -d -m 0700 "${candidate_dir}" + printf '%s\n' "${candidate}" > "${candidate_dir}/metadata.json" + bundle_ref="refs/heads/__opencode_candidate" + git update-ref "${bundle_ref}" "${candidate_head}" + git bundle create "${candidate_dir}/candidate.bundle" \ + "${bundle_ref}" "^${predecessor_sha}" + git update-ref -d "${bundle_ref}" + bundle_size="$(stat -c '%s' "${candidate_dir}/candidate.bundle")" + if [[ "${bundle_size}" -gt 26214400 ]]; then + echo "Agent candidate bundle exceeds the 25 MiB publication limit" >&2 + exit 1 + fi + sha256sum "${candidate_dir}/candidate.bundle" \ + | awk '{print $1}' > "${candidate_dir}/candidate.bundle.sha256" + printf 'agent_candidate=%s\n' "${candidate}" >> "${GITHUB_OUTPUT}" + printf 'has_candidate=true\n' >> "${GITHUB_OUTPUT}" + + - name: Upload exact local candidate for isolated branch publication + if: ${{ always() && !cancelled() && steps.detect_candidate.outputs.has_candidate == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: opencode-candidate-${{ github.run_id }} + path: ${{ runner.temp }}/opencode-candidate + if-no-files-found: error + retention-days: 1 - name: Preserve an OpenCode execution failure after candidate capture if: ${{ steps.run_opencode.outcome == 'failure' }} @@ -256,9 +455,198 @@ jobs: echo "OpenCode failed after any reviewable branch evidence was captured" >&2 exit 1 - publish-agent-pull-request: + publish-agent-branch: needs: maintain-repository - if: ${{ always() && !cancelled() && needs.maintain-repository.outputs.agent_candidate != '' }} + if: ${{ always() && !cancelled() && needs.maintain-repository.outputs.has_candidate == 'true' }} + permissions: + actions: read + contents: write + outputs: + agent_candidate: ${{ steps.publish_branch.outputs.agent_candidate }} + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Download exact candidate bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: opencode-candidate-${{ github.run_id }} + path: ${{ runner.temp }}/opencode-candidate + + - name: Publish exact candidate by non-forced branch update + id: publish_branch + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_CANDIDATE: ${{ needs.maintain-repository.outputs.agent_candidate }} + run: | + set -euo pipefail + repository="${GITHUB_REPOSITORY}" + candidate_dir="${RUNNER_TEMP}/opencode-candidate" + metadata_file="${candidate_dir}/metadata.json" + bundle_file="${candidate_dir}/candidate.bundle" + digest_file="${candidate_dir}/candidate.bundle.sha256" + + test -f "${metadata_file}" + test -f "${bundle_file}" + test -f "${digest_file}" + metadata="$(jq -c . "${metadata_file}")" + expected_metadata="$(jq -c . <<<"${EXPECTED_CANDIDATE}")" + if [[ "${metadata}" != "${expected_metadata}" ]]; then + echo "Downloaded candidate metadata does not match the exact model-job output" >&2 + exit 1 + fi + expected_digest="$(tr -d '[:space:]' < "${digest_file}")" + if ! [[ "${expected_digest}" =~ ^[0-9a-f]{64}$ ]]; then + echo "Candidate bundle digest is malformed" >&2 + exit 1 + fi + actual_digest="$(sha256sum "${bundle_file}" | awk '{print $1}')" + if [[ "${actual_digest}" != "${expected_digest}" ]]; then + echo "Candidate bundle digest mismatch" >&2 + exit 1 + fi + + kind="$(jq -r '.kind' <<<"${metadata}")" + head_ref="$(jq -r '.head_ref' <<<"${metadata}")" + candidate_head="$(jq -r '.head_sha' <<<"${metadata}")" + predecessor_sha="$(jq -r '.predecessor_sha' <<<"${metadata}")" + predecessor_ref="$(jq -r '.predecessor_ref' <<<"${metadata}")" + develop_head_before="$(jq -r '.develop_head_before' <<<"${metadata}")" + if ! [[ "${candidate_head}" =~ ^[0-9a-f]{40}$ \ + && "${predecessor_sha}" =~ ^[0-9a-f]{40}$ \ + && "${develop_head_before}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Candidate metadata contains an invalid exact SHA" >&2 + exit 1 + fi + git check-ref-format --branch "${head_ref}" >/dev/null + git check-ref-format --branch "${predecessor_ref}" >/dev/null + + current_develop="$(gh api "/repos/${repository}/git/ref/heads/develop" --jq '.object.sha')" + if [[ "${current_develop}" != "${develop_head_before}" ]]; then + echo "Protected develop moved before isolated branch publication" >&2 + exit 1 + fi + + if [[ "${kind}" == "existing_pr" ]]; then + number="$(jq -r '.number' <<<"${metadata}")" + before_head="$(jq -r '.before_head_sha' <<<"${metadata}")" + if ! [[ "${number}" =~ ^[0-9]+$ && "${before_head}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Existing pull-request candidate metadata is malformed" >&2 + exit 1 + fi + live="$(gh api "/repos/${repository}/pulls/${number}")" + jq -e \ + --arg repo "${repository}" \ + --arg head_ref "${head_ref}" \ + --arg before_head "${before_head}" ' + select( + .state == "open" + and .base.repo.full_name == $repo + and .head.repo.full_name == $repo + and .base.ref == "develop" + and .head.ref == $head_ref + and .head.sha == $before_head + ) + ' >/dev/null <<<"${live}" + if [[ "${predecessor_sha}" != "${before_head}" || "${predecessor_ref}" != "${head_ref}" ]]; then + echo "Existing pull-request predecessor metadata does not bind the live branch" >&2 + exit 1 + fi + elif [[ "${kind}" == "new_branch" ]]; then + if ! [[ "${head_ref}" =~ ^automation/opencode-[0-9]{8}T[0-9]{6}Z-[a-z0-9][a-z0-9-]{0,48}$ ]]; then + echo "New candidate branch violates the strict automation namespace" >&2 + exit 1 + fi + encoded_ref="$(jq -rn --arg value "${head_ref}" '$value | @uri')" + if gh api "/repos/${repository}/branches/${encoded_ref}" >/dev/null 2>&1; then + echo "New candidate branch appeared before isolated publication" >&2 + exit 1 + fi + if [[ "${predecessor_sha}" != "${develop_head_before}" || "${predecessor_ref}" != "develop" ]]; then + echo "New branch predecessor does not bind the exact protected develop head" >&2 + exit 1 + fi + else + echo "Unknown agent candidate kind: ${kind}" >&2 + exit 1 + fi + + work_dir="${RUNNER_TEMP}/opencode-branch-publisher" + rm -rf "${work_dir}" + install -d -m 0700 "${work_dir}" + cd "${work_dir}" + git init --quiet + git remote add origin "https://github.com/${repository}.git" + + git_credential_key="credential.https://github.com.helper" + cleanup_git_credentials() { + git config --local --unset-all "${git_credential_key}" >/dev/null 2>&1 || true + } + trap cleanup_git_credentials EXIT + cleanup_git_credentials + git config --local --add "${git_credential_key}" "" + git config --local --add "${git_credential_key}" "!gh auth git-credential" + git config --local user.name "opencode-agent[bot]" + git config --local user.email "opencode-agent[bot]@users.noreply.github.com" + + git fetch --no-tags origin \ + "refs/heads/${predecessor_ref}:refs/remotes/origin/predecessor" + fetched_predecessor="$(git rev-parse refs/remotes/origin/predecessor)" + if [[ "${fetched_predecessor}" != "${predecessor_sha}" ]]; then + echo "Exact predecessor moved while the isolated publisher was preparing" >&2 + exit 1 + fi + + git bundle verify "${bundle_file}" + git fetch --no-tags "${bundle_file}" \ + "refs/heads/__opencode_candidate:refs/heads/__opencode_candidate" + imported_head="$(git rev-parse refs/heads/__opencode_candidate)" + if [[ "${imported_head}" != "${candidate_head}" ]]; then + echo "Candidate bundle does not contain the expected exact head" >&2 + exit 1 + fi + git fsck --strict --no-dangling + if ! git merge-base --is-ancestor "${predecessor_sha}" "${candidate_head}"; then + echo "Candidate bundle is not a non-destructive descendant of the exact predecessor" >&2 + exit 1 + fi + ahead_count="$(git rev-list --count "${predecessor_sha}..${candidate_head}")" + if [[ "${ahead_count}" -lt 1 || "${ahead_count}" -gt 50 ]]; then + echo "Candidate commit count is outside the bounded publication range" >&2 + exit 1 + fi + if git rev-list --merges "${predecessor_sha}..${candidate_head}" | grep -q .; then + echo "Candidate bundle contains a merge commit; refusing non-linear publication" >&2 + exit 1 + fi + + changed_paths_file="${RUNNER_TEMP}/opencode-publisher-paths.txt" + git log --format= --name-only "${predecessor_sha}..${candidate_head}" \ + | sed '/^$/d' | sort -u > "${changed_paths_file}" + file_count="$(wc -l < "${changed_paths_file}")" + if [[ "${file_count}" -gt 50 ]]; then + echo "Candidate exceeds the bounded 50-file publication limit" >&2 + exit 1 + fi + if grep -Eq '(^\.github/|(^|/)CODEOWNERS$)' "${changed_paths_file}"; then + echo "Candidate changes .github or CODEOWNERS policy and requires manual handling" >&2 + exit 1 + fi + git diff --check "${predecessor_sha}" "${candidate_head}" + + git push origin "${candidate_head}:refs/heads/${head_ref}" + encoded_ref="$(jq -rn --arg value "${head_ref}" '$value | @uri')" + live_head="$(gh api "/repos/${repository}/branches/${encoded_ref}" --jq '.commit.sha')" + if [[ "${live_head}" != "${candidate_head}" ]]; then + echo "Published branch does not retain the expected exact candidate head" >&2 + exit 1 + fi + + printf 'agent_candidate=%s\n' "${metadata}" >> "${GITHUB_OUTPUT}" + + publish-agent-pull-request: + needs: publish-agent-branch + if: ${{ always() && !cancelled() && needs.publish-agent-branch.result == 'success' && needs.publish-agent-branch.outputs.agent_candidate != '' }} permissions: contents: read pull-requests: write @@ -274,7 +662,7 @@ jobs: shell: bash env: GH_TOKEN: ${{ github.token }} - AGENT_CANDIDATE: ${{ needs.maintain-repository.outputs.agent_candidate }} + AGENT_CANDIDATE: ${{ needs.publish-agent-branch.outputs.agent_candidate }} run: | set -euo pipefail repository="${GITHUB_REPOSITORY}" @@ -282,10 +670,6 @@ jobs: base_branch="develop" kind="$(jq -r '.kind' <<<"${AGENT_CANDIDATE}")" - if [[ "${kind}" == "none" ]]; then - exit 0 - fi - head_ref="$(jq -r '.head_ref' <<<"${AGENT_CANDIDATE}")" expected_head="$(jq -r '.head_sha' <<<"${AGENT_CANDIDATE}")" if ! [[ "${expected_head}" =~ ^[0-9a-f]{40}$ ]]; then @@ -398,11 +782,11 @@ jobs: body="$(printf '%s\n' \ "## Automated bounded development slice" \ "" \ - "This draft pull request was published by a deterministic non-model job after OpenCode pushed exactly one validated branch." \ + "This draft pull request was published after a deterministic non-model branch writer transferred exactly one validated local OpenCode candidate." \ "" \ "- Exact head: \`${expected_head}\`" \ "- Base: \`${base_branch}\`" \ - "- Model credential: repository \`NVIDIA_NIM_API_KEY\` mapped only inside the preceding OpenCode job" \ + "- Model credential: repository \`NVIDIA_NIM_API_KEY\` mapped only inside the preceding read-only OpenCode job" \ "- Pull-request approval and merge: explicitly outside the development agent's authority" \ "" \ "All tests, security checks, review threads, independent exact-head approvals, documentation, and release gates remain required. Pending or absent evidence is not passing." \ @@ -435,7 +819,6 @@ jobs: authorize-exact-head-checks: needs: - - maintain-repository - publish-agent-pull-request if: ${{ always() && !cancelled() && needs.publish-agent-pull-request.result == 'success' && needs.publish-agent-pull-request.outputs.pr_number != '' }} permissions: From 327d1e4f8eeb2f97b8c6065ae6df4a93332f30bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 05:34:04 +0900 Subject: [PATCH 128/152] test(ci): bind policy guard to isolated publisher --- .../HourlyOpenCodeCandidateSelectionTest.java | 78 +++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java index 784754fa..0f3d02c8 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java @@ -15,10 +15,11 @@ * Guards the candidate-selection and publication boundaries used by the hourly OpenCode workflow. * *

Inside {@code $existing_refs | index(...)}, the jq input changes from the branch object to the - * reference array. Reading {@code .name} after that pipe therefore fails at runtime. Production + * reference array. Reading {@code .name} after that pipe therefore fails at runtime, so production * must capture the branch name first and pass the scalar variable to {@code index}. Updated - * existing pull requests must also be compared with their pre-agent heads so policy-file changes - * cannot bypass the new-branch publication guard.

+ * existing pull requests must also preserve their pre-agent head through local candidate capture, + * isolated branch publication, and pull-request publication so policy-file changes are rejected + * before and after the remote branch update.

*/ class HourlyOpenCodeCandidateSelectionTest { @@ -37,33 +38,82 @@ void capturesBranchNameBeforeExistingReferenceLookup() throws IOException { } /** - * Requires updated existing pull requests to reject agent-introduced policy-file changes. + * Requires policy-file rejection at both deterministic publication boundaries. * - *

The model job may update an already-open pull request. The deterministic publisher must - * therefore retain that pull request's pre-agent head, compare it with the candidate head, and - * apply the same {@code .github/**} and {@code CODEOWNERS} exclusion used for new branches.

+ *

The model job now has read-only repository credentials and produces only a local commit. + * The isolated branch publisher must re-bind an existing pull request to its exact pre-agent + * head, validate the complete candidate path set before its sole branch push, and refuse policy + * files. The separate pull-request publisher must then compare that pre-agent head with the + * exact published head and independently reject the same policy paths. This keeps the original + * guard effective after publication authority was removed from the model-executing job.

* * @throws IOException when the workflow cannot be read */ @Test void rejectsPolicyChangesOnUpdatedExistingPullRequests() throws IOException { String workflow = workflowText(); - String existingPrBlock = between( + String branchPublisher = between( workflow, + "\n publish-agent-branch:\n", + "\n publish-agent-pull-request:\n" + ); + String branchExistingPrPreflight = between( + branchPublisher, + "if [[ \"${kind}\" == \"existing_pr\" ]]; then", + "elif [[ \"${kind}\" == \"new_branch\" ]]; then" + ); + String pullRequestPublisher = between( + workflow, + "\n publish-agent-pull-request:\n", + "\n authorize-exact-head-checks:\n" + ); + String pullRequestExistingPrValidation = between( + pullRequestPublisher, "if [[ \"${kind}\" == \"existing_pr\" ]]; then", "elif [[ \"${kind}\" == \"new_branch\" ]]; then" ); - assertTrue(workflow.contains("before_head_sha: $before[$number_key]")); - assertTrue(existingPrBlock.contains( + assertTrue(workflow.contains("before_head_sha: $before_head_sha")); + assertTrue(branchExistingPrPreflight.contains( + "before_head=\"$(jq -r '.before_head_sha' <<<\"${metadata}\")\"" + )); + assertTrue(branchExistingPrPreflight.contains("and .head.sha == $before_head")); + assertTrue(branchPublisher.contains( + "git log --format= --name-only \"${predecessor_sha}..${candidate_head}\"" + )); + assertTrue(branchPublisher.contains( + "grep -Eq '(^\\.github/|(^|/)CODEOWNERS$)' \"${changed_paths_file}\"" + )); + assertAppearsBefore( + branchPublisher, + "grep -Eq '(^\\.github/|(^|/)CODEOWNERS$)' \"${changed_paths_file}\"", + "git push origin \"${candidate_head}:refs/heads/${head_ref}\"" + ); + + assertTrue(pullRequestExistingPrValidation.contains( "before_head=\"$(jq -r '.before_head_sha' <<<\"${AGENT_CANDIDATE}\")\"" )); - assertTrue(existingPrBlock.contains( + assertTrue(pullRequestExistingPrValidation.contains( "comparison=\"$(gh api \"/repos/${repository}/compare/${before_head}...${expected_head}\")\"" )); - assertTrue(existingPrBlock.contains("startswith(\".github/\")")); - assertTrue(existingPrBlock.contains("or . == \"CODEOWNERS\"")); - assertTrue(existingPrBlock.contains("or endswith(\"/CODEOWNERS\")")); + assertTrue(pullRequestExistingPrValidation.contains(".files[].filename")); + assertTrue(pullRequestExistingPrValidation.contains("startswith(\".github/\")")); + assertTrue(pullRequestExistingPrValidation.contains("or . == \"CODEOWNERS\"")); + assertTrue(pullRequestExistingPrValidation.contains("or endswith(\"/CODEOWNERS\")")); + } + + /** + * Requires one security-sensitive marker to occur before another in the same workflow section. + * + * @param text workflow section being checked + * @param first marker that must execute first + * @param second marker that must execute later + */ + private static void assertAppearsBefore(String text, String first, String second) { + int firstIndex = text.indexOf(first); + int secondIndex = text.indexOf(second); + assertTrue(firstIndex >= 0, () -> "Missing first marker: " + first); + assertTrue(secondIndex > firstIndex, () -> "Marker must appear after first marker: " + second); } /** From b6efb1fc05c0161e82278c675ae7a631878b9302 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 13:10:30 +0900 Subject: [PATCH 129/152] test(security): require immutable container base images --- .../ContainerImagePinningTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java new file mode 100644 index 00000000..76c7aad6 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java @@ -0,0 +1,93 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Prevents mutable container tags from silently changing the software supply-chain inputs. + * + *

Docker image tags are intentionally mutable. A build that names only a tag can therefore + * consume different base-image bytes without any repository change. This contract keeps the tag + * for human readability while requiring every external Dockerfile build stage to include an + * immutable SHA-256 digest. References to an earlier local build stage are exempt because they do + * not resolve through a registry.

+ */ +class ContainerImagePinningTest { + + private static final Pattern FROM_INSTRUCTION = Pattern.compile( + "(?im)^FROM\\s+(?:--platform=\\S+\\s+)?(?\\S+)" + + "(?:\\s+AS\\s+(?[A-Za-z0-9._-]+))?\\s*$" + ); + private static final Pattern SHA256_PIN = Pattern.compile( + "^[^@\\s]+@sha256:[0-9a-f]{64}$" + ); + + /** + * Requires every registry-backed Dockerfile build stage to resolve by immutable SHA-256 digest. + * + * @throws IOException when the repository Dockerfile cannot be read + */ + @Test + void pinsEveryExternalBaseImageBySha256Digest() throws IOException { + Path dockerfilePath = projectRoot().resolve("Dockerfile"); + assertTrue(Files.isRegularFile(dockerfilePath), "The repository Dockerfile must exist"); + + String dockerfile = Files.readString(dockerfilePath, StandardCharsets.UTF_8); + Matcher matcher = FROM_INSTRUCTION.matcher(dockerfile); + Set localStageAliases = new HashSet<>(); + int externalImageCount = 0; + + while (matcher.find()) { + String imageReference = matcher.group("image"); + if (!localStageAliases.contains(imageReference)) { + externalImageCount++; + assertTrue( + SHA256_PIN.matcher(imageReference).matches(), + () -> "Dockerfile base image must use an immutable SHA-256 digest: " + + imageReference + ); + } + String stageAlias = matcher.group("alias"); + if (stageAlias != null) { + localStageAliases.add(stageAlias); + } + } + + assertTrue(externalImageCount > 0, "The Dockerfile must declare at least one base image"); + } + + /** + * Finds the repository root from either root-level or module-local Maven execution. + * + * @return absolute repository root containing the top-level Dockerfile and Maven project + * @throws IllegalStateException when no repository root can be found + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From f8cdcf77b2a940a474d1b8080e3e9b6bfeacca4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 13:13:18 +0900 Subject: [PATCH 130/152] fix(security): pin container base image digests --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1be2d6e0..7c07cdc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.9.13-eclipse-temurin-25 AS build +FROM maven:3.9.13-eclipse-temurin-25@sha256:ade3c87ed2874c8b773ccb9b238cd66db8a7c56c77d99a1c825bf929f3afcb96 AS build WORKDIR /workspace @@ -18,7 +18,7 @@ RUN mvn -B -DskipTests -pl "${SERVICE}" -am package \ && mkdir -p /out \ && cp "${SERVICE}/target/${SERVICE}-"*.jar /out/app.jar -FROM eclipse-temurin:25-jre +FROM eclipse-temurin:25-jre@sha256:f19dbf0dc677ae28efed04b8b99d3123d6aaf2e6b3c9d35c09274dd8b5d53a4f WORKDIR /app COPY --from=build --chown=65532:65532 /out/app.jar /app/app.jar From 832061b179624c95119396aea9790a146c980f71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 13:15:11 +0900 Subject: [PATCH 131/152] docs(security): record container image pinning evidence --- .../container-base-image-pinning-evidence.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/doctoring/container-base-image-pinning-evidence.md diff --git a/docs/doctoring/container-base-image-pinning-evidence.md b/docs/doctoring/container-base-image-pinning-evidence.md new file mode 100644 index 00000000..7280b5e6 --- /dev/null +++ b/docs/doctoring/container-base-image-pinning-evidence.md @@ -0,0 +1,62 @@ +# Container base-image pinning evidence + +## Purpose + +mightyETL container builds must not silently consume different registry bytes when a mutable image tag moves. Docker supports `FROM image:tag@sha256:` so a human-readable release tag can remain visible while the build resolves to one immutable content digest. This control reduces software-supply-chain drift and makes base-image changes reviewable as repository changes. + +This evidence is a build-input integrity control. It does **not** claim that a digest makes an image trustworthy, vulnerability-free, or permanently suitable. Vulnerability management and planned digest rotation remain separate duties. + +## Governing contract + +Every registry-backed `FROM` instruction in the repository `Dockerfile` must include a lowercase SHA-256 digest. References to an earlier local build-stage alias are exempt because they do not resolve through an external registry. + +The current reviewed references are: + +- build stage: `maven:3.9.13-eclipse-temurin-25@sha256:ade3c87ed2874c8b773ccb9b238cd66db8a7c56c77d99a1c825bf929f3afcb96` +- runtime stage: `eclipse-temurin:25-jre@sha256:f19dbf0dc677ae28efed04b8b99d3123d6aaf2e6b3c9d35c09274dd8b5d53a4f` + +The digest values were surfaced by the OpenSSF Scorecard remediation output associated with mightyETL's Security Scan. That historical scan executed against GitHub's synthetic pull-request merge revision, so it is used here only as remediation provenance for the digest-resolved references and is **not** accepted as literal-head security-gate evidence. + +## Red-green TDD evidence + +### RED + +Commit `b6efb1fc05c0161e82278c675ae7a631878b9302` added `ContainerImagePinningTest` without changing the mutable Dockerfile tags. Exact-head CI run `31238858595` checked out that literal SHA. The macOS job `93056263926` ran the Maven test suite and failed specifically with: + +`Dockerfile base image must use an immutable SHA-256 digest: maven:3.9.13-eclipse-temurin-25` + +The failure is intentionally retained in history as evidence that the contract detects the pre-existing mutable input. + +### GREEN implementation + +Commit `f8cdcf77b2a940a474d1b8080e3e9b6bfeacca4b` changed only the two external Dockerfile base references to the reviewed digest-qualified form. Exact-head CI run `31238960659` checked out that literal SHA; its macOS Maven test step completed successfully, including `ContainerImagePinningTest`. Full integrated exact-head gate acceptance is evaluated separately after all documentation commits so older-head evidence cannot be reused. + +## Enforcement design + +`ContainerImagePinningTest` parses every `FROM` instruction, tracks local stage aliases, and fails when any registry-backed image reference lacks exactly one `@sha256:` value followed by 64 lowercase hexadecimal characters. It also fails if the Dockerfile unexpectedly contains no external base image. The test is intentionally repository-level because the risk is configuration drift rather than Java runtime behavior. + +Keeping the tag alongside the digest is deliberate. Docker documents that the digest provides an immutable identifier while the tag preserves release intent and readability. A tag update, digest update, or both therefore becomes an explicit reviewed diff rather than an implicit registry-side change. + +## Update procedure + +When a Maven or Eclipse Temurin base image must be updated: + +1. Resolve the intended official image tag to its current registry digest using a trusted registry-aware Docker/BuildKit inspection path. +2. Review the upstream image release and security rationale; do not copy an untrusted third-party digest. +3. Update the readable tag and SHA-256 digest together when the release changes, or update only the digest when intentionally adopting a rebuilt image under the same reviewed tag. +4. Run the full test suite and container build verification on the exact candidate head. +5. Require exact-head Dependency Review, SBOM, SAST, security scanning, provenance/release checks, and independent review according to repository policy before integration. + +## Rollback + +If a newly pinned base image causes a regression, revert to the previously reviewed **digest-qualified** reference and rerun exact-head validation. Never remove the digest merely to make a build move again, because that would restore unreviewed registry mutability. + +## Standards and primary references + +Docker, Inc. (2026). *Dockerfile reference*. Docker Docs. https://docs.docker.com/reference/dockerfile/ + +Docker, Inc. (2026). *Building best practices*. Docker Docs. https://docs.docker.com/build/building/best-practices/ + +Docker, Inc. (2026). *docker image pull*. Docker Docs. https://docs.docker.com/reference/cli/docker/image/pull/ + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 From 699d3f20685e707f01e61abc6fca1fb87c465a43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 13:17:23 +0900 Subject: [PATCH 132/152] docs(changelog): record immutable base image pins --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2833cc..2de2d0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Container builds now pin Maven and Eclipse Temurin base-image tags to reviewed SHA-256 digests, with a fail-first contract test preventing mutable registry tags from re-entering the Dockerfile. - The model-executing hourly OpenCode maintenance job now has read-only issue access; fail-first workflow-contract coverage proves `issues: write` is unnecessary while preserving issue and roadmap inspection. - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. - Dependency Review now relies on the immutably pinned GitHub Dependency Review Action's documented `pull_request` event endpoints; ignored `base-ref`/`head-ref` overrides are prohibited on pull-request runs, and dependency-delta evidence is invalidated whenever either endpoint moves. From 7ae7dcc2446c5a6bacd75a3602b83e8ea1f6f3f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:17:52 +0900 Subject: [PATCH 133/152] test(ci): specify nonblocking maintenance progress contract --- .../HourlyOpenCodeProgressPolicyTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java new file mode 100644 index 00000000..94651532 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java @@ -0,0 +1,112 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Specifies how scheduled OpenCode maintenance must keep making safe repository-local progress + * while external review, check, or read-only dependency gates remain unavailable. + * + *

The policy deliberately keeps merge evidence fail-closed while preventing an unrelated + * external wait from stopping bounded, non-conflicting mightyETL work. It also preserves stack + * integrity by prohibiting work on invalid downstream boundaries.

+ */ +class HourlyOpenCodeProgressPolicyTest { + + private static String agentPolicy; + private static String workflow; + + /** + * Reads the scheduler's repository instruction and workflow inputs once with normalized line + * endings so the contract behaves identically on every supported operating system. + * + * @throws IOException when either authoritative UTF-8 source cannot be read + */ + @BeforeAll + static void readSchedulerPolicy() throws IOException { + Path root = projectRoot(); + agentPolicy = Files.readString(root.resolve("AGENTS.md"), StandardCharsets.UTF_8) + .replace("\r\n", "\n"); + workflow = Files.readString( + root.resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ) + .replace("\r\n", "\n"); + } + + /** + * Requires external-only latency to remain a merge blocker without becoming a blanket + * development stop condition. + */ + @Test + void continuesOneBoundedNonConflictingSliceWhenNoPullRequestIsSourceActionable() { + assertTrue(agentPolicy.contains("## Scheduled maintenance progress contract")); + assertTrue(agentPolicy.contains( + "External review, approval, check, or read-only dependency latency is not a " + + "reason to stop all productive mightyETL work." + )); + assertTrue(agentPolicy.contains( + "A pull request is source-actionable only when its exact current head has a valid " + + "repository-local finding or failing source gate that mightyETL can repair." + )); + assertTrue(agentPolicy.contains( + "When no open pull request is source-actionable, select exactly one non-conflicting " + + "bounded mightyETL slice from the protected `develop` head." + )); + assertTrue(agentPolicy.contains( + "Do not deepen an invalid stack or modify a blocked stack branch merely to appear " + + "productive." + )); + assertTrue(agentPolicy.contains( + "The independent slice must not depend on, retarget, rewrite, or overlap files " + + "changed by the invalid stack." + )); + } + + /** Keeps every separately leased repository outside this scheduler's write authority. */ + @Test + void preservesReadOnlyDependencyLeasesWhileContinuingLocalWork() { + assertTrue(agentPolicy.contains( + "ContextualWisdomLab/.github, naruon, contextual-orchestrator, and every separately " + + "leased repository remain read-only." + )); + assertTrue(agentPolicy.contains( + "Inspect their exact integration state, but never mutate, dispatch a write-capable " + + "agent, or post a mutation-trigger comment there." + )); + assertTrue(workflow.contains("Checkout protected default-branch source")); + assertTrue(workflow.contains("opencode run --model \"${MODEL}\" --auto")); + assertTrue(workflow.contains("Use the repository-scoped token for read operations only")); + } + + /** + * Finds the repository root from either reactor-root or module-local Maven execution. + * + * @return absolute repository root + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 8bac86c8bcdd981a9b88d888bc6cfbabb8d1ee53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:18:50 +0900 Subject: [PATCH 134/152] docs(agents): keep scheduled maintenance productive during external waits --- AGENTS.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 757f5141..39807830 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,32 @@ workflows, and service-level maintenance. - Keep automation explicit and auditable (clear triggers, least-privilege permissions). - When unsure, prefer conservative defaults that reduce security and release risk. +## Scheduled maintenance progress contract + +External review, approval, check, or read-only dependency latency is not a reason to stop all +productive mightyETL work. Those unavailable gates remain not passing and must never be reused as +merge or release evidence. + +A pull request is source-actionable only when its exact current head has a valid repository-local +finding or failing source gate that mightyETL can repair. Queued checks, missing independent +approval, synthetic-merge-only scanner evidence, and a separately leased dependency that has not +yet integrated are external-only blockers rather than source-actionable findings. + +When no open pull request is source-actionable, select exactly one non-conflicting bounded +mightyETL slice from the protected `develop` head. Prefer documentation, tests, security, +reliability, packaging, release evidence, or a buyer-visible vertical slice that can be developed +and reviewed independently of the blocked stack. Keep the one-candidate-per-run publication +boundary and all exact-head validation requirements. + +Do not deepen an invalid stack or modify a blocked stack branch merely to appear productive. The +independent slice must not depend on, retarget, rewrite, or overlap files changed by the invalid +stack. If no such independent slice exists, perform read-only analysis and return without a +candidate rather than weakening ancestry, tests, or branch protection. + +ContextualWisdomLab/.github, naruon, contextual-orchestrator, and every separately leased repository +remain read-only. Inspect their exact integration state, but never mutate, dispatch a write-capable +agent, or post a mutation-trigger comment there. Their dedicated loops own those writes. + ## Code-owner review gates — disabled (on hold) As of 2026-08-04, code-owner review requirements (`require_code_owner_reviews` in branch From 6d5101c95206d498006176b43c9948c70a704f7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:19:45 +0900 Subject: [PATCH 135/152] docs(doctoring): record nonblocking scheduler progress contract --- ...-opencode-nonblocking-progress-evidence.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md diff --git a/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md b/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md new file mode 100644 index 00000000..bdd3a8b3 --- /dev/null +++ b/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md @@ -0,0 +1,101 @@ +# Hourly OpenCode nonblocking progress evidence + +Reviewed on: **2026-08-08** + +## Incident + +The scheduled maintenance contract correctly refused to treat queued checks, missing approvals, +synthetic-merge-only scanner output, and separately leased repository state as passing evidence. +However, it did not distinguish those external-only waits from repository-local source defects. +An external gate could therefore leave the hourly development loop repeatedly reporting the same +blocker without selecting safe, independent mightyETL work. + +This was an orchestration defect, not permission to weaken merge policy. The correct behavior is: + +1. keep every unavailable or stale gate non-passing; +2. repair the earliest valid repository-local finding first; +3. never deepen an invalid stack; +4. when no open pull request is source-actionable, select at most one independent bounded slice + from protected `develop` that does not depend on or overlap the invalid stack; and +5. keep separately leased repositories read-only. + +## Decision + +`AGENTS.md` now defines `source-actionable` narrowly. A pull request is source-actionable only when +its exact current head contains a valid repository-local finding or failing source gate that this +repository can repair. Review latency, approval latency, queued checks, synthetic-only scanner +identity, or an unintegrated read-only central dependency remain blockers to merge but do not stop +all unrelated mightyETL development. + +When no open pull request is source-actionable, the scheduled agent may create exactly one bounded, +non-conflicting candidate from protected `develop`. The candidate must not depend on, retarget, +rewrite, or overlap files changed by the invalid stack. If no such slice exists, the run returns +without a candidate rather than manufacturing activity. + +The repository-writer lease is unchanged. `ContextualWisdomLab/.github`, `naruon`, +`contextual-orchestrator`, and every separately leased repository remain read-only to this loop. +The scheduler may inspect their exact integration state but may not mutate them, dispatch a +write-capable agent there, or post a mutation-trigger comment. + +## Why `AGENTS.md` is authoritative to OpenCode + +OpenCode's project initialization creates a repository-root `AGENTS.md`, and its official +instructions recommend committing that file so OpenCode can understand project structure and +coding patterns. The scheduled workflow checks out protected default-branch source before invoking +plain `opencode run`, so the repository instruction is present at agent startup without adding a +second credential or changing the review-agent contract. + +## Test-first evidence + +### RED + +Commit `7ae7dcc2446c5a6bacd75a3602b83e8ea1f6f3f2` added +`HourlyOpenCodeProgressPolicyTest` before the policy existed. Literal-head CI run `31256917651` +checked out that exact SHA. macOS job `93101583785` ran 313 tests and failed exactly the two new +policy tests: + +- `continuesOneBoundedNonConflictingSliceWhenNoPullRequestIsSourceActionable`; +- `preservesReadOnlyDependencyLeasesWhileContinuingLocalWork`. + +The failure was caused by absent scheduler policy text, not compilation, unrelated production code, +or a stale checkout. That commit and run remain permanent fail-first evidence. + +### GREEN implementation + +Commit `8bac86c8bcdd981a9b88d888bc6cfbabb8d1ee53` added the minimal AGENTS contract required by the +failing tests. Checks from the RED head do not transfer. The exact current head must complete its own +CI, dependency, SBOM, SAST, security, commit-status, review-thread, and independent-approval gates +before merge. + +## Safety properties + +- External-only blockers remain not passing. +- No approval, review, check, or scanner evidence is synthesized. +- No protected branch is written directly. +- No invalid downstream stack boundary is deepened. +- At most one independently reviewable candidate may be produced per run. +- Candidate paths must be disjoint from the invalid stack's changed paths. +- Central and separately leased repositories remain read-only. +- The existing NVIDIA NIM and review-agent credential contracts are unchanged. + +## Failure behavior + +If an agent cannot prove that a proposed slice is independent of every invalid stack item, it must +produce no candidate. If any relevant head, base, open-PR set, or automation branch changes during +the run, deterministic publication continues to fail closed. If a later repository instruction +conflicts with the workflow's permission boundary, the narrower no-write and no-merge controls win. + +## Rollback + +Rollback removes this policy section and its regression test together only after replacing them +with a stricter reviewed mechanism that still preserves both properties: unavailable gates are not +passing, and external latency does not halt unrelated bounded work. Do not roll back by allowing +stack deepening, central-repository mutation, direct pull-request lifecycle operations, or stale +check reuse. + +## References + +GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 8, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + +OpenCode. (2026). *Intro*. https://opencode.ai/docs From 3c2284161c35f4a5dba4a9024fa911184607468b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:20:44 +0900 Subject: [PATCH 136/152] docs(changelog): record nonblocking scheduled progress --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de2d0b2..b6b392a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Scheduled OpenCode maintenance now keeps unavailable external gates fail-closed while continuing exactly one independent, non-overlapping mightyETL slice from protected `develop` when no open pull request is source-actionable; invalid stacks and separately leased dependency repositories remain untouched. - Container builds now pin Maven and Eclipse Temurin base-image tags to reviewed SHA-256 digests, with a fail-first contract test preventing mutable registry tags from re-entering the Dockerfile. - The model-executing hourly OpenCode maintenance job now has read-only issue access; fail-first workflow-contract coverage proves `issues: write` is unnecessary while preserving issue and roadmap inspection. - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. @@ -32,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Test-first doctoring for external-wait progress, source-actionable pull-request classification, invalid-stack isolation, and read-only dependency leases in `docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md`. - Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including observed synthetic-merge checkout behavior, cross-platform source identity assertions, the rejected ignored Dependency Review ref-override experiment, corrective pull-request event-endpoint semantics, least-privilege boundaries, stack invalidation rules, rollback prohibition, and APA 7th GitHub references. - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. @@ -40,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. - Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`. - ETL problem-details client and operator contract: `docs/api/problem-details.md`. -- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.etl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings. +- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings. - ETL transaction rollback integration coverage and the operator runbook `docs/etl/bounded-atomic-batches.md`. - Connector scaffolds (contracts + docs only): Qlik Sense, Databricks, Snowflake under `docs/connectors/` and `etl-service` SPI stubs. - Any-to-any CDC design notes and source SPI scaffold: `docs/cdc/any-to-any-cdc.md`, `cdc-service` SPI stubs. From 29b373fc131b68c7780979cd0aaf5f75475bacee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:22:38 +0900 Subject: [PATCH 137/152] fix(changelog): preserve exact legacy config alias scope --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b392a7..fac6ad95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`. - Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`. - ETL problem-details client and operator contract: `docs/api/problem-details.md`. -- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings. +- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.etl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings. - ETL transaction rollback integration coverage and the operator runbook `docs/etl/bounded-atomic-batches.md`. - Connector scaffolds (contracts + docs only): Qlik Sense, Databricks, Snowflake under `docs/connectors/` and `etl-service` SPI stubs. - Any-to-any CDC design notes and source SPI scaffold: `docs/cdc/any-to-any-cdc.md`, `cdc-service` SPI stubs. From 06f49cccde826855314b9d2cd27ce01a91ef5089 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 21:24:59 +0900 Subject: [PATCH 138/152] test(ci): compare scheduler policy independent of Markdown wrapping --- .../HourlyOpenCodeProgressPolicyTest.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java index 94651532..810cf4d7 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java @@ -33,8 +33,9 @@ class HourlyOpenCodeProgressPolicyTest { @BeforeAll static void readSchedulerPolicy() throws IOException { Path root = projectRoot(); - agentPolicy = Files.readString(root.resolve("AGENTS.md"), StandardCharsets.UTF_8) - .replace("\r\n", "\n"); + agentPolicy = canonicalWhitespace( + Files.readString(root.resolve("AGENTS.md"), StandardCharsets.UTF_8) + ); workflow = Files.readString( root.resolve(".github/workflows/hourly-opencode-maintenance.yml"), StandardCharsets.UTF_8 @@ -87,6 +88,16 @@ void preservesReadOnlyDependencyLeasesWhileContinuingLocalWork() { assertTrue(workflow.contains("Use the repository-scoped token for read operations only")); } + /** + * Collapses semantically irrelevant Markdown and platform whitespace before phrase matching. + * + * @param value UTF-8 text whose prose contract must be compared independently of wrapping + * @return one-space canonical representation + */ + private static String canonicalWhitespace(String value) { + return value.replaceAll("\\s+", " ").trim(); + } + /** * Finds the repository root from either reactor-root or module-local Maven execution. * From dba0af66481e7ba98fd16b19792feebd7fb71e1b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:21:16 +0900 Subject: [PATCH 139/152] test(ci): require RCA feasibility loop in scheduler --- .../HourlyOpenCodeProgressPolicyTest.java | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java index 810cf4d7..844e4fda 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java @@ -26,7 +26,8 @@ class HourlyOpenCodeProgressPolicyTest { /** * Reads the scheduler's repository instruction and workflow inputs once with normalized line - * endings so the contract behaves identically on every supported operating system. + * endings and whitespace so the contract behaves identically on every supported operating + * system. * * @throws IOException when either authoritative UTF-8 source cannot be read */ @@ -36,11 +37,12 @@ static void readSchedulerPolicy() throws IOException { agentPolicy = canonicalWhitespace( Files.readString(root.resolve("AGENTS.md"), StandardCharsets.UTF_8) ); - workflow = Files.readString( + workflow = canonicalWhitespace( + Files.readString( root.resolve(".github/workflows/hourly-opencode-maintenance.yml"), StandardCharsets.UTF_8 ) - .replace("\r\n", "\n"); + ); } /** @@ -72,6 +74,52 @@ void continuesOneBoundedNonConflictingSliceWhenNoPullRequestIsSourceActionable() )); } + /** Requires root-cause evidence and a realistic execution decision before remediation. */ + @Test + void performsRootCauseAnalysisAndFeasibilityClassificationBeforeActing() { + assertTrue(workflow.contains( + "For every failing or blocked outcome, perform root-cause analysis before choosing " + + "a remediation." + )); + assertTrue(workflow.contains( + "source, configuration, permission, quota, runner, provider, dependency, or policy " + + "boundary" + )); + assertTrue(workflow.contains( + "Generate bounded remediation options that address the identified cause." + )); + assertTrue(workflow.contains( + "test each option's feasibility against current permissions, branch protection, " + + "tool capability, runtime and compute budgets, dependency state, and path " + + "ownership" + )); + assertTrue(workflow.contains( + "Classify each option as executable now, requires an external actor, or unsafe or " + + "infeasible." + )); + } + + /** Requires the scheduler to act, verify, and continue after an infeasible preferred option. */ + @Test + void executesTheBestFeasibleActionAndContinuesAfterExternalOnlyBlockers() { + assertTrue(workflow.contains( + "Execute the highest-impact safe option that is executable in this run" + )); + assertTrue(workflow.contains("rerun the exact failing test or gate")); + assertTrue(workflow.contains( + "If the preferred option requires an external actor or is infeasible, keep that gate " + + "fail-closed and immediately choose the next safe feasible non-overlapping " + + "remediation or independent bounded product slice instead of stopping." + )); + assertTrue(workflow.contains( + "A pull request with only external blockers is not source-actionable." + )); + assertTrue(workflow.contains( + "When no open pull request is source-actionable, whether or not blocked pull requests " + + "remain open" + )); + } + /** Keeps every separately leased repository outside this scheduler's write authority. */ @Test void preservesReadOnlyDependencyLeasesWhileContinuingLocalWork() { From ab3d5b28353289b8d2f3789ce06b63b506d37b8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:27:28 +0900 Subject: [PATCH 140/152] fix(ci): require RCA and feasible action selection --- .github/workflows/hourly-opencode-maintenance.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 1ef5efd2..41a99c83 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -131,9 +131,13 @@ jobs: Start every run by inspecting every open pull request and its exact current head. Review unresolved human, CodeRabbit, GitHub Advanced Security, Dependabot, and automated feedback. Inspect required checks, statuses, and workflow outcomes. Treat queued, pending, skipped-required, stale-head, cancelled, absent, or unsuccessful gates as not passing. Distinguish valid current findings from stale, duplicate, incorrect, or superseded feedback. Resolve only source defects; deterministic non-model jobs and independent reviewers own remote branch publication and pull-request lifecycle operations. - When one dependency-eligible development pull request exists, fetch and check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and commit only to that local branch. Do not publish the branch remotely; the isolated deterministic branch publisher owns that authority. Do not create another branch for the same work. + For every failing or blocked outcome, perform root-cause analysis before choosing a remediation. Trace the observed evidence to the responsible source, configuration, permission, quota, runner, provider, dependency, or policy boundary; do not label a symptom, a queued state, or a repeated retry as the root cause. Generate bounded remediation options that address the identified cause. Before acting, test each option's feasibility against current permissions, branch protection, tool capability, runtime and compute budgets, dependency state, and path ownership. Classify each option as executable now, requires an external actor, or unsafe or infeasible. - When no development pull request exists, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice. Use exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug local branch and commit the bounded slice there. Do not publish the branch remotely or create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. + Execute the highest-impact safe option that is executable in this run, then rerun the exact failing test or gate and verify whether the condition changed. If the preferred option requires an external actor or is infeasible, keep that gate fail-closed and immediately choose the next safe feasible non-overlapping remediation or independent bounded product slice instead of stopping. Never claim resolution from a proposal, retry, aggregate-green result, or predecessor evidence; require exact-head or literal-source evidence that the failing condition is gone. A pull request with only external blockers is not source-actionable. + + When one source-actionable dependency-eligible development pull request exists, fetch and check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and commit only to that local branch. Do not publish the branch remotely; the isolated deterministic branch publisher owns that authority. Do not create another branch for the same work. + + When no open pull request is source-actionable, whether or not blocked pull requests remain open, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice that is independent of every blocked or invalid stack. Start from the unchanged protected develop head, use exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug local branch, and commit the bounded slice there. Do not publish the branch remotely or create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services. Database objects must contain at least two descriptive words and use snake_case by default. Public production APIs require complete beginner-readable documentation. Added production statements and branches require deterministic 100% statement and branch coverage. Use current authoritative standards, primary technical documentation, and peer-reviewed evidence where material, with APA 7th references in repository documentation. @@ -834,7 +838,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ needs.publish-agent-pull-request.outputs.pr_number }} HEAD_REF: ${{ needs.publish-agent-pull-request.outputs.head_ref }} - EXPECTED_HEAD: ${{ needs.publish-agent-pull-request.outputs.head_sha }} + EXPECTED_HEAD: ${{ needs.publish-agent-pull_request.outputs.head_sha }} run: | set -euo pipefail repository="${GITHUB_REPOSITORY}" From 36c87525a95bfeedebd604c545af4d9427271984 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:31:40 +0900 Subject: [PATCH 141/152] fix(ci): restore exact-head authorization dependency --- .github/workflows/hourly-opencode-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index 41a99c83..a02e98c3 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -838,7 +838,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ needs.publish-agent-pull-request.outputs.pr_number }} HEAD_REF: ${{ needs.publish-agent-pull-request.outputs.head_ref }} - EXPECTED_HEAD: ${{ needs.publish-agent-pull_request.outputs.head_sha }} + EXPECTED_HEAD: ${{ needs.publish-agent-pull-request.outputs.head_sha }} run: | set -euo pipefail repository="${GITHUB_REPOSITORY}" From 5efb68ed8e3028d03d530c1c416488db87892d4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:32:46 +0900 Subject: [PATCH 142/152] docs(agents): require RCA feasibility before action --- AGENTS.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 39807830..d6908a30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,19 @@ External review, approval, check, or read-only dependency latency is not a reaso productive mightyETL work. Those unavailable gates remain not passing and must never be reused as merge or release evidence. +For every failing or blocked outcome, identify the root cause before selecting a remediation. Trace +the observed evidence to a source, configuration, permission, quota, runner, provider, dependency, +or policy boundary. A symptom, queued state, repeated retry, or aggregate-green result is not a +root cause. Generate bounded options that address the identified cause, then test each option +against current permissions, branch protection, tool capability, runtime and compute budgets, +dependency state, path ownership, and repository-writer leases. + +Classify each option as executable now, requiring an external actor, or unsafe or infeasible. +Execute the highest-impact safe option that is executable during the current run and rerun the exact +failing test or gate. If the preferred option is external or infeasible, keep its gate fail-closed +and immediately continue with the next safe feasible non-overlapping remediation or independent +bounded product slice. Do not stop merely because the preferred option cannot be executed here. + A pull request is source-actionable only when its exact current head has a valid repository-local finding or failing source gate that mightyETL can repair. Queued checks, missing independent approval, synthetic-merge-only scanner evidence, and a separately leased dependency that has not From 1b90dbca4d9022a101a720d171892961d8f043cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:33:29 +0900 Subject: [PATCH 143/152] docs(doctoring): record RCA feasibility contract --- ...-opencode-nonblocking-progress-evidence.md | 177 +++++++++++++----- 1 file changed, 133 insertions(+), 44 deletions(-) diff --git a/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md b/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md index bdd3a8b3..7a5d34c4 100644 --- a/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md +++ b/docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md @@ -6,53 +6,111 @@ Reviewed on: **2026-08-08** The scheduled maintenance contract correctly refused to treat queued checks, missing approvals, synthetic-merge-only scanner output, and separately leased repository state as passing evidence. -However, it did not distinguish those external-only waits from repository-local source defects. -An external gate could therefore leave the hourly development loop repeatedly reporting the same -blocker without selecting safe, independent mightyETL work. +However, the first repair placed the nonblocking policy only in root `AGENTS.md`. The runtime +`PROMPT` passed directly to `opencode run` still selected work using these older conditions: -This was an orchestration defect, not permission to weaken merge policy. The correct behavior is: +- act on a pull request whenever a dependency-eligible development pull request exists; and +- develop a new product slice only when no development pull request exists. + +That made the behavior operationally incomplete. An open pull request blocked only by an external +review, approval, scanner identity, central dependency, quota, or runner condition could still be +mistaken for source-actionable work. The agent could repeatedly observe the same blocker without +performing root-cause analysis, testing whether a proposed remedy was executable in the current +run, or selecting an independent feasible product slice. + +This was an orchestration defect, not permission to weaken merge policy. The runtime contract must: 1. keep every unavailable or stale gate non-passing; -2. repair the earliest valid repository-local finding first; -3. never deepen an invalid stack; -4. when no open pull request is source-actionable, select at most one independent bounded slice - from protected `develop` that does not depend on or overlap the invalid stack; and -5. keep separately leased repositories read-only. +2. identify the root cause rather than restating a symptom; +3. generate bounded remedies that address the identified cause; +4. test each remedy against live authority, protection, capacity, dependency, and ownership + constraints; +5. execute and verify the best safe option that is feasible now; +6. preserve an external or infeasible gate as blocked while immediately selecting the next safe + feasible non-overlapping action; and +7. never deepen an invalid stack or cross a repository-writer lease. -## Decision +## Root-cause analysis + +The immediate root cause was **instruction-path drift**. The repository guidance and the actual +scheduled model prompt no longer expressed the same work-selection state machine. The prompt used +open-PR existence as the branch condition, while the intended policy required source-actionability. -`AGENTS.md` now defines `source-actionable` narrowly. A pull request is source-actionable only when -its exact current head contains a valid repository-local finding or failing source gate that this -repository can repair. Review latency, approval latency, queued checks, synthetic-only scanner -identity, or an unintegrated read-only central dependency remain blockers to merge but do not stop -all unrelated mightyETL development. +The following are explicitly not root causes: + +- a queued check by itself; +- a repeated retry that reproduces the same state; +- an aggregate-green workflow whose relevant evidence uses a synthetic merge revision; +- an independent approval that does not yet exist; or +- a protected dependency that this repository is not authorized to mutate. + +Those are observations or external boundaries. A valid RCA traces them to the responsible source, +configuration, permission, quota, runner, provider, dependency, or policy boundary. + +## Decision -When no open pull request is source-actionable, the scheduled agent may create exactly one bounded, -non-conflicting candidate from protected `develop`. The candidate must not depend on, retarget, -rewrite, or overlap files changed by the invalid stack. If no such slice exists, the run returns -without a candidate rather than manufacturing activity. +Both root `AGENTS.md` and the runtime OpenCode prompt now require the same bounded loop: + +```text +observe exact current state +→ perform RCA +→ generate cause-addressing options +→ classify feasibility +→ execute the highest-impact safe feasible option +→ rerun the exact failing test or gate +→ verify the condition changed +→ otherwise keep that gate fail-closed and continue with the next feasible action +``` + +A remediation is classified as one of: + +- **executable now** — current repository authority, tooling, runtime, compute budget, dependency + state, path ownership, and writer lease permit the action; +- **requires an external actor** — a human reviewer, organization policy owner, protected central + repository, provider, entitlement administrator, or other authority must act; or +- **unsafe or infeasible** — the action would bypass protection, exceed bounded resources, race + another writer, mutate an unleased repository, deepen an invalid stack, or cannot be verified. + +The agent executes only the first category. The second and third categories do not become passing +evidence. They also do not halt unrelated work when a safe independent slice exists. + +A pull request is source-actionable only when its exact current head contains a valid +repository-local finding or failing source gate that this repository can repair. Review latency, +approval latency, queued checks, synthetic-only scanner identity, or an unintegrated read-only +central dependency remain blockers to merge but do not stop unrelated mightyETL development. + +When no open pull request is source-actionable, even if blocked pull requests remain open, the +scheduled agent may create exactly one bounded, non-conflicting candidate from the unchanged +protected `develop` head. The candidate must not depend on, retarget, rewrite, or overlap files +changed by a blocked or invalid stack. If no such slice exists, the run returns without a candidate +rather than manufacturing activity. The repository-writer lease is unchanged. `ContextualWisdomLab/.github`, `naruon`, `contextual-orchestrator`, and every separately leased repository remain read-only to this loop. The scheduler may inspect their exact integration state but may not mutate them, dispatch a write-capable agent there, or post a mutation-trigger comment. -## Why `AGENTS.md` is authoritative to OpenCode +## Why feasibility is an execution gate -OpenCode's project initialization creates a repository-root `AGENTS.md`, and its official -instructions recommend committing that file so OpenCode can understand project structure and -coding patterns. The scheduled workflow checks out protected default-branch source before invoking -plain `opencode run`, so the repository instruction is present at agent startup without adding a -second credential or changing the review-agent contract. +A technically plausible remedy is not necessarily executable. For example: + +- changing a protected central workflow from this repository violates the writer lease; +- manufacturing an approval violates independent-review policy; +- treating a synthetic merge scan as literal-head evidence does not repair source identity; +- repeatedly rerunning an unavailable provider does not address entitlement or quota; and +- opening another stacked branch can worsen an invalid dependency graph. + +The scheduler therefore checks feasibility before writing. This turns “find a solution” into a +bounded decision that can actually be executed and verified under current conditions. ## Test-first evidence -### RED +### Initial RED: nonblocking progress policy Commit `7ae7dcc2446c5a6bacd75a3602b83e8ea1f6f3f2` added -`HourlyOpenCodeProgressPolicyTest` before the policy existed. Literal-head CI run `31256917651` -checked out that exact SHA. macOS job `93101583785` ran 313 tests and failed exactly the two new -policy tests: +`HourlyOpenCodeProgressPolicyTest` before the nonblocking policy existed. Literal-head CI run +`31256917651` checked out that exact SHA. macOS job `93101583785` ran 313 tests and failed exactly +the two initial policy tests: - `continuesOneBoundedNonConflictingSliceWhenNoPullRequestIsSourceActionable`; - `preservesReadOnlyDependencyLeasesWhileContinuingLocalWork`. @@ -60,38 +118,69 @@ policy tests: The failure was caused by absent scheduler policy text, not compilation, unrelated production code, or a stale checkout. That commit and run remain permanent fail-first evidence. -### GREEN implementation +### Current RED: RCA and realistic feasibility + +Commit `dba0af66481e7ba98fd16b19792feebd7fb71e1b` added two additional contract tests before the +runtime workflow prompt was changed. Literal-head CI run `31259348638` checked out that exact SHA. +macOS job `93107620623` ran 315 tests and failed exactly these two new tests: + +- `performsRootCauseAnalysisAndFeasibilityClassificationBeforeActing`; +- `executesTheBestFeasibleActionAndContinuesAfterExternalOnlyBlockers`. -Commit `8bac86c8bcdd981a9b88d888bc6cfbabb8d1ee53` added the minimal AGENTS contract required by the -failing tests. Checks from the RED head do not transfer. The exact current head must complete its own -CI, dependency, SBOM, SAST, security, commit-status, review-thread, and independent-approval gates -before merge. +The existing 313 tests, including the earlier nonblocking and lease tests, passed. This proves the +new failure was specifically the missing runtime RCA and feasibility contract rather than a broad +repository regression. + +### Implementation and refactor + +Commit `ab3d5b28353289b8d2f3789ce06b63b506d37b8a` added the minimal runtime prompt changes required +by the current RED tests. During exact diff inspection, an unrelated `needs` identifier typo caused +by whole-file workflow publication was found before it could be accepted. Commit +`36c87525a95bfeedebd604c545af4d9427271984` restored the original exact-head authorization +expression. The corrective commit changes only that one identifier. + +Root guidance was then aligned with the runtime prompt so future agents see the same RCA, +feasibility, execution, verification, and fallback state machine. Checks from every predecessor +head are stale. The final exact current head must complete its own CI, dependency, SBOM, SAST, +security, commit-status, review-thread, and independent-approval gates before merge. ## Safety properties - External-only blockers remain not passing. +- RCA must identify a responsible boundary rather than relabel a symptom. +- Proposed remedies are tested against current authority and operational constraints before use. +- Only a safe option classified executable now may be performed. +- The exact failing test or gate is rerun after remediation. +- An external or infeasible preferred option does not stop the next independent feasible action. - No approval, review, check, or scanner evidence is synthesized. - No protected branch is written directly. - No invalid downstream stack boundary is deepened. - At most one independently reviewable candidate may be produced per run. -- Candidate paths must be disjoint from the invalid stack's changed paths. +- Candidate paths must be disjoint from blocked and invalid stack paths. - Central and separately leased repositories remain read-only. - The existing NVIDIA NIM and review-agent credential contracts are unchanged. ## Failure behavior -If an agent cannot prove that a proposed slice is independent of every invalid stack item, it must -produce no candidate. If any relevant head, base, open-PR set, or automation branch changes during -the run, deterministic publication continues to fail closed. If a later repository instruction -conflicts with the workflow's permission boundary, the narrower no-write and no-merge controls win. +If the scheduler cannot trace a blocker to evidence, it must not guess at a destructive remedy. If +an option requires authority unavailable to the current run, that option remains external rather +than being retried as though it were executable. If the preferred option is infeasible, the run +continues to the next safe feasible non-overlapping action. If no safe action exists, it performs +read-only analysis and emits no candidate. + +If an agent cannot prove that a proposed slice is independent of every blocked or invalid stack +item, it must produce no candidate. If any relevant head, base, open-PR set, or automation branch +changes during the run, deterministic publication continues to fail closed. If repository guidance +conflicts with the workflow permission boundary, the narrower no-write and no-merge controls win. ## Rollback -Rollback removes this policy section and its regression test together only after replacing them -with a stricter reviewed mechanism that still preserves both properties: unavailable gates are not -passing, and external latency does not halt unrelated bounded work. Do not roll back by allowing -stack deepening, central-repository mutation, direct pull-request lifecycle operations, or stale -check reuse. +Rollback removes the RCA and feasibility prompt, aligned repository guidance, and regression tests +together only after replacing them with a stricter reviewed mechanism that preserves all of these +properties: unavailable gates are not passing; remedies address evidenced causes; feasibility is +checked before execution; exact verification follows action; and external latency does not halt +unrelated bounded work. Do not roll back by allowing stack deepening, central-repository mutation, +direct pull-request lifecycle operations, or stale check reuse. ## References From 427ce2b0258b837cd01f80cecea2a7e0981963a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 22:34:33 +0900 Subject: [PATCH 144/152] docs(changelog): record RCA feasibility scheduler --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fac6ad95..5c8ea782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Scheduled OpenCode maintenance now keeps unavailable external gates fail-closed while continuing exactly one independent, non-overlapping mightyETL slice from protected `develop` when no open pull request is source-actionable; invalid stacks and separately leased dependency repositories remain untouched. +- Scheduled OpenCode maintenance now performs root-cause analysis, tests remediation feasibility against live authority, protection, resource, dependency, path-ownership, and writer-lease constraints, executes and verifies the best safe option available now, and continues exactly one independent bounded mightyETL slice from protected `develop` when only external blockers remain; invalid stacks and separately leased repositories stay untouched. - Container builds now pin Maven and Eclipse Temurin base-image tags to reviewed SHA-256 digests, with a fail-first contract test preventing mutable registry tags from re-entering the Dockerfile. - The model-executing hourly OpenCode maintenance job now has read-only issue access; fail-first workflow-contract coverage proves `issues: write` is unnecessary while preserving issue and roadmap inspection. - Pull-request CI and CycloneDX SBOM jobs now check out the literal current source head, immediately assert `git rev-parse HEAD` against `github.event.pull_request.head.sha`, and disable checkout credential persistence; generated merge revisions remain useful compatibility previews but no longer masquerade as direct exact-head source evidence. @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Test-first doctoring for external-wait progress, source-actionable pull-request classification, invalid-stack isolation, and read-only dependency leases in `docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md`. +- Test-first doctoring for external-wait progress, root-cause analysis, realistic remediation feasibility, exact post-action verification, source-actionable pull-request classification, invalid-stack isolation, and read-only dependency leases in `docs/doctoring/hourly-opencode-nonblocking-progress-evidence.md`. - Permanent fail-first exact-head workflow contracts and authoritative evidence in `docs/doctoring/exact-head-source-workflow-evidence.md`, including observed synthetic-merge checkout behavior, cross-platform source identity assertions, the rejected ignored Dependency Review ref-override experiment, corrective pull-request event-endpoint semantics, least-privilege boundaries, stack invalidation rules, rollback prohibition, and APA 7th GitHub references. - A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow. - Exact-head workflow-run authorization doctoring evidence for the repository-token recursion boundary, before/after SHA snapshots, policy-path exclusion, time-of-check/time-of-use validation, least privilege, test-first regression evidence, and rollback in `docs/doctoring/github-token-exact-head-check-authorization-evidence.md`. From b1afefd9a8264c4cf7f5c409f853abebfe70dc17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 01:15:08 +0900 Subject: [PATCH 145/152] test(docs): require current OpenCode authority topology --- ...rlyOpenCodeAuthorityDocumentationTest.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java new file mode 100644 index 00000000..42a7ea06 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java @@ -0,0 +1,117 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Keeps the authoritative OpenCode security doctoring aligned with the executable workflow. + * + *

This contract prevents acquisition and security evidence from describing superseded token + * authority. The model-executing job must remain repository-read-only, while branch publication, + * pull-request publication, and workflow-run authorization stay in separate deterministic jobs.

+ */ +class HourlyOpenCodeAuthorityDocumentationTest { + + private static String workflow; + private static String doctoring; + + /** + * Reads the executable workflow and its authoritative security evidence using canonical + * whitespace so prose wrapping cannot change the contract. + * + * @throws IOException when either checked repository document cannot be read + */ + @BeforeAll + static void readAuthoritySources() throws IOException { + Path root = projectRoot(); + workflow = canonicalWhitespace(Files.readString( + root.resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + )); + doctoring = canonicalWhitespace(Files.readString( + root.resolve("docs/doctoring/github-token-exact-head-check-authorization-evidence.md"), + StandardCharsets.UTF_8 + )); + } + + /** Requires doctoring to state that the model job has no repository write permission. */ + @Test + void documentsModelJobAsReadOnlyGitHubAuthority() { + assertTrue(workflow.contains("maintain-repository:")); + assertTrue(workflow.contains("actions: read")); + assertTrue(workflow.contains("contents: read")); + assertTrue(workflow.contains("issues: read")); + assertTrue(workflow.contains("pull-requests: read")); + + assertTrue(doctoring.contains( + "`maintain-repository` is the only job that checks out source or runs OpenCode. " + + "It has read-only GitHub authority" + )); + assertTrue(doctoring.contains( + "actions: read checks: read contents: read issues: read pull-requests: read " + + "security-events: read statuses: read" + )); + } + + /** Requires doctoring to identify the isolated deterministic jobs that own each write. */ + @Test + void documentsSeparatedBranchPullRequestAndActionsWriters() { + assertTrue(workflow.contains("publish-agent-branch:")); + assertTrue(workflow.contains("publish-agent-pull-request:")); + assertTrue(workflow.contains("authorize-exact-head-checks:")); + + assertTrue(doctoring.contains( + "`publish-agent-branch` is the sole `contents: write` holder" + )); + assertTrue(doctoring.contains( + "`publish-agent-pull-request` is the sole `pull-requests: write` holder" + )); + assertTrue(doctoring.contains( + "`authorize-exact-head-checks` is the sole `actions: write` holder" + )); + assertTrue(doctoring.contains( + "The model-executing job receives none of those write permissions" + )); + } + + /** + * Collapses semantically irrelevant whitespace for stable Markdown and YAML prose assertions. + * + * @param value repository text to normalize + * @return one-space canonical representation + */ + private static String canonicalWhitespace(String value) { + return value.replaceAll("\\s+", " ").trim(); + } + + /** + * Finds the repository root from either reactor-root or module-local Maven execution. + * + * @return absolute repository root + */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 75265fae9fcf5faac6f8b9cab63247ac86f23dd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 01:18:19 +0900 Subject: [PATCH 146/152] docs(security): align OpenCode authority evidence --- ...exact-head-check-authorization-evidence.md | 271 ++++++++++++++---- 1 file changed, 210 insertions(+), 61 deletions(-) diff --git a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md index 221f4546..3082f3e5 100644 --- a/docs/doctoring/github-token-exact-head-check-authorization-evidence.md +++ b/docs/doctoring/github-token-exact-head-check-authorization-evidence.md @@ -1,13 +1,20 @@ # GitHub-token publication and exact-head authorization evidence +Reviewed on: **2026-08-09** + ## Decision -The hourly development loop uses the repository-scoped `GITHUB_TOKEN`, but it does not expose the same GitHub authority to the model, the pull-request publisher, and the workflow-run authorizer. +The hourly development loop uses the repository-scoped `GITHUB_TOKEN`, but no model-executing +process receives remote repository write authority. Source analysis and local candidate creation, +branch publication, pull-request publication, workflow-run authorization, independent review, and +merge are separate authorities. The deployed contract is: ```text -model execution and branch push +model execution and local commit + ≠ +deterministic branch publication ≠ draft pull-request publication ≠ @@ -16,69 +23,113 @@ workflow-run authorization independent review and merge ``` -This separation addresses two concrete failure modes found during exact-head review: +This separation addresses three concrete failure modes found during exact-head review: + +1. `pull-requests: write` on a model job could allow a model process to call pull-request lifecycle + endpoints even when its prompt prohibited that behavior. +2. model-scoped `contents: write` unnecessarily coupled repository execution to remote branch + mutation. +3. authorizing workflow runs by `head_sha` alone could authorize a run associated with another pull + request that referenced the same commit. -1. `pull-requests: write` on the OpenCode job allowed the model process to call review and merge endpoints even when the prompt prohibited it. -2. authorizing workflow runs by `head_sha` alone could authorize a run associated with another pull request that referenced the same commit. +The current design therefore keeps the OpenCode job read-only and transfers one checksum-bound local +candidate through deterministic jobs with narrowly separated write permissions. ## Primary-source finding OpenCode 1.18.13 exposes two relevant execution paths. -- `opencode github run` includes GitHub lifecycle behavior and can call the pull-request creation endpoint after committing and pushing scheduled work. -- `opencode run` is the plain non-interactive model runner and does not itself own GitHub pull-request publication. +- `opencode github run` includes GitHub lifecycle behavior and can participate in pull-request + publication after model execution. +- `opencode run` is the plain non-interactive model runner and does not itself require pull-request + publication authority. -mightyETL therefore uses `opencode run --model ... --auto` under a read-only pull-request token. Deterministic jobs validate and publish the resulting branch separately. +mightyETL uses `opencode run --model ... --auto` with repository-read-only GitHub authority. The +model may inspect, edit, test, and commit one bounded candidate in the checked-out local Git +repository, but it cannot publish that commit to GitHub. Deterministic non-model jobs validate and +publish the exact candidate separately. ## Authority topology ```mermaid flowchart TB - M[maintain-repository] -->|candidate JSON| P[publish-agent-pull-request] + M[maintain-repository: read-only model execution] -->|checksum-bound candidate artifact| B[publish-agent-branch] + B -->|published exact branch head| P[publish-agent-pull-request] P -->|PR number, head ref, exact SHA| A[authorize-exact-head-checks] A --> C[required CI and security workflows] - C --> R[independent OpenCode and Noema reviews] + C --> R[independent OpenCode / Noema / human review] R --> D[expected-head merge disposition] ``` ### Model job -`maintain-repository` is the only job that checks out source or runs OpenCode. It has: +`maintain-repository` is the only job that checks out source or runs OpenCode. It has read-only GitHub authority: ```text actions: read checks: read -contents: write -issues: write +contents: read +issues: read pull-requests: read security-events: read statuses: read ``` -The model can inspect pull requests and push one branch. It cannot create, update, approve, close, or merge a pull request through the token. The prompt prohibition remains defense in depth rather than the primary authorization boundary. +The model can inspect GitHub state and can create commits only in the runner-local checkout. It has +no GitHub token permission that can update a branch, issue, pull request, workflow run, status, or +security result. It contains no `git push`. The prompt prohibition on remote pull-request mutation, +protected-branch pushes, approval, merge, and release remains defense in depth rather than the +primary authorization boundary. -### Deterministic publisher +### Deterministic branch publisher -`publish-agent-pull-request` has: +`publish-agent-branch` is the sole `contents: write` holder. It has: ```text -contents: read -pull-requests: write +actions: read +contents: write ``` -It never checks out or executes repository code and never receives `NVIDIA_API_KEY`. It accepts only one structured candidate emitted by the model job. A new branch must: +It never checks out repository source through `actions/checkout`, never receives `NVIDIA_API_KEY`, +and never runs OpenCode. It downloads only the candidate artifact emitted by the model job and +requires the artifact metadata and SHA-256 digest to match the exact upstream job output. + +Before a branch write, it verifies: + +- the protected `develop` head is unchanged from the model-job snapshot; +- for an existing pull request, the exact live branch and captured predecessor SHA are unchanged; +- for a new branch, the strict `automation/opencode-YYYYMMDDTHHMMSSZ-short-slug` namespace does not + already exist remotely; +- the imported bundle contains the expected exact candidate head; +- the candidate is a non-destructive descendant of the exact predecessor; +- the candidate has between 1 and 50 commits and contains no merge commit; +- at most 50 paths changed; +- no `.github/**` or `CODEOWNERS` path changed; and +- `git diff --check` succeeds. -- match `automation/opencode-YYYYMMDDTHHMMSSZ-short-slug`; -- retain the captured exact SHA; -- be ahead of `develop`; -- change at most 50 files; -- avoid `.github/**` and all `CODEOWNERS` paths. +Only then does the job perform one non-forced branch push. It immediately reads the remote branch +back and requires the live SHA to equal the exact candidate head. A moved predecessor, changed +protected head, invalid artifact, unexpected path, destructive ancestry, ambiguous remote branch, +or mismatched post-push head fails closed. -The publisher creates a draft with a fixed JSON payload. It contains no pull-request review or merge endpoint. +### Deterministic pull-request publisher + +`publish-agent-pull-request` is the sole `pull-requests: write` holder. It has: + +```text +contents: read +pull-requests: write +``` + +It never checks out or executes repository source and never receives `NVIDIA_API_KEY`. It accepts +only metadata from the successful deterministic branch publisher, rereads the live branch or +existing pull request, verifies the exact expected head and bounded path policy, and creates at most +one draft pull request when a new validated branch has no existing pull request. It contains no +review-approval or merge endpoint. ### Exact-head run authorizer -`authorize-exact-head-checks` has: +`authorize-exact-head-checks` is the sole `actions: write` holder. It has: ```text actions: write @@ -86,7 +137,8 @@ contents: read pull-requests: read ``` -It never checks out source and never receives the model credential. Before authorizing a run, it requires: +It never checks out source and never receives the model credential. Before authorizing a waiting +run, it requires: ```text run.event == pull_request @@ -95,11 +147,21 @@ any(run.pull_requests; number == expected_pull_request_number) live pull request head == expected_head ``` -The `pull_requests` association is mandatory. A commit SHA is not a unique pull-request identity: more than one pull request can reference the same commit. +The `pull_requests` association is mandatory. A commit SHA is not a unique pull-request identity: +more than one pull request can reference the same commit. The job may authorize only +`action_required` or `waiting` workflow runs and contains no pull-request approval or merge +operation. + +The model-executing job receives none of those write permissions. No deterministic writer receives +the NVIDIA model credential, and neither the model job nor any deterministic scheduler job can +manufacture the independent non-author review required before merge. ## Complete workflow materialization -GitHub can materialize pull-request workflows asynchronously. The authorizer repeats bounded discovery and authorizes newly visible `action_required` or `waiting` runs on every pass. It succeeds only after this complete workflow-name set is associated with the exact pull request and exact SHA: +GitHub can materialize pull-request workflows asynchronously. The authorizer repeats bounded +discovery and authorizes newly visible `action_required` or `waiting` runs on every pass. It +succeeds only after this complete workflow-name set is associated with the exact pull request and +exact SHA: ```text CI @@ -109,62 +171,149 @@ SAST Semgrep Security Scan ``` -Name presence proves only that a run was created. Every run must still complete successfully before merge. +Name presence proves only that a run was created. Every required run must still complete +successfully on acceptable exact-head evidence before merge. Queued, waiting, action-required, +skipped-required, cancelled, stale-head, synthetic-merge-only, absent, neutral-required, or failed +evidence remains non-passing. ## Time-of-check/time-of-use controls -The loop validates state at several points: +The loop validates state at several boundaries: 1. snapshot `develop`, open pull-request heads, and prior automation branches before model execution; -2. require `develop` to remain unchanged after the model exits; -3. select at most one changed existing pull request or strict automation branch; -4. re-read the branch or pull request before draft publication; -5. re-read the pull request before workflow-run discovery; -6. re-read the exact head on every discovery pass; -7. re-read it once more before declaring authorization complete. - -A moved head, multiple candidate, invalid namespace, policy-file change, absent required workflow, or GitHub authorization rejection fails closed. +2. require `develop`, the open pull-request set, and automation-branch state to remain unchanged + after the model exits; +3. select at most one exact local candidate and bind its predecessor, branch, and candidate SHA into + metadata; +4. create a Git bundle plus SHA-256 digest and transfer only that bounded artifact to the branch + publisher; +5. re-read protected `develop` and the exact predecessor immediately before branch publication; +6. verify bundle integrity, ancestry, commit count, merge-free history, path count, policy-path + exclusion, and `git diff --check` before one non-forced push; +7. read the published branch back and require its live SHA to equal the candidate SHA; +8. re-read the branch or pull request before draft pull-request publication; +9. re-read the pull request before workflow-run discovery; +10. re-read the exact head on every discovery pass; and +11. re-read it once more before declaring workflow-run authorization complete. + +A moved head or base, multiple candidate, invalid namespace, changed policy path, artifact mismatch, +absent required workflow, or GitHub authorization rejection fails closed. ## Test-first evidence -`HourlyOpenCodeMaintenanceWorkflowTest` was changed before production to require: +`HourlyOpenCodeMaintenanceWorkflowTest` proves the executable authority boundary, including: - plain `opencode run`, not the GitHub lifecycle handler; -- `pull-requests: read` on the model job; -- exactly one non-checkout publisher with `pull-requests: write`; +- `contents: read`, `issues: read`, and `pull-requests: read` on the model job; +- no `contents: write`, `pull-requests: write`, or `actions: write` on the model job; +- exactly one isolated branch publisher with `contents: write` and no model credential; +- exactly one non-checkout pull-request publisher with `pull-requests: write`; - exactly one non-checkout authorizer with `actions: write`; -- no NVIDIA credential in either privileged deterministic job; +- no NVIDIA credential in any privileged deterministic writer; - one strict publication candidate; -- draft-only deterministic publication; +- draft-only deterministic pull-request publication; - `.github/**` and `CODEOWNERS` exclusion; - exact pull-request association for every workflow run; -- complete required-workflow materialization; +- complete required-workflow materialization; and - continued absence of review and merge endpoints. -The initial test commit intentionally made the existing workflow contract fail. Production was then changed to satisfy the new authority and association requirements. - -## Residual risks and controls - -- `contents: write` remains necessary for a branch push. Protected-branch rules remain authoritative for `develop` and `main`. -- The deterministic publisher necessarily has coarse pull-request write permission. It has no checkout, model input, or executable repository source and its script exposes only draft creation or metadata validation. -- Workflow-run approval is an Actions control, not a successful check or pull-request approval. -- A workflow or `CODEOWNERS` change always requires explicit human authorization. -- Independent exact-head approval remains mandatory after every new commit. +During the 2026-08-09 acquisition-evidence audit, this doctoring file was found to describe an older +authority topology in which the model job still held `contents: write` and pushed a branch itself. +That prose contradicted the executable workflow even though the executable least-privilege boundary +was already narrower. + +A fail-first contract was therefore added before changing this document. Exact-head commit +`b1afefd9a8264c4cf7f5c409f853abebfe70dc17` was checked out by CI run `31266495903`; macOS job +`93125370929` ran 317 tests and failed exactly the two new +`HourlyOpenCodeAuthorityDocumentationTest` methods that require current model-read-only and +separated-writer doctoring. The existing tests passed up to that intentional documentation +contract. Checks from that RED head do not transfer to a later head. + +The authoritative acceptance condition after this correction is a fresh exact-head CI run in which +those two tests and the complete project suite pass, plus the ordinary dependency, SBOM, SAST, +security, status, review-thread, and independent-approval gates for that unchanged head. + +## Residual risks, remediation feasibility, and controls + +### `contents: write` Scorecard finding + +GitHub Advanced Security currently reports the isolated `publish-agent-branch` job's job-scoped +`contents: write` permission. This is a real sensitive capability and remains visible rather than +being mislabeled as eliminated. + +Root-cause options were evaluated against the deployed architecture: + +1. **Remove `contents: write`.** Rejected for the current autonomous-development design because the + validated local commit would have no GitHub branch-persistence authority; this removes the + required function rather than reducing its write surface while preserving behavior. +2. **Move branch publication back into the model job.** Rejected because it expands the untrusted + model-execution authority and reverses the existing separation of duties. +3. **Introduce a PAT, new secret, or GitHub App solely to avoid the Scorecard signal.** Rejected + unless a separately reviewed credential actually exists and proves narrower endpoint scope, + lifetime, installation scope, auditability, and branch-protection behavior. No credential is + invented merely because a scanner dislikes a required permission. +4. **Keep one deterministic `contents: write` branch publisher with exact artifact, ancestry, + live-ref, path, commit-count, and post-write SHA validation.** Executable now and the narrowest + currently proven design that retains autonomous branch publication. + +The unresolved Scorecard thread therefore remains open while the permission exists. A green +aggregate Security Scan is not treated as literal-head proof if its relevant scanner checked a +synthetic merge. The separately leased organization scanner repair must integrate before +literal-head scanner/SARIF evidence can support final disposition. Documentation alone does not +resolve the scanner finding. + +### Other residual risks + +- The deterministic pull-request publisher necessarily has coarse `pull-requests: write` + permission. It has no checkout, model input, NVIDIA credential, or executable repository source, + and its script exposes only bounded draft publication or metadata validation. +- `actions: write` can authorize workflow execution. It is isolated in a non-checkout job, exact + pull-request association is mandatory, and workflow-path changes are refused from autonomous + candidate publication. +- Workflow-run authorization is an Actions control, not a successful check or pull-request + approval. +- A workflow or `CODEOWNERS` change always requires explicit human authorization under this + scheduler contract. +- Independent exact-head approval remains mandatory after every new commit; checks, statuses, + comments, reactions, author reviews, and textual acknowledgements do not substitute for it. + +## Failure behavior + +Any mismatch between the documented authority topology and the executable workflow is a failed +security-evidence contract, not a documentation-only cosmetic issue. The fix is to reconcile the +authoritative evidence to the narrower executable behavior or deliberately redesign the workflow +under a new fail-first contract. Do not make the executable workflow more permissive merely to make +an old document true. + +If GitHub changes `GITHUB_TOKEN` recursion, branch-update, review-request, or workflow-run approval +semantics, disable the affected autonomous path until the exact authority boundary is revalidated. +Do not restore pull-request or content write authority to the model job as a shortcut. ## Rollback -Disable the hourly workflow if the platform's `GITHUB_TOKEN` recursion or workflow-run approval model changes. Do not restore pull-request write authority to the model job and do not revert to SHA-only run authorization. +Rollback of this evidence correction is permitted only together with a reviewed replacement that +still describes the exact deployed authority topology. Do not roll back by moving branch +publication into the model job, by restoring model-scoped `contents: write` or `pull-requests: +write`, by accepting SHA-only workflow-run authorization, or by weakening exact-head, review, +branch-protection, and policy-path gates. -A GitHub App or endpoint proxy may replace the publisher only after its endpoint allowlist, actor identity, installation scope, token lifetime, audit log, and exact-head behavior are independently tested and documented. +A GitHub App or endpoint proxy may replace the deterministic branch publisher only after its +endpoint allowlist, actor identity, installation scope, token lifetime, audit log, protected-branch +behavior, and exact-head acceptance path are independently tested and documented. ## References — APA 7th -Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts +Anomaly. (2026). *GitHub handler (Version 1.18.13)* [Source code]. GitHub. +https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/github.handler.ts -Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts +Anomaly. (2026). *Run command (Version 1.18.13)* [Source code]. GitHub. +https://github.com/anomalyco/opencode/blob/v1.18.13/packages/opencode/src/cli/cmd/run.ts -GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token +GitHub, Inc. (2026). *GITHUB_TOKEN*. GitHub Docs. +https://docs.github.com/en/actions/concepts/security/github_token -GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs +GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. +https://docs.github.com/en/rest/actions/workflow-runs -GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions +GitHub, Inc. (2026). *Security hardening for GitHub Actions*. GitHub Docs. +https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions From d55e06d7fe4e28632849aa0ce72239cdddf4dee5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:18:08 +0900 Subject: [PATCH 147/152] test(automation): prove duplicate run approval red --- ...CodeRequiredWorkflowAuthorizationTest.java | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java index 342e01b1..58eb13c8 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java @@ -22,16 +22,14 @@ class HourlyOpenCodeRequiredWorkflowAuthorizationTest { /** * Requires the authorization loop to wait for, associate, and account for every required - * workflow without coupling the test to incidental shell-variable or log-message wording. + * workflow. The polling count and terminal diagnostic text are intentionally pinned because + * they are part of the bounded authorization and operator-diagnostic contract. * * @throws IOException when the production workflow cannot be read */ @Test void waitsForEveryRequiredExactHeadWorkflow() throws IOException { - String workflow = Files.readString( - projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), - StandardCharsets.UTF_8 - ).replace("\r\n", "\n"); + String workflow = workflow(); assertTrue(workflow.contains( "required_workflow_names='[\"CI\",\"Dependency Review\"," @@ -50,6 +48,53 @@ void waitsForEveryRequiredExactHeadWorkflow() throws IOException { assertTrue(workflow.contains("Authorized exact-head pull-request checks for PR")); } + /** + * Requires successful approvals to be remembered across polling passes so eventual-consistency + * lag cannot make the authorizer POST the same run twice and fail under {@code set -e}. + * + * @throws IOException when the production workflow cannot be read + */ + @Test + void doesNotApproveTheSameWorkflowRunTwiceAcrossPollingPasses() throws IOException { + String workflow = workflow(); + + assertTrue( + workflow.contains( + "approved_run_ids_file=\"${RUNNER_TEMP}/pr-${number}-approved-run-ids.txt\"" + ), + "Authorization must keep one approved-run ledger for the overall polling operation" + ); + assertTrue( + workflow.contains(": > \"${approved_run_ids_file}\""), + "The approved-run ledger must be initialized once before polling begins" + ); + assertTrue( + workflow.contains( + "if grep -Fxq \"${run_id}\" \"${approved_run_ids_file}\"; then" + ), + "Polling must skip a run id that was already approved on an earlier pass" + ); + assertTrue( + workflow.contains( + "printf '%s\\n' \"${run_id}\" >> \"${approved_run_ids_file}\"" + ), + "A run id must be recorded only after its approval request succeeds" + ); + } + + /** + * Reads the production workflow with normalized line endings. + * + * @return UTF-8 workflow source + * @throws IOException when the production workflow cannot be read + */ + private static String workflow() throws IOException { + return Files.readString( + projectRoot().resolve(".github/workflows/hourly-opencode-maintenance.yml"), + StandardCharsets.UTF_8 + ).replace("\r\n", "\n"); + } + /** * Finds the repository root from root- or module-scoped Maven execution. * @@ -72,4 +117,4 @@ private static Path projectRoot() { } throw new IllegalStateException("Could not find project root"); } -} +} \ No newline at end of file From 5d083a08f92a633c7c85e18f21e4a42535bf2caa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:18:54 +0900 Subject: [PATCH 148/152] test(security): prove indented Docker FROM bypass red --- .../ContainerImagePinningTest.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java index 76c7aad6..a52c09c5 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java @@ -12,6 +12,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -43,7 +44,36 @@ void pinsEveryExternalBaseImageBySha256Digest() throws IOException { Path dockerfilePath = projectRoot().resolve("Dockerfile"); assertTrue(Files.isRegularFile(dockerfilePath), "The repository Dockerfile must exist"); - String dockerfile = Files.readString(dockerfilePath, StandardCharsets.UTF_8); + assertExternalImagesArePinned( + Files.readString(dockerfilePath, StandardCharsets.UTF_8) + ); + } + + /** + * Requires Docker-compatible leading indentation to remain inside the immutable-image policy. + * + *

Docker accepts spaces or tabs before an instruction. A mutable external image must + * therefore still fail this policy when its {@code FROM} instruction is indented.

+ */ + @Test + void rejectsIndentedMutableExternalBaseImage() { + String pinnedDigest = "a".repeat(64); + String dockerfile = "FROM example.invalid/build@sha256:" + pinnedDigest + " AS build\n" + + " FROM alpine:latest\n"; + + assertThrows( + AssertionError.class, + () -> assertExternalImagesArePinned(dockerfile), + "Indented FROM instructions must not bypass immutable image enforcement" + ); + } + + /** + * Applies the immutable-image policy to Dockerfile source text. + * + * @param dockerfile Dockerfile text to validate + */ + private static void assertExternalImagesArePinned(String dockerfile) { Matcher matcher = FROM_INSTRUCTION.matcher(dockerfile); Set localStageAliases = new HashSet<>(); int externalImageCount = 0; @@ -90,4 +120,4 @@ private static Path projectRoot() { } throw new IllegalStateException("Could not find project root"); } -} +} \ No newline at end of file From e67f9125717743b50e87f3d1ec9f207cc52bece2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:19:33 +0900 Subject: [PATCH 149/152] test(automation): prove work-conserving prompt red --- .../HourlyOpenCodeProgressPolicyTest.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java index 844e4fda..62a08a3e 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeProgressPolicyTest.java @@ -120,6 +120,51 @@ void executesTheBestFeasibleActionAndContinuesAfterExternalOnlyBlockers() { )); } + /** Requires every completed or deferred action to return to a live work-conserving queue. */ + @Test + void treatsEveryActionAsIntermediateAndReturnsToTheExecutableQueue() { + assertTrue(workflow.contains( + "Completing an action is intermediate state, not an invocation endpoint." + )); + assertTrue(workflow.contains( + "After every remediation, commit, documentation update, test result, deferred " + + "blocker, or completed slice, return to the highest-value safe executable " + + "queue." + )); + assertTrue(workflow.contains( + "The one remote publication candidate limit constrains mutation output, not further " + + "read-only diagnosis, testing, or documentation analysis after a candidate " + + "is prepared." + )); + assertTrue(workflow.contains( + "Queued checks, reviews, and provider waits are local deferred items, not reasons to " + + "idle." + )); + assertTrue(workflow.contains( + "Same-branch writer movement freezes only that branch; continue safe work on other " + + "non-overlapping branches or read-only lanes." + )); + } + + /** Requires two clean fresh exit sweeps before finite-run termination. */ + @Test + void requiresDoubleFreshExitSweepBeforeTermination() { + assertTrue(workflow.contains( + "Before terminating, perform a fresh whole-repository sweep of pull requests, " + + "issues, checks, reviews, security, stack ancestry, documentation, release " + + "readiness, and product gaps." + )); + assertTrue(workflow.contains( + "If that sweep finds any safe executable item, execute the highest-value item and " + + "restart the exit sweep count." + )); + assertTrue(workflow.contains( + "Terminate only on genuine finite run-budget exhaustion or after a second " + + "consecutive fresh sweep proves no safe executable action remains." + )); + assertTrue(workflow.contains("Routine status narration is not work.")); + } + /** Keeps every separately leased repository outside this scheduler's write authority. */ @Test void preservesReadOnlyDependencyLeasesWhileContinuingLocalWork() { @@ -168,4 +213,4 @@ private static Path projectRoot() { } throw new IllegalStateException("Could not find project root"); } -} +} \ No newline at end of file From 3641d6070021d0eeb56907e0fbcdd839bc1534b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:22:09 +0900 Subject: [PATCH 150/152] fix(security): include indented Docker FROM instructions --- .../xtrmetl/etl/documentation/ContainerImagePinningTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java index a52c09c5..bd33262a 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/ContainerImagePinningTest.java @@ -27,7 +27,7 @@ class ContainerImagePinningTest { private static final Pattern FROM_INSTRUCTION = Pattern.compile( - "(?im)^FROM\\s+(?:--platform=\\S+\\s+)?(?\\S+)" + "(?im)^[\\t ]*FROM\\s+(?:--platform=\\S+\\s+)?(?\\S+)" + "(?:\\s+AS\\s+(?[A-Za-z0-9._-]+))?\\s*$" ); private static final Pattern SHA256_PIN = Pattern.compile( From 69b39f5e26a32d99667bc0108a50de94d7bb4c0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:53:40 +0900 Subject: [PATCH 151/152] fix(automation): complete work-conserving runtime contract --- .github/workflows/hourly-opencode-maintenance.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/hourly-opencode-maintenance.yml b/.github/workflows/hourly-opencode-maintenance.yml index a02e98c3..1fe99098 100644 --- a/.github/workflows/hourly-opencode-maintenance.yml +++ b/.github/workflows/hourly-opencode-maintenance.yml @@ -135,6 +135,10 @@ jobs: Execute the highest-impact safe option that is executable in this run, then rerun the exact failing test or gate and verify whether the condition changed. If the preferred option requires an external actor or is infeasible, keep that gate fail-closed and immediately choose the next safe feasible non-overlapping remediation or independent bounded product slice instead of stopping. Never claim resolution from a proposal, retry, aggregate-green result, or predecessor evidence; require exact-head or literal-source evidence that the failing condition is gone. A pull request with only external blockers is not source-actionable. + Completing an action is intermediate state, not an invocation endpoint. After every remediation, commit, documentation update, test result, deferred blocker, or completed slice, return to the highest-value safe executable queue. The one remote publication candidate limit constrains mutation output, not further read-only diagnosis, testing, or documentation analysis after a candidate is prepared. Queued checks, reviews, and provider waits are local deferred items, not reasons to idle. Same-branch writer movement freezes only that branch; continue safe work on other non-overlapping branches or read-only lanes. + + Before terminating, perform a fresh whole-repository sweep of pull requests, issues, checks, reviews, security, stack ancestry, documentation, release readiness, and product gaps. If that sweep finds any safe executable item, execute the highest-value item and restart the exit sweep count. Terminate only on genuine finite run-budget exhaustion or after a second consecutive fresh sweep proves no safe executable action remains. Routine status narration is not work. + When one source-actionable dependency-eligible development pull request exists, fetch and check out that same-repository head branch, implement valid fixes test-first, rerun relevant tests, update authoritative documentation and CHANGELOG.md, and commit only to that local branch. Do not publish the branch remotely; the isolated deterministic branch publisher owns that authority. Do not create another branch for the same work. When no open pull request is source-actionable, whether or not blocked pull requests remain open, inspect open issues, roadmap, architecture, security, privacy, reliability, observability, accessibility, packaging, interoperability, deployment, data governance, operational workflows, release evidence, and buyer-visible gaps. Select exactly one highest-impact bounded vertical slice that is independent of every blocked or invalid stack. Start from the unchanged protected develop head, use exactly one automation/opencode-YYYYMMDDTHHMMSSZ-short-slug local branch, and commit the bounded slice there. Do not publish the branch remotely or create a second branch or pull request. Prefer durable ETL execution, idempotency, replay, dead-letter handling, schema contracts, connector reliability, target warehouse support, tenancy, auditability, SLO evidence, and operator controls. @@ -848,6 +852,8 @@ jobs: required_workflow_names='["CI","Dependency Review","SBOM (CycloneDX)","SAST Semgrep","Security Scan"]' all_runs_file="${RUNNER_TEMP}/pr-${number}-all-exact-head-runs.json" runs_file="${RUNNER_TEMP}/pr-${number}-associated-exact-head-runs.json" + approved_run_ids_file="${RUNNER_TEMP}/pr-${number}-approved-run-ids.txt" + : > "${approved_run_ids_file}" live="$(gh api "/repos/${repository}/pulls/${number}")" jq -e \ @@ -901,7 +907,11 @@ jobs: while IFS= read -r run_id; do [[ -n "${run_id}" ]] || continue + if grep -Fxq "${run_id}" "${approved_run_ids_file}"; then + continue + fi gh api --method POST "/repos/${repository}/actions/runs/${run_id}/approve" + printf '%s\n' "${run_id}" >> "${approved_run_ids_file}" done < <( jq -r ' .[] From d22e7d2094dddaf70d5bb51f80fc02392d310625 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:04:43 +0900 Subject: [PATCH 152/152] test(automation): pin current review contracts --- ...rlyOpenCodeAuthorityDocumentationTest.java | 87 ++++++++++++++++--- .../HourlyOpenCodeCandidateSelectionTest.java | 21 ++++- ...CodeRequiredWorkflowAuthorizationTest.java | 64 ++++++++++++-- 3 files changed, 153 insertions(+), 19 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java index 42a7ea06..a30753d3 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeAuthorityDocumentationTest.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -22,34 +23,56 @@ class HourlyOpenCodeAuthorityDocumentationTest { private static String workflow; private static String doctoring; + private static String design; + private static String plan; + private static String changelog; /** - * Reads the executable workflow and its authoritative security evidence using canonical - * whitespace so prose wrapping cannot change the contract. + * Reads the executable workflow as raw normalized YAML and the prose sources using canonical + * whitespace so job-scoped permission checks retain YAML boundaries while prose wrapping does + * not change the documentation contract. * - * @throws IOException when either checked repository document cannot be read + * @throws IOException when a checked repository document cannot be read */ @BeforeAll static void readAuthoritySources() throws IOException { Path root = projectRoot(); - workflow = canonicalWhitespace(Files.readString( + workflow = Files.readString( root.resolve(".github/workflows/hourly-opencode-maintenance.yml"), StandardCharsets.UTF_8 - )); + ).replace("\r\n", "\n"); doctoring = canonicalWhitespace(Files.readString( root.resolve("docs/doctoring/github-token-exact-head-check-authorization-evidence.md"), StandardCharsets.UTF_8 )); + design = canonicalWhitespace(Files.readString( + root.resolve("docs/superpowers/specs/2026-08-04-hourly-opencode-maintenance-design.md"), + StandardCharsets.UTF_8 + )); + plan = canonicalWhitespace(Files.readString( + root.resolve("docs/superpowers/plans/2026-08-04-hourly-opencode-maintenance-plan.md"), + StandardCharsets.UTF_8 + )); + changelog = canonicalWhitespace(Files.readString( + root.resolve("CHANGELOG.md"), + StandardCharsets.UTF_8 + )); } /** Requires doctoring to state that the model job has no repository write permission. */ @Test void documentsModelJobAsReadOnlyGitHubAuthority() { - assertTrue(workflow.contains("maintain-repository:")); - assertTrue(workflow.contains("actions: read")); - assertTrue(workflow.contains("contents: read")); - assertTrue(workflow.contains("issues: read")); - assertTrue(workflow.contains("pull-requests: read")); + String maintenanceJob = between( + workflow, + " maintain-repository:", + " publish-agent-branch:" + ); + assertTrue(maintenanceJob.contains("actions: read")); + assertTrue(maintenanceJob.contains("contents: read")); + assertTrue(maintenanceJob.contains("issues: read")); + assertTrue(maintenanceJob.contains("pull-requests: read")); + assertFalse(maintenanceJob.contains("contents: write")); + assertFalse(maintenanceJob.contains("issues: write")); assertTrue(doctoring.contains( "`maintain-repository` is the only job that checks out source or runs OpenCode. " @@ -82,8 +105,50 @@ void documentsSeparatedBranchPullRequestAndActionsWriters() { )); } + /** Requires the design and implementation plan to describe the live four-job topology. */ + @Test + void documentsTheCurrentFourJobTopologyAndCredentialLifecycle() { + assertTrue(design.contains("Use four physically separated GitHub Actions jobs")); + assertTrue(design.contains("`maintain-repository` creates local commits only")); + assertTrue(design.contains("The model job uses an ephemeral `GIT_ASKPASS` script")); + assertTrue(design.contains( + "`publish-agent-branch` uses the repository-local `!gh auth git-credential` helper" + )); + + assertTrue(plan.contains( + "Four jobs separate model execution, deterministic branch publication, " + + "deterministic draft-PR publication, and exact-head workflow-run authorization" + )); + assertTrue(plan.contains("Set model-job permissions to read-only GitHub authority")); + assertTrue(plan.contains("Create local commits only; do not push from the model job")); + assertTrue(plan.contains("## Task 4 — Publish the exact branch in an isolated job")); + } + + /** Requires durable release notes to avoid time-sensitive endpoint-pricing claims. */ + @Test + void avoidsTimeSensitiveFreeEndpointClaimsInTheChangelog() { + assertFalse(changelog.contains("current free NVIDIA")); + assertTrue(changelog.contains("NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint")); + } + + /** + * Extracts a required raw-text section between ordered markers. + * + * @param text complete source text + * @param startMarker inclusive section marker + * @param endMarker exclusive section marker + * @return required section text + */ + private static String between(String text, String startMarker, String endMarker) { + int start = text.indexOf(startMarker); + assertTrue(start >= 0, () -> "Missing start marker: " + startMarker); + int end = text.indexOf(endMarker, start + startMarker.length()); + assertTrue(end > start, () -> "Missing end marker after start: " + endMarker); + return text.substring(start, end); + } + /** - * Collapses semantically irrelevant whitespace for stable Markdown and YAML prose assertions. + * Collapses semantically irrelevant whitespace for stable Markdown prose assertions. * * @param value repository text to normalize * @return one-space canonical representation diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java index 0f3d02c8..25844946 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeCandidateSelectionTest.java @@ -37,6 +37,25 @@ void capturesBranchNameBeforeExistingReferenceLookup() throws IOException { assertFalse(workflow.contains("index(.name)")); } + /** + * Requires unexpected remote publication movement to use a diagnostic that is accurate for + * either one or many remote candidates. + * + * @throws IOException when the workflow cannot be read + */ + @Test + void reportsAnyUnexpectedRemotePublicationCandidateAccurately() throws IOException { + String workflow = workflowText(); + + assertTrue(workflow.contains( + "Remote publication candidates changed during the model job; " + + "refusing to race another writer" + )); + assertFalse(workflow.contains( + "Multiple agent publication candidates were detected remotely" + )); + } + /** * Requires policy-file rejection at both deterministic publication boundaries. * @@ -139,8 +158,8 @@ private static String workflowText() throws IOException { */ private static String between(String text, String startMarker, String endMarker) { int start = text.indexOf(startMarker); - int end = text.indexOf(endMarker, start + startMarker.length()); assertTrue(start >= 0, () -> "Missing start marker: " + startMarker); + int end = text.indexOf(endMarker, start + startMarker.length()); assertTrue(end > start, () -> "Missing end marker after start: " + endMarker); return text.substring(start, end); } diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java index 58eb13c8..3e38d1aa 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/HourlyOpenCodeRequiredWorkflowAuthorizationTest.java @@ -21,7 +21,7 @@ class HourlyOpenCodeRequiredWorkflowAuthorizationTest { /** - * Requires the authorization loop to wait for, associate, and account for every required + * Requires the authorization loop to wait for, structurally associate, and account for every * workflow. The polling count and terminal diagnostic text are intentionally pinned because * they are part of the bounded authorization and operator-diagnostic contract. * @@ -30,6 +30,16 @@ class HourlyOpenCodeRequiredWorkflowAuthorizationTest { @Test void waitsForEveryRequiredExactHeadWorkflow() throws IOException { String workflow = workflow(); + String associatedRunSelection = between( + workflow, + " jq -c \\\n --arg expected_head", + " ' \"${all_runs_file}\" > \"${runs_file}\"" + ); + String approvalLoop = between( + workflow, + " while IFS= read -r run_id; do", + " observed_workflow_names=" + ); assertTrue(workflow.contains( "required_workflow_names='[\"CI\",\"Dependency Review\"," @@ -38,11 +48,21 @@ void waitsForEveryRequiredExactHeadWorkflow() throws IOException { assertTrue(workflow.contains("observed_workflow_names")); assertTrue(workflow.contains("missing_workflow_names")); assertTrue(workflow.contains("for _ in $(seq 1 18); do")); - assertTrue(workflow.contains(".head_sha == $expected_head")); - assertTrue(workflow.contains( - "any(.pull_requests[]?; .number == $pull_request_number)" - )); - assertTrue(workflow.contains("/actions/runs/${run_id}/approve")); + assertTrue( + associatedRunSelection.contains("select(.head_sha == $expected_head)"), + "The associated-run selection must bind the exact expected head" + ); + assertTrue( + associatedRunSelection.contains( + "select(any(.pull_requests[]?; .number == $pull_request_number))" + ), + "The same associated-run selection must bind the exact pull-request number" + ); + assertAppearsBefore( + approvalLoop, + "gh api --method POST \"/repos/${repository}/actions/runs/${run_id}/approve\"", + "printf '%s\\n' \"${run_id}\" >> \"${approved_run_ids_file}\"" + ); assertTrue(workflow.contains("jq 'length' <<<\"${missing_workflow_names}\"")); assertTrue(workflow.contains("Missing required exact-head workflows for PR")); assertTrue(workflow.contains("Authorized exact-head pull-request checks for PR")); @@ -82,6 +102,36 @@ void doesNotApproveTheSameWorkflowRunTwiceAcrossPollingPasses() throws IOExcepti ); } + /** + * Requires one marker to occur before another in the same authorization section. + * + * @param text authorization section being checked + * @param first marker that must execute first + * @param second marker that must execute later + */ + private static void assertAppearsBefore(String text, String first, String second) { + int firstIndex = text.indexOf(first); + int secondIndex = text.indexOf(second); + assertTrue(firstIndex >= 0, () -> "Missing first marker: " + first); + assertTrue(secondIndex > firstIndex, () -> "Marker must appear after first marker: " + second); + } + + /** + * Extracts a required text section between two ordered markers. + * + * @param text complete source text + * @param startMarker inclusive section marker + * @param endMarker exclusive section marker + * @return required section text + */ + private static String between(String text, String startMarker, String endMarker) { + int start = text.indexOf(startMarker); + assertTrue(start >= 0, () -> "Missing start marker: " + startMarker); + int end = text.indexOf(endMarker, start + startMarker.length()); + assertTrue(end > start, () -> "Missing end marker after start: " + endMarker); + return text.substring(start, end); + } + /** * Reads the production workflow with normalized line endings. * @@ -117,4 +167,4 @@ private static Path projectRoot() { } throw new IllegalStateException("Could not find project root"); } -} \ No newline at end of file +}