|
1 | 1 | #!/usr/bin/env node |
2 | | -import { execFile } from 'node:child_process'; |
3 | 2 | import { createHash } from 'node:crypto'; |
4 | | -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; |
5 | | -import { tmpdir } from 'node:os'; |
6 | | -import { join, resolve } from 'node:path'; |
7 | | -import { promisify } from 'node:util'; |
| 3 | +import { readFile } from 'node:fs/promises'; |
| 4 | +import { dirname, resolve } from 'node:path'; |
| 5 | +import { fileURLToPath } from 'node:url'; |
8 | 6 |
|
9 | | -const run = promisify(execFile); |
10 | | -const [gitDirectoryInput, commit, admissionInput] = process.argv.slice(2); |
11 | | -if (!gitDirectoryInput || !/^[0-9a-f]{40}$/u.test(commit ?? '') || !admissionInput) { |
12 | | - throw new Error('usage: verify-hosted-approval-runtime-admission-authority <git-dir> <commit> <admission-json>'); |
| 7 | +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); |
| 8 | +const goldenPath = resolve( |
| 9 | + root, |
| 10 | + 'test/fixtures/hosted-approval-runtime-admission-v1.release-golden.json' |
| 11 | +); |
| 12 | +const runtimeLockPath = resolve(root, 'runtime.lock.json'); |
| 13 | +const [admissionInput] = process.argv.slice(2); |
| 14 | +const golden = JSON.parse(await readFile(goldenPath, 'utf8')); |
| 15 | +const runtimeLock = JSON.parse(await readFile(runtimeLockPath, 'utf8')); |
| 16 | + |
| 17 | +const exactKeys = (value, expected) => |
| 18 | + value !== null && |
| 19 | + typeof value === 'object' && |
| 20 | + !Array.isArray(value) && |
| 21 | + Object.keys(value).length === expected.length && |
| 22 | + expected.every((key) => Object.hasOwn(value, key)); |
| 23 | +const sha256 = (value) => createHash('sha256').update(value).digest('hex'); |
| 24 | +const SHA256 = /^[0-9a-f]{64}$/u; |
| 25 | +const COMMIT = /^[0-9a-f]{40}$/u; |
| 26 | + |
| 27 | +function invalid(reason) { |
| 28 | + throw new Error(`hosted-approval-runtime-release-golden-invalid:${reason}`); |
| 29 | +} |
| 30 | + |
| 31 | +function verifyReleaseGolden() { |
| 32 | + if (!exactKeys(golden, ['schemaVersion', 'source', 'release', 'canonicalJson', 'sha256'])) { |
| 33 | + invalid('schema'); |
| 34 | + } |
| 35 | + if ( |
| 36 | + golden.schemaVersion !== 2 || |
| 37 | + golden.source !== 'agent-teams-orchestrator/hosted-approval-runtime-admission.v1' |
| 38 | + ) { |
| 39 | + invalid('identity'); |
| 40 | + } |
| 41 | + if ( |
| 42 | + !exactKeys(golden.release, [ |
| 43 | + 'sourceRepository', |
| 44 | + 'releaseRepository', |
| 45 | + 'releaseTag', |
| 46 | + 'sourceCommit', |
| 47 | + 'manifestAttestation', |
| 48 | + ]) || |
| 49 | + golden.release.sourceRepository !== '777genius/agent_teams_orchestrator' || |
| 50 | + golden.release.releaseRepository !== '777genius/agent_teams_orchestrator_binaries' || |
| 51 | + golden.release.releaseTag !== runtimeLock.releaseTag || |
| 52 | + golden.release.sourceCommit !== '1f9a7ffc00715e2434eb7549ac2c53880d9f6f83' || |
| 53 | + !COMMIT.test(golden.release.sourceCommit) |
| 54 | + ) { |
| 55 | + invalid('release-pin'); |
| 56 | + } |
| 57 | + const attestation = golden.release.manifestAttestation; |
| 58 | + if ( |
| 59 | + !exactKeys(attestation, ['kind', 'releaseId', 'assetId', 'assetName', 'assetSha256']) || |
| 60 | + attestation.kind !== 'github-release-asset-digest/v1' || |
| 61 | + attestation.releaseId !== 363595793 || |
| 62 | + attestation.assetId !== 498146437 || |
| 63 | + attestation.assetName !== `agent-teams-runtime-manifest-${runtimeLock.sourceRef}.json` || |
| 64 | + attestation.assetSha256 !== |
| 65 | + '0b1036e1f110eefeed5b12c9637a7efa3bf35d6b09a8eb20519909e86bed2bcb' || |
| 66 | + !SHA256.test(attestation.assetSha256) |
| 67 | + ) { |
| 68 | + invalid('manifest-attestation-pin'); |
| 69 | + } |
| 70 | + if ( |
| 71 | + typeof golden.canonicalJson !== 'string' || |
| 72 | + golden.sha256 !== `sha256:${sha256(golden.canonicalJson)}` |
| 73 | + ) { |
| 74 | + invalid('canonical-digest'); |
| 75 | + } |
| 76 | + const snapshot = JSON.parse(golden.canonicalJson); |
| 77 | + if ( |
| 78 | + !exactKeys(snapshot, ['schemaVersion', 'approvalGeneration', 'authorities']) || |
| 79 | + snapshot.schemaVersion !== 1 || |
| 80 | + snapshot.approvalGeneration !== 1 || |
| 81 | + !Array.isArray(snapshot.authorities) || |
| 82 | + snapshot.authorities.length === 0 |
| 83 | + ) { |
| 84 | + invalid('authority-snapshot'); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +verifyReleaseGolden(); |
| 89 | +if (admissionInput === '--verify-golden') { |
| 90 | + process.stdout.write(`${JSON.stringify({ verified: true, releaseGolden: goldenPath })}\n`); |
| 91 | + process.exit(0); |
| 92 | +} |
| 93 | +if (!admissionInput) { |
| 94 | + throw new Error('usage: verify-hosted-approval-runtime-admission-authority <admission-json>'); |
13 | 95 | } |
14 | 96 |
|
15 | | -const gitDirectory = resolve(gitDirectoryInput); |
16 | 97 | const admissionPath = resolve(admissionInput); |
17 | | -const root = await mkdtemp(join(tmpdir(), 'hosted-approval-authority-')); |
18 | | -const authorityFiles = [ |
19 | | - 'HostedApprovalRuntimeAdmission.ts', |
20 | | - 'HostedApprovalWire.ts', |
21 | | - 'ownerProof.ts', |
22 | | - 'protocol.ts', |
23 | | -]; |
| 98 | +const admissionBytes = await readFile(admissionPath); |
| 99 | +const admission = JSON.parse(admissionBytes.toString('utf8')); |
| 100 | +const match = /^approval-admission-generation_([1-9][0-9]*)_owner_[1-9][0-9]*$/u.exec( |
| 101 | + admission.admissionGeneration ?? '' |
| 102 | +); |
| 103 | +if (!match || !Array.isArray(admission.routes)) invalid('admission-shape'); |
| 104 | +const snapshot = { |
| 105 | + schemaVersion: 1, |
| 106 | + approvalGeneration: Number(match[1]), |
| 107 | + authorities: [...admission.routes] |
| 108 | + .toSorted((left, right) => left.routeId.localeCompare(right.routeId)) |
| 109 | + .map((route) => route.authority), |
| 110 | +}; |
| 111 | +const canonicalJson = JSON.stringify(snapshot); |
| 112 | +if (canonicalJson !== golden.canonicalJson) invalid('cross-repository-authority-mismatch'); |
24 | 113 |
|
25 | | -try { |
26 | | - for (const file of authorityFiles) { |
27 | | - const objectPath = `src/services/hostedControl/${file}`; |
28 | | - const { stdout } = await run('/usr/bin/git', [ |
29 | | - `--git-dir=${gitDirectory}`, |
30 | | - 'show', |
31 | | - `${commit}:${objectPath}`, |
32 | | - ]); |
33 | | - await writeFile(join(root, file), stdout, { mode: 0o600 }); |
34 | | - } |
35 | | - const admission = JSON.parse(await readFile(admissionPath, 'utf8')); |
36 | | - const runner = ` |
37 | | - import { PrivateFileHostedApprovalRuntimeAdmissionStore } from './HostedApprovalRuntimeAdmission.ts'; |
38 | | - import { readFile } from 'node:fs/promises'; |
39 | | - void (async () => { |
40 | | - const admission = JSON.parse(await readFile(${JSON.stringify(admissionPath)}, 'utf8')); |
41 | | - const outer = admission.outerAuthority; |
42 | | - const loaded = await new PrivateFileHostedApprovalRuntimeAdmissionStore(${JSON.stringify(admissionPath)}).load({ |
43 | | - deploymentId: outer.deploymentId, |
44 | | - bootId: outer.bootId, |
45 | | - workspaceId: outer.workspaceId, |
46 | | - teamId: outer.teamId, |
47 | | - restoreGeneration: outer.restoreGeneration, |
48 | | - mountBinding: outer.mountBinding, |
49 | | - }); |
50 | | - if (!(await loaded.isCurrent())) throw new Error('external-authority-drift'); |
51 | | - })(); |
52 | | - `; |
53 | | - await writeFile(join(root, 'verify.ts'), runner, { mode: 0o600 }); |
54 | | - await run(process.execPath, ['--import', 'tsx', join(root, 'verify.ts')], { |
55 | | - cwd: process.cwd(), |
56 | | - }); |
57 | | - const source = await readFile(join(root, 'HostedApprovalRuntimeAdmission.ts')); |
58 | | - const admissionBytes = await readFile(admissionPath); |
59 | | - process.stdout.write(`${JSON.stringify({ |
| 114 | +process.stdout.write( |
| 115 | + `${JSON.stringify({ |
60 | 116 | verified: true, |
61 | | - authorityCommit: commit, |
62 | | - authorityBlobSha256: createHash('sha256').update(source).digest('hex'), |
63 | | - admissionSha256: createHash('sha256').update(admissionBytes).digest('hex'), |
64 | | - approvalGeneration: admission.admissionGeneration, |
65 | | - })}\n`); |
66 | | -} finally { |
67 | | - await rm(root, { recursive: true, force: true }); |
68 | | -} |
| 117 | + sourceRepository: golden.release.sourceRepository, |
| 118 | + releaseRepository: golden.release.releaseRepository, |
| 119 | + releaseTag: golden.release.releaseTag, |
| 120 | + sourceCommit: golden.release.sourceCommit, |
| 121 | + manifestAttestationSha256: golden.release.manifestAttestation.assetSha256, |
| 122 | + admissionSha256: sha256(admissionBytes), |
| 123 | + approvalGeneration: snapshot.approvalGeneration, |
| 124 | + })}\n` |
| 125 | +); |
0 commit comments