From d0ad4e1bf31793d920791ab19dec362e46bc3604 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:51:46 +0800 Subject: [PATCH 1/5] fix(ci): harden ingestion autopilot secrets --- .github/workflows/ingestion-autopilot.yml | 8 +++++- tests/ingestion-autopilot-workflow.test.ts | 29 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/ingestion-autopilot-workflow.test.ts diff --git a/.github/workflows/ingestion-autopilot.yml b/.github/workflows/ingestion-autopilot.yml index 6715da82e6..b4474b00d6 100644 --- a/.github/workflows/ingestion-autopilot.yml +++ b/.github/workflows/ingestion-autopilot.yml @@ -33,19 +33,24 @@ env: SUPABASE_PROJECT_REF: sjrfecxgysukkwxsowpy SUPABASE_PROJECT_NAME: Clinical KB Database NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: placeholder-ci-anon-key - SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} jobs: ingestion-autopilot: + if: ${{ github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch }} runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 with: + # Manual dispatch can select a same-repository branch; always execute the + # trusted default-branch code before exposing production credentials. + ref: ${{ github.event.repository.default_branch }} persist-credentials: false - name: Preflight required secrets + env: + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} run: | if [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then echo "::error::Ingestion autopilot cannot run — missing repo secret SUPABASE_SERVICE_ROLE_KEY" @@ -68,6 +73,7 @@ jobs: - name: Run autopilot id: autopilot env: + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} # Apply only when BOTH the dispatch input and the repo variable allow it. APPLY_REQUESTED: ${{ github.event.inputs.apply }} APPLY_ALLOWED: ${{ vars.INGESTION_AUTOPILOT_APPLY }} diff --git a/tests/ingestion-autopilot-workflow.test.ts b/tests/ingestion-autopilot-workflow.test.ts new file mode 100644 index 0000000000..803c274897 --- /dev/null +++ b/tests/ingestion-autopilot-workflow.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const workflow = readFileSync( + join(process.cwd(), ".github", "workflows", "ingestion-autopilot.yml"), + "utf8", +).replace(/\r\n/g, "\n"); + +describe("ingestion autopilot workflow secret handling", () => { + it("does not expose the Supabase service-role key to checkout or install steps", () => { + expect(workflow).not.toMatch(/env:\n(?: [^\n]+\n)* SUPABASE_SERVICE_ROLE_KEY:/); + expect(workflow).toContain( + "if: ${{ github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch }}", + ); + expect(workflow).toContain("ref: ${{ github.event.repository.default_branch }}"); + + const installIndex = workflow.indexOf(" - name: Install dependencies"); + const autopilotIndex = workflow.indexOf(" - name: Run autopilot"); + expect(installIndex).toBeGreaterThan(-1); + expect(autopilotIndex).toBeGreaterThan(installIndex); + + const installBlock = workflow.slice(installIndex, autopilotIndex); + expect(installBlock).not.toContain("SUPABASE_SERVICE_ROLE_KEY"); + + const autopilotBlock = workflow.slice(autopilotIndex); + expect(autopilotBlock).toContain("SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}"); + }); +}); From 6cc0b6ffdae3b515a05a17f2a87e5f51e5347263 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 09:19:01 +0000 Subject: [PATCH 2/5] fix(ci): register autopilot workflow secret contract Add the new workflow-reading regression suite to test:ci-workflows so Static PR checks pass, tighten the secret-scoping assertions, and apply Prettier so autofix.ci does not need to rewrite the file. --- package.json | 2 +- tests/ingestion-autopilot-workflow.test.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4083af3737..0b9e0b3020 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "test:coverage": "node scripts/run-vitest.mjs run --coverage", "test:coverage:node": "node scripts/run-vitest.mjs run --project=node --coverage", "test:coverage:ui": "node scripts/run-vitest.mjs run --project=jsdom --coverage", - "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/eval-canary-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/check-lighthouse-budget.test.ts tests/offline-release-profile.test.ts", + "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/eval-canary-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/offline-release-profile.test.ts", "test:e2e": "node scripts/run-playwright.mjs", "test:e2e:all": "node scripts/run-playwright.mjs", "test:e2e:accessibility": "node scripts/run-playwright.mjs tests/ui-accessibility.spec.ts --project=chromium", diff --git a/tests/ingestion-autopilot-workflow.test.ts b/tests/ingestion-autopilot-workflow.test.ts index 803c274897..a27155f81d 100644 --- a/tests/ingestion-autopilot-workflow.test.ts +++ b/tests/ingestion-autopilot-workflow.test.ts @@ -1,20 +1,30 @@ -import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -const workflow = readFileSync( - join(process.cwd(), ".github", "workflows", "ingestion-autopilot.yml"), - "utf8", -).replace(/\r\n/g, "\n"); +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync(join(process.cwd(), ".github", "workflows", "ingestion-autopilot.yml"), "utf8").replace( + /\r\n/g, + "\n", +); describe("ingestion autopilot workflow secret handling", () => { it("does not expose the Supabase service-role key to checkout or install steps", () => { - expect(workflow).not.toMatch(/env:\n(?: [^\n]+\n)* SUPABASE_SERVICE_ROLE_KEY:/); + // Workflow-level and job-level env both use fewer than 10 spaces; only + // step-scoped env under Preflight / Run autopilot should inject the secret. + expect(workflow).not.toMatch(/^(?: {0,6})env:\n(?: {2,8}[^\n]+\n)* {2,8}SUPABASE_SERVICE_ROLE_KEY:/m); + expect(workflow).toContain( "if: ${{ github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch }}", ); expect(workflow).toContain("ref: ${{ github.event.repository.default_branch }}"); + const secretLines = workflow.split("\n").filter((line) => line.includes("SUPABASE_SERVICE_ROLE_KEY:")); + expect(secretLines).toEqual([ + " SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}", + " SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}", + ]); + const installIndex = workflow.indexOf(" - name: Install dependencies"); const autopilotIndex = workflow.indexOf(" - name: Run autopilot"); expect(installIndex).toBeGreaterThan(-1); From ace81d84942c8d98c396e462ab5ffb4b7b29c68b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 09:19:07 +0000 Subject: [PATCH 3/5] docs: record PR #1572 ingestion-autopilot secret review Capture the reviewed HEAD, CI fixes, and verification after syncing the draft PR with main and registering the workflow secret contract. --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7bd29b4a07..4d9815eefc 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -588,3 +588,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-02 | claude/ds-v2-values | d33721b436c05014e8ef1f362a945fe9574e676e | DS V2 PR-V values | Wave 2 self-verified before push: 5a close-out (no value deltas remain after decision J2; contract metrics unchanged at rawColorLiterals 2 / literalShadowClasses 0 / legacyTapClasses 0), 5b tap knob 2.75rem to 3rem in @theme with --tap-min reduced to var(--spacing-tap) across 426 *-tap sites and three pins flipped in-commit, 5c --radius-md 8px to 10px across 243 rounded-md sites with the 4px-grid pin naming both half-steps and 14 arbitrary radius literals absorbed. Two carve-outs recorded in SPEC 4.10/4.6: the phone composer stays 44px below 431px, and the 2px mode-nav hairline stays off the ladder. An offline.html tap bump was reverted before push because it would have forced a service-worker CACHE_VERSION bump. Known-red local gate: tests/ui-smoke.spec.ts:2221 phone runway pin fails on unmodified origin/main here too (maxOffset 99 at base vs 111 at this head, reproduced in a pristine capture worktree) while CI run 30720360137 is green on the same tree - plan finding L, CI binding. | typecheck exit 0; verify:pr-local PASS with 4880 passed 3 skipped (4883) in 471 files, production build compiled, client-bundle secret scan passed, eval:rag:offline fixture line plus 574/574 (better than the J4 known-red baseline); verify:ui 346 passed 1 failed (the known-red phone runway pin); verify:phone-chrome --full=never contracts 116/116 and ui-tools 87/87 with the same single known-red test; test:e2e:critical 15/15; design-system contract, branch-review-ledger, outstanding-issues, sitemap, docs link/index/inventory/scripts all exit 0 | | 2026-08-02 | claude/ds-v2-values | 48012d359b84daae347201697c82f3e433c182c5 | PR #1571 review-and-fix (ds-v2-values tap/radius) | fixed: Tools submit + account-setup close onto h-tap; phone composer input 44px pin; gate 2 demoted to implemented-partial; SPEC 407→426; short-runway/short-answer smoke pins retuned; verify:cheap + verify:pr-local + 2 smoke tests green; CI Production UI in progress | verify:cheap 471/4879; verify:pr-local build+fixtures; test:e2e 2 phone smoke passed; design-system-contract raw 2/0/0 | | 2026-08-02 | claude/ds-v2-values | d2a29198415292a28ed1dbf1c740c94f6c12ddb3 | PR #1571 review-and-fix (ds-v2-values tap/radius) | fixed+clarified: gate2 objective acknowledges h-10/unwired audit not blocked; overlay 430px assertion scoped to media block; prior tip 48012d35 Production UI+PR required green | verify:cheap; verify:pr-local; 2 smoke; UI overlay contract; CI Production UI (1)(2)(3)+PR required SUCCESS on 48012d35 | +| 2026-08-02 | codex/fix-manual-workflow-service-role-key-exposure / PR #1572 | 6cc0b6ffdae3b515a05a17f2a87e5f51e5347263 | PR review + fix (ingestion-autopilot service-role exposure) | Reviewed secret-hardening: workflow/job-level SUPABASE_SERVICE_ROLE_KEY removed, manual dispatch limited to default branch, checkout pinned to default_branch, secret scoped to Preflight + Run autopilot only. Fixed CI blockers: merged origin/main (branch was ~1144 behind; Gitleaks needed run-gitleaks-pinned.mjs), registered tests/ingestion-autopilot-workflow.test.ts in test:ci-workflows, Prettier + stronger step-only secret assertions. No P0/P1 residual in the hardened workflow; residual risk is intentional inability to dry-run workflow_dispatch from non-default branches until merge. | npm run test:ci-workflows 206/206; focused ci-cache-safety + ingestion-autopilot-workflow 21/21; check:github-actions; prettier --check; git diff --check. No OpenAI/Supabase/provider calls. | From fcefa764e5a4625e30955c3f536a87db834715be Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:54:11 +0800 Subject: [PATCH 4/5] fix(ci): trust default-branch ingestion dispatch --- .github/workflows/ingestion-autopilot.yml | 26 +++++++++------------- tests/ingestion-autopilot-workflow.test.ts | 8 +++---- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ingestion-autopilot.yml b/.github/workflows/ingestion-autopilot.yml index 488e4dd49b..cd1ff5c576 100644 --- a/.github/workflows/ingestion-autopilot.yml +++ b/.github/workflows/ingestion-autopilot.yml @@ -4,20 +4,20 @@ # or an explicitly requested recovery fails. # # ENABLED IN DRY-RUN: the 6-hourly schedule is active, but recovery only APPLIES when -# BOTH the dispatch input `apply=true` AND repo variable INGESTION_AUTOPILOT_APPLY == +# BOTH the dispatch payload `apply=true` AND repo variable INGESTION_AUTOPILOT_APPLY == # "true". The variable is intentionally unset, so scheduled runs are read-only probes # that alert on detected stuck/stranded work, unreachable Supabase, or (with apply) # failed recovery. To allow real recovery after reviewing a dry-run, set -# INGESTION_AUTOPILOT_APPLY=true and dispatch with apply=true. +# INGESTION_AUTOPILOT_APPLY=true and send an `ingestion-autopilot` +# repository dispatch with `client_payload.apply=true`. name: Ingestion Autopilot on: - workflow_dispatch: - inputs: - apply: - description: "Apply recovery (requires repo var INGESTION_AUTOPILOT_APPLY=true)" - required: false - default: "false" + # repository_dispatch always loads this workflow from the trusted default + # branch. Do not replace it with workflow_dispatch: manual branch selection + # would let same-repository writers alter a secret-bearing workflow definition. + repository_dispatch: + types: [ingestion-autopilot] schedule: # Every 6 hours. - cron: "0 */6 * * *" @@ -38,16 +38,12 @@ env: jobs: ingestion-autopilot: - if: ${{ github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch }} runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # Manual dispatch can select a same-repository branch; always execute the - # trusted default-branch code before exposing production credentials. - ref: ${{ github.event.repository.default_branch }} persist-credentials: false - name: Preflight required secrets @@ -76,8 +72,8 @@ jobs: id: autopilot env: SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} - # Apply only when BOTH the dispatch input and the repo variable allow it. - APPLY_REQUESTED: ${{ github.event.inputs.apply }} + # Apply only when BOTH the trusted dispatch payload and repo variable allow it. + APPLY_REQUESTED: ${{ github.event.client_payload.apply || 'false' }} APPLY_ALLOWED: ${{ vars.INGESTION_AUTOPILOT_APPLY }} run: | if [ "$APPLY_REQUESTED" = "true" ] && [ "$APPLY_ALLOWED" = "true" ]; then @@ -87,7 +83,7 @@ jobs: echo "Scheduled dry-run; alert if recoverable work is detected." npm run ingestion:autopilot -- --alert-on-stuck else - echo "Dry-run (set repo var INGESTION_AUTOPILOT_APPLY=true and dispatch apply=true to recover)." + echo "Dry-run (set repo var INGESTION_AUTOPILOT_APPLY=true and dispatch client_payload.apply=true to recover)." npm run ingestion:autopilot fi diff --git a/tests/ingestion-autopilot-workflow.test.ts b/tests/ingestion-autopilot-workflow.test.ts index a27155f81d..0bab164bf8 100644 --- a/tests/ingestion-autopilot-workflow.test.ts +++ b/tests/ingestion-autopilot-workflow.test.ts @@ -14,10 +14,10 @@ describe("ingestion autopilot workflow secret handling", () => { // step-scoped env under Preflight / Run autopilot should inject the secret. expect(workflow).not.toMatch(/^(?: {0,6})env:\n(?: {2,8}[^\n]+\n)* {2,8}SUPABASE_SERVICE_ROLE_KEY:/m); - expect(workflow).toContain( - "if: ${{ github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch }}", - ); - expect(workflow).toContain("ref: ${{ github.event.repository.default_branch }}"); + expect(workflow).not.toMatch(/^ workflow_dispatch:/m); + expect(workflow).toContain("repository_dispatch:"); + expect(workflow).toContain("types: [ingestion-autopilot]"); + expect(workflow).toContain("APPLY_REQUESTED: ${{ github.event.client_payload.apply || 'false' }}"); const secretLines = workflow.split("\n").filter((line) => line.includes("SUPABASE_SERVICE_ROLE_KEY:")); expect(secretLines).toEqual([ From f3b99dc01b85ae38eb2643d1ae534b59305ae23b Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:56:23 +0800 Subject: [PATCH 5/5] test(ci): lock ingestion secret step boundaries --- docs/branch-review-ledger.md | 1 + tests/ingestion-autopilot-workflow.test.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b8994ad699..4e2967bb02 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -603,3 +603,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-02 | codex/mcp-config-hardening-merge | 30aac79e655225f41006d5739fc4e91ef7791514 | MCP Cloud config hardening | Supersedes prior review; synced main and retained parser hardening with Windows Cloud test coverage | check:codex-cloud; Cloud/Python tests 22 passed | | 2026-08-03 | claude/ds-v2-adopt | 407c8e74a240fbc2e0469b5a8334f55afd61e60d | bugbot PR #1595 | findings: P2 empty-sources fallback fixed; no P0/P1; residuals #217/#224 EmptyState heading, clipboard metadata unwired, #216 AnswerCard deferred | npm test 5061 passed; vitest answer surfaces 112 passed; no cursor[bot] Bugbot threads on PR | | 2026-08-03 | claude/ds-v2-adopt | cb898bec11082c533aae5f42fbd415de1ff0f5e4 | review-and-fix PR #1595 | fixed P1 stale Open source nav + P2 clipboard metadata + RadioGroup id; dispositioned #225 favourites triple live; merge-tree clean | vitest 111 passed (answer-copy, cited-href, clipboard-product, ui-v2-components, ui-v2-answer-safety) | +| 2026-08-04 | codex/fix-manual-workflow-service-role-key-exposure / PR #1572 | 6cc0b6ffdae3b515a05a17f2a87e5f51e5347263 | PR review + fix (ingestion-autopilot service-role exposure) (supersedes 2026-08-02) | Supersedes the earlier row to record decisive gate results; historical review outcome otherwise unchanged. | PASS: npm run test:ci-workflows — 206/206 passed; PASS: focused ci-cache-safety + ingestion-autopilot-workflow — 21/21 passed; PASS: npm run check:github-actions — GitHub Actions pin check passed; PASS: prettier --check — all matched files use Prettier code style; PASS: git diff --check — clean. No provider calls. | diff --git a/tests/ingestion-autopilot-workflow.test.ts b/tests/ingestion-autopilot-workflow.test.ts index 0bab164bf8..4722171449 100644 --- a/tests/ingestion-autopilot-workflow.test.ts +++ b/tests/ingestion-autopilot-workflow.test.ts @@ -26,10 +26,21 @@ describe("ingestion autopilot workflow secret handling", () => { ]); const installIndex = workflow.indexOf(" - name: Install dependencies"); + const checkoutIndex = workflow.indexOf(" - name: Checkout"); + const preflightIndex = workflow.indexOf(" - name: Preflight required secrets"); + const setupIndex = workflow.indexOf(" - name: Setup Node.js"); const autopilotIndex = workflow.indexOf(" - name: Run autopilot"); + expect(checkoutIndex).toBeGreaterThan(-1); + expect(preflightIndex).toBeGreaterThan(checkoutIndex); + expect(setupIndex).toBeGreaterThan(preflightIndex); expect(installIndex).toBeGreaterThan(-1); expect(autopilotIndex).toBeGreaterThan(installIndex); + const checkoutBlock = workflow.slice(checkoutIndex, preflightIndex); + const preflightBlock = workflow.slice(preflightIndex, setupIndex); + expect(checkoutBlock).not.toContain("SUPABASE_SERVICE_ROLE_KEY"); + expect(preflightBlock).toContain("SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}"); + const installBlock = workflow.slice(installIndex, autopilotIndex); expect(installBlock).not.toContain("SUPABASE_SERVICE_ROLE_KEY");