Skip to content

Commit b545508

Browse files
bradygasterCopilot
andcommitted
fix(gh-aw): preserve bootstrap candidate isolation
Validate candidate bytes from the isolated tree while resolving committed Cast provenance from the trusted checkout. Adds regression coverage for CAO integration issue octodemo/zava-social-cao-ops-20260929111840#26. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 26f4010 commit b545508

11 files changed

Lines changed: 54 additions & 28 deletions

‎.changeset/bright-gh-aw-central-ops.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"@bradygaster/squad-sdk": patch
44
---
55

6-
Require gh-aw v0.89.22 across the Squad workflow package, enlistment guidance, integrity contract, CI, and hosted E2E controller for central agentic ops compatibility.
6+
Require gh-aw v0.89.22 across the Squad workflow package, enlistment guidance, integrity contract, CI, and hosted E2E controller for central agentic ops compatibility. Validate isolated bootstrap candidates against the trusted checkout's Git history so materialization can create the Cast PR and research issue.

‎scripts/validate-gh-aw-cast.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ function committedRegistry(root, errors) {
531531
}
532532
}
533533

534-
function parseRegistry(root, errors) {
534+
function parseRegistry(root, errors, gitRoot = root) {
535535
let registryRaw;
536536
let historyRaw;
537537
try {
@@ -543,7 +543,7 @@ function parseRegistry(root, errors) {
543543
}
544544
const registry = parseCastingPair(registryRaw, historyRaw, 'registry', errors);
545545
if (!registry) return [];
546-
const base = committedRegistry(root, errors);
546+
const base = committedRegistry(gitRoot, errors);
547547
if (base) {
548548
const baseRevision = base.schema === 'squad-agent-provenance/v1' ? base.revision : 0;
549549
if (registry.revision <= baseRevision) {
@@ -631,7 +631,7 @@ function validateCapabilities(coordinator, active, routingRows, errors) {
631631
}
632632
}
633633

634-
export function validateCastTree({ root, payloadPath }) {
634+
export function validateCastTree({ root, payloadPath, gitRoot = root }) {
635635
const errors = [];
636636
let payloadValue;
637637
try {
@@ -660,7 +660,7 @@ export function validateCastTree({ root, payloadPath }) {
660660
if (!payload.includes(required)) errors.push(`payload: missing required path ${required}`);
661661
}
662662

663-
const active = parseRegistry(root, errors);
663+
const active = parseRegistry(root, errors, gitRoot);
664664
const activeNames = new Set(active.map(({ name }) => name));
665665
const activeCharters = active.map(({ id }) => `.squad/agents/${id}/charter.md`);
666666
const expectedPayload = new Set([...CORE_PAYLOAD, ...activeCharters, ...REQUIRED_BUILTIN_CHARTERS]);

‎test/gh-aw-bootstrap-workflow.test.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from 'node:fs';
1212
import { execFileSync, spawnSync } from 'node:child_process';
1313
import { createHash } from 'node:crypto';
14-
import { dirname, join, resolve } from 'node:path';
14+
import { dirname, join, relative, resolve } from 'node:path';
1515
import { tmpdir } from 'node:os';
1616
import { fileURLToPath } from 'node:url';
1717
import { parse } from 'yaml';
@@ -31,6 +31,7 @@ import {
3131
findBootstrapResearchArtifacts,
3232
isBootstrapResearchSeed,
3333
reconstructBootstrapPayload,
34+
validateBootstrapPayload,
3435
} from '../workflows/shared/squad-bootstrap-validator.mjs';
3536

3637
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -601,6 +602,27 @@ describe('automatic Squad bootstrap workflow', () => {
601602
const result = validateFixture(fixture);
602603
expect(result.status, result.stderr).toBe(0);
603604
expect(result.stdout).toBe('Squad bootstrap validation passed.\n');
605+
});
606+
607+
it('validates an isolated candidate against the trusted checkout Git history', () => {
608+
const fixture = createFixture();
609+
const candidate = mkdtempSync(join(tmpdir(), 'gh-aw-bootstrap-candidate-'));
610+
workspaces.push(candidate);
611+
cpSync(fixture.root, candidate, {
612+
recursive: true,
613+
filter: source => relative(fixture.root, source).split('/')[0] !== '.git',
614+
});
615+
const options = {
616+
root: candidate,
617+
payloadPath: join(candidate, 'payload.json'),
618+
repository: 'octo/example',
619+
defaultBranch: 'main',
620+
linkMode: 'placeholder',
621+
};
622+
expect(validateBootstrapPayload(options)).toContainEqual(
623+
expect.stringContaining('registry base: committed HEAD is unavailable'),
624+
);
625+
expect(validateBootstrapPayload({ ...options, gitRoot: fixture.root })).toEqual([]);
604626

605627
const resolved = {
606628
...fixture.payload,
@@ -849,6 +871,7 @@ describe('automatic Squad bootstrap workflow', () => {
849871
expect(lock).not.toMatch(/"materialize-bootstrap":\{"inputs":\{"payload":/);
850872
expect(lock).toContain('reconstructBootstrapPayload(items[0])');
851873
expect(lock).toContain("mkdtempSync(join(tmpdir(), 'squad-bootstrap-candidate-'))");
874+
expect(lock).toContain('gitRoot: checkout');
852875
expect(lock).toContain('Bootstrap payload path crosses a symbolic link');
853876
expect(lock.indexOf("validate(payload, 'placeholder')"))
854877
.toBeLessThan(lock.indexOf('safeTarget(checkout, file.path)'));

‎test/gh-aw-quality.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3961,8 +3961,8 @@ describe('gh-aw: canonical package integrity contract', () => {
39613961
expect(mutable).not.toContain(actionReference);
39623962
expect(createHash('sha256').update(normalizeCompiledLock(mutable, revisionA)).digest('hex'))
39633963
.toBe(sourceBinding === 'workflow'
3964-
? '0914fadb4a0f37867840b9394c60f3cd40fb3072dd003518a17efb23718dd632'
3965-
: 'cac9dea1082405512f66b8efcdb14b554c71e5adc75da158ecc12a0458c7fe52');
3964+
? 'a3290dad380577fd6d64c92169b9d74a7f5248851b110d8eafc6260b34f772d9'
3965+
: 'fd7c735e2de5088954e8d0d647a15e5f95ed5850947f201c1a4ce17433d66632');
39663966
expect(() => validateCompilerActionPins(mutable)).toThrow(/invalid immutable action pin/);
39673967
writeFileSync(lockPath, mutable);
39683968
expect(verifyInstall(root).failures.join('\n'))
@@ -4016,7 +4016,7 @@ describe('gh-aw: canonical package integrity contract', () => {
40164016
compile();
40174017
const unpinned = readText(lockPath);
40184018
expect(createHash('sha256').update(normalizeCompiledLock(unpinned, revisionA)).digest('hex'))
4019-
.toBe('cac9dea1082405512f66b8efcdb14b554c71e5adc75da158ecc12a0458c7fe52');
4019+
.toBe('fd7c735e2de5088954e8d0d647a15e5f95ed5850947f201c1a4ce17433d66632');
40204020
expect(verifyInstall(root).failures.join('\n')).toContain('Installed digest mismatch');
40214021

40224022
const seedPins = () => spawnSync(process.execPath, ['--input-type=module', '-e', seed!], {

‎workflows/package/squad-bootstrap.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ pre-agent-steps:
153153
}
154154
# BEGIN GENERATED RESOURCE DIGESTS
155155
check_hash "$install_verifier" "cf474be9b04d339f7e7a18c65e776b8a53e84bea5b4b85abfe65ed11f7b782ce"
156-
check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
157-
check_hash "$bootstrap_validator" "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7"
156+
check_hash "$cast_validator" "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada"
157+
check_hash "$bootstrap_validator" "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e"
158158
# END GENERATED RESOURCE DIGESTS
159159
node "$bootstrap_validator" \
160160
--encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \
@@ -316,6 +316,7 @@ safe-outputs:
316316
writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`);
317317
const errors = validatorModule.validateBootstrapPayload({
318318
root: candidate,
319+
gitRoot: checkout,
319320
payloadPath: candidatePayloadPath,
320321
repository: context.repo.owner + '/' + context.repo.repo,
321322
defaultBranch: process.env.SQUAD_BOOTSTRAP_DEFAULT_BRANCH,

‎workflows/package/squad.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ pre-agent-steps:
840840
validator_script="$(cd "$(dirname "$validator_script")" && pwd -P)/$(basename "$validator_script")"
841841
842842
# BEGIN GENERATED RESOURCE DIGEST
843-
validator_expected_sha256="0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
843+
validator_expected_sha256="c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada"
844844
# END GENERATED RESOURCE DIGEST
845845
: > "$stderr_file"
846846
validator_actual_sha256="$(

‎workflows/shared/squad-bootstrap-validator.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ export function createBootstrapResearchComment(issueBodyText, issueNumber) {
532532

533533
export function validateBootstrapPayload({
534534
root,
535+
gitRoot = root,
535536
payloadPath,
536537
repository,
537538
defaultBranch,
@@ -588,7 +589,7 @@ export function validateBootstrapPayload({
588589
try {
589590
const castPayloadPath = join(tempDir, 'cast-paths.json');
590591
writeFileSync(castPayloadPath, `${JSON.stringify(castPaths)}\n`);
591-
errors.push(...validateCastTree({ root, payloadPath: castPayloadPath }));
592+
errors.push(...validateCastTree({ root, gitRoot, payloadPath: castPayloadPath }));
592593
} finally {
593594
rmSync(tempDir, { recursive: true, force: true });
594595
}

‎workflows/shared/squad-cast-validator.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ function committedRegistry(root, errors) {
531531
}
532532
}
533533

534-
function parseRegistry(root, errors) {
534+
function parseRegistry(root, errors, gitRoot = root) {
535535
let registryRaw;
536536
let historyRaw;
537537
try {
@@ -543,7 +543,7 @@ function parseRegistry(root, errors) {
543543
}
544544
const registry = parseCastingPair(registryRaw, historyRaw, 'registry', errors);
545545
if (!registry) return [];
546-
const base = committedRegistry(root, errors);
546+
const base = committedRegistry(gitRoot, errors);
547547
if (base) {
548548
const baseRevision = base.schema === 'squad-agent-provenance/v1' ? base.revision : 0;
549549
if (registry.revision <= baseRevision) {
@@ -631,7 +631,7 @@ function validateCapabilities(coordinator, active, routingRows, errors) {
631631
}
632632
}
633633

634-
export function validateCastTree({ root, payloadPath }) {
634+
export function validateCastTree({ root, payloadPath, gitRoot = root }) {
635635
const errors = [];
636636
let payloadValue;
637637
try {
@@ -660,7 +660,7 @@ export function validateCastTree({ root, payloadPath }) {
660660
if (!payload.includes(required)) errors.push(`payload: missing required path ${required}`);
661661
}
662662

663-
const active = parseRegistry(root, errors);
663+
const active = parseRegistry(root, errors, gitRoot);
664664
const activeNames = new Set(active.map(({ name }) => name));
665665
const activeCharters = active.map(({ id }) => `.squad/agents/${id}/charter.md`);
666666
const expectedPayload = new Set([...CORE_PAYLOAD, ...activeCharters, ...REQUIRED_BUILTIN_CHARTERS]);

‎workflows/squad-bootstrap.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ pre-agent-steps:
160160
}
161161
# BEGIN GENERATED RESOURCE DIGESTS
162162
check_hash "$install_verifier" "cf474be9b04d339f7e7a18c65e776b8a53e84bea5b4b85abfe65ed11f7b782ce"
163-
check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
164-
check_hash "$bootstrap_validator" "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7"
163+
check_hash "$cast_validator" "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada"
164+
check_hash "$bootstrap_validator" "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e"
165165
# END GENERATED RESOURCE DIGESTS
166166
node "$bootstrap_validator" \
167167
--encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \
@@ -307,6 +307,7 @@ safe-outputs:
307307
writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`);
308308
const errors = validatorModule.validateBootstrapPayload({
309309
root: candidate,
310+
gitRoot: checkout,
310311
payloadPath: candidatePayloadPath,
311312
repository: context.repo.owner + '/' + context.repo.repo,
312313
defaultBranch: process.env.SQUAD_BOOTSTRAP_DEFAULT_BRANCH,

‎workflows/squad-workflows.manifest.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
"source": "workflows/package/squad.md",
1515
"destination": ".github/workflows/squad.md",
1616
"lock": ".github/workflows/squad.lock.yml",
17-
"source_sha256": "0f292c576983ac99dace0661e34f900ddd91fcd50f28ff6726eff78704e283f2",
18-
"lock_sha256": "4fde07528253a2dd0f3aef6fda7bcda6773464d8268f91a4b30c122b8f4ebdad",
19-
"package_lock_sha256": "13ce3b90940c358105e2caeeab925570c0f13a522cefc0f35d23b2d81ef81b0d"
17+
"source_sha256": "f86eb83a75c6de63edf520bf21ca2b50fcb9c6e7c5a60ecbd92a527a108a19af",
18+
"lock_sha256": "b11d0ef4a4cc067f71d6e4fae218fa7689609b1c689effc72cd689b1e363c4bc",
19+
"package_lock_sha256": "e2129810e4a6ae01a0802cd22cd8c80e81fb3e6722ed1dcd2ba0a62ad48d5c49"
2020
},
2121
{
2222
"name": "squad-implement-worker",
@@ -68,9 +68,9 @@
6868
"source": "workflows/package/squad-bootstrap.md",
6969
"destination": ".github/workflows/squad-bootstrap.md",
7070
"lock": ".github/workflows/squad-bootstrap.lock.yml",
71-
"source_sha256": "9ebc7297c684a591e0c2144d952736077f7bfae1d39974b1d02a1ea624a964c6",
72-
"lock_sha256": "2d4cae423240f98791cd983de7029a693f8c6fb8be3fbed2694833ce4c031631",
73-
"package_lock_sha256": "1df0be116f7553b122be483cafc1463d3894eeb0ab50327532d4982757d646ea"
71+
"source_sha256": "9cccdec152866720f99452b77fc0407b6a308e86e22f4091b50d844eb40b4fa7",
72+
"lock_sha256": "96a02f7d5837e9753eacae9fbf99658fedf96f33f40ce164e5da9fbb8a283058",
73+
"package_lock_sha256": "33bc16d47c98b27de5fb3a9931437a43d6e8dcde60b1b15ba29214b42256a235"
7474
},
7575
{
7676
"name": "squad-command-router",
@@ -113,15 +113,15 @@
113113
"package_destination": ".github/workflows/shared/squad-cast-validator.mjs",
114114
"destination": ".github/workflows/shared/squad-cast-validator.mjs",
115115
"ownership": "manifest",
116-
"sha256": "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
116+
"sha256": "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada"
117117
},
118118
{
119119
"path": "shared/squad-bootstrap-validator.mjs",
120120
"source": "workflows/shared/squad-bootstrap-validator.mjs",
121121
"package_destination": ".github/workflows/shared/squad-bootstrap-validator.mjs",
122122
"destination": ".github/workflows/shared/squad-bootstrap-validator.mjs",
123123
"ownership": "manifest",
124-
"sha256": "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7"
124+
"sha256": "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e"
125125
},
126126
{
127127
"path": "shared/squad-improvement-gate.mjs",

0 commit comments

Comments
 (0)