owlrunkit 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +274 -66
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +223 -117
- 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 +250 -115
- 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 +456 -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 +701 -0
- package/scripts/runkit-contract/project-control-parser.mjs +830 -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 +1097 -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 +155 -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 +31 -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 +161 -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,456 @@
|
|
|
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 SKIPPED_DIRECTORIES = new Set([
|
|
23
|
+
".cache",
|
|
24
|
+
".git",
|
|
25
|
+
".hg",
|
|
26
|
+
".owlcoda",
|
|
27
|
+
".svn",
|
|
28
|
+
"node_modules",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function compareCodeUnits(left, right) {
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function canonicalWorkspace(workspaceRoot) {
|
|
36
|
+
if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
|
|
37
|
+
throw new Error("fleet_workspace_invalid");
|
|
38
|
+
}
|
|
39
|
+
const requested = path.resolve(workspaceRoot);
|
|
40
|
+
const stat = await lstat(requested);
|
|
41
|
+
if (stat.isSymbolicLink()) throw new Error("fleet_workspace_symlink_rejected");
|
|
42
|
+
if (!stat.isDirectory()) throw new Error("fleet_workspace_invalid");
|
|
43
|
+
return realpath(requested);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function frozenManifestSha256({ coverageRoots, workspaceRoots }) {
|
|
47
|
+
const frozen = {
|
|
48
|
+
schemaVersion: "OwlCodaRunKitFrozenFleetManifestV1",
|
|
49
|
+
skillName: "owlcoda-runkit",
|
|
50
|
+
coverageRoots,
|
|
51
|
+
workspaceRoots,
|
|
52
|
+
authorizationGranted: false,
|
|
53
|
+
};
|
|
54
|
+
return `sha256:${createHash("sha256")
|
|
55
|
+
.update(`${JSON.stringify(frozen)}\n`)
|
|
56
|
+
.digest("hex")}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sha256Json(value) {
|
|
60
|
+
return `sha256:${createHash("sha256")
|
|
61
|
+
.update(`${JSON.stringify(value)}\n`)
|
|
62
|
+
.digest("hex")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function defaultFleetRegistryPath() {
|
|
66
|
+
const owlcodaHome = process.env.OWLCODA_HOME
|
|
67
|
+
? path.resolve(process.env.OWLCODA_HOME)
|
|
68
|
+
: path.join(userInfo().homedir, ".owlcoda");
|
|
69
|
+
return path.join(owlcodaHome, "runkit-fleet-root-registry-v1.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function hasRunKitConfig(directory) {
|
|
73
|
+
const owlRoot = path.join(directory, ".owlcoda");
|
|
74
|
+
const runkitRoot = path.join(owlRoot, "runkit");
|
|
75
|
+
const configPath = path.join(runkitRoot, "config.json");
|
|
76
|
+
|
|
77
|
+
let owlStat;
|
|
78
|
+
try {
|
|
79
|
+
owlStat = await lstat(owlRoot);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code === "ENOENT") return false;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
owlStat.isSymbolicLink()
|
|
86
|
+
|| !owlStat.isDirectory()
|
|
87
|
+
|| await realpath(owlRoot) !== path.resolve(owlRoot)
|
|
88
|
+
) {
|
|
89
|
+
throw new Error("fleet_project_marker_invalid");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let runkitStat;
|
|
93
|
+
try {
|
|
94
|
+
runkitStat = await lstat(runkitRoot);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error?.code === "ENOENT") return false;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
runkitStat.isSymbolicLink()
|
|
101
|
+
|| !runkitStat.isDirectory()
|
|
102
|
+
|| await realpath(runkitRoot) !== path.resolve(runkitRoot)
|
|
103
|
+
) {
|
|
104
|
+
throw new Error("fleet_project_marker_invalid");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let configStat;
|
|
108
|
+
try {
|
|
109
|
+
configStat = await lstat(configPath);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error?.code === "ENOENT") {
|
|
112
|
+
throw new Error("fleet_project_marker_incomplete");
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
configStat.isSymbolicLink()
|
|
118
|
+
|| !configStat.isFile()
|
|
119
|
+
|| await realpath(configPath) !== path.resolve(configPath)
|
|
120
|
+
) {
|
|
121
|
+
throw new Error("fleet_project_marker_invalid");
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function scanFleetRoot(fleetRoot) {
|
|
127
|
+
const requested = path.resolve(fleetRoot);
|
|
128
|
+
const canonicalRoot = await canonicalWorkspace(requested);
|
|
129
|
+
const projects = [];
|
|
130
|
+
let visitedDirectories = 0;
|
|
131
|
+
async function visit(directory, depth) {
|
|
132
|
+
if (depth > MAX_SCAN_DEPTH) throw new Error("fleet_scan_depth_exceeded");
|
|
133
|
+
visitedDirectories += 1;
|
|
134
|
+
if (visitedDirectories > MAX_SCAN_DIRECTORIES) {
|
|
135
|
+
throw new Error("fleet_scan_directory_limit_exceeded");
|
|
136
|
+
}
|
|
137
|
+
if (await hasRunKitConfig(directory)) projects.push(directory);
|
|
138
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
139
|
+
entries.sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (SKIPPED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
142
|
+
if (!entry.isDirectory()) continue;
|
|
143
|
+
await visit(path.join(directory, entry.name), depth + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await visit(canonicalRoot, 0);
|
|
147
|
+
return {
|
|
148
|
+
coverageRoot: canonicalRoot,
|
|
149
|
+
workspaceRoots: projects,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function discoverFromFleetRoots(
|
|
154
|
+
fleetRoots,
|
|
155
|
+
{
|
|
156
|
+
source = "fleet_roots",
|
|
157
|
+
registryPath = null,
|
|
158
|
+
registrySha256 = null,
|
|
159
|
+
} = {},
|
|
160
|
+
) {
|
|
161
|
+
if (
|
|
162
|
+
!Array.isArray(fleetRoots)
|
|
163
|
+
|| fleetRoots.length === 0
|
|
164
|
+
|| fleetRoots.length > MAX_FLEET_ROOTS
|
|
165
|
+
) {
|
|
166
|
+
throw new Error("fleet_roots_required");
|
|
167
|
+
}
|
|
168
|
+
const coverageRoots = [];
|
|
169
|
+
const unreachableRoots = [];
|
|
170
|
+
const workspaceRoots = [];
|
|
171
|
+
for (const fleetRoot of fleetRoots) {
|
|
172
|
+
if (typeof fleetRoot !== "string" || fleetRoot.length === 0) {
|
|
173
|
+
throw new Error("fleet_root_invalid");
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const scanned = await scanFleetRoot(fleetRoot);
|
|
177
|
+
coverageRoots.push(scanned.coverageRoot);
|
|
178
|
+
workspaceRoots.push(...scanned.workspaceRoots);
|
|
179
|
+
} catch {
|
|
180
|
+
unreachableRoots.push(path.resolve(fleetRoot));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const normalizedCoverage = [...new Set(coverageRoots)].sort(compareCodeUnits);
|
|
184
|
+
const normalizedUnreachable = [...new Set(unreachableRoots)].sort(compareCodeUnits);
|
|
185
|
+
const normalizedWorkspaces = [...new Set(workspaceRoots)].sort(compareCodeUnits);
|
|
186
|
+
if (normalizedWorkspaces.length > MAX_FLEET_WORKSPACES) {
|
|
187
|
+
throw new Error("fleet_workspace_limit_exceeded");
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
191
|
+
source,
|
|
192
|
+
...(registryPath === null
|
|
193
|
+
? {}
|
|
194
|
+
: { registryPath, registrySha256 }),
|
|
195
|
+
coverageRoots: normalizedCoverage,
|
|
196
|
+
unreachableRoots: normalizedUnreachable,
|
|
197
|
+
workspaceRoots: normalizedWorkspaces,
|
|
198
|
+
complete: normalizedUnreachable.length === 0,
|
|
199
|
+
frozenManifestSha256: frozenManifestSha256({
|
|
200
|
+
coverageRoots: normalizedCoverage,
|
|
201
|
+
workspaceRoots: normalizedWorkspaces,
|
|
202
|
+
}),
|
|
203
|
+
authorizationGranted: false,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readFleetRegistry(fleetRegistryPath) {
|
|
208
|
+
const selected = path.resolve(
|
|
209
|
+
fleetRegistryPath ?? defaultFleetRegistryPath(),
|
|
210
|
+
);
|
|
211
|
+
let loaded;
|
|
212
|
+
try {
|
|
213
|
+
loaded = await readManifestBounded(selected);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error?.code === "ENOENT") throw new Error("fleet_registry_missing");
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
const registry = loaded.manifest;
|
|
219
|
+
if (
|
|
220
|
+
registry === null
|
|
221
|
+
|| typeof registry !== "object"
|
|
222
|
+
|| Array.isArray(registry)
|
|
223
|
+
|| Object.keys(registry).sort().join(",")
|
|
224
|
+
!== "authorizationGranted,coverageRoots,registrySha256,schemaVersion"
|
|
225
|
+
|| registry.schemaVersion !== FLEET_REGISTRY_SCHEMA
|
|
226
|
+
|| registry.authorizationGranted !== false
|
|
227
|
+
|| !Array.isArray(registry.coverageRoots)
|
|
228
|
+
|| registry.coverageRoots.length === 0
|
|
229
|
+
|| registry.coverageRoots.length > MAX_FLEET_ROOTS
|
|
230
|
+
) {
|
|
231
|
+
throw new Error("fleet_registry_invalid");
|
|
232
|
+
}
|
|
233
|
+
const body = {
|
|
234
|
+
schemaVersion: registry.schemaVersion,
|
|
235
|
+
coverageRoots: registry.coverageRoots,
|
|
236
|
+
authorizationGranted: false,
|
|
237
|
+
};
|
|
238
|
+
if (registry.registrySha256 !== sha256Json(body)) {
|
|
239
|
+
throw new Error("fleet_registry_hash_mismatch");
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
registryPath: loaded.manifestPath,
|
|
243
|
+
registrySha256: registry.registrySha256,
|
|
244
|
+
coverageRoots: [...registry.coverageRoots],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function registerFleetCoverageRoot({
|
|
249
|
+
fleetRoot,
|
|
250
|
+
fleetRegistryPath = defaultFleetRegistryPath(),
|
|
251
|
+
} = {}) {
|
|
252
|
+
const coverageRoot = await canonicalWorkspace(fleetRoot);
|
|
253
|
+
const registryPath = path.resolve(fleetRegistryPath);
|
|
254
|
+
const parent = path.dirname(registryPath);
|
|
255
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
256
|
+
const lockPath = `${registryPath}.lock`;
|
|
257
|
+
try {
|
|
258
|
+
await mkdir(lockPath);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error?.code === "EEXIST") throw new Error("fleet_registry_transaction_active");
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
let coverageRoots = [];
|
|
265
|
+
try {
|
|
266
|
+
coverageRoots = (await readFleetRegistry(registryPath)).coverageRoots;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error?.message !== "fleet_registry_missing") throw error;
|
|
269
|
+
}
|
|
270
|
+
const alreadyRegistered = coverageRoots.includes(coverageRoot);
|
|
271
|
+
const normalized = [...new Set([
|
|
272
|
+
...coverageRoots,
|
|
273
|
+
coverageRoot,
|
|
274
|
+
])].sort(compareCodeUnits);
|
|
275
|
+
if (normalized.length > MAX_FLEET_ROOTS) {
|
|
276
|
+
throw new Error("fleet_registry_root_limit_exceeded");
|
|
277
|
+
}
|
|
278
|
+
const body = {
|
|
279
|
+
schemaVersion: FLEET_REGISTRY_SCHEMA,
|
|
280
|
+
coverageRoots: normalized,
|
|
281
|
+
authorizationGranted: false,
|
|
282
|
+
};
|
|
283
|
+
const registry = {
|
|
284
|
+
...body,
|
|
285
|
+
registrySha256: sha256Json(body),
|
|
286
|
+
};
|
|
287
|
+
if (!alreadyRegistered) {
|
|
288
|
+
const temporaryPath = `${registryPath}.tmp-${process.pid}`;
|
|
289
|
+
try {
|
|
290
|
+
await writeFile(
|
|
291
|
+
temporaryPath,
|
|
292
|
+
`${JSON.stringify(registry, null, 2)}\n`,
|
|
293
|
+
{ flag: "wx", mode: 0o600 },
|
|
294
|
+
);
|
|
295
|
+
await rename(temporaryPath, registryPath);
|
|
296
|
+
} finally {
|
|
297
|
+
await rm(temporaryPath, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
status: alreadyRegistered
|
|
302
|
+
? "fleet_root_already_registered"
|
|
303
|
+
: "fleet_root_registered",
|
|
304
|
+
registryPath,
|
|
305
|
+
registrySha256: registry.registrySha256,
|
|
306
|
+
coverageRoots: normalized,
|
|
307
|
+
authorizationGranted: false,
|
|
308
|
+
};
|
|
309
|
+
} finally {
|
|
310
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function readManifestBounded(fleetManifestPath) {
|
|
315
|
+
if (typeof fleetManifestPath !== "string" || fleetManifestPath.length === 0) {
|
|
316
|
+
throw new Error("fleet_manifest_path_invalid");
|
|
317
|
+
}
|
|
318
|
+
const requested = path.resolve(fleetManifestPath);
|
|
319
|
+
const stat = await lstat(requested);
|
|
320
|
+
if (stat.isSymbolicLink()) throw new Error("fleet_manifest_symlink_rejected");
|
|
321
|
+
if (!stat.isFile()) throw new Error("fleet_manifest_path_invalid");
|
|
322
|
+
if (stat.size > MAX_FLEET_MANIFEST_BYTES) {
|
|
323
|
+
throw new Error("fleet_manifest_too_large");
|
|
324
|
+
}
|
|
325
|
+
const canonicalPath = await realpath(requested);
|
|
326
|
+
const handle = await open(canonicalPath, "r");
|
|
327
|
+
try {
|
|
328
|
+
const before = await handle.stat();
|
|
329
|
+
const bytes = await handle.readFile();
|
|
330
|
+
const after = await handle.stat();
|
|
331
|
+
const current = await lstat(canonicalPath);
|
|
332
|
+
if (
|
|
333
|
+
current.isSymbolicLink()
|
|
334
|
+
|| before.dev !== after.dev
|
|
335
|
+
|| before.ino !== after.ino
|
|
336
|
+
|| before.size !== after.size
|
|
337
|
+
|| before.mtimeMs !== after.mtimeMs
|
|
338
|
+
|| after.dev !== current.dev
|
|
339
|
+
|| after.ino !== current.ino
|
|
340
|
+
|| after.size !== current.size
|
|
341
|
+
|| bytes.length !== current.size
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("fleet_manifest_changed_during_read");
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return {
|
|
347
|
+
manifest: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
|
348
|
+
manifestPath: canonicalPath,
|
|
349
|
+
};
|
|
350
|
+
} catch {
|
|
351
|
+
throw new Error("fleet_manifest_invalid_json");
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
await handle.close();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function validateManifest(manifest) {
|
|
359
|
+
if (
|
|
360
|
+
manifest === null
|
|
361
|
+
|| typeof manifest !== "object"
|
|
362
|
+
|| Array.isArray(manifest)
|
|
363
|
+
|| Object.keys(manifest).some(key => ![
|
|
364
|
+
"authorizationGranted",
|
|
365
|
+
"schemaVersion",
|
|
366
|
+
"skillName",
|
|
367
|
+
"workspaceRoots",
|
|
368
|
+
].includes(key))
|
|
369
|
+
|| manifest.schemaVersion !== "OwlCodaRunKitFleetManifestV1"
|
|
370
|
+
|| manifest.skillName !== "owlcoda-runkit"
|
|
371
|
+
|| manifest.authorizationGranted !== false
|
|
372
|
+
|| !Array.isArray(manifest.workspaceRoots)
|
|
373
|
+
|| manifest.workspaceRoots.length === 0
|
|
374
|
+
|| manifest.workspaceRoots.length > MAX_FLEET_WORKSPACES
|
|
375
|
+
) {
|
|
376
|
+
throw new Error("fleet_manifest_invalid");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function canonicalizeRoots(workspaceRoots, { rejectDuplicates }) {
|
|
381
|
+
if (
|
|
382
|
+
!Array.isArray(workspaceRoots)
|
|
383
|
+
|| workspaceRoots.length === 0
|
|
384
|
+
|| workspaceRoots.length > MAX_FLEET_WORKSPACES
|
|
385
|
+
) {
|
|
386
|
+
throw new Error("fleet_workspaces_required");
|
|
387
|
+
}
|
|
388
|
+
const roots = [];
|
|
389
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
390
|
+
roots.push(await canonicalWorkspace(workspaceRoot));
|
|
391
|
+
}
|
|
392
|
+
if (rejectDuplicates && new Set(roots).size !== roots.length) {
|
|
393
|
+
throw new Error("fleet_workspace_duplicate");
|
|
394
|
+
}
|
|
395
|
+
return [...new Set(roots)].sort(compareCodeUnits);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function discoverFleet({
|
|
399
|
+
workspaceRoots = null,
|
|
400
|
+
fleetManifestPath = null,
|
|
401
|
+
fleetRoots = null,
|
|
402
|
+
fleetRegistryPath = null,
|
|
403
|
+
} = {}) {
|
|
404
|
+
const sources = [workspaceRoots, fleetManifestPath, fleetRoots]
|
|
405
|
+
.filter(source => source !== null);
|
|
406
|
+
if (sources.length > 1) {
|
|
407
|
+
throw new Error("fleet_source_ambiguous");
|
|
408
|
+
}
|
|
409
|
+
if (fleetRoots !== null) return discoverFromFleetRoots(fleetRoots);
|
|
410
|
+
if (fleetManifestPath !== null) {
|
|
411
|
+
const { manifest, manifestPath } = await readManifestBounded(fleetManifestPath);
|
|
412
|
+
validateManifest(manifest);
|
|
413
|
+
const normalizedWorkspaces = await canonicalizeRoots(
|
|
414
|
+
manifest.workspaceRoots,
|
|
415
|
+
{ rejectDuplicates: true },
|
|
416
|
+
);
|
|
417
|
+
return {
|
|
418
|
+
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
419
|
+
source: "fleet_manifest",
|
|
420
|
+
manifestPath,
|
|
421
|
+
coverageRoots: [],
|
|
422
|
+
unreachableRoots: [],
|
|
423
|
+
workspaceRoots: normalizedWorkspaces,
|
|
424
|
+
complete: true,
|
|
425
|
+
frozenManifestSha256: frozenManifestSha256({
|
|
426
|
+
coverageRoots: [],
|
|
427
|
+
workspaceRoots: normalizedWorkspaces,
|
|
428
|
+
}),
|
|
429
|
+
authorizationGranted: false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (workspaceRoots === null) {
|
|
433
|
+
const registry = await readFleetRegistry(fleetRegistryPath);
|
|
434
|
+
return discoverFromFleetRoots(registry.coverageRoots, {
|
|
435
|
+
source: "fleet_registry",
|
|
436
|
+
registryPath: registry.registryPath,
|
|
437
|
+
registrySha256: registry.registrySha256,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const normalizedWorkspaces = await canonicalizeRoots(workspaceRoots, {
|
|
441
|
+
rejectDuplicates: false,
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
schemaVersion: "OwlCodaRunKitFleetDiscoveryV1",
|
|
445
|
+
source: "explicit_workspaces",
|
|
446
|
+
coverageRoots: [],
|
|
447
|
+
unreachableRoots: [],
|
|
448
|
+
workspaceRoots: normalizedWorkspaces,
|
|
449
|
+
complete: true,
|
|
450
|
+
frozenManifestSha256: frozenManifestSha256({
|
|
451
|
+
coverageRoots: [],
|
|
452
|
+
workspaceRoots: normalizedWorkspaces,
|
|
453
|
+
}),
|
|
454
|
+
authorizationGranted: false,
|
|
455
|
+
};
|
|
456
|
+
}
|