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.
- package/CHANGELOG.md +38 -0
- package/README.md +102 -4
- package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -4
- 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 +9 -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 +202 -9
- package/packages/attest/src/quick.mjs +40 -4
- package/packages/attest/src/workspace-snapshot.mjs +133 -6
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +21 -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 +2 -1
- package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +22 -2
- package/scripts/runkit-contract/project-cli-resolver.mjs +23 -5
- 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 +216 -25
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +1 -0
- package/scripts/runkit-contract/runkit-cli.mjs +330 -18
- package/scripts/runkit-contract/runtime-transition.mjs +478 -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/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-v4.schema.json +142 -0
- package/scripts/runkit-contract/team-project.mjs +704 -16
- 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
|
-
|
|
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 @@ 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
|
|
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
|
|
@@ -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,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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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,
|