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,314 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
symlinkSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { captureDependencyEnvironment } from "../../packages/attest/src/dependency-environment.mjs";
|
|
20
|
+
import { sha256Bytes, sha256Canonical } from "./quick-canonical.mjs";
|
|
21
|
+
|
|
22
|
+
const CONTROL_ROOT = ".owlcoda/runkit";
|
|
23
|
+
const MAX_SOURCE_FILES = 100_000;
|
|
24
|
+
const MAX_SOURCE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
25
|
+
const CACHE_ENVIRONMENT = Object.freeze({
|
|
26
|
+
TMPDIR: "tmp",
|
|
27
|
+
TMP: "tmp",
|
|
28
|
+
TEMP: "tmp",
|
|
29
|
+
XDG_CACHE_HOME: "cache/xdg",
|
|
30
|
+
npm_config_cache: "cache/npm",
|
|
31
|
+
NPM_CONFIG_CACHE: "cache/npm",
|
|
32
|
+
YARN_CACHE_FOLDER: "cache/yarn",
|
|
33
|
+
PIP_CACHE_DIR: "cache/pip",
|
|
34
|
+
UV_CACHE_DIR: "cache/uv",
|
|
35
|
+
CARGO_TARGET_DIR: "cache/cargo-target",
|
|
36
|
+
GOCACHE: "cache/go-build",
|
|
37
|
+
GOMODCACHE: "cache/go-mod",
|
|
38
|
+
PYTHONPYCACHEPREFIX: "cache/python-bytecode",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function git(root, args, options = {}) {
|
|
42
|
+
return execFileSync("git", args, {
|
|
43
|
+
cwd: root,
|
|
44
|
+
encoding: options.encoding ?? "utf8",
|
|
45
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePath(value) {
|
|
52
|
+
return value.split(path.sep).join("/");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withinRoot(root, candidate) {
|
|
56
|
+
const relative = path.relative(root, candidate);
|
|
57
|
+
return relative === ""
|
|
58
|
+
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rootsOverlap(left, right) {
|
|
62
|
+
return withinRoot(left, right) || withinRoot(right, left);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function canonicalRealDirectory(requested, label) {
|
|
66
|
+
const absolute = path.resolve(requested);
|
|
67
|
+
const stat = lstatSync(absolute);
|
|
68
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(absolute) !== absolute) {
|
|
69
|
+
throw new Error(`${label} must be a real directory without symlink traversal.`);
|
|
70
|
+
}
|
|
71
|
+
return absolute;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sourcePaths(root) {
|
|
75
|
+
return git(root, ["ls-files", "-co", "--exclude-standard", "-z"])
|
|
76
|
+
.split("\0")
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(normalizePath)
|
|
79
|
+
.filter(relativePath => relativePath !== CONTROL_ROOT && !relativePath.startsWith(`${CONTROL_ROOT}/`))
|
|
80
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function symlinkHash(linkTarget) {
|
|
84
|
+
return sha256Bytes(Buffer.from(`symlink:${linkTarget}`, "utf8"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function copyVisibleSource(root, consumerRoot) {
|
|
88
|
+
const files = [];
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const relativePath of sourcePaths(root)) {
|
|
91
|
+
const source = path.resolve(root, ...relativePath.split("/"));
|
|
92
|
+
const target = path.resolve(consumerRoot, ...relativePath.split("/"));
|
|
93
|
+
if (!withinRoot(root, source) || !withinRoot(consumerRoot, target)) {
|
|
94
|
+
throw new Error(`Isolated source path escapes its root: ${relativePath}`);
|
|
95
|
+
}
|
|
96
|
+
const stat = lstatSync(source);
|
|
97
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
98
|
+
let hash;
|
|
99
|
+
if (stat.isSymbolicLink()) {
|
|
100
|
+
const linkTarget = readlinkSync(source);
|
|
101
|
+
if (path.isAbsolute(linkTarget)) {
|
|
102
|
+
throw new Error(`Isolated source copy rejects an absolute symlink: ${relativePath}`);
|
|
103
|
+
}
|
|
104
|
+
const resolvedTarget = realpathSync(path.resolve(path.dirname(source), linkTarget));
|
|
105
|
+
if (!withinRoot(root, resolvedTarget)) {
|
|
106
|
+
throw new Error(`Isolated source copy rejects a symlink escape: ${relativePath}`);
|
|
107
|
+
}
|
|
108
|
+
symlinkSync(linkTarget, target);
|
|
109
|
+
hash = symlinkHash(linkTarget);
|
|
110
|
+
} else if (stat.isFile()) {
|
|
111
|
+
totalBytes += stat.size;
|
|
112
|
+
copyFileSync(source, target);
|
|
113
|
+
chmodSync(target, stat.mode & 0o777);
|
|
114
|
+
hash = sha256Bytes(readFileSync(source));
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`Isolated source copy does not support a directory or special Git entry: ${relativePath}`);
|
|
117
|
+
}
|
|
118
|
+
files.push({ path: relativePath, sha256: hash });
|
|
119
|
+
if (files.length > MAX_SOURCE_FILES || totalBytes > MAX_SOURCE_BYTES) {
|
|
120
|
+
throw new Error("Isolated source copy exceeds the bounded file or byte limit.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
files,
|
|
125
|
+
fileCount: files.length,
|
|
126
|
+
totalBytes,
|
|
127
|
+
fingerprint: sha256Canonical(files),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function projectNodeModules(dependencyRoot, consumerRoot) {
|
|
132
|
+
if (dependencyRoot === null) return null;
|
|
133
|
+
const sourceRoot = path.join(dependencyRoot.root, "node_modules");
|
|
134
|
+
const targetRoot = path.join(consumerRoot, "node_modules");
|
|
135
|
+
mkdirSync(targetRoot);
|
|
136
|
+
for (const name of readdirSync(sourceRoot).sort((left, right) => left.localeCompare(right, "en"))) {
|
|
137
|
+
const source = path.join(sourceRoot, name);
|
|
138
|
+
const target = path.join(targetRoot, name);
|
|
139
|
+
const stat = lstatSync(source);
|
|
140
|
+
if (name === ".bin") {
|
|
141
|
+
mkdirSync(target);
|
|
142
|
+
for (const binName of readdirSync(source).sort((left, right) => left.localeCompare(right, "en"))) {
|
|
143
|
+
const sourceBin = path.join(source, binName);
|
|
144
|
+
const targetBin = path.join(target, binName);
|
|
145
|
+
const binStat = lstatSync(sourceBin);
|
|
146
|
+
if (binStat.isSymbolicLink()) {
|
|
147
|
+
const realBin = realpathSync(sourceBin);
|
|
148
|
+
symlinkSync(realBin, targetBin, "file");
|
|
149
|
+
} else if (binStat.isFile()) {
|
|
150
|
+
copyFileSync(sourceBin, targetBin);
|
|
151
|
+
chmodSync(targetBin, binStat.mode & 0o777);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (name.startsWith("@") && stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
157
|
+
mkdirSync(target);
|
|
158
|
+
for (const child of readdirSync(source).sort((left, right) => left.localeCompare(right, "en"))) {
|
|
159
|
+
symlinkSync(realpathSync(path.join(source, child)), path.join(target, child), "dir");
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (stat.isFile()) {
|
|
164
|
+
copyFileSync(source, target);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
symlinkSync(realpathSync(source), target, "dir");
|
|
168
|
+
}
|
|
169
|
+
return targetRoot;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isolatedEnvironment(consumerRoot, dependencyEnvironment) {
|
|
173
|
+
const controlRoot = path.join(consumerRoot, ".owlrunkit-isolation");
|
|
174
|
+
const overrides = {};
|
|
175
|
+
for (const [name, relativePath] of Object.entries(CACHE_ENVIRONMENT)) {
|
|
176
|
+
const target = path.join(controlRoot, relativePath);
|
|
177
|
+
mkdirSync(target, { recursive: true });
|
|
178
|
+
overrides[name] = target;
|
|
179
|
+
}
|
|
180
|
+
overrides.OWLRUNKIT_CONSUMER_ROOT = consumerRoot;
|
|
181
|
+
overrides.PWD = consumerRoot;
|
|
182
|
+
overrides.INIT_CWD = consumerRoot;
|
|
183
|
+
if (dependencyEnvironment !== null) {
|
|
184
|
+
overrides.OWLRUNKIT_DEPENDENCY_ROOT = dependencyEnvironment.root;
|
|
185
|
+
overrides.NODE_PATH = [
|
|
186
|
+
path.join(consumerRoot, "node_modules"),
|
|
187
|
+
process.env.NODE_PATH,
|
|
188
|
+
].filter(Boolean).join(path.delimiter);
|
|
189
|
+
overrides.PATH = [
|
|
190
|
+
path.join(consumerRoot, "node_modules", ".bin"),
|
|
191
|
+
process.env.PATH,
|
|
192
|
+
].filter(Boolean).join(path.delimiter);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
controlRoot,
|
|
196
|
+
overrides,
|
|
197
|
+
environmentVariables: Object.keys(overrides).sort(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function prepareQuickIsolation({ workspaceRoot, dependencyRoot }) {
|
|
202
|
+
const sourceRoot = canonicalRealDirectory(workspaceRoot, "Source workspace");
|
|
203
|
+
const consumerRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "owlrunkit-quick-consumer-")));
|
|
204
|
+
try {
|
|
205
|
+
const sourceCopy = copyVisibleSource(sourceRoot, consumerRoot);
|
|
206
|
+
const dependencyEnvironment = captureDependencyEnvironment(dependencyRoot);
|
|
207
|
+
if (dependencyEnvironment !== null && rootsOverlap(sourceRoot, dependencyEnvironment.root)) {
|
|
208
|
+
throw new Error("Dependency root and source workspace must be disjoint real directories.");
|
|
209
|
+
}
|
|
210
|
+
projectNodeModules(dependencyEnvironment, consumerRoot);
|
|
211
|
+
const environment = isolatedEnvironment(consumerRoot, dependencyEnvironment);
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: "OwlCodaRunKitQuickIsolationV1",
|
|
214
|
+
sourceRoot,
|
|
215
|
+
consumerRoot,
|
|
216
|
+
sourceCopy,
|
|
217
|
+
dependencyEnvironment,
|
|
218
|
+
environment,
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
rmSync(consumerRoot, { recursive: true, force: true });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function currentConsumerFiles(consumerRoot) {
|
|
227
|
+
const files = [];
|
|
228
|
+
const visit = (directory, prefix) => {
|
|
229
|
+
for (const name of readdirSync(directory).sort((left, right) => left.localeCompare(right, "en"))) {
|
|
230
|
+
if (prefix === "" && ["node_modules", ".owlrunkit-isolation"].includes(name)) continue;
|
|
231
|
+
const absolute = path.join(directory, name);
|
|
232
|
+
const relativePath = prefix ? `${prefix}/${name}` : name;
|
|
233
|
+
const stat = lstatSync(absolute);
|
|
234
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
235
|
+
visit(absolute, relativePath);
|
|
236
|
+
} else if (stat.isSymbolicLink()) {
|
|
237
|
+
files.push({ path: relativePath, sha256: symlinkHash(readlinkSync(absolute)) });
|
|
238
|
+
} else if (stat.isFile()) {
|
|
239
|
+
files.push({ path: relativePath, sha256: sha256Bytes(readFileSync(absolute)) });
|
|
240
|
+
} else {
|
|
241
|
+
files.push({ path: relativePath, sha256: "special" });
|
|
242
|
+
}
|
|
243
|
+
if (files.length > MAX_SOURCE_FILES) {
|
|
244
|
+
throw new Error("Isolated consumer delta exceeds the bounded file limit.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
visit(consumerRoot, "");
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function ignoredByGit(sourceRoot, relativePath) {
|
|
253
|
+
if (relativePath === CONTROL_ROOT || relativePath.startsWith(`${CONTROL_ROOT}/`)) return true;
|
|
254
|
+
try {
|
|
255
|
+
git(sourceRoot, ["check-ignore", "--no-index", "-q", "--", relativePath]);
|
|
256
|
+
return true;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function inspectQuickIsolation(isolation) {
|
|
263
|
+
const before = new Map(isolation.sourceCopy.files.map(row => [row.path, row.sha256]));
|
|
264
|
+
const afterRows = currentConsumerFiles(isolation.consumerRoot);
|
|
265
|
+
const after = new Map(afterRows.map(row => [row.path, row.sha256]));
|
|
266
|
+
const changed = [...new Set([...before.keys(), ...after.keys()])]
|
|
267
|
+
.filter(relativePath => before.get(relativePath) !== after.get(relativePath))
|
|
268
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
269
|
+
const sourcePaths = [];
|
|
270
|
+
const ignoredPaths = [];
|
|
271
|
+
for (const relativePath of changed) {
|
|
272
|
+
if (!before.has(relativePath) && ignoredByGit(isolation.sourceRoot, relativePath)) {
|
|
273
|
+
ignoredPaths.push(relativePath);
|
|
274
|
+
} else {
|
|
275
|
+
sourcePaths.push(relativePath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let dependencyEnvironmentAfter = null;
|
|
279
|
+
let dependencyChanged = false;
|
|
280
|
+
if (isolation.dependencyEnvironment !== null) {
|
|
281
|
+
try {
|
|
282
|
+
dependencyEnvironmentAfter = captureDependencyEnvironment(
|
|
283
|
+
isolation.dependencyEnvironment.root,
|
|
284
|
+
);
|
|
285
|
+
dependencyChanged = dependencyEnvironmentAfter.fingerprint
|
|
286
|
+
!== isolation.dependencyEnvironment.fingerprint;
|
|
287
|
+
} catch {
|
|
288
|
+
dependencyChanged = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
consumerDelta: { sourcePaths, ignoredPaths },
|
|
293
|
+
dependencyEnvironmentAfter,
|
|
294
|
+
dependencyChanged,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function removeQuickIsolation(isolation) {
|
|
299
|
+
try {
|
|
300
|
+
rmSync(isolation.consumerRoot, { recursive: true, force: true });
|
|
301
|
+
return "removed";
|
|
302
|
+
} catch {
|
|
303
|
+
return "cleanup_failed";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function dependencyEnvironmentMatches(binding) {
|
|
308
|
+
if (binding === null) return true;
|
|
309
|
+
try {
|
|
310
|
+
return captureDependencyEnvironment(binding.root).fingerprint === binding.fingerprint;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -22,6 +22,13 @@ const INVALID_ATTESTATION_ISSUES = new Set([
|
|
|
22
22
|
"attestation_material_missing",
|
|
23
23
|
"receipt_material_hash_mismatch",
|
|
24
24
|
]);
|
|
25
|
+
const V3_MUTATION_ISSUES = new Set([
|
|
26
|
+
"source_mutated_during_verification",
|
|
27
|
+
"bound_ignored_path_mutated_during_verification",
|
|
28
|
+
"dependency_environment_mutated_during_verification",
|
|
29
|
+
"consumer_source_mutated_during_verification",
|
|
30
|
+
"quick_isolation_cleanup_failed",
|
|
31
|
+
]);
|
|
25
32
|
|
|
26
33
|
function readHistoricalReceipt(receiptPath) {
|
|
27
34
|
const { bytes } = readFileBytesBounded(receiptPath);
|
|
@@ -31,9 +38,12 @@ function readHistoricalReceipt(receiptPath) {
|
|
|
31
38
|
&& quickSnapshotFingerprintValid(receipt.workspaceAfter);
|
|
32
39
|
const sourceChanged = receipt.workspaceBefore.sourceFingerprint
|
|
33
40
|
!== receipt.workspaceAfter.sourceFingerprint;
|
|
41
|
+
const receiptV3 = receipt.schemaVersion === "OwlCodaQuickVerificationReceiptV3";
|
|
34
42
|
const mutationBindingValid = receipt.mutationDecision === "source_unchanged"
|
|
35
|
-
? !sourceChanged && !receipt.issueCodes.
|
|
36
|
-
:
|
|
43
|
+
? !sourceChanged && !receipt.issueCodes.some(issue => V3_MUTATION_ISSUES.has(issue))
|
|
44
|
+
: receiptV3
|
|
45
|
+
? receipt.issueCodes.some(issue => V3_MUTATION_ISSUES.has(issue))
|
|
46
|
+
: sourceChanged && receipt.issueCodes.includes("source_mutated_during_verification");
|
|
37
47
|
if (!snapshotsValid || !mutationBindingValid) {
|
|
38
48
|
throw new Error("quick_receipt_source_binding_invalid");
|
|
39
49
|
}
|
|
@@ -59,6 +69,44 @@ function trustedReceiptRoot(workspaceRoot) {
|
|
|
59
69
|
return { status: "valid", root };
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
export function readTrustedQuickReceiptHistory(workspaceRoot) {
|
|
73
|
+
const selected = trustedReceiptRoot(workspaceRoot);
|
|
74
|
+
if (selected.status !== "valid") {
|
|
75
|
+
return {
|
|
76
|
+
status: selected.status,
|
|
77
|
+
receiptPaths: [],
|
|
78
|
+
receipts: [],
|
|
79
|
+
invalidCount: selected.status === "invalid" ? 1 : 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const receiptPaths = readdirSync(selected.root, { withFileTypes: true })
|
|
83
|
+
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
|
84
|
+
.map((entry) => path.join(selected.root, entry.name, "receipt.json"))
|
|
85
|
+
.filter(existsSync)
|
|
86
|
+
.sort();
|
|
87
|
+
const receipts = [];
|
|
88
|
+
let invalidCount = 0;
|
|
89
|
+
for (const receiptPath of receiptPaths) {
|
|
90
|
+
try {
|
|
91
|
+
const receipt = readHistoricalReceipt(receiptPath);
|
|
92
|
+
const attestation = attestQuickReceipt({ receiptPath, workspaceRoot });
|
|
93
|
+
if (attestation.issueCodes.some((issue) => INVALID_ATTESTATION_ISSUES.has(issue))) {
|
|
94
|
+
invalidCount += 1;
|
|
95
|
+
} else {
|
|
96
|
+
receipts.push({ receiptPath, receipt });
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
invalidCount += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
status: "valid",
|
|
104
|
+
receiptPaths,
|
|
105
|
+
receipts,
|
|
106
|
+
invalidCount,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
62
110
|
export function readLocalQuickMetrics(workspaceRoot, { verbose = false } = {}) {
|
|
63
111
|
const selected = trustedReceiptRoot(workspaceRoot);
|
|
64
112
|
if (selected.status === "invalid") {
|
|
@@ -100,7 +148,7 @@ export function readLocalQuickMetrics(workspaceRoot, { verbose = false } = {}) {
|
|
|
100
148
|
const attestation = attestQuickReceipt({ receiptPath, workspaceRoot });
|
|
101
149
|
if (attestation.issueCodes.some((issue) => INVALID_ATTESTATION_ISSUES.has(issue))) {
|
|
102
150
|
totals.invalid += 1;
|
|
103
|
-
} else if (receipt.mutationDecision
|
|
151
|
+
} else if (receipt.mutationDecision !== "source_unchanged") {
|
|
104
152
|
totals.sourceMutated += 1;
|
|
105
153
|
} else if (receipt.exitResult.exitCode === 0 && receipt.exitResult.signal === null) {
|
|
106
154
|
totals.passed += 1;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
chmodSync,
|
|
3
4
|
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
copyFileSync,
|
|
4
7
|
existsSync,
|
|
5
8
|
fstatSync,
|
|
6
9
|
lstatSync,
|
|
@@ -9,10 +12,12 @@ import {
|
|
|
9
12
|
readFileSync,
|
|
10
13
|
realpathSync,
|
|
11
14
|
renameSync,
|
|
15
|
+
rmSync,
|
|
12
16
|
writeFileSync,
|
|
13
17
|
} from "node:fs";
|
|
14
18
|
import path from "node:path";
|
|
15
19
|
|
|
20
|
+
import { persistEvidenceBytes } from "./evidence-store.mjs";
|
|
16
21
|
import { sha256Bytes } from "./quick-canonical.mjs";
|
|
17
22
|
|
|
18
23
|
const QUICK_ROOT = ".owlcoda/runkit/quick";
|
|
@@ -132,13 +137,27 @@ function summarizeOutputBytes(bytes) {
|
|
|
132
137
|
|
|
133
138
|
function outputArtifact(workspaceRoot, filePath) {
|
|
134
139
|
const bytes = readFileSync(filePath);
|
|
140
|
+
const evidence = persistEvidenceBytes({ workspaceRoot, bytes });
|
|
141
|
+
const stat = lstatSync(filePath);
|
|
142
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
143
|
+
throw new Error(`Quick output material is not a regular file: ${filePath}`);
|
|
144
|
+
}
|
|
145
|
+
const replacement = `${filePath}.cas-${process.pid}-${randomUUID()}`;
|
|
146
|
+
try {
|
|
147
|
+
copyFileSync(evidence.absolutePath, replacement, constants.COPYFILE_FICLONE);
|
|
148
|
+
chmodSync(replacement, 0o600);
|
|
149
|
+
renameSync(replacement, filePath);
|
|
150
|
+
} finally {
|
|
151
|
+
rmSync(replacement, { force: true });
|
|
152
|
+
}
|
|
135
153
|
return {
|
|
136
154
|
artifact: {
|
|
137
155
|
path: path.relative(realpathSync(workspaceRoot), filePath).split(path.sep).join("/"),
|
|
138
|
-
sha256:
|
|
156
|
+
sha256: evidence.sha256,
|
|
139
157
|
sizeBytes: bytes.byteLength,
|
|
140
158
|
},
|
|
141
159
|
summary: summarizeOutputBytes(bytes),
|
|
160
|
+
evidence,
|
|
142
161
|
};
|
|
143
162
|
}
|
|
144
163
|
|
|
@@ -224,6 +243,7 @@ export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
|
|
|
224
243
|
},
|
|
225
244
|
};
|
|
226
245
|
atomicWriteJson(store.receiptPath, complete);
|
|
246
|
+
const evidenceRows = [stdout.evidence, stderr.evidence];
|
|
227
247
|
return {
|
|
228
248
|
receipt: complete,
|
|
229
249
|
receiptPath: store.receiptPath,
|
|
@@ -236,6 +256,16 @@ export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
|
|
|
236
256
|
stdoutTruncated: stdout.summary.truncated,
|
|
237
257
|
stderrTruncated: stderr.summary.truncated,
|
|
238
258
|
},
|
|
259
|
+
evidenceStorage: {
|
|
260
|
+
mode: "content_addressed_sha256",
|
|
261
|
+
objectPaths: [...new Set(evidenceRows.map(row => row.path))].sort(),
|
|
262
|
+
uniqueBytes: evidenceRows
|
|
263
|
+
.filter(row => row.created)
|
|
264
|
+
.reduce((total, row) => total + row.sizeBytes, 0),
|
|
265
|
+
reusedBytes: evidenceRows
|
|
266
|
+
.filter(row => !row.created)
|
|
267
|
+
.reduce((total, row) => total + row.sizeBytes, 0),
|
|
268
|
+
},
|
|
239
269
|
};
|
|
240
270
|
}
|
|
241
271
|
|