owlrunkit 0.18.4 → 0.19.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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +102 -4
  3. package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
  4. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
  5. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -4
  6. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  7. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +57 -0
  8. package/package.json +1 -1
  9. package/packages/attest/README.md +9 -5
  10. package/packages/attest/consumer-surface-v2.json +263 -0
  11. package/packages/attest/distribution-manifest-v1.json +9 -1
  12. package/packages/attest/schemas/quick-verification-receipt-v3.schema.json +244 -0
  13. package/packages/attest/schemas/workspace-snapshot-v2.schema.json +114 -0
  14. package/packages/attest/src/dependency-environment.mjs +128 -0
  15. package/packages/attest/src/index.mjs +1 -0
  16. package/packages/attest/src/quick-receipt-contract.mjs +202 -9
  17. package/packages/attest/src/quick.mjs +40 -4
  18. package/packages/attest/src/workspace-snapshot.mjs +133 -6
  19. package/registry-release-policy-v1.json +5 -5
  20. package/sbom.spdx.json +5 -5
  21. package/scripts/runkit-contract/core-contract.mjs +21 -1
  22. package/scripts/runkit-contract/efficiency-metrics.mjs +559 -0
  23. package/scripts/runkit-contract/evidence-store.mjs +130 -0
  24. package/scripts/runkit-contract/onboarding-bootstrap.mjs +685 -0
  25. package/scripts/runkit-contract/onboarding-doctor.mjs +2 -1
  26. package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
  27. package/scripts/runkit-contract/profile-onboarding.mjs +22 -2
  28. package/scripts/runkit-contract/project-cli-resolver.mjs +23 -5
  29. package/scripts/runkit-contract/quick-evidence-reuse.mjs +590 -0
  30. package/scripts/runkit-contract/quick-isolation.mjs +314 -0
  31. package/scripts/runkit-contract/quick-metrics.mjs +51 -3
  32. package/scripts/runkit-contract/quick-receipt.mjs +31 -1
  33. package/scripts/runkit-contract/quick-verify.mjs +216 -25
  34. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
  35. package/scripts/runkit-contract/runkit-bootstrap.mjs +1 -0
  36. package/scripts/runkit-contract/runkit-cli.mjs +330 -18
  37. package/scripts/runkit-contract/runtime-transition.mjs +478 -0
  38. package/scripts/runkit-contract/schemas/efficiency-event-v1.schema.json +56 -0
  39. package/scripts/runkit-contract/schemas/efficiency-metrics-v1.schema.json +220 -0
  40. package/scripts/runkit-contract/schemas/external-runtime-report-v1.schema.json +125 -0
  41. package/scripts/runkit-contract/schemas/onboarding-bootstrap-plan-v1.schema.json +168 -0
  42. package/scripts/runkit-contract/schemas/onboarding-bootstrap-receipt-v1.schema.json +130 -0
  43. package/scripts/runkit-contract/schemas/outcome-summary-v1.schema.json +83 -0
  44. package/scripts/runkit-contract/schemas/quick-evidence-reuse-plan-v1.schema.json +59 -0
  45. package/scripts/runkit-contract/schemas/quick-evidence-reuse-receipt-v1.schema.json +79 -0
  46. package/scripts/runkit-contract/schemas/runtime-transition-receipt-v1.schema.json +185 -0
  47. package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
  48. package/scripts/runkit-contract/team-project.mjs +704 -16
  49. package/scripts/runkit-contract/verification-envelope.mjs +15 -18
