owlrunkit 0.16.0 → 0.17.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 +40 -3
- package/README.md +61 -12
- package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +154 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +74 -16
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +12 -2
- package/scripts/runkit-contract/fleet-discovery.mjs +768 -26
- package/scripts/runkit-contract/formal-workflow.mjs +201 -0
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +7 -2
- package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
- package/scripts/runkit-contract/project-control-parser.mjs +62 -3
- package/scripts/runkit-contract/runkit-cli.mjs +162 -13
- package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +14 -2
- package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +33 -2
- package/scripts/runkit-contract/schemas/team-project-definition-v1.schema.json +108 -0
- package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +99 -0
- package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +50 -0
- package/scripts/runkit-contract/schemas/verification-envelope-v1.schema.json +102 -0
- package/scripts/runkit-contract/sync-core016-release-identity.mjs +9 -4
- package/scripts/runkit-contract/sync-release-identity.mjs +224 -0
- package/scripts/runkit-contract/team-project.mjs +1006 -0
- package/scripts/runkit-contract/templates/team-project-definition.json +30 -0
- package/scripts/runkit-contract/templates/team-project-event.json +9 -0
- package/scripts/runkit-contract/templates/verification-envelope.json +28 -0
- package/scripts/runkit-contract/verification-envelope-check.mjs +45 -0
- package/scripts/runkit-contract/verification-envelope.mjs +846 -0
|
@@ -19,6 +19,10 @@ const MAX_SCAN_DIRECTORIES = 10_000;
|
|
|
19
19
|
const MAX_SCAN_DEPTH = 16;
|
|
20
20
|
const FLEET_REGISTRY_SCHEMA =
|
|
21
21
|
"OwlCodaRunKitFleetRootRegistryV1";
|
|
22
|
+
const FLEET_REGISTRY_SCHEMA_V2 =
|
|
23
|
+
"OwlCodaRunKitFleetRootRegistryV2";
|
|
24
|
+
const FLEET_MEMBERSHIP_SCHEMA =
|
|
25
|
+
"OwlCodaRunKitFleetMembershipV1";
|
|
22
26
|
const SKIPPED_DIRECTORIES = new Set([
|
|
23
27
|
".cache",
|
|
24
28
|
".git",
|
|
@@ -43,12 +47,17 @@ async function canonicalWorkspace(workspaceRoot) {
|
|
|
43
47
|
return realpath(requested);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
function frozenManifestSha256({
|
|
50
|
+
function frozenManifestSha256({
|
|
51
|
+
coverageRoots,
|
|
52
|
+
workspaceRoots,
|
|
53
|
+
classifications = emptyClassifications(),
|
|
54
|
+
}) {
|
|
47
55
|
const frozen = {
|
|
48
56
|
schemaVersion: "OwlCodaRunKitFrozenFleetManifestV1",
|
|
49
57
|
skillName: "owlcoda-runkit",
|
|
50
58
|
coverageRoots,
|
|
51
59
|
workspaceRoots,
|
|
60
|
+
classifications,
|
|
52
61
|
authorizationGranted: false,
|
|
53
62
|
};
|
|
54
63
|
return `sha256:${createHash("sha256")
|
|
@@ -62,6 +71,29 @@ function sha256Json(value) {
|
|
|
62
71
|
.digest("hex")}`;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
function sha256Bytes(value) {
|
|
75
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function within(root, candidate) {
|
|
79
|
+
const relative = path.relative(root, candidate);
|
|
80
|
+
return relative === ""
|
|
81
|
+
|| (
|
|
82
|
+
relative !== ".."
|
|
83
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
84
|
+
&& !path.isAbsolute(relative)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function emptyClassifications() {
|
|
89
|
+
return {
|
|
90
|
+
active: [],
|
|
91
|
+
retired: [],
|
|
92
|
+
excluded: [],
|
|
93
|
+
invalid: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
65
97
|
export function defaultFleetRegistryPath() {
|
|
66
98
|
const owlcodaHome = process.env.OWLCODA_HOME
|
|
67
99
|
? path.resolve(process.env.OWLCODA_HOME)
|
|
@@ -69,7 +101,7 @@ export function defaultFleetRegistryPath() {
|
|
|
69
101
|
return path.join(owlcodaHome, "runkit-fleet-root-registry-v1.json");
|
|
70
102
|
}
|
|
71
103
|
|
|
72
|
-
async function
|
|
104
|
+
async function inspectRunKitMarker(directory) {
|
|
73
105
|
const owlRoot = path.join(directory, ".owlcoda");
|
|
74
106
|
const runkitRoot = path.join(owlRoot, "runkit");
|
|
75
107
|
const configPath = path.join(runkitRoot, "config.json");
|
|
@@ -78,7 +110,7 @@ async function hasRunKitConfig(directory) {
|
|
|
78
110
|
try {
|
|
79
111
|
owlStat = await lstat(owlRoot);
|
|
80
112
|
} catch (error) {
|
|
81
|
-
if (error?.code === "ENOENT") return
|
|
113
|
+
if (error?.code === "ENOENT") return { status: "none" };
|
|
82
114
|
throw error;
|
|
83
115
|
}
|
|
84
116
|
if (
|
|
@@ -86,14 +118,14 @@ async function hasRunKitConfig(directory) {
|
|
|
86
118
|
|| !owlStat.isDirectory()
|
|
87
119
|
|| await realpath(owlRoot) !== path.resolve(owlRoot)
|
|
88
120
|
) {
|
|
89
|
-
|
|
121
|
+
return { status: "issue", code: "fleet_project_marker_invalid" };
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
let runkitStat;
|
|
93
125
|
try {
|
|
94
126
|
runkitStat = await lstat(runkitRoot);
|
|
95
127
|
} catch (error) {
|
|
96
|
-
if (error?.code === "ENOENT") return
|
|
128
|
+
if (error?.code === "ENOENT") return { status: "none" };
|
|
97
129
|
throw error;
|
|
98
130
|
}
|
|
99
131
|
if (
|
|
@@ -101,7 +133,7 @@ async function hasRunKitConfig(directory) {
|
|
|
101
133
|
|| !runkitStat.isDirectory()
|
|
102
134
|
|| await realpath(runkitRoot) !== path.resolve(runkitRoot)
|
|
103
135
|
) {
|
|
104
|
-
|
|
136
|
+
return { status: "issue", code: "fleet_project_marker_invalid" };
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
let configStat;
|
|
@@ -109,7 +141,7 @@ async function hasRunKitConfig(directory) {
|
|
|
109
141
|
configStat = await lstat(configPath);
|
|
110
142
|
} catch (error) {
|
|
111
143
|
if (error?.code === "ENOENT") {
|
|
112
|
-
|
|
144
|
+
return { status: "issue", code: "fleet_project_marker_incomplete" };
|
|
113
145
|
}
|
|
114
146
|
throw error;
|
|
115
147
|
}
|
|
@@ -118,23 +150,76 @@ async function hasRunKitConfig(directory) {
|
|
|
118
150
|
|| !configStat.isFile()
|
|
119
151
|
|| await realpath(configPath) !== path.resolve(configPath)
|
|
120
152
|
) {
|
|
121
|
-
|
|
153
|
+
return { status: "issue", code: "fleet_project_marker_invalid" };
|
|
122
154
|
}
|
|
123
|
-
return
|
|
155
|
+
return { status: "project" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function inspectRepositoryBoundary(directory) {
|
|
159
|
+
const markerPath = path.join(directory, ".git");
|
|
160
|
+
let marker;
|
|
161
|
+
try {
|
|
162
|
+
marker = await lstat(markerPath);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error?.code === "ENOENT") return { status: "none" };
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
if (marker.isSymbolicLink() || (!marker.isDirectory() && !marker.isFile())) {
|
|
168
|
+
return { status: "issue", code: "fleet_repository_marker_invalid" };
|
|
169
|
+
}
|
|
170
|
+
return { status: "repository" };
|
|
124
171
|
}
|
|
125
172
|
|
|
126
173
|
async function scanFleetRoot(fleetRoot) {
|
|
127
174
|
const requested = path.resolve(fleetRoot);
|
|
128
175
|
const canonicalRoot = await canonicalWorkspace(requested);
|
|
129
176
|
const projects = [];
|
|
177
|
+
const issues = [];
|
|
130
178
|
let visitedDirectories = 0;
|
|
179
|
+
let scanStopped = false;
|
|
131
180
|
async function visit(directory, depth) {
|
|
132
|
-
if (
|
|
181
|
+
if (scanStopped) return;
|
|
182
|
+
if (depth > MAX_SCAN_DEPTH) {
|
|
183
|
+
issues.push({
|
|
184
|
+
code: "fleet_scan_depth_exceeded",
|
|
185
|
+
path: directory,
|
|
186
|
+
coverageRoot: canonicalRoot,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
133
190
|
visitedDirectories += 1;
|
|
134
191
|
if (visitedDirectories > MAX_SCAN_DIRECTORIES) {
|
|
135
|
-
|
|
192
|
+
scanStopped = true;
|
|
193
|
+
issues.push({
|
|
194
|
+
code: "fleet_scan_directory_limit_exceeded",
|
|
195
|
+
path: directory,
|
|
196
|
+
coverageRoot: canonicalRoot,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const marker = await inspectRunKitMarker(directory);
|
|
201
|
+
if (marker.status === "project") {
|
|
202
|
+
projects.push(directory);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (marker.status === "issue") {
|
|
206
|
+
issues.push({
|
|
207
|
+
code: marker.code,
|
|
208
|
+
path: directory,
|
|
209
|
+
coverageRoot: canonicalRoot,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const repository = await inspectRepositoryBoundary(directory);
|
|
214
|
+
if (repository.status === "repository") return;
|
|
215
|
+
if (repository.status === "issue") {
|
|
216
|
+
issues.push({
|
|
217
|
+
code: repository.code,
|
|
218
|
+
path: directory,
|
|
219
|
+
coverageRoot: canonicalRoot,
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
136
222
|
}
|
|
137
|
-
if (await hasRunKitConfig(directory)) projects.push(directory);
|
|
138
223
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
139
224
|
entries.sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
140
225
|
for (const entry of entries) {
|
|
@@ -147,6 +232,7 @@ async function scanFleetRoot(fleetRoot) {
|
|
|
147
232
|
return {
|
|
148
233
|
coverageRoot: canonicalRoot,
|
|
149
234
|
workspaceRoots: projects,
|
|
235
|
+
issues,
|
|
150
236
|
};
|
|
151
237
|
}
|
|
152
238
|
|
|
@@ -168,6 +254,7 @@ async function discoverFromFleetRoots(
|
|
|
168
254
|
const coverageRoots = [];
|
|
169
255
|
const unreachableRoots = [];
|
|
170
256
|
const workspaceRoots = [];
|
|
257
|
+
const issues = [];
|
|
171
258
|
for (const fleetRoot of fleetRoots) {
|
|
172
259
|
if (typeof fleetRoot !== "string" || fleetRoot.length === 0) {
|
|
173
260
|
throw new Error("fleet_root_invalid");
|
|
@@ -176,13 +263,25 @@ async function discoverFromFleetRoots(
|
|
|
176
263
|
const scanned = await scanFleetRoot(fleetRoot);
|
|
177
264
|
coverageRoots.push(scanned.coverageRoot);
|
|
178
265
|
workspaceRoots.push(...scanned.workspaceRoots);
|
|
179
|
-
|
|
266
|
+
issues.push(...scanned.issues);
|
|
267
|
+
} catch (error) {
|
|
180
268
|
unreachableRoots.push(path.resolve(fleetRoot));
|
|
269
|
+
issues.push({
|
|
270
|
+
code: error instanceof Error ? error.message : "fleet_root_unreachable",
|
|
271
|
+
path: path.resolve(fleetRoot),
|
|
272
|
+
coverageRoot: path.resolve(fleetRoot),
|
|
273
|
+
});
|
|
181
274
|
}
|
|
182
275
|
}
|
|
183
276
|
const normalizedCoverage = [...new Set(coverageRoots)].sort(compareCodeUnits);
|
|
184
277
|
const normalizedUnreachable = [...new Set(unreachableRoots)].sort(compareCodeUnits);
|
|
185
278
|
const normalizedWorkspaces = [...new Set(workspaceRoots)].sort(compareCodeUnits);
|
|
279
|
+
issues.sort((left, right) => (
|
|
280
|
+
compareCodeUnits(left.path, right.path)
|
|
281
|
+
|| compareCodeUnits(left.code, right.code)
|
|
282
|
+
));
|
|
283
|
+
const classifications = emptyClassifications();
|
|
284
|
+
classifications.active = normalizedWorkspaces;
|
|
186
285
|
if (normalizedWorkspaces.length > MAX_FLEET_WORKSPACES) {
|
|
187
286
|
throw new Error("fleet_workspace_limit_exceeded");
|
|
188
287
|
}
|
|
@@ -195,15 +294,198 @@ async function discoverFromFleetRoots(
|
|
|
195
294
|
coverageRoots: normalizedCoverage,
|
|
196
295
|
unreachableRoots: normalizedUnreachable,
|
|
197
296
|
workspaceRoots: normalizedWorkspaces,
|
|
198
|
-
|
|
297
|
+
classifications,
|
|
298
|
+
issues,
|
|
299
|
+
complete: normalizedUnreachable.length === 0 && issues.length === 0,
|
|
199
300
|
frozenManifestSha256: frozenManifestSha256({
|
|
200
301
|
coverageRoots: normalizedCoverage,
|
|
201
302
|
workspaceRoots: normalizedWorkspaces,
|
|
303
|
+
classifications,
|
|
202
304
|
}),
|
|
203
305
|
authorizationGranted: false,
|
|
204
306
|
};
|
|
205
307
|
}
|
|
206
308
|
|
|
309
|
+
function expectedMembershipEntryKeys(classification) {
|
|
310
|
+
if (classification === "active") return ["classification", "path"];
|
|
311
|
+
return ["classification", "evidence", "path", "reasonCode"];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function validateMembership(
|
|
315
|
+
membership,
|
|
316
|
+
coverageRoots,
|
|
317
|
+
{ verifyEvidence = false } = {},
|
|
318
|
+
) {
|
|
319
|
+
if (
|
|
320
|
+
membership === null
|
|
321
|
+
|| typeof membership !== "object"
|
|
322
|
+
|| Array.isArray(membership)
|
|
323
|
+
|| Object.keys(membership).some(key => ![
|
|
324
|
+
"authorizationGranted",
|
|
325
|
+
"entries",
|
|
326
|
+
"manifestSha256",
|
|
327
|
+
"schemaVersion",
|
|
328
|
+
].includes(key))
|
|
329
|
+
|| membership.schemaVersion !== FLEET_MEMBERSHIP_SCHEMA
|
|
330
|
+
|| membership.authorizationGranted !== false
|
|
331
|
+
|| !Array.isArray(membership.entries)
|
|
332
|
+
|| membership.entries.length === 0
|
|
333
|
+
|| membership.entries.length > MAX_FLEET_WORKSPACES
|
|
334
|
+
) throw new Error("fleet_membership_invalid");
|
|
335
|
+
|
|
336
|
+
const normalizedEntries = [];
|
|
337
|
+
const issues = [];
|
|
338
|
+
for (const entry of membership.entries) {
|
|
339
|
+
if (
|
|
340
|
+
entry === null
|
|
341
|
+
|| typeof entry !== "object"
|
|
342
|
+
|| Array.isArray(entry)
|
|
343
|
+
|| !["active", "retired", "excluded", "invalid"].includes(
|
|
344
|
+
entry.classification,
|
|
345
|
+
)
|
|
346
|
+
|| Object.keys(entry).sort().join(",") !== expectedMembershipEntryKeys(
|
|
347
|
+
entry.classification,
|
|
348
|
+
).sort().join(",")
|
|
349
|
+
|| typeof entry.path !== "string"
|
|
350
|
+
|| entry.path.length === 0
|
|
351
|
+
|| !path.isAbsolute(entry.path)
|
|
352
|
+
) throw new Error("fleet_membership_entry_invalid");
|
|
353
|
+
const normalizedPath = path.resolve(entry.path);
|
|
354
|
+
if (!coverageRoots.some(root => within(root, normalizedPath))) {
|
|
355
|
+
throw new Error("fleet_membership_entry_outside_coverage");
|
|
356
|
+
}
|
|
357
|
+
if (entry.classification !== "active") {
|
|
358
|
+
if (
|
|
359
|
+
typeof entry.reasonCode !== "string"
|
|
360
|
+
|| entry.reasonCode.length === 0
|
|
361
|
+
|| entry.evidence === null
|
|
362
|
+
|| typeof entry.evidence !== "object"
|
|
363
|
+
|| Array.isArray(entry.evidence)
|
|
364
|
+
|| Object.keys(entry.evidence).sort().join(",") !== "path,sha256"
|
|
365
|
+
|| typeof entry.evidence.path !== "string"
|
|
366
|
+
|| !path.isAbsolute(entry.evidence.path)
|
|
367
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(entry.evidence.sha256 ?? "")
|
|
368
|
+
) throw new Error("fleet_membership_evidence_invalid");
|
|
369
|
+
}
|
|
370
|
+
normalizedEntries.push({
|
|
371
|
+
...entry,
|
|
372
|
+
path: normalizedPath,
|
|
373
|
+
...(entry.evidence === undefined
|
|
374
|
+
? {}
|
|
375
|
+
: {
|
|
376
|
+
evidence: {
|
|
377
|
+
path: path.resolve(entry.evidence.path),
|
|
378
|
+
sha256: entry.evidence.sha256,
|
|
379
|
+
},
|
|
380
|
+
}),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
normalizedEntries.sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
384
|
+
if (new Set(normalizedEntries.map(entry => entry.path)).size !== normalizedEntries.length) {
|
|
385
|
+
throw new Error("fleet_membership_path_duplicate");
|
|
386
|
+
}
|
|
387
|
+
const body = {
|
|
388
|
+
schemaVersion: FLEET_MEMBERSHIP_SCHEMA,
|
|
389
|
+
entries: normalizedEntries,
|
|
390
|
+
authorizationGranted: false,
|
|
391
|
+
};
|
|
392
|
+
const manifestSha256 = sha256Json(body);
|
|
393
|
+
if (
|
|
394
|
+
membership.manifestSha256 !== undefined
|
|
395
|
+
&& membership.manifestSha256 !== manifestSha256
|
|
396
|
+
) throw new Error("fleet_membership_hash_mismatch");
|
|
397
|
+
|
|
398
|
+
if (verifyEvidence) {
|
|
399
|
+
for (const entry of normalizedEntries) {
|
|
400
|
+
if (entry.classification === "active") {
|
|
401
|
+
try {
|
|
402
|
+
const canonical = await canonicalWorkspace(entry.path);
|
|
403
|
+
const marker = await inspectRunKitMarker(canonical);
|
|
404
|
+
if (marker.status !== "project") {
|
|
405
|
+
issues.push({
|
|
406
|
+
code: marker.code ?? "fleet_active_project_marker_missing",
|
|
407
|
+
path: entry.path,
|
|
408
|
+
coverageRoot: coverageRoots.find(root => within(root, entry.path)),
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
entry.path = canonical;
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
issues.push({
|
|
415
|
+
code: error instanceof Error
|
|
416
|
+
? error.message
|
|
417
|
+
: "fleet_active_project_unreadable",
|
|
418
|
+
path: entry.path,
|
|
419
|
+
coverageRoot: coverageRoots.find(root => within(root, entry.path)),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
let evidence;
|
|
425
|
+
try {
|
|
426
|
+
evidence = await readManifestBounded(entry.evidence.path);
|
|
427
|
+
if (evidence.fileSha256 !== entry.evidence.sha256) {
|
|
428
|
+
throw new Error("fleet_membership_evidence_hash_mismatch");
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
issues.push({
|
|
432
|
+
code: error instanceof Error
|
|
433
|
+
? error.message
|
|
434
|
+
: "fleet_membership_evidence_invalid",
|
|
435
|
+
path: entry.path,
|
|
436
|
+
coverageRoot: coverageRoots.find(root => within(root, entry.path)),
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (entry.classification === "retired") {
|
|
441
|
+
const receipt = evidence.manifest;
|
|
442
|
+
if (
|
|
443
|
+
receipt?.status !== "retired"
|
|
444
|
+
|| path.resolve(receipt.sourceProjectPath ?? "") !== entry.path
|
|
445
|
+
|| receipt.sourceRemoval?.sourcePathAbsent !== true
|
|
446
|
+
|| receipt.revokedControlFacts?.execution?.disposition
|
|
447
|
+
!== "explicitly_voided"
|
|
448
|
+
|| receipt.revokedControlFacts?.lease?.disposition
|
|
449
|
+
!== "explicitly_voided"
|
|
450
|
+
|| receipt.archive?.manifestsIdentical !== true
|
|
451
|
+
|| receipt.retirementAuthorization !== "explicit_user_authorization"
|
|
452
|
+
|| receipt.authorizationGranted !== false
|
|
453
|
+
|| await pathExists(path.join(entry.path, ".owlcoda/runkit"))
|
|
454
|
+
) {
|
|
455
|
+
issues.push({
|
|
456
|
+
code: "fleet_retirement_evidence_invalid",
|
|
457
|
+
path: entry.path,
|
|
458
|
+
coverageRoot: coverageRoots.find(root => within(root, entry.path)),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
const matched = evidence.manifest?.schemaVersion
|
|
463
|
+
=== "OwlCodaRunKitFleetClassificationEvidenceV1"
|
|
464
|
+
&& evidence.manifest.authorizationGranted === false
|
|
465
|
+
&& Array.isArray(evidence.manifest.entries)
|
|
466
|
+
&& evidence.manifest.entries.some(candidate => (
|
|
467
|
+
path.resolve(candidate?.path ?? "") === entry.path
|
|
468
|
+
&& candidate.classification === entry.classification
|
|
469
|
+
&& candidate.reasonCode === entry.reasonCode
|
|
470
|
+
));
|
|
471
|
+
if (!matched) {
|
|
472
|
+
issues.push({
|
|
473
|
+
code: "fleet_classification_evidence_invalid",
|
|
474
|
+
path: entry.path,
|
|
475
|
+
coverageRoot: coverageRoots.find(root => within(root, entry.path)),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
...body,
|
|
484
|
+
manifestSha256,
|
|
485
|
+
issues,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
207
489
|
async function readFleetRegistry(fleetRegistryPath) {
|
|
208
490
|
const selected = path.resolve(
|
|
209
491
|
fleetRegistryPath ?? defaultFleetRegistryPath(),
|
|
@@ -216,32 +498,77 @@ async function readFleetRegistry(fleetRegistryPath) {
|
|
|
216
498
|
throw error;
|
|
217
499
|
}
|
|
218
500
|
const registry = loaded.manifest;
|
|
219
|
-
|
|
501
|
+
const commonInvalid = (
|
|
220
502
|
registry === null
|
|
221
503
|
|| typeof registry !== "object"
|
|
222
504
|
|| Array.isArray(registry)
|
|
223
|
-
|| Object.keys(registry).sort().join(",")
|
|
224
|
-
!== "authorizationGranted,coverageRoots,registrySha256,schemaVersion"
|
|
225
|
-
|| registry.schemaVersion !== FLEET_REGISTRY_SCHEMA
|
|
226
505
|
|| registry.authorizationGranted !== false
|
|
227
506
|
|| !Array.isArray(registry.coverageRoots)
|
|
228
507
|
|| registry.coverageRoots.length === 0
|
|
229
508
|
|| registry.coverageRoots.length > MAX_FLEET_ROOTS
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
509
|
+
);
|
|
510
|
+
if (commonInvalid) throw new Error("fleet_registry_invalid");
|
|
511
|
+
const v1 = registry.schemaVersion === FLEET_REGISTRY_SCHEMA
|
|
512
|
+
&& Object.keys(registry).sort().join(",")
|
|
513
|
+
=== "authorizationGranted,coverageRoots,registrySha256,schemaVersion";
|
|
514
|
+
const v2 = registry.schemaVersion === FLEET_REGISTRY_SCHEMA_V2
|
|
515
|
+
&& Object.keys(registry).sort().join(",")
|
|
516
|
+
=== "authorizationGranted,coverageRoots,membership,registrySha256,schemaVersion";
|
|
517
|
+
if (!v1 && !v2) throw new Error("fleet_registry_invalid");
|
|
518
|
+
const membership = v2
|
|
519
|
+
? await validateMembership(registry.membership, registry.coverageRoots)
|
|
520
|
+
: null;
|
|
521
|
+
const body = v1
|
|
522
|
+
? {
|
|
523
|
+
schemaVersion: registry.schemaVersion,
|
|
524
|
+
coverageRoots: registry.coverageRoots,
|
|
525
|
+
authorizationGranted: false,
|
|
526
|
+
}
|
|
527
|
+
: {
|
|
528
|
+
schemaVersion: registry.schemaVersion,
|
|
529
|
+
coverageRoots: registry.coverageRoots,
|
|
530
|
+
membership: {
|
|
531
|
+
schemaVersion: membership.schemaVersion,
|
|
532
|
+
entries: membership.entries,
|
|
533
|
+
authorizationGranted: false,
|
|
534
|
+
manifestSha256: membership.manifestSha256,
|
|
535
|
+
},
|
|
536
|
+
authorizationGranted: false,
|
|
537
|
+
};
|
|
238
538
|
if (registry.registrySha256 !== sha256Json(body)) {
|
|
239
539
|
throw new Error("fleet_registry_hash_mismatch");
|
|
240
540
|
}
|
|
241
541
|
return {
|
|
242
542
|
registryPath: loaded.manifestPath,
|
|
543
|
+
schemaVersion: registry.schemaVersion,
|
|
243
544
|
registrySha256: registry.registrySha256,
|
|
545
|
+
fileSha256: loaded.fileSha256,
|
|
546
|
+
fileBytes: loaded.bytes,
|
|
244
547
|
coverageRoots: [...registry.coverageRoots],
|
|
548
|
+
membership: membership === null
|
|
549
|
+
? null
|
|
550
|
+
: {
|
|
551
|
+
schemaVersion: membership.schemaVersion,
|
|
552
|
+
entries: membership.entries,
|
|
553
|
+
authorizationGranted: false,
|
|
554
|
+
manifestSha256: membership.manifestSha256,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function inspectFleetRegistry({
|
|
560
|
+
fleetRegistryPath = defaultFleetRegistryPath(),
|
|
561
|
+
} = {}) {
|
|
562
|
+
const registry = await readFleetRegistry(fleetRegistryPath);
|
|
563
|
+
return {
|
|
564
|
+
status: "fleet_registry_inspected",
|
|
565
|
+
schemaVersion: registry.schemaVersion,
|
|
566
|
+
registryPath: registry.registryPath,
|
|
567
|
+
registrySha256: registry.registrySha256,
|
|
568
|
+
fileSha256: registry.fileSha256,
|
|
569
|
+
coverageRoots: registry.coverageRoots,
|
|
570
|
+
membership: registry.membership,
|
|
571
|
+
authorizationGranted: false,
|
|
245
572
|
};
|
|
246
573
|
}
|
|
247
574
|
|
|
@@ -311,6 +638,342 @@ export async function registerFleetCoverageRoot({
|
|
|
311
638
|
}
|
|
312
639
|
}
|
|
313
640
|
|
|
641
|
+
async function pathExists(selected) {
|
|
642
|
+
try {
|
|
643
|
+
await lstat(selected);
|
|
644
|
+
return true;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error?.code === "ENOENT") return false;
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function validateRemovedCoverageEvidence(removedRoots, entries) {
|
|
652
|
+
if (!Array.isArray(entries)) {
|
|
653
|
+
throw new Error("fleet_registry_removed_coverage_evidence_invalid");
|
|
654
|
+
}
|
|
655
|
+
const byRoot = new Map();
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
if (
|
|
658
|
+
entry === null
|
|
659
|
+
|| typeof entry !== "object"
|
|
660
|
+
|| Array.isArray(entry)
|
|
661
|
+
|| Object.keys(entry).sort().join(",") !== "coverageRoot,evidence"
|
|
662
|
+
|| typeof entry.coverageRoot !== "string"
|
|
663
|
+
|| !path.isAbsolute(entry.coverageRoot)
|
|
664
|
+
|| entry.evidence === null
|
|
665
|
+
|| typeof entry.evidence !== "object"
|
|
666
|
+
|| Array.isArray(entry.evidence)
|
|
667
|
+
|| Object.keys(entry.evidence).sort().join(",") !== "path,sha256"
|
|
668
|
+
|| typeof entry.evidence.path !== "string"
|
|
669
|
+
|| !path.isAbsolute(entry.evidence.path)
|
|
670
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(entry.evidence.sha256 ?? "")
|
|
671
|
+
) throw new Error("fleet_registry_removed_coverage_evidence_invalid");
|
|
672
|
+
const coverageRoot = path.resolve(entry.coverageRoot);
|
|
673
|
+
if (byRoot.has(coverageRoot)) {
|
|
674
|
+
throw new Error("fleet_registry_removed_coverage_evidence_invalid");
|
|
675
|
+
}
|
|
676
|
+
const loaded = await readManifestBounded(entry.evidence.path);
|
|
677
|
+
if (
|
|
678
|
+
loaded.fileSha256 !== entry.evidence.sha256
|
|
679
|
+
|| loaded.manifest?.schemaVersion
|
|
680
|
+
!== "OwlCodaRunKitFleetCoverageExclusionEvidenceV1"
|
|
681
|
+
|| path.resolve(loaded.manifest.coverageRoot ?? "") !== coverageRoot
|
|
682
|
+
|| typeof loaded.manifest.reasonCode !== "string"
|
|
683
|
+
|| loaded.manifest.reasonCode.length === 0
|
|
684
|
+
|| loaded.manifest.authorizationGranted !== false
|
|
685
|
+
) throw new Error("fleet_registry_removed_coverage_evidence_invalid");
|
|
686
|
+
byRoot.set(coverageRoot, {
|
|
687
|
+
coverageRoot,
|
|
688
|
+
evidence: {
|
|
689
|
+
path: loaded.manifestPath,
|
|
690
|
+
sha256: loaded.fileSha256,
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
removedRoots.length !== byRoot.size
|
|
696
|
+
|| removedRoots.some(root => !byRoot.has(root))
|
|
697
|
+
) throw new Error("fleet_registry_coverage_reduction_evidence_required");
|
|
698
|
+
return [...byRoot.values()].sort((left, right) => (
|
|
699
|
+
compareCodeUnits(left.coverageRoot, right.coverageRoot)
|
|
700
|
+
));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function registryReplacementResult({
|
|
704
|
+
status,
|
|
705
|
+
registryPath,
|
|
706
|
+
before,
|
|
707
|
+
afterRegistry,
|
|
708
|
+
afterBytes,
|
|
709
|
+
coverageDelta,
|
|
710
|
+
receiptPath = null,
|
|
711
|
+
}) {
|
|
712
|
+
return {
|
|
713
|
+
status,
|
|
714
|
+
registryPath,
|
|
715
|
+
beforeRegistrySha256: before.registrySha256,
|
|
716
|
+
afterRegistrySha256: afterRegistry.registrySha256,
|
|
717
|
+
beforeFileSha256: before.fileSha256,
|
|
718
|
+
afterFileSha256: sha256Bytes(afterBytes),
|
|
719
|
+
coverageDelta,
|
|
720
|
+
...(receiptPath === null ? {} : { receiptPath }),
|
|
721
|
+
authorizationGranted: false,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function replaceFleetRegistry({
|
|
726
|
+
replacementRequestPath,
|
|
727
|
+
receiptPath,
|
|
728
|
+
dryRun = false,
|
|
729
|
+
fleetRegistryPath = defaultFleetRegistryPath(),
|
|
730
|
+
} = {}) {
|
|
731
|
+
const request = await readManifestBounded(replacementRequestPath);
|
|
732
|
+
if (
|
|
733
|
+
request.manifest === null
|
|
734
|
+
|| typeof request.manifest !== "object"
|
|
735
|
+
|| Array.isArray(request.manifest)
|
|
736
|
+
|| Object.keys(request.manifest).sort().join(",")
|
|
737
|
+
!== "authorizationGranted,coverageRoots,expectedRegistrySha256,membership,removedCoverageEvidence,schemaVersion"
|
|
738
|
+
|| request.manifest.schemaVersion
|
|
739
|
+
!== "OwlCodaRunKitFleetRegistryReplacementRequestV1"
|
|
740
|
+
|| request.manifest.authorizationGranted !== false
|
|
741
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(
|
|
742
|
+
request.manifest.expectedRegistrySha256 ?? "",
|
|
743
|
+
)
|
|
744
|
+
|| !Array.isArray(request.manifest.coverageRoots)
|
|
745
|
+
|| request.manifest.coverageRoots.length === 0
|
|
746
|
+
|| request.manifest.coverageRoots.length > MAX_FLEET_ROOTS
|
|
747
|
+
) throw new Error("fleet_registry_replacement_request_invalid");
|
|
748
|
+
|
|
749
|
+
const before = await readFleetRegistry(fleetRegistryPath);
|
|
750
|
+
if (before.registrySha256 !== request.manifest.expectedRegistrySha256) {
|
|
751
|
+
throw new Error("fleet_registry_replacement_preimage_mismatch");
|
|
752
|
+
}
|
|
753
|
+
const coverageRoots = [];
|
|
754
|
+
for (const fleetRoot of request.manifest.coverageRoots) {
|
|
755
|
+
coverageRoots.push(await canonicalWorkspace(fleetRoot));
|
|
756
|
+
}
|
|
757
|
+
const normalizedCoverage = [...new Set(coverageRoots)].sort(compareCodeUnits);
|
|
758
|
+
if (normalizedCoverage.length !== request.manifest.coverageRoots.length) {
|
|
759
|
+
throw new Error("fleet_registry_replacement_coverage_duplicate");
|
|
760
|
+
}
|
|
761
|
+
const removed = before.coverageRoots
|
|
762
|
+
.filter(root => !normalizedCoverage.includes(root))
|
|
763
|
+
.sort(compareCodeUnits);
|
|
764
|
+
const added = normalizedCoverage
|
|
765
|
+
.filter(root => !before.coverageRoots.includes(root))
|
|
766
|
+
.sort(compareCodeUnits);
|
|
767
|
+
await validateRemovedCoverageEvidence(
|
|
768
|
+
removed,
|
|
769
|
+
request.manifest.removedCoverageEvidence,
|
|
770
|
+
);
|
|
771
|
+
const membership = await validateMembership(
|
|
772
|
+
request.manifest.membership,
|
|
773
|
+
normalizedCoverage,
|
|
774
|
+
{ verifyEvidence: true },
|
|
775
|
+
);
|
|
776
|
+
if (membership.issues.length > 0) {
|
|
777
|
+
throw new Error(`fleet_registry_membership_invalid:${membership.issues
|
|
778
|
+
.map(issue => `${issue.code}:${issue.path}`)
|
|
779
|
+
.join(",")}`);
|
|
780
|
+
}
|
|
781
|
+
const membershipBody = {
|
|
782
|
+
schemaVersion: membership.schemaVersion,
|
|
783
|
+
entries: membership.entries,
|
|
784
|
+
authorizationGranted: false,
|
|
785
|
+
manifestSha256: membership.manifestSha256,
|
|
786
|
+
};
|
|
787
|
+
const body = {
|
|
788
|
+
schemaVersion: FLEET_REGISTRY_SCHEMA_V2,
|
|
789
|
+
coverageRoots: normalizedCoverage,
|
|
790
|
+
membership: membershipBody,
|
|
791
|
+
authorizationGranted: false,
|
|
792
|
+
};
|
|
793
|
+
const afterRegistry = {
|
|
794
|
+
...body,
|
|
795
|
+
registrySha256: sha256Json(body),
|
|
796
|
+
};
|
|
797
|
+
const afterBytes = Buffer.from(`${JSON.stringify(afterRegistry, null, 2)}\n`);
|
|
798
|
+
const coverageDelta = { added, removed };
|
|
799
|
+
if (dryRun) {
|
|
800
|
+
return registryReplacementResult({
|
|
801
|
+
status: "fleet_registry_replacement_dry_run",
|
|
802
|
+
registryPath: before.registryPath,
|
|
803
|
+
before,
|
|
804
|
+
afterRegistry,
|
|
805
|
+
afterBytes,
|
|
806
|
+
coverageDelta,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (typeof receiptPath !== "string" || receiptPath.length === 0) {
|
|
810
|
+
throw new Error("fleet_registry_replacement_receipt_required");
|
|
811
|
+
}
|
|
812
|
+
const resolvedReceipt = path.resolve(receiptPath);
|
|
813
|
+
if (await pathExists(resolvedReceipt)) {
|
|
814
|
+
throw new Error("fleet_registry_replacement_receipt_exists");
|
|
815
|
+
}
|
|
816
|
+
const registryPath = before.registryPath;
|
|
817
|
+
const lockPath = `${registryPath}.lock`;
|
|
818
|
+
try {
|
|
819
|
+
await mkdir(lockPath);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (error?.code === "EEXIST") throw new Error("fleet_registry_transaction_active");
|
|
822
|
+
throw error;
|
|
823
|
+
}
|
|
824
|
+
const registryTemporary = `${registryPath}.tmp-${process.pid}`;
|
|
825
|
+
const receiptTemporary = `${resolvedReceipt}.tmp-${process.pid}`;
|
|
826
|
+
let registryReplaced = false;
|
|
827
|
+
try {
|
|
828
|
+
const current = await readFleetRegistry(registryPath);
|
|
829
|
+
if (
|
|
830
|
+
current.registrySha256 !== before.registrySha256
|
|
831
|
+
|| current.fileSha256 !== before.fileSha256
|
|
832
|
+
) throw new Error("fleet_registry_changed_during_replacement");
|
|
833
|
+
await mkdir(path.dirname(resolvedReceipt), { recursive: true, mode: 0o700 });
|
|
834
|
+
const receipt = {
|
|
835
|
+
schemaVersion: "OwlCodaRunKitFleetRegistryReplacementReceiptV1",
|
|
836
|
+
requestPath: request.manifestPath,
|
|
837
|
+
requestSha256: request.fileSha256,
|
|
838
|
+
registryPath,
|
|
839
|
+
beforeRegistrySha256: before.registrySha256,
|
|
840
|
+
afterRegistrySha256: afterRegistry.registrySha256,
|
|
841
|
+
beforeFileSha256: before.fileSha256,
|
|
842
|
+
afterFileSha256: sha256Bytes(afterBytes),
|
|
843
|
+
previousRegistryBytesBase64: Buffer.from(before.fileBytes).toString("base64"),
|
|
844
|
+
coverageDelta,
|
|
845
|
+
rollbackAvailable: true,
|
|
846
|
+
authorizationGranted: false,
|
|
847
|
+
};
|
|
848
|
+
await writeFile(registryTemporary, afterBytes, { flag: "wx", mode: 0o600 });
|
|
849
|
+
await writeFile(
|
|
850
|
+
receiptTemporary,
|
|
851
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
852
|
+
{ flag: "wx", mode: 0o600 },
|
|
853
|
+
);
|
|
854
|
+
await rename(registryTemporary, registryPath);
|
|
855
|
+
registryReplaced = true;
|
|
856
|
+
await rename(receiptTemporary, resolvedReceipt);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (registryReplaced) {
|
|
859
|
+
const rollbackTemporary = `${registryPath}.rollback-${process.pid}`;
|
|
860
|
+
await writeFile(rollbackTemporary, before.fileBytes, {
|
|
861
|
+
flag: "wx",
|
|
862
|
+
mode: 0o600,
|
|
863
|
+
});
|
|
864
|
+
await rename(rollbackTemporary, registryPath);
|
|
865
|
+
}
|
|
866
|
+
throw error;
|
|
867
|
+
} finally {
|
|
868
|
+
await rm(registryTemporary, { force: true });
|
|
869
|
+
await rm(receiptTemporary, { force: true });
|
|
870
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
871
|
+
}
|
|
872
|
+
return registryReplacementResult({
|
|
873
|
+
status: "fleet_registry_replaced",
|
|
874
|
+
registryPath,
|
|
875
|
+
before,
|
|
876
|
+
afterRegistry,
|
|
877
|
+
afterBytes,
|
|
878
|
+
coverageDelta,
|
|
879
|
+
receiptPath: resolvedReceipt,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export async function rollbackFleetRegistry({
|
|
884
|
+
replacementReceiptPath,
|
|
885
|
+
rollbackReceiptPath,
|
|
886
|
+
fleetRegistryPath = defaultFleetRegistryPath(),
|
|
887
|
+
} = {}) {
|
|
888
|
+
const replacement = await readManifestBounded(replacementReceiptPath);
|
|
889
|
+
const receipt = replacement.manifest;
|
|
890
|
+
if (
|
|
891
|
+
receipt?.schemaVersion
|
|
892
|
+
!== "OwlCodaRunKitFleetRegistryReplacementReceiptV1"
|
|
893
|
+
|| receipt.authorizationGranted !== false
|
|
894
|
+
|| receipt.rollbackAvailable !== true
|
|
895
|
+
|| typeof receipt.previousRegistryBytesBase64 !== "string"
|
|
896
|
+
) throw new Error("fleet_registry_rollback_receipt_invalid");
|
|
897
|
+
const current = await readFleetRegistry(fleetRegistryPath);
|
|
898
|
+
if (
|
|
899
|
+
current.registryPath !== path.resolve(receipt.registryPath)
|
|
900
|
+
|| current.registrySha256 !== receipt.afterRegistrySha256
|
|
901
|
+
|| current.fileSha256 !== receipt.afterFileSha256
|
|
902
|
+
) throw new Error("fleet_registry_rollback_current_mismatch");
|
|
903
|
+
const previousBytes = Buffer.from(receipt.previousRegistryBytesBase64, "base64");
|
|
904
|
+
if (sha256Bytes(previousBytes) !== receipt.beforeFileSha256) {
|
|
905
|
+
throw new Error("fleet_registry_rollback_preimage_hash_mismatch");
|
|
906
|
+
}
|
|
907
|
+
const rollbackPath = path.resolve(rollbackReceiptPath);
|
|
908
|
+
if (await pathExists(rollbackPath)) throw new Error("fleet_registry_rollback_receipt_exists");
|
|
909
|
+
const lockPath = `${current.registryPath}.lock`;
|
|
910
|
+
try {
|
|
911
|
+
await mkdir(lockPath);
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (error?.code === "EEXIST") throw new Error("fleet_registry_transaction_active");
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
const registryTemporary = `${current.registryPath}.rollback-${process.pid}`;
|
|
917
|
+
const receiptTemporary = `${rollbackPath}.tmp-${process.pid}`;
|
|
918
|
+
let registryRestored = false;
|
|
919
|
+
try {
|
|
920
|
+
const lockedCurrent = await readFleetRegistry(current.registryPath);
|
|
921
|
+
if (
|
|
922
|
+
lockedCurrent.registrySha256 !== current.registrySha256
|
|
923
|
+
|| lockedCurrent.fileSha256 !== current.fileSha256
|
|
924
|
+
) throw new Error("fleet_registry_changed_during_rollback");
|
|
925
|
+
await mkdir(path.dirname(rollbackPath), { recursive: true, mode: 0o700 });
|
|
926
|
+
const rollbackReceipt = {
|
|
927
|
+
schemaVersion: "OwlCodaRunKitFleetRegistryRollbackReceiptV1",
|
|
928
|
+
replacementReceiptPath: replacement.manifestPath,
|
|
929
|
+
replacementReceiptSha256: replacement.fileSha256,
|
|
930
|
+
registryPath: current.registryPath,
|
|
931
|
+
restoredRegistrySha256: receipt.beforeRegistrySha256,
|
|
932
|
+
restoredFileSha256: receipt.beforeFileSha256,
|
|
933
|
+
authorizationGranted: false,
|
|
934
|
+
};
|
|
935
|
+
await writeFile(registryTemporary, previousBytes, {
|
|
936
|
+
flag: "wx",
|
|
937
|
+
mode: 0o600,
|
|
938
|
+
});
|
|
939
|
+
await writeFile(
|
|
940
|
+
receiptTemporary,
|
|
941
|
+
`${JSON.stringify(rollbackReceipt, null, 2)}\n`,
|
|
942
|
+
{ flag: "wx", mode: 0o600 },
|
|
943
|
+
);
|
|
944
|
+
await rename(registryTemporary, current.registryPath);
|
|
945
|
+
registryRestored = true;
|
|
946
|
+
const restored = await readFleetRegistry(current.registryPath);
|
|
947
|
+
if (
|
|
948
|
+
restored.registrySha256 !== receipt.beforeRegistrySha256
|
|
949
|
+
|| restored.fileSha256 !== receipt.beforeFileSha256
|
|
950
|
+
) throw new Error("fleet_registry_rollback_verification_failed");
|
|
951
|
+
await rename(receiptTemporary, rollbackPath);
|
|
952
|
+
return {
|
|
953
|
+
status: "fleet_registry_rolled_back",
|
|
954
|
+
registryPath: restored.registryPath,
|
|
955
|
+
registrySha256: restored.registrySha256,
|
|
956
|
+
fileSha256: restored.fileSha256,
|
|
957
|
+
rollbackReceiptPath: rollbackPath,
|
|
958
|
+
authorizationGranted: false,
|
|
959
|
+
};
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (registryRestored) {
|
|
962
|
+
const restoreCurrentTemporary = `${current.registryPath}.restore-${process.pid}`;
|
|
963
|
+
await writeFile(restoreCurrentTemporary, current.fileBytes, {
|
|
964
|
+
flag: "wx",
|
|
965
|
+
mode: 0o600,
|
|
966
|
+
});
|
|
967
|
+
await rename(restoreCurrentTemporary, current.registryPath);
|
|
968
|
+
}
|
|
969
|
+
throw error;
|
|
970
|
+
} finally {
|
|
971
|
+
await rm(registryTemporary, { force: true });
|
|
972
|
+
await rm(receiptTemporary, { force: true });
|
|
973
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
314
977
|
async function readManifestBounded(fleetManifestPath) {
|
|
315
978
|
if (typeof fleetManifestPath !== "string" || fleetManifestPath.length === 0) {
|
|
316
979
|
throw new Error("fleet_manifest_path_invalid");
|
|
@@ -346,6 +1009,8 @@ async function readManifestBounded(fleetManifestPath) {
|
|
|
346
1009
|
return {
|
|
347
1010
|
manifest: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
|
348
1011
|
manifestPath: canonicalPath,
|
|
1012
|
+
bytes,
|
|
1013
|
+
fileSha256: sha256Bytes(bytes),
|
|
349
1014
|
};
|
|
350
1015
|
} catch {
|
|
351
1016
|
throw new Error("fleet_manifest_invalid_json");
|
|
@@ -395,6 +1060,70 @@ async function canonicalizeRoots(workspaceRoots, { rejectDuplicates }) {
|
|
|
395
1060
|
return [...new Set(roots)].sort(compareCodeUnits);
|
|
396
1061
|
}
|
|
397
1062
|
|
|
1063
|
+
async function discoverFromRegisteredMembership(registry) {
|
|
1064
|
+
const coverageRoots = [];
|
|
1065
|
+
const unreachableRoots = [];
|
|
1066
|
+
const coverageIssues = [];
|
|
1067
|
+
for (const fleetRoot of registry.coverageRoots) {
|
|
1068
|
+
try {
|
|
1069
|
+
coverageRoots.push(await canonicalWorkspace(fleetRoot));
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
const resolved = path.resolve(fleetRoot);
|
|
1072
|
+
unreachableRoots.push(resolved);
|
|
1073
|
+
coverageIssues.push({
|
|
1074
|
+
code: error instanceof Error
|
|
1075
|
+
? error.message
|
|
1076
|
+
: "fleet_root_unreachable",
|
|
1077
|
+
path: resolved,
|
|
1078
|
+
coverageRoot: resolved,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const normalizedCoverage = [...new Set(coverageRoots)].sort(compareCodeUnits);
|
|
1083
|
+
const normalizedUnreachable = [...new Set(unreachableRoots)].sort(
|
|
1084
|
+
compareCodeUnits,
|
|
1085
|
+
);
|
|
1086
|
+
const declaredCoverage = [...new Set(registry.coverageRoots.map(root => (
|
|
1087
|
+
path.resolve(root)
|
|
1088
|
+
)))].sort(compareCodeUnits);
|
|
1089
|
+
const membership = await validateMembership(
|
|
1090
|
+
registry.membership,
|
|
1091
|
+
declaredCoverage,
|
|
1092
|
+
{ verifyEvidence: true },
|
|
1093
|
+
);
|
|
1094
|
+
const classifications = emptyClassifications();
|
|
1095
|
+
for (const entry of membership.entries) {
|
|
1096
|
+
classifications[entry.classification].push(entry.path);
|
|
1097
|
+
}
|
|
1098
|
+
for (const values of Object.values(classifications)) {
|
|
1099
|
+
values.sort(compareCodeUnits);
|
|
1100
|
+
}
|
|
1101
|
+
const issues = [...coverageIssues, ...membership.issues].sort(
|
|
1102
|
+
(left, right) => (
|
|
1103
|
+
compareCodeUnits(left.path, right.path)
|
|
1104
|
+
|| compareCodeUnits(left.code, right.code)
|
|
1105
|
+
),
|
|
1106
|
+
);
|
|
1107
|
+
return {
|
|
1108
|
+
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
1109
|
+
source: "fleet_registry_membership",
|
|
1110
|
+
registryPath: registry.registryPath,
|
|
1111
|
+
registrySha256: registry.registrySha256,
|
|
1112
|
+
coverageRoots: normalizedCoverage,
|
|
1113
|
+
unreachableRoots: normalizedUnreachable,
|
|
1114
|
+
workspaceRoots: classifications.active,
|
|
1115
|
+
classifications,
|
|
1116
|
+
issues,
|
|
1117
|
+
complete: normalizedUnreachable.length === 0 && issues.length === 0,
|
|
1118
|
+
frozenManifestSha256: frozenManifestSha256({
|
|
1119
|
+
coverageRoots: normalizedCoverage,
|
|
1120
|
+
workspaceRoots: classifications.active,
|
|
1121
|
+
classifications,
|
|
1122
|
+
}),
|
|
1123
|
+
authorizationGranted: false,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
398
1127
|
export async function discoverFleet({
|
|
399
1128
|
workspaceRoots = null,
|
|
400
1129
|
fleetManifestPath = null,
|
|
@@ -414,6 +1143,8 @@ export async function discoverFleet({
|
|
|
414
1143
|
manifest.workspaceRoots,
|
|
415
1144
|
{ rejectDuplicates: true },
|
|
416
1145
|
);
|
|
1146
|
+
const classifications = emptyClassifications();
|
|
1147
|
+
classifications.active = normalizedWorkspaces;
|
|
417
1148
|
return {
|
|
418
1149
|
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
419
1150
|
source: "fleet_manifest",
|
|
@@ -421,16 +1152,22 @@ export async function discoverFleet({
|
|
|
421
1152
|
coverageRoots: [],
|
|
422
1153
|
unreachableRoots: [],
|
|
423
1154
|
workspaceRoots: normalizedWorkspaces,
|
|
1155
|
+
classifications,
|
|
1156
|
+
issues: [],
|
|
424
1157
|
complete: true,
|
|
425
1158
|
frozenManifestSha256: frozenManifestSha256({
|
|
426
1159
|
coverageRoots: [],
|
|
427
1160
|
workspaceRoots: normalizedWorkspaces,
|
|
1161
|
+
classifications,
|
|
428
1162
|
}),
|
|
429
1163
|
authorizationGranted: false,
|
|
430
1164
|
};
|
|
431
1165
|
}
|
|
432
1166
|
if (workspaceRoots === null) {
|
|
433
1167
|
const registry = await readFleetRegistry(fleetRegistryPath);
|
|
1168
|
+
if (registry.schemaVersion === FLEET_REGISTRY_SCHEMA_V2) {
|
|
1169
|
+
return discoverFromRegisteredMembership(registry);
|
|
1170
|
+
}
|
|
434
1171
|
return discoverFromFleetRoots(registry.coverageRoots, {
|
|
435
1172
|
source: "fleet_registry",
|
|
436
1173
|
registryPath: registry.registryPath,
|
|
@@ -440,16 +1177,21 @@ export async function discoverFleet({
|
|
|
440
1177
|
const normalizedWorkspaces = await canonicalizeRoots(workspaceRoots, {
|
|
441
1178
|
rejectDuplicates: false,
|
|
442
1179
|
});
|
|
1180
|
+
const classifications = emptyClassifications();
|
|
1181
|
+
classifications.active = normalizedWorkspaces;
|
|
443
1182
|
return {
|
|
444
1183
|
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
445
1184
|
source: "explicit_workspaces",
|
|
446
1185
|
coverageRoots: [],
|
|
447
1186
|
unreachableRoots: [],
|
|
448
1187
|
workspaceRoots: normalizedWorkspaces,
|
|
1188
|
+
classifications,
|
|
1189
|
+
issues: [],
|
|
449
1190
|
complete: true,
|
|
450
1191
|
frozenManifestSha256: frozenManifestSha256({
|
|
451
1192
|
coverageRoots: [],
|
|
452
1193
|
workspaceRoots: normalizedWorkspaces,
|
|
1194
|
+
classifications,
|
|
453
1195
|
}),
|
|
454
1196
|
authorizationGranted: false,
|
|
455
1197
|
};
|