owlrunkit 0.15.1 → 0.16.1

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +284 -66
  3. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +235 -116
  4. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
  6. package/package.json +2 -2
  7. package/packages/attest/distribution-manifest-v1.json +10 -0
  8. package/packages/attest/src/quick.mjs +10 -0
  9. package/registry-release-policy-v1.json +8 -2
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/assurance-router.mjs +119 -0
  12. package/scripts/runkit-contract/core-contract.mjs +203 -206
  13. package/scripts/runkit-contract/core-successor.mjs +2043 -0
  14. package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
  15. package/scripts/runkit-contract/finalize.mjs +242 -21
  16. package/scripts/runkit-contract/fleet-discovery.mjs +1198 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +850 -0
  18. package/scripts/runkit-contract/human-status.mjs +553 -0
  19. package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
  20. package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
  21. package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
  22. package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
  23. package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
  24. package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
  25. package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
  26. package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
  27. package/scripts/runkit-contract/project-cli-resolver.mjs +704 -0
  28. package/scripts/runkit-contract/project-control-parser.mjs +889 -0
  29. package/scripts/runkit-contract/project-control-state.mjs +28 -0
  30. package/scripts/runkit-contract/provenance-common.mjs +19 -2
  31. package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
  32. package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
  33. package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
  34. package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
  35. package/scripts/runkit-contract/runkit-cli.mjs +1155 -21
  36. package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
  37. package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
  38. package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +167 -0
  39. package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
  40. package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
  41. package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
  42. package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
  43. package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +62 -0
  44. package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
  45. package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
  46. package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
  47. package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
  48. package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
  49. package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
  50. package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
  51. package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
  52. package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
  53. package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
  54. package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
  55. package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
  56. package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
  57. package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
  58. package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
  59. package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
  60. package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
  61. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
  62. package/scripts/runkit-contract/snapshot.mjs +1 -1
  63. package/scripts/runkit-contract/source-candidate.mjs +1382 -0
  64. package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
  65. package/scripts/runkit-contract/sync-core016-release-identity.mjs +166 -0
  66. package/scripts/runkit-contract/templates/assurance-request.json +13 -0
  67. package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
  68. package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
  69. package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
  70. package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
  71. package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
  72. package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
  73. package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
  74. package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
  75. package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
  76. package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
