owlrunkit 0.20.0 → 0.22.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +102 -10
  3. package/docs/architecture/OWLCODA_RUNKIT_0201_RELEASE_TRUTH_AND_PROFILE_NORMALIZATION_PATCH_DECISION_20260813.md +95 -0
  4. package/docs/architecture/OWLCODA_RUNKIT_021_TEAM_DELIVERY_PHASE2_PRODUCT_DECISION_20260813.md +249 -0
  5. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +117 -12
  6. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  7. package/package.json +1 -1
  8. package/packages/attest/README.md +7 -1
  9. package/packages/attest/consumer-surface-v3.json +275 -0
  10. package/packages/attest/distribution-manifest-v1.json +24 -0
  11. package/packages/attest/schemas/canonical-candidate-identity-v1.schema.json +112 -0
  12. package/packages/attest/schemas/foreign-workspace-filesystem-identity-v1.schema.json +32 -0
  13. package/packages/attest/schemas/quick-verification-receipt-v4.schema.json +226 -0
  14. package/packages/attest/src/candidate-identity.mjs +454 -0
  15. package/packages/attest/src/index.mjs +11 -0
  16. package/packages/attest/src/quick-receipt-contract.mjs +126 -5
  17. package/packages/attest/src/quick.mjs +53 -8
  18. package/{registry-release-policy-v1.json → registry-release-policy-v2.json} +8 -9
  19. package/sbom.spdx.json +4 -4
  20. package/scripts/runkit-contract/core-contract.mjs +19 -1
  21. package/scripts/runkit-contract/fleet-skill-plan.mjs +223 -0
  22. package/scripts/runkit-contract/formal-workflow.mjs +3 -1
  23. package/scripts/runkit-contract/hook-recovery.mjs +496 -0
  24. package/scripts/runkit-contract/inspect-presentation.mjs +187 -0
  25. package/scripts/runkit-contract/install-codex-skill.mjs +1 -0
  26. package/scripts/runkit-contract/onboarding-bootstrap.mjs +15 -0
  27. package/scripts/runkit-contract/onboarding-doctor.mjs +378 -9
  28. package/scripts/runkit-contract/profile-onboarding.mjs +6 -0
  29. package/scripts/runkit-contract/project-cli-resolver.mjs +17 -1
  30. package/scripts/runkit-contract/quick-metrics.mjs +9 -5
  31. package/scripts/runkit-contract/quick-verify.mjs +108 -24
  32. package/scripts/runkit-contract/registry-adoption-gate.mjs +12 -3
  33. package/scripts/runkit-contract/registry-adoption.mjs +10 -5
  34. package/scripts/runkit-contract/runkit-bootstrap.mjs +1 -0
  35. package/scripts/runkit-contract/runkit-cli.mjs +332 -30
  36. package/scripts/runkit-contract/schemas/delivery-lifecycle-v1.schema.json +91 -0
  37. package/scripts/runkit-contract/schemas/external-gate-v1.schema.json +68 -0
  38. package/scripts/runkit-contract/schemas/fleet-skill-upgrade-plan-v1.schema.json +140 -0
  39. package/scripts/runkit-contract/schemas/hook-recovery-v1.schema.json +592 -0
  40. package/scripts/runkit-contract/schemas/inspect-compact-v1.schema.json +93 -0
  41. package/scripts/runkit-contract/schemas/inspect-compact-v2.schema.json +122 -0
  42. package/scripts/runkit-contract/schemas/team-delivery-status-v2.schema.json +219 -0
  43. package/scripts/runkit-contract/schemas/team-delivery-status-v3.schema.json +113 -0
  44. package/scripts/runkit-contract/schemas/team-project-event-v6.schema.json +82 -0
  45. package/scripts/runkit-contract/schemas/team-project-event-v7.schema.json +80 -0
  46. package/scripts/runkit-contract/schemas/team-project-status-v4.schema.json +208 -0
  47. package/scripts/runkit-contract/schemas/team-recommendation-v2.schema.json +120 -0
  48. package/scripts/runkit-contract/sync-release-identity.mjs +87 -0
  49. package/scripts/runkit-contract/team-delivery.mjs +203 -3
  50. package/scripts/runkit-contract/team-project.mjs +1032 -11
