owlrunkit 0.22.0 → 0.23.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 +51 -0
- package/README.md +53 -9
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +55 -16
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v2.json +5 -5
- package/sbom.spdx.json +4 -4
- package/scripts/runkit-contract/core-contract.mjs +5 -1
- package/scripts/runkit-contract/fleet-skill-plan.mjs +14 -66
- package/scripts/runkit-contract/formal-workflow.mjs +1 -1
- package/scripts/runkit-contract/install-codex-skill.mjs +2097 -196
- package/scripts/runkit-contract/project-cli-resolver.mjs +8 -0
- package/scripts/runkit-contract/project-completion-continuity.mjs +545 -0
- package/scripts/runkit-contract/registry-adoption.mjs +4 -4
- package/scripts/runkit-contract/runkit-cli.mjs +41 -5
- package/scripts/runkit-contract/schemas/fleet-skill-upgrade-plan-v1.schema.json +16 -3
- package/scripts/runkit-contract/schemas/project-completion-continuity-v1.schema.json +144 -0
- package/scripts/runkit-contract/schemas/successor-scaffold-v1.schema.json +184 -0
- package/scripts/runkit-contract/schemas/team-project-status-v5.schema.json +169 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import {
|
|
5
5
|
cp,
|
|
6
|
+
link,
|
|
6
7
|
lstat,
|
|
7
8
|
mkdir,
|
|
8
9
|
mkdtemp,
|
|
10
|
+
open,
|
|
9
11
|
readFile,
|
|
10
12
|
readdir,
|
|
13
|
+
realpath,
|
|
11
14
|
rename,
|
|
12
15
|
rm,
|
|
13
16
|
writeFile,
|
|
@@ -17,14 +20,26 @@ import { pathToFileURL } from "node:url";
|
|
|
17
20
|
|
|
18
21
|
import {
|
|
19
22
|
currentCoreIdentity,
|
|
20
|
-
initializeProjectRunKit,
|
|
21
23
|
isDirectExecution,
|
|
22
24
|
validateProjectConfigV2,
|
|
23
25
|
} from "./core-contract.mjs";
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
defaultFleetRegistryPath,
|
|
28
|
+
discoverFleet,
|
|
29
|
+
} from "./fleet-discovery.mjs";
|
|
25
30
|
import { inspectProjectControlState } from "./project-control-state.mjs";
|
|
31
|
+
import { resolveProjectCli } from "./project-cli-resolver.mjs";
|
|
32
|
+
import { validateStoredRegistryAdoptionEvidence } from "./registry-adoption-gate.mjs";
|
|
26
33
|
|
|
27
34
|
const MANIFEST_FILE = ".owlcoda-install-manifest.json";
|
|
35
|
+
const TRANSACTION_SCHEMA = "OwlCodaRunKitSharedSkillTransactionV1";
|
|
36
|
+
const TRANSACTION_OWNER_FILE = "shared-skill-owner.json";
|
|
37
|
+
const MAX_PROJECT_EVIDENCE_BYTES = 1_048_576;
|
|
38
|
+
const MAX_PROJECT_LOCKFILE_BYTES = 33_554_432;
|
|
39
|
+
const MAX_FLEET_PROJECTS = 256;
|
|
40
|
+
const MAX_SKILL_RECEIPT_BYTES = 134_217_728;
|
|
41
|
+
const MAX_V2_SKILL_RECEIPT_BYTES = 33_554_432;
|
|
42
|
+
const MAX_TRANSACTION_JOURNAL_BYTES = 16_777_216;
|
|
28
43
|
|
|
29
44
|
function sha256(value) {
|
|
30
45
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -42,6 +57,149 @@ function sameCoreIdentity(left, right) {
|
|
|
42
57
|
));
|
|
43
58
|
}
|
|
44
59
|
|
|
60
|
+
function validLegacyConfigV1(config) {
|
|
61
|
+
return config?.schemaVersion === "OwlCodaRunKitConfigV1"
|
|
62
|
+
&& config?.core?.contractVersion === "0.1"
|
|
63
|
+
&& config?.authorizationPolicy === "external_explicit_authority_required";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shellQuote(value) {
|
|
67
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function projectRuntimeRepairDetail(issueCode, workspaceRoot, version) {
|
|
71
|
+
return `${issueCode}; install exact owlrunkit@${version} with the project's lockfile-owning package manager, then run npx --no-install owlrunkit bootstrap --workspace ${shellQuote(workspaceRoot)} --exact owlrunkit@${version} --apply`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function projectAdoptionRepairDetail(issueCodes, workspaceRoot, version) {
|
|
75
|
+
return `${issueCodes.join(",")}; run npx --no-install owlrunkit adopt --workspace ${shellQuote(workspaceRoot)} --exact owlrunkit@${version}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sameFileIdentity(left, right) {
|
|
79
|
+
return left.dev === right.dev
|
|
80
|
+
&& left.ino === right.ino
|
|
81
|
+
&& left.size === right.size
|
|
82
|
+
&& left.mtimeMs === right.mtimeMs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readProjectEvidenceBytes(workspaceRoot, relativePath, {
|
|
86
|
+
maxBytes = MAX_PROJECT_EVIDENCE_BYTES,
|
|
87
|
+
pathInvalidCode = "adoption_evidence_path_invalid",
|
|
88
|
+
tooLargeCode = "adoption_evidence_too_large",
|
|
89
|
+
identityChangedCode = "adoption_evidence_identity_changed",
|
|
90
|
+
} = {}) {
|
|
91
|
+
const root = await realpath(path.resolve(workspaceRoot));
|
|
92
|
+
const target = path.resolve(root, relativePath);
|
|
93
|
+
const relative = path.relative(root, target);
|
|
94
|
+
if (
|
|
95
|
+
relative.length === 0
|
|
96
|
+
|| relative === ".."
|
|
97
|
+
|| relative.startsWith(`..${path.sep}`)
|
|
98
|
+
|| path.isAbsolute(relative)
|
|
99
|
+
) {
|
|
100
|
+
throw new Error(pathInvalidCode);
|
|
101
|
+
}
|
|
102
|
+
let current = root;
|
|
103
|
+
const segments = relative.split(path.sep);
|
|
104
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
105
|
+
current = path.join(current, segments[index]);
|
|
106
|
+
const stat = await lstat(current);
|
|
107
|
+
const leaf = index === segments.length - 1;
|
|
108
|
+
if (
|
|
109
|
+
stat.isSymbolicLink()
|
|
110
|
+
|| (!leaf && !stat.isDirectory())
|
|
111
|
+
|| (leaf && !stat.isFile())
|
|
112
|
+
) {
|
|
113
|
+
throw new Error(pathInvalidCode);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (await realpath(target) !== target) {
|
|
117
|
+
throw new Error(pathInvalidCode);
|
|
118
|
+
}
|
|
119
|
+
const handle = await open(target, "r");
|
|
120
|
+
try {
|
|
121
|
+
const before = await handle.stat();
|
|
122
|
+
if (!before.isFile() || before.size > maxBytes) {
|
|
123
|
+
throw new Error(tooLargeCode);
|
|
124
|
+
}
|
|
125
|
+
const bytes = await handle.readFile();
|
|
126
|
+
const after = await handle.stat();
|
|
127
|
+
const currentStat = await lstat(target);
|
|
128
|
+
if (
|
|
129
|
+
currentStat.isSymbolicLink()
|
|
130
|
+
|| !currentStat.isFile()
|
|
131
|
+
|| !sameFileIdentity(before, after)
|
|
132
|
+
|| !sameFileIdentity(after, currentStat)
|
|
133
|
+
|| await realpath(target) !== target
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(identityChangedCode);
|
|
136
|
+
}
|
|
137
|
+
return { bytes, path: target };
|
|
138
|
+
} finally {
|
|
139
|
+
await handle.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function governedProjectFiles(projectCli, {
|
|
144
|
+
configPath,
|
|
145
|
+
configBytes,
|
|
146
|
+
adoptionPath,
|
|
147
|
+
adoptionBytes,
|
|
148
|
+
}) {
|
|
149
|
+
const workspaceRoot = projectCli.workspaceRoot;
|
|
150
|
+
const specs = [
|
|
151
|
+
{
|
|
152
|
+
role: "workspace_package_manifest",
|
|
153
|
+
absolutePath: path.join(workspaceRoot, "package.json"),
|
|
154
|
+
maxBytes: MAX_PROJECT_EVIDENCE_BYTES,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
role: "lockfile",
|
|
158
|
+
absolutePath: path.join(workspaceRoot, projectCli.lockfilePath),
|
|
159
|
+
maxBytes: MAX_PROJECT_LOCKFILE_BYTES,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
role: "installed_package_manifest",
|
|
163
|
+
absolutePath: path.join(projectCli.packageRoot, "package.json"),
|
|
164
|
+
maxBytes: MAX_PROJECT_EVIDENCE_BYTES,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
const files = [];
|
|
168
|
+
for (const spec of specs) {
|
|
169
|
+
const evidence = await readProjectEvidenceBytes(
|
|
170
|
+
workspaceRoot,
|
|
171
|
+
path.relative(workspaceRoot, spec.absolutePath),
|
|
172
|
+
{
|
|
173
|
+
maxBytes: spec.maxBytes,
|
|
174
|
+
pathInvalidCode: "project_governed_file_invalid",
|
|
175
|
+
tooLargeCode: "project_governed_file_too_large",
|
|
176
|
+
identityChangedCode: "project_governed_file_identity_changed",
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
files.push({
|
|
180
|
+
role: spec.role,
|
|
181
|
+
path: evidence.path,
|
|
182
|
+
sha256: `sha256:${sha256(evidence.bytes)}`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
files.push(
|
|
186
|
+
{
|
|
187
|
+
role: "project_config",
|
|
188
|
+
path: configPath,
|
|
189
|
+
sha256: `sha256:${sha256(configBytes)}`,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
role: "registry_adoption",
|
|
193
|
+
path: adoptionPath,
|
|
194
|
+
sha256: `sha256:${sha256(adoptionBytes)}`,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
return {
|
|
198
|
+
files,
|
|
199
|
+
sha256: `sha256:${sha256(Buffer.from(`${JSON.stringify(files)}\n`))}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
45
203
|
async function exists(filePath) {
|
|
46
204
|
try {
|
|
47
205
|
await lstat(filePath);
|
|
@@ -59,6 +217,318 @@ async function atomicWriteBytes(filePath, bytes) {
|
|
|
59
217
|
await rename(temporaryPath, filePath);
|
|
60
218
|
}
|
|
61
219
|
|
|
220
|
+
function transactionJournalPath(targetRoot) {
|
|
221
|
+
return `${path.resolve(targetRoot)}.transaction.json`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function transactionDigest(body) {
|
|
225
|
+
return `sha256:${sha256(Buffer.from(`${JSON.stringify(body)}\n`))}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function boundedJsonBytes(value, maxBytes, tooLargeCode) {
|
|
229
|
+
const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
230
|
+
if (bytes.length > maxBytes) throw new Error(tooLargeCode);
|
|
231
|
+
return bytes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function transactionEvidenceRoot(transaction) {
|
|
235
|
+
return path.join(
|
|
236
|
+
path.resolve(transaction.archiveRoot),
|
|
237
|
+
"transactions",
|
|
238
|
+
transaction.transactionId,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function transactionConfigReferenceIsValid(entry, side, transaction) {
|
|
243
|
+
const base64 = entry[`${side}Base64`];
|
|
244
|
+
const evidencePath = entry[`${side}Path`];
|
|
245
|
+
return typeof entry.configPath === "string"
|
|
246
|
+
&& typeof entry[`${side}Sha256`] === "string"
|
|
247
|
+
&& (
|
|
248
|
+
(typeof base64 === "string" && evidencePath === undefined)
|
|
249
|
+
|| (
|
|
250
|
+
base64 === undefined
|
|
251
|
+
&& typeof evidencePath === "string"
|
|
252
|
+
&& pathIsStrictChild(transactionEvidenceRoot(transaction), evidencePath)
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pathIsStrictChild(root, candidate) {
|
|
258
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
259
|
+
return relative.length > 0
|
|
260
|
+
&& relative !== ".."
|
|
261
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
262
|
+
&& !path.isAbsolute(relative);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function writeTransactionJournal(journalPath, transaction, {
|
|
266
|
+
exclusive = false,
|
|
267
|
+
} = {}) {
|
|
268
|
+
const body = { ...transaction };
|
|
269
|
+
delete body.transactionSha256;
|
|
270
|
+
const stored = {
|
|
271
|
+
...body,
|
|
272
|
+
transactionSha256: transactionDigest(body),
|
|
273
|
+
};
|
|
274
|
+
const bytes = boundedJsonBytes(
|
|
275
|
+
stored,
|
|
276
|
+
MAX_TRANSACTION_JOURNAL_BYTES,
|
|
277
|
+
"skill_update_transaction_journal_too_large",
|
|
278
|
+
);
|
|
279
|
+
if (exclusive) {
|
|
280
|
+
const temporaryPath = `${journalPath}.pending-${randomUUID()}`;
|
|
281
|
+
try {
|
|
282
|
+
await writeFile(temporaryPath, bytes, { flag: "wx" });
|
|
283
|
+
await link(temporaryPath, journalPath);
|
|
284
|
+
} finally {
|
|
285
|
+
await rm(temporaryPath, { force: true });
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
await atomicWriteBytes(journalPath, bytes);
|
|
289
|
+
}
|
|
290
|
+
return stored;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function readTransactionJournal(journalPath, {
|
|
294
|
+
targetRoot,
|
|
295
|
+
archiveRoot,
|
|
296
|
+
} = {}) {
|
|
297
|
+
const stat = await lstat(journalPath);
|
|
298
|
+
if (
|
|
299
|
+
stat.isSymbolicLink()
|
|
300
|
+
|| !stat.isFile()
|
|
301
|
+
|| stat.size > MAX_TRANSACTION_JOURNAL_BYTES
|
|
302
|
+
) {
|
|
303
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
304
|
+
}
|
|
305
|
+
const transaction = JSON.parse(await readFile(journalPath, "utf8"));
|
|
306
|
+
const body = { ...transaction };
|
|
307
|
+
delete body.transactionSha256;
|
|
308
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
309
|
+
const resolvedArchive = path.resolve(archiveRoot);
|
|
310
|
+
if (
|
|
311
|
+
transaction.schemaVersion !== TRANSACTION_SCHEMA
|
|
312
|
+
|| !new Set(["install", "restore", "restore_locking"]).has(
|
|
313
|
+
transaction.operation,
|
|
314
|
+
)
|
|
315
|
+
|| transaction.targetRoot !== resolvedTarget
|
|
316
|
+
|| transaction.archiveRoot !== resolvedArchive
|
|
317
|
+
|| transaction.authorizationGranted !== false
|
|
318
|
+
|| !/^[0-9a-f-]{36}$/u.test(transaction.transactionId ?? "")
|
|
319
|
+
|| transaction.transactionSha256 !== transactionDigest(body)
|
|
320
|
+
|| !Array.isArray(transaction.projectRoots)
|
|
321
|
+
|| transaction.projectRoots.some(root => typeof root !== "string")
|
|
322
|
+
) {
|
|
323
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
324
|
+
}
|
|
325
|
+
if (transaction.operation === "restore_locking") {
|
|
326
|
+
if (transaction.phase !== "locking") {
|
|
327
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
328
|
+
}
|
|
329
|
+
} else if (transaction.operation === "install") {
|
|
330
|
+
if (
|
|
331
|
+
!new Set([
|
|
332
|
+
"prepared",
|
|
333
|
+
"locked",
|
|
334
|
+
"prior_archived",
|
|
335
|
+
"candidate_installed",
|
|
336
|
+
"receipt_pending",
|
|
337
|
+
"committed",
|
|
338
|
+
"rolled_back",
|
|
339
|
+
]).has(transaction.phase)
|
|
340
|
+
|| typeof transaction.stagingRoot !== "string"
|
|
341
|
+
|| typeof transaction.toManifestSha256 !== "string"
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
344
|
+
}
|
|
345
|
+
const resolvedStaging = path.resolve(transaction.stagingRoot);
|
|
346
|
+
if (
|
|
347
|
+
path.dirname(resolvedStaging) !== path.dirname(resolvedTarget)
|
|
348
|
+
|| !path.basename(resolvedStaging).startsWith(".owlcoda-runkit.install-")
|
|
349
|
+
) {
|
|
350
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
351
|
+
}
|
|
352
|
+
if (transaction.adoptionOnly === true && (
|
|
353
|
+
transaction.priorManaged !== false
|
|
354
|
+
|| transaction.priorManifestSha256 !== transaction.toManifestSha256
|
|
355
|
+
|| transaction.priorArchivePath !== null
|
|
356
|
+
)) {
|
|
357
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
358
|
+
}
|
|
359
|
+
if (transaction.priorManifestSha256 === null) {
|
|
360
|
+
if (transaction.priorArchivePath !== null) {
|
|
361
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
362
|
+
}
|
|
363
|
+
} else if (transaction.adoptionOnly !== true && (
|
|
364
|
+
typeof transaction.priorArchivePath !== "string"
|
|
365
|
+
|| !pathIsStrictChild(resolvedArchive, transaction.priorArchivePath)
|
|
366
|
+
)) {
|
|
367
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
368
|
+
}
|
|
369
|
+
} else if (transaction.operation === "restore" && (
|
|
370
|
+
!new Set([
|
|
371
|
+
"prepared",
|
|
372
|
+
"current_archived",
|
|
373
|
+
"prior_restored",
|
|
374
|
+
"configs_restoring",
|
|
375
|
+
"receipt_pending",
|
|
376
|
+
"committed",
|
|
377
|
+
"rolled_back",
|
|
378
|
+
]).has(transaction.phase)
|
|
379
|
+
|| typeof transaction.fromManifestSha256 !== "string"
|
|
380
|
+
|| typeof transaction.toManifestSha256 !== "string"
|
|
381
|
+
|| typeof transaction.priorManaged !== "boolean"
|
|
382
|
+
|| typeof transaction.priorArchivePath !== "string"
|
|
383
|
+
|| !pathIsStrictChild(resolvedArchive, transaction.priorArchivePath)
|
|
384
|
+
|| typeof transaction.replacedInstallationArchivePath !== "string"
|
|
385
|
+
|| !pathIsStrictChild(
|
|
386
|
+
resolvedArchive,
|
|
387
|
+
transaction.replacedInstallationArchivePath,
|
|
388
|
+
)
|
|
389
|
+
|| typeof transaction.sourceUpgradeReceiptPath !== "string"
|
|
390
|
+
|| !pathIsStrictChild(
|
|
391
|
+
path.join(resolvedArchive, "receipts"),
|
|
392
|
+
transaction.sourceUpgradeReceiptPath,
|
|
393
|
+
)
|
|
394
|
+
|| typeof transaction.sourceUpgradeReceiptSha256 !== "string"
|
|
395
|
+
|| !Array.isArray(transaction.legacyConfigRestores)
|
|
396
|
+
|| typeof transaction.receiptPath !== "string"
|
|
397
|
+
|| typeof transaction.receiptSha256 !== "string"
|
|
398
|
+
)) {
|
|
399
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
400
|
+
}
|
|
401
|
+
if (transaction.operation === "restore") {
|
|
402
|
+
const allowedConfigPaths = new Set(transaction.projectRoots.map(root => (
|
|
403
|
+
path.join(path.resolve(root), ".owlcoda/runkit/config.json")
|
|
404
|
+
)));
|
|
405
|
+
for (const entry of transaction.legacyConfigRestores) {
|
|
406
|
+
if (!allowedConfigPaths.has(path.resolve(entry.configPath ?? ""))) {
|
|
407
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
408
|
+
}
|
|
409
|
+
if (transaction.phase === "committed") {
|
|
410
|
+
if (
|
|
411
|
+
!transactionConfigReferenceIsValid(entry, "before", transaction)
|
|
412
|
+
|| !transactionConfigReferenceIsValid(entry, "after", transaction)
|
|
413
|
+
) {
|
|
414
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
await transactionConfigBytes(entry, "before", transaction);
|
|
418
|
+
await transactionConfigBytes(entry, "after", transaction);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (transaction.receiptPath != null && (
|
|
423
|
+
!pathIsStrictChild(
|
|
424
|
+
path.join(resolvedArchive, "receipts"),
|
|
425
|
+
transaction.receiptPath,
|
|
426
|
+
)
|
|
427
|
+
|| typeof transaction.receiptSha256 !== "string"
|
|
428
|
+
)) {
|
|
429
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
430
|
+
}
|
|
431
|
+
return transaction;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function processIsAlive(pid) {
|
|
435
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return true;
|
|
436
|
+
try {
|
|
437
|
+
process.kill(pid, 0);
|
|
438
|
+
return true;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return error?.code !== "ESRCH";
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function readOwnedLock(lockPath) {
|
|
445
|
+
const stat = await lstat(lockPath);
|
|
446
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) {
|
|
447
|
+
throw new Error("skill_update_lock_invalid");
|
|
448
|
+
}
|
|
449
|
+
const ownerPath = stat.isDirectory()
|
|
450
|
+
? path.join(lockPath, TRANSACTION_OWNER_FILE)
|
|
451
|
+
: lockPath;
|
|
452
|
+
const ownerStat = await lstat(ownerPath);
|
|
453
|
+
if (
|
|
454
|
+
ownerStat.isSymbolicLink()
|
|
455
|
+
|| !ownerStat.isFile()
|
|
456
|
+
|| ownerStat.size > MAX_PROJECT_EVIDENCE_BYTES
|
|
457
|
+
) {
|
|
458
|
+
throw new Error("skill_update_lock_invalid");
|
|
459
|
+
}
|
|
460
|
+
return JSON.parse(await readFile(ownerPath, "utf8"));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function acquireOwnedDirectoryLock(lockPath, {
|
|
464
|
+
activeCode,
|
|
465
|
+
transactionId,
|
|
466
|
+
targetRoot,
|
|
467
|
+
recoverStale = false,
|
|
468
|
+
}) {
|
|
469
|
+
const owner = {
|
|
470
|
+
schemaVersion: "OwlCodaRunKitSharedSkillLockOwnerV1",
|
|
471
|
+
transactionId,
|
|
472
|
+
pid: process.pid,
|
|
473
|
+
targetRoot: path.resolve(targetRoot),
|
|
474
|
+
authorizationGranted: false,
|
|
475
|
+
};
|
|
476
|
+
const parent = path.dirname(lockPath);
|
|
477
|
+
const temporaryPath = path.join(
|
|
478
|
+
parent,
|
|
479
|
+
`.${path.basename(lockPath)}.pending-${randomUUID()}`,
|
|
480
|
+
);
|
|
481
|
+
await writeFile(
|
|
482
|
+
temporaryPath,
|
|
483
|
+
`${JSON.stringify(owner, null, 2)}\n`,
|
|
484
|
+
{ flag: "wx" },
|
|
485
|
+
);
|
|
486
|
+
const activate = async () => {
|
|
487
|
+
try {
|
|
488
|
+
await link(temporaryPath, lockPath);
|
|
489
|
+
return true;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error?.code !== "EEXIST") throw error;
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
try {
|
|
496
|
+
if (!await activate()) {
|
|
497
|
+
if (recoverStale) {
|
|
498
|
+
let existing = null;
|
|
499
|
+
try {
|
|
500
|
+
existing = await readOwnedLock(lockPath);
|
|
501
|
+
} catch {
|
|
502
|
+
// Unknown locks are never removed automatically.
|
|
503
|
+
}
|
|
504
|
+
if (
|
|
505
|
+
existing?.schemaVersion === "OwlCodaRunKitSharedSkillLockOwnerV1"
|
|
506
|
+
&& existing.targetRoot === path.resolve(targetRoot)
|
|
507
|
+
&& existing.authorizationGranted === false
|
|
508
|
+
&& !processIsAlive(existing.pid)
|
|
509
|
+
) {
|
|
510
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
511
|
+
if (!await activate()) throw new Error(activeCode);
|
|
512
|
+
} else {
|
|
513
|
+
throw new Error(activeCode);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
throw new Error(activeCode);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} finally {
|
|
520
|
+
await rm(temporaryPath, { force: true });
|
|
521
|
+
}
|
|
522
|
+
return async () => {
|
|
523
|
+
if (!await exists(lockPath)) return;
|
|
524
|
+
const currentOwner = await readOwnedLock(lockPath);
|
|
525
|
+
if (currentOwner.transactionId !== transactionId) {
|
|
526
|
+
throw new Error("skill_update_lock_owner_changed");
|
|
527
|
+
}
|
|
528
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
62
532
|
async function nextAvailablePath(parentRoot, baseName) {
|
|
63
533
|
for (let attempt = 1; ; attempt += 1) {
|
|
64
534
|
const suffix = attempt === 1
|
|
@@ -89,6 +559,17 @@ async function writeAppendOnlyReceipt(receiptsRoot, baseName, receipt) {
|
|
|
89
559
|
}
|
|
90
560
|
}
|
|
91
561
|
|
|
562
|
+
async function nextAppendOnlyReceiptPath(receiptsRoot, baseName) {
|
|
563
|
+
await mkdir(receiptsRoot, { recursive: true });
|
|
564
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
565
|
+
const suffix = attempt === 1
|
|
566
|
+
? ""
|
|
567
|
+
: `-attempt-${String(attempt).padStart(3, "0")}`;
|
|
568
|
+
const receiptPath = path.join(receiptsRoot, `${baseName}${suffix}.json`);
|
|
569
|
+
if (!await exists(receiptPath)) return receiptPath;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
92
573
|
async function fileMap(root, { excludeManifest = true } = {}) {
|
|
93
574
|
const files = {};
|
|
94
575
|
async function walk(directory, prefix = "") {
|
|
@@ -230,60 +711,958 @@ export async function inspectInstalledSkillCompatibility({ targetRoot } = {}) {
|
|
|
230
711
|
`installed_skill_core_version_mismatch:expected_${expectedCore.coreVersion}`
|
|
231
712
|
+ `:skill_${installedSkillVersion}:config_${installedConfigCoreVersion}`,
|
|
232
713
|
);
|
|
233
|
-
} else if (!sameCoreIdentity(installedConfigCore, expectedCore)) {
|
|
234
|
-
issues.push(
|
|
235
|
-
`installed_skill_core_identity_mismatch:expected_${expectedCore.coreManifestSha256}`
|
|
236
|
-
+ `:config_${installedConfigCore?.coreManifestSha256 ?? "missing"}`,
|
|
714
|
+
} else if (!sameCoreIdentity(installedConfigCore, expectedCore)) {
|
|
715
|
+
issues.push(
|
|
716
|
+
`installed_skill_core_identity_mismatch:expected_${expectedCore.coreManifestSha256}`
|
|
717
|
+
+ `:config_${installedConfigCore?.coreManifestSha256 ?? "missing"}`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
...integrity,
|
|
723
|
+
status: issues.length === 0 ? "valid" : "version_mismatch",
|
|
724
|
+
valid: issues.length === 0,
|
|
725
|
+
integrityValid: true,
|
|
726
|
+
expectedCoreVersion: expectedCore.coreVersion,
|
|
727
|
+
expectedCore,
|
|
728
|
+
installedSkillVersion,
|
|
729
|
+
installedConfigCoreVersion,
|
|
730
|
+
installedConfigCore,
|
|
731
|
+
issues,
|
|
732
|
+
authorizationGranted: false,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function inspectFleetSnapshot({
|
|
737
|
+
workspaceRoots = null,
|
|
738
|
+
fleetManifestPath = null,
|
|
739
|
+
fleetRoots = null,
|
|
740
|
+
fleetRegistryPath = null,
|
|
741
|
+
} = {}) {
|
|
742
|
+
let discovery;
|
|
743
|
+
try {
|
|
744
|
+
discovery = await discoverFleet({
|
|
745
|
+
workspaceRoots,
|
|
746
|
+
fleetManifestPath,
|
|
747
|
+
fleetRoots,
|
|
748
|
+
fleetRegistryPath,
|
|
749
|
+
});
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (
|
|
752
|
+
workspaceRoots === null
|
|
753
|
+
&& fleetManifestPath === null
|
|
754
|
+
&& fleetRoots === null
|
|
755
|
+
&& error?.message === "fleet_registry_missing"
|
|
756
|
+
) {
|
|
757
|
+
throw new Error("skill_update_fleet_registry_missing");
|
|
758
|
+
}
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
if (!discovery.complete) {
|
|
762
|
+
const details = discovery.issues
|
|
763
|
+
.map(issue => `${issue.code}:${issue.path}`)
|
|
764
|
+
.join(",");
|
|
765
|
+
throw new Error(
|
|
766
|
+
`skill_update_fleet_discovery_incomplete:${details}`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
const projects = [];
|
|
770
|
+
for (const workspaceRoot of discovery.workspaceRoots) {
|
|
771
|
+
const inspected = await inspectProjectForSkillChange(workspaceRoot);
|
|
772
|
+
const activeBlockers = inspected.blockers.filter(blocker => (
|
|
773
|
+
blocker.code === "active_execution"
|
|
774
|
+
|| blocker.code === "active_lease"
|
|
775
|
+
|| blocker.code === "control_state_invalid"
|
|
776
|
+
));
|
|
777
|
+
if (activeBlockers.length > 0) {
|
|
778
|
+
const detail = activeBlockers
|
|
779
|
+
.map(blocker => blocker.artifactId ?? blocker.detail)
|
|
780
|
+
.join(",");
|
|
781
|
+
throw new Error(
|
|
782
|
+
`skill_update_blocked_active_execution:${workspaceRoot}:${detail}`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const configBlocker = inspected.blockers.find(
|
|
786
|
+
blocker => blocker.code === "project_config_invalid",
|
|
787
|
+
);
|
|
788
|
+
if (configBlocker) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`skill_update_fleet_config_invalid:${workspaceRoot}:${configBlocker.detail}`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const runtimeBlocker = inspected.blockers.find(
|
|
794
|
+
blocker => blocker.code === "project_runtime_mismatch",
|
|
795
|
+
);
|
|
796
|
+
if (runtimeBlocker) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`skill_update_project_runtime_invalid:${workspaceRoot}:${runtimeBlocker.detail}`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
const adoptionBlocker = inspected.blockers.find(
|
|
802
|
+
blocker => blocker.code === "project_adoption_mismatch",
|
|
803
|
+
);
|
|
804
|
+
if (adoptionBlocker) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`skill_update_project_adoption_invalid:${workspaceRoot}:${adoptionBlocker.detail}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
projects.push(inspected.binding);
|
|
810
|
+
}
|
|
811
|
+
return { discovery, projects };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export async function inspectFleetForSkillChange(options = {}) {
|
|
815
|
+
return (await inspectFleetSnapshot(options)).projects;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export async function inspectProjectForSkillChange(workspaceRoot) {
|
|
819
|
+
const expectedCore = currentCoreIdentity();
|
|
820
|
+
const blockers = [];
|
|
821
|
+
const control = inspectProjectControlState({ workspaceRoot }).upgradeSafety;
|
|
822
|
+
for (const artifactId of control.activeRunIds) {
|
|
823
|
+
blockers.push({ code: "active_execution", workspaceRoot, artifactId });
|
|
824
|
+
}
|
|
825
|
+
for (const artifactId of control.activeLeaseIds) {
|
|
826
|
+
blockers.push({ code: "active_lease", workspaceRoot, artifactId });
|
|
827
|
+
}
|
|
828
|
+
for (const issue of control.issues) {
|
|
829
|
+
blockers.push({ code: "control_state_invalid", workspaceRoot, detail: issue });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const configPath = path.join(workspaceRoot, ".owlcoda/runkit/config.json");
|
|
833
|
+
let configBytes = null;
|
|
834
|
+
let config = null;
|
|
835
|
+
try {
|
|
836
|
+
configBytes = await readFile(configPath);
|
|
837
|
+
config = JSON.parse(configBytes.toString("utf8"));
|
|
838
|
+
} catch (error) {
|
|
839
|
+
blockers.push({
|
|
840
|
+
code: "project_config_invalid",
|
|
841
|
+
workspaceRoot,
|
|
842
|
+
detail: error instanceof SyntaxError
|
|
843
|
+
? "invalid_json"
|
|
844
|
+
: error instanceof Error ? error.message : String(error),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (config !== null) {
|
|
848
|
+
const validation = config.schemaVersion === "OwlCodaRunKitConfigV2"
|
|
849
|
+
? validateProjectConfigV2(config)
|
|
850
|
+
: null;
|
|
851
|
+
if ((validation !== null && !validation.valid)
|
|
852
|
+
|| (validation === null && !validLegacyConfigV1(config))) {
|
|
853
|
+
blockers.push({
|
|
854
|
+
code: "project_config_invalid",
|
|
855
|
+
workspaceRoot,
|
|
856
|
+
detail: validation?.issues?.join(",") ?? "unsupported_config",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let projectCli = null;
|
|
862
|
+
let adoptionPath = null;
|
|
863
|
+
let adoptionBytes = null;
|
|
864
|
+
let governedFiles = null;
|
|
865
|
+
if (!blockers.some(blocker => blocker.code === "project_config_invalid")) {
|
|
866
|
+
projectCli = resolveProjectCli({
|
|
867
|
+
workspaceRoot,
|
|
868
|
+
expectedVersion: expectedCore.coreVersion,
|
|
869
|
+
});
|
|
870
|
+
if (projectCli.status !== "bound") {
|
|
871
|
+
const issueCode = projectCli.issueCodes?.join(",")
|
|
872
|
+
|| "project_cli_install_binding_mismatch";
|
|
873
|
+
blockers.push({
|
|
874
|
+
code: "project_runtime_mismatch",
|
|
875
|
+
workspaceRoot,
|
|
876
|
+
detail: projectRuntimeRepairDetail(
|
|
877
|
+
issueCode,
|
|
878
|
+
workspaceRoot,
|
|
879
|
+
expectedCore.coreVersion,
|
|
880
|
+
),
|
|
881
|
+
});
|
|
882
|
+
} else {
|
|
883
|
+
const adoptionRelativePath = path.join(
|
|
884
|
+
".owlcoda/runkit/adoption",
|
|
885
|
+
`owlrunkit-${expectedCore.coreVersion}.json`,
|
|
886
|
+
);
|
|
887
|
+
let adoption = null;
|
|
888
|
+
try {
|
|
889
|
+
const evidence = await readProjectEvidenceBytes(
|
|
890
|
+
projectCli.workspaceRoot,
|
|
891
|
+
adoptionRelativePath,
|
|
892
|
+
);
|
|
893
|
+
adoptionPath = evidence.path;
|
|
894
|
+
adoptionBytes = evidence.bytes;
|
|
895
|
+
adoption = JSON.parse(adoptionBytes.toString("utf8"));
|
|
896
|
+
} catch (error) {
|
|
897
|
+
blockers.push({
|
|
898
|
+
code: "project_adoption_mismatch",
|
|
899
|
+
workspaceRoot,
|
|
900
|
+
detail: projectAdoptionRepairDetail(
|
|
901
|
+
[error instanceof SyntaxError
|
|
902
|
+
? "adoption_evidence_invalid_json"
|
|
903
|
+
: error instanceof Error ? error.message : String(error)],
|
|
904
|
+
workspaceRoot,
|
|
905
|
+
expectedCore.coreVersion,
|
|
906
|
+
),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (adoption !== null) {
|
|
910
|
+
const validation = validateStoredRegistryAdoptionEvidence({
|
|
911
|
+
evidence: adoption,
|
|
912
|
+
packageName: "owlrunkit",
|
|
913
|
+
version: expectedCore.coreVersion,
|
|
914
|
+
localInstall: projectCli,
|
|
915
|
+
});
|
|
916
|
+
if (!validation.valid) {
|
|
917
|
+
blockers.push({
|
|
918
|
+
code: "project_adoption_mismatch",
|
|
919
|
+
workspaceRoot,
|
|
920
|
+
detail: projectAdoptionRepairDetail(
|
|
921
|
+
validation.issueCodes,
|
|
922
|
+
workspaceRoot,
|
|
923
|
+
expectedCore.coreVersion,
|
|
924
|
+
),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (!blockers.some(blocker => (
|
|
929
|
+
blocker.code === "project_runtime_mismatch"
|
|
930
|
+
|| blocker.code === "project_adoption_mismatch"
|
|
931
|
+
))) {
|
|
932
|
+
try {
|
|
933
|
+
governedFiles = await governedProjectFiles(projectCli, {
|
|
934
|
+
configPath,
|
|
935
|
+
configBytes,
|
|
936
|
+
adoptionPath,
|
|
937
|
+
adoptionBytes,
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
blockers.push({
|
|
941
|
+
code: "project_runtime_mismatch",
|
|
942
|
+
workspaceRoot,
|
|
943
|
+
detail: projectRuntimeRepairDetail(
|
|
944
|
+
error instanceof Error ? error.message : String(error),
|
|
945
|
+
workspaceRoot,
|
|
946
|
+
expectedCore.coreVersion,
|
|
947
|
+
),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const binding = projectCli?.status === "bound"
|
|
955
|
+
&& configBytes !== null
|
|
956
|
+
&& adoptionBytes !== null
|
|
957
|
+
&& governedFiles !== null
|
|
958
|
+
&& !blockers.some(blocker => (
|
|
959
|
+
blocker.code === "project_config_invalid"
|
|
960
|
+
|| blocker.code === "project_runtime_mismatch"
|
|
961
|
+
|| blocker.code === "project_adoption_mismatch"
|
|
962
|
+
))
|
|
963
|
+
? {
|
|
964
|
+
workspaceRoot: projectCli.workspaceRoot,
|
|
965
|
+
configPath,
|
|
966
|
+
configSha256: sha256(configBytes),
|
|
967
|
+
packageName: projectCli.packageName,
|
|
968
|
+
packageVersion: projectCli.version,
|
|
969
|
+
packageManager: projectCli.packageManager,
|
|
970
|
+
lockfilePath: path.join(projectCli.workspaceRoot, projectCli.lockfilePath),
|
|
971
|
+
packageRoot: projectCli.packageRoot,
|
|
972
|
+
cliPath: projectCli.cliPath,
|
|
973
|
+
coreVersion: projectCli.installedCoreBinding.coreVersion,
|
|
974
|
+
coreManifestSha256: projectCli.installedCoreBinding.coreManifestSha256,
|
|
975
|
+
adoptionPath,
|
|
976
|
+
adoptionSha256: sha256(adoptionBytes),
|
|
977
|
+
governedFiles: governedFiles.files,
|
|
978
|
+
governedFilesSha256: governedFiles.sha256,
|
|
979
|
+
authorizationGranted: false,
|
|
980
|
+
}
|
|
981
|
+
: null;
|
|
982
|
+
return {
|
|
983
|
+
project: {
|
|
984
|
+
workspaceRoot,
|
|
985
|
+
configPath,
|
|
986
|
+
configCoreVersion: config?.core?.coreVersion ?? null,
|
|
987
|
+
activeRunIds: [...control.activeRunIds],
|
|
988
|
+
activeLeaseIds: [...control.activeLeaseIds],
|
|
989
|
+
upgradeSafe: blockers.length === 0,
|
|
990
|
+
},
|
|
991
|
+
blockers,
|
|
992
|
+
binding,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function acquireProjectControlLocks(projects, {
|
|
997
|
+
transactionId,
|
|
998
|
+
targetRoot,
|
|
999
|
+
}) {
|
|
1000
|
+
const releases = [];
|
|
1001
|
+
try {
|
|
1002
|
+
for (const project of [...projects].sort((left, right) => (
|
|
1003
|
+
left.workspaceRoot < right.workspaceRoot
|
|
1004
|
+
? -1
|
|
1005
|
+
: left.workspaceRoot > right.workspaceRoot ? 1 : 0
|
|
1006
|
+
))) {
|
|
1007
|
+
const canonicalWorkspaceRoot = await realpath(
|
|
1008
|
+
path.resolve(project.workspaceRoot),
|
|
1009
|
+
);
|
|
1010
|
+
const runtimeRoot = path.join(
|
|
1011
|
+
canonicalWorkspaceRoot,
|
|
1012
|
+
".owlcoda/runkit",
|
|
1013
|
+
);
|
|
1014
|
+
const runtimeStat = await lstat(runtimeRoot);
|
|
1015
|
+
if (
|
|
1016
|
+
runtimeStat.isSymbolicLink()
|
|
1017
|
+
|| !runtimeStat.isDirectory()
|
|
1018
|
+
|| await realpath(runtimeRoot) !== runtimeRoot
|
|
1019
|
+
) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`skill_update_project_control_root_invalid:${project.workspaceRoot}`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
releases.push(await acquireOwnedDirectoryLock(
|
|
1025
|
+
path.join(runtimeRoot, "control.lock"),
|
|
1026
|
+
{
|
|
1027
|
+
activeCode:
|
|
1028
|
+
`skill_update_project_control_transaction_active:${project.workspaceRoot}`,
|
|
1029
|
+
transactionId,
|
|
1030
|
+
targetRoot,
|
|
1031
|
+
},
|
|
1032
|
+
));
|
|
1033
|
+
}
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
for (const release of releases.reverse()) await release();
|
|
1036
|
+
throw error;
|
|
1037
|
+
}
|
|
1038
|
+
return async () => {
|
|
1039
|
+
for (const release of releases.reverse()) await release();
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function sameFleetIdentity(left, right) {
|
|
1044
|
+
return left.discovery.frozenManifestSha256
|
|
1045
|
+
=== right.discovery.frozenManifestSha256
|
|
1046
|
+
&& left.discovery.source === right.discovery.source
|
|
1047
|
+
&& (left.discovery.registrySha256 ?? null)
|
|
1048
|
+
=== (right.discovery.registrySha256 ?? null);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function usesFleetRegistry(source) {
|
|
1052
|
+
return source.workspaceRoots === null
|
|
1053
|
+
&& source.fleetManifestPath === null
|
|
1054
|
+
&& source.fleetRoots === null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function fleetRegistryPathFor(source) {
|
|
1058
|
+
return path.resolve(source.fleetRegistryPath ?? defaultFleetRegistryPath());
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function acquireFleetRegistryTransactionLock(source, {
|
|
1062
|
+
transactionId,
|
|
1063
|
+
targetRoot,
|
|
1064
|
+
}) {
|
|
1065
|
+
if (!usesFleetRegistry(source)) return null;
|
|
1066
|
+
const registryPath = fleetRegistryPathFor(source);
|
|
1067
|
+
await mkdir(path.dirname(registryPath), { recursive: true });
|
|
1068
|
+
return acquireOwnedDirectoryLock(`${registryPath}.lock`, {
|
|
1069
|
+
activeCode: "skill_update_fleet_registry_transaction_active",
|
|
1070
|
+
transactionId,
|
|
1071
|
+
targetRoot,
|
|
1072
|
+
recoverStale: true,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function fleetBindingFromSnapshot(snapshot) {
|
|
1077
|
+
const { discovery } = snapshot;
|
|
1078
|
+
return {
|
|
1079
|
+
schemaVersion: "OwlCodaRunKitFleetBindingV1",
|
|
1080
|
+
source: discovery.source,
|
|
1081
|
+
frozenManifestSha256: discovery.frozenManifestSha256,
|
|
1082
|
+
registryPath: discovery.registryPath ?? null,
|
|
1083
|
+
registrySha256: discovery.registrySha256 ?? null,
|
|
1084
|
+
manifestPath: discovery.manifestPath ?? null,
|
|
1085
|
+
coverageRoots: [...discovery.coverageRoots],
|
|
1086
|
+
workspaceRoots: [...discovery.workspaceRoots],
|
|
1087
|
+
authorizationGranted: false,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function sourceFromFleetBinding(binding) {
|
|
1092
|
+
if (
|
|
1093
|
+
binding?.schemaVersion !== "OwlCodaRunKitFleetBindingV1"
|
|
1094
|
+
|| binding.authorizationGranted !== false
|
|
1095
|
+
|| typeof binding.frozenManifestSha256 !== "string"
|
|
1096
|
+
|| !Array.isArray(binding.coverageRoots)
|
|
1097
|
+
|| !Array.isArray(binding.workspaceRoots)
|
|
1098
|
+
) {
|
|
1099
|
+
throw new Error("skill_rollback_fleet_binding_invalid");
|
|
1100
|
+
}
|
|
1101
|
+
if (binding.source === "explicit_workspaces") {
|
|
1102
|
+
return {
|
|
1103
|
+
workspaceRoots: binding.workspaceRoots,
|
|
1104
|
+
fleetManifestPath: null,
|
|
1105
|
+
fleetRoots: null,
|
|
1106
|
+
fleetRegistryPath: null,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
if (binding.source === "fleet_manifest" && typeof binding.manifestPath === "string") {
|
|
1110
|
+
return {
|
|
1111
|
+
workspaceRoots: null,
|
|
1112
|
+
fleetManifestPath: binding.manifestPath,
|
|
1113
|
+
fleetRoots: null,
|
|
1114
|
+
fleetRegistryPath: null,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
if (binding.source === "fleet_roots") {
|
|
1118
|
+
return {
|
|
1119
|
+
workspaceRoots: null,
|
|
1120
|
+
fleetManifestPath: null,
|
|
1121
|
+
fleetRoots: binding.coverageRoots,
|
|
1122
|
+
fleetRegistryPath: null,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (
|
|
1126
|
+
new Set(["fleet_registry", "fleet_registry_membership"]).has(binding.source)
|
|
1127
|
+
&& typeof binding.registryPath === "string"
|
|
1128
|
+
) {
|
|
1129
|
+
return {
|
|
1130
|
+
workspaceRoots: null,
|
|
1131
|
+
fleetManifestPath: null,
|
|
1132
|
+
fleetRoots: null,
|
|
1133
|
+
fleetRegistryPath: binding.registryPath,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error("skill_rollback_fleet_binding_invalid");
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function sameFleetBinding(binding, snapshot) {
|
|
1140
|
+
const current = fleetBindingFromSnapshot(snapshot);
|
|
1141
|
+
return JSON.stringify(binding) === JSON.stringify(current);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function sameProjectBindings(left, right) {
|
|
1145
|
+
if (left.length !== right.length) return false;
|
|
1146
|
+
return left.every((project, index) => {
|
|
1147
|
+
const candidate = right[index];
|
|
1148
|
+
return project.workspaceRoot === candidate.workspaceRoot
|
|
1149
|
+
&& project.governedFilesSha256 === candidate.governedFilesSha256;
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function lockFleetForSkillChange(source, {
|
|
1154
|
+
preliminary,
|
|
1155
|
+
transactionId,
|
|
1156
|
+
targetRoot,
|
|
1157
|
+
}) {
|
|
1158
|
+
let releaseProjects = null;
|
|
1159
|
+
try {
|
|
1160
|
+
const frozen = await inspectFleetSnapshot(source);
|
|
1161
|
+
if (!sameFleetIdentity(preliminary, frozen)) {
|
|
1162
|
+
throw new Error("skill_update_fleet_identity_changed");
|
|
1163
|
+
}
|
|
1164
|
+
releaseProjects = await acquireProjectControlLocks(frozen.projects, {
|
|
1165
|
+
transactionId,
|
|
1166
|
+
targetRoot,
|
|
1167
|
+
});
|
|
1168
|
+
const locked = await inspectFleetSnapshot(source);
|
|
1169
|
+
if (
|
|
1170
|
+
!sameFleetIdentity(frozen, locked)
|
|
1171
|
+
|| !sameProjectBindings(frozen.projects, locked.projects)
|
|
1172
|
+
) {
|
|
1173
|
+
throw new Error("skill_update_fleet_identity_changed");
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
snapshot: locked,
|
|
1177
|
+
async release() {
|
|
1178
|
+
await releaseProjects();
|
|
1179
|
+
releaseProjects = null;
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (releaseProjects !== null) await releaseProjects();
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function removeTransactionProjectLocks(transaction) {
|
|
1189
|
+
for (const workspaceRoot of [...transaction.projectRoots].sort().reverse()) {
|
|
1190
|
+
const lockPath = path.join(workspaceRoot, ".owlcoda/runkit/control.lock");
|
|
1191
|
+
if (!await exists(lockPath)) continue;
|
|
1192
|
+
let owner = null;
|
|
1193
|
+
try {
|
|
1194
|
+
owner = await readOwnedLock(lockPath);
|
|
1195
|
+
} catch {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (
|
|
1199
|
+
owner.schemaVersion !== "OwlCodaRunKitSharedSkillLockOwnerV1"
|
|
1200
|
+
|| owner.transactionId !== transaction.transactionId
|
|
1201
|
+
|| owner.targetRoot !== transaction.targetRoot
|
|
1202
|
+
|| owner.authorizationGranted !== false
|
|
1203
|
+
) {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function inspectExpectedInstallation(root, expectedManifestSha256, {
|
|
1211
|
+
allowUnmanaged = false,
|
|
1212
|
+
} = {}) {
|
|
1213
|
+
if (!await exists(root)) return { status: "missing" };
|
|
1214
|
+
const inspected = await inspectInstalledSkill({ targetRoot: root });
|
|
1215
|
+
const identityValid = inspected.manifestSha256 === expectedManifestSha256
|
|
1216
|
+
&& (inspected.valid || (allowUnmanaged && inspected.status === "unmanaged"));
|
|
1217
|
+
if (!identityValid) {
|
|
1218
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1219
|
+
}
|
|
1220
|
+
return inspected;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function transactionReceiptIsCommitted(transaction) {
|
|
1224
|
+
if (transaction.receiptPath === null) {
|
|
1225
|
+
return transaction.phase === "committed"
|
|
1226
|
+
&& (
|
|
1227
|
+
transaction.operation === "restore"
|
|
1228
|
+
|| transaction.priorManifestSha256 === null
|
|
1229
|
+
|| transaction.adoptionOnly === true
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (!await exists(transaction.receiptPath)) return false;
|
|
1233
|
+
const bytes = await readFile(transaction.receiptPath);
|
|
1234
|
+
if (`sha256:${sha256(bytes)}` !== transaction.receiptSha256) return false;
|
|
1235
|
+
let receipt;
|
|
1236
|
+
try {
|
|
1237
|
+
receipt = JSON.parse(bytes.toString("utf8"));
|
|
1238
|
+
} catch {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
if (transaction.operation === "install") {
|
|
1242
|
+
return receipt.schemaVersion === "OwlCodaRunKitSkillUpgradeReceiptV2"
|
|
1243
|
+
&& receipt.operation === "upgrade"
|
|
1244
|
+
&& receipt.targetRoot === transaction.targetRoot
|
|
1245
|
+
&& receipt.archiveRoot === transaction.archiveRoot
|
|
1246
|
+
&& receipt.fromManifestSha256 === transaction.priorManifestSha256
|
|
1247
|
+
&& receipt.toManifestSha256 === transaction.toManifestSha256
|
|
1248
|
+
&& receipt.priorArchivePath === transaction.priorArchivePath
|
|
1249
|
+
&& receipt.priorManaged === (transaction.priorManaged !== false)
|
|
1250
|
+
&& receipt.projectMutationPerformed === false
|
|
1251
|
+
&& receipt.authorizationGranted === false;
|
|
1252
|
+
}
|
|
1253
|
+
return new Set([
|
|
1254
|
+
"OwlCodaRunKitSkillRollbackReceiptV1",
|
|
1255
|
+
"OwlCodaRunKitSkillRollbackReceiptV2",
|
|
1256
|
+
]).has(receipt.schemaVersion)
|
|
1257
|
+
&& receipt.operation === "rollback"
|
|
1258
|
+
&& receipt.sourceUpgradeReceiptPath === transaction.sourceUpgradeReceiptPath
|
|
1259
|
+
&& `sha256:${receipt.sourceUpgradeReceiptSha256}`
|
|
1260
|
+
=== transaction.sourceUpgradeReceiptSha256
|
|
1261
|
+
&& receipt.targetRoot === transaction.targetRoot
|
|
1262
|
+
&& receipt.archiveRoot === transaction.archiveRoot
|
|
1263
|
+
&& receipt.fromManifestSha256 === transaction.fromManifestSha256
|
|
1264
|
+
&& receipt.toManifestSha256 === transaction.toManifestSha256
|
|
1265
|
+
&& receipt.replacedInstallationArchivePath
|
|
1266
|
+
=== transaction.replacedInstallationArchivePath
|
|
1267
|
+
&& receipt.authorizationGranted === false;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function cleanRecoveredTransaction(transaction, journalPath) {
|
|
1271
|
+
await removeTransactionProjectLocks(transaction);
|
|
1272
|
+
await rm(journalPath, { force: true });
|
|
1273
|
+
await rm(transactionEvidenceRoot(transaction), {
|
|
1274
|
+
recursive: true,
|
|
1275
|
+
force: true,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async function recoverInterruptedInstallTransaction(
|
|
1280
|
+
transaction,
|
|
1281
|
+
{ resolvedTarget, resolvedArchive, journalPath },
|
|
1282
|
+
) {
|
|
1283
|
+
if (transaction.phase === "rolled_back") {
|
|
1284
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1285
|
+
return {
|
|
1286
|
+
status: "recovered_rolled_back_transaction",
|
|
1287
|
+
transactionId: transaction.transactionId,
|
|
1288
|
+
authorizationGranted: false,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const priorManaged = transaction.priorManaged !== false;
|
|
1292
|
+
if (transaction.adoptionOnly === true) {
|
|
1293
|
+
const target = await inspectExpectedInstallation(
|
|
1294
|
+
resolvedTarget,
|
|
1295
|
+
transaction.toManifestSha256,
|
|
1296
|
+
{ allowUnmanaged: true },
|
|
1297
|
+
);
|
|
1298
|
+
const committed = target.valid
|
|
1299
|
+
&& await transactionReceiptIsCommitted(transaction);
|
|
1300
|
+
if (target.status === "missing") {
|
|
1301
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1302
|
+
}
|
|
1303
|
+
if (!committed && target.valid) {
|
|
1304
|
+
await rm(path.join(resolvedTarget, MANIFEST_FILE), { force: true });
|
|
1305
|
+
await inspectExpectedInstallation(
|
|
1306
|
+
resolvedTarget,
|
|
1307
|
+
transaction.priorManifestSha256,
|
|
1308
|
+
{ allowUnmanaged: true },
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
if (await exists(transaction.stagingRoot)) {
|
|
1312
|
+
await inspectExpectedInstallation(
|
|
1313
|
+
transaction.stagingRoot,
|
|
1314
|
+
transaction.toManifestSha256,
|
|
1315
|
+
);
|
|
1316
|
+
await rm(transaction.stagingRoot, { recursive: true, force: true });
|
|
1317
|
+
}
|
|
1318
|
+
if (!committed) {
|
|
1319
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1320
|
+
...transaction,
|
|
1321
|
+
phase: "rolled_back",
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1325
|
+
return {
|
|
1326
|
+
status: committed
|
|
1327
|
+
? "recovered_committed_transaction"
|
|
1328
|
+
: "recovered_rolled_back_transaction",
|
|
1329
|
+
transactionId: transaction.transactionId,
|
|
1330
|
+
authorizationGranted: false,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const target = await inspectExpectedInstallation(
|
|
1335
|
+
resolvedTarget,
|
|
1336
|
+
transaction.toManifestSha256,
|
|
1337
|
+
).catch(async (error) => {
|
|
1338
|
+
if (transaction.priorManifestSha256 !== null) {
|
|
1339
|
+
const prior = await inspectExpectedInstallation(
|
|
1340
|
+
resolvedTarget,
|
|
1341
|
+
transaction.priorManifestSha256,
|
|
1342
|
+
{ allowUnmanaged: !priorManaged },
|
|
1343
|
+
).catch(() => null);
|
|
1344
|
+
if (prior !== null) return prior;
|
|
1345
|
+
}
|
|
1346
|
+
throw error;
|
|
1347
|
+
});
|
|
1348
|
+
const committed = target.status !== "missing"
|
|
1349
|
+
&& target.manifestSha256 === transaction.toManifestSha256
|
|
1350
|
+
&& await transactionReceiptIsCommitted(transaction);
|
|
1351
|
+
if (committed) {
|
|
1352
|
+
if (transaction.priorManifestSha256 !== null) {
|
|
1353
|
+
await inspectExpectedInstallation(
|
|
1354
|
+
transaction.priorArchivePath,
|
|
1355
|
+
transaction.priorManifestSha256,
|
|
1356
|
+
{ allowUnmanaged: !priorManaged },
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
if (await exists(transaction.stagingRoot)) {
|
|
1360
|
+
await inspectExpectedInstallation(
|
|
1361
|
+
transaction.stagingRoot,
|
|
1362
|
+
transaction.toManifestSha256,
|
|
1363
|
+
);
|
|
1364
|
+
await rm(transaction.stagingRoot, { recursive: true, force: true });
|
|
1365
|
+
}
|
|
1366
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1367
|
+
return {
|
|
1368
|
+
status: "recovered_committed_transaction",
|
|
1369
|
+
transactionId: transaction.transactionId,
|
|
1370
|
+
authorizationGranted: false,
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
await mkdir(resolvedArchive, { recursive: true });
|
|
1375
|
+
if (transaction.priorManifestSha256 === null) {
|
|
1376
|
+
if (target.status !== "missing") {
|
|
1377
|
+
if (target.manifestSha256 !== transaction.toManifestSha256) {
|
|
1378
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1379
|
+
}
|
|
1380
|
+
const failedArchivePath = await nextAvailablePath(
|
|
1381
|
+
resolvedArchive,
|
|
1382
|
+
`owlcoda-runkit-failed-${transaction.toManifestSha256.slice(7)}`,
|
|
1383
|
+
);
|
|
1384
|
+
await rename(resolvedTarget, failedArchivePath);
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
const priorArchive = await inspectExpectedInstallation(
|
|
1388
|
+
transaction.priorArchivePath,
|
|
1389
|
+
transaction.priorManifestSha256,
|
|
1390
|
+
{ allowUnmanaged: !priorManaged },
|
|
1391
|
+
);
|
|
1392
|
+
if (
|
|
1393
|
+
target.status !== "missing"
|
|
1394
|
+
&& target.manifestSha256 === transaction.priorManifestSha256
|
|
1395
|
+
) {
|
|
1396
|
+
if (priorArchive.status !== "missing") {
|
|
1397
|
+
throw new Error("skill_update_transaction_prior_identity_duplicated");
|
|
1398
|
+
}
|
|
1399
|
+
} else {
|
|
1400
|
+
if (priorArchive.status === "missing") {
|
|
1401
|
+
throw new Error("skill_update_transaction_prior_identity_missing");
|
|
1402
|
+
}
|
|
1403
|
+
if (target.status !== "missing") {
|
|
1404
|
+
if (target.manifestSha256 !== transaction.toManifestSha256) {
|
|
1405
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1406
|
+
}
|
|
1407
|
+
const failedArchivePath = await nextAvailablePath(
|
|
1408
|
+
resolvedArchive,
|
|
1409
|
+
`owlcoda-runkit-failed-${transaction.toManifestSha256.slice(7)}`,
|
|
1410
|
+
);
|
|
1411
|
+
await rename(resolvedTarget, failedArchivePath);
|
|
1412
|
+
}
|
|
1413
|
+
await rename(transaction.priorArchivePath, resolvedTarget);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (await exists(transaction.stagingRoot)) {
|
|
1417
|
+
await inspectExpectedInstallation(
|
|
1418
|
+
transaction.stagingRoot,
|
|
1419
|
+
transaction.toManifestSha256,
|
|
1420
|
+
);
|
|
1421
|
+
await rm(transaction.stagingRoot, { recursive: true, force: true });
|
|
1422
|
+
}
|
|
1423
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1424
|
+
...transaction,
|
|
1425
|
+
phase: "rolled_back",
|
|
1426
|
+
});
|
|
1427
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1428
|
+
return {
|
|
1429
|
+
status: "recovered_rolled_back_transaction",
|
|
1430
|
+
transactionId: transaction.transactionId,
|
|
1431
|
+
authorizationGranted: false,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
async function transactionConfigBytes(entry, side, transaction) {
|
|
1436
|
+
const base64 = entry[`${side}Base64`];
|
|
1437
|
+
const evidencePath = entry[`${side}Path`];
|
|
1438
|
+
const expectedSha256 = entry[`${side}Sha256`];
|
|
1439
|
+
if (
|
|
1440
|
+
typeof entry.configPath !== "string"
|
|
1441
|
+
|| typeof expectedSha256 !== "string"
|
|
1442
|
+
) {
|
|
1443
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
1444
|
+
}
|
|
1445
|
+
let bytes;
|
|
1446
|
+
if (typeof base64 === "string" && evidencePath === undefined) {
|
|
1447
|
+
bytes = Buffer.from(base64, "base64");
|
|
1448
|
+
if (bytes.length > MAX_PROJECT_EVIDENCE_BYTES) {
|
|
1449
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
1450
|
+
}
|
|
1451
|
+
} else if (
|
|
1452
|
+
base64 === undefined
|
|
1453
|
+
&& typeof evidencePath === "string"
|
|
1454
|
+
&& transaction !== undefined
|
|
1455
|
+
&& pathIsStrictChild(transactionEvidenceRoot(transaction), evidencePath)
|
|
1456
|
+
) {
|
|
1457
|
+
const root = transactionEvidenceRoot(transaction);
|
|
1458
|
+
const evidence = await readProjectEvidenceBytes(
|
|
1459
|
+
root,
|
|
1460
|
+
path.relative(root, evidencePath),
|
|
1461
|
+
{
|
|
1462
|
+
maxBytes: MAX_PROJECT_EVIDENCE_BYTES,
|
|
1463
|
+
pathInvalidCode: "skill_update_transaction_journal_invalid",
|
|
1464
|
+
tooLargeCode: "skill_update_transaction_journal_invalid",
|
|
1465
|
+
identityChangedCode: "skill_update_transaction_journal_invalid",
|
|
1466
|
+
},
|
|
1467
|
+
);
|
|
1468
|
+
bytes = evidence.bytes;
|
|
1469
|
+
} else {
|
|
1470
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
1471
|
+
}
|
|
1472
|
+
if (`sha256:${sha256(bytes)}` !== expectedSha256) {
|
|
1473
|
+
throw new Error("skill_update_transaction_journal_invalid");
|
|
1474
|
+
}
|
|
1475
|
+
return bytes;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function writeLegacyConfigRestoreEvidence(transaction, restores) {
|
|
1479
|
+
for (const restore of restores) {
|
|
1480
|
+
if (
|
|
1481
|
+
restore.beforeBytes.length > MAX_PROJECT_EVIDENCE_BYTES
|
|
1482
|
+
|| restore.afterBytes.length > MAX_PROJECT_EVIDENCE_BYTES
|
|
1483
|
+
) {
|
|
1484
|
+
throw new Error("skill_rollback_legacy_config_too_large");
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const root = transactionEvidenceRoot(transaction);
|
|
1488
|
+
await mkdir(path.dirname(root), { recursive: true });
|
|
1489
|
+
await mkdir(root);
|
|
1490
|
+
const entries = [];
|
|
1491
|
+
for (const [index, restore] of restores.entries()) {
|
|
1492
|
+
const stem = String(index).padStart(3, "0");
|
|
1493
|
+
const beforePath = path.join(root, `${stem}-before.json`);
|
|
1494
|
+
const afterPath = path.join(root, `${stem}-after.json`);
|
|
1495
|
+
await writeFile(beforePath, restore.beforeBytes, { flag: "wx" });
|
|
1496
|
+
await writeFile(afterPath, restore.afterBytes, { flag: "wx" });
|
|
1497
|
+
entries.push({
|
|
1498
|
+
configPath: restore.configPath,
|
|
1499
|
+
beforeSha256: `sha256:${sha256(restore.beforeBytes)}`,
|
|
1500
|
+
beforePath,
|
|
1501
|
+
afterSha256: `sha256:${sha256(restore.afterBytes)}`,
|
|
1502
|
+
afterPath,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
return entries;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
async function maybeInspectExpectedInstallation(root, expectedManifestSha256, options) {
|
|
1509
|
+
try {
|
|
1510
|
+
const inspected = await inspectExpectedInstallation(
|
|
1511
|
+
root,
|
|
1512
|
+
expectedManifestSha256,
|
|
1513
|
+
options,
|
|
1514
|
+
);
|
|
1515
|
+
return inspected.status === "missing" ? null : inspected;
|
|
1516
|
+
} catch {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async function recoverInterruptedRestoreTransaction(
|
|
1522
|
+
transaction,
|
|
1523
|
+
{ resolvedTarget, journalPath },
|
|
1524
|
+
) {
|
|
1525
|
+
if (transaction.phase === "rolled_back") {
|
|
1526
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1527
|
+
return {
|
|
1528
|
+
status: "recovered_rolled_back_transaction",
|
|
1529
|
+
transactionId: transaction.transactionId,
|
|
1530
|
+
authorizationGranted: false,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const sourceReceiptBytes = await readFile(transaction.sourceUpgradeReceiptPath);
|
|
1534
|
+
if (`sha256:${sha256(sourceReceiptBytes)}` !== transaction.sourceUpgradeReceiptSha256) {
|
|
1535
|
+
throw new Error("skill_update_transaction_source_receipt_identity_mismatch");
|
|
1536
|
+
}
|
|
1537
|
+
const targetPrior = await maybeInspectExpectedInstallation(
|
|
1538
|
+
resolvedTarget,
|
|
1539
|
+
transaction.toManifestSha256,
|
|
1540
|
+
{ allowUnmanaged: !transaction.priorManaged },
|
|
1541
|
+
);
|
|
1542
|
+
const targetCurrent = targetPrior === null
|
|
1543
|
+
? await maybeInspectExpectedInstallation(
|
|
1544
|
+
resolvedTarget,
|
|
1545
|
+
transaction.fromManifestSha256,
|
|
1546
|
+
)
|
|
1547
|
+
: null;
|
|
1548
|
+
if (await exists(resolvedTarget) && targetPrior === null && targetCurrent === null) {
|
|
1549
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1550
|
+
}
|
|
1551
|
+
const committed = targetPrior !== null
|
|
1552
|
+
&& await transactionReceiptIsCommitted(transaction);
|
|
1553
|
+
if (committed) {
|
|
1554
|
+
const priorArchive = await maybeInspectExpectedInstallation(
|
|
1555
|
+
transaction.priorArchivePath,
|
|
1556
|
+
transaction.toManifestSha256,
|
|
1557
|
+
{ allowUnmanaged: !transaction.priorManaged },
|
|
1558
|
+
);
|
|
1559
|
+
const replaced = await maybeInspectExpectedInstallation(
|
|
1560
|
+
transaction.replacedInstallationArchivePath,
|
|
1561
|
+
transaction.fromManifestSha256,
|
|
237
1562
|
);
|
|
1563
|
+
if (priorArchive !== null || replaced === null) {
|
|
1564
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1565
|
+
}
|
|
1566
|
+
for (const entry of transaction.legacyConfigRestores) {
|
|
1567
|
+
const actual = await readFile(entry.configPath);
|
|
1568
|
+
if (`sha256:${sha256(actual)}` !== entry.afterSha256) {
|
|
1569
|
+
throw new Error("skill_update_transaction_config_identity_mismatch");
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1573
|
+
return {
|
|
1574
|
+
status: "recovered_committed_transaction",
|
|
1575
|
+
operation: "restore",
|
|
1576
|
+
transactionId: transaction.transactionId,
|
|
1577
|
+
manifestSha256: transaction.toManifestSha256,
|
|
1578
|
+
rollbackReceipt: transaction.receiptPath,
|
|
1579
|
+
replacedInstallation: {
|
|
1580
|
+
manifestSha256: transaction.fromManifestSha256,
|
|
1581
|
+
archivePath: transaction.replacedInstallationArchivePath,
|
|
1582
|
+
},
|
|
1583
|
+
projectMutationPerformed: transaction.legacyConfigRestores.length > 0,
|
|
1584
|
+
authorizationGranted: false,
|
|
1585
|
+
};
|
|
238
1586
|
}
|
|
239
1587
|
|
|
1588
|
+
const priorArchive = await maybeInspectExpectedInstallation(
|
|
1589
|
+
transaction.priorArchivePath,
|
|
1590
|
+
transaction.toManifestSha256,
|
|
1591
|
+
{ allowUnmanaged: !transaction.priorManaged },
|
|
1592
|
+
);
|
|
1593
|
+
const replaced = await maybeInspectExpectedInstallation(
|
|
1594
|
+
transaction.replacedInstallationArchivePath,
|
|
1595
|
+
transaction.fromManifestSha256,
|
|
1596
|
+
);
|
|
1597
|
+
if (targetPrior !== null) {
|
|
1598
|
+
if (priorArchive !== null) {
|
|
1599
|
+
throw new Error("skill_update_transaction_prior_identity_duplicated");
|
|
1600
|
+
}
|
|
1601
|
+
await rename(resolvedTarget, transaction.priorArchivePath);
|
|
1602
|
+
} else if (priorArchive === null) {
|
|
1603
|
+
throw new Error("skill_update_transaction_prior_identity_missing");
|
|
1604
|
+
}
|
|
1605
|
+
if (targetCurrent === null) {
|
|
1606
|
+
if (replaced === null) {
|
|
1607
|
+
throw new Error("skill_update_transaction_installation_identity_mismatch");
|
|
1608
|
+
}
|
|
1609
|
+
await rename(transaction.replacedInstallationArchivePath, resolvedTarget);
|
|
1610
|
+
} else if (replaced !== null) {
|
|
1611
|
+
throw new Error("skill_update_transaction_installation_identity_duplicated");
|
|
1612
|
+
}
|
|
1613
|
+
for (const entry of transaction.legacyConfigRestores) {
|
|
1614
|
+
await atomicWriteBytes(
|
|
1615
|
+
entry.configPath,
|
|
1616
|
+
await transactionConfigBytes(entry, "before", transaction),
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1620
|
+
...transaction,
|
|
1621
|
+
phase: "rolled_back",
|
|
1622
|
+
});
|
|
1623
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
240
1624
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
valid: issues.length === 0,
|
|
244
|
-
integrityValid: true,
|
|
245
|
-
expectedCoreVersion: expectedCore.coreVersion,
|
|
246
|
-
expectedCore,
|
|
247
|
-
installedSkillVersion,
|
|
248
|
-
installedConfigCoreVersion,
|
|
249
|
-
installedConfigCore,
|
|
250
|
-
issues,
|
|
1625
|
+
status: "recovered_rolled_back_transaction",
|
|
1626
|
+
transactionId: transaction.transactionId,
|
|
251
1627
|
authorizationGranted: false,
|
|
252
1628
|
};
|
|
253
1629
|
}
|
|
254
1630
|
|
|
255
|
-
export async function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
fleetRoots = null,
|
|
259
|
-
fleetRegistryPath = null,
|
|
1631
|
+
export async function recoverInterruptedCodexSkillTransaction({
|
|
1632
|
+
targetRoot,
|
|
1633
|
+
archiveRoot,
|
|
260
1634
|
} = {}) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
discovery = await discoverFleet({
|
|
264
|
-
workspaceRoots,
|
|
265
|
-
fleetManifestPath,
|
|
266
|
-
fleetRoots,
|
|
267
|
-
fleetRegistryPath,
|
|
268
|
-
});
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if (
|
|
271
|
-
workspaceRoots === null
|
|
272
|
-
&& fleetManifestPath === null
|
|
273
|
-
&& fleetRoots === null
|
|
274
|
-
&& error?.message === "fleet_registry_missing"
|
|
275
|
-
) {
|
|
276
|
-
throw new Error("skill_update_fleet_registry_missing");
|
|
277
|
-
}
|
|
278
|
-
throw error;
|
|
1635
|
+
if (typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
|
|
1636
|
+
throw new Error("targetRoot and archiveRoot are required");
|
|
279
1637
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
1638
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
1639
|
+
const resolvedArchive = path.resolve(archiveRoot);
|
|
1640
|
+
const journalPath = transactionJournalPath(resolvedTarget);
|
|
1641
|
+
if (!await exists(journalPath)) {
|
|
1642
|
+
return { status: "no_pending_transaction", authorizationGranted: false };
|
|
1643
|
+
}
|
|
1644
|
+
const transaction = await readTransactionJournal(journalPath, {
|
|
1645
|
+
targetRoot: resolvedTarget,
|
|
1646
|
+
archiveRoot: resolvedArchive,
|
|
1647
|
+
});
|
|
1648
|
+
const context = { resolvedTarget, resolvedArchive, journalPath };
|
|
1649
|
+
if (transaction.operation === "restore_locking") {
|
|
1650
|
+
await cleanRecoveredTransaction(transaction, journalPath);
|
|
1651
|
+
return {
|
|
1652
|
+
status: "recovered_rolled_back_transaction",
|
|
1653
|
+
operation: "restore_locking",
|
|
1654
|
+
transactionId: transaction.transactionId,
|
|
1655
|
+
authorizationGranted: false,
|
|
1656
|
+
};
|
|
284
1657
|
}
|
|
1658
|
+
return transaction.operation === "install"
|
|
1659
|
+
? recoverInterruptedInstallTransaction(transaction, context)
|
|
1660
|
+
: recoverInterruptedRestoreTransaction(transaction, context);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function inspectLegacyProjectsForRollback(workspaceRoots) {
|
|
285
1664
|
const projects = [];
|
|
286
|
-
for (const workspaceRoot of
|
|
1665
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
287
1666
|
const safety = inspectProjectControlState({ workspaceRoot }).upgradeSafety;
|
|
288
1667
|
if (safety.status !== "safe") {
|
|
289
1668
|
const blockers = [
|
|
@@ -295,10 +1674,7 @@ export async function inspectFleetForSkillChange({
|
|
|
295
1674
|
`skill_update_blocked_active_execution:${workspaceRoot}:${blockers}`,
|
|
296
1675
|
);
|
|
297
1676
|
}
|
|
298
|
-
const configPath = path.join(
|
|
299
|
-
workspaceRoot,
|
|
300
|
-
".owlcoda/runkit/config.json",
|
|
301
|
-
);
|
|
1677
|
+
const configPath = path.join(workspaceRoot, ".owlcoda/runkit/config.json");
|
|
302
1678
|
const configBytes = await readFile(configPath);
|
|
303
1679
|
let config;
|
|
304
1680
|
try {
|
|
@@ -306,19 +1682,13 @@ export async function inspectFleetForSkillChange({
|
|
|
306
1682
|
} catch {
|
|
307
1683
|
throw new Error(`skill_update_fleet_config_invalid:${workspaceRoot}:invalid_json`);
|
|
308
1684
|
}
|
|
309
|
-
const
|
|
1685
|
+
const validation = config?.schemaVersion === "OwlCodaRunKitConfigV2"
|
|
310
1686
|
? validateProjectConfigV2(config)
|
|
311
1687
|
: null;
|
|
312
|
-
|
|
313
|
-
&& config
|
|
314
|
-
&& config?.authorizationPolicy === "external_explicit_authority_required";
|
|
315
|
-
if (
|
|
316
|
-
(configV2 !== null && !configV2.valid)
|
|
317
|
-
|| (configV2 === null && !supportedV1)
|
|
318
|
-
) {
|
|
319
|
-
const issues = configV2?.issues?.join(",") ?? "unsupported_config";
|
|
1688
|
+
if ((validation !== null && !validation.valid)
|
|
1689
|
+
|| (validation === null && !validLegacyConfigV1(config))) {
|
|
320
1690
|
throw new Error(
|
|
321
|
-
`skill_update_fleet_config_invalid:${workspaceRoot}:${issues}`,
|
|
1691
|
+
`skill_update_fleet_config_invalid:${workspaceRoot}:${validation?.issues?.join(",") ?? "unsupported_config"}`,
|
|
322
1692
|
);
|
|
323
1693
|
}
|
|
324
1694
|
projects.push({
|
|
@@ -331,12 +1701,6 @@ export async function inspectFleetForSkillChange({
|
|
|
331
1701
|
return projects;
|
|
332
1702
|
}
|
|
333
1703
|
|
|
334
|
-
async function restoreProjectConfigs(projects) {
|
|
335
|
-
for (const project of projects) {
|
|
336
|
-
await atomicWriteBytes(project.configPath, project.configBytes);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
1704
|
export async function installCodexSkill({
|
|
341
1705
|
repositoryRoot,
|
|
342
1706
|
targetRoot,
|
|
@@ -350,119 +1714,307 @@ export async function installCodexSkill({
|
|
|
350
1714
|
if (typeof repositoryRoot !== "string" || typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
|
|
351
1715
|
throw new Error("repositoryRoot, targetRoot, and archiveRoot are required");
|
|
352
1716
|
}
|
|
353
|
-
const
|
|
1717
|
+
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
1718
|
+
const resolvedArchiveRoot = path.resolve(archiveRoot);
|
|
1719
|
+
const resolvedRepositoryRoot = path.resolve(repositoryRoot);
|
|
1720
|
+
const source = {
|
|
1721
|
+
workspaceRoots,
|
|
1722
|
+
fleetManifestPath,
|
|
1723
|
+
fleetRoots,
|
|
1724
|
+
fleetRegistryPath,
|
|
1725
|
+
};
|
|
1726
|
+
const parent = path.dirname(resolvedTargetRoot);
|
|
354
1727
|
await mkdir(parent, { recursive: true });
|
|
355
|
-
const
|
|
1728
|
+
const transactionId = randomUUID();
|
|
1729
|
+
const releaseSkillLock = await acquireOwnedDirectoryLock(
|
|
1730
|
+
`${resolvedTargetRoot}.lock`,
|
|
1731
|
+
{
|
|
1732
|
+
activeCode: "skill_update_shared_skill_transaction_active",
|
|
1733
|
+
transactionId,
|
|
1734
|
+
targetRoot: resolvedTargetRoot,
|
|
1735
|
+
recoverStale: true,
|
|
1736
|
+
},
|
|
1737
|
+
);
|
|
1738
|
+
let stagingRoot = null;
|
|
356
1739
|
let stagingPresent = true;
|
|
1740
|
+
let fleetLock = null;
|
|
1741
|
+
let releaseRegistryLock = null;
|
|
1742
|
+
let transaction = null;
|
|
1743
|
+
const journalPath = transactionJournalPath(resolvedTargetRoot);
|
|
357
1744
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1745
|
+
releaseRegistryLock = await acquireFleetRegistryTransactionLock(source, {
|
|
1746
|
+
transactionId,
|
|
1747
|
+
targetRoot: resolvedTargetRoot,
|
|
1748
|
+
});
|
|
1749
|
+
await recoverInterruptedCodexSkillTransaction({
|
|
1750
|
+
targetRoot: resolvedTargetRoot,
|
|
1751
|
+
archiveRoot: resolvedArchiveRoot,
|
|
1752
|
+
});
|
|
1753
|
+
stagingRoot = await mkdtemp(path.join(parent, ".owlcoda-runkit.install-"));
|
|
1754
|
+
const desired = await assemble({
|
|
1755
|
+
repositoryRoot: resolvedRepositoryRoot,
|
|
1756
|
+
stagingRoot,
|
|
1757
|
+
});
|
|
1758
|
+
const prior = await inspectInstalledSkill({ targetRoot: resolvedTargetRoot });
|
|
1759
|
+
const explicitFleetSource = (
|
|
1760
|
+
workspaceRoots !== null
|
|
1761
|
+
|| fleetManifestPath !== null
|
|
1762
|
+
|| fleetRoots !== null
|
|
1763
|
+
|| fleetRegistryPath !== null
|
|
1764
|
+
);
|
|
1765
|
+
const registeredFleetExists = await exists(fleetRegistryPathFor(source));
|
|
365
1766
|
if (prior.status === "unmanaged") {
|
|
366
1767
|
if (prior.manifestSha256 === desired.manifestSha256) {
|
|
367
|
-
|
|
1768
|
+
const preliminary = explicitFleetSource || registeredFleetExists
|
|
1769
|
+
? await inspectFleetSnapshot(source)
|
|
1770
|
+
: null;
|
|
1771
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1772
|
+
schemaVersion: TRANSACTION_SCHEMA,
|
|
1773
|
+
transactionId,
|
|
1774
|
+
operation: "install",
|
|
1775
|
+
phase: "prepared",
|
|
1776
|
+
targetRoot: resolvedTargetRoot,
|
|
1777
|
+
archiveRoot: resolvedArchiveRoot,
|
|
1778
|
+
stagingRoot,
|
|
1779
|
+
priorManifestSha256: prior.manifestSha256,
|
|
1780
|
+
priorArchivePath: null,
|
|
1781
|
+
priorManaged: false,
|
|
1782
|
+
adoptionOnly: true,
|
|
1783
|
+
toManifestSha256: desired.manifestSha256,
|
|
1784
|
+
projectRoots: preliminary?.projects.map(project => project.workspaceRoot) ?? [],
|
|
1785
|
+
receiptPath: null,
|
|
1786
|
+
receiptSha256: null,
|
|
1787
|
+
authorizationGranted: false,
|
|
1788
|
+
}, { exclusive: true });
|
|
1789
|
+
if (preliminary !== null) {
|
|
1790
|
+
fleetLock = await lockFleetForSkillChange(source, {
|
|
1791
|
+
preliminary,
|
|
1792
|
+
transactionId,
|
|
1793
|
+
targetRoot: resolvedTargetRoot,
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1797
|
+
...transaction,
|
|
1798
|
+
phase: "locked",
|
|
1799
|
+
});
|
|
1800
|
+
await replaceManifestAtomically(resolvedTargetRoot, desired);
|
|
1801
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1802
|
+
...transaction,
|
|
1803
|
+
phase: "candidate_installed",
|
|
1804
|
+
});
|
|
1805
|
+
if (fleetLock !== null) {
|
|
1806
|
+
const postflight = await inspectFleetSnapshot(source);
|
|
1807
|
+
if (
|
|
1808
|
+
!sameFleetIdentity(fleetLock.snapshot, postflight)
|
|
1809
|
+
|| !sameProjectBindings(fleetLock.snapshot.projects, postflight.projects)
|
|
1810
|
+
) {
|
|
1811
|
+
throw new Error("skill_update_fleet_postflight_changed");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1815
|
+
...transaction,
|
|
1816
|
+
phase: "committed",
|
|
1817
|
+
});
|
|
1818
|
+
if (fleetLock !== null) {
|
|
1819
|
+
await fleetLock.release();
|
|
1820
|
+
fleetLock = null;
|
|
1821
|
+
}
|
|
1822
|
+
await rm(journalPath, { force: true });
|
|
1823
|
+
transaction = null;
|
|
368
1824
|
return { status: "adopted", manifestSha256: desired.manifestSha256, previousInstallation: null };
|
|
369
1825
|
}
|
|
370
1826
|
if (prior.manifestSha256 !== expectedUnmanagedManifestSha256) {
|
|
371
1827
|
throw new Error("installed_skill_drift: unmanaged installation differs from authoritative source and expected prior manifest");
|
|
372
1828
|
}
|
|
373
|
-
prior = { ...prior, valid: true };
|
|
374
1829
|
}
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1830
|
+
if (
|
|
1831
|
+
prior.status !== "missing"
|
|
1832
|
+
&& prior.status !== "unmanaged"
|
|
1833
|
+
&& !prior.valid
|
|
1834
|
+
) {
|
|
1835
|
+
throw new Error(`installed_skill_drift: ${prior.issues.join("; ")}`);
|
|
1836
|
+
}
|
|
1837
|
+
if (
|
|
1838
|
+
prior.status !== "missing"
|
|
1839
|
+
&& prior.manifestSha256 === desired.manifestSha256
|
|
1840
|
+
) {
|
|
1841
|
+
if (prior.manifest?.sourceRepository !== desired.sourceRepository) {
|
|
1842
|
+
await replaceManifestAtomically(resolvedTargetRoot, desired);
|
|
379
1843
|
return { status: "provenance_updated", manifestSha256: desired.manifestSha256, previousInstallation: null };
|
|
380
1844
|
}
|
|
381
1845
|
return { status: "unchanged", manifestSha256: desired.manifestSha256, previousInstallation: null };
|
|
382
1846
|
}
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1847
|
+
const shouldInspectFleet = prior.status !== "missing"
|
|
1848
|
+
|| explicitFleetSource
|
|
1849
|
+
|| registeredFleetExists;
|
|
1850
|
+
const preliminary = shouldInspectFleet
|
|
1851
|
+
? await inspectFleetSnapshot(source)
|
|
1852
|
+
: null;
|
|
1853
|
+
let archivePath = null;
|
|
1854
|
+
if (prior.status !== "missing") {
|
|
1855
|
+
await mkdir(resolvedArchiveRoot, { recursive: true });
|
|
1856
|
+
archivePath = await nextAvailablePath(
|
|
1857
|
+
resolvedArchiveRoot,
|
|
1858
|
+
`owlcoda-runkit-${prior.manifestSha256.slice(7)}`,
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1862
|
+
schemaVersion: TRANSACTION_SCHEMA,
|
|
1863
|
+
transactionId,
|
|
1864
|
+
operation: "install",
|
|
1865
|
+
phase: "prepared",
|
|
1866
|
+
targetRoot: resolvedTargetRoot,
|
|
1867
|
+
archiveRoot: resolvedArchiveRoot,
|
|
1868
|
+
stagingRoot,
|
|
1869
|
+
priorManifestSha256: prior.status === "missing"
|
|
1870
|
+
? null
|
|
1871
|
+
: prior.manifestSha256,
|
|
1872
|
+
priorArchivePath: archivePath,
|
|
1873
|
+
priorManaged: prior.status !== "unmanaged",
|
|
1874
|
+
adoptionOnly: false,
|
|
1875
|
+
toManifestSha256: desired.manifestSha256,
|
|
1876
|
+
projectRoots: preliminary?.projects.map(project => project.workspaceRoot) ?? [],
|
|
1877
|
+
receiptPath: null,
|
|
1878
|
+
receiptSha256: null,
|
|
1879
|
+
authorizationGranted: false,
|
|
1880
|
+
}, { exclusive: true });
|
|
1881
|
+
if (preliminary !== null) {
|
|
1882
|
+
fleetLock = await lockFleetForSkillChange(source, {
|
|
1883
|
+
preliminary,
|
|
1884
|
+
transactionId,
|
|
1885
|
+
targetRoot: resolvedTargetRoot,
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1889
|
+
...transaction,
|
|
1890
|
+
phase: "locked",
|
|
388
1891
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
if (initialized.exitCode !== 0) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`skill_update_project_migration_blocked:${project.workspaceRoot}:${initialized.status}`,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const currentConfigBytes = await readFile(project.configPath);
|
|
409
|
-
projectMigrations.push({
|
|
410
|
-
workspaceRoot: project.workspaceRoot,
|
|
411
|
-
configPath: project.configPath,
|
|
412
|
-
fromConfigSha256: project.configSha256,
|
|
413
|
-
fromConfigBase64: project.configBytes.toString("base64"),
|
|
414
|
-
toConfigSha256: sha256(currentConfigBytes),
|
|
415
|
-
migrationReceipt: initialized.migrationReceipt ?? null,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
const receipt = {
|
|
419
|
-
schemaVersion: "OwlCodaRunKitSkillUpgradeReceiptV1",
|
|
1892
|
+
let upgradeReceipt = null;
|
|
1893
|
+
let upgradeReceiptBytes = null;
|
|
1894
|
+
let projectBindings = [];
|
|
1895
|
+
if (archivePath !== null) {
|
|
1896
|
+
projectBindings = fleetLock.snapshot.projects.map(project => ({
|
|
1897
|
+
...project,
|
|
1898
|
+
postflightConfigSha256: project.configSha256,
|
|
1899
|
+
postflightAdoptionSha256: project.adoptionSha256,
|
|
1900
|
+
postflightGovernedFiles: project.governedFiles,
|
|
1901
|
+
postflightGovernedFilesSha256: project.governedFilesSha256,
|
|
1902
|
+
}));
|
|
1903
|
+
upgradeReceiptBytes = boundedJsonBytes({
|
|
1904
|
+
schemaVersion: "OwlCodaRunKitSkillUpgradeReceiptV2",
|
|
420
1905
|
operation: "upgrade",
|
|
421
|
-
targetRoot:
|
|
422
|
-
archiveRoot:
|
|
1906
|
+
targetRoot: resolvedTargetRoot,
|
|
1907
|
+
archiveRoot: resolvedArchiveRoot,
|
|
423
1908
|
fromManifestSha256: prior.manifestSha256,
|
|
424
1909
|
toManifestSha256: desired.manifestSha256,
|
|
425
1910
|
priorArchivePath: archivePath,
|
|
426
|
-
|
|
1911
|
+
priorManaged: prior.status !== "unmanaged",
|
|
1912
|
+
fleetBinding: fleetBindingFromSnapshot(fleetLock.snapshot),
|
|
1913
|
+
projects: projectBindings,
|
|
1914
|
+
projectMutationPerformed: false,
|
|
427
1915
|
authorizationGranted: false,
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
path.join(
|
|
1916
|
+
}, MAX_V2_SKILL_RECEIPT_BYTES, "skill_update_receipt_too_large");
|
|
1917
|
+
upgradeReceipt = await nextAppendOnlyReceiptPath(
|
|
1918
|
+
path.join(resolvedArchiveRoot, "receipts"),
|
|
431
1919
|
`skill-upgrade-${prior.manifestSha256.slice(7)}-to-${desired.manifestSha256.slice(7)}`,
|
|
432
|
-
receipt,
|
|
433
1920
|
);
|
|
1921
|
+
}
|
|
1922
|
+
if (archivePath !== null) {
|
|
1923
|
+
await rename(resolvedTargetRoot, archivePath);
|
|
1924
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1925
|
+
...transaction,
|
|
1926
|
+
phase: "prior_archived",
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
await rename(stagingRoot, resolvedTargetRoot);
|
|
1930
|
+
stagingPresent = false;
|
|
1931
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1932
|
+
...transaction,
|
|
1933
|
+
phase: "candidate_installed",
|
|
1934
|
+
});
|
|
1935
|
+
let postflight = null;
|
|
1936
|
+
if (fleetLock !== null) {
|
|
1937
|
+
postflight = await inspectFleetSnapshot(source);
|
|
1938
|
+
if (
|
|
1939
|
+
!sameFleetIdentity(fleetLock.snapshot, postflight)
|
|
1940
|
+
|| !sameProjectBindings(
|
|
1941
|
+
fleetLock.snapshot.projects,
|
|
1942
|
+
postflight.projects,
|
|
1943
|
+
)
|
|
1944
|
+
) {
|
|
1945
|
+
throw new Error("skill_update_fleet_postflight_changed");
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (archivePath !== null) {
|
|
1950
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1951
|
+
...transaction,
|
|
1952
|
+
phase: "receipt_pending",
|
|
1953
|
+
receiptPath: upgradeReceipt,
|
|
1954
|
+
receiptSha256: `sha256:${sha256(upgradeReceiptBytes)}`,
|
|
1955
|
+
});
|
|
1956
|
+
await writeFile(upgradeReceipt, upgradeReceiptBytes, { flag: "wx" });
|
|
1957
|
+
}
|
|
1958
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
1959
|
+
...transaction,
|
|
1960
|
+
phase: "committed",
|
|
1961
|
+
});
|
|
1962
|
+
if (fleetLock !== null) {
|
|
1963
|
+
await fleetLock.release();
|
|
1964
|
+
fleetLock = null;
|
|
1965
|
+
}
|
|
1966
|
+
await rm(journalPath, { force: true });
|
|
1967
|
+
transaction = null;
|
|
1968
|
+
if (archivePath === null) {
|
|
434
1969
|
return {
|
|
435
|
-
status: "
|
|
1970
|
+
status: "installed",
|
|
436
1971
|
manifestSha256: desired.manifestSha256,
|
|
437
|
-
previousInstallation:
|
|
438
|
-
|
|
439
|
-
archivePath,
|
|
440
|
-
},
|
|
441
|
-
upgradeReceipt,
|
|
442
|
-
projectMigrations,
|
|
1972
|
+
previousInstallation: null,
|
|
1973
|
+
projectMutationPerformed: false,
|
|
443
1974
|
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
status: "updated",
|
|
1978
|
+
manifestSha256: desired.manifestSha256,
|
|
1979
|
+
previousInstallation: {
|
|
1980
|
+
manifestSha256: prior.manifestSha256,
|
|
1981
|
+
archivePath,
|
|
1982
|
+
},
|
|
1983
|
+
upgradeReceipt,
|
|
1984
|
+
projectBindings,
|
|
1985
|
+
projectMutationPerformed: false,
|
|
1986
|
+
};
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
if (transaction !== null || await exists(journalPath)) {
|
|
1989
|
+
try {
|
|
1990
|
+
await recoverInterruptedCodexSkillTransaction({
|
|
1991
|
+
targetRoot: resolvedTargetRoot,
|
|
1992
|
+
archiveRoot: resolvedArchiveRoot,
|
|
1993
|
+
});
|
|
1994
|
+
transaction = null;
|
|
1995
|
+
} catch (recoveryError) {
|
|
1996
|
+
throw new Error(
|
|
1997
|
+
`${error instanceof Error ? error.message : String(error)}; `
|
|
1998
|
+
+ `skill_update_transaction_recovery_failed:${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`,
|
|
449
1999
|
);
|
|
450
|
-
await rename(targetRoot, failedArchivePath);
|
|
451
2000
|
}
|
|
452
|
-
await rename(archivePath, targetRoot);
|
|
453
|
-
await restoreProjectConfigs(projects);
|
|
454
|
-
throw error;
|
|
455
2001
|
}
|
|
2002
|
+
throw error;
|
|
456
2003
|
} finally {
|
|
457
|
-
if (
|
|
2004
|
+
if (fleetLock !== null) await fleetLock.release();
|
|
2005
|
+
if (stagingPresent && stagingRoot !== null) {
|
|
2006
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
2007
|
+
}
|
|
2008
|
+
if (releaseRegistryLock !== null) await releaseRegistryLock();
|
|
2009
|
+
await releaseSkillLock();
|
|
458
2010
|
}
|
|
459
2011
|
}
|
|
460
2012
|
|
|
461
|
-
|
|
2013
|
+
async function readUpgradeReceiptForRestore({
|
|
462
2014
|
targetRoot,
|
|
463
2015
|
archiveRoot,
|
|
464
2016
|
upgradeReceiptPath,
|
|
465
|
-
}
|
|
2017
|
+
}) {
|
|
466
2018
|
if (
|
|
467
2019
|
typeof targetRoot !== "string"
|
|
468
2020
|
|| typeof archiveRoot !== "string"
|
|
@@ -484,19 +2036,37 @@ export async function restoreCodexSkill({
|
|
|
484
2036
|
) {
|
|
485
2037
|
throw new Error("skill_rollback_receipt_outside_archive");
|
|
486
2038
|
}
|
|
2039
|
+
const receiptStat = await lstat(resolvedReceipt);
|
|
2040
|
+
if (
|
|
2041
|
+
receiptStat.isSymbolicLink()
|
|
2042
|
+
|| !receiptStat.isFile()
|
|
2043
|
+
|| receiptStat.size > MAX_SKILL_RECEIPT_BYTES
|
|
2044
|
+
) {
|
|
2045
|
+
throw new Error("skill_rollback_receipt_too_large");
|
|
2046
|
+
}
|
|
487
2047
|
const receiptBytes = await readFile(resolvedReceipt);
|
|
488
2048
|
const receipt = JSON.parse(receiptBytes.toString("utf8"));
|
|
2049
|
+
const legacyReceipt = receipt.schemaVersion === "OwlCodaRunKitSkillUpgradeReceiptV1";
|
|
2050
|
+
const boundReceipt = receipt.schemaVersion === "OwlCodaRunKitSkillUpgradeReceiptV2";
|
|
489
2051
|
if (
|
|
490
|
-
|
|
2052
|
+
(!legacyReceipt && !boundReceipt)
|
|
491
2053
|
|| receipt.operation !== "upgrade"
|
|
492
2054
|
|| receipt.targetRoot !== resolvedTarget
|
|
493
2055
|
|| receipt.archiveRoot !== resolvedArchive
|
|
494
2056
|
|| receipt.authorizationGranted !== false
|
|
495
2057
|
|| !Array.isArray(receipt.projects)
|
|
496
|
-
|| receipt.projects.length
|
|
2058
|
+
|| receipt.projects.length > MAX_FLEET_PROJECTS
|
|
2059
|
+
|| (legacyReceipt && receipt.projects.length === 0)
|
|
2060
|
+
|| (boundReceipt && receipt.projectMutationPerformed !== false)
|
|
497
2061
|
) {
|
|
498
2062
|
throw new Error("skill_rollback_receipt_invalid");
|
|
499
2063
|
}
|
|
2064
|
+
if (boundReceipt) {
|
|
2065
|
+
if (receiptBytes.length > MAX_V2_SKILL_RECEIPT_BYTES) {
|
|
2066
|
+
throw new Error("skill_rollback_receipt_too_large");
|
|
2067
|
+
}
|
|
2068
|
+
sourceFromFleetBinding(receipt.fleetBinding);
|
|
2069
|
+
}
|
|
500
2070
|
const resolvedPriorArchive = path.resolve(receipt.priorArchivePath);
|
|
501
2071
|
const priorArchiveRelative = path.relative(
|
|
502
2072
|
resolvedArchive,
|
|
@@ -512,71 +2082,328 @@ export async function restoreCodexSkill({
|
|
|
512
2082
|
) {
|
|
513
2083
|
throw new Error("skill_rollback_prior_archive_outside_root");
|
|
514
2084
|
}
|
|
2085
|
+
if (legacyReceipt) {
|
|
2086
|
+
for (const project of receipt.projects) {
|
|
2087
|
+
project.workspaceRoot = await realpath(path.resolve(project.workspaceRoot));
|
|
2088
|
+
project.configPath = await realpath(path.resolve(project.configPath));
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return {
|
|
2092
|
+
receipt,
|
|
2093
|
+
receiptBytes,
|
|
2094
|
+
legacyReceipt,
|
|
2095
|
+
boundReceipt,
|
|
2096
|
+
resolvedTarget,
|
|
2097
|
+
resolvedArchive,
|
|
2098
|
+
resolvedReceipt,
|
|
2099
|
+
resolvedPriorArchive,
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function restoreFleetSource(receipt, options) {
|
|
2104
|
+
if (receipt.schemaVersion === "OwlCodaRunKitSkillUpgradeReceiptV2") {
|
|
2105
|
+
return sourceFromFleetBinding(receipt.fleetBinding);
|
|
2106
|
+
}
|
|
2107
|
+
const source = {
|
|
2108
|
+
workspaceRoots: options.workspaceRoots ?? null,
|
|
2109
|
+
fleetManifestPath: options.fleetManifestPath ?? null,
|
|
2110
|
+
fleetRoots: options.fleetRoots ?? null,
|
|
2111
|
+
fleetRegistryPath: options.fleetRegistryPath ?? null,
|
|
2112
|
+
};
|
|
2113
|
+
if (
|
|
2114
|
+
source.workspaceRoots === null
|
|
2115
|
+
&& source.fleetManifestPath === null
|
|
2116
|
+
&& source.fleetRoots === null
|
|
2117
|
+
&& source.fleetRegistryPath === null
|
|
2118
|
+
) {
|
|
2119
|
+
throw new Error("skill_rollback_current_fleet_source_required");
|
|
2120
|
+
}
|
|
2121
|
+
return source;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
async function discoverLegacyFleetForRestore(source) {
|
|
2125
|
+
const discovery = await discoverFleet(source);
|
|
2126
|
+
if (!discovery.complete) {
|
|
2127
|
+
const detail = discovery.issues
|
|
2128
|
+
.map(issue => `${issue.code}:${issue.path}`)
|
|
2129
|
+
.join(",");
|
|
2130
|
+
throw new Error(`skill_update_fleet_discovery_incomplete:${detail}`);
|
|
2131
|
+
}
|
|
2132
|
+
return discovery;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function sameWorkspaceRoots(left, right) {
|
|
2136
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
async function restoreCodexSkillUnlocked({
|
|
2140
|
+
receiptRecord,
|
|
2141
|
+
fleetSource,
|
|
2142
|
+
transactionId,
|
|
2143
|
+
} = {}) {
|
|
2144
|
+
const {
|
|
2145
|
+
receipt,
|
|
2146
|
+
receiptBytes,
|
|
2147
|
+
legacyReceipt,
|
|
2148
|
+
boundReceipt,
|
|
2149
|
+
resolvedTarget,
|
|
2150
|
+
resolvedArchive,
|
|
2151
|
+
resolvedReceipt,
|
|
2152
|
+
resolvedPriorArchive,
|
|
2153
|
+
} = receiptRecord;
|
|
515
2154
|
const current = await inspectInstalledSkill({ targetRoot: resolvedTarget });
|
|
516
2155
|
if (!current.valid || current.manifestSha256 !== receipt.toManifestSha256) {
|
|
517
2156
|
throw new Error("skill_rollback_current_identity_mismatch");
|
|
518
2157
|
}
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
2158
|
+
const priorManaged = receipt.priorManaged !== false;
|
|
2159
|
+
const prior = await inspectExpectedInstallation(
|
|
2160
|
+
resolvedPriorArchive,
|
|
2161
|
+
receipt.fromManifestSha256,
|
|
2162
|
+
{ allowUnmanaged: !priorManaged },
|
|
2163
|
+
).catch(() => null);
|
|
2164
|
+
if (prior === null) {
|
|
523
2165
|
throw new Error("skill_rollback_prior_identity_mismatch");
|
|
524
2166
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
for (const project of currentProjects) {
|
|
531
|
-
const expected = receiptProjectByRoot.get(project.workspaceRoot);
|
|
2167
|
+
let currentSnapshot = null;
|
|
2168
|
+
let currentProjects;
|
|
2169
|
+
if (boundReceipt) {
|
|
2170
|
+
currentSnapshot = await inspectFleetSnapshot(fleetSource);
|
|
532
2171
|
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|| expected.toConfigSha256 !== project.configSha256
|
|
536
|
-
|| typeof expected.fromConfigBase64 !== "string"
|
|
537
|
-
|| sha256(Buffer.from(expected.fromConfigBase64, "base64"))
|
|
538
|
-
!== expected.fromConfigSha256
|
|
2172
|
+
receipt.fleetBinding !== undefined
|
|
2173
|
+
&& !sameFleetBinding(receipt.fleetBinding, currentSnapshot)
|
|
539
2174
|
) {
|
|
540
|
-
throw new Error(
|
|
541
|
-
|
|
542
|
-
|
|
2175
|
+
throw new Error("skill_rollback_fleet_identity_changed");
|
|
2176
|
+
}
|
|
2177
|
+
if (!sameWorkspaceRoots(
|
|
2178
|
+
currentSnapshot.discovery.workspaceRoots,
|
|
2179
|
+
receipt.projects.map(project => project.workspaceRoot),
|
|
2180
|
+
)) {
|
|
2181
|
+
throw new Error("skill_rollback_fleet_identity_changed");
|
|
543
2182
|
}
|
|
2183
|
+
currentProjects = currentSnapshot.projects;
|
|
2184
|
+
} else {
|
|
2185
|
+
const discovery = await discoverLegacyFleetForRestore(fleetSource);
|
|
2186
|
+
if (!sameWorkspaceRoots(
|
|
2187
|
+
discovery.workspaceRoots,
|
|
2188
|
+
receipt.projects.map(project => project.workspaceRoot),
|
|
2189
|
+
)) {
|
|
2190
|
+
throw new Error("skill_rollback_fleet_identity_changed");
|
|
2191
|
+
}
|
|
2192
|
+
currentProjects = await inspectLegacyProjectsForRollback(
|
|
2193
|
+
discovery.workspaceRoots,
|
|
2194
|
+
);
|
|
544
2195
|
}
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
2196
|
+
const journalPath = transactionJournalPath(resolvedTarget);
|
|
2197
|
+
let transaction = await writeTransactionJournal(journalPath, {
|
|
2198
|
+
schemaVersion: TRANSACTION_SCHEMA,
|
|
2199
|
+
transactionId,
|
|
2200
|
+
operation: "restore_locking",
|
|
2201
|
+
phase: "locking",
|
|
2202
|
+
targetRoot: resolvedTarget,
|
|
2203
|
+
archiveRoot: resolvedArchive,
|
|
2204
|
+
projectRoots: currentProjects.map(project => project.workspaceRoot),
|
|
2205
|
+
authorizationGranted: false,
|
|
2206
|
+
}, { exclusive: true });
|
|
2207
|
+
let releaseProjectLocks = null;
|
|
550
2208
|
try {
|
|
551
|
-
await
|
|
2209
|
+
releaseProjectLocks = await acquireProjectControlLocks(
|
|
2210
|
+
currentProjects,
|
|
2211
|
+
{ transactionId, targetRoot: resolvedTarget },
|
|
2212
|
+
);
|
|
2213
|
+
let lockedSnapshot = null;
|
|
2214
|
+
let lockedProjects;
|
|
2215
|
+
if (boundReceipt) {
|
|
2216
|
+
lockedSnapshot = await inspectFleetSnapshot(fleetSource);
|
|
2217
|
+
if (
|
|
2218
|
+
(receipt.fleetBinding !== undefined
|
|
2219
|
+
&& !sameFleetBinding(receipt.fleetBinding, lockedSnapshot))
|
|
2220
|
+
|| !sameFleetIdentity(currentSnapshot, lockedSnapshot)
|
|
2221
|
+
) {
|
|
2222
|
+
throw new Error("skill_rollback_fleet_identity_changed");
|
|
2223
|
+
}
|
|
2224
|
+
lockedProjects = lockedSnapshot.projects;
|
|
2225
|
+
} else {
|
|
2226
|
+
const lockedDiscovery = await discoverLegacyFleetForRestore(fleetSource);
|
|
2227
|
+
if (!sameWorkspaceRoots(
|
|
2228
|
+
lockedDiscovery.workspaceRoots,
|
|
2229
|
+
currentProjects.map(project => project.workspaceRoot),
|
|
2230
|
+
)) {
|
|
2231
|
+
throw new Error("skill_rollback_fleet_identity_changed");
|
|
2232
|
+
}
|
|
2233
|
+
lockedProjects = await inspectLegacyProjectsForRollback(
|
|
2234
|
+
lockedDiscovery.workspaceRoots,
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
const lockedIdentityValid = boundReceipt
|
|
2238
|
+
? sameProjectBindings(currentProjects, lockedProjects)
|
|
2239
|
+
: currentProjects.every((project, index) => (
|
|
2240
|
+
project.workspaceRoot === lockedProjects[index]?.workspaceRoot
|
|
2241
|
+
&& project.configSha256 === lockedProjects[index]?.configSha256
|
|
2242
|
+
));
|
|
2243
|
+
if (!lockedIdentityValid) {
|
|
2244
|
+
throw new Error("skill_rollback_project_identity_changed");
|
|
2245
|
+
}
|
|
2246
|
+
currentProjects = lockedProjects;
|
|
2247
|
+
const receiptProjectByRoot = new Map(
|
|
2248
|
+
receipt.projects.map((project) => [project.workspaceRoot, project]),
|
|
2249
|
+
);
|
|
2250
|
+
const legacyConfigRestoreBytes = [];
|
|
552
2251
|
for (const project of currentProjects) {
|
|
553
2252
|
const expected = receiptProjectByRoot.get(project.workspaceRoot);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
Buffer.from(expected.fromConfigBase64, "base64")
|
|
557
|
-
|
|
2253
|
+
const legacyAfterBytes = legacyReceipt
|
|
2254
|
+
&& typeof expected?.fromConfigBase64 === "string"
|
|
2255
|
+
? Buffer.from(expected.fromConfigBase64, "base64")
|
|
2256
|
+
: null;
|
|
2257
|
+
const legacyIdentityValid = legacyReceipt
|
|
2258
|
+
&& expected?.configPath === project.configPath
|
|
2259
|
+
&& expected.toConfigSha256 === project.configSha256
|
|
2260
|
+
&& legacyAfterBytes !== null
|
|
2261
|
+
&& sha256(legacyAfterBytes) === expected.fromConfigSha256;
|
|
2262
|
+
const boundIdentityValid = boundReceipt
|
|
2263
|
+
&& expected?.configPath === project.configPath
|
|
2264
|
+
&& expected.configSha256 === project.configSha256
|
|
2265
|
+
&& expected.postflightConfigSha256 === project.configSha256
|
|
2266
|
+
&& expected.packageName === project.packageName
|
|
2267
|
+
&& expected.packageVersion === project.packageVersion
|
|
2268
|
+
&& expected.packageManager === project.packageManager
|
|
2269
|
+
&& expected.lockfilePath === project.lockfilePath
|
|
2270
|
+
&& expected.packageRoot === project.packageRoot
|
|
2271
|
+
&& expected.cliPath === project.cliPath
|
|
2272
|
+
&& expected.coreVersion === project.coreVersion
|
|
2273
|
+
&& expected.coreManifestSha256 === project.coreManifestSha256
|
|
2274
|
+
&& expected.adoptionPath === project.adoptionPath
|
|
2275
|
+
&& expected.adoptionSha256 === project.adoptionSha256
|
|
2276
|
+
&& expected.postflightAdoptionSha256 === project.adoptionSha256
|
|
2277
|
+
&& expected.governedFilesSha256 === project.governedFilesSha256
|
|
2278
|
+
&& expected.postflightGovernedFilesSha256
|
|
2279
|
+
=== project.governedFilesSha256
|
|
2280
|
+
&& JSON.stringify(expected.governedFiles)
|
|
2281
|
+
=== JSON.stringify(project.governedFiles)
|
|
2282
|
+
&& JSON.stringify(expected.postflightGovernedFiles)
|
|
2283
|
+
=== JSON.stringify(project.governedFiles)
|
|
2284
|
+
&& expected.authorizationGranted === false;
|
|
2285
|
+
if (!legacyIdentityValid && !boundIdentityValid) {
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`skill_rollback_project_identity_mismatch:${project.workspaceRoot}`,
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
if (legacyIdentityValid) {
|
|
2291
|
+
legacyConfigRestoreBytes.push({
|
|
2292
|
+
configPath: project.configPath,
|
|
2293
|
+
beforeBytes: project.configBytes,
|
|
2294
|
+
afterBytes: legacyAfterBytes,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
558
2297
|
}
|
|
559
|
-
const
|
|
2298
|
+
const legacyConfigRestores = legacyReceipt
|
|
2299
|
+
? await writeLegacyConfigRestoreEvidence({
|
|
2300
|
+
archiveRoot: resolvedArchive,
|
|
2301
|
+
transactionId,
|
|
2302
|
+
}, legacyConfigRestoreBytes)
|
|
2303
|
+
: [];
|
|
2304
|
+
const currentArchivePath = await nextAvailablePath(
|
|
2305
|
+
resolvedArchive,
|
|
2306
|
+
`owlcoda-runkit-${current.manifestSha256.slice(7)}`,
|
|
2307
|
+
);
|
|
2308
|
+
const rollbackReceiptBody = {
|
|
2309
|
+
schemaVersion: legacyReceipt
|
|
2310
|
+
? "OwlCodaRunKitSkillRollbackReceiptV1"
|
|
2311
|
+
: "OwlCodaRunKitSkillRollbackReceiptV2",
|
|
2312
|
+
operation: "rollback",
|
|
2313
|
+
sourceUpgradeReceiptPath: resolvedReceipt,
|
|
2314
|
+
sourceUpgradeReceiptSha256: sha256(receiptBytes),
|
|
2315
|
+
targetRoot: resolvedTarget,
|
|
2316
|
+
archiveRoot: resolvedArchive,
|
|
2317
|
+
fromManifestSha256: receipt.toManifestSha256,
|
|
2318
|
+
toManifestSha256: receipt.fromManifestSha256,
|
|
2319
|
+
restoredArchivePath: resolvedPriorArchive,
|
|
2320
|
+
replacedInstallationArchivePath: currentArchivePath,
|
|
2321
|
+
priorManaged,
|
|
2322
|
+
...(boundReceipt && receipt.fleetBinding !== undefined
|
|
2323
|
+
? { fleetBinding: receipt.fleetBinding }
|
|
2324
|
+
: {}),
|
|
2325
|
+
projects: receipt.projects.map((project) => legacyReceipt
|
|
2326
|
+
? {
|
|
2327
|
+
workspaceRoot: project.workspaceRoot,
|
|
2328
|
+
restoredConfigSha256: project.fromConfigSha256,
|
|
2329
|
+
}
|
|
2330
|
+
: {
|
|
2331
|
+
workspaceRoot: project.workspaceRoot,
|
|
2332
|
+
configSha256: project.configSha256,
|
|
2333
|
+
adoptionSha256: project.adoptionSha256,
|
|
2334
|
+
governedFilesSha256: project.governedFilesSha256,
|
|
2335
|
+
}),
|
|
2336
|
+
...(boundReceipt ? { projectMutationPerformed: false } : {}),
|
|
2337
|
+
authorizationGranted: false,
|
|
2338
|
+
};
|
|
2339
|
+
const rollbackReceiptBytes = boundedJsonBytes(
|
|
2340
|
+
rollbackReceiptBody,
|
|
2341
|
+
MAX_V2_SKILL_RECEIPT_BYTES,
|
|
2342
|
+
"skill_rollback_receipt_too_large",
|
|
2343
|
+
);
|
|
2344
|
+
const rollbackReceipt = await nextAppendOnlyReceiptPath(
|
|
560
2345
|
path.join(resolvedArchive, "receipts"),
|
|
561
2346
|
`skill-rollback-${receipt.toManifestSha256.slice(7)}-to-${receipt.fromManifestSha256.slice(7)}`,
|
|
562
|
-
{
|
|
563
|
-
schemaVersion: "OwlCodaRunKitSkillRollbackReceiptV1",
|
|
564
|
-
operation: "rollback",
|
|
565
|
-
sourceUpgradeReceiptPath: resolvedReceipt,
|
|
566
|
-
sourceUpgradeReceiptSha256: sha256(receiptBytes),
|
|
567
|
-
targetRoot: resolvedTarget,
|
|
568
|
-
archiveRoot: resolvedArchive,
|
|
569
|
-
fromManifestSha256: receipt.toManifestSha256,
|
|
570
|
-
toManifestSha256: receipt.fromManifestSha256,
|
|
571
|
-
restoredArchivePath: resolvedPriorArchive,
|
|
572
|
-
replacedInstallationArchivePath: currentArchivePath,
|
|
573
|
-
projects: receipt.projects.map((project) => ({
|
|
574
|
-
workspaceRoot: project.workspaceRoot,
|
|
575
|
-
restoredConfigSha256: project.fromConfigSha256,
|
|
576
|
-
})),
|
|
577
|
-
authorizationGranted: false,
|
|
578
|
-
},
|
|
579
2347
|
);
|
|
2348
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2349
|
+
schemaVersion: TRANSACTION_SCHEMA,
|
|
2350
|
+
transactionId,
|
|
2351
|
+
operation: "restore",
|
|
2352
|
+
phase: "prepared",
|
|
2353
|
+
targetRoot: resolvedTarget,
|
|
2354
|
+
archiveRoot: resolvedArchive,
|
|
2355
|
+
fromManifestSha256: receipt.toManifestSha256,
|
|
2356
|
+
toManifestSha256: receipt.fromManifestSha256,
|
|
2357
|
+
priorArchivePath: resolvedPriorArchive,
|
|
2358
|
+
replacedInstallationArchivePath: currentArchivePath,
|
|
2359
|
+
priorManaged,
|
|
2360
|
+
sourceUpgradeReceiptPath: resolvedReceipt,
|
|
2361
|
+
sourceUpgradeReceiptSha256: `sha256:${sha256(receiptBytes)}`,
|
|
2362
|
+
projectRoots: currentProjects.map(project => project.workspaceRoot),
|
|
2363
|
+
legacyConfigRestores,
|
|
2364
|
+
receiptPath: rollbackReceipt,
|
|
2365
|
+
receiptSha256: `sha256:${sha256(rollbackReceiptBytes)}`,
|
|
2366
|
+
authorizationGranted: false,
|
|
2367
|
+
});
|
|
2368
|
+
await rename(resolvedTarget, currentArchivePath);
|
|
2369
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2370
|
+
...transaction,
|
|
2371
|
+
phase: "current_archived",
|
|
2372
|
+
});
|
|
2373
|
+
await rename(resolvedPriorArchive, resolvedTarget);
|
|
2374
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2375
|
+
...transaction,
|
|
2376
|
+
phase: "prior_restored",
|
|
2377
|
+
});
|
|
2378
|
+
if (legacyReceipt) {
|
|
2379
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2380
|
+
...transaction,
|
|
2381
|
+
phase: "configs_restoring",
|
|
2382
|
+
});
|
|
2383
|
+
for (const entry of legacyConfigRestores) {
|
|
2384
|
+
await atomicWriteBytes(
|
|
2385
|
+
entry.configPath,
|
|
2386
|
+
await transactionConfigBytes(entry, "after", transaction),
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2391
|
+
...transaction,
|
|
2392
|
+
phase: "receipt_pending",
|
|
2393
|
+
});
|
|
2394
|
+
await writeFile(rollbackReceipt, rollbackReceiptBytes, { flag: "wx" });
|
|
2395
|
+
transaction = await writeTransactionJournal(journalPath, {
|
|
2396
|
+
...transaction,
|
|
2397
|
+
phase: "committed",
|
|
2398
|
+
});
|
|
2399
|
+
await releaseProjectLocks();
|
|
2400
|
+
releaseProjectLocks = null;
|
|
2401
|
+
await rm(transactionEvidenceRoot(transaction), {
|
|
2402
|
+
recursive: true,
|
|
2403
|
+
force: true,
|
|
2404
|
+
});
|
|
2405
|
+
await rm(journalPath, { force: true });
|
|
2406
|
+
transaction = null;
|
|
580
2407
|
return {
|
|
581
2408
|
status: "restored",
|
|
582
2409
|
manifestSha256: receipt.fromManifestSha256,
|
|
@@ -585,15 +2412,85 @@ export async function restoreCodexSkill({
|
|
|
585
2412
|
manifestSha256: receipt.toManifestSha256,
|
|
586
2413
|
archivePath: currentArchivePath,
|
|
587
2414
|
},
|
|
2415
|
+
projectMutationPerformed: legacyReceipt,
|
|
588
2416
|
authorizationGranted: false,
|
|
589
2417
|
};
|
|
590
2418
|
} catch (error) {
|
|
591
|
-
if (await exists(
|
|
592
|
-
|
|
2419
|
+
if (transaction !== null || await exists(journalPath)) {
|
|
2420
|
+
try {
|
|
2421
|
+
await recoverInterruptedCodexSkillTransaction({
|
|
2422
|
+
targetRoot: resolvedTarget,
|
|
2423
|
+
archiveRoot: resolvedArchive,
|
|
2424
|
+
});
|
|
2425
|
+
transaction = null;
|
|
2426
|
+
} catch (recoveryError) {
|
|
2427
|
+
throw new Error(
|
|
2428
|
+
`${error instanceof Error ? error.message : String(error)}; `
|
|
2429
|
+
+ `skill_update_transaction_recovery_failed:${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`,
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
593
2432
|
}
|
|
594
|
-
await rename(currentArchivePath, resolvedTarget);
|
|
595
|
-
await restoreProjectConfigs(currentProjects);
|
|
596
2433
|
throw error;
|
|
2434
|
+
} finally {
|
|
2435
|
+
if (releaseProjectLocks !== null) await releaseProjectLocks();
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
export async function restoreCodexSkill(options = {}) {
|
|
2440
|
+
const { targetRoot, archiveRoot, upgradeReceiptPath } = options;
|
|
2441
|
+
if (typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
|
|
2442
|
+
throw new Error("targetRoot and archiveRoot are required");
|
|
2443
|
+
}
|
|
2444
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
2445
|
+
await mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
2446
|
+
const transactionId = randomUUID();
|
|
2447
|
+
const releaseSkillLock = await acquireOwnedDirectoryLock(
|
|
2448
|
+
`${resolvedTarget}.lock`,
|
|
2449
|
+
{
|
|
2450
|
+
activeCode: "skill_update_shared_skill_transaction_active",
|
|
2451
|
+
transactionId,
|
|
2452
|
+
targetRoot: resolvedTarget,
|
|
2453
|
+
recoverStale: true,
|
|
2454
|
+
},
|
|
2455
|
+
);
|
|
2456
|
+
let releaseRegistryLock = null;
|
|
2457
|
+
try {
|
|
2458
|
+
const receiptRecord = await readUpgradeReceiptForRestore({
|
|
2459
|
+
targetRoot,
|
|
2460
|
+
archiveRoot,
|
|
2461
|
+
upgradeReceiptPath,
|
|
2462
|
+
});
|
|
2463
|
+
const fleetSource = restoreFleetSource(receiptRecord.receipt, options);
|
|
2464
|
+
releaseRegistryLock = await acquireFleetRegistryTransactionLock(fleetSource, {
|
|
2465
|
+
transactionId,
|
|
2466
|
+
targetRoot: resolvedTarget,
|
|
2467
|
+
});
|
|
2468
|
+
const recovered = await recoverInterruptedCodexSkillTransaction({
|
|
2469
|
+
targetRoot: resolvedTarget,
|
|
2470
|
+
archiveRoot,
|
|
2471
|
+
});
|
|
2472
|
+
if (
|
|
2473
|
+
recovered.status === "recovered_committed_transaction"
|
|
2474
|
+
&& recovered.operation === "restore"
|
|
2475
|
+
) {
|
|
2476
|
+
return {
|
|
2477
|
+
status: "restored",
|
|
2478
|
+
manifestSha256: recovered.manifestSha256,
|
|
2479
|
+
rollbackReceipt: recovered.rollbackReceipt,
|
|
2480
|
+
replacedInstallation: recovered.replacedInstallation,
|
|
2481
|
+
projectMutationPerformed: recovered.projectMutationPerformed,
|
|
2482
|
+
recoveredTransaction: true,
|
|
2483
|
+
authorizationGranted: false,
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
return await restoreCodexSkillUnlocked({
|
|
2487
|
+
receiptRecord,
|
|
2488
|
+
fleetSource,
|
|
2489
|
+
transactionId,
|
|
2490
|
+
});
|
|
2491
|
+
} finally {
|
|
2492
|
+
if (releaseRegistryLock !== null) await releaseRegistryLock();
|
|
2493
|
+
await releaseSkillLock();
|
|
597
2494
|
}
|
|
598
2495
|
}
|
|
599
2496
|
|
|
@@ -799,6 +2696,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
799
2696
|
targetRoot: options.target,
|
|
800
2697
|
archiveRoot: options.archive,
|
|
801
2698
|
upgradeReceiptPath: options.receipt,
|
|
2699
|
+
workspaceRoots: options.workspace ?? null,
|
|
2700
|
+
fleetManifestPath: options["fleet-manifest"] ?? null,
|
|
2701
|
+
fleetRoots: options["fleet-root"] ?? null,
|
|
2702
|
+
fleetRegistryPath: options.registry ?? null,
|
|
802
2703
|
});
|
|
803
2704
|
return { exitCode: 0, ...output };
|
|
804
2705
|
}
|