Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/squad-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,10 @@ jobs:
test "$(find .github/workflows -maxdepth 1 -name 'squad*.md' | wc -l | tr -d ' ')" = 8
test "$(find .github/workflows/shared -type f ! -name 'squad-bootstrap-trigger-probe.json' | wc -l | tr -d ' ')" = 17
test "$(find .github/skills/gh-aw-enlistment -type f | wc -l | tr -d ' ')" = 1
node --input-type=module -e '
import { compileWithPinnedActions } from "./.github/workflows/shared/squad-install-verifier.mjs";
compileWithPinnedActions(process.cwd());
'
gh aw compile --strict --approve --no-check-update
for workflow in squad squad-implement-worker squad-review squad-deps-worker squad-retro squad-improvement-worker squad-bootstrap squad-command-router; do
gh aw compile "$workflow" --strict --no-check-update
Expand Down
7 changes: 7 additions & 0 deletions docs/src/content/docs/guide/gh-aw.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ validated payload:
- a draft Cast PR on `squad/bootstrap-cast`; and
- `[Research Proposals] Agent-discovered repo opportunities`.

Bootstrap submits the shared payload through one authenticated command. Runtime
code assembles and validates its bounded chunks, then passes JSON directly to
the existing typed safe-output tool; the agent never copies chunks into tool
arguments. Missing, duplicate, reordered, malformed, oversized, or mismatched
payloads fail closed before submission, and the write job independently
revalidates the payload. A submission error is terminal, not permission to retry.

