diff --git a/.github/workflows/live-validation.yml b/.github/workflows/live-validation.yml index c50e895..9cfecab 100644 --- a/.github/workflows/live-validation.yml +++ b/.github/workflows/live-validation.yml @@ -33,7 +33,6 @@ jobs: permissions: contents: read env: - GH_TOKEN: ${{ github.token }} NO_COLOR: '1' steps: - name: Checkout @@ -55,6 +54,10 @@ jobs: run: npm run build - name: Walk the credential-free fixture chain + # The trusted driver reads a public GitHub issue through gh. Repository + # commands run as separate children and strip GH_TOKEN before execution. + env: + GH_TOKEN: ${{ github.token }} run: node scripts/e2e-validation.mjs no-key - name: Upload sanitized no-key evidence @@ -78,7 +81,6 @@ jobs: permissions: contents: read env: - GH_TOKEN: ${{ github.token }} NO_COLOR: '1' # The image and engine the product's repository runner must use. The proof step # below preloads and measures this same digest; a repository command never runs @@ -204,7 +206,11 @@ jobs: run: npm run check - name: Run the bounded real-model chain + # Only this trusted driver can use gh for GitHub API reads. All install, + # build, audit, deterministic tests and sandbox proofs run token-free. + # Workspace-facing children are given a stripped environment by the driver. env: + GH_TOKEN: ${{ github.token }} BHARATCODE_API_KEY: ${{ secrets.BHARATCODE_API_KEY }} BHARATCODE_MODEL: ${{ inputs.model }} # Below the transport's own ceiling, not above it. Node's fetch gives up at diff --git a/tests/security/live-validation-workflow-policy.test.ts b/tests/security/live-validation-workflow-policy.test.ts index c1fa84d..3c8d288 100644 --- a/tests/security/live-validation-workflow-policy.test.ts +++ b/tests/security/live-validation-workflow-policy.test.ts @@ -50,7 +50,39 @@ describe('the controlled live-validation workflow', () => { expect(WORKFLOW).not.toMatch(/NPM_TOKEN|NODE_AUTH_TOKEN|GITHUB_TOKEN:\s*\$\{\{\s*secrets/); }); - it('uses the ephemeral GitHub token only for MergeSutra-owned reads', () => { + it('never gives a GitHub token to job-wide install, build, test or audit commands', () => { + // Two token references are not enough: the previous policy passed even + // when both were at job scope, exposing them to npm ci and npm run check. + const scopes = [ + { + body: job('no-key-chain', 'live-model'), + allowedStep: 'Walk the credential-free fixture chain', + }, + { + body: job('live-model'), + allowedStep: 'Run the bounded real-model chain', + }, + ]; + + for (const { body, allowedStep } of scopes) { + const stepsStart = body.indexOf(' steps:\n'); + expect(stepsStart, 'the job has no steps boundary').toBeGreaterThanOrEqual(0); + const jobHeader = body.slice(0, stepsStart); + expect(jobHeader, 'GH_TOKEN must not be defined for an entire job').not.toMatch( + /^\s*GH_TOKEN:/m, + ); + + const sections = body.split(/^ {6}- name: /m).slice(1); + const exposed = sections + .filter((section) => /^ {10}GH_TOKEN:/m.test(section)) + .map((section) => section.split('\n')[0]); + expect(exposed, 'only the trusted E2E driver may receive the token').toEqual([allowedStep]); + + const driver = sections.find((section) => section.split('\n')[0] === allowedStep); + expect(driver, 'the driver step is missing').toBeDefined(); + expect(driver).toMatch(/^ {8}env:\n {10}GH_TOKEN: \$\{\{ github\.token \}\}\n/m); + } + expect(WORKFLOW.match(/GH_TOKEN:\s*\$\{\{ github\.token \}\}/g) ?? []).toHaveLength(2); expect(WORKFLOW).not.toMatch(/GITHUB_TOKEN:/); });