From 2e074c4adc35f1b7db38550d8c3e8f7095fc3332 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Wed, 2 Sep 2026 16:26:27 -0700 Subject: [PATCH 1/2] fix: keep Cast validator inside sandbox workspace Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-cast-validator.test.ts | 16 ++++++++++++---- test/gh-aw-quality.test.ts | 13 +++++++++++-- workflows/squad.md | 27 ++++++++++++++------------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/test/gh-aw-cast-validator.test.ts b/test/gh-aw-cast-validator.test.ts index 63f41ddb9..228f886de 100644 --- a/test/gh-aw-cast-validator.test.ts +++ b/test/gh-aw-cast-validator.test.ts @@ -120,7 +120,8 @@ Architecture, Implementation, Quality function createFixture(): { root: string; payload: string; runnerTemp: string } { const root = mkdtempSync(join(tmpdir(), 'gh-aw-cast-validator-')); workspaces.push(root); - const runnerTemp = join(root, 'runner-temp'); + const runnerTemp = mkdtempSync(join(tmpdir(), 'gh-aw-cast-runner-temp-')); + workspaces.push(runnerTemp); mkdirSync(runnerTemp, { recursive: true }); write(root, '.squad/team.md', teamMarkdown()); write(root, '.squad/routing.md', routingMarkdown()); @@ -137,7 +138,8 @@ function createFixture(): { root: string; payload: string; runnerTemp: string } } write(root, '.github/agents/squad.agent.md', coordinatorMarkdown()); write(root, 'meet-the-squad.md', '# Meet the Squad\n'); - const payload = join(runnerTemp, 'squad-cast-payload.json'); + const payload = join(root, '.github', 'workflows', 'squad-cast-payload.json'); + mkdirSync(dirname(payload), { recursive: true }); writeFileSync(payload, JSON.stringify(corePayload), 'utf8'); return { root, payload, runnerTemp }; } @@ -211,7 +213,7 @@ function materializeRunner( fixture: ReturnType, source = validatorRunnerSource(), ): string { - const runner = join(fixture.runnerTemp, 'run-squad-cast-validator'); + const runner = join(fixture.root, '.github', 'workflows', 'run-squad-cast-validator'); writeFileSync(runner, source, 'utf8'); chmodSync(runner, 0o500); return runner; @@ -307,7 +309,9 @@ describe('GH-AW Cast final-tree validator', () => { const command = validatorCommand(); const runner = validatorRunnerSource(); const workflow = readFileSync(workflowPath, 'utf8'); - expect(command.trim()).toBe('"${RUNNER_TEMP:?}/run-squad-cast-validator"'); + expect(command.trim()).toBe( + '"${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-cast-validator"', + ); expect(command).not.toMatch(/[A-Za-z0-9+/]{256}/); expect(command).not.toMatch(/cat\s+<<|base64|gzip|awk|validator_expected_sha256/); expect(command).not.toContain('H4sI'); @@ -318,6 +322,7 @@ describe('GH-AW Cast final-tree validator', () => { expect(runner).toContain( 'validator_script="${GITHUB_WORKSPACE:?}/.github/workflows/shared/squad-cast-validator.mjs"', ); + expect(runner).not.toContain('RUNNER_TEMP'); expect(runner.indexOf('validator_expected_sha256=')).toBeLessThan( runner.indexOf('node --check "$validator_script"'), ); @@ -335,8 +340,11 @@ describe('GH-AW Cast final-tree validator', () => { expect(result.status, result.stderr).toBe(0); expect(result.stdout).toBe('Cast validation passed.\n'); expect(authorizesPullRequest(result)).toBe(true); + expect(fixture.payload.startsWith(fixture.root)).toBe(true); + expect(fixture.payload.startsWith(fixture.runnerTemp)).toBe(false); // Never copied or extracted into $RUNNER_TEMP -- executed in place from $GITHUB_WORKSPACE. expect(existsSync(join(fixture.runnerTemp, 'validate-gh-aw-cast.mjs'))).toBe(false); + expect(existsSync(join(fixture.runnerTemp, 'run-squad-cast-validator'))).toBe(false); }); it('fails clearly when the installed validator resource is missing', () => { diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index c39a8429e..86316c9ee 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1479,6 +1479,13 @@ describe('gh-aw: compiled workflow shell input security contract', () => { )?.[0] ?? ''; const normalizedRunnerStep = runnerStep.replace(/\\"/g, '"'); expect(normalizedRunnerStep).toContain('cat > "$validator_runner"'); + expect(normalizedRunnerStep).toContain( + 'validator_runner="${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-cast-validator"', + ); + expect(normalizedRunnerStep).toContain( + '--payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-payload.json"', + ); + expect(normalizedRunnerStep).not.toContain('RUNNER_TEMP'); expect(normalizedRunnerStep).toContain('validator_expected_sha256="82aa5620d81e26513658fbde210b0f8d2ac3bc7572e672b421aaa17a2832e8cc"'); expect(normalizedRunnerStep).toContain("outcome: 'cast_failure'"); expect(normalizedRunnerStep).toContain('chmod 500 "$validator_runner"'); @@ -2670,7 +2677,9 @@ describe('gh-aw: Cast replaces disposable bootstrap state (#1909)', () => { expect(candidateCast).toMatch( /only exit status zero with stdout exactly\s+`Cast validation passed\.` authorizes exactly one `create-pull-request`/s, ); - expect(candidateCast).toContain('"${RUNNER_TEMP:?}/run-squad-cast-validator"'); + expect(candidateCast).toContain( + '"${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-cast-validator"', + ); expect(candidateCast).toMatch(/Do not transcribe validator bytes, and do not invoke or\s+load/); expect(candidateCast).toContain('emits one JSON\nrecord on stdout'); expect(candidateCast.match( @@ -2758,7 +2767,7 @@ describe('gh-aw: Cast replaces disposable bootstrap state (#1909)', () => { it('enforces mutually exclusive factual Cast terminal outcomes', () => { expect(cast).toContain('Deterministic final-tree validation'); - expect(cast).toContain('$RUNNER_TEMP/squad-cast-payload.json'); + expect(cast).toContain('$GITHUB_WORKSPACE/.github/workflows/squad-cast-payload.json'); expect(cast).toContain('squad-cast-validator'); assertTruthfulCastTerminalContract(squadContent); }); diff --git a/workflows/squad.md b/workflows/squad.md index a3998dc02..7338d84e8 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -55,16 +55,16 @@ steps: shell: bash run: | set -euo pipefail - validator_runner="${RUNNER_TEMP:?}/run-squad-cast-validator" + validator_runner="${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-cast-validator" cat > "$validator_runner" <<'SQUAD_CAST_VALIDATOR_RUNNER' #!/usr/bin/env bash set -u -o pipefail cd "${GITHUB_WORKSPACE:?}" - stderr_file="${RUNNER_TEMP:?}/squad-cast-validator.stderr" + stderr_file="${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-validator.stderr" validator_script="${GITHUB_WORKSPACE:?}/.github/workflows/shared/squad-cast-validator.mjs" - validator_output="${RUNNER_TEMP:?}/squad-cast-validator.stdout" - expected_output="${RUNNER_TEMP:?}/squad-cast-validator.expected" + validator_output="${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-validator.stdout" + expected_output="${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-validator.expected" fail_cast() { local stage="$1" @@ -122,7 +122,7 @@ steps: : > "$stderr_file" node "$validator_script" \ --root "$PWD" \ - --payload "${RUNNER_TEMP:?}/squad-cast-payload.json" \ + --payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-payload.json" \ > "$validator_output" 2> "$stderr_file" status=$? if [ "$status" -ne 0 ]; then @@ -1002,17 +1002,18 @@ its charter from scratch. ##### Step 7: Deterministic final-tree validation Natural-language review is not the gate. Immediately before requesting safe -output, create `$RUNNER_TEMP/squad-cast-payload.json` as a JSON array containing -every concrete Step 6 payload path, then invoke the prepared runner with the -exact command below. Do not transcribe validator bytes, and do not invoke or -load `squad-cast-validator` into model context. The runner was prepared before -this turn and deterministically executes the plaintext validator resource that -gh-aw installed at `.github/workflows/shared/squad-cast-validator.mjs` under -`$GITHUB_WORKSPACE`. +output, create +`$GITHUB_WORKSPACE/.github/workflows/squad-cast-payload.json` as a JSON array +containing every concrete Step 6 payload path, then invoke the prepared runner +with the exact command below. Do not transcribe validator bytes, and do not invoke or +load `squad-cast-validator` into model context. The runner was +prepared before this turn under the sandbox-visible `$GITHUB_WORKSPACE` and +deterministically executes the plaintext validator resource that gh-aw installed +at `.github/workflows/shared/squad-cast-validator.mjs`. ```bash -"${RUNNER_TEMP:?}/run-squad-cast-validator" +"${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-cast-validator" ``` From 15e773f809773871eba7b37e02cfc79aaa678760 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Wed, 2 Sep 2026 16:35:37 -0700 Subject: [PATCH 2/2] test: remove redundant Cast fixture setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-cast-validator.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/gh-aw-cast-validator.test.ts b/test/gh-aw-cast-validator.test.ts index 228f886de..7ddb44ca4 100644 --- a/test/gh-aw-cast-validator.test.ts +++ b/test/gh-aw-cast-validator.test.ts @@ -122,7 +122,6 @@ function createFixture(): { root: string; payload: string; runnerTemp: string } workspaces.push(root); const runnerTemp = mkdtempSync(join(tmpdir(), 'gh-aw-cast-runner-temp-')); workspaces.push(runnerTemp); - mkdirSync(runnerTemp, { recursive: true }); write(root, '.squad/team.md', teamMarkdown()); write(root, '.squad/routing.md', routingMarkdown()); write(root, '.squad/casting/registry.json', JSON.stringify({