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