@@ -0,0 +1,454 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ closeSync,
5
+ constants,
6
+ fstatSync,
7
+ lstatSync,
8
+ mkdtempSync,
9
+ openSync,
10
+ readSync,
11
+ readdirSync,
12
+ readlinkSync,
13
+ realpathSync,
14
+ rmSync,
15
+ } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import path from "node:path";
18
+
19
+ import {
20
+ canonicalJson,
21
+ hasExactKeys,
22
+ isSha256Ref,
23
+ sha256Bytes,
24
+ sha256Canonical,
25
+ } from "./formal.mjs";
26
+
27
+ export const CANDIDATE_LEDGER_ALGORITHM_V1 = "owlcoda-canonical-candidate-ledger-v1";
28
+ export const FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1 = "owlcoda-foreign-workspace-ledger-v1";
29
+
30
+ const MAX_LEDGER_FILES = 100_000;
31
+ const MAX_LEDGER_BYTES = 2 * 1024 * 1024 * 1024;
32
+ const STATUS_ARGV = Object.freeze([
33
+ "git", "-c", "color.ui=false", "-c", "core.abbrev=40",
34
+ "status", "--porcelain=v1", "-z", "--untracked-files=all",
35
+ ]);
36
+ const TRANSPORT_DIFF_ARGV = Object.freeze([
37
+ "git", "-c", "color.ui=false", "-c", "core.abbrev=40",
38
+ "diff", "--full-index", "--binary", "--no-ext-diff", "--no-textconv", "HEAD",
39
+ ]);
40
+ const GIT_IDENTITY_ENVIRONMENT = Object.freeze({
41
+ GIT_OPTIONAL_LOCKS: "0",
42
+ LC_ALL: "C",
43
+ });
44
+
45
+ function compareCodeUnits(left, right) {
46
+ return left < right ? -1 : left > right ? 1 : 0;
47
+ }
48
+
49
+ function git(root, argv) {
50
+ return execFileSync(argv[0], argv.slice(1), {
51
+ cwd: root,
52
+ encoding: "utf8",
53
+ maxBuffer: 64 * 1024 * 1024,
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ env: {
56
+ ...process.env,
57
+ ...GIT_IDENTITY_ENVIRONMENT,
58
+ },
59
+ });
60
+ }
61
+
62
+ function sameFileIdentity(left, right) {
63
+ return left.dev === right.dev
64
+ && left.ino === right.ino
65
+ && left.mode === right.mode
66
+ && left.size === right.size
67
+ && left.mtimeMs === right.mtimeMs
68
+ && left.ctimeMs === right.ctimeMs;
69
+ }
70
+
71
+ function hashRegularFile(absolute, expectedStat, maximumBytes = MAX_LEDGER_BYTES) {
72
+ if (!expectedStat.isFile() || expectedStat.size > maximumBytes) {
73
+ throw new Error(`Candidate file exceeds the bounded ledger limit: ${absolute}`);
74
+ }
75
+ const descriptor = openSync(absolute, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
76
+ try {
77
+ const before = fstatSync(descriptor);
78
+ if (!before.isFile() || !sameFileIdentity(before, expectedStat)) {
79
+ throw new Error(`Candidate file identity changed before hashing: ${absolute}`);
80
+ }
81
+ const hash = createHash("sha256");
82
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
83
+ let sizeBytes = 0;
84
+ while (true) {
85
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.byteLength, null);
86
+ if (bytesRead === 0) break;
87
+ sizeBytes += bytesRead;
88
+ if (sizeBytes > maximumBytes) {
89
+ throw new Error(`Candidate file exceeds the bounded ledger limit: ${absolute}`);
90
+ }
91
+ hash.update(buffer.subarray(0, bytesRead));
92
+ }
93
+ const after = fstatSync(descriptor);
94
+ const current = lstatSync(absolute);
95
+ if (!sameFileIdentity(before, after) || !sameFileIdentity(after, current)) {
96
+ throw new Error(`Candidate file changed while hashing: ${absolute}`);
97
+ }
98
+ return { sizeBytes, sha256: `sha256:${hash.digest("hex")}` };
99
+ } finally {
100
+ closeSync(descriptor);
101
+ }
102
+ }
103
+
104
+ function captureTransportDiff(root) {
105
+ const scratch = realpathSync(mkdtempSync(path.join(tmpdir(), "owlrunkit-candidate-diff-")));
106
+ const output = path.join(scratch, "candidate.diff");
107
+ try {
108
+ const descriptor = openSync(output, "wx", 0o600);
109
+ try {
110
+ execFileSync(TRANSPORT_DIFF_ARGV[0], TRANSPORT_DIFF_ARGV.slice(1), {
111
+ cwd: root,
112
+ maxBuffer: 64 * 1024 * 1024,
113
+ stdio: ["ignore", descriptor, "pipe"],
114
+ env: { ...process.env, ...GIT_IDENTITY_ENVIRONMENT },
115
+ });
116
+ } finally {
117
+ closeSync(descriptor);
118
+ }
119
+ return hashRegularFile(output, lstatSync(output));
120
+ } finally {
121
+ rmSync(scratch, { recursive: true, force: true });
122
+ }
123
+ }
124
+
125
+ function canonicalGitRoot(requestedRoot) {
126
+ const root = realpathSync(requestedRoot);
127
+ const stat = lstatSync(root);
128
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
129
+ throw new Error("Candidate workspace must be a real directory.");
130
+ }
131
+ const gitRoot = realpathSync(git(root, ["git", "rev-parse", "--show-toplevel"]).trim());
132
+ if (gitRoot !== root) {
133
+ throw new Error("Candidate workspace must be the Git worktree top-level.");
134
+ }
135
+ return root;
136
+ }
137
+
138
+ function normalizeGitPath(value) {
139
+ return value.split(path.sep).join("/");
140
+ }
141
+
142
+ function safeRelativePath(value) {
143
+ if (typeof value !== "string" || value.length === 0 || path.isAbsolute(value)) return false;
144
+ if (value.includes("\\") || value.includes("\0")) return false;
145
+ return value.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== "..");
146
+ }
147
+
148
+ function withinRoot(root, candidate) {
149
+ const relative = path.relative(root, candidate);
150
+ return relative === ""
151
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
152
+ }
153
+
154
+ function changedRecords(root) {
155
+ const raw = git(root, STATUS_ARGV);
156
+ const fields = raw.split("\0");
157
+ const records = [];
158
+ for (let index = 0; index < fields.length; index += 1) {
159
+ const field = fields[index];
160
+ if (!field) continue;
161
+ const status = field.slice(0, 2);
162
+ const currentPath = normalizeGitPath(field.slice(3));
163
+ let previousPath = null;
164
+ if (status.includes("R") || status.includes("C")) {
165
+ previousPath = normalizeGitPath(fields[index + 1] ?? "");
166
+ index += 1;
167
+ }
168
+ if (!safeRelativePath(currentPath) || previousPath !== null && !safeRelativePath(previousPath)) {
169
+ throw new Error("Git status contains an unsafe candidate path.");
170
+ }
171
+ records.push({ status, path: currentPath, previousPath });
172
+ }
173
+ return records.sort((left, right) => (
174
+ compareCodeUnits(left.path, right.path)
175
+ || compareCodeUnits(left.previousPath ?? "", right.previousPath ?? "")
176
+ ));
177
+ }
178
+
179
+ function fileLedgerEntry(root, record) {
180
+ const deleted = record.status.includes("D");
181
+ const operation = record.status.includes("R")
182
+ ? "renamed"
183
+ : record.status.includes("C")
184
+ ? "copied"
185
+ : deleted
186
+ ? "deleted"
187
+ : record.status === "??" || record.status.includes("A")
188
+ ? "added"
189
+ : "modified";
190
+ if (deleted) {
191
+ return {
192
+ path: record.path,
193
+ previousPath: record.previousPath,
194
+ operation,
195
+ status: record.status,
196
+ kind: null,
197
+ mode: null,
198
+ sizeBytes: null,
199
+ sha256: null,
200
+ };
201
+ }
202
+ const absolute = path.resolve(root, ...record.path.split("/"));
203
+ if (!withinRoot(root, absolute)) throw new Error(`Candidate path escapes workspace: ${record.path}`);
204
+ const stat = lstatSync(absolute);
205
+ let kind;
206
+ let material;
207
+ if (stat.isSymbolicLink()) {
208
+ kind = "symlink";
209
+ const bytes = Buffer.from(readlinkSync(absolute), "utf8");
210
+ material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
211
+ } else if (stat.isFile()) {
212
+ kind = "file";
213
+ material = hashRegularFile(absolute, stat);
214
+ } else if (stat.isDirectory()) {
215
+ kind = "gitlink";
216
+ const bytes = Buffer.from(git(absolute, ["git", "rev-parse", "HEAD"]).trim(), "utf8");
217
+ material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
218
+ } else {
219
+ throw new Error(`Candidate path is not a supported file type: ${record.path}`);
220
+ }
221
+ return {
222
+ path: record.path,
223
+ previousPath: record.previousPath,
224
+ operation,
225
+ status: record.status,
226
+ kind,
227
+ mode: stat.mode & 0o777,
228
+ ...material,
229
+ };
230
+ }
231
+
232
+ function ledgerBinding(ledger) {
233
+ const bytes = Buffer.from(canonicalJson(ledger), "utf8");
234
+ return {
235
+ ledgerByteLength: bytes.byteLength,
236
+ ledgerSha256: sha256Bytes(bytes),
237
+ };
238
+ }
239
+
240
+ function gitVersion() {
241
+ return execFileSync("git", ["--version"], {
242
+ encoding: "utf8",
243
+ stdio: ["ignore", "pipe", "pipe"],
244
+ env: { ...process.env, LC_ALL: "C" },
245
+ }).trim();
246
+ }
247
+
248
+ export function captureCanonicalCandidateIdentityV1(workspaceRoot) {
249
+ const root = canonicalGitRoot(workspaceRoot);
250
+ const ledger = [];
251
+ let totalBytes = 0;
252
+ for (const record of changedRecords(root)) {
253
+ const entry = fileLedgerEntry(root, record);
254
+ totalBytes += entry.sizeBytes ?? 0;
255
+ ledger.push(entry);
256
+ if (ledger.length > MAX_LEDGER_FILES || totalBytes > MAX_LEDGER_BYTES) {
257
+ throw new Error("Candidate exceeds the bounded ledger limit.");
258
+ }
259
+ }
260
+ const binding = ledgerBinding(ledger);
261
+ const headCommit = git(root, ["git", "rev-parse", "HEAD"]).trim();
262
+ const trackedTree = git(root, ["git", "rev-parse", "HEAD^{tree}"]).trim();
263
+ const transport = captureTransportDiff(root);
264
+ const candidateFingerprint = sha256Canonical({
265
+ manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
266
+ headCommit,
267
+ trackedTree,
268
+ ledgerSha256: binding.ledgerSha256,
269
+ });
270
+ return {
271
+ schemaVersion: "OwlCodaRunKitCanonicalCandidateIdentityV1",
272
+ manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
273
+ workspaceRoot: root,
274
+ headCommit,
275
+ trackedTree,
276
+ gitVersion: gitVersion(),
277
+ discoveryCommand: [...STATUS_ARGV],
278
+ discoveryEnvironment: { ...GIT_IDENTITY_ENVIRONMENT },
279
+ ledger,
280
+ ...binding,
281
+ transportDiff: {
282
+ exactArgv: [...TRANSPORT_DIFF_ARGV],
283
+ environment: { ...GIT_IDENTITY_ENVIRONMENT },
284
+ sizeBytes: transport.sizeBytes,
285
+ sha256: transport.sha256,
286
+ identityRole: "transport_only",
287
+ },
288
+ candidateFingerprint,
289
+ authorizationGranted: false,
290
+ };
291
+ }
292
+
293
+ function excludedFilesystemPath(relativePath) {
294
+ return relativePath === ".git"
295
+ || relativePath.startsWith(".git/");
296
+ }
297
+
298
+ export function captureForeignWorkspaceFilesystemIdentityV1(workspaceRoot) {
299
+ const root = canonicalGitRoot(workspaceRoot);
300
+ const ledger = [];
301
+ let totalFileBytes = 0;
302
+ const visit = (directory, prefix) => {
303
+ for (const name of readdirSync(directory).sort(compareCodeUnits)) {
304
+ const relativePath = prefix ? `${prefix}/${name}` : name;
305
+ if (excludedFilesystemPath(relativePath)) continue;
306
+ const absolute = path.join(directory, name);
307
+ const stat = lstatSync(absolute);
308
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
309
+ ledger.push({
310
+ path: relativePath,
311
+ kind: "directory",
312
+ mode: stat.mode & 0o777,
313
+ sizeBytes: 0,
314
+ sha256: null,
315
+ });
316
+ visit(absolute, relativePath);
317
+ } else {
318
+ let kind;
319
+ let material;
320
+ if (stat.isSymbolicLink()) {
321
+ kind = "symlink";
322
+ const bytes = Buffer.from(readlinkSync(absolute), "utf8");
323
+ material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
324
+ } else if (stat.isFile()) {
325
+ kind = "file";
326
+ if (totalFileBytes + stat.size > MAX_LEDGER_BYTES) {
327
+ throw new Error("Foreign workspace exceeds the bounded zero-write ledger limit.");
328
+ }
329
+ material = hashRegularFile(absolute, stat, MAX_LEDGER_BYTES - totalFileBytes);
330
+ } else {
331
+ throw new Error(`Foreign workspace contains an unsupported file type: ${relativePath}`);
332
+ }
333
+ totalFileBytes += material.sizeBytes;
334
+ ledger.push({
335
+ path: relativePath,
336
+ kind,
337
+ mode: stat.mode & 0o777,
338
+ ...material,
339
+ });
340
+ }
341
+ if (ledger.length > MAX_LEDGER_FILES || totalFileBytes > MAX_LEDGER_BYTES) {
342
+ throw new Error("Foreign workspace exceeds the bounded zero-write ledger limit.");
343
+ }
344
+ }
345
+ };
346
+ visit(root, "");
347
+ ledger.sort((left, right) => compareCodeUnits(left.path, right.path));
348
+ const binding = ledgerBinding(ledger);
349
+ return {
350
+ schemaVersion: "OwlCodaRunKitForeignWorkspaceFilesystemIdentityV1",
351
+ manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
352
+ workspaceRoot: root,
353
+ excludedRoots: [".git"],
354
+ entryCount: ledger.length,
355
+ totalFileBytes,
356
+ ...binding,
357
+ filesystemFingerprint: sha256Canonical({
358
+ manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
359
+ ledgerSha256: binding.ledgerSha256,
360
+ entryCount: ledger.length,
361
+ totalFileBytes,
362
+ }),
363
+ };
364
+ }
365
+
366
+ function validLedgerEntry(value) {
367
+ return hasExactKeys(value, [
368
+ "path", "previousPath", "operation", "status", "kind", "mode", "sizeBytes", "sha256",
369
+ ])
370
+ && safeRelativePath(value.path)
371
+ && (value.previousPath === null || safeRelativePath(value.previousPath))
372
+ && ["added", "copied", "deleted", "modified", "renamed"].includes(value.operation)
373
+ && typeof value.status === "string"
374
+ && value.status.length === 2
375
+ && (value.kind === null || ["file", "gitlink", "symlink"].includes(value.kind))
376
+ && (value.mode === null || Number.isInteger(value.mode) && value.mode >= 0 && value.mode <= 0o777)
377
+ && (value.sizeBytes === null || Number.isInteger(value.sizeBytes) && value.sizeBytes >= 0)
378
+ && (value.sha256 === null || isSha256Ref(value.sha256));
379
+ }
380
+
381
+ export function validCanonicalCandidateIdentityV1(value) {
382
+ if (!hasExactKeys(value, [
383
+ "schemaVersion", "manifestAlgorithmId", "workspaceRoot", "headCommit", "trackedTree",
384
+ "gitVersion", "discoveryCommand", "discoveryEnvironment", "ledger", "ledgerByteLength", "ledgerSha256",
385
+ "transportDiff", "candidateFingerprint", "authorizationGranted",
386
+ ])) return false;
387
+ if (
388
+ value.schemaVersion !== "OwlCodaRunKitCanonicalCandidateIdentityV1"
389
+ || value.manifestAlgorithmId !== CANDIDATE_LEDGER_ALGORITHM_V1
390
+ || typeof value.workspaceRoot !== "string"
391
+ || !path.isAbsolute(value.workspaceRoot)
392
+ || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value.headCommit)
393
+ || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value.trackedTree)
394
+ || typeof value.gitVersion !== "string"
395
+ || JSON.stringify(value.discoveryCommand) !== JSON.stringify(STATUS_ARGV)
396
+ || !hasExactKeys(value.discoveryEnvironment, ["GIT_OPTIONAL_LOCKS", "LC_ALL"])
397
+ || value.discoveryEnvironment.GIT_OPTIONAL_LOCKS !== "0"
398
+ || value.discoveryEnvironment.LC_ALL !== "C"
399
+ || !Array.isArray(value.ledger)
400
+ || !value.ledger.every(validLedgerEntry)
401
+ || !isSha256Ref(value.ledgerSha256)
402
+ || !isSha256Ref(value.candidateFingerprint)
403
+ || value.authorizationGranted !== false
404
+ || !hasExactKeys(value.transportDiff, ["exactArgv", "environment", "sizeBytes", "sha256", "identityRole"])
405
+ || JSON.stringify(value.transportDiff.exactArgv) !== JSON.stringify(TRANSPORT_DIFF_ARGV)
406
+ || !hasExactKeys(value.transportDiff.environment, ["GIT_OPTIONAL_LOCKS", "LC_ALL"])
407
+ || value.transportDiff.environment.GIT_OPTIONAL_LOCKS !== "0"
408
+ || value.transportDiff.environment.LC_ALL !== "C"
409
+ || !Number.isInteger(value.transportDiff.sizeBytes)
410
+ || value.transportDiff.sizeBytes < 0
411
+ || !isSha256Ref(value.transportDiff.sha256)
412
+ || value.transportDiff.identityRole !== "transport_only"
413
+ ) return false;
414
+ const sorted = [...value.ledger].sort((left, right) => (
415
+ compareCodeUnits(left.path, right.path)
416
+ || compareCodeUnits(left.previousPath ?? "", right.previousPath ?? "")
417
+ ));
418
+ const binding = ledgerBinding(value.ledger);
419
+ return canonicalJson(sorted) === canonicalJson(value.ledger)
420
+ && binding.ledgerByteLength === value.ledgerByteLength
421
+ && binding.ledgerSha256 === value.ledgerSha256
422
+ && value.candidateFingerprint === sha256Canonical({
423
+ manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
424
+ headCommit: value.headCommit,
425
+ trackedTree: value.trackedTree,
426
+ ledgerSha256: value.ledgerSha256,
427
+ });
428
+ }
429
+
430
+ export function validForeignWorkspaceFilesystemIdentityV1(value) {
431
+ return hasExactKeys(value, [
432
+ "schemaVersion", "manifestAlgorithmId", "workspaceRoot", "excludedRoots", "entryCount", "totalFileBytes",
433
+ "ledgerByteLength", "ledgerSha256", "filesystemFingerprint",
434
+ ])
435
+ && value.schemaVersion === "OwlCodaRunKitForeignWorkspaceFilesystemIdentityV1"
436
+ && value.manifestAlgorithmId === FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1
437
+ && typeof value.workspaceRoot === "string"
438
+ && path.isAbsolute(value.workspaceRoot)
439
+ && JSON.stringify(value.excludedRoots) === JSON.stringify([".git"])
440
+ && Number.isInteger(value.entryCount)
441
+ && value.entryCount >= 0
442
+ && Number.isInteger(value.totalFileBytes)
443
+ && value.totalFileBytes >= 0
444
+ && Number.isInteger(value.ledgerByteLength)
445
+ && value.ledgerByteLength >= 0
446
+ && isSha256Ref(value.ledgerSha256)
447
+ && isSha256Ref(value.filesystemFingerprint)
448
+ && value.filesystemFingerprint === sha256Canonical({
449
+ manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
450
+ ledgerSha256: value.ledgerSha256,
451
+ entryCount: value.entryCount,
452
+ totalFileBytes: value.totalFileBytes,
453
+ });
454
+ }
@@ -29,6 +29,12 @@ import {
29
29
  parseOfflineAttestationBundle,
30
30
  } from "./offline-bundle.mjs";
