owlrunkit 0.13.0 → 0.15.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 +41 -0
- package/README.md +66 -4
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +79 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +163 -2
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +1 -1
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v1.json +1 -1
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +145 -4
- package/scripts/runkit-contract/install-codex-skill.mjs +505 -11
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +119 -4
- package/scripts/runkit-contract/onboarding-doctor.mjs +805 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +510 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +121 -7
- package/scripts/runkit-contract/registry-adoption.mjs +198 -0
- package/scripts/runkit-contract/resume-execution.mjs +8 -2
- package/scripts/runkit-contract/runkit-cli.mjs +216 -6
- package/scripts/runkit-contract/schemas/adoption-evidence-v1.schema.json +63 -0
- package/scripts/runkit-contract/schemas/verification-finalize-attempt-v1.schema.json +52 -0
- package/scripts/runkit-contract/schemas/verification-preflight-v1.schema.json +52 -0
|
@@ -8,13 +8,20 @@ import {
|
|
|
8
8
|
mkdtemp,
|
|
9
9
|
readFile,
|
|
10
10
|
readdir,
|
|
11
|
+
realpath,
|
|
11
12
|
rename,
|
|
12
13
|
rm,
|
|
13
14
|
writeFile,
|
|
14
15
|
} from "node:fs/promises";
|
|
15
16
|
import path from "node:path";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
16
18
|
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
initializeProjectRunKit,
|
|
21
|
+
inspectProjectUpgradeSafety,
|
|
22
|
+
isDirectExecution,
|
|
23
|
+
validateProjectConfigV2,
|
|
24
|
+
} from "./core-contract.mjs";
|
|
18
25
|
|
|
19
26
|
const MANIFEST_FILE = ".owlcoda-install-manifest.json";
|
|
20
27
|
|
|
@@ -22,6 +29,18 @@ function sha256(value) {
|
|
|
22
29
|
return createHash("sha256").update(value).digest("hex");
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function sameCoreIdentity(left, right) {
|
|
33
|
+
return [
|
|
34
|
+
"contractVersion",
|
|
35
|
+
"coreVersion",
|
|
36
|
+
"coreManifestSha256",
|
|
37
|
+
"coreSourceRef",
|
|
38
|
+
].every((field) => (
|
|
39
|
+
typeof left?.[field] === "string"
|
|
40
|
+
&& left[field] === right?.[field]
|
|
41
|
+
));
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
async function exists(filePath) {
|
|
26
45
|
try {
|
|
27
46
|
await lstat(filePath);
|
|
@@ -32,6 +51,43 @@ async function exists(filePath) {
|
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
async function atomicWriteBytes(filePath, bytes) {
|
|
55
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
56
|
+
await rm(temporaryPath, { force: true });
|
|
57
|
+
await writeFile(temporaryPath, bytes, { flag: "wx" });
|
|
58
|
+
await rename(temporaryPath, filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function nextAvailablePath(parentRoot, baseName) {
|
|
62
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
63
|
+
const suffix = attempt === 1
|
|
64
|
+
? ""
|
|
65
|
+
: `-attempt-${String(attempt).padStart(3, "0")}`;
|
|
66
|
+
const candidate = path.join(parentRoot, `${baseName}${suffix}`);
|
|
67
|
+
if (!await exists(candidate)) return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function writeAppendOnlyReceipt(receiptsRoot, baseName, receipt) {
|
|
72
|
+
await mkdir(receiptsRoot, { recursive: true });
|
|
73
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
74
|
+
const suffix = attempt === 1
|
|
75
|
+
? ""
|
|
76
|
+
: `-attempt-${String(attempt).padStart(3, "0")}`;
|
|
77
|
+
const receiptPath = path.join(receiptsRoot, `${baseName}${suffix}.json`);
|
|
78
|
+
try {
|
|
79
|
+
await writeFile(
|
|
80
|
+
receiptPath,
|
|
81
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
82
|
+
{ flag: "wx" },
|
|
83
|
+
);
|
|
84
|
+
return receiptPath;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error?.code !== "EEXIST") throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
35
91
|
async function fileMap(root, { excludeManifest = true } = {}) {
|
|
36
92
|
const files = {};
|
|
37
93
|
async function walk(directory, prefix = "") {
|
|
@@ -137,11 +193,79 @@ export async function inspectInstalledSkill({ targetRoot } = {}) {
|
|
|
137
193
|
};
|
|
138
194
|
}
|
|
139
195
|
|
|
196
|
+
async function inspectFleetForSkillChange(workspaceRoots) {
|
|
197
|
+
if (!Array.isArray(workspaceRoots) || workspaceRoots.length === 0) {
|
|
198
|
+
throw new Error("skill_update_fleet_preflight_required");
|
|
199
|
+
}
|
|
200
|
+
const roots = [];
|
|
201
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
202
|
+
if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
|
|
203
|
+
throw new Error("skill_update_fleet_workspace_invalid");
|
|
204
|
+
}
|
|
205
|
+
roots.push(await realpath(workspaceRoot));
|
|
206
|
+
}
|
|
207
|
+
const fleet = [...new Set(roots)].sort();
|
|
208
|
+
const projects = [];
|
|
209
|
+
for (const workspaceRoot of fleet) {
|
|
210
|
+
const safety = inspectProjectUpgradeSafety({ workspaceRoot });
|
|
211
|
+
if (safety.status !== "safe") {
|
|
212
|
+
const blockers = [
|
|
213
|
+
...safety.activeRunIds,
|
|
214
|
+
...safety.activeLeaseIds,
|
|
215
|
+
...safety.issues,
|
|
216
|
+
].join(",");
|
|
217
|
+
throw new Error(
|
|
218
|
+
`skill_update_blocked_active_execution:${workspaceRoot}:${blockers}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const configPath = path.join(
|
|
222
|
+
workspaceRoot,
|
|
223
|
+
".owlcoda/runkit/config.json",
|
|
224
|
+
);
|
|
225
|
+
const configBytes = await readFile(configPath);
|
|
226
|
+
let config;
|
|
227
|
+
try {
|
|
228
|
+
config = JSON.parse(configBytes.toString("utf8"));
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error(`skill_update_fleet_config_invalid:${workspaceRoot}:invalid_json`);
|
|
231
|
+
}
|
|
232
|
+
const configV2 = config?.schemaVersion === "OwlCodaRunKitConfigV2"
|
|
233
|
+
? validateProjectConfigV2(config)
|
|
234
|
+
: null;
|
|
235
|
+
const supportedV1 = config?.schemaVersion === "OwlCodaRunKitConfigV1"
|
|
236
|
+
&& config?.core?.contractVersion === "0.1"
|
|
237
|
+
&& config?.authorizationPolicy === "external_explicit_authority_required";
|
|
238
|
+
if (
|
|
239
|
+
(configV2 !== null && !configV2.valid)
|
|
240
|
+
|| (configV2 === null && !supportedV1)
|
|
241
|
+
) {
|
|
242
|
+
const issues = configV2?.issues?.join(",") ?? "unsupported_config";
|
|
243
|
+
throw new Error(
|
|
244
|
+
`skill_update_fleet_config_invalid:${workspaceRoot}:${issues}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
projects.push({
|
|
248
|
+
workspaceRoot,
|
|
249
|
+
configPath,
|
|
250
|
+
configBytes,
|
|
251
|
+
configSha256: sha256(configBytes),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return projects;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function restoreProjectConfigs(projects) {
|
|
258
|
+
for (const project of projects) {
|
|
259
|
+
await atomicWriteBytes(project.configPath, project.configBytes);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
140
263
|
export async function installCodexSkill({
|
|
141
264
|
repositoryRoot,
|
|
142
265
|
targetRoot,
|
|
143
266
|
archiveRoot,
|
|
144
267
|
expectedUnmanagedManifestSha256 = null,
|
|
268
|
+
workspaceRoots = null,
|
|
145
269
|
} = {}) {
|
|
146
270
|
if (typeof repositoryRoot !== "string" || typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
|
|
147
271
|
throw new Error("repositoryRoot, targetRoot, and archiveRoot are required");
|
|
@@ -176,35 +300,377 @@ export async function installCodexSkill({
|
|
|
176
300
|
}
|
|
177
301
|
return { status: "unchanged", manifestSha256: desired.manifestSha256, previousInstallation: null };
|
|
178
302
|
}
|
|
179
|
-
|
|
303
|
+
const projects = await inspectFleetForSkillChange(workspaceRoots);
|
|
180
304
|
await mkdir(archiveRoot, { recursive: true });
|
|
181
|
-
const archivePath =
|
|
182
|
-
|
|
305
|
+
const archivePath = await nextAvailablePath(
|
|
306
|
+
archiveRoot,
|
|
307
|
+
`owlcoda-runkit-${prior.manifestSha256.slice(7)}`,
|
|
308
|
+
);
|
|
183
309
|
await rename(targetRoot, archivePath);
|
|
184
310
|
try {
|
|
185
311
|
await rename(stagingRoot, targetRoot);
|
|
186
312
|
stagingPresent = false;
|
|
313
|
+
const projectMigrations = [];
|
|
314
|
+
for (const project of projects) {
|
|
315
|
+
const initialized = await initializeProjectRunKit({
|
|
316
|
+
workspaceRoot: project.workspaceRoot,
|
|
317
|
+
});
|
|
318
|
+
if (initialized.exitCode !== 0) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`skill_update_project_migration_blocked:${project.workspaceRoot}:${initialized.status}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const currentConfigBytes = await readFile(project.configPath);
|
|
324
|
+
projectMigrations.push({
|
|
325
|
+
workspaceRoot: project.workspaceRoot,
|
|
326
|
+
configPath: project.configPath,
|
|
327
|
+
fromConfigSha256: project.configSha256,
|
|
328
|
+
fromConfigBase64: project.configBytes.toString("base64"),
|
|
329
|
+
toConfigSha256: sha256(currentConfigBytes),
|
|
330
|
+
migrationReceipt: initialized.migrationReceipt ?? null,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const receipt = {
|
|
334
|
+
schemaVersion: "OwlCodaRunKitSkillUpgradeReceiptV1",
|
|
335
|
+
operation: "upgrade",
|
|
336
|
+
targetRoot: path.resolve(targetRoot),
|
|
337
|
+
archiveRoot: path.resolve(archiveRoot),
|
|
338
|
+
fromManifestSha256: prior.manifestSha256,
|
|
339
|
+
toManifestSha256: desired.manifestSha256,
|
|
340
|
+
priorArchivePath: archivePath,
|
|
341
|
+
projects: projectMigrations,
|
|
342
|
+
authorizationGranted: false,
|
|
343
|
+
};
|
|
344
|
+
const upgradeReceipt = await writeAppendOnlyReceipt(
|
|
345
|
+
path.join(archiveRoot, "receipts"),
|
|
346
|
+
`skill-upgrade-${prior.manifestSha256.slice(7)}-to-${desired.manifestSha256.slice(7)}`,
|
|
347
|
+
receipt,
|
|
348
|
+
);
|
|
349
|
+
return {
|
|
350
|
+
status: "updated",
|
|
351
|
+
manifestSha256: desired.manifestSha256,
|
|
352
|
+
previousInstallation: {
|
|
353
|
+
manifestSha256: prior.manifestSha256,
|
|
354
|
+
archivePath,
|
|
355
|
+
},
|
|
356
|
+
upgradeReceipt,
|
|
357
|
+
projectMigrations,
|
|
358
|
+
};
|
|
187
359
|
} catch (error) {
|
|
360
|
+
if (await exists(targetRoot)) {
|
|
361
|
+
const failedArchivePath = await nextAvailablePath(
|
|
362
|
+
archiveRoot,
|
|
363
|
+
`owlcoda-runkit-failed-${desired.manifestSha256.slice(7)}`,
|
|
364
|
+
);
|
|
365
|
+
await rename(targetRoot, failedArchivePath);
|
|
366
|
+
}
|
|
188
367
|
await rename(archivePath, targetRoot);
|
|
368
|
+
await restoreProjectConfigs(projects);
|
|
189
369
|
throw error;
|
|
190
370
|
}
|
|
191
|
-
return {
|
|
192
|
-
status: "updated",
|
|
193
|
-
manifestSha256: desired.manifestSha256,
|
|
194
|
-
previousInstallation: { manifestSha256: prior.manifestSha256, archivePath },
|
|
195
|
-
};
|
|
196
371
|
} finally {
|
|
197
372
|
if (stagingPresent) await rm(stagingRoot, { recursive: true, force: true });
|
|
198
373
|
}
|
|
199
374
|
}
|
|
200
375
|
|
|
376
|
+
export async function restoreCodexSkill({
|
|
377
|
+
targetRoot,
|
|
378
|
+
archiveRoot,
|
|
379
|
+
upgradeReceiptPath,
|
|
380
|
+
} = {}) {
|
|
381
|
+
if (
|
|
382
|
+
typeof targetRoot !== "string"
|
|
383
|
+
|| typeof archiveRoot !== "string"
|
|
384
|
+
|| typeof upgradeReceiptPath !== "string"
|
|
385
|
+
) {
|
|
386
|
+
throw new Error("targetRoot, archiveRoot, and upgradeReceiptPath are required");
|
|
387
|
+
}
|
|
388
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
389
|
+
const resolvedArchive = path.resolve(archiveRoot);
|
|
390
|
+
const resolvedReceipt = path.resolve(upgradeReceiptPath);
|
|
391
|
+
const receiptRelative = path.relative(
|
|
392
|
+
path.join(resolvedArchive, "receipts"),
|
|
393
|
+
resolvedReceipt,
|
|
394
|
+
);
|
|
395
|
+
if (
|
|
396
|
+
receiptRelative.startsWith("..")
|
|
397
|
+
|| path.isAbsolute(receiptRelative)
|
|
398
|
+
|| !receiptRelative.endsWith(".json")
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("skill_rollback_receipt_outside_archive");
|
|
401
|
+
}
|
|
402
|
+
const receiptBytes = await readFile(resolvedReceipt);
|
|
403
|
+
const receipt = JSON.parse(receiptBytes.toString("utf8"));
|
|
404
|
+
if (
|
|
405
|
+
receipt.schemaVersion !== "OwlCodaRunKitSkillUpgradeReceiptV1"
|
|
406
|
+
|| receipt.operation !== "upgrade"
|
|
407
|
+
|| receipt.targetRoot !== resolvedTarget
|
|
408
|
+
|| receipt.archiveRoot !== resolvedArchive
|
|
409
|
+
|| receipt.authorizationGranted !== false
|
|
410
|
+
|| !Array.isArray(receipt.projects)
|
|
411
|
+
|| receipt.projects.length === 0
|
|
412
|
+
) {
|
|
413
|
+
throw new Error("skill_rollback_receipt_invalid");
|
|
414
|
+
}
|
|
415
|
+
const resolvedPriorArchive = path.resolve(receipt.priorArchivePath);
|
|
416
|
+
const priorArchiveRelative = path.relative(
|
|
417
|
+
resolvedArchive,
|
|
418
|
+
resolvedPriorArchive,
|
|
419
|
+
);
|
|
420
|
+
if (
|
|
421
|
+
receipt.priorArchivePath !== resolvedPriorArchive
|
|
422
|
+
|| priorArchiveRelative.startsWith("..")
|
|
423
|
+
|| path.isAbsolute(priorArchiveRelative)
|
|
424
|
+
|| priorArchiveRelative.length === 0
|
|
425
|
+
|| priorArchiveRelative === "receipts"
|
|
426
|
+
|| priorArchiveRelative.startsWith(`receipts${path.sep}`)
|
|
427
|
+
) {
|
|
428
|
+
throw new Error("skill_rollback_prior_archive_outside_root");
|
|
429
|
+
}
|
|
430
|
+
const current = await inspectInstalledSkill({ targetRoot: resolvedTarget });
|
|
431
|
+
if (!current.valid || current.manifestSha256 !== receipt.toManifestSha256) {
|
|
432
|
+
throw new Error("skill_rollback_current_identity_mismatch");
|
|
433
|
+
}
|
|
434
|
+
const prior = await inspectInstalledSkill({
|
|
435
|
+
targetRoot: resolvedPriorArchive,
|
|
436
|
+
});
|
|
437
|
+
if (!prior.valid || prior.manifestSha256 !== receipt.fromManifestSha256) {
|
|
438
|
+
throw new Error("skill_rollback_prior_identity_mismatch");
|
|
439
|
+
}
|
|
440
|
+
const workspaceRoots = receipt.projects.map((project) => project.workspaceRoot);
|
|
441
|
+
const currentProjects = await inspectFleetForSkillChange(workspaceRoots);
|
|
442
|
+
const receiptProjectByRoot = new Map(
|
|
443
|
+
receipt.projects.map((project) => [project.workspaceRoot, project]),
|
|
444
|
+
);
|
|
445
|
+
for (const project of currentProjects) {
|
|
446
|
+
const expected = receiptProjectByRoot.get(project.workspaceRoot);
|
|
447
|
+
if (
|
|
448
|
+
!expected
|
|
449
|
+
|| expected.configPath !== project.configPath
|
|
450
|
+
|| expected.toConfigSha256 !== project.configSha256
|
|
451
|
+
|| typeof expected.fromConfigBase64 !== "string"
|
|
452
|
+
|| sha256(Buffer.from(expected.fromConfigBase64, "base64"))
|
|
453
|
+
!== expected.fromConfigSha256
|
|
454
|
+
) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`skill_rollback_project_identity_mismatch:${project.workspaceRoot}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const currentArchivePath = await nextAvailablePath(
|
|
461
|
+
resolvedArchive,
|
|
462
|
+
`owlcoda-runkit-${current.manifestSha256.slice(7)}`,
|
|
463
|
+
);
|
|
464
|
+
await rename(resolvedTarget, currentArchivePath);
|
|
465
|
+
try {
|
|
466
|
+
await rename(resolvedPriorArchive, resolvedTarget);
|
|
467
|
+
for (const project of currentProjects) {
|
|
468
|
+
const expected = receiptProjectByRoot.get(project.workspaceRoot);
|
|
469
|
+
await atomicWriteBytes(
|
|
470
|
+
project.configPath,
|
|
471
|
+
Buffer.from(expected.fromConfigBase64, "base64"),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const rollbackReceipt = await writeAppendOnlyReceipt(
|
|
475
|
+
path.join(resolvedArchive, "receipts"),
|
|
476
|
+
`skill-rollback-${receipt.toManifestSha256.slice(7)}-to-${receipt.fromManifestSha256.slice(7)}`,
|
|
477
|
+
{
|
|
478
|
+
schemaVersion: "OwlCodaRunKitSkillRollbackReceiptV1",
|
|
479
|
+
operation: "rollback",
|
|
480
|
+
sourceUpgradeReceiptPath: resolvedReceipt,
|
|
481
|
+
sourceUpgradeReceiptSha256: sha256(receiptBytes),
|
|
482
|
+
targetRoot: resolvedTarget,
|
|
483
|
+
archiveRoot: resolvedArchive,
|
|
484
|
+
fromManifestSha256: receipt.toManifestSha256,
|
|
485
|
+
toManifestSha256: receipt.fromManifestSha256,
|
|
486
|
+
restoredArchivePath: resolvedPriorArchive,
|
|
487
|
+
replacedInstallationArchivePath: currentArchivePath,
|
|
488
|
+
projects: receipt.projects.map((project) => ({
|
|
489
|
+
workspaceRoot: project.workspaceRoot,
|
|
490
|
+
restoredConfigSha256: project.fromConfigSha256,
|
|
491
|
+
})),
|
|
492
|
+
authorizationGranted: false,
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
return {
|
|
496
|
+
status: "restored",
|
|
497
|
+
manifestSha256: receipt.fromManifestSha256,
|
|
498
|
+
rollbackReceipt,
|
|
499
|
+
replacedInstallation: {
|
|
500
|
+
manifestSha256: receipt.toManifestSha256,
|
|
501
|
+
archivePath: currentArchivePath,
|
|
502
|
+
},
|
|
503
|
+
authorizationGranted: false,
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (await exists(resolvedTarget)) {
|
|
507
|
+
await rename(resolvedTarget, resolvedPriorArchive);
|
|
508
|
+
}
|
|
509
|
+
await rename(currentArchivePath, resolvedTarget);
|
|
510
|
+
await restoreProjectConfigs(currentProjects);
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export async function recoverCodexSkillForActiveFleet({
|
|
516
|
+
targetRoot,
|
|
517
|
+
archiveRoot,
|
|
518
|
+
archivedInstallationPath,
|
|
519
|
+
workspaceRoots,
|
|
520
|
+
} = {}) {
|
|
521
|
+
if (
|
|
522
|
+
typeof targetRoot !== "string"
|
|
523
|
+
|| typeof archiveRoot !== "string"
|
|
524
|
+
|| typeof archivedInstallationPath !== "string"
|
|
525
|
+
|| !Array.isArray(workspaceRoots)
|
|
526
|
+
|| workspaceRoots.length === 0
|
|
527
|
+
) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"targetRoot, archiveRoot, archivedInstallationPath, and workspaceRoots are required",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
533
|
+
const resolvedArchive = path.resolve(archiveRoot);
|
|
534
|
+
const resolvedPrior = path.resolve(archivedInstallationPath);
|
|
535
|
+
const priorRelative = path.relative(resolvedArchive, resolvedPrior);
|
|
536
|
+
if (
|
|
537
|
+
priorRelative.startsWith("..")
|
|
538
|
+
|| path.isAbsolute(priorRelative)
|
|
539
|
+
|| priorRelative.length === 0
|
|
540
|
+
) {
|
|
541
|
+
throw new Error("skill_recovery_archive_outside_root");
|
|
542
|
+
}
|
|
543
|
+
const current = await inspectInstalledSkill({ targetRoot: resolvedTarget });
|
|
544
|
+
const prior = await inspectInstalledSkill({ targetRoot: resolvedPrior });
|
|
545
|
+
if (!current.valid || !prior.valid) {
|
|
546
|
+
throw new Error("skill_recovery_installation_identity_invalid");
|
|
547
|
+
}
|
|
548
|
+
const archivedCoreModule = await import(pathToFileURL(path.join(
|
|
549
|
+
resolvedPrior,
|
|
550
|
+
"scripts/runkit-contract/core-contract.mjs",
|
|
551
|
+
)).href);
|
|
552
|
+
const archivedCore = archivedCoreModule.currentCoreIdentity?.();
|
|
553
|
+
if (!archivedCore || typeof archivedCore.coreManifestSha256 !== "string") {
|
|
554
|
+
throw new Error("skill_recovery_archived_core_identity_missing");
|
|
555
|
+
}
|
|
556
|
+
const fleetRoots = [];
|
|
557
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
558
|
+
fleetRoots.push(await realpath(workspaceRoot));
|
|
559
|
+
}
|
|
560
|
+
const fleet = [...new Set(fleetRoots)].sort();
|
|
561
|
+
const projects = [];
|
|
562
|
+
let activeRunCount = 0;
|
|
563
|
+
for (const workspaceRoot of fleet) {
|
|
564
|
+
const configPath = path.join(
|
|
565
|
+
workspaceRoot,
|
|
566
|
+
".owlcoda/runkit/config.json",
|
|
567
|
+
);
|
|
568
|
+
const configBytes = await readFile(configPath);
|
|
569
|
+
const config = JSON.parse(configBytes.toString("utf8"));
|
|
570
|
+
if (!sameCoreIdentity(config.core, archivedCore)) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`skill_recovery_config_core_mismatch:${workspaceRoot}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const safety = inspectProjectUpgradeSafety({ workspaceRoot });
|
|
576
|
+
if (safety.issues.length > 0) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`skill_recovery_control_state_invalid:${workspaceRoot}:${safety.issues.join(",")}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const activeRunSet = new Set(safety.activeRunIds);
|
|
582
|
+
if (safety.activeLeaseIds.some((leaseId) => (
|
|
583
|
+
!activeRunSet.has(leaseId.split(":", 1)[0])
|
|
584
|
+
))) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`skill_recovery_lease_without_matching_active_run:${workspaceRoot}`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
for (const runId of safety.activeRunIds) {
|
|
590
|
+
const enginePinPath = path.join(
|
|
591
|
+
workspaceRoot,
|
|
592
|
+
".owlcoda/runkit/executions",
|
|
593
|
+
runId,
|
|
594
|
+
"engine-pin.json",
|
|
595
|
+
);
|
|
596
|
+
const pinStat = await lstat(enginePinPath);
|
|
597
|
+
if (pinStat.isSymbolicLink() || !pinStat.isFile()) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`skill_recovery_engine_pin_invalid:${workspaceRoot}:${runId}`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const enginePin = JSON.parse(await readFile(enginePinPath, "utf8"));
|
|
603
|
+
if (!sameCoreIdentity(enginePin, archivedCore)) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`skill_recovery_active_pin_mismatch:${workspaceRoot}:${runId}`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
activeRunCount += 1;
|
|
609
|
+
}
|
|
610
|
+
projects.push({
|
|
611
|
+
workspaceRoot,
|
|
612
|
+
configSha256: sha256(configBytes),
|
|
613
|
+
activeRunIds: safety.activeRunIds,
|
|
614
|
+
activeLeaseIds: safety.activeLeaseIds,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (activeRunCount === 0) {
|
|
618
|
+
throw new Error("skill_recovery_active_execution_required");
|
|
619
|
+
}
|
|
620
|
+
const replacedInstallationArchivePath = await nextAvailablePath(
|
|
621
|
+
resolvedArchive,
|
|
622
|
+
`owlcoda-runkit-replaced-${current.manifestSha256.slice(7)}`,
|
|
623
|
+
);
|
|
624
|
+
await rename(resolvedTarget, replacedInstallationArchivePath);
|
|
625
|
+
try {
|
|
626
|
+
await rename(resolvedPrior, resolvedTarget);
|
|
627
|
+
const recoveryReceipt = await writeAppendOnlyReceipt(
|
|
628
|
+
path.join(resolvedArchive, "receipts"),
|
|
629
|
+
`skill-active-recovery-${current.manifestSha256.slice(7)}-to-${prior.manifestSha256.slice(7)}`,
|
|
630
|
+
{
|
|
631
|
+
schemaVersion: "OwlCodaRunKitSkillActiveRecoveryReceiptV1",
|
|
632
|
+
operation: "active_execution_recovery",
|
|
633
|
+
targetRoot: resolvedTarget,
|
|
634
|
+
archiveRoot: resolvedArchive,
|
|
635
|
+
fromManifestSha256: current.manifestSha256,
|
|
636
|
+
toManifestSha256: prior.manifestSha256,
|
|
637
|
+
restoredCore: archivedCore,
|
|
638
|
+
restoredArchivePath: resolvedPrior,
|
|
639
|
+
replacedInstallationArchivePath,
|
|
640
|
+
projects,
|
|
641
|
+
authorizationGranted: false,
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
return {
|
|
645
|
+
status: "recovered_active_fleet",
|
|
646
|
+
manifestSha256: prior.manifestSha256,
|
|
647
|
+
core: archivedCore,
|
|
648
|
+
recoveryReceipt,
|
|
649
|
+
replacedInstallationArchivePath,
|
|
650
|
+
authorizationGranted: false,
|
|
651
|
+
};
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (await exists(resolvedTarget)) {
|
|
654
|
+
await rename(resolvedTarget, resolvedPrior);
|
|
655
|
+
}
|
|
656
|
+
await rename(replacedInstallationArchivePath, resolvedTarget);
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
201
661
|
function parseOptions(values) {
|
|
202
662
|
const options = {};
|
|
203
663
|
for (let index = 0; index < values.length; index += 2) {
|
|
204
664
|
const flag = values[index];
|
|
205
665
|
const value = values[index + 1];
|
|
206
666
|
if (!flag?.startsWith("--") || value === undefined) throw new Error("Options must be --name value pairs");
|
|
207
|
-
|
|
667
|
+
const name = flag.slice(2);
|
|
668
|
+
if (name === "workspace") {
|
|
669
|
+
options.workspace ??= [];
|
|
670
|
+
options.workspace.push(value);
|
|
671
|
+
} else {
|
|
672
|
+
options[name] = value;
|
|
673
|
+
}
|
|
208
674
|
}
|
|
209
675
|
return options;
|
|
210
676
|
}
|
|
@@ -219,6 +685,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
219
685
|
targetRoot: options.target,
|
|
220
686
|
archiveRoot: options.archive,
|
|
221
687
|
expectedUnmanagedManifestSha256: options["expected-unmanaged"] ?? null,
|
|
688
|
+
workspaceRoots: options.workspace ?? null,
|
|
222
689
|
});
|
|
223
690
|
return { exitCode: 0, ...output };
|
|
224
691
|
}
|
|
@@ -226,8 +693,35 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
226
693
|
const output = await inspectInstalledSkill({ targetRoot: options.target });
|
|
227
694
|
return { exitCode: output.valid ? 0 : 2, ...output };
|
|
228
695
|
}
|
|
229
|
-
|
|
696
|
+
if (command === "restore") {
|
|
697
|
+
const output = await restoreCodexSkill({
|
|
698
|
+
targetRoot: options.target,
|
|
699
|
+
archiveRoot: options.archive,
|
|
700
|
+
upgradeReceiptPath: options.receipt,
|
|
701
|
+
});
|
|
702
|
+
return { exitCode: 0, ...output };
|
|
703
|
+
}
|
|
704
|
+
if (command === "recover-active") {
|
|
705
|
+
const output = await recoverCodexSkillForActiveFleet({
|
|
706
|
+
targetRoot: options.target,
|
|
707
|
+
archiveRoot: options.archive,
|
|
708
|
+
archivedInstallationPath: options["archived-installation"],
|
|
709
|
+
workspaceRoots: options.workspace,
|
|
710
|
+
});
|
|
711
|
+
return { exitCode: 0, ...output };
|
|
712
|
+
}
|
|
713
|
+
throw new Error(
|
|
714
|
+
"Usage: install-codex-skill.mjs <install|inspect|restore|recover-active> [options]",
|
|
715
|
+
);
|
|
230
716
|
} catch (error) {
|
|
717
|
+
const issue = error instanceof Error ? error.message : String(error);
|
|
718
|
+
if (issue.startsWith("skill_update_blocked_active_execution:")) {
|
|
719
|
+
return {
|
|
720
|
+
status: "skill_update_blocked_active_execution",
|
|
721
|
+
exitCode: 2,
|
|
722
|
+
issues: [issue],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
231
725
|
return { status: "invalid_input", exitCode: 3, issues: [error instanceof Error ? error.message : String(error)] };
|
|
232
726
|
}
|
|
233
727
|
}
|