@@ -0,0 +1,128 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ realpathSync,
7
+ } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ import { sha256Bytes, sha256Canonical } from "./formal.mjs";
11
+
12
+ const MAX_DEPENDENCY_MATERIAL_BYTES = 128 * 1024 * 1024;
13
+ const DEPENDENCY_MATERIALS = Object.freeze([
14
+ "package.json",
15
+ "package-lock.json",
16
+ "npm-shrinkwrap.json",
17
+ "pnpm-lock.yaml",
18
+ "yarn.lock",
19
+ "bun.lock",
20
+ "bun.lockb",
21
+ ".pnp.cjs",
22
+ ".pnp.loader.mjs",
23
+ "node_modules/.package-lock.json",
24
+ "node_modules/.modules.yaml",
25
+ ]);
26
+
27
+ function normalizePath(value) {
28
+ return value.split(path.sep).join("/");
29
+ }
30
+
31
+ function withinRoot(root, candidate) {
32
+ const relative = path.relative(root, candidate);
33
+ return relative === ""
34
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
35
+ }
36
+
37
+ function comparePaths(left, right) {
38
+ return left.localeCompare(right, "en");
39
+ }
40
+
41
+ function canonicalRealDirectory(requested, label) {
42
+ const absolute = path.resolve(requested);
43
+ const stat = lstatSync(absolute);
44
+ if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(absolute) !== absolute) {
45
+ throw new Error(`${label} must be a real directory without symlink traversal.`);
46
+ }
47
+ return absolute;
48
+ }
49
+
50
+ function dependencyMaterial(root, relativePath, budget) {
51
+ const absolute = path.resolve(root, ...relativePath.split("/"));
52
+ if (!withinRoot(root, absolute) || !existsSync(absolute)) return null;
53
+ const stat = lstatSync(absolute);
54
+ if (stat.isSymbolicLink() || !stat.isFile() || realpathSync(absolute) !== absolute) {
55
+ throw new Error(`Dependency binding material must be a regular non-symlink file: ${relativePath}`);
56
+ }
57
+ budget.bytes += stat.size;
58
+ if (budget.bytes > MAX_DEPENDENCY_MATERIAL_BYTES) {
59
+ throw new Error("Dependency binding material exceeds the byte limit.");
60
+ }
61
+ return { path: relativePath, sha256: sha256Bytes(readFileSync(absolute)) };
62
+ }
63
+
64
+ function installedDependencyMaterialPaths(root) {
65
+ const nodeModules = path.join(root, "node_modules");
66
+ const discovered = new Set();
67
+ const addRealFile = (candidate) => {
68
+ let resolved;
69
+ try {
70
+ resolved = realpathSync(candidate);
71
+ } catch {
72
+ return;
73
+ }
74
+ if (!withinRoot(root, resolved) || !lstatSync(resolved).isFile()) {
75
+ throw new Error("Dependency resolution metadata escapes its declared root.");
76
+ }
77
+ discovered.add(normalizePath(path.relative(root, resolved)));
78
+ };
79
+ const addPackage = (candidate) => addRealFile(path.join(realpathSync(candidate), "package.json"));
80
+ for (const name of readdirSync(nodeModules).sort(comparePaths)) {
81
+ if (name === ".bin") {
82
+ for (const binName of readdirSync(path.join(nodeModules, name)).sort(comparePaths)) {
83
+ addRealFile(path.join(nodeModules, name, binName));
84
+ }
85
+ continue;
86
+ }
87
+ if (name.startsWith(".")) continue;
88
+ const entry = path.join(nodeModules, name);
89
+ const stat = lstatSync(entry);
90
+ if (name.startsWith("@") && stat.isDirectory() && !stat.isSymbolicLink()) {
91
+ for (const child of readdirSync(entry).sort(comparePaths)) addPackage(path.join(entry, child));
92
+ } else {
93
+ addPackage(entry);
94
+ }
95
+ if (discovered.size > 20_000) {
96
+ throw new Error("Dependency resolution metadata exceeds the file limit.");
97
+ }
98
+ }
99
+ return [...discovered].sort(comparePaths);
100
+ }
101
+
102
+ export function captureDependencyEnvironment(dependencyRoot) {
103
+ if (dependencyRoot === undefined || dependencyRoot === null) return null;
104
+ const root = canonicalRealDirectory(dependencyRoot, "Dependency root");
105
+ const nodeModules = path.join(root, "node_modules");
106
+ const nodeModulesStat = lstatSync(nodeModules);
107
+ if (nodeModulesStat.isSymbolicLink()
108
+ || !nodeModulesStat.isDirectory()
109
+ || realpathSync(nodeModules) !== nodeModules) {
110
+ throw new Error("Dependency root must contain a real node_modules directory.");
111
+ }
112
+ const budget = { bytes: 0 };
113
+ const materialPaths = [...new Set([
114
+ ...DEPENDENCY_MATERIALS,
115
+ ...installedDependencyMaterialPaths(root),
116
+ ])].sort(comparePaths);
117
+ const materials = materialPaths
118
+ .map(relativePath => dependencyMaterial(root, relativePath, budget))
119
+ .filter(Boolean);
120
+ if (!materials.some(material => material.path === "package.json") || materials.length < 2) {
121
+ throw new Error("Dependency root must bind package.json and lock or install metadata.");
122
+ }
123
+ const identity = { root, materials };
124
+ return {
125
+ ...identity,
126
+ fingerprint: sha256Canonical(identity),
127
+ };
128
+ }
@@ -44,6 +44,7 @@ export function attestFile({ subjectPath, workspaceRoot }) {
44
44
  if (new Set([
45
45
  "OwlCodaQuickVerificationReceiptV1",
46
46
  "OwlCodaQuickVerificationReceiptV2",
47
+ "OwlCodaQuickVerificationReceiptV3",
47
48
  ]).has(subject.value.schemaVersion)) {
48
49
  const details = attestQuickReceiptDetailsFromBytes({
49
50
  receiptPath: subject.absolutePath,
@@ -34,10 +34,33 @@ const SNAPSHOT_REQUIRED_KEYS = [
34
34
  "policyVersion",
35
35
  "sourceFingerprint",
36
36
  ];
37
+ const SNAPSHOT_V2_REQUIRED_KEYS = [
38
+ ...SNAPSHOT_REQUIRED_KEYS.filter(key => key !== "sourceFingerprint"),
39
+ "ignoredPathBindings",
40
+ "ignoredPathsFingerprint",
41
+ "sourceStateFingerprint",
42
+ "sourceFingerprint",
43
+ ];
37
44
  const KNOWN_RECEIPT_ISSUES = new Set([
38
45
  "quick_ignored_artifact_unbound",
39
46
  "source_mutated_during_verification",
40
47
  ]);
48
+ const V3_RECEIPT_ISSUES = new Set([
49
+ ...KNOWN_RECEIPT_ISSUES,
50
+ "bound_ignored_path_mutated_during_verification",
51
+ "dependency_environment_mutated_during_verification",
52
+ "consumer_source_mutated_during_verification",
53
+ "quick_isolation_cleanup_failed",
54
+ ]);
55
+ const V3_MUTATION_DECISIONS = new Set([
56
+ "source_unchanged",
57
+ "invalidated_by_command_source_mutation",
58
+ "invalidated_by_bound_ignored_mutation",
59
+ "invalidated_by_dependency_environment_mutation",
60
+ "invalidated_by_consumer_source_mutation",
61
+ "invalidated_by_isolation_cleanup_failure",
62
+ "invalidated_by_multiple_mutations",
63
+ ]);
41
64
 
42
65
  function validDateTime(value) {
43
66
  return typeof value === "string"
@@ -54,16 +77,30 @@ function validCoreIdentity(value) {
54
77
  && isSha256Ref(value.coreManifestSha256);
55
78
  }
56
79
 
80
+ function sourceStateFingerprint(value) {
81
+ return sha256Canonical({
82
+ repositoryIdentity: value.repositoryIdentity,
83
+ headCommit: value.headCommit,
84
+ trackedTreeIdentity: value.trackedTreeIdentity,
85
+ submodules: value.submodules,
86
+ dirtyOverlay: value.dirtyOverlay,
87
+ dependencyLockfiles: value.dependencyLockfiles,
88
+ excludedRoots: value.excludedRoots,
89
+ });
90
+ }
91
+
57
92
  export function validWorkspaceSnapshotShape(value) {
58
- if (!hasExactKeys(value, SNAPSHOT_REQUIRED_KEYS)) return false;
93
+ const snapshotV1 = value?.schemaVersion === "OwlCodaWorkspaceSnapshotV1";
94
+ const snapshotV2 = value?.schemaVersion === "OwlCodaWorkspaceSnapshotV2";
95
+ if (!(snapshotV1 || snapshotV2)) return false;
96
+ if (!hasExactKeys(value, snapshotV2 ? SNAPSHOT_V2_REQUIRED_KEYS : SNAPSHOT_REQUIRED_KEYS)) return false;
59
97
  if (
60
- value.schemaVersion !== "OwlCodaWorkspaceSnapshotV1"
61
- || typeof value.repositoryIdentity !== "string"
98
+ typeof value.repositoryIdentity !== "string"
62
99
  || value.repositoryIdentity.length === 0
63
100
  || !(value.headCommit === null || /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value.headCommit))
64
101
  || !isSha256Ref(value.trackedTreeIdentity)
65
- || value.ignoredPathsBound !== false
66
- || value.policyVersion !== "workspace-snapshot-v1"
102
+ || value.ignoredPathsBound !== snapshotV2
103
+ || value.policyVersion !== (snapshotV2 ? "workspace-snapshot-v2" : "workspace-snapshot-v1")
67
104
  || !isSha256Ref(value.sourceFingerprint)
68
105
  || !Array.isArray(value.submodules)
69
106
  || !Array.isArray(value.dirtyOverlay)
@@ -92,6 +129,22 @@ export function validWorkspaceSnapshotShape(value) {
92
129
  && isSha256Ref(entry.sha256))) {
93
130
  return false;
94
131
  }
132
+ if (snapshotV2 && (
133
+ !Array.isArray(value.ignoredPathBindings)
134
+ || value.ignoredPathBindings.length === 0
135
+ || !isSha256Ref(value.ignoredPathsFingerprint)
136
+ || !isSha256Ref(value.sourceStateFingerprint)
137
+ || !value.ignoredPathBindings.every(entry =>
138
+ hasExactKeys(entry, ["path", "state", "sha256"])
139
+ && typeof entry.path === "string"
140
+ && entry.path.length > 0
141
+ && ["missing", "file", "directory"].includes(entry.state)
142
+ && (entry.sha256 === null || isSha256Ref(entry.sha256)))
143
+ || sha256Canonical(value.ignoredPathBindings) !== value.ignoredPathsFingerprint
144
+ || sourceStateFingerprint(value) !== value.sourceStateFingerprint
145
+ )) {
146
+ return false;
147
+ }
95
148
  return true;
96
149
  }
97
150
 
@@ -109,13 +162,138 @@ function validOutputArtifact(value) {
109
162
  && value.sizeBytes >= 0;
110
163
  }
111
164
 
165
+ function validDependencyEnvironment(value) {
166
+ if (value === null) return true;
167
+ if (!hasExactKeys(value, ["root", "materials", "fingerprint"])
168
+ || typeof value.root !== "string"
169
+ || value.root.length === 0
170
+ || !Array.isArray(value.materials)
171
+ || value.materials.length < 2
172
+ || !isSha256Ref(value.fingerprint)
173
+ || !value.materials.every(material =>
174
+ hasExactKeys(material, ["path", "sha256"])
175
+ && typeof material.path === "string"
176
+ && material.path.length > 0
177
+ && isSha256Ref(material.sha256))) {
178
+ return false;
179
+ }
180
+ return sha256Canonical({ root: value.root, materials: value.materials }) === value.fingerprint;
181
+ }
182
+
183
+ function validExecutionIsolation(value, receipt) {
184
+ if (value === null) {
185
+ return receipt.mutationClasses.consumerSource === "not_isolated"
186
+ && receipt.mutationClasses.consumerIgnoredArtifacts === "not_isolated";
187
+ }
188
+ return hasExactKeys(value, [
189
+ "schemaVersion",
190
+ "mode",
191
+ "consumerRoot",
192
+ "sourceCopy",
193
+ "cacheEnvironmentVariables",
194
+ "dependencyEnvironment",
195
+ "dependencyEnvironmentAfter",
196
+ "consumerDelta",
197
+ "cleanupStatus",
198
+ ])
199
+ && value.schemaVersion === "OwlCodaRunKitQuickIsolationV1"
200
+ && value.mode === "temporary_consumer"
201
+ && typeof value.consumerRoot === "string"
202
+ && value.consumerRoot.length > 0
203
+ && value.consumerRoot === receipt.exactCommand.cwd
204
+ && hasExactKeys(value.sourceCopy, ["fileCount", "totalBytes", "fingerprint"])
205
+ && Number.isInteger(value.sourceCopy.fileCount)
206
+ && value.sourceCopy.fileCount >= 0
207
+ && Number.isInteger(value.sourceCopy.totalBytes)
208
+ && value.sourceCopy.totalBytes >= 0
209
+ && isSha256Ref(value.sourceCopy.fingerprint)
210
+ && Array.isArray(value.cacheEnvironmentVariables)
211
+ && value.cacheEnvironmentVariables.length > 0
212
+ && new Set(value.cacheEnvironmentVariables).size === value.cacheEnvironmentVariables.length
213
+ && value.cacheEnvironmentVariables.every(name => typeof name === "string" && name.length > 0)
214
+ && validDependencyEnvironment(value.dependencyEnvironment)
215
+ && validDependencyEnvironment(value.dependencyEnvironmentAfter)
216
+ && (value.dependencyEnvironment !== null || value.dependencyEnvironmentAfter === null)
217
+ && hasExactKeys(value.consumerDelta, ["sourcePaths", "ignoredPaths"])
218
+ && [value.consumerDelta.sourcePaths, value.consumerDelta.ignoredPaths].every(paths =>
219
+ Array.isArray(paths)
220
+ && new Set(paths).size === paths.length
221
+ && paths.every(entry => typeof entry === "string" && entry.length > 0))
222
+ && ["removed", "cleanup_failed"].includes(value.cleanupStatus);
223
+ }
224
+
225
+ function validMutationClasses(value) {
226
+ return hasExactKeys(value, [
227
+ "workspaceSource",
228
+ "boundIgnoredPaths",
229
+ "dependencyEnvironment",
230
+ "consumerSource",
231
+ "consumerIgnoredArtifacts",
232
+ ])
233
+ && ["unchanged", "changed"].includes(value.workspaceSource)
234
+ && ["not_bound", "unchanged", "changed"].includes(value.boundIgnoredPaths)
235
+ && ["not_bound", "unchanged", "changed"].includes(value.dependencyEnvironment)
236
+ && ["not_isolated", "unchanged", "changed"].includes(value.consumerSource)
237
+ && ["not_isolated", "none", "changed_and_discarded"].includes(value.consumerIgnoredArtifacts);
238
+ }
239
+
240
+ function validV3MutationTruth(receipt) {
241
+ const workspaceSourceChanged = sourceStateFingerprint(receipt.workspaceBefore)
242
+ !== sourceStateFingerprint(receipt.workspaceAfter);
243
+ const boundIgnored = receipt.workspaceBefore.ignoredPathsBound === true;
244
+ const boundIgnoredChanged = boundIgnored
245
+ && receipt.workspaceBefore.ignoredPathsFingerprint
246
+ !== receipt.workspaceAfter.ignoredPathsFingerprint;
247
+ const isolation = receipt.executionIsolation;
248
+ const consumerSourceChanged = (isolation?.consumerDelta.sourcePaths.length ?? 0) > 0;
249
+ const consumerIgnoredChanged = (isolation?.consumerDelta.ignoredPaths.length ?? 0) > 0;
250
+ const dependencyBound = isolation !== null && isolation?.dependencyEnvironment !== null;
251
+ const dependencyChanged = dependencyBound
252
+ && (isolation.dependencyEnvironmentAfter === null
253
+ || isolation.dependencyEnvironmentAfter.fingerprint
254
+ !== isolation.dependencyEnvironment.fingerprint);
255
+ const cleanupFailed = isolation?.cleanupStatus === "cleanup_failed";
256
+ if (receipt.mutationClasses.workspaceSource !== (workspaceSourceChanged ? "changed" : "unchanged")
257
+ || receipt.mutationClasses.boundIgnoredPaths !== (boundIgnored
258
+ ? boundIgnoredChanged ? "changed" : "unchanged"
259
+ : "not_bound")
260
+ || receipt.mutationClasses.dependencyEnvironment !== (dependencyBound
261
+ ? dependencyChanged ? "changed" : "unchanged"
262
+ : "not_bound")
263
+ || receipt.mutationClasses.consumerSource !== (isolation === null
264
+ ? "not_isolated"
265
+ : consumerSourceChanged ? "changed" : "unchanged")
266
+ || receipt.mutationClasses.consumerIgnoredArtifacts !== (isolation === null
267
+ ? "not_isolated"
268
+ : consumerIgnoredChanged ? "changed_and_discarded" : "none")) {
269
+ return false;
270
+ }
271
+ const facts = [
272
+ [workspaceSourceChanged, "invalidated_by_command_source_mutation", "source_mutated_during_verification"],
273
+ [boundIgnoredChanged, "invalidated_by_bound_ignored_mutation", "bound_ignored_path_mutated_during_verification"],
274
+ [dependencyChanged, "invalidated_by_dependency_environment_mutation", "dependency_environment_mutated_during_verification"],
275
+ [consumerSourceChanged, "invalidated_by_consumer_source_mutation", "consumer_source_mutated_during_verification"],
276
+ [cleanupFailed, "invalidated_by_isolation_cleanup_failure", "quick_isolation_cleanup_failed"],
277
+ ].filter(([active]) => active);
278
+ const expectedDecision = facts.length === 0
279
+ ? "source_unchanged"
280
+ : facts.length === 1 ? facts[0][1] : "invalidated_by_multiple_mutations";
281
+ const expectedIssues = facts.map(([, , issue]) => issue).sort();
282
+ return receipt.mutationDecision === expectedDecision
283
+ && JSON.stringify([...receipt.issueCodes].sort()) === JSON.stringify(expectedIssues);
284
+ }
285
+
112
286
  export function validQuickReceiptShape(receipt) {
113
287
  const receiptV1 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV1";
114
288
  const receiptV2 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV2";
115
- const requiredKeys = receiptV2
289
+ const receiptV3 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV3";
290
+ const requiredKeys = receiptV3
291
+ ? [...RECEIPT_REQUIRED_KEYS, "sourceWorkspaceRoot", "executionIsolation", "mutationClasses"]
292
+ : receiptV2
116
293
  ? [...RECEIPT_REQUIRED_KEYS, "inputArtifacts"]
117
294
  : RECEIPT_REQUIRED_KEYS;
118
- if (!(receiptV1 || receiptV2) || !hasExactKeys(receipt, requiredKeys, ["signatureRef"])) return false;
295
+ const optionalKeys = receiptV3 ? ["inputArtifacts", "signatureRef"] : ["signatureRef"];
296
+ if (!(receiptV1 || receiptV2 || receiptV3) || !hasExactKeys(receipt, requiredKeys, optionalKeys)) return false;
119
297
  if (
120
298
  typeof receipt.receiptId !== "string"
121
299
  || receipt.receiptId.length === 0
@@ -142,10 +320,13 @@ export function validQuickReceiptShape(receipt) {
142
320
  || !hasExactKeys(receipt.outputArtifacts, ["stdout", "stderr"])
143
321
  || !validOutputArtifact(receipt.outputArtifacts.stdout)
144
322
  || !validOutputArtifact(receipt.outputArtifacts.stderr)
145
- || !["source_unchanged", "invalidated_by_command_source_mutation"].includes(receipt.mutationDecision)
323
+ || !(receiptV3
324
+ ? V3_MUTATION_DECISIONS.has(receipt.mutationDecision)
325
+ : ["source_unchanged", "invalidated_by_command_source_mutation"].includes(receipt.mutationDecision))
146
326
  || !Array.isArray(receipt.issueCodes)
147
327
  || new Set(receipt.issueCodes).size !== receipt.issueCodes.length
148
- || !receipt.issueCodes.every((entry) => typeof entry === "string" && KNOWN_RECEIPT_ISSUES.has(entry))
328
+ || !receipt.issueCodes.every((entry) => typeof entry === "string"
329
+ && (receiptV3 ? V3_RECEIPT_ISSUES : KNOWN_RECEIPT_ISSUES).has(entry))
149
330
  ) {
150
331
  return false;
151
332
  }
@@ -155,6 +336,18 @@ export function validQuickReceiptShape(receipt) {
155
336
  )) {
156
337
  return false;
157
338
  }
339
+ if (receiptV3 && (
340
+ typeof receipt.sourceWorkspaceRoot !== "string"
341
+ || receipt.sourceWorkspaceRoot.length === 0
342
+ || !validMutationClasses(receipt.mutationClasses)
343
+ || !validExecutionIsolation(receipt.executionIsolation, receipt)
344
+ || receipt.inputArtifacts !== undefined
345
+ && (!hasExactKeys(receipt.inputArtifacts, ["stdin"])
346
+ || !validOutputArtifact(receipt.inputArtifacts.stdin))
347
+ || !validV3MutationTruth(receipt)
348
+ )) {
349
+ return false;
350
+ }
158
351
  if (receipt.signatureRef !== undefined) {
159
352
  if (!hasExactKeys(receipt.signatureRef, ["path", "sha256"])
160
353
  || typeof receipt.signatureRef.path !== "string"
@@ -13,11 +13,17 @@ import {
13
13
  quickSnapshotFingerprintValid,
14
14
  validQuickReceiptShape,
15
15
  } from "./quick-receipt-contract.mjs";
16
+ import { captureDependencyEnvironment } from "./dependency-environment.mjs";
16
17
  import { captureWorkspaceSnapshot } from "./workspace-snapshot.mjs";
17
18
 
18
19
  export { validQuickReceiptShape } from "./quick-receipt-contract.mjs";
19
20
 
20
21
  export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
22
+ {
23
+ contractVersion: "0.2",
24
+ coreVersion: "0.19.0",
25
+ coreManifestSha256: "sha256:803fb7de76a6ccffd1ec5e0164eeff15a580209fa6dfbef9e561345ab616b403",
26
+ },
21
27
  {
22
28
  contractVersion: "0.2",
23
29
  coreVersion: "0.18.4",
@@ -125,6 +131,23 @@ export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
125
131
  },
126
132
  ]);
127
133
 
134
+ function dependencyEnvironmentCurrent(binding, materials) {
135
+ if (binding === null || binding === undefined) return true;
136
+ try {
137
+ const current = captureDependencyEnvironment(binding.root);
138
+ if (current.root !== binding.root || current.fingerprint !== binding.fingerprint) return false;
139
+ for (const material of current.materials) {
140
+ materials.push({
141
+ path: resolveBoundMaterial(current.root, material.path),
142
+ sha256: material.sha256,
143
+ });
144
+ }
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
128
151
  function resolveBoundMaterial(root, relativePath) {
129
152
  if (path.isAbsolute(relativePath) || relativePath.includes("\0")) {
130
153
  throw new Error("material path must be workspace-relative");
@@ -199,7 +222,7 @@ export function attestQuickReceiptDetailsFromBytes({
199
222
  throw error;
200
223
  }
201
224
  if (!isPlainJsonObject(receipt) || !validQuickReceiptShape(receipt)) {
202
- const error = new Error("Quick receipt does not satisfy the strict Wave 1 shape");
225
+ const error = new Error("Quick receipt does not satisfy a supported strict shape");
203
226
  error.code = "receipt_schema_invalid";
204
227
  throw error;
205
228
  }
@@ -239,7 +262,11 @@ export function attestQuickReceiptDetailsFromBytes({
239
262
  let receiptWorkspaceRoot;
240
263
  if (selectedWorkspaceRoot !== undefined) {
241
264
  try {
242
- receiptWorkspaceRoot = realpathSync(receipt.exactCommand.cwd);
265
+ receiptWorkspaceRoot = realpathSync(
266
+ receipt.schemaVersion === "OwlCodaQuickVerificationReceiptV3"
267
+ ? receipt.sourceWorkspaceRoot
268
+ : receipt.exactCommand.cwd,
269
+ );
243
270
  } catch {
244
271
  receiptWorkspaceRoot = undefined;
245
272
  }
@@ -272,11 +299,19 @@ export function attestQuickReceiptDetailsFromBytes({
272
299
  }
273
300
  if (materialMissing) issues.push("attestation_material_missing");
274
301
  if (materialMismatch) issues.push("receipt_material_hash_mismatch");
302
+ const ignoredPaths = receipt.workspaceAfter.ignoredPathsBound === true
303
+ ? receipt.workspaceAfter.ignoredPathBindings.map(entry => entry.path)
304
+ : [];
275
305
  const currentSourceValid = selectedWorkspaceRoot === undefined
276
306
  || commandWorkspaceMatches
277
- && captureWorkspaceSnapshot(selectedWorkspaceRoot).sourceFingerprint
307
+ && captureWorkspaceSnapshot(selectedWorkspaceRoot, { ignoredPaths }).sourceFingerprint
278
308
  === receipt.workspaceAfter.sourceFingerprint;
279
- const sourceValid = receiptSourceValid && currentSourceValid;
309
+ const dependencyEnvironmentValid = dependencyEnvironmentCurrent(
310
+ receipt.executionIsolation?.dependencyEnvironment,
311
+ materials,
312
+ );
313
+ if (!dependencyEnvironmentValid) issues.push("receipt_dependency_environment_mismatch");
314
+ const sourceValid = receiptSourceValid && currentSourceValid && dependencyEnvironmentValid;
280
315
  if (!sourceValid || receipt.mutationDecision !== "source_unchanged") {
281
316
  issues.push("receipt_source_mismatch");
282
317
  }
@@ -285,6 +320,7 @@ export function attestQuickReceiptDetailsFromBytes({
285
320
  const deterministicFailure = !coreValid
286
321
  || !contextValid
287
322
  || materialMismatch
323
+ || !dependencyEnvironmentValid
288
324
  || !sourceValid
289
325
  || receipt.mutationDecision !== "source_unchanged"
290
326
  || !commandPassed;
@@ -1,10 +1,12 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { sha256Bytes, sha256Canonical } from "./formal.mjs";
6
6
 
7
7
  const CONTROL_ROOT = ".owlcoda/runkit";
8
+ const MAX_BOUND_IGNORED_FILES = 10_000;
9
+ const MAX_BOUND_IGNORED_BYTES = 512 * 1024 * 1024;
8
10
  const LOCKFILE_NAMES = new Set([
9
11
  "bun.lock",
10
12
  "bun.lockb",
@@ -157,12 +159,117 @@ function trackedTreeIdentity(root, headCommit) {
157
159
  });
158
160
  }
159
161
 
160
- export function captureWorkspaceSnapshot(workspaceRoot) {
162
+ function safeIgnoredPath(value) {
163
+ if (typeof value !== "string" || value.length === 0 || path.isAbsolute(value)) return false;
164
+ if (value.includes("\\") || value.includes("\0")) return false;
165
+ const segments = value.split("/");
166
+ return segments.every(segment => segment.length > 0 && segment !== "." && segment !== "..")
167
+ && !isControlPath(value);
168
+ }
169
+
170
+ function withinRoot(root, candidate) {
171
+ const relative = path.relative(root, candidate);
172
+ return relative === ""
173
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
174
+ }
175
+
176
+ function assertIgnoredByGit(root, relativePath) {
177
+ const tracked = git(root, ["ls-files", "-z", "--", relativePath]);
178
+ if (tracked.length > 0) {
179
+ throw new Error(`Explicit ignored binding cannot replace tracked source binding: ${relativePath}`);
180
+ }
181
+ try {
182
+ git(root, ["check-ignore", "--no-index", "-q", "--", relativePath]);
183
+ } catch {
184
+ throw new Error(`Explicit ignored binding is not covered by Git ignore rules: ${relativePath}`);
185
+ }
186
+ }
187
+
188
+ function hashIgnoredTree(root, relativePath, budget) {
189
+ const absolute = path.resolve(root, ...relativePath.split("/"));
190
+ if (!withinRoot(root, absolute)) throw new Error(`Ignored binding escapes workspace: ${relativePath}`);
191
+ let stat;
192
+ try {
193
+ stat = lstatSync(absolute);
194
+ } catch (error) {
195
+ if (error?.code === "ENOENT") return { path: relativePath, state: "missing", sha256: null };
196
+ throw error;
197
+ }
198
+ if (stat.isSymbolicLink() || realpathSync(absolute) !== absolute) {
199
+ throw new Error(`Ignored binding cannot traverse a symlink: ${relativePath}`);
200
+ }
201
+ if (stat.isFile()) {
202
+ budget.files += 1;
203
+ budget.bytes += stat.size;
204
+ if (budget.files > MAX_BOUND_IGNORED_FILES || budget.bytes > MAX_BOUND_IGNORED_BYTES) {
205
+ throw new Error("Explicit ignored binding exceeds the bounded file or byte limit.");
206
+ }
207
+ return { path: relativePath, state: "file", sha256: sha256Bytes(readFileSync(absolute)) };
208
+ }
209
+ if (!stat.isDirectory()) {
210
+ throw new Error(`Ignored binding must be a regular file, directory, or missing path: ${relativePath}`);
211
+ }
212
+ const files = [];
213
+ const visit = (directory, prefix) => {
214
+ for (const name of readdirSync(directory).sort((left, right) => left.localeCompare(right, "en"))) {
215
+ const child = path.join(directory, name);
216
+ const childRelative = prefix ? `${prefix}/${name}` : name;
217
+ const childStat = lstatSync(child);
218
+ if (childStat.isSymbolicLink() || realpathSync(child) !== child) {
219
+ throw new Error(`Ignored binding cannot traverse a symlink: ${relativePath}/${childRelative}`);
220
+ }
221
+ if (childStat.isDirectory()) {
222
+ visit(child, childRelative);
223
+ } else if (childStat.isFile()) {
224
+ budget.files += 1;
225
+ budget.bytes += childStat.size;
226
+ if (budget.files > MAX_BOUND_IGNORED_FILES || budget.bytes > MAX_BOUND_IGNORED_BYTES) {
227
+ throw new Error("Explicit ignored binding exceeds the bounded file or byte limit.");
228
+ }
229
+ files.push({ path: childRelative, sha256: sha256Bytes(readFileSync(child)) });
230
+ } else {
231
+ throw new Error(`Ignored binding contains a special file: ${relativePath}/${childRelative}`);
232
+ }
233
+ }
234
+ };
235
+ visit(absolute, "");
236
+ return { path: relativePath, state: "directory", sha256: sha256Canonical(files) };
237
+ }
238
+
239
+ function bindIgnoredPaths(root, requestedPaths) {
240
+ if (!Array.isArray(requestedPaths)
241
+ || requestedPaths.length === 0
242
+ || requestedPaths.some(value => !safeIgnoredPath(value))) {
243
+ throw new Error("Explicit ignored bindings must be safe literal workspace-relative paths.");
244
+ }
245
+ const paths = [...new Set(requestedPaths)].sort((left, right) => left.localeCompare(right, "en"));
246
+ if (paths.length !== requestedPaths.length) {
247
+ throw new Error("Explicit ignored bindings must not contain duplicates.");
248
+ }
249
+ const budget = { files: 0, bytes: 0 };
250
+ return paths.map(relativePath => {
251
+ assertIgnoredByGit(root, relativePath);
252
+ return hashIgnoredTree(root, relativePath, budget);
253
+ });
254
+ }
255
+
256
+ export function workspaceSourceStateFingerprint(snapshot) {
257
+ return sha256Canonical({
258
+ repositoryIdentity: snapshot.repositoryIdentity,
259
+ headCommit: snapshot.headCommit,
260
+ trackedTreeIdentity: snapshot.trackedTreeIdentity,
261
+ submodules: snapshot.submodules,
262
+ dirtyOverlay: snapshot.dirtyOverlay,
263
+ dependencyLockfiles: snapshot.dependencyLockfiles,
264
+ excludedRoots: snapshot.excludedRoots,
265
+ });
266
+ }
267
+
268
+ export function captureWorkspaceSnapshot(workspaceRoot, { ignoredPaths = [] } = {}) {
161
269
  const root = realpathSync(workspaceRoot);
162
270
  git(root, ["rev-parse", "--is-inside-work-tree"]);
163
271
  const headCommit = optionalGit(root, ["rev-parse", "HEAD"]) || null;
164
- const payload = {
165
- schemaVersion: "OwlCodaWorkspaceSnapshotV1",
272
+ const base = {
166
273
  repositoryIdentity: repositoryIdentity(root),
167
274
  headCommit,
168
275
  trackedTreeIdentity: trackedTreeIdentity(root, headCommit),
@@ -170,8 +277,28 @@ export function captureWorkspaceSnapshot(workspaceRoot) {
170
277
  dirtyOverlay: parseStatus(root),
171
278
  dependencyLockfiles: dependencyLockfiles(root),
172
279
  excludedRoots: [CONTROL_ROOT],
173
- ignoredPathsBound: false,
174
- policyVersion: "workspace-snapshot-v1",
280
+ };
281
+ if (ignoredPaths.length === 0) {
282
+ const payload = {
283
+ schemaVersion: "OwlCodaWorkspaceSnapshotV1",
284
+ ...base,
285
+ ignoredPathsBound: false,
286
+ policyVersion: "workspace-snapshot-v1",
287
+ };
288
+ return {
289
+ ...payload,
290
+ sourceFingerprint: sha256Canonical(payload),
291
+ };
292
+ }
293
+ const ignoredPathBindings = bindIgnoredPaths(root, ignoredPaths);
294
+ const payload = {
295
+ schemaVersion: "OwlCodaWorkspaceSnapshotV2",
296
+ ...base,
297
+ ignoredPathsBound: true,
298
+ ignoredPathBindings,
299
+ ignoredPathsFingerprint: sha256Canonical(ignoredPathBindings),
300
+ sourceStateFingerprint: workspaceSourceStateFingerprint(base),
301
+ policyVersion: "workspace-snapshot-v2",
175
302
  };
176
303
  return {
177
304
  ...payload,