@@ -0,0 +1,1198 @@
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ open,
5
+ readdir,
6
+ realpath,
7
+ rename,
8
+ rm,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { createHash } from "node:crypto";
12
+ import { userInfo } from "node:os";
13
+ import path from "node:path";
14
+
15
+ const MAX_FLEET_MANIFEST_BYTES = 1_048_576;
16
+ const MAX_FLEET_WORKSPACES = 256;
17
+ const MAX_FLEET_ROOTS = 32;
18
+ const MAX_SCAN_DIRECTORIES = 10_000;
19
+ const MAX_SCAN_DEPTH = 16;
20
+ const FLEET_REGISTRY_SCHEMA =
21
+ "OwlCodaRunKitFleetRootRegistryV1";
22
+ const FLEET_REGISTRY_SCHEMA_V2 =
23
+ "OwlCodaRunKitFleetRootRegistryV2";
24
+ const FLEET_MEMBERSHIP_SCHEMA =
25
+ "OwlCodaRunKitFleetMembershipV1";
26
+ const SKIPPED_DIRECTORIES = new Set([
27
+ ".cache",
28
+ ".git",
29
+ ".hg",
30
+ ".owlcoda",
31
+ ".svn",
32
+ "node_modules",
33
+ ]);
34
+
35
+ function compareCodeUnits(left, right) {
36
+ return left < right ? -1 : left > right ? 1 : 0;
37
+ }
38
+
39
+ async function canonicalWorkspace(workspaceRoot) {
40
+ if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
41
+ throw new Error("fleet_workspace_invalid");
42
+ }
43
+ const requested = path.resolve(workspaceRoot);
44
+ const stat = await lstat(requested);
45
+ if (stat.isSymbolicLink()) throw new Error("fleet_workspace_symlink_rejected");
46
+ if (!stat.isDirectory()) throw new Error("fleet_workspace_invalid");
47
+ return realpath(requested);
48
+ }
49
+
50
+ function frozenManifestSha256({
51
+ coverageRoots,
52
+ workspaceRoots,
53
+ classifications = emptyClassifications(),
54
+ }) {
55
+ const frozen = {
56
+ schemaVersion: "OwlCodaRunKitFrozenFleetManifestV1",
57
+ skillName: "owlcoda-runkit",
58
+ coverageRoots,
59
+ workspaceRoots,
60
+ classifications,
61
+ authorizationGranted: false,
62
+ };
63
+ return `sha256:${createHash("sha256")
64
+ .update(`${JSON.stringify(frozen)}\n`)
65
+ .digest("hex")}`;
66
+ }
67
+
68
+ function sha256Json(value) {
69
+ return `sha256:${createHash("sha256")
70
+ .update(`${JSON.stringify(value)}\n`)
71
+ .digest("hex")}`;
72
+ }
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
+
97
+ export function defaultFleetRegistryPath() {
98
+ const owlcodaHome = process.env.OWLCODA_HOME
99
+ ? path.resolve(process.env.OWLCODA_HOME)
100
+ : path.join(userInfo().homedir, ".owlcoda");
101
+ return path.join(owlcodaHome, "runkit-fleet-root-registry-v1.json");
102
+ }
103
+
104
+ async function inspectRunKitMarker(directory) {
105
+ const owlRoot = path.join(directory, ".owlcoda");
106
+ const runkitRoot = path.join(owlRoot, "runkit");
107
+ const configPath = path.join(runkitRoot, "config.json");
108
+
109
+ let owlStat;
110
+ try {
111
+ owlStat = await lstat(owlRoot);
112
+ } catch (error) {
113
+ if (error?.code === "ENOENT") return { status: "none" };
114
+ throw error;
115
+ }
116
+ if (
117
+ owlStat.isSymbolicLink()
118
+ || !owlStat.isDirectory()
119
+ || await realpath(owlRoot) !== path.resolve(owlRoot)
120
+ ) {
121
+ return { status: "issue", code: "fleet_project_marker_invalid" };
122
+ }
123
+
124
+ let runkitStat;
125
+ try {
126
+ runkitStat = await lstat(runkitRoot);
127
+ } catch (error) {
128
+ if (error?.code === "ENOENT") return { status: "none" };
129
+ throw error;
130
+ }
131
+ if (
132
+ runkitStat.isSymbolicLink()
133
+ || !runkitStat.isDirectory()
134
+ || await realpath(runkitRoot) !== path.resolve(runkitRoot)
135
+ ) {
136
+ return { status: "issue", code: "fleet_project_marker_invalid" };
137
+ }
138
+
139
+ let configStat;
140
+ try {
141
+ configStat = await lstat(configPath);
142
+ } catch (error) {
143
+ if (error?.code === "ENOENT") {
144
+ return { status: "issue", code: "fleet_project_marker_incomplete" };
145
+ }
146
+ throw error;
147
+ }
148
+ if (
149
+ configStat.isSymbolicLink()
150
+ || !configStat.isFile()
151
+ || await realpath(configPath) !== path.resolve(configPath)
152
+ ) {
153
+ return { status: "issue", code: "fleet_project_marker_invalid" };
154
+ }
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" };
171
+ }
172
+
173
+ async function scanFleetRoot(fleetRoot) {
174
+ const requested = path.resolve(fleetRoot);
175
+ const canonicalRoot = await canonicalWorkspace(requested);
176
+ const projects = [];
177
+ const issues = [];
178
+ let visitedDirectories = 0;
179
+ let scanStopped = false;
180
+ async function visit(directory, depth) {
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
+ }
190
+ visitedDirectories += 1;
191
+ if (visitedDirectories > MAX_SCAN_DIRECTORIES) {
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;
222
+ }
223
+ const entries = await readdir(directory, { withFileTypes: true });
224
+ entries.sort((left, right) => compareCodeUnits(left.name, right.name));
225
+ for (const entry of entries) {
226
+ if (SKIPPED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
227
+ if (!entry.isDirectory()) continue;
228
+ await visit(path.join(directory, entry.name), depth + 1);
229
+ }
230
+ }
231
+ await visit(canonicalRoot, 0);
232
+ return {
233
+ coverageRoot: canonicalRoot,
234
+ workspaceRoots: projects,
235
+ issues,
236
+ };
237
+ }
238
+
239
+ async function discoverFromFleetRoots(
240
+ fleetRoots,
241
+ {
242
+ source = "fleet_roots",
243
+ registryPath = null,
244
+ registrySha256 = null,
245
+ } = {},
246
+ ) {
247
+ if (
248
+ !Array.isArray(fleetRoots)
249
+ || fleetRoots.length === 0
250
+ || fleetRoots.length > MAX_FLEET_ROOTS
251
+ ) {
252
+ throw new Error("fleet_roots_required");
253
+ }
254
+ const coverageRoots = [];
255
+ const unreachableRoots = [];
256
+ const workspaceRoots = [];
257
+ const issues = [];
258
+ for (const fleetRoot of fleetRoots) {
259
+ if (typeof fleetRoot !== "string" || fleetRoot.length === 0) {
260
+ throw new Error("fleet_root_invalid");
261
+ }
262
+ try {
263
+ const scanned = await scanFleetRoot(fleetRoot);
264
+ coverageRoots.push(scanned.coverageRoot);
265
+ workspaceRoots.push(...scanned.workspaceRoots);
266
+ issues.push(...scanned.issues);
267
+ } catch (error) {
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
+ });
274
+ }
275
+ }
276
+ const normalizedCoverage = [...new Set(coverageRoots)].sort(compareCodeUnits);
277
+ const normalizedUnreachable = [...new Set(unreachableRoots)].sort(compareCodeUnits);
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;
285
+ if (normalizedWorkspaces.length > MAX_FLEET_WORKSPACES) {
286
+ throw new Error("fleet_workspace_limit_exceeded");
287
+ }
288
+ return {
289
+ schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
290
+ source,
291
+ ...(registryPath === null
292
+ ? {}
293
+ : { registryPath, registrySha256 }),
294
+ coverageRoots: normalizedCoverage,
295
+ unreachableRoots: normalizedUnreachable,
296
+ workspaceRoots: normalizedWorkspaces,
297
+ classifications,
298
+ issues,
299
+ complete: normalizedUnreachable.length === 0 && issues.length === 0,
300
+ frozenManifestSha256: frozenManifestSha256({
301
+ coverageRoots: normalizedCoverage,
302
+ workspaceRoots: normalizedWorkspaces,
303
+ classifications,
304
+ }),
305
+ authorizationGranted: false,
306
+ };
307
+ }
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
+
489
+ async function readFleetRegistry(fleetRegistryPath) {
490
+ const selected = path.resolve(
491
+ fleetRegistryPath ?? defaultFleetRegistryPath(),
492
+ );
493
+ let loaded;
494
+ try {
495
+ loaded = await readManifestBounded(selected);
496
+ } catch (error) {
497
+ if (error?.code === "ENOENT") throw new Error("fleet_registry_missing");
498
+ throw error;
499
+ }
500
+ const registry = loaded.manifest;
501
+ const commonInvalid = (
502
+ registry === null
503
+ || typeof registry !== "object"
504
+ || Array.isArray(registry)
505
+ || registry.authorizationGranted !== false
506
+ || !Array.isArray(registry.coverageRoots)
507
+ || registry.coverageRoots.length === 0
508
+ || registry.coverageRoots.length > MAX_FLEET_ROOTS
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
+ };
538
+ if (registry.registrySha256 !== sha256Json(body)) {
539
+ throw new Error("fleet_registry_hash_mismatch");
540
+ }
541
+ return {
542
+ registryPath: loaded.manifestPath,
543
+ schemaVersion: registry.schemaVersion,
544
+ registrySha256: registry.registrySha256,
545
+ fileSha256: loaded.fileSha256,
546
+ fileBytes: loaded.bytes,
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,
572
+ };
573
+ }
574
+
575
+ export async function registerFleetCoverageRoot({
576
+ fleetRoot,
577
+ fleetRegistryPath = defaultFleetRegistryPath(),
578
+ } = {}) {
579
+ const coverageRoot = await canonicalWorkspace(fleetRoot);
580
+ const registryPath = path.resolve(fleetRegistryPath);
581
+ const parent = path.dirname(registryPath);
582
+ await mkdir(parent, { recursive: true, mode: 0o700 });
583
+ const lockPath = `${registryPath}.lock`;
584
+ try {
585
+ await mkdir(lockPath);
586
+ } catch (error) {
587
+ if (error?.code === "EEXIST") throw new Error("fleet_registry_transaction_active");
588
+ throw error;
589
+ }
590
+ try {
591
+ let coverageRoots = [];
592
+ try {
593
+ coverageRoots = (await readFleetRegistry(registryPath)).coverageRoots;
594
+ } catch (error) {
595
+ if (error?.message !== "fleet_registry_missing") throw error;
596
+ }
597
+ const alreadyRegistered = coverageRoots.includes(coverageRoot);
598
+ const normalized = [...new Set([
599
+ ...coverageRoots,
600
+ coverageRoot,
601
+ ])].sort(compareCodeUnits);
602
+ if (normalized.length > MAX_FLEET_ROOTS) {
603
+ throw new Error("fleet_registry_root_limit_exceeded");
604
+ }
605
+ const body = {
606
+ schemaVersion: FLEET_REGISTRY_SCHEMA,
607
+ coverageRoots: normalized,
608
+ authorizationGranted: false,
609
+ };
610
+ const registry = {
611
+ ...body,
612
+ registrySha256: sha256Json(body),
613
+ };
614
+ if (!alreadyRegistered) {
615
+ const temporaryPath = `${registryPath}.tmp-${process.pid}`;
616
+ try {
617
+ await writeFile(
618
+ temporaryPath,
619
+ `${JSON.stringify(registry, null, 2)}\n`,
620
+ { flag: "wx", mode: 0o600 },
621
+ );
622
+ await rename(temporaryPath, registryPath);
623
+ } finally {
624
+ await rm(temporaryPath, { force: true });
625
+ }
626
+ }
627
+ return {
628
+ status: alreadyRegistered
629
+ ? "fleet_root_already_registered"
630
+ : "fleet_root_registered",
631
+ registryPath,
632
+ registrySha256: registry.registrySha256,
633
+ coverageRoots: normalized,
634
+ authorizationGranted: false,
635
+ };
636
+ } finally {
637
+ await rm(lockPath, { recursive: true, force: true });
638
+ }
639
+ }
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
+
977
+ async function readManifestBounded(fleetManifestPath) {
978
+ if (typeof fleetManifestPath !== "string" || fleetManifestPath.length === 0) {
979
+ throw new Error("fleet_manifest_path_invalid");
980
+ }
981
+ const requested = path.resolve(fleetManifestPath);
982
+ const stat = await lstat(requested);
983
+ if (stat.isSymbolicLink()) throw new Error("fleet_manifest_symlink_rejected");
984
+ if (!stat.isFile()) throw new Error("fleet_manifest_path_invalid");
985
+ if (stat.size > MAX_FLEET_MANIFEST_BYTES) {
986
+ throw new Error("fleet_manifest_too_large");
987
+ }
988
+ const canonicalPath = await realpath(requested);
989
+ const handle = await open(canonicalPath, "r");
990
+ try {
991
+ const before = await handle.stat();
992
+ const bytes = await handle.readFile();
993
+ const after = await handle.stat();
994
+ const current = await lstat(canonicalPath);
995
+ if (
996
+ current.isSymbolicLink()
997
+ || before.dev !== after.dev
998
+ || before.ino !== after.ino
999
+ || before.size !== after.size
1000
+ || before.mtimeMs !== after.mtimeMs
1001
+ || after.dev !== current.dev
1002
+ || after.ino !== current.ino
1003
+ || after.size !== current.size
1004
+ || bytes.length !== current.size
1005
+ ) {
1006
+ throw new Error("fleet_manifest_changed_during_read");
1007
+ }
1008
+ try {
1009
+ return {
1010
+ manifest: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
1011
+ manifestPath: canonicalPath,
1012
+ bytes,
1013
+ fileSha256: sha256Bytes(bytes),
1014
+ };
1015
+ } catch {
1016
+ throw new Error("fleet_manifest_invalid_json");
1017
+ }
1018
+ } finally {
1019
+ await handle.close();
1020
+ }
1021
+ }
1022
+
1023
+ function validateManifest(manifest) {
1024
+ if (
1025
+ manifest === null
1026
+ || typeof manifest !== "object"
1027
+ || Array.isArray(manifest)
1028
+ || Object.keys(manifest).some(key => ![
1029
+ "authorizationGranted",
1030
+ "schemaVersion",
1031
+ "skillName",
1032
+ "workspaceRoots",
1033
+ ].includes(key))
1034
+ || manifest.schemaVersion !== "OwlCodaRunKitFleetManifestV1"
1035
+ || manifest.skillName !== "owlcoda-runkit"
1036
+ || manifest.authorizationGranted !== false
1037
+ || !Array.isArray(manifest.workspaceRoots)
1038
+ || manifest.workspaceRoots.length === 0
1039
+ || manifest.workspaceRoots.length > MAX_FLEET_WORKSPACES
1040
+ ) {
1041
+ throw new Error("fleet_manifest_invalid");
1042
+ }
1043
+ }
1044
+
1045
+ async function canonicalizeRoots(workspaceRoots, { rejectDuplicates }) {
1046
+ if (
1047
+ !Array.isArray(workspaceRoots)
1048
+ || workspaceRoots.length === 0
1049
+ || workspaceRoots.length > MAX_FLEET_WORKSPACES
1050
+ ) {
1051
+ throw new Error("fleet_workspaces_required");
1052
+ }
1053
+ const roots = [];
1054
+ for (const workspaceRoot of workspaceRoots) {
1055
+ roots.push(await canonicalWorkspace(workspaceRoot));
1056
+ }
1057
+ if (rejectDuplicates && new Set(roots).size !== roots.length) {
1058
+ throw new Error("fleet_workspace_duplicate");
1059
+ }
1060
+ return [...new Set(roots)].sort(compareCodeUnits);
1061
+ }
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
+
1127
+ export async function discoverFleet({
1128
+ workspaceRoots = null,
1129
+ fleetManifestPath = null,
1130
+ fleetRoots = null,
1131
+ fleetRegistryPath = null,
1132
+ } = {}) {
1133
+ const sources = [workspaceRoots, fleetManifestPath, fleetRoots]
1134
+ .filter(source => source !== null);
1135
+ if (sources.length > 1) {
1136
+ throw new Error("fleet_source_ambiguous");
1137
+ }
1138
+ if (fleetRoots !== null) return discoverFromFleetRoots(fleetRoots);
1139
+ if (fleetManifestPath !== null) {
1140
+ const { manifest, manifestPath } = await readManifestBounded(fleetManifestPath);
1141
+ validateManifest(manifest);
1142
+ const normalizedWorkspaces = await canonicalizeRoots(
1143
+ manifest.workspaceRoots,
1144
+ { rejectDuplicates: true },
1145
+ );
1146
+ const classifications = emptyClassifications();
1147
+ classifications.active = normalizedWorkspaces;
1148
+ return {
1149
+ schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
1150
+ source: "fleet_manifest",
1151
+ manifestPath,
1152
+ coverageRoots: [],
1153
+ unreachableRoots: [],
1154
+ workspaceRoots: normalizedWorkspaces,
1155
+ classifications,
1156
+ issues: [],
1157
+ complete: true,
1158
+ frozenManifestSha256: frozenManifestSha256({
1159
+ coverageRoots: [],
1160
+ workspaceRoots: normalizedWorkspaces,
1161
+ classifications,
1162
+ }),
1163
+ authorizationGranted: false,
1164
+ };
1165
+ }
1166
+ if (workspaceRoots === null) {
1167
+ const registry = await readFleetRegistry(fleetRegistryPath);
1168
+ if (registry.schemaVersion === FLEET_REGISTRY_SCHEMA_V2) {
1169
+ return discoverFromRegisteredMembership(registry);
1170
+ }
1171
+ return discoverFromFleetRoots(registry.coverageRoots, {
1172
+ source: "fleet_registry",
1173
+ registryPath: registry.registryPath,
1174
+ registrySha256: registry.registrySha256,
1175
+ });
1176
+ }
1177
+ const normalizedWorkspaces = await canonicalizeRoots(workspaceRoots, {
1178
+ rejectDuplicates: false,
1179
+ });
1180
+ const classifications = emptyClassifications();
1181
+ classifications.active = normalizedWorkspaces;
1182
+ return {
1183
+ schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
1184
+ source: "explicit_workspaces",
1185
+ coverageRoots: [],
1186
+ unreachableRoots: [],
1187
+ workspaceRoots: normalizedWorkspaces,
1188
+ classifications,
1189
+ issues: [],
1190
+ complete: true,
1191
+ frozenManifestSha256: frozenManifestSha256({
1192
+ coverageRoots: [],
1193
+ workspaceRoots: normalizedWorkspaces,
1194
+ classifications,
1195
+ }),
1196
+ authorizationGranted: false,
1197
+ };
1198
+ }