owlrunkit 0.18.3 → 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.
- package/CHANGELOG.md +80 -0
- package/README.md +223 -52
- package/docs/architecture/OWLCODA_RUNKIT_0184_EXTERNAL_DELIVERY_INTAKE_PRODUCT_DECISION_20260812.md +209 -0
- package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
- package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +8 -7
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +133 -77
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +57 -0
- package/package.json +1 -1
- package/packages/attest/README.md +9 -5
- package/packages/attest/consumer-surface-v2.json +263 -0
- package/packages/attest/distribution-manifest-v1.json +14 -1
- package/packages/attest/schemas/quick-verification-receipt-v3.schema.json +244 -0
- package/packages/attest/schemas/workspace-snapshot-v2.schema.json +114 -0
- package/packages/attest/src/dependency-environment.mjs +128 -0
- package/packages/attest/src/index.mjs +1 -0
- package/packages/attest/src/quick-receipt-contract.mjs +205 -12
- package/packages/attest/src/quick.mjs +45 -4
- package/packages/attest/src/workspace-snapshot.mjs +143 -7
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +25 -6
- package/scripts/runkit-contract/core-contract.mjs +29 -1
- package/scripts/runkit-contract/efficiency-metrics.mjs +559 -0
- package/scripts/runkit-contract/evidence-store.mjs +130 -0
- package/scripts/runkit-contract/onboarding-bootstrap.mjs +685 -0
- package/scripts/runkit-contract/onboarding-doctor.mjs +3 -2
- package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +285 -17
- package/scripts/runkit-contract/project-cli-resolver.mjs +29 -5
- package/scripts/runkit-contract/project-external-delivery.mjs +707 -0
- package/scripts/runkit-contract/quick-evidence-reuse.mjs +590 -0
- package/scripts/runkit-contract/quick-isolation.mjs +314 -0
- package/scripts/runkit-contract/quick-metrics.mjs +51 -3
- package/scripts/runkit-contract/quick-receipt.mjs +31 -1
- package/scripts/runkit-contract/quick-verify.mjs +217 -26
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +7 -1
- package/scripts/runkit-contract/runkit-cli.mjs +602 -152
- package/scripts/runkit-contract/runtime-transition.mjs +478 -0
- package/scripts/runkit-contract/schemas/assurance-route-v2.schema.json +26 -0
- package/scripts/runkit-contract/schemas/efficiency-event-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/efficiency-metrics-v1.schema.json +220 -0
- package/scripts/runkit-contract/schemas/external-runtime-report-v1.schema.json +125 -0
- package/scripts/runkit-contract/schemas/onboarding-bootstrap-plan-v1.schema.json +168 -0
- package/scripts/runkit-contract/schemas/onboarding-bootstrap-receipt-v1.schema.json +130 -0
- package/scripts/runkit-contract/schemas/outcome-summary-v1.schema.json +83 -0
- package/scripts/runkit-contract/schemas/profile-detection-v3.schema.json +170 -0
- package/scripts/runkit-contract/schemas/quick-evidence-reuse-plan-v1.schema.json +59 -0
- package/scripts/runkit-contract/schemas/quick-evidence-reuse-receipt-v1.schema.json +79 -0
- package/scripts/runkit-contract/schemas/runtime-transition-receipt-v1.schema.json +185 -0
- package/scripts/runkit-contract/schemas/team-project-event-v3.schema.json +146 -0
- package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
- package/scripts/runkit-contract/schemas/team-project-status-v2.schema.json +85 -0
- package/scripts/runkit-contract/schemas/usage-mode-recommendation-v2.schema.json +136 -0
- package/scripts/runkit-contract/team-project.mjs +1172 -22
- package/scripts/runkit-contract/usage-mode.mjs +28 -7
- 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
|
|
|
57
|
-
function
|
|
58
|
-
|
|
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
|
+
|
|
92
|
+
export function validWorkspaceSnapshotShape(value) {
|
|
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.
|
|
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 !==
|
|
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 @@ function validSnapshot(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,21 +162,146 @@ 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
|
|
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
|
-
|
|
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
|
|
122
300
|
|| receipt.assurance !== "captured_verification"
|
|
123
301
|
|| receipt.authorizationGranted !== false
|
|
124
302
|
|| !validCoreIdentity(receipt.coreIdentity)
|
|
125
|
-
|| !
|
|
126
|
-
|| !
|
|
303
|
+
|| !validWorkspaceSnapshotShape(receipt.workspaceBefore)
|
|
304
|
+
|| !validWorkspaceSnapshotShape(receipt.workspaceAfter)
|
|
127
305
|
|| !hasExactKeys(receipt.exactCommand, ["executable", "argv", "cwd"])
|
|
128
306
|
|| typeof receipt.exactCommand.executable !== "string"
|
|
129
307
|
|| receipt.exactCommand.executable.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
|
-
|| !
|
|
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"
|
|
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,22 @@ 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
|
+
},
|
|
27
|
+
{
|
|
28
|
+
contractVersion: "0.2",
|
|
29
|
+
coreVersion: "0.18.4",
|
|
30
|
+
coreManifestSha256: "sha256:372d4c247e4b40b0fd2f3fe0f5bf928548095bfb7e2e92e9ac66790e2717f77d",
|
|
31
|
+
},
|
|
21
32
|
{
|
|
22
33
|
contractVersion: "0.2",
|
|
23
34
|
coreVersion: "0.18.3",
|
|
@@ -120,6 +131,23 @@ export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
|
|
|
120
131
|
},
|
|
121
132
|
]);
|
|
122
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
|
+
|
|
123
151
|
function resolveBoundMaterial(root, relativePath) {
|
|
124
152
|
if (path.isAbsolute(relativePath) || relativePath.includes("\0")) {
|
|
125
153
|
throw new Error("material path must be workspace-relative");
|
|
@@ -194,7 +222,7 @@ export function attestQuickReceiptDetailsFromBytes({
|
|
|
194
222
|
throw error;
|
|
195
223
|
}
|
|
196
224
|
if (!isPlainJsonObject(receipt) || !validQuickReceiptShape(receipt)) {
|
|
197
|
-
const error = new Error("Quick receipt does not satisfy
|
|
225
|
+
const error = new Error("Quick receipt does not satisfy a supported strict shape");
|
|
198
226
|
error.code = "receipt_schema_invalid";
|
|
199
227
|
throw error;
|
|
200
228
|
}
|
|
@@ -234,7 +262,11 @@ export function attestQuickReceiptDetailsFromBytes({
|
|
|
234
262
|
let receiptWorkspaceRoot;
|
|
235
263
|
if (selectedWorkspaceRoot !== undefined) {
|
|
236
264
|
try {
|
|
237
|
-
receiptWorkspaceRoot = realpathSync(
|
|
265
|
+
receiptWorkspaceRoot = realpathSync(
|
|
266
|
+
receipt.schemaVersion === "OwlCodaQuickVerificationReceiptV3"
|
|
267
|
+
? receipt.sourceWorkspaceRoot
|
|
268
|
+
: receipt.exactCommand.cwd,
|
|
269
|
+
);
|
|
238
270
|
} catch {
|
|
239
271
|
receiptWorkspaceRoot = undefined;
|
|
240
272
|
}
|
|
@@ -267,11 +299,19 @@ export function attestQuickReceiptDetailsFromBytes({
|
|
|
267
299
|
}
|
|
268
300
|
if (materialMissing) issues.push("attestation_material_missing");
|
|
269
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
|
+
: [];
|
|
270
305
|
const currentSourceValid = selectedWorkspaceRoot === undefined
|
|
271
306
|
|| commandWorkspaceMatches
|
|
272
|
-
&& captureWorkspaceSnapshot(selectedWorkspaceRoot).sourceFingerprint
|
|
307
|
+
&& captureWorkspaceSnapshot(selectedWorkspaceRoot, { ignoredPaths }).sourceFingerprint
|
|
273
308
|
=== receipt.workspaceAfter.sourceFingerprint;
|
|
274
|
-
const
|
|
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;
|
|
275
315
|
if (!sourceValid || receipt.mutationDecision !== "source_unchanged") {
|
|
276
316
|
issues.push("receipt_source_mismatch");
|
|
277
317
|
}
|
|
@@ -280,6 +320,7 @@ export function attestQuickReceiptDetailsFromBytes({
|
|
|
280
320
|
const deterministicFailure = !coreValid
|
|
281
321
|
|| !contextValid
|
|
282
322
|
|| materialMismatch
|
|
323
|
+
|| !dependencyEnvironmentValid
|
|
283
324
|
|| !sourceValid
|
|
284
325
|
|| receipt.mutationDecision !== "source_unchanged"
|
|
285
326
|
|| !commandPassed;
|