31
31
  import { captureWorkspaceSnapshot } from "./workspace-snapshot.mjs";
32
+ import {
33
+ captureCanonicalCandidateIdentityV1,
34
+ captureForeignWorkspaceFilesystemIdentityV1,
35
+ validCanonicalCandidateIdentityV1,
36
+ validForeignWorkspaceFilesystemIdentityV1,
37
+ } from "./candidate-identity.mjs";
32
38
 
33
39
  function readSubject(subjectPath) {
34
40
  const { absolutePath, bytes } = readFileBytesBounded(subjectPath);
@@ -45,6 +51,7 @@ export function attestFile({ subjectPath, workspaceRoot }) {
45
51
  "OwlCodaQuickVerificationReceiptV1",
46
52
  "OwlCodaQuickVerificationReceiptV2",
47
53
  "OwlCodaQuickVerificationReceiptV3",
54
+ "OwlCodaQuickVerificationReceiptV4",
48
55
  ]).has(subject.value.schemaVersion)) {
49
56
  const details = attestQuickReceiptDetailsFromBytes({
50
57
  receiptPath: subject.absolutePath,
@@ -99,6 +106,8 @@ export {
99
106
  attestQuickReceipt,
100
107
  attestQuickReceiptDetails,
101
108
  canonicalJson,
109
+ captureCanonicalCandidateIdentityV1,
110
+ captureForeignWorkspaceFilesystemIdentityV1,
102
111
  captureWorkspaceSnapshot,
103
112
  createAttestationRef,
104
113
  createOfflineAttestationBundle,
@@ -113,5 +122,7 @@ export {
113
122
  sha256Bytes,
114
123
  sha256Canonical,
115
124
  validQuickReceiptShape,
125
+ validCanonicalCandidateIdentityV1,
126
+ validForeignWorkspaceFilesystemIdentityV1,
116
127
  verifyBundle,
117
128
  };
@@ -3,6 +3,10 @@ import {
3
3
  isSha256Ref,
4
4
  sha256Canonical,
5
5
  } from "./formal.mjs";
6
+ import {
7
+ validCanonicalCandidateIdentityV1,
8
+ validForeignWorkspaceFilesystemIdentityV1,
9
+ } from "./candidate-identity.mjs";
6
10
 
7
11
  const RECEIPT_REQUIRED_KEYS = [
8
12
  "schemaVersion",
@@ -52,6 +56,10 @@ const V3_RECEIPT_ISSUES = new Set([
52
56
  "consumer_source_mutated_during_verification",
53
57
  "quick_isolation_cleanup_failed",
54
58
  ]);
59
+ const V4_RECEIPT_ISSUES = new Set([
60
+ ...V3_RECEIPT_ISSUES,
61
+ "foreign_workspace_mutated_during_verification",
62
+ ]);
55
63
  const V3_MUTATION_DECISIONS = new Set([
56
64
  "source_unchanged",
57
65
  "invalidated_by_command_source_mutation",
@@ -61,6 +69,10 @@ const V3_MUTATION_DECISIONS = new Set([
61
69
  "invalidated_by_isolation_cleanup_failure",
62
70
  "invalidated_by_multiple_mutations",
63
71
  ]);
72
+ const V4_MUTATION_DECISIONS = new Set([
73
+ ...V3_MUTATION_DECISIONS,
74
+ "invalidated_by_foreign_workspace_mutation",
75
+ ]);
64
76
 
65
77
  function validDateTime(value) {
66
78
  return typeof value === "string"
@@ -237,6 +249,48 @@ function validMutationClasses(value) {
237
249
  && ["not_isolated", "none", "changed_and_discarded"].includes(value.consumerIgnoredArtifacts);
238
250
  }
239
251
 
252
+ function validForeignMutationClasses(value) {
253
+ return hasExactKeys(value, [
254
+ "workspaceSource",
255
+ "boundIgnoredPaths",
256
+ "dependencyEnvironment",
257
+ "consumerSource",
258
+ "consumerIgnoredArtifacts",
259
+ "foreignTarget",
260
+ ])
261
+ && ["unchanged", "changed"].includes(value.workspaceSource)
262
+ && ["not_bound", "unchanged", "changed"].includes(value.boundIgnoredPaths)
263
+ && ["not_bound", "unchanged", "changed"].includes(value.dependencyEnvironment)
264
+ && ["unchanged", "changed"].includes(value.consumerSource)
265
+ && ["none", "changed_and_discarded"].includes(value.consumerIgnoredArtifacts)
266
+ && ["unchanged", "changed"].includes(value.foreignTarget);
267
+ }
268
+
269
+ function validForeignWorkspace(value, sourceWorkspaceRoot) {
270
+ if (!hasExactKeys(value, [
271
+ "candidateBefore",
272
+ "candidateAfter",
273
+ "filesystemBefore",
274
+ "filesystemAfter",
275
+ "zeroWriteObserved",
276
+ ])) return false;
277
+ if (
278
+ !validCanonicalCandidateIdentityV1(value.candidateBefore)
279
+ || !validCanonicalCandidateIdentityV1(value.candidateAfter)
280
+ || !validForeignWorkspaceFilesystemIdentityV1(value.filesystemBefore)
281
+ || !validForeignWorkspaceFilesystemIdentityV1(value.filesystemAfter)
282
+ || value.candidateBefore.workspaceRoot !== sourceWorkspaceRoot
283
+ || value.candidateAfter.workspaceRoot !== sourceWorkspaceRoot
284
+ || value.filesystemBefore.workspaceRoot !== sourceWorkspaceRoot
285
+ || value.filesystemAfter.workspaceRoot !== sourceWorkspaceRoot
286
+ ) return false;
287
+ const unchanged = value.candidateBefore.candidateFingerprint
288
+ === value.candidateAfter.candidateFingerprint
289
+ && value.filesystemBefore.filesystemFingerprint
290
+ === value.filesystemAfter.filesystemFingerprint;
291
+ return value.zeroWriteObserved === unchanged;
292
+ }
293
+
240
294
  function validV3MutationTruth(receipt) {
241
295
  const workspaceSourceChanged = sourceStateFingerprint(receipt.workspaceBefore)
242
296
  !== sourceStateFingerprint(receipt.workspaceAfter);
@@ -283,17 +337,66 @@ function validV3MutationTruth(receipt) {
283
337
  && JSON.stringify([...receipt.issueCodes].sort()) === JSON.stringify(expectedIssues);
284
338
  }
285
339
 
340
+ function validV4MutationTruth(receipt) {
341
+ const workspaceSourceChanged = sourceStateFingerprint(receipt.workspaceBefore)
342
+ !== sourceStateFingerprint(receipt.workspaceAfter);
343
+ const boundIgnored = receipt.workspaceBefore.ignoredPathsBound === true;
344
+ const boundIgnoredChanged = boundIgnored
345
+ && receipt.workspaceBefore.ignoredPathsFingerprint
346
+ !== receipt.workspaceAfter.ignoredPathsFingerprint;
347
+ const isolation = receipt.executionIsolation;
348
+ const consumerSourceChanged = isolation.consumerDelta.sourcePaths.length > 0;
349
+ const consumerIgnoredChanged = isolation.consumerDelta.ignoredPaths.length > 0;
350
+ const dependencyBound = isolation.dependencyEnvironment !== null;
351
+ const dependencyChanged = dependencyBound
352
+ && (isolation.dependencyEnvironmentAfter === null
353
+ || isolation.dependencyEnvironmentAfter.fingerprint
354
+ !== isolation.dependencyEnvironment.fingerprint);
355
+ const cleanupFailed = isolation.cleanupStatus === "cleanup_failed";
356
+ const foreignChanged = receipt.foreignWorkspace.zeroWriteObserved !== true;
357
+ if (receipt.mutationClasses.workspaceSource !== (workspaceSourceChanged ? "changed" : "unchanged")
358
+ || receipt.mutationClasses.boundIgnoredPaths !== (boundIgnored
359
+ ? boundIgnoredChanged ? "changed" : "unchanged"
360
+ : "not_bound")
361
+ || receipt.mutationClasses.dependencyEnvironment !== (dependencyBound
362
+ ? dependencyChanged ? "changed" : "unchanged"
363
+ : "not_bound")
364
+ || receipt.mutationClasses.consumerSource !== (consumerSourceChanged ? "changed" : "unchanged")
365
+ || receipt.mutationClasses.consumerIgnoredArtifacts !== (consumerIgnoredChanged
366
+ ? "changed_and_discarded" : "none")
367
+ || receipt.mutationClasses.foreignTarget !== (foreignChanged ? "changed" : "unchanged")) {
368
+ return false;
369
+ }
370
+ const facts = [
371
+ [workspaceSourceChanged, "invalidated_by_command_source_mutation", "source_mutated_during_verification"],
372
+ [boundIgnoredChanged, "invalidated_by_bound_ignored_mutation", "bound_ignored_path_mutated_during_verification"],
373
+ [dependencyChanged, "invalidated_by_dependency_environment_mutation", "dependency_environment_mutated_during_verification"],
374
+ [consumerSourceChanged, "invalidated_by_consumer_source_mutation", "consumer_source_mutated_during_verification"],
375
+ [cleanupFailed, "invalidated_by_isolation_cleanup_failure", "quick_isolation_cleanup_failed"],
376
+ [foreignChanged, "invalidated_by_foreign_workspace_mutation", "foreign_workspace_mutated_during_verification"],
377
+ ].filter(([active]) => active);
378
+ const expectedDecision = facts.length === 0
379
+ ? "source_unchanged"
380
+ : facts.length === 1 ? facts[0][1] : "invalidated_by_multiple_mutations";
381
+ const expectedIssues = facts.map(([, , issue]) => issue).sort();
382
+ return receipt.mutationDecision === expectedDecision
383
+ && JSON.stringify([...receipt.issueCodes].sort()) === JSON.stringify(expectedIssues);
384
+ }
385
+
286
386
  export function validQuickReceiptShape(receipt) {
287
387
  const receiptV1 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV1";
288
388
  const receiptV2 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV2";
289
389
  const receiptV3 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV3";
290
- const requiredKeys = receiptV3
390
+ const receiptV4 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV4";
391
+ const requiredKeys = receiptV4
392
+ ? [...RECEIPT_REQUIRED_KEYS, "controllerWorkspaceRoot", "sourceWorkspaceRoot", "executionIsolation", "mutationClasses", "foreignWorkspace"]
393
+ : receiptV3
291
394
  ? [...RECEIPT_REQUIRED_KEYS, "sourceWorkspaceRoot", "executionIsolation", "mutationClasses"]
292
395
  : receiptV2
293
396
  ? [...RECEIPT_REQUIRED_KEYS, "inputArtifacts"]
294
397
  : RECEIPT_REQUIRED_KEYS;
295
- const optionalKeys = receiptV3 ? ["inputArtifacts", "signatureRef"] : ["signatureRef"];
296
- if (!(receiptV1 || receiptV2 || receiptV3) || !hasExactKeys(receipt, requiredKeys, optionalKeys)) return false;
398
+ const optionalKeys = receiptV3 || receiptV4 ? ["inputArtifacts", "signatureRef"] : ["signatureRef"];
399
+ if (!(receiptV1 || receiptV2 || receiptV3 || receiptV4) || !hasExactKeys(receipt, requiredKeys, optionalKeys)) return false;
297
400
  if (
298
401
  typeof receipt.receiptId !== "string"
299
402
  || receipt.receiptId.length === 0
@@ -320,13 +423,15 @@ export function validQuickReceiptShape(receipt) {
320
423
  || !hasExactKeys(receipt.outputArtifacts, ["stdout", "stderr"])
321
424
  || !validOutputArtifact(receipt.outputArtifacts.stdout)
322
425
  || !validOutputArtifact(receipt.outputArtifacts.stderr)
323
- || !(receiptV3
426
+ || !(receiptV4
427
+ ? V4_MUTATION_DECISIONS.has(receipt.mutationDecision)
428
+ : receiptV3
324
429
  ? V3_MUTATION_DECISIONS.has(receipt.mutationDecision)
325
430
  : ["source_unchanged", "invalidated_by_command_source_mutation"].includes(receipt.mutationDecision))
326
431
  || !Array.isArray(receipt.issueCodes)
327
432
  || new Set(receipt.issueCodes).size !== receipt.issueCodes.length
328
433
  || !receipt.issueCodes.every((entry) => typeof entry === "string"
329
- && (receiptV3 ? V3_RECEIPT_ISSUES : KNOWN_RECEIPT_ISSUES).has(entry))
434
+ && (receiptV4 ? V4_RECEIPT_ISSUES : receiptV3 ? V3_RECEIPT_ISSUES : KNOWN_RECEIPT_ISSUES).has(entry))
330
435
  ) {
331
436
  return false;
332
437
  }
@@ -348,6 +453,22 @@ export function validQuickReceiptShape(receipt) {
348
453
  )) {
349
454
  return false;
350
455
  }
456
+ if (receiptV4 && (
457
+ typeof receipt.controllerWorkspaceRoot !== "string"
458
+ || receipt.controllerWorkspaceRoot.length === 0
459
+ || typeof receipt.sourceWorkspaceRoot !== "string"
460
+ || receipt.sourceWorkspaceRoot.length === 0
461
+ || receipt.controllerWorkspaceRoot === receipt.sourceWorkspaceRoot
462
+ || !validForeignMutationClasses(receipt.mutationClasses)
463
+ || !validExecutionIsolation(receipt.executionIsolation, receipt)
464
+ || !validForeignWorkspace(receipt.foreignWorkspace, receipt.sourceWorkspaceRoot)
465
+ || receipt.inputArtifacts !== undefined
466
+ && (!hasExactKeys(receipt.inputArtifacts, ["stdin"])
467
+ || !validOutputArtifact(receipt.inputArtifacts.stdin))
468
+ || !validV4MutationTruth(receipt)
469
+ )) {
470
+ return false;
471
+ }
351
472
  if (receipt.signatureRef !== undefined) {
352
473
  if (!hasExactKeys(receipt.signatureRef, ["path", "sha256"])
353
474
  || typeof receipt.signatureRef.path !== "string"