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
37 changes: 35 additions & 2 deletions scripts/validate-gh-aw-cast.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -474,15 +474,48 @@ function parseCastingPair(registryRaw, historyRaw, source, errors, options = {})
}

function committedRegistry(root, errors) {
try {
execFileSync(
'git',
['rev-parse', '--verify', 'HEAD'],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
} catch (error) {
errors.push(`registry base: committed HEAD is unavailable (${error.message})`);
return null;
}

const registryPath = '.squad/casting/registry.json';
const historyPath = '.squad/casting/history.json';
let committedPaths;
try {
committedPaths = new Set(execFileSync(
'git',
['ls-tree', '-r', '--name-only', 'HEAD', '--', registryPath, historyPath],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
).trim().split('\n').filter(Boolean));
} catch (error) {
errors.push(`registry base: committed registry/history pair is unavailable (${error.message})`);
return null;
}
const hasRegistry = committedPaths.has(registryPath);
const hasHistory = committedPaths.has(historyPath);

if (!hasRegistry && !hasHistory) return null;
if (hasRegistry !== hasHistory) {
errors.push('registry base: complete committed registry/history pair is required');
return null;
}

try {
const registryRaw = execFileSync(
'git',
['show', 'HEAD:.squad/casting/registry.json'],
['show', `HEAD:${registryPath}`],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
const historyRaw = execFileSync(
'git',
['show', 'HEAD:.squad/casting/history.json'],
['show', `HEAD:${historyPath}`],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
return parseCastingPair(
Expand Down
80 changes: 78 additions & 2 deletions test/gh-aw-cast-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ function commitRegistry(root: string): void {
expect(spawnSync('git', ['commit', '-qm', 'versioned registry'], { cwd: root }).status).toBe(0);
}

function removeCommittedCastingFiles(root: string, ...paths: string[]): void {
expect(spawnSync('git', [
'rm',
'--cached',
'--quiet',
'--force',
'--',
...paths,
], { cwd: root }).status).toBe(0);
expect(spawnSync('git', ['commit', '-qm', 'remove committed casting files'], { cwd: root }).status).toBe(0);
}

function advanceHistory(root: string, revision: number, generatedAt: string): void {
const historyPath = join(root, '.squad', 'casting', 'history.json');
const history = JSON.parse(readFileSync(historyPath, 'utf8'));
Expand Down Expand Up @@ -491,6 +503,69 @@ describe('GH-AW Cast final-tree validator', () => {
expect(commandDigest).toBe(sha256(canonical));
});

it.each([
{
name: 'no committed pair',
mutateBase: (root: string) => removeCommittedCastingFiles(
root,
'.squad/casting/registry.json',
'.squad/casting/history.json',
),
expectedStatus: 0,
expectedError: undefined,
},
{
name: 'only a committed registry',
mutateBase: (root: string) => removeCommittedCastingFiles(
root,
'.squad/casting/history.json',
),
expectedStatus: 1,
expectedError: /complete committed registry\/history pair is required/,
},
{
name: 'only a committed history',
mutateBase: (root: string) => removeCommittedCastingFiles(
root,
'.squad/casting/registry.json',
),
expectedStatus: 1,
expectedError: /complete committed registry\/history pair is required/,
},
{
name: 'a valid committed pair',
mutateBase: (_root: string) => {},
expectedStatus: 0,
expectedError: undefined,
},
{
name: 'a malformed committed pair',
mutateBase: (root: string) => {
write(root, '.squad/casting/registry.json', '{');
expect(spawnSync('git', [
'add',
'.squad/casting/registry.json',
'.squad/casting/history.json',
], { cwd: root }).status).toBe(0);
expect(spawnSync('git', ['commit', '-qm', 'malformed committed pair'], { cwd: root }).status).toBe(0);
writeCanonicalGenesisPair(root);
},
expectedStatus: 1,
expectedError: /registry base: committed registry\/history pair is unavailable or malformed/,
},
])('handles $name at HEAD through the distributed validator', ({
mutateBase,
expectedStatus,
expectedError,
}) => {
const fixture = createFixture();
mutateBase(fixture.root);
const result = validateWorkflowResource(fixture.root, fixture.payload);
expect(result.status, result.stderr).toBe(expectedStatus);
if (expectedError) expect(result.stderr).toMatch(expectedError);
else expect(result.stderr).not.toContain('registry base:');
});

it.each([
{
name: 'missing casting pair',
Expand Down Expand Up @@ -827,9 +902,10 @@ describe('GH-AW Cast final-tree validator', () => {
installResource(fixture.root, "console.log('Cast validation passed.');\n");
const result = runValidatorCommand(fixture);
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(
/Cast validator SHA-256 mismatch: expected 62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce, got [a-f0-9]{64}\./,
expect(result.stderr).toContain(
`Cast validator SHA-256 mismatch: expected ${sha256(resourceSource())}, got `,
);
expect(result.stderr).toMatch(/got [a-f0-9]{64}\./);
expect(result.stdout).not.toContain('Cast validation passed.');
expect(authorizesPullRequest(result)).toBe(false);
});
Expand Down
5 changes: 4 additions & 1 deletion test/gh-aw-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1724,7 +1724,10 @@ describe('gh-aw: compiled workflow shell input security contract', () => {
'--payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-cast-payload.json"',
);
expect(normalizedRunnerStep).not.toContain('RUNNER_TEMP');
expect(normalizedRunnerStep).toContain('validator_expected_sha256="62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"');
const validatorDigest = createHash('sha256')
.update(readFileSync(join(SHARED_DIR, 'squad-cast-validator.mjs')))
.digest('hex');
expect(normalizedRunnerStep).toContain(`validator_expected_sha256="${validatorDigest}"`);
expect(normalizedRunnerStep).toContain("outcome: 'cast_failure'");
expect(normalizedRunnerStep).toContain('chmod 500 "$validator_runner"');
// Prepared as a pre-agent-step (see the built-in fidelity ordering test below):
Expand Down
2 changes: 1 addition & 1 deletion workflows/package/squad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pre-agent-steps:
}
# BEGIN GENERATED RESOURCE DIGESTS
check_hash "$install_verifier" "0da2b31a80b47e428cb215a00d4655b54b7b66e4a6a2522f0f4da8f119d0346e"
check_hash "$cast_validator" "62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"
check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
check_hash "$bootstrap_validator" "d449b9204f7fad133ff7133c1a30c9381c87e3c0c9d481352819ca93ea1a1dad"
# END GENERATED RESOURCE DIGESTS
node "$bootstrap_validator" \
Expand Down
2 changes: 1 addition & 1 deletion workflows/package/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ pre-agent-steps:
validator_script="$(cd "$(dirname "$validator_script")" && pwd -P)/$(basename "$validator_script")"

# BEGIN GENERATED RESOURCE DIGEST
validator_expected_sha256="62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"
validator_expected_sha256="0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
# END GENERATED RESOURCE DIGEST
: > "$stderr_file"
validator_actual_sha256="$(
Expand Down
37 changes: 35 additions & 2 deletions workflows/shared/squad-cast-validator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -474,15 +474,48 @@ function parseCastingPair(registryRaw, historyRaw, source, errors, options = {})
}

function committedRegistry(root, errors) {
try {
execFileSync(
'git',
['rev-parse', '--verify', 'HEAD'],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
} catch (error) {
errors.push(`registry base: committed HEAD is unavailable (${error.message})`);
return null;
}

const registryPath = '.squad/casting/registry.json';
const historyPath = '.squad/casting/history.json';
let committedPaths;
try {
committedPaths = new Set(execFileSync(
'git',
['ls-tree', '-r', '--name-only', 'HEAD', '--', registryPath, historyPath],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
).trim().split('\n').filter(Boolean));
} catch (error) {
errors.push(`registry base: committed registry/history pair is unavailable (${error.message})`);
return null;
}
const hasRegistry = committedPaths.has(registryPath);
const hasHistory = committedPaths.has(historyPath);

if (!hasRegistry && !hasHistory) return null;
if (hasRegistry !== hasHistory) {
errors.push('registry base: complete committed registry/history pair is required');
return null;
}

try {
const registryRaw = execFileSync(
'git',
['show', 'HEAD:.squad/casting/registry.json'],
['show', `HEAD:${registryPath}`],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
const historyRaw = execFileSync(
'git',
['show', 'HEAD:.squad/casting/history.json'],
['show', `HEAD:${historyPath}`],
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
);
return parseCastingPair(
Expand Down
2 changes: 1 addition & 1 deletion workflows/squad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pre-agent-steps:
}
# BEGIN GENERATED RESOURCE DIGESTS
check_hash "$install_verifier" "0da2b31a80b47e428cb215a00d4655b54b7b66e4a6a2522f0f4da8f119d0346e"
check_hash "$cast_validator" "62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"
check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
check_hash "$bootstrap_validator" "d449b9204f7fad133ff7133c1a30c9381c87e3c0c9d481352819ca93ea1a1dad"
# END GENERATED RESOURCE DIGESTS
node "$bootstrap_validator" \
Expand Down
6 changes: 3 additions & 3 deletions workflows/squad-workflows.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"source": "workflows/package/squad.md",
"destination": ".github/workflows/squad.md",
"lock": ".github/workflows/squad.lock.yml",
"source_sha256": "ae015aa7d55efd0c9c6a5e6ef759a3b6456735e7929487554402185dcaefb4a3"
"source_sha256": "70eb04edba8c0ca129f910f1d7c11d9378c7832e737a8922d5fa928278a670e6"
},
{
"name": "squad-implement-worker",
Expand Down Expand Up @@ -56,7 +56,7 @@
"source": "workflows/package/squad-bootstrap.md",
"destination": ".github/workflows/squad-bootstrap.md",
"lock": ".github/workflows/squad-bootstrap.lock.yml",
"source_sha256": "4362c1c3b9d686bc920a76991cd8756cadd0beaf3021fe12171834188df665b7"
"source_sha256": "f17065511fdc32fb57c2bb41758afd922dfeddbbc6168339d6800498f2470209"
}
],
"shared_runtime": [
Expand All @@ -74,7 +74,7 @@
"package_destination": ".github/workflows/shared/squad-cast-validator.mjs",
"destination": ".github/workflows/shared/squad-cast-validator.mjs",
"ownership": "manifest",
"sha256": "62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"
"sha256": "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
},
{
"path": "shared/squad-bootstrap-validator.mjs",
Expand Down
2 changes: 1 addition & 1 deletion workflows/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pre-agent-steps:
validator_script="$(cd "$(dirname "$validator_script")" && pwd -P)/$(basename "$validator_script")"

# BEGIN GENERATED RESOURCE DIGEST
validator_expected_sha256="62fbf47b51639fd1878c143e5176ee3099e390065997411511e9d483d467bbce"
validator_expected_sha256="0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685"
# END GENERATED RESOURCE DIGEST
: > "$stderr_file"
validator_actual_sha256="$(
Expand Down
Loading