Review and merge the Cast PR, then rerun `/squad triage` on the linked issue to
classify its existing bootstrap proposals. If a proposal needs deeper or newer
evidence, use one of the issue's focused `/squad research ...` commands first;
Expand Down
136 changes: 130 additions & 6 deletions test/gh-aw-bootstrap-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { afterAll, describe, expect, it } from 'vitest';
import {
cpSync,
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
Expand All @@ -12,6 +14,7 @@ import { createHash } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { parse } from 'yaml';
import {
BOOTSTRAP_BRANCH,
BOOTSTRAP_ISSUE_MARKER,
Expand Down Expand Up @@ -375,6 +378,48 @@ function validateFixture(
);
}

function submissionFixture() {
const fixture = createFixture();
const targetBytes = 40_843;
const remaining = targetBytes - Buffer.byteLength(JSON.stringify(fixture.payload));
const rationale = ' Repository evidence informs this proposed specialist; human review is required before activation.';
fixture.payload.pr_body += rationale.repeat(Math.ceil(remaining / rationale.length)).slice(0, remaining);
const text = JSON.stringify(fixture.payload);
expect(Buffer.byteLength(text)).toBe(targetBytes);
writeFileSync(fixture.payloadPath, text);
const envelope = createBootstrapPayloadEnvelope(text) as Record<string, string>;
expect(envelope.payload_chunk_count).toBe('7');
const envelopePath = join(fixture.root, 'envelope.json');
writeFileSync(envelopePath, JSON.stringify(envelope));
const capture = join(fixture.root, 'calls.jsonl');
const proxy = join(fixture.root, 'bin/safeoutputs');
write(fixture.root, 'bin/safeoutputs', `#!${process.execPath}
const fs = require('node:fs');
const args = process.argv.slice(2);
if (JSON.stringify(args) !== JSON.stringify(['materialize_bootstrap', '.'])) process.exit(2);
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
if (Object.values(input).some(value => typeof value !== 'string' || Buffer.byteLength(value) > 10240)) process.exit(3);
fs.appendFileSync(process.env.CAPTURE, JSON.stringify(input) + '\\n');
if (process.env.PROXY_FAIL === '1') { console.error('proxy rejected request'); process.exit(4); }
console.log('tool accepted');
`);
chmodSync(proxy, 0o700);
const env = {
...process.env,
PATH: `${join(fixture.root, 'bin')}:${process.env.PATH}`,
CAPTURE: capture,
GITHUB_WORKSPACE: fixture.root,
GITHUB_REPOSITORY: 'octo/example',
DEFAULT_BRANCH: 'main',
};
const submit = (overrides = {}) => spawnSync(process.execPath, [
VALIDATOR, '--submit-envelope', envelopePath,
'--root', fixture.root, '--payload', fixture.payloadPath,
'--repository', 'octo/example', '--default-branch', 'main', '--link-mode', 'placeholder',
], { env: { ...env, ...overrides }, encoding: 'utf8' });
return { ...fixture, text, envelope, envelopePath, capture, proxy, env, submit };
}

function compileWorkflow(source = WORKFLOW): string {
const root = mkdtempSync(join(tmpdir(), 'gh-aw-bootstrap-compile-'));
workspaces.push(root);
Expand Down Expand Up @@ -601,12 +646,8 @@ describe('automatic Squad bootstrap workflow', () => {
});

it('fails closed for every chunk transport corruption class', () => {
const payloadText = JSON.stringify({
shared: 'Cast and research stay in one validated payload.',
content: 'x'.repeat(PAYLOAD_CHUNK_BYTES * 2),
});
const valid = createBootstrapPayloadEnvelope(payloadText) as Record<string, string>;
expect(Number(valid.payload_chunk_count)).toBeGreaterThanOrEqual(3);
const fixture = submissionFixture();
const valid = fixture.envelope;

const mutations: Array<[string, (envelope: Record<string, string>) => void, RegExp]> = [
['missing', (envelope) => delete envelope.payload_chunk_00, /payload_chunk_00 is missing/],
Expand Down Expand Up @@ -653,6 +694,8 @@ describe('automatic Squad bootstrap workflow', () => {
/decoded length does not match|length mismatch/,
],
['hash mismatch', (envelope) => { envelope.payload_sha256 = '0'.repeat(64); }, /SHA-256 mismatch/],
['noncanonical count', (envelope) => { envelope.payload_chunk_count = '07'; }, /canonical decimal/],
['invalid slot', (envelope) => { envelope.payload_chunk_16 = '16:QQ=='; }, /not a valid fixed chunk slot/],
[
'trailing data',
(envelope) => {
Expand Down Expand Up @@ -680,7 +723,74 @@ describe('automatic Squad bootstrap workflow', () => {
() => reconstructBootstrapPayload(envelope),
`${name} corruption must fail closed`,
).toThrow(error);
writeFileSync(fixture.envelopePath, JSON.stringify(envelope));
const result = fixture.submit();
expect(result.status, name).not.toBe(0);
expect(result.stderr, name).toMatch(error);
expect(existsSync(fixture.capture), `${name} must not invoke safeoutputs`).toBe(false);
}
});

it('submits seven chunks through the authenticated runner without printing or transcribing them', () => {
const fixture = submissionFixture();
cpSync(resolve(ROOT, 'workflows/shared'), join(fixture.root, '.github/workflows/shared'), { recursive: true });
write(fixture.root, '.github/workflows/squad-bootstrap-payload.json', fixture.text);
const config = parse(WORKFLOW.split('---')[1]);
const prepare = config['pre-agent-steps'].find(
(step: { name: string }) => step.name === 'Prepare authenticated bootstrap validator',
);
const setup = spawnSync('bash', ['-c', prepare.run], { env: fixture.env, encoding: 'utf8' });
expect(setup.status, setup.stderr).toBe(0);
const result = spawnSync(
join(fixture.root, '.github/workflows/run-squad-bootstrap-validator'),
[],
{ env: fixture.env, encoding: 'utf8' },
);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe('Squad bootstrap validation passed; materialize_bootstrap submitted.\n');
const calls = readFileSync(fixture.capture, 'utf8').trim().split('\n');
expect(calls).toHaveLength(1);
expect(JSON.parse(calls[0])).toEqual(fixture.envelope);
expect(reconstructBootstrapPayload(JSON.parse(calls[0]))).toBe(fixture.text);
expect(result.stdout.length).toBeLessThan(100);
expect(result.stderr).toBe('');
});

it('rejects schema, repository, Cast, and stale payload mutations before submitting', () => {
for (const mutate of [
(payload: ReturnType<typeof createFixture>['payload']) => { payload.schema_version = '2'; },
(payload: ReturnType<typeof createFixture>['payload']) => { payload.repository = 'other/repo'; },
(payload: ReturnType<typeof createFixture>['payload']) => { payload.default_branch = 'other'; },
(payload: ReturnType<typeof createFixture>['payload']) => { payload.files[0].content += '\nchanged'; },
]) {
const fixture = submissionFixture();
mutate(fixture.payload);
const text = JSON.stringify(fixture.payload);
writeFileSync(fixture.payloadPath, text);
writeFileSync(fixture.envelopePath, JSON.stringify(createBootstrapPayloadEnvelope(text)));
const result = fixture.submit();
expect(result.status).not.toBe(0);
expect(result.stderr).toContain('Squad bootstrap validation failed');
expect(existsSync(fixture.capture)).toBe(false);
}
const fixture = submissionFixture();
writeFileSync(fixture.payloadPath, `${fixture.text}\n`);
expect(fixture.submit().stderr).toContain('does not match the generated payload bytes');
expect(existsSync(fixture.capture)).toBe(false);
});

it('fails explicitly without retries when the mounted tool fails or is unavailable', () => {
const fixture = submissionFixture();
const failed = fixture.submit({ PROXY_FAIL: '1' });
expect(failed.status).not.toBe(0);
expect(failed.stderr).toContain('proxy rejected request');
expect(failed.stderr).toContain('Do not retry');
expect(readFileSync(fixture.capture, 'utf8').trim().split('\n')).toHaveLength(1);
rmSync(fixture.proxy);
const unavailable = fixture.submit({ PATH: join(fixture.root, 'bin') });
expect(unavailable.status).not.toBe(0);
expect(unavailable.stderr).toContain('ENOENT');
expect(unavailable.stdout).toBe('');
});

it('enforces encoder total-size and fixed chunk-count bounds', () => {
Expand Down Expand Up @@ -738,6 +848,10 @@ describe('automatic Squad bootstrap workflow', () => {
expect(lock).toContain('"payload_sha256"');
expect(lock).not.toMatch(/"materialize-bootstrap":\{"inputs":\{"payload":/);
expect(lock).toContain('reconstructBootstrapPayload(items[0])');
expect(lock).toContain('mcp-cli');
expect(lock).toContain('--submit-envelope');
expect(parse(lock).jobs.agent.env.DEFAULT_BRANCH).toBe('${{ github.event.repository.default_branch }}');
expect(parse(WORKFLOW.split('---')[1]).tools['cli-proxy']).toBe(true);
expect(lock).toContain('SQUAD_BOOTSTRAP_INSTALL_SHA: ${{ github.sha }}');
expect(lock).toContain('SQUAD_BOOTSTRAP_REPOSITORY: ${{ github.repository }}');
expect(lock).toContain('SQUAD_BOOTSTRAP_RUN_ID: ${{ github.run_id }}');
Expand All @@ -762,6 +876,16 @@ describe('automatic Squad bootstrap workflow', () => {
);
}, 180000);

it('detects a compiled mutation that removes the file-backed safe-output CLI', () => {
const assertMounted = (lock: string) => {
const agent = parse(lock).jobs.agent;
expect(agent.steps.some((step: { id?: string }) => step.id === 'mount-mcp-clis')).toBe(true);
expect(lock).toContain('GH_AW_MCP_CLI_SERVERS=\'["safeoutputs"]\'');
};
assertMounted(compileWorkflow());
expect(() => assertMounted(compileWorkflow(WORKFLOW.replace(' cli-proxy: true\n', '')))).toThrow();
}, 180000);

it('declares deterministic output bounds and paginated recovery checks', () => {
expect(WORKFLOW).toContain('max: 1');
expect(WORKFLOW).toContain("state: 'all'");
Expand Down
51 changes: 51 additions & 0 deletions test/gh-aw-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3992,6 +3992,57 @@ describe('gh-aw: canonical package integrity contract', () => {
120_000,
);

it('seeds native-consumer CI pins without usable authentication and rejects tampering', () => {
const workflow = parseDocument(readText(join(process.cwd(), '.github/workflows/squad-ci.yml'))).toJS();
const step = workflow.jobs['gh-aw-compile'].steps.find(
(candidate: { name?: string }) => candidate.name === 'Verify a clean native package consumer',
);
const seed = step.run.match(/node --input-type=module -e '([\s\S]*?)'/)?.[1];
expect(seed, 'native consumer must execute the version-bound pin helper').toBeDefined();
expect(step.run.indexOf('compileWithPinnedActions(process.cwd())'))
.toBeLessThan(step.run.indexOf('gh aw compile --strict --approve'));
expect(step.run).toContain('--verify-install');
expect(step.run).toContain('--strict-compile');

const root = makeConsumer(revisionA, true, 'package');
execFileSync('git', ['init', '--quiet'], { cwd: root });
execFileSync('git', ['remote', 'add', 'origin', 'https://github.com/example/squad-consumer.git'], { cwd: root });
// A deliberately invalid test credential prevents fallback to local keychain authentication.
const env = { ...process.env, GH_TOKEN: 'invalid-test-token', GITHUB_TOKEN: '' };
const compile = () => execFileSync('gh', ['aw', 'compile', '--strict', '--no-check-update'], {
cwd: root, env, stdio: 'pipe', timeout: 120_000,
});
const lockPath = join(root, '.github/workflows/squad.lock.yml');
compile();
const unpinned = readText(lockPath);
expect(createHash('sha256').update(normalizeCompiledLock(unpinned, revisionA)).digest('hex'))
.toBe('bf54175720e6e9ab3fe5d5e3054c28f3a69595c95cdf50e56f9b30988158270f');
expect(verifyInstall(root).failures.join('\n')).toContain('Installed digest mismatch');

const seedPins = () => spawnSync(process.execPath, ['--input-type=module', '-e', seed!], {
cwd: root, env, encoding: 'utf8', timeout: 120_000,
});
const seeded = seedPins();
expect(seeded.status, seeded.stderr).toBe(0);
compile();
expect(verifyInstall(root, { expectedRevision: revisionA, strictCompile: true }).failures).toEqual([]);
const contract = JSON.parse(readText(join(root, CONTRACT_DESTINATION)));
for (const entry of contract.workflows) {
const lock = readText(join(root, entry.lock));
expect(() => validateCompilerActionPins(lock)).not.toThrow();
expect(createHash('sha256').update(normalizeCompiledLock(lock, revisionA)).digest('hex'))
.toBe(entry.package_lock_sha256);
}

const pinPath = join(root, '.github/aw/actions-lock.json');
const pins = JSON.parse(readText(pinPath));
pins.entries[`github/gh-aw-actions/setup@${MIN_GH_AW_VERSION}`].sha = '0'.repeat(40);
writeFileSync(pinPath, JSON.stringify(pins));
const tampered = seedPins();
expect(tampered.status).not.toBe(0);
expect(tampered.stderr).toContain('missing or noncanonical pin');
}, 120_000);

function updateOwnedDigest(root: string, destination: string): void {
const path = join(root, '.github/aw/packages/bradygaster-squad-workflows-test.json');
const record = JSON.parse(readFileSync(path, 'utf8'));
Expand Down
54 changes: 24 additions & 30 deletions workflows/package/squad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ network:
allowed:
- defaults
tools:
cli-proxy: true
edit: null
bash: true
github:
Expand Down Expand Up @@ -153,17 +154,18 @@ pre-agent-steps:
# BEGIN GENERATED RESOURCE DIGESTS
check_hash "$install_verifier" "514e210ebeae3b355ff73ed81bfd17f37207c9a3576ca44eaf8af7bfa21fc173"
check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
check_hash "$bootstrap_validator" "d449b9204f7fad133ff7133c1a30c9381c87e3c0c9d481352819ca93ea1a1dad"
check_hash "$bootstrap_validator" "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7"
# END GENERATED RESOURCE DIGESTS
node "$bootstrap_validator" \
--encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \
> "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-envelope.json"
node "$bootstrap_validator" \
--submit-envelope "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-envelope.json" \
--root "$PWD" \
--payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \
--repository "${GITHUB_REPOSITORY:?}" \
--default-branch "${SQUAD_BOOTSTRAP_DEFAULT_BRANCH:?}" \
--default-branch "${DEFAULT_BRANCH:?}" \
--link-mode placeholder
node "$bootstrap_validator" \
--encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \
> "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-envelope.json"
SQUAD_BOOTSTRAP_VALIDATOR
chmod 500 "$runner"
safe-outputs:
Expand Down Expand Up @@ -737,33 +739,25 @@ exemplar's long audit format.

## Validation and output

Run exactly:
After generating the payload, run this command exactly once:

```bash
"${GITHUB_WORKSPACE:?}/.github/workflows/run-squad-bootstrap-validator"
```

Only exit status zero with stdout exactly
`Squad bootstrap validation passed.` authorizes reading
`.github/workflows/squad-bootstrap-envelope.json`. That file is the only
transport source for one `materialize_bootstrap` call.

The envelope contains:

- `payload_encoding`: exactly `base64`
- `payload_byte_length`: canonical decimal UTF-8 byte length, maximum 96,000
- `payload_sha256`: lowercase SHA-256 of the complete payload bytes
- `payload_chunk_count`: canonical decimal from 1 through 16
- `payload_chunk_00` through `payload_chunk_15`: only the populated fixed slots

Each populated chunk is `NN:` followed by canonical Base64 for at most 6,000
payload bytes, so every string is at most 8,003 bytes and stays conservatively
below gh-aw's 10,240-byte per-string input limit. Pass every property from the
envelope byte-for-byte to the typed safe-output call. Do not reserialize the
payload, recompute metadata, rename slots, add unused slots, or split semantic
generation into separate Cast and research outputs. The writer reconstructs the
one shared payload and verifies order, count, bounds, UTF-8, total byte length,
and SHA-256 before parsing any JSON.

Any validation or envelope error is terminal: emit no materialization output,
report the exact validator stderr, and stop.
The authenticated command deterministically encodes the payload, reconstructs
and checks all chunk ordinals, count, bounds, UTF-8, byte length and SHA-256,
validates the payload schema and Cast tree, then invokes the existing typed
`materialize_bootstrap` safe-output tool through its mounted CLI with JSON on
stdin. Its tool invocation is bounded to 60 seconds. GitHub writes still occur
only in the safe-output job, which independently revalidates the payload.

Exit status zero with stdout exactly
`Squad bootstrap validation passed; materialize_bootstrap submitted.`
means the one output has already been submitted. Stop immediately.
Never read, print, extract, or transcribe `squad-bootstrap-envelope.json` or its
chunks. Never invoke `materialize_bootstrap` separately, retry the command, or
split Cast and research into separate outputs.

Any validation, envelope, or submission error is terminal: report the exact
stderr and stop without another materialization attempt.
Loading
Loading