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.
- package/CHANGELOG.md +105 -0
- package/README.md +284 -66
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +235 -116
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
- package/package.json +2 -2
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v1.json +8 -2
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +119 -0
- package/scripts/runkit-contract/core-contract.mjs +203 -206
- package/scripts/runkit-contract/core-successor.mjs +2043 -0
- package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
- package/scripts/runkit-contract/finalize.mjs +242 -21
- package/scripts/runkit-contract/fleet-discovery.mjs +1198 -0
- package/scripts/runkit-contract/formal-workflow.mjs +850 -0
- package/scripts/runkit-contract/human-status.mjs +553 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
- package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
- package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
- package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
- package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
- package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
- package/scripts/runkit-contract/project-cli-resolver.mjs +704 -0
- package/scripts/runkit-contract/project-control-parser.mjs +889 -0
- package/scripts/runkit-contract/project-control-state.mjs +28 -0
- package/scripts/runkit-contract/provenance-common.mjs +19 -2
- package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
- package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
- package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1155 -21
- package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
- package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +167 -0
- package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
- package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
- package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
- package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
- package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +62 -0
- package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
- package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
- package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
- package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
- package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
- package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
- package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
- package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
- package/scripts/runkit-contract/snapshot.mjs +1 -1
- package/scripts/runkit-contract/source-candidate.mjs +1382 -0
- package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
- package/scripts/runkit-contract/sync-core016-release-identity.mjs +166 -0
- package/scripts/runkit-contract/templates/assurance-request.json +13 -0
- package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
- package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
- package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
- package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
- package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
- package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
- package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
- package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
- 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
|
+
}
|