@tangle-network/agent-app 0.45.24 → 0.45.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-routes/index.d.ts +57 -6
- package/dist/chat-routes/index.js +225 -17
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/{chunk-AWPK7ZDS.js → chunk-AGCRCOQH.js} +1 -1
- package/dist/chunk-N3G3FSM4.js +509 -0
- package/dist/chunk-N3G3FSM4.js.map +1 -0
- package/dist/chunk-WD2B6HGY.js +1046 -0
- package/dist/chunk-WD2B6HGY.js.map +1 -0
- package/dist/design-canvas-react/index.js +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/run-tEsZUhAf.d.ts +40 -0
- package/dist/signoff/cli.d.ts +12 -0
- package/dist/signoff/cli.js +166 -0
- package/dist/signoff/cli.js.map +1 -0
- package/dist/signoff/index.d.ts +430 -0
- package/dist/signoff/index.js +63 -0
- package/dist/signoff/index.js.map +1 -0
- package/dist/signoff/proof-cli.d.ts +1 -0
- package/dist/signoff/proof-cli.js +92 -0
- package/dist/signoff/proof-cli.js.map +1 -0
- package/dist/signoff/proof.d.ts +515 -0
- package/dist/signoff/proof.js +148 -0
- package/dist/signoff/proof.js.map +1 -0
- package/dist/types-U7Nz-txa.d.ts +233 -0
- package/package.json +16 -3
- /package/dist/{chunk-AWPK7ZDS.js.map → chunk-AGCRCOQH.js.map} +0 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// src/signoff/proof-git.ts
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var SignoffGitError = class extends Error {
|
|
7
|
+
args;
|
|
8
|
+
status;
|
|
9
|
+
stderr;
|
|
10
|
+
constructor(args, status, stderr) {
|
|
11
|
+
super(`git ${args.join(" ")} exited ${status ?? "null"}: ${stderr.trim()}`);
|
|
12
|
+
this.name = "SignoffGitError";
|
|
13
|
+
this.args = args;
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.stderr = stderr;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function runGit(repoDir, args, options = {}) {
|
|
19
|
+
const result = spawnSync("git", [...args], {
|
|
20
|
+
cwd: repoDir,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
input: options.input,
|
|
23
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", ...options.env },
|
|
24
|
+
maxBuffer: 128 * 1024 * 1024
|
|
25
|
+
});
|
|
26
|
+
if (result.error) throw new Error(`git ${args.join(" ")} could not run: ${result.error.message}`);
|
|
27
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
28
|
+
}
|
|
29
|
+
function gitText(repoDir, args, options = {}) {
|
|
30
|
+
const result = runGit(repoDir, args, options);
|
|
31
|
+
if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr);
|
|
32
|
+
return result.stdout.replace(/\n$/, "");
|
|
33
|
+
}
|
|
34
|
+
function gitIsAncestor(repoDir) {
|
|
35
|
+
return (ancestor, descendant) => {
|
|
36
|
+
const result = runGit(repoDir, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
37
|
+
if (result.status === 0) return true;
|
|
38
|
+
if (result.status === 1) return false;
|
|
39
|
+
throw new SignoffGitError(["merge-base", "--is-ancestor", ancestor, descendant], result.status, result.stderr);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function resolveCommit(repoDir, rev) {
|
|
43
|
+
return gitText(repoDir, ["rev-parse", "--verify", `${rev}^{commit}`]);
|
|
44
|
+
}
|
|
45
|
+
function readCommitFacts(repoDir, rev) {
|
|
46
|
+
const commit = resolveCommit(repoDir, rev);
|
|
47
|
+
const record = gitText(repoDir, ["show", "--no-patch", "--format=%T%n%P%n%cI", commit]);
|
|
48
|
+
const [tree, parents, committedAt] = record.split("\n");
|
|
49
|
+
if (tree === void 0 || parents === void 0 || committedAt === void 0) {
|
|
50
|
+
throw new Error(`git show returned an unreadable record for ${commit}: ${JSON.stringify(record)}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
commit,
|
|
54
|
+
commitTree: tree,
|
|
55
|
+
parents: parents.length === 0 ? [] : parents.split(" "),
|
|
56
|
+
committedAt: new Date(committedAt).toISOString()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function computeWorktreeTree(repoDir) {
|
|
60
|
+
const scratch = mkdtempSync(join(tmpdir(), "agent-app-signoff-index-"));
|
|
61
|
+
const indexFile = join(scratch, "index");
|
|
62
|
+
try {
|
|
63
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
64
|
+
gitText(repoDir, ["add", "-A", "--"], { env });
|
|
65
|
+
return gitText(repoDir, ["write-tree"], { env });
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/signoff/proof-record.ts
|
|
72
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
73
|
+
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
74
|
+
import { readFileSync } from "fs";
|
|
75
|
+
import { hostname, userInfo } from "os";
|
|
76
|
+
import { join as join2 } from "path";
|
|
77
|
+
import { z } from "zod";
|
|
78
|
+
var SIGNOFF_PROOF_VERSION = 1;
|
|
79
|
+
var isoString = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp");
|
|
80
|
+
var sha1Hex = z.string().regex(/^[0-9a-f]{40}$/, "must be a 40-hex git object id");
|
|
81
|
+
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "must be a 64-hex sha256 digest");
|
|
82
|
+
var SIGNOFF_STEP_STATUSES = ["passed", "failed", "skipped", "cancelled", "blocked"];
|
|
83
|
+
var signoffProofStepSchema = z.object({
|
|
84
|
+
/** Stable id a repo's required-step table refers to (`typecheck`, `test`, `knip`, …). */
|
|
85
|
+
id: z.string().min(1),
|
|
86
|
+
command: z.string().min(1),
|
|
87
|
+
cwd: z.string().min(1),
|
|
88
|
+
/** How the runner judged the step. Only `passed` can satisfy a requirement. */
|
|
89
|
+
status: z.enum(SIGNOFF_STEP_STATUSES),
|
|
90
|
+
exitCode: z.number().int(),
|
|
91
|
+
durationMs: z.number().int().nonnegative(),
|
|
92
|
+
startedAt: isoString,
|
|
93
|
+
/** sha256 of the step's combined stdout+stderr. Logs are not carried; the digest is. */
|
|
94
|
+
outputSha256: sha256Hex
|
|
95
|
+
});
|
|
96
|
+
var signoffProofPeerSchema = z.object({
|
|
97
|
+
name: z.string().min(1),
|
|
98
|
+
/** `null` when the package is not resolvable on disk — recorded, never guessed. */
|
|
99
|
+
version: z.string().min(1).nullable()
|
|
100
|
+
});
|
|
101
|
+
var signoffProofSubjectSchema = z.object({
|
|
102
|
+
repo: z.string().min(1),
|
|
103
|
+
commit: sha1Hex,
|
|
104
|
+
/** Tree the checks actually ran against, including uncommitted work. */
|
|
105
|
+
tree: sha1Hex,
|
|
106
|
+
/** Tree the commit itself carries. Equal to `tree` on a clean sign-off. */
|
|
107
|
+
commitTree: sha1Hex,
|
|
108
|
+
parents: z.array(sha1Hex),
|
|
109
|
+
committedAt: isoString
|
|
110
|
+
});
|
|
111
|
+
var signoffProofBodySchema = z.object({
|
|
112
|
+
proofVersion: z.number().int().positive(),
|
|
113
|
+
subject: signoffProofSubjectSchema,
|
|
114
|
+
signedAt: isoString,
|
|
115
|
+
host: z.object({
|
|
116
|
+
hostname: z.string().min(1),
|
|
117
|
+
platform: z.string().min(1),
|
|
118
|
+
arch: z.string().min(1),
|
|
119
|
+
user: z.string().min(1)
|
|
120
|
+
}),
|
|
121
|
+
tooling: z.object({
|
|
122
|
+
node: z.string().min(1),
|
|
123
|
+
pnpm: z.string().min(1).nullable(),
|
|
124
|
+
peers: z.array(signoffProofPeerSchema)
|
|
125
|
+
}),
|
|
126
|
+
/**
|
|
127
|
+
* Real elapsed time for the whole run. Recorded separately from the steps
|
|
128
|
+
* because the runner schedules them as wide as their dependencies allow, so
|
|
129
|
+
* the sum of step durations is the SERIAL cost and would overstate this.
|
|
130
|
+
*/
|
|
131
|
+
wallClockMs: z.number().int().nonnegative(),
|
|
132
|
+
/** Seeds fed to anything non-deterministic, so a reader can reproduce the same run. */
|
|
133
|
+
seeds: z.record(z.string(), z.union([z.string(), z.number()])),
|
|
134
|
+
/** The step ids this run claims were required. The verifier holds the authoritative table. */
|
|
135
|
+
declaredRequired: z.array(z.string().min(1)),
|
|
136
|
+
steps: z.array(signoffProofStepSchema),
|
|
137
|
+
verdict: z.enum(["pass", "fail"])
|
|
138
|
+
});
|
|
139
|
+
var signoffProofSealSchema = z.object({
|
|
140
|
+
algorithm: z.enum(["sha256", "hmac-sha256"]),
|
|
141
|
+
/** sha256 over the canonical body. Chains the seal to every field, including the commit and tree. */
|
|
142
|
+
bodySha256: sha256Hex,
|
|
143
|
+
/** First 12 hex of sha256(key), so a reader can tell WHICH key sealed this. */
|
|
144
|
+
keyId: z.string().regex(/^[0-9a-f]{12}$/).nullable(),
|
|
145
|
+
mac: sha256Hex.nullable()
|
|
146
|
+
});
|
|
147
|
+
var signoffProofSchema = z.object({
|
|
148
|
+
body: signoffProofBodySchema,
|
|
149
|
+
seal: signoffProofSealSchema
|
|
150
|
+
});
|
|
151
|
+
function canonicalJson(value) {
|
|
152
|
+
if (value === null) return "null";
|
|
153
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
154
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
155
|
+
if (typeof value === "number") {
|
|
156
|
+
if (!Number.isFinite(value)) throw new Error(`canonicalJson: ${String(value)} is not representable`);
|
|
157
|
+
return JSON.stringify(value);
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
160
|
+
const record = value;
|
|
161
|
+
const keys = Object.keys(record).sort();
|
|
162
|
+
const fields = keys.map((key) => {
|
|
163
|
+
const entry = record[key];
|
|
164
|
+
if (entry === void 0) throw new Error(`canonicalJson: field ${JSON.stringify(key)} is undefined`);
|
|
165
|
+
return `${JSON.stringify(key)}:${canonicalJson(entry)}`;
|
|
166
|
+
});
|
|
167
|
+
return `{${fields.join(",")}}`;
|
|
168
|
+
}
|
|
169
|
+
function canonicalizeProofBody(body) {
|
|
170
|
+
return canonicalJson(body);
|
|
171
|
+
}
|
|
172
|
+
function hashProofBody(body) {
|
|
173
|
+
return createHash("sha256").update(canonicalizeProofBody(body), "utf8").digest("hex");
|
|
174
|
+
}
|
|
175
|
+
function hashStepOutput(output) {
|
|
176
|
+
return createHash("sha256").update(output, "utf8").digest("hex");
|
|
177
|
+
}
|
|
178
|
+
function signoffKeyId(key) {
|
|
179
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 12);
|
|
180
|
+
}
|
|
181
|
+
function readSignoffKey(path) {
|
|
182
|
+
const raw = readFileSync(path);
|
|
183
|
+
if (raw.byteLength < 16) throw new Error(`sign-off key at ${path} is ${raw.byteLength} bytes; at least 16 are required`);
|
|
184
|
+
return new Uint8Array(raw);
|
|
185
|
+
}
|
|
186
|
+
function sealProof(body, key) {
|
|
187
|
+
const canonical = canonicalizeProofBody(body);
|
|
188
|
+
const bodySha256 = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
189
|
+
if (key === void 0) {
|
|
190
|
+
return { body, seal: { algorithm: "sha256", bodySha256, keyId: null, mac: null } };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
body,
|
|
194
|
+
seal: {
|
|
195
|
+
algorithm: "hmac-sha256",
|
|
196
|
+
bodySha256,
|
|
197
|
+
keyId: signoffKeyId(key),
|
|
198
|
+
mac: createHmac("sha256", key).update(canonical, "utf8").digest("hex")
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function macMatches(expected, actual) {
|
|
203
|
+
if (expected.length !== actual.length) return false;
|
|
204
|
+
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"));
|
|
205
|
+
}
|
|
206
|
+
function collectToolingFacts(input) {
|
|
207
|
+
return {
|
|
208
|
+
node: process.version,
|
|
209
|
+
pnpm: readPnpmVersion(input.repoDir),
|
|
210
|
+
peers: input.peerNames.map((name) => ({ name, version: readInstalledVersion(input.repoDir, name) }))
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function readPnpmVersion(repoDir) {
|
|
214
|
+
const result = spawnSync2("pnpm", ["--version"], { cwd: repoDir, encoding: "utf8" });
|
|
215
|
+
if (result.error || result.status !== 0) return null;
|
|
216
|
+
return result.stdout.trim();
|
|
217
|
+
}
|
|
218
|
+
function readInstalledVersion(repoDir, packageName) {
|
|
219
|
+
try {
|
|
220
|
+
const manifest = JSON.parse(readFileSync(join2(repoDir, "node_modules", packageName, "package.json"), "utf8"));
|
|
221
|
+
return typeof manifest.version === "string" ? manifest.version : null;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function tangleDependencyNames(repoDir) {
|
|
227
|
+
const manifest = JSON.parse(readFileSync(join2(repoDir, "package.json"), "utf8"));
|
|
228
|
+
const names = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const block of [manifest.dependencies, manifest.peerDependencies, manifest.devDependencies]) {
|
|
230
|
+
for (const name of Object.keys(block ?? {})) {
|
|
231
|
+
if (name.startsWith("@tangle-network/")) names.add(name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return [...names].sort();
|
|
235
|
+
}
|
|
236
|
+
function buildSignoffProof(input) {
|
|
237
|
+
const facts = readCommitFacts(input.repoDir, input.rev ?? "HEAD");
|
|
238
|
+
const body = {
|
|
239
|
+
proofVersion: SIGNOFF_PROOF_VERSION,
|
|
240
|
+
subject: {
|
|
241
|
+
repo: input.repo,
|
|
242
|
+
commit: facts.commit,
|
|
243
|
+
tree: computeWorktreeTree(input.repoDir),
|
|
244
|
+
commitTree: facts.commitTree,
|
|
245
|
+
parents: [...facts.parents],
|
|
246
|
+
committedAt: facts.committedAt
|
|
247
|
+
},
|
|
248
|
+
signedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
249
|
+
wallClockMs: Math.max(0, Math.round(input.wallClockMs)),
|
|
250
|
+
host: { hostname: hostname(), platform: process.platform, arch: process.arch, user: userInfo().username },
|
|
251
|
+
tooling: collectToolingFacts({ repoDir: input.repoDir, peerNames: input.peerNames ?? tangleDependencyNames(input.repoDir) }),
|
|
252
|
+
seeds: { ...input.seeds },
|
|
253
|
+
declaredRequired: [...input.declaredRequired],
|
|
254
|
+
steps: input.steps.map((step) => ({ ...step })),
|
|
255
|
+
verdict: input.steps.every((step) => step.status === "passed" && step.exitCode === 0) ? "pass" : "fail"
|
|
256
|
+
};
|
|
257
|
+
return sealProof(signoffProofBodySchema.parse(body), input.key);
|
|
258
|
+
}
|
|
259
|
+
function parseSignoffProof(json) {
|
|
260
|
+
return signoffProofSchema.parse(JSON.parse(json));
|
|
261
|
+
}
|
|
262
|
+
function serializeSignoffProof(proof) {
|
|
263
|
+
return `${JSON.stringify(proof, null, 2)}
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
function formatDuration(ms) {
|
|
267
|
+
const seconds = Math.round(ms / 1e3);
|
|
268
|
+
if (seconds < 60) return `${seconds}s`;
|
|
269
|
+
return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
270
|
+
}
|
|
271
|
+
function formatSignoffSummary(proof) {
|
|
272
|
+
const { body, seal } = proof;
|
|
273
|
+
const passed = body.steps.filter((step) => step.status === "passed" && step.exitCode === 0).length;
|
|
274
|
+
const serialMs = body.steps.reduce((total, step) => total + step.durationMs, 0);
|
|
275
|
+
const wall = serialMs > body.wallClockMs ? `${formatDuration(body.wallClockMs)} (serial ${formatDuration(serialMs)})` : formatDuration(body.wallClockMs);
|
|
276
|
+
const seeds = Object.entries(body.seeds).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `${name}=${value}`).join(" ");
|
|
277
|
+
const seal_ = seal.algorithm === "hmac-sha256" ? `sealed ${seal.algorithm} key ${seal.keyId ?? "unknown"}` : `unsealed sha256 ${seal.bodySha256.slice(0, 12)}`;
|
|
278
|
+
const dirty = body.subject.tree === body.subject.commitTree ? "" : " DIRTY-TREE";
|
|
279
|
+
return [
|
|
280
|
+
`signoff ${body.verdict}${dirty}`,
|
|
281
|
+
`${passed}/${body.steps.length} steps`,
|
|
282
|
+
wall,
|
|
283
|
+
`${body.subject.repo}@${body.subject.commit.slice(0, 9)} tree ${body.subject.tree.slice(0, 9)}`,
|
|
284
|
+
seeds.length === 0 ? "seeds none" : `seeds ${seeds}`,
|
|
285
|
+
`node ${body.tooling.node} pnpm ${body.tooling.pnpm ?? "unresolved"}`,
|
|
286
|
+
seal_,
|
|
287
|
+
body.signedAt
|
|
288
|
+
].join(" \xB7 ");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/signoff/proof-attach.ts
|
|
292
|
+
var SIGNOFF_NOTES_REF = "refs/notes/signoff";
|
|
293
|
+
var SIGNOFF_NOTES_GIT_CONFIG = [
|
|
294
|
+
`git config --add remote.origin.fetch '+${SIGNOFF_NOTES_REF}:${SIGNOFF_NOTES_REF}'`,
|
|
295
|
+
`git config --add remote.origin.push '${SIGNOFF_NOTES_REF}'`,
|
|
296
|
+
`git config notes.rewriteRef '${SIGNOFF_NOTES_REF}'`,
|
|
297
|
+
"git config notes.rewrite.amend true",
|
|
298
|
+
"git config notes.rewrite.rebase true"
|
|
299
|
+
];
|
|
300
|
+
function attachSignoffProof(input) {
|
|
301
|
+
const commit = resolveCommit(input.repoDir, input.rev ?? input.proof.body.subject.commit);
|
|
302
|
+
const args = ["notes", `--ref=${SIGNOFF_NOTES_REF}`, "add"];
|
|
303
|
+
if (input.overwrite === true) args.push("-f");
|
|
304
|
+
args.push("-F", "-", commit);
|
|
305
|
+
const result = runGit(input.repoDir, args, { input: `${JSON.stringify(input.proof, null, 2)}
|
|
306
|
+
` });
|
|
307
|
+
if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr);
|
|
308
|
+
return { commit, ref: SIGNOFF_NOTES_REF };
|
|
309
|
+
}
|
|
310
|
+
function readSignoffProofNote(repoDir, rev) {
|
|
311
|
+
const commit = resolveCommit(repoDir, rev);
|
|
312
|
+
const result = runGit(repoDir, ["notes", `--ref=${SIGNOFF_NOTES_REF}`, "show", commit]);
|
|
313
|
+
if (result.status !== 0) return { found: false, commit };
|
|
314
|
+
return { found: true, proof: parseSignoffProof(result.stdout), commit, binding: "exact" };
|
|
315
|
+
}
|
|
316
|
+
function listSignoffProofs(repoDir) {
|
|
317
|
+
const listed = runGit(repoDir, ["notes", `--ref=${SIGNOFF_NOTES_REF}`, "list"]);
|
|
318
|
+
if (listed.status !== 0) return [];
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const line of listed.stdout.split("\n")) {
|
|
321
|
+
const [noteBlob, commit] = line.trim().split(/\s+/);
|
|
322
|
+
if (noteBlob === void 0 || commit === void 0) continue;
|
|
323
|
+
out.push({ commit, proof: parseSignoffProof(gitText(repoDir, ["cat-file", "blob", noteBlob])) });
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
function resolveSignoffProof(repoDir, rev) {
|
|
328
|
+
const direct = readSignoffProofNote(repoDir, rev);
|
|
329
|
+
if (direct.found) return direct;
|
|
330
|
+
const facts = readCommitFacts(repoDir, rev);
|
|
331
|
+
for (const entry of listSignoffProofs(repoDir)) {
|
|
332
|
+
if (entry.proof.body.subject.commitTree === facts.commitTree) {
|
|
333
|
+
return { found: true, proof: entry.proof, commit: facts.commit, binding: "tree-equivalent" };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { found: false, commit: facts.commit };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/signoff/proof-verify.ts
|
|
340
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
341
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
342
|
+
var SIGNOFF_REQUIRED_STEPS = {
|
|
343
|
+
"agent-app": ["install", "typecheck", "test:gates", "test", "build", "test:generated", "knip"],
|
|
344
|
+
"tax-agent": ["install", "peer-check", "typecheck", "test", "toolkit-deps", "toolkit-test", "build", "worker-startup"],
|
|
345
|
+
"legal-agent": ["install", "peer-check", "typegen", "typecheck", "test", "build:check"]
|
|
346
|
+
};
|
|
347
|
+
function requiredStepsFor(repo) {
|
|
348
|
+
const steps = SIGNOFF_REQUIRED_STEPS[repo];
|
|
349
|
+
if (steps === void 0) {
|
|
350
|
+
throw new Error(`no required-step table for repo ${JSON.stringify(repo)}; known: ${Object.keys(SIGNOFF_REQUIRED_STEPS).sort().join(", ")}`);
|
|
351
|
+
}
|
|
352
|
+
return steps;
|
|
353
|
+
}
|
|
354
|
+
function verifySignoffProof(proof, options) {
|
|
355
|
+
const failures = [];
|
|
356
|
+
const { body, seal } = proof;
|
|
357
|
+
const target = options.target;
|
|
358
|
+
if (body.proofVersion !== SIGNOFF_PROOF_VERSION) {
|
|
359
|
+
failures.push({ code: "unsupported-version", detail: `proof declares version ${body.proofVersion}; this verifier reads ${SIGNOFF_PROOF_VERSION}` });
|
|
360
|
+
}
|
|
361
|
+
const recomputed = hashProofBody(body);
|
|
362
|
+
if (recomputed !== seal.bodySha256) {
|
|
363
|
+
failures.push({ code: "body-tampered", detail: `seal claims body sha256 ${seal.bodySha256}; the body hashes to ${recomputed}` });
|
|
364
|
+
}
|
|
365
|
+
let macChecked = false;
|
|
366
|
+
if (options.key !== void 0) {
|
|
367
|
+
if (seal.mac === null) {
|
|
368
|
+
failures.push({ code: "mac-missing", detail: "a key was supplied but the proof carries no mac; it was produced unsealed" });
|
|
369
|
+
} else {
|
|
370
|
+
const expected = createHmac2("sha256", options.key).update(canonicalizeProofBody(body), "utf8").digest("hex");
|
|
371
|
+
if (macMatches(expected, seal.mac)) macChecked = true;
|
|
372
|
+
else failures.push({ code: "mac-invalid", detail: `mac does not verify under the supplied key (proof keyId ${seal.keyId ?? "none"})` });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (options.expectRepo !== void 0 && options.expectRepo !== body.subject.repo) {
|
|
376
|
+
failures.push({ code: "repo-mismatch", detail: `proof is for repo ${body.subject.repo}; ${options.expectRepo} was requested` });
|
|
377
|
+
}
|
|
378
|
+
let requiredSteps = options.requiredSteps ?? [];
|
|
379
|
+
if (options.requiredSteps === void 0) {
|
|
380
|
+
const known = SIGNOFF_REQUIRED_STEPS[body.subject.repo];
|
|
381
|
+
if (known === void 0) {
|
|
382
|
+
failures.push({ code: "unknown-repo", detail: `no required-step table for ${body.subject.repo}; a repo with no declared bar cannot be signed off` });
|
|
383
|
+
} else {
|
|
384
|
+
requiredSteps = known;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const seen = /* @__PURE__ */ new Map();
|
|
388
|
+
for (const step of body.steps) seen.set(step.id, (seen.get(step.id) ?? 0) + 1);
|
|
389
|
+
for (const [id, count] of seen) {
|
|
390
|
+
if (count > 1) failures.push({ code: "duplicate-step", detail: `step ${id} appears ${count} times; a repeated id makes coverage ambiguous` });
|
|
391
|
+
}
|
|
392
|
+
const missing = requiredSteps.filter((id) => !seen.has(id));
|
|
393
|
+
if (missing.length > 0) {
|
|
394
|
+
failures.push({ code: "missing-required-step", detail: `required step(s) never ran: ${missing.join(", ")}` });
|
|
395
|
+
}
|
|
396
|
+
const declared = new Set(body.declaredRequired);
|
|
397
|
+
const understated = requiredSteps.filter((id) => !declared.has(id));
|
|
398
|
+
if (understated.length > 0) {
|
|
399
|
+
failures.push({ code: "lowered-bar", detail: `proof declares a smaller required set than ${body.subject.repo}'s table; missing: ${understated.join(", ")}` });
|
|
400
|
+
}
|
|
401
|
+
for (const step of body.steps) {
|
|
402
|
+
if (step.status !== "passed") {
|
|
403
|
+
failures.push({ code: "step-failed", detail: `step ${step.id} is ${step.status}, not passed (exit ${step.exitCode}, ${step.command})` });
|
|
404
|
+
} else if (step.exitCode !== 0) {
|
|
405
|
+
failures.push({ code: "step-failed", detail: `step ${step.id} claims status passed but exited ${step.exitCode} (${step.command})` });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (body.verdict !== "pass") {
|
|
409
|
+
failures.push({ code: "verdict-fail", detail: `the run recorded verdict ${body.verdict}` });
|
|
410
|
+
}
|
|
411
|
+
if (body.subject.tree !== body.subject.commitTree) {
|
|
412
|
+
failures.push({
|
|
413
|
+
code: "dirty-worktree",
|
|
414
|
+
detail: `checks ran against tree ${body.subject.tree} while the commit carries ${body.subject.commitTree}; uncommitted work was in the tree`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (body.subject.commitTree !== target.commitTree) {
|
|
418
|
+
failures.push({ code: "tree-mismatch", detail: `proof covers tree ${body.subject.commitTree}; ${target.commit} carries ${target.commitTree}` });
|
|
419
|
+
}
|
|
420
|
+
const commitBinding = body.subject.commit === target.commit ? "exact" : body.subject.commitTree === target.commitTree ? "tree-equivalent" : "none";
|
|
421
|
+
if (commitBinding === "none") {
|
|
422
|
+
failures.push({ code: "commit-unbound", detail: `proof names commit ${body.subject.commit}, which is neither ${target.commit} nor its content` });
|
|
423
|
+
}
|
|
424
|
+
const signedAt = Date.parse(body.signedAt);
|
|
425
|
+
if (signedAt < Date.parse(body.subject.committedAt)) {
|
|
426
|
+
failures.push({ code: "stale-proof", detail: `signed at ${body.signedAt}, before the commit it names was written at ${body.subject.committedAt}` });
|
|
427
|
+
}
|
|
428
|
+
if (commitBinding === "tree-equivalent" && options.isAncestor(body.subject.commit, target.commit)) {
|
|
429
|
+
failures.push({
|
|
430
|
+
code: "stale-proof",
|
|
431
|
+
detail: `${body.subject.commit} is an ancestor of ${target.commit}; the tree matches only because later work was undone, and that work was never signed off`
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return { ok: failures.length === 0, commitBinding, macChecked, failures, proof, target, requiredSteps };
|
|
435
|
+
}
|
|
436
|
+
function verifySignoffAtRev(input) {
|
|
437
|
+
const lookup = resolveSignoffProof(input.repoDir, input.rev);
|
|
438
|
+
const target = readCommitFacts(input.repoDir, input.rev);
|
|
439
|
+
if (!lookup.found) return { found: false, commit: lookup.commit, hint: SIGNOFF_NOTES_GIT_CONFIG };
|
|
440
|
+
return {
|
|
441
|
+
found: true,
|
|
442
|
+
...verifySignoffProof(lookup.proof, {
|
|
443
|
+
target,
|
|
444
|
+
isAncestor: gitIsAncestor(input.repoDir),
|
|
445
|
+
key: input.key,
|
|
446
|
+
requiredSteps: input.requiredSteps,
|
|
447
|
+
expectRepo: input.expectRepo
|
|
448
|
+
})
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function verifySignoffProofFile(input) {
|
|
452
|
+
const proof = parseSignoffProof(readFileSync2(input.file, "utf8"));
|
|
453
|
+
const target = readCommitFacts(input.repoDir, input.rev);
|
|
454
|
+
return verifySignoffProof(proof, {
|
|
455
|
+
target,
|
|
456
|
+
isAncestor: gitIsAncestor(input.repoDir),
|
|
457
|
+
key: input.key,
|
|
458
|
+
requiredSteps: input.requiredSteps,
|
|
459
|
+
expectRepo: input.expectRepo
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function formatSignoffVerification(result) {
|
|
463
|
+
const head = result.ok ? `VERIFIED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} mac=${result.macChecked ? "checked" : "unchecked"}` : `REJECTED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} (${result.failures.length} failure${result.failures.length === 1 ? "" : "s"})`;
|
|
464
|
+
return [head, ...result.failures.map((failure) => ` ${failure.code}: ${failure.detail}`)].join("\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export {
|
|
468
|
+
SignoffGitError,
|
|
469
|
+
runGit,
|
|
470
|
+
gitText,
|
|
471
|
+
gitIsAncestor,
|
|
472
|
+
resolveCommit,
|
|
473
|
+
readCommitFacts,
|
|
474
|
+
computeWorktreeTree,
|
|
475
|
+
SIGNOFF_PROOF_VERSION,
|
|
476
|
+
SIGNOFF_STEP_STATUSES,
|
|
477
|
+
signoffProofStepSchema,
|
|
478
|
+
signoffProofBodySchema,
|
|
479
|
+
signoffProofSealSchema,
|
|
480
|
+
signoffProofSchema,
|
|
481
|
+
canonicalJson,
|
|
482
|
+
canonicalizeProofBody,
|
|
483
|
+
hashProofBody,
|
|
484
|
+
hashStepOutput,
|
|
485
|
+
signoffKeyId,
|
|
486
|
+
readSignoffKey,
|
|
487
|
+
sealProof,
|
|
488
|
+
macMatches,
|
|
489
|
+
collectToolingFacts,
|
|
490
|
+
readInstalledVersion,
|
|
491
|
+
tangleDependencyNames,
|
|
492
|
+
buildSignoffProof,
|
|
493
|
+
parseSignoffProof,
|
|
494
|
+
serializeSignoffProof,
|
|
495
|
+
formatSignoffSummary,
|
|
496
|
+
SIGNOFF_NOTES_REF,
|
|
497
|
+
SIGNOFF_NOTES_GIT_CONFIG,
|
|
498
|
+
attachSignoffProof,
|
|
499
|
+
readSignoffProofNote,
|
|
500
|
+
listSignoffProofs,
|
|
501
|
+
resolveSignoffProof,
|
|
502
|
+
SIGNOFF_REQUIRED_STEPS,
|
|
503
|
+
requiredStepsFor,
|
|
504
|
+
verifySignoffProof,
|
|
505
|
+
verifySignoffAtRev,
|
|
506
|
+
verifySignoffProofFile,
|
|
507
|
+
formatSignoffVerification
|
|
508
|
+
};
|
|
509
|
+
//# sourceMappingURL=chunk-N3G3FSM4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signoff/proof-git.ts","../src/signoff/proof-record.ts","../src/signoff/proof-attach.ts","../src/signoff/proof-verify.ts"],"sourcesContent":["/**\n * The git facts a sign-off proof is bound to, read straight from the object\n * database rather than reported by the process that ran the checks.\n *\n * Everything here is re-derivable by anyone holding the repository, which is\n * what makes the proof checkable: a verifier never trusts a field in the JSON,\n * it recomputes the same value with the same commands and compares.\n */\nimport { spawnSync } from 'node:child_process'\nimport { mkdtempSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\n/** A git invocation that exited non-zero, carrying stderr so the caller can act. */\nexport class SignoffGitError extends Error {\n readonly args: readonly string[]\n readonly status: number | null\n readonly stderr: string\n\n constructor(args: readonly string[], status: number | null, stderr: string) {\n super(`git ${args.join(' ')} exited ${status ?? 'null'}: ${stderr.trim()}`)\n this.name = 'SignoffGitError'\n this.args = args\n this.status = status\n this.stderr = stderr\n }\n}\n\nexport interface GitResult {\n readonly status: number | null\n readonly stdout: string\n readonly stderr: string\n}\n\n/**\n * Run git and hand back the raw result. `GIT_OPTIONAL_LOCKS=0` keeps a read\n * from touching the index while another agent works the same worktree — this\n * repo is shared by concurrent sessions.\n */\nexport function runGit(repoDir: string, args: readonly string[], options: { readonly input?: string; readonly env?: Readonly<Record<string, string>> } = {}): GitResult {\n const result = spawnSync('git', [...args], {\n cwd: repoDir,\n encoding: 'utf8',\n input: options.input,\n env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...options.env },\n maxBuffer: 128 * 1024 * 1024,\n })\n if (result.error) throw new Error(`git ${args.join(' ')} could not run: ${result.error.message}`)\n return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }\n}\n\n/** Run git, or throw. Trailing newline is stripped — every caller wants the value, not the line. */\nexport function gitText(repoDir: string, args: readonly string[], options: { readonly input?: string; readonly env?: Readonly<Record<string, string>> } = {}): string {\n const result = runGit(repoDir, args, options)\n if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr)\n return result.stdout.replace(/\\n$/, '')\n}\n\n/**\n * Is `ancestor` reachable from `descendant`? A port rather than a direct call so\n * the verifier stays a pure function over facts a caller supplies.\n */\nexport type IsAncestorFn = (ancestor: string, descendant: string) => boolean\n\nexport function gitIsAncestor(repoDir: string): IsAncestorFn {\n return (ancestor, descendant) => {\n const result = runGit(repoDir, ['merge-base', '--is-ancestor', ancestor, descendant])\n if (result.status === 0) return true\n if (result.status === 1) return false\n throw new SignoffGitError(['merge-base', '--is-ancestor', ancestor, descendant], result.status, result.stderr)\n }\n}\n\nexport interface CommitFacts {\n /** 40-hex commit id. */\n readonly commit: string\n /** 40-hex id of the tree the COMMIT points at. */\n readonly commitTree: string\n readonly parents: readonly string[]\n /** Committer date, UTC ISO-8601 with a `Z` suffix. */\n readonly committedAt: string\n}\n\n/** Resolve a revision to the commit it names, failing loud on an unknown rev. */\nexport function resolveCommit(repoDir: string, rev: string): string {\n return gitText(repoDir, ['rev-parse', '--verify', `${rev}^{commit}`])\n}\n\nexport function readCommitFacts(repoDir: string, rev: string): CommitFacts {\n const commit = resolveCommit(repoDir, rev)\n const record = gitText(repoDir, ['show', '--no-patch', '--format=%T%n%P%n%cI', commit])\n const [tree, parents, committedAt] = record.split('\\n')\n if (tree === undefined || parents === undefined || committedAt === undefined) {\n throw new Error(`git show returned an unreadable record for ${commit}: ${JSON.stringify(record)}`)\n }\n return {\n commit,\n commitTree: tree,\n parents: parents.length === 0 ? [] : parents.split(' '),\n committedAt: new Date(committedAt).toISOString(),\n }\n}\n\n/**\n * Hash the tree the checks actually ran against, including uncommitted and\n * untracked (non-ignored) files.\n *\n * This is the field that makes drift detectable. A sign-off that ran over an\n * edited worktree produces a tree id no commit carries, so it cannot verify\n * against the commit it claims — which is the intended outcome, not a bug.\n *\n * Written through a throwaway index (`GIT_INDEX_FILE`) so a concurrent agent's\n * staged work in the real index is neither read nor disturbed. `git add -A`\n * honours `.gitignore`, so `node_modules` / `dist` stay out.\n */\nexport function computeWorktreeTree(repoDir: string): string {\n const scratch = mkdtempSync(join(tmpdir(), 'agent-app-signoff-index-'))\n const indexFile = join(scratch, 'index')\n try {\n const env = { GIT_INDEX_FILE: indexFile }\n gitText(repoDir, ['add', '-A', '--'], { env })\n return gitText(repoDir, ['write-tree'], { env })\n } finally {\n rmSync(scratch, { recursive: true, force: true })\n }\n}\n","/**\n * The sign-off proof record: what a local verification run produces so that a\n * merged commit can be interrogated later.\n *\n * ## Threat model — read this before trusting a proof\n *\n * A proof defends against **accident and drift**:\n * - a check that was never run, or ran and failed, being reported as green\n * - a proof produced over a different tree than the commit carries\n * - a proof copied from one commit onto another\n * - a field edited by hand after the fact\n * - a run whose peer versions or tool versions differ from what a reader assumes\n *\n * It does **not** defend against a malicious operator. The HMAC key is a local\n * file on the same machine that runs the checks, so anyone who can run a\n * sign-off can also mint a proof for checks that never ran. There is no secret\n * server, by requirement. What the seal buys is that a proof cannot be produced\n * or altered by someone WITHOUT that key, and that a proof cannot be silently\n * retargeted, which is the whole failure class an absent CI leaves open.\n *\n * Trust in the numbers themselves comes from the steps being real commands with\n * real exit codes and an output digest — not from cryptography.\n */\nimport { spawnSync } from 'node:child_process'\nimport { createHash, createHmac, timingSafeEqual } from 'node:crypto'\nimport { readFileSync } from 'node:fs'\nimport { hostname, userInfo } from 'node:os'\nimport { join } from 'node:path'\nimport { z } from 'zod'\nimport { computeWorktreeTree, readCommitFacts } from './proof-git'\nimport type { SignoffStepStatus } from './types'\n\n/** Bumped when the canonical body shape changes; a verifier refuses versions it does not know. */\nexport const SIGNOFF_PROOF_VERSION = 1\n\nconst isoString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$/, 'must be a UTC ISO-8601 timestamp')\nconst sha1Hex = z.string().regex(/^[0-9a-f]{40}$/, 'must be a 40-hex git object id')\nconst sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, 'must be a 64-hex sha256 digest')\n\n/**\n * The runner's step outcomes, restated as proof vocabulary.\n *\n * A status is carried alongside the exit code because they answer different\n * questions and only one of them is safe on its own: a step that was `skipped`,\n * `cancelled` or `blocked` never produced an exit code at all, and defaulting\n * that to `0` is precisely how a run that did not happen reads as a pass.\n *\n * The `satisfies` and the `Exclude` below pin this list to the runner's own\n * union in BOTH directions, so a status added there fails this file's typecheck\n * instead of quietly arriving as an unmodelled string.\n */\nexport const SIGNOFF_STEP_STATUSES = ['passed', 'failed', 'skipped', 'cancelled', 'blocked'] as const satisfies readonly SignoffStepStatus[]\ntype UnmodelledStatus = Exclude<SignoffStepStatus, (typeof SIGNOFF_STEP_STATUSES)[number]>\nconst _everyRunnerStatusIsModelled: UnmodelledStatus[] = []\nvoid _everyRunnerStatusIsModelled\n\nexport const signoffProofStepSchema = z.object({\n /** Stable id a repo's required-step table refers to (`typecheck`, `test`, `knip`, …). */\n id: z.string().min(1),\n command: z.string().min(1),\n cwd: z.string().min(1),\n /** How the runner judged the step. Only `passed` can satisfy a requirement. */\n status: z.enum(SIGNOFF_STEP_STATUSES),\n exitCode: z.number().int(),\n durationMs: z.number().int().nonnegative(),\n startedAt: isoString,\n /** sha256 of the step's combined stdout+stderr. Logs are not carried; the digest is. */\n outputSha256: sha256Hex,\n})\n\nconst signoffProofPeerSchema = z.object({\n name: z.string().min(1),\n /** `null` when the package is not resolvable on disk — recorded, never guessed. */\n version: z.string().min(1).nullable(),\n})\n\nconst signoffProofSubjectSchema = z.object({\n repo: z.string().min(1),\n commit: sha1Hex,\n /** Tree the checks actually ran against, including uncommitted work. */\n tree: sha1Hex,\n /** Tree the commit itself carries. Equal to `tree` on a clean sign-off. */\n commitTree: sha1Hex,\n parents: z.array(sha1Hex),\n committedAt: isoString,\n})\n\nexport const signoffProofBodySchema = z.object({\n proofVersion: z.number().int().positive(),\n subject: signoffProofSubjectSchema,\n signedAt: isoString,\n host: z.object({\n hostname: z.string().min(1),\n platform: z.string().min(1),\n arch: z.string().min(1),\n user: z.string().min(1),\n }),\n tooling: z.object({\n node: z.string().min(1),\n pnpm: z.string().min(1).nullable(),\n peers: z.array(signoffProofPeerSchema),\n }),\n /**\n * Real elapsed time for the whole run. Recorded separately from the steps\n * because the runner schedules them as wide as their dependencies allow, so\n * the sum of step durations is the SERIAL cost and would overstate this.\n */\n wallClockMs: z.number().int().nonnegative(),\n /** Seeds fed to anything non-deterministic, so a reader can reproduce the same run. */\n seeds: z.record(z.string(), z.union([z.string(), z.number()])),\n /** The step ids this run claims were required. The verifier holds the authoritative table. */\n declaredRequired: z.array(z.string().min(1)),\n steps: z.array(signoffProofStepSchema),\n verdict: z.enum(['pass', 'fail']),\n})\n\nexport const signoffProofSealSchema = z.object({\n algorithm: z.enum(['sha256', 'hmac-sha256']),\n /** sha256 over the canonical body. Chains the seal to every field, including the commit and tree. */\n bodySha256: sha256Hex,\n /** First 12 hex of sha256(key), so a reader can tell WHICH key sealed this. */\n keyId: z.string().regex(/^[0-9a-f]{12}$/).nullable(),\n mac: sha256Hex.nullable(),\n})\n\nexport const signoffProofSchema = z.object({\n body: signoffProofBodySchema,\n seal: signoffProofSealSchema,\n})\n\nexport type SignoffProofStep = z.infer<typeof signoffProofStepSchema>\nexport type SignoffProofPeer = z.infer<typeof signoffProofPeerSchema>\nexport type SignoffProofSubject = z.infer<typeof signoffProofSubjectSchema>\nexport type SignoffProofBody = z.infer<typeof signoffProofBodySchema>\nexport type SignoffProofSeal = z.infer<typeof signoffProofSealSchema>\nexport type SignoffProof = z.infer<typeof signoffProofSchema>\n\ntype CanonicalValue = string | number | boolean | null | readonly CanonicalValue[] | { readonly [key: string]: CanonicalValue }\n\n/**\n * Deterministic JSON: object keys sorted, array order preserved, no whitespace.\n *\n * The seal is a hash over this string, so two readers must produce byte-identical\n * bytes from the same record. `undefined` throws rather than vanishing — a field\n * that silently disappears is a field the hash stops covering.\n */\nexport function canonicalJson(value: CanonicalValue): string {\n if (value === null) return 'null'\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new Error(`canonicalJson: ${String(value)} is not representable`)\n return JSON.stringify(value)\n }\n if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`\n const record = value as { readonly [key: string]: CanonicalValue }\n const keys = Object.keys(record).sort()\n const fields = keys.map((key) => {\n const entry = record[key]\n if (entry === undefined) throw new Error(`canonicalJson: field ${JSON.stringify(key)} is undefined`)\n return `${JSON.stringify(key)}:${canonicalJson(entry)}`\n })\n return `{${fields.join(',')}}`\n}\n\nexport function canonicalizeProofBody(body: SignoffProofBody): string {\n return canonicalJson(body as unknown as CanonicalValue)\n}\n\nexport function hashProofBody(body: SignoffProofBody): string {\n return createHash('sha256').update(canonicalizeProofBody(body), 'utf8').digest('hex')\n}\n\n/** The digest a runner records for a step's combined output. Shared so both halves agree. */\nexport function hashStepOutput(output: string): string {\n return createHash('sha256').update(output, 'utf8').digest('hex')\n}\n\nexport function signoffKeyId(key: Uint8Array): string {\n return createHash('sha256').update(key).digest('hex').slice(0, 12)\n}\n\n/** Read the local sign-off key. Throws when absent — an unreadable key is never a silent downgrade to unsealed. */\nexport function readSignoffKey(path: string): Uint8Array {\n const raw = readFileSync(path)\n if (raw.byteLength < 16) throw new Error(`sign-off key at ${path} is ${raw.byteLength} bytes; at least 16 are required`)\n return new Uint8Array(raw)\n}\n\nexport function sealProof(body: SignoffProofBody, key?: Uint8Array): SignoffProof {\n const canonical = canonicalizeProofBody(body)\n const bodySha256 = createHash('sha256').update(canonical, 'utf8').digest('hex')\n if (key === undefined) {\n return { body, seal: { algorithm: 'sha256', bodySha256, keyId: null, mac: null } }\n }\n return {\n body,\n seal: {\n algorithm: 'hmac-sha256',\n bodySha256,\n keyId: signoffKeyId(key),\n mac: createHmac('sha256', key).update(canonical, 'utf8').digest('hex'),\n },\n }\n}\n\n/** Constant-time comparison of two hex digests of equal length. */\nexport function macMatches(expected: string, actual: string): boolean {\n if (expected.length !== actual.length) return false\n return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex'))\n}\n\nexport interface ToolingFactsInput {\n readonly repoDir: string\n /** Package names whose resolved on-disk version belongs in the proof. */\n readonly peerNames: readonly string[]\n}\n\n/**\n * The versions that actually resolved on disk — the field CI's clean install\n * makes trustworthy and a warm local `node_modules` does not. A below-floor peer\n * is invisible to typecheck and to a green suite (it fails at the wire call), so\n * the proof records what was really there rather than what the manifest asks for.\n */\nexport function collectToolingFacts(input: ToolingFactsInput): SignoffProofBody['tooling'] {\n return {\n node: process.version,\n pnpm: readPnpmVersion(input.repoDir),\n peers: input.peerNames.map((name) => ({ name, version: readInstalledVersion(input.repoDir, name) })),\n }\n}\n\n/** `null` when pnpm is not on PATH — a missing version is recorded as missing, never as a guess. */\nfunction readPnpmVersion(repoDir: string): string | null {\n const result = spawnSync('pnpm', ['--version'], { cwd: repoDir, encoding: 'utf8' })\n if (result.error || result.status !== 0) return null\n return result.stdout.trim()\n}\n\n/** Resolved version from the consumer's own `node_modules`, or `null` when the package is not there. */\nexport function readInstalledVersion(repoDir: string, packageName: string): string | null {\n try {\n const manifest = JSON.parse(readFileSync(join(repoDir, 'node_modules', packageName, 'package.json'), 'utf8')) as { version?: unknown }\n return typeof manifest.version === 'string' ? manifest.version : null\n } catch {\n return null\n }\n}\n\n/** Every `@tangle-network/*` name the repo declares as a peer or a dependency. */\nexport function tangleDependencyNames(repoDir: string): readonly string[] {\n const manifest = JSON.parse(readFileSync(join(repoDir, 'package.json'), 'utf8')) as {\n dependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n }\n const names = new Set<string>()\n for (const block of [manifest.dependencies, manifest.peerDependencies, manifest.devDependencies]) {\n for (const name of Object.keys(block ?? {})) {\n if (name.startsWith('@tangle-network/')) names.add(name)\n }\n }\n return [...names].sort()\n}\n\nexport interface BuildSignoffProofInput {\n readonly repoDir: string\n /** Repo identity the verifier's required-step table is keyed on. */\n readonly repo: string\n /** Revision the sign-off is for; defaults to `HEAD`. */\n readonly rev?: string\n readonly steps: readonly SignoffProofStep[]\n /** Measured elapsed time for the run. Required — it is not derivable from the steps. */\n readonly wallClockMs: number\n /** Step ids this run treated as required. Checked against the verifier's table. */\n readonly declaredRequired: readonly string[]\n readonly seeds: Readonly<Record<string, string | number>>\n readonly peerNames?: readonly string[]\n readonly key?: Uint8Array\n readonly now?: Date\n}\n\nexport function buildSignoffProof(input: BuildSignoffProofInput): SignoffProof {\n const facts = readCommitFacts(input.repoDir, input.rev ?? 'HEAD')\n const body: SignoffProofBody = {\n proofVersion: SIGNOFF_PROOF_VERSION,\n subject: {\n repo: input.repo,\n commit: facts.commit,\n tree: computeWorktreeTree(input.repoDir),\n commitTree: facts.commitTree,\n parents: [...facts.parents],\n committedAt: facts.committedAt,\n },\n signedAt: (input.now ?? new Date()).toISOString(),\n wallClockMs: Math.max(0, Math.round(input.wallClockMs)),\n host: { hostname: hostname(), platform: process.platform, arch: process.arch, user: userInfo().username },\n tooling: collectToolingFacts({ repoDir: input.repoDir, peerNames: input.peerNames ?? tangleDependencyNames(input.repoDir) }),\n seeds: { ...input.seeds },\n declaredRequired: [...input.declaredRequired],\n steps: input.steps.map((step) => ({ ...step })),\n verdict: input.steps.every((step) => step.status === 'passed' && step.exitCode === 0) ? 'pass' : 'fail',\n }\n return sealProof(signoffProofBodySchema.parse(body), input.key)\n}\n\n/** Parse an untrusted proof document, failing loud on any shape the verifier cannot reason about. */\nexport function parseSignoffProof(json: string): SignoffProof {\n return signoffProofSchema.parse(JSON.parse(json))\n}\n\nexport function serializeSignoffProof(proof: SignoffProof): string {\n return `${JSON.stringify(proof, null, 2)}\\n`\n}\n\nfunction formatDuration(ms: number): string {\n const seconds = Math.round(ms / 1000)\n if (seconds < 60) return `${seconds}s`\n return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`\n}\n\n/**\n * The single line an operator pastes into a PR or a merge commit: how many steps\n * ran, how long it took, the seeds, and the verdict.\n */\nexport function formatSignoffSummary(proof: SignoffProof): string {\n const { body, seal } = proof\n const passed = body.steps.filter((step) => step.status === 'passed' && step.exitCode === 0).length\n const serialMs = body.steps.reduce((total, step) => total + step.durationMs, 0)\n // Wall clock is the number an operator is deciding on. The serial total rides\n // alongside it only when the schedule actually overlapped, so a parallel run\n // shows its saving instead of claiming one.\n const wall = serialMs > body.wallClockMs ? `${formatDuration(body.wallClockMs)} (serial ${formatDuration(serialMs)})` : formatDuration(body.wallClockMs)\n const seeds = Object.entries(body.seeds)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([name, value]) => `${name}=${value}`)\n .join(' ')\n const seal_ = seal.algorithm === 'hmac-sha256' ? `sealed ${seal.algorithm} key ${seal.keyId ?? 'unknown'}` : `unsealed sha256 ${seal.bodySha256.slice(0, 12)}`\n const dirty = body.subject.tree === body.subject.commitTree ? '' : ' DIRTY-TREE'\n return [\n `signoff ${body.verdict}${dirty}`,\n `${passed}/${body.steps.length} steps`,\n wall,\n `${body.subject.repo}@${body.subject.commit.slice(0, 9)} tree ${body.subject.tree.slice(0, 9)}`,\n seeds.length === 0 ? 'seeds none' : `seeds ${seeds}`,\n `node ${body.tooling.node} pnpm ${body.tooling.pnpm ?? 'unresolved'}`,\n seal_,\n body.signedAt,\n ].join(' · ')\n}\n","/**\n * Attaching a proof to the commit it verified.\n *\n * ## Why `git notes`, and what the two rejected carriers cost\n *\n * **A commit-message trailer** cannot work for the field that matters. The\n * trailer is part of the commit object, so writing it changes the SHA the proof\n * claims to cover — the proof would have to be produced before the commit it\n * names exists. Amending re-writes the SHA again, and GitHub's squash-merge\n * rebuilds the message from the PR body, so the trailer does not survive the one\n * transition it would need to.\n *\n * **A committed artifact directory** (`.signoff/<sha>.json`) changes the tree,\n * so the tree hash inside the proof can never cover the file carrying it. It\n * also lands in every diff, and it is exactly the \"file someone can forget\"\n * shape the requirement rules out — deleting it is a normal-looking change.\n *\n * **`git notes` wins because it is the only carrier that is addressed BY the\n * SHA without being part of it.** The note is written after the commit exists,\n * it changes neither the SHA nor the tree, and `git notes show <sha>` is a\n * definite yes/no for any commit anyone can name.\n *\n * ## The tradeoff `git notes` really carries, and how this module answers it\n *\n * Notes are attached to a commit id, and a rebase or a squash-merge produces a\n * NEW commit id, so the note does not follow by itself. Two mechanisms close\n * that gap, and both are needed:\n *\n * 1. `notes.rewriteRef` (see `SIGNOFF_NOTES_GIT_CONFIG`) makes local rebase and\n * amend copy the note forward automatically.\n * 2. `resolveSignoffProof` falls back to a **tree scan**: it reads every proof\n * under the notes ref and matches on the TREE the commit carries. A rebase\n * or a squash of one branch onto an unchanged base preserves the tree, so\n * the content that was verified is still provably the content that merged,\n * even though the SHA changed. The verifier reports which binding it used —\n * `exact` or `tree-equivalent` — and never conflates them.\n *\n * Notes are neither pushed nor fetched by default; `SIGNOFF_NOTES_GIT_CONFIG`\n * carries the refspecs that fix that, and a missing note is reported as\n * `not-found`, never as a pass.\n */\nimport { parseSignoffProof, type SignoffProof } from './proof-record'\nimport { gitText, readCommitFacts, resolveCommit, runGit, SignoffGitError } from './proof-git'\n\nexport const SIGNOFF_NOTES_REF = 'refs/notes/signoff'\n\n/**\n * The git configuration a repo needs for notes to travel. Emitted by the CLI\n * when a proof cannot be found, because the usual cause is an unconfigured repo\n * rather than an unsigned commit.\n */\nexport const SIGNOFF_NOTES_GIT_CONFIG: readonly string[] = [\n `git config --add remote.origin.fetch '+${SIGNOFF_NOTES_REF}:${SIGNOFF_NOTES_REF}'`,\n `git config --add remote.origin.push '${SIGNOFF_NOTES_REF}'`,\n `git config notes.rewriteRef '${SIGNOFF_NOTES_REF}'`,\n 'git config notes.rewrite.amend true',\n 'git config notes.rewrite.rebase true',\n]\n\nexport interface AttachSignoffProofInput {\n readonly repoDir: string\n readonly proof: SignoffProof\n /** Revision to annotate; defaults to the commit the proof names. */\n readonly rev?: string\n /** Replace an existing note. Off by default so a second sign-off cannot quietly overwrite the first. */\n readonly overwrite?: boolean\n}\n\nexport interface AttachedSignoffProof {\n readonly commit: string\n readonly ref: string\n}\n\nexport function attachSignoffProof(input: AttachSignoffProofInput): AttachedSignoffProof {\n const commit = resolveCommit(input.repoDir, input.rev ?? input.proof.body.subject.commit)\n const args = ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'add']\n if (input.overwrite === true) args.push('-f')\n args.push('-F', '-', commit)\n const result = runGit(input.repoDir, args, { input: `${JSON.stringify(input.proof, null, 2)}\\n` })\n if (result.status !== 0) throw new SignoffGitError(args, result.status, result.stderr)\n return { commit, ref: SIGNOFF_NOTES_REF }\n}\n\nexport type SignoffProofLookup =\n | { readonly found: true; readonly proof: SignoffProof; readonly commit: string; readonly binding: 'exact' | 'tree-equivalent' }\n | { readonly found: false; readonly commit: string }\n\n/** The note attached to exactly this commit, or `found: false`. Never searches. */\nexport function readSignoffProofNote(repoDir: string, rev: string): SignoffProofLookup {\n const commit = resolveCommit(repoDir, rev)\n const result = runGit(repoDir, ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'show', commit])\n if (result.status !== 0) return { found: false, commit }\n return { found: true, proof: parseSignoffProof(result.stdout), commit, binding: 'exact' }\n}\n\n/** Every proof stored under the notes ref, paired with the commit it annotates. */\nexport function listSignoffProofs(repoDir: string): readonly { readonly commit: string; readonly proof: SignoffProof }[] {\n const listed = runGit(repoDir, ['notes', `--ref=${SIGNOFF_NOTES_REF}`, 'list'])\n if (listed.status !== 0) return []\n const out: { commit: string; proof: SignoffProof }[] = []\n for (const line of listed.stdout.split('\\n')) {\n const [noteBlob, commit] = line.trim().split(/\\s+/)\n if (noteBlob === undefined || commit === undefined) continue\n out.push({ commit, proof: parseSignoffProof(gitText(repoDir, ['cat-file', 'blob', noteBlob])) })\n }\n return out\n}\n\n/**\n * Answer \"was this SHA signed off\" for any commit, including one produced by a\n * rebase or a squash-merge of the branch that was actually verified.\n *\n * Exact note first. Then the tree scan — a proof whose `commitTree` equals this\n * commit's tree verified byte-identical content, which is the strongest claim\n * available once the SHA has been rewritten. The binding is returned so a caller\n * can tell the two apart; nothing here treats them as the same thing.\n */\nexport function resolveSignoffProof(repoDir: string, rev: string): SignoffProofLookup {\n const direct = readSignoffProofNote(repoDir, rev)\n if (direct.found) return direct\n const facts = readCommitFacts(repoDir, rev)\n for (const entry of listSignoffProofs(repoDir)) {\n if (entry.proof.body.subject.commitTree === facts.commitTree) {\n return { found: true, proof: entry.proof, commit: facts.commit, binding: 'tree-equivalent' }\n }\n }\n return { found: false, commit: facts.commit }\n}\n","/**\n * `verify-proof` — the half a reader runs, on a machine that did not produce\n * the proof, to decide whether a commit was really signed off.\n *\n * The verifier never believes the document. Every claim it can re-derive, it\n * re-derives from git and compares; every claim it cannot re-derive is reported\n * as recorded, not as checked. The one field the proof is deliberately not\n * allowed to own is **which steps were required**: a run that declares its own\n * bar can declare an empty one, so the authoritative table lives here and a\n * proof declaring fewer requirements than its repo's table fails as\n * `lowered-bar`.\n */\nimport type { CommitFacts, IsAncestorFn } from './proof-git'\nimport { gitIsAncestor, readCommitFacts } from './proof-git'\nimport { readFileSync } from 'node:fs'\nimport { resolveSignoffProof, SIGNOFF_NOTES_GIT_CONFIG } from './proof-attach'\nimport { canonicalizeProofBody, hashProofBody, macMatches, parseSignoffProof, SIGNOFF_PROOF_VERSION, type SignoffProof } from './proof-record'\nimport { createHmac } from 'node:crypto'\n\n/**\n * The steps a repo's sign-off MUST cover, transcribed from each repo's CI job.\n * A runner is free to run more; it may never run fewer.\n *\n * agent-app .github/workflows/ci.yml\n * tax-agent .github/workflows/deploy.yml (the `ci` job)\n * legal-agent .github/workflows/deploy.yml (the `ci` job)\n */\nexport const SIGNOFF_REQUIRED_STEPS: Readonly<Record<string, readonly string[]>> = {\n 'agent-app': ['install', 'typecheck', 'test:gates', 'test', 'build', 'test:generated', 'knip'],\n 'tax-agent': ['install', 'peer-check', 'typecheck', 'test', 'toolkit-deps', 'toolkit-test', 'build', 'worker-startup'],\n 'legal-agent': ['install', 'peer-check', 'typegen', 'typecheck', 'test', 'build:check'],\n}\n\n/** Throws for a repo with no table — an unrecognised repo has no bar, and no bar is not a pass. */\nexport function requiredStepsFor(repo: string): readonly string[] {\n const steps = SIGNOFF_REQUIRED_STEPS[repo]\n if (steps === undefined) {\n throw new Error(`no required-step table for repo ${JSON.stringify(repo)}; known: ${Object.keys(SIGNOFF_REQUIRED_STEPS).sort().join(', ')}`)\n }\n return steps\n}\n\nexport type SignoffFailureCode =\n | 'unsupported-version'\n | 'body-tampered'\n | 'mac-missing'\n | 'mac-invalid'\n | 'unknown-repo'\n | 'repo-mismatch'\n | 'tree-mismatch'\n | 'dirty-worktree'\n | 'commit-unbound'\n | 'missing-required-step'\n | 'lowered-bar'\n | 'duplicate-step'\n | 'step-failed'\n | 'verdict-fail'\n | 'stale-proof'\n\nexport interface SignoffFailure {\n readonly code: SignoffFailureCode\n readonly detail: string\n}\n\n/** How the proof attaches to the commit that was asked about. */\nexport type SignoffCommitBinding =\n /** The proof names this exact commit. */\n | 'exact'\n /** A different commit id, but byte-identical content — a rebase or a squash of what was verified. */\n | 'tree-equivalent'\n /** Neither. The proof does not describe this commit. */\n | 'none'\n\nexport interface SignoffVerification {\n readonly ok: boolean\n readonly commitBinding: SignoffCommitBinding\n /** True only when a key was supplied AND the mac over the canonical body matched. */\n readonly macChecked: boolean\n readonly failures: readonly SignoffFailure[]\n readonly proof: SignoffProof\n readonly target: CommitFacts\n readonly requiredSteps: readonly string[]\n}\n\nexport interface VerifySignoffProofOptions {\n /** The commit the reader is asking about, read from git — never from the proof. */\n readonly target: CommitFacts\n /**\n * Reachability in the target's history. Required, because the clock cannot\n * answer staleness on its own: git records committer time to the SECOND, so a\n * proof and the commit it is being replayed onto routinely share a timestamp.\n * Ancestry is exact and clock-free.\n */\n readonly isAncestor: IsAncestorFn\n /** Local HMAC key. Omitted, the mac is reported unchecked rather than assumed good. */\n readonly key?: Uint8Array\n /** Overrides the built-in table. Supplying `[]` is a deliberate no-bar check, and says so. */\n readonly requiredSteps?: readonly string[]\n /** Repo identity the caller expects; a mismatch against the proof is a failure, not a rename. */\n readonly expectRepo?: string\n}\n\nexport function verifySignoffProof(proof: SignoffProof, options: VerifySignoffProofOptions): SignoffVerification {\n const failures: SignoffFailure[] = []\n const { body, seal } = proof\n const target = options.target\n\n if (body.proofVersion !== SIGNOFF_PROOF_VERSION) {\n failures.push({ code: 'unsupported-version', detail: `proof declares version ${body.proofVersion}; this verifier reads ${SIGNOFF_PROOF_VERSION}` })\n }\n\n const recomputed = hashProofBody(body)\n if (recomputed !== seal.bodySha256) {\n failures.push({ code: 'body-tampered', detail: `seal claims body sha256 ${seal.bodySha256}; the body hashes to ${recomputed}` })\n }\n\n let macChecked = false\n if (options.key !== undefined) {\n if (seal.mac === null) {\n failures.push({ code: 'mac-missing', detail: 'a key was supplied but the proof carries no mac; it was produced unsealed' })\n } else {\n const expected = createHmac('sha256', options.key).update(canonicalizeProofBody(body), 'utf8').digest('hex')\n if (macMatches(expected, seal.mac)) macChecked = true\n else failures.push({ code: 'mac-invalid', detail: `mac does not verify under the supplied key (proof keyId ${seal.keyId ?? 'none'})` })\n }\n }\n\n if (options.expectRepo !== undefined && options.expectRepo !== body.subject.repo) {\n failures.push({ code: 'repo-mismatch', detail: `proof is for repo ${body.subject.repo}; ${options.expectRepo} was requested` })\n }\n\n let requiredSteps: readonly string[] = options.requiredSteps ?? []\n if (options.requiredSteps === undefined) {\n const known = SIGNOFF_REQUIRED_STEPS[body.subject.repo]\n if (known === undefined) {\n failures.push({ code: 'unknown-repo', detail: `no required-step table for ${body.subject.repo}; a repo with no declared bar cannot be signed off` })\n } else {\n requiredSteps = known\n }\n }\n\n const seen = new Map<string, number>()\n for (const step of body.steps) seen.set(step.id, (seen.get(step.id) ?? 0) + 1)\n for (const [id, count] of seen) {\n if (count > 1) failures.push({ code: 'duplicate-step', detail: `step ${id} appears ${count} times; a repeated id makes coverage ambiguous` })\n }\n\n const missing = requiredSteps.filter((id) => !seen.has(id))\n if (missing.length > 0) {\n failures.push({ code: 'missing-required-step', detail: `required step(s) never ran: ${missing.join(', ')}` })\n }\n\n const declared = new Set(body.declaredRequired)\n const understated = requiredSteps.filter((id) => !declared.has(id))\n if (understated.length > 0) {\n failures.push({ code: 'lowered-bar', detail: `proof declares a smaller required set than ${body.subject.repo}'s table; missing: ${understated.join(', ')}` })\n }\n\n for (const step of body.steps) {\n if (step.status !== 'passed') {\n failures.push({ code: 'step-failed', detail: `step ${step.id} is ${step.status}, not passed (exit ${step.exitCode}, ${step.command})` })\n } else if (step.exitCode !== 0) {\n // An internally inconsistent record: the runner called it passed and the\n // process disagreed. Reported rather than resolved in either direction.\n failures.push({ code: 'step-failed', detail: `step ${step.id} claims status passed but exited ${step.exitCode} (${step.command})` })\n }\n }\n\n if (body.verdict !== 'pass') {\n failures.push({ code: 'verdict-fail', detail: `the run recorded verdict ${body.verdict}` })\n }\n\n if (body.subject.tree !== body.subject.commitTree) {\n failures.push({\n code: 'dirty-worktree',\n detail: `checks ran against tree ${body.subject.tree} while the commit carries ${body.subject.commitTree}; uncommitted work was in the tree`,\n })\n }\n\n if (body.subject.commitTree !== target.commitTree) {\n failures.push({ code: 'tree-mismatch', detail: `proof covers tree ${body.subject.commitTree}; ${target.commit} carries ${target.commitTree}` })\n }\n\n const commitBinding: SignoffCommitBinding =\n body.subject.commit === target.commit ? 'exact' : body.subject.commitTree === target.commitTree ? 'tree-equivalent' : 'none'\n if (commitBinding === 'none') {\n failures.push({ code: 'commit-unbound', detail: `proof names commit ${body.subject.commit}, which is neither ${target.commit} nor its content` })\n }\n\n // Staleness, checked two ways because neither is sufficient alone.\n //\n // The clock catches a proof back-dated relative to the commit it names. Its\n // resolution is git's, one second, so it is deliberately not the only check.\n const signedAt = Date.parse(body.signedAt)\n if (signedAt < Date.parse(body.subject.committedAt)) {\n failures.push({ code: 'stale-proof', detail: `signed at ${body.signedAt}, before the commit it names was written at ${body.subject.committedAt}` })\n }\n // Ancestry catches the case the clock cannot: a proof accepted on CONTENT\n // grounds for a LATER commit in the same history. A rebase or a squash\n // replaces the verified commit, so it is not reachable from the result — but a\n // revert restores an old tree on top of work nobody verified, and there the\n // proof's commit IS an ancestor. Same tree, different history, unverified\n // commits in between.\n if (commitBinding === 'tree-equivalent' && options.isAncestor(body.subject.commit, target.commit)) {\n failures.push({\n code: 'stale-proof',\n detail: `${body.subject.commit} is an ancestor of ${target.commit}; the tree matches only because later work was undone, and that work was never signed off`,\n })\n }\n\n return { ok: failures.length === 0, commitBinding, macChecked, failures, proof, target, requiredSteps }\n}\n\nexport interface VerifySignoffAtRevInput {\n readonly repoDir: string\n readonly rev: string\n readonly key?: Uint8Array\n readonly requiredSteps?: readonly string[]\n readonly expectRepo?: string\n}\n\nexport type SignoffLookupFailure = { readonly found: false; readonly commit: string; readonly hint: readonly string[] }\nexport type SignoffVerifyOutcome = ({ readonly found: true } & SignoffVerification) | SignoffLookupFailure\n\n/** Verify by revision: find the proof attached to that SHA (or to its content), then check it. */\nexport function verifySignoffAtRev(input: VerifySignoffAtRevInput): SignoffVerifyOutcome {\n const lookup = resolveSignoffProof(input.repoDir, input.rev)\n const target = readCommitFacts(input.repoDir, input.rev)\n if (!lookup.found) return { found: false, commit: lookup.commit, hint: SIGNOFF_NOTES_GIT_CONFIG }\n return {\n found: true,\n ...verifySignoffProof(lookup.proof, {\n target,\n isAncestor: gitIsAncestor(input.repoDir),\n key: input.key,\n requiredSteps: input.requiredSteps,\n expectRepo: input.expectRepo,\n }),\n }\n}\n\nexport interface VerifySignoffFileInput {\n readonly repoDir: string\n readonly file: string\n /**\n * The commit to check the file against. REQUIRED, and deliberately not\n * defaulted to the commit the proof names: a proof checked against its own\n * subject can never fail the commit binding, which is a check that reads as\n * strong and cannot catch anything. The reader always states what they are\n * asking about.\n */\n readonly rev: string\n readonly key?: Uint8Array\n readonly requiredSteps?: readonly string[]\n readonly expectRepo?: string\n}\n\n/** Verify a proof document on disk. The repo is still required — \"matches the tree it claims\" is not answerable without it. */\nexport function verifySignoffProofFile(input: VerifySignoffFileInput): SignoffVerification {\n const proof = parseSignoffProof(readFileSync(input.file, 'utf8'))\n const target = readCommitFacts(input.repoDir, input.rev)\n return verifySignoffProof(proof, {\n target,\n isAncestor: gitIsAncestor(input.repoDir),\n key: input.key,\n requiredSteps: input.requiredSteps,\n expectRepo: input.expectRepo,\n })\n}\n\n/** One line per failure, prefixed by its code, in the order the checks ran. */\nexport function formatSignoffVerification(result: SignoffVerification): string {\n const head = result.ok\n ? `VERIFIED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} mac=${result.macChecked ? 'checked' : 'unchecked'}`\n : `REJECTED ${result.proof.body.subject.repo}@${result.target.commit.slice(0, 9)} binding=${result.commitBinding} (${result.failures.length} failure${result.failures.length === 1 ? '' : 's'})`\n return [head, ...result.failures.map((failure) => ` ${failure.code}: ${failure.detail}`)].join('\\n')\n}\n"],"mappings":"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","names":["spawnSync","join","spawnSync","join","readFileSync","createHmac","readFileSync"]}
|