@getformation/cloud-cli 1.0.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.
@@ -0,0 +1,520 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import {
4
+ lstat,
5
+ mkdir,
6
+ open,
7
+ readFile,
8
+ readdir,
9
+ rename,
10
+ rm,
11
+ stat,
12
+ unlink,
13
+ } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { isDeepStrictEqual } from "node:util";
17
+ import { fail } from "./errors.mjs";
18
+ import {
19
+ downloadSkillBundle,
20
+ isArtifactRevisionId,
21
+ isCanonicalSkillPath,
22
+ manifestFromOperation,
23
+ revalidateCurrentManifest,
24
+ sha256Bytes,
25
+ SKILL_LIMITS,
26
+ } from "./manifest.mjs";
27
+
28
+ export const RECEIPT_NAME = ".formation-cloud-install.json";
29
+ const RECEIPT_SCHEMA_VERSION = 1;
30
+ const PACKAGE_VERSION = "1.0.0";
31
+ const MAX_LOCAL_ENTRIES = 512;
32
+ const MAX_RECEIPT_BYTES = 65_536;
33
+ const SHA256 = /^[0-9a-f]{64}$/u;
34
+
35
+ function conflict(message, details) {
36
+ fail("install_conflict", message, "Choose an absent target path, or restore the exact receipt-owned install before replacement.", details);
37
+ }
38
+
39
+ function identity(info) {
40
+ return { dev: info.dev, ino: info.ino, uid: info.uid, gid: info.gid, mode: info.mode, type: info.isDirectory() ? "directory" : info.isFile() ? "file" : "other" };
41
+ }
42
+
43
+ function sameIdentity(left, right) {
44
+ return left.dev === right.dev && left.ino === right.ino && left.type === right.type;
45
+ }
46
+
47
+ function assertOwnedSafeMetadata(info, subject, {
48
+ uid = typeof process.getuid === "function" ? process.getuid() : info.uid,
49
+ gid = typeof process.getgid === "function" ? process.getgid() : info.gid,
50
+ } = {}) {
51
+ if (info.uid !== uid || info.gid !== gid) conflict(`${subject} has foreign ownership.`);
52
+ if ((info.mode & 0o022) !== 0) conflict(`${subject} is group- or world-writable.`);
53
+ }
54
+
55
+ function currentIdentity(info, subject) {
56
+ assertOwnedSafeMetadata(info, subject);
57
+ }
58
+
59
+ async function maybeLstat(filePath) {
60
+ try { return await lstat(filePath); }
61
+ catch (error) {
62
+ if (error?.code === "ENOENT") return null;
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ function assertExactTarget(target) {
68
+ if (typeof target !== "string" || !path.isAbsolute(target) || path.resolve(target) !== target) {
69
+ fail("invalid_target", "The Skill target must be one exact normalized absolute path.",
70
+ "Pass --target with an absolute path that contains no dot segments or trailing separator.");
71
+ }
72
+ const root = path.parse(target).root;
73
+ const home = path.resolve(os.homedir());
74
+ if (target === root || target === home) {
75
+ fail("unsafe_target", "The filesystem root and home directory cannot be Skill install targets.",
76
+ "Choose a dedicated absent Skill directory below an owner-controlled parent.");
77
+ }
78
+ return target;
79
+ }
80
+
81
+ async function rejectSymlinkAncestors(target) {
82
+ const root = path.parse(target).root;
83
+ const relative = path.relative(root, path.dirname(target));
84
+ let cursor = root;
85
+ for (const part of relative.split(path.sep).filter(Boolean)) {
86
+ cursor = path.join(cursor, part);
87
+ const info = await maybeLstat(cursor);
88
+ if (!info) fail("invalid_target_parent", "A target ancestor does not exist.", "Create one owner-controlled parent directory first.");
89
+ if (info.isSymbolicLink()) conflict("A target ancestor is a symbolic link.", { path: cursor });
90
+ if (!info.isDirectory()) conflict("A target ancestor is not a directory.", { path: cursor });
91
+ }
92
+ }
93
+
94
+ async function safeParent(target) {
95
+ await rejectSymlinkAncestors(target);
96
+ const parent = path.dirname(target);
97
+ const info = await lstat(parent);
98
+ if (!info.isDirectory() || info.isSymbolicLink()) conflict("The target parent is not a real directory.");
99
+ currentIdentity(info, "The target parent");
100
+ return { path: parent, identity: identity(info) };
101
+ }
102
+
103
+ function receiptKeys(value) {
104
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : [];
105
+ }
106
+
107
+ function validateReceipt(value, origin) {
108
+ const expected = ["schemaVersion", "installer", "installerVersion", "origin", "artifactRevisionId", "manifestDigest", "installedAt", "files"].sort();
109
+ const actual = receiptKeys(value);
110
+ if (actual.length !== expected.length || actual.some((entry, index) => entry !== expected[index])) conflict("The local install receipt is invalid.");
111
+ if (value.schemaVersion !== RECEIPT_SCHEMA_VERSION || value.installer !== "@getformation/cloud-cli" || value.installerVersion !== PACKAGE_VERSION) conflict("The local install receipt is not owned by this installer version.");
112
+ if (value.origin !== origin) conflict("The installed Skill belongs to a different Cloud origin.");
113
+ if (!isArtifactRevisionId(value.artifactRevisionId) || typeof value.manifestDigest !== "string" || !SHA256.test(value.manifestDigest)) conflict("The local install receipt identifiers are invalid.");
114
+ if (typeof value.installedAt !== "string" || !Number.isFinite(Date.parse(value.installedAt))) conflict("The local install receipt timestamp is invalid.");
115
+ if (!Array.isArray(value.files) || value.files.length < 1 || value.files.length > SKILL_LIMITS.fileCount) conflict("The local install receipt file list is invalid.");
116
+ const paths = [];
117
+ let totalBytes = 0;
118
+ for (const entry of value.files) {
119
+ const keys = receiptKeys(entry);
120
+ if (keys.join("\0") !== ["byteLength", "path", "sha256"].sort().join("\0") || !isCanonicalSkillPath(entry.path)
121
+ || !Number.isSafeInteger(entry.byteLength) || entry.byteLength < 1 || entry.byteLength > SKILL_LIMITS.fileBytes
122
+ || !SHA256.test(entry.sha256)) conflict("The local install receipt has an invalid file entry.");
123
+ paths.push(entry.path);
124
+ totalBytes += entry.byteLength;
125
+ }
126
+ if (!paths.includes("SKILL.md") || totalBytes > SKILL_LIMITS.totalBytes || new Set(paths).size !== paths.length
127
+ || paths.some((entry, index) => index > 0 && paths[index - 1] >= entry)) conflict("The local install receipt paths or aggregate bounds are invalid.");
128
+ return value;
129
+ }
130
+
131
+ async function readBoundedFile(filePath, maximumBytes) {
132
+ const before = await lstat(filePath);
133
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > maximumBytes) conflict("A receipt-owned path is not one bounded regular file.", { path: filePath });
134
+ currentIdentity(before, "A receipt-owned file");
135
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
136
+ try {
137
+ const opened = await handle.stat();
138
+ if (!opened.isFile() || opened.nlink !== 1 || !sameIdentity(identity(before), identity(opened))) conflict("A receipt-owned file changed while it was opened.", { path: filePath });
139
+ currentIdentity(opened, "A receipt-owned file");
140
+ const chunks = [];
141
+ let total = 0;
142
+ while (true) {
143
+ const bytes = Buffer.allocUnsafe(Math.min(65_536, maximumBytes + 1 - total));
144
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, null);
145
+ if (bytesRead === 0) break;
146
+ total += bytesRead;
147
+ if (total > maximumBytes) conflict("A receipt-owned file exceeded its read bound.", { path: filePath });
148
+ chunks.push(bytes.subarray(0, bytesRead));
149
+ }
150
+ const after = await handle.stat();
151
+ if (!sameIdentity(identity(opened), identity(after)) || after.nlink !== 1) conflict("A receipt-owned file changed while it was read.", { path: filePath });
152
+ return Buffer.concat(chunks, total);
153
+ } finally { await handle.close(); }
154
+ }
155
+
156
+ async function inspectExistingTarget(target, origin) {
157
+ const targetInfo = await maybeLstat(target);
158
+ if (!targetInfo) return { exists: false, identity: null };
159
+ if (targetInfo.isSymbolicLink() || !targetInfo.isDirectory()) conflict("The Skill target is not a real directory.");
160
+ currentIdentity(targetInfo, "The Skill target");
161
+ const queue = [{ absolute: target, relative: "", depth: 0 }];
162
+ const foundFiles = [];
163
+ const foundDirectories = [];
164
+ let entries = 0;
165
+ while (queue.length) {
166
+ const current = queue.shift();
167
+ const children = await readdir(current.absolute, { withFileTypes: false });
168
+ for (const name of children) {
169
+ entries += 1;
170
+ if (entries > MAX_LOCAL_ENTRIES) conflict("The Skill target contains too many paths.");
171
+ const absolute = path.join(current.absolute, name);
172
+ const relative = current.relative ? `${current.relative}/${name}` : name;
173
+ const info = await lstat(absolute);
174
+ if (info.isSymbolicLink()) conflict("The Skill target contains a symbolic link.", { path: relative });
175
+ currentIdentity(info, `Skill target path ${relative}`);
176
+ if (info.isDirectory()) {
177
+ if (current.depth >= 1) conflict("The Skill target contains a directory below the allowed one-level tree.", { path: relative });
178
+ foundDirectories.push(relative);
179
+ queue.push({ absolute, relative, depth: current.depth + 1 });
180
+ } else if (info.isFile()) {
181
+ if (info.nlink !== 1) conflict("The Skill target contains a hard-linked file.", { path: relative });
182
+ foundFiles.push(relative);
183
+ } else {
184
+ conflict("The Skill target contains a device or other non-file path.", { path: relative });
185
+ }
186
+ }
187
+ }
188
+ if (!foundFiles.includes(RECEIPT_NAME)) conflict("The existing target is not owned by a Formation Cloud install receipt.");
189
+ const receiptBytes = await readBoundedFile(path.join(target, RECEIPT_NAME), MAX_RECEIPT_BYTES);
190
+ let receipt;
191
+ try { receipt = validateReceipt(JSON.parse(receiptBytes.toString("utf8")), origin); }
192
+ catch (error) {
193
+ if (error?.code) throw error;
194
+ conflict("The local install receipt is not valid JSON.");
195
+ }
196
+ const expectedFiles = [RECEIPT_NAME, ...receipt.files.map(({ path: filePath }) => filePath)].sort();
197
+ const actualFiles = foundFiles.sort();
198
+ if (actualFiles.length !== expectedFiles.length || actualFiles.some((entry, index) => entry !== expectedFiles[index])) conflict("The Skill target contains missing, modified, or extra files.", { expectedFiles, actualFiles });
199
+ const expectedDirectories = receipt.files.some(({ path: filePath }) => filePath.startsWith("references/")) ? ["references"] : [];
200
+ foundDirectories.sort();
201
+ if (foundDirectories.length !== expectedDirectories.length || foundDirectories.some((entry, index) => entry !== expectedDirectories[index])) conflict("The Skill target contains an undeclared directory.");
202
+ for (const descriptor of receipt.files) {
203
+ const bytes = await readBoundedFile(path.join(target, ...descriptor.path.split("/")), descriptor.byteLength);
204
+ if (bytes.byteLength !== descriptor.byteLength || sha256Bytes(bytes) !== descriptor.sha256) conflict("A receipt-owned Skill file has changed.", { path: descriptor.path });
205
+ }
206
+ return { exists: true, identity: identity(targetInfo), receipt };
207
+ }
208
+
209
+ async function assertIdentity(filePath, expected, subject) {
210
+ const info = await maybeLstat(filePath);
211
+ if (info) currentIdentity(info, subject);
212
+ if (!info || !sameIdentity(identity(info), expected)) conflict(`${subject} changed during installation.`);
213
+ }
214
+
215
+ async function fsyncDirectory(directory) {
216
+ const handle = await open(directory, "r");
217
+ try { await handle.sync(); } finally { await handle.close(); }
218
+ }
219
+
220
+ async function writeExclusive(filePath, bytes, mode = 0o600) {
221
+ const handle = await open(filePath, "wx", mode);
222
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
223
+ }
224
+
225
+ function newReceipt(bundle, origin) {
226
+ return {
227
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
228
+ installer: "@getformation/cloud-cli",
229
+ installerVersion: PACKAGE_VERSION,
230
+ origin,
231
+ artifactRevisionId: bundle.manifest.artifactRevisionId,
232
+ manifestDigest: bundle.manifest.manifestDigest,
233
+ installedAt: new Date().toISOString(),
234
+ files: bundle.files.map(({ path: filePath, sha256, byteLength }) => ({ path: filePath, sha256, byteLength })),
235
+ };
236
+ }
237
+
238
+ function receiptFilesForManifest(manifest) {
239
+ return manifest.files.map(({ path: filePath, sha256, byteLength }) => ({ path: filePath, sha256, byteLength }));
240
+ }
241
+
242
+ function receiptMatchesManifest(receipt, manifest) {
243
+ return receipt.manifestDigest === manifest.manifestDigest
244
+ && receipt.artifactRevisionId === manifest.artifactRevisionId
245
+ && isDeepStrictEqual(receipt.files, receiptFilesForManifest(manifest));
246
+ }
247
+
248
+ async function createStage(target, bundle, receipt, token, afterCreate) {
249
+ const parent = path.dirname(target);
250
+ const stage = path.join(parent, `.${path.basename(target)}.formation-cloud-stage-${token}`);
251
+ await mkdir(stage, { mode: 0o700 });
252
+ const stageIdentity = identity(await lstat(stage));
253
+ await afterCreate?.({ stage });
254
+ if (bundle.files.some(({ path: filePath }) => filePath.startsWith("references/"))) {
255
+ await mkdir(path.join(stage, "references"), { mode: 0o700 });
256
+ }
257
+ for (const file of bundle.files) await writeExclusive(path.join(stage, ...file.path.split("/")), file.bytes);
258
+ await writeExclusive(path.join(stage, RECEIPT_NAME), Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`, "utf8"));
259
+ if (bundle.files.some(({ path: filePath }) => filePath.startsWith("references/"))) await fsyncDirectory(path.join(stage, "references"));
260
+ await fsyncDirectory(stage);
261
+ await fsyncDirectory(parent);
262
+ return { path: stage, identity: stageIdentity };
263
+ }
264
+
265
+ async function acquireLock(target, parentIdentity, targetState, afterCreate) {
266
+ const lockPath = `${target}.formation-cloud.lock`;
267
+ const token = randomBytes(18).toString("hex");
268
+ await assertIdentity(path.dirname(target), parentIdentity, "The target parent");
269
+ const handle = await open(lockPath, "wx", 0o600).catch((error) => {
270
+ if (error?.code === "EEXIST") conflict("Another Skill install lock already exists.", { lockPath });
271
+ throw error;
272
+ });
273
+ const lockIdentity = identity(await handle.stat());
274
+ let setupError = null;
275
+ try {
276
+ await afterCreate?.({ lockPath });
277
+ await handle.writeFile(`${JSON.stringify({ schemaVersion: 1, token, parent: parentIdentity, target: targetState.identity })}\n`);
278
+ await handle.sync();
279
+ await fsyncDirectory(path.dirname(target));
280
+ } catch (error) { setupError = error; }
281
+ finally { await handle.close(); }
282
+ if (setupError) {
283
+ const info = await maybeLstat(lockPath).catch(() => null);
284
+ if (info && sameIdentity(identity(info), lockIdentity)) {
285
+ await unlink(lockPath).catch(() => {});
286
+ await fsyncDirectory(path.dirname(target)).catch(() => {});
287
+ }
288
+ throw setupError;
289
+ }
290
+ return {
291
+ token,
292
+ path: lockPath,
293
+ async release() {
294
+ const info = await maybeLstat(lockPath);
295
+ if (!info || !sameIdentity(identity(info), lockIdentity)) return;
296
+ const bytes = await readFile(lockPath).catch(() => null);
297
+ if (!bytes || !bytes.toString("utf8").includes(`"token":"${token}"`)) return;
298
+ await unlink(lockPath);
299
+ await fsyncDirectory(path.dirname(target));
300
+ },
301
+ };
302
+ }
303
+
304
+ async function isExactReceiptOwnedTree(treePath, expectedIdentity, expectedReceipt, origin) {
305
+ if (!treePath || !expectedIdentity || !expectedReceipt) return false;
306
+ try {
307
+ const state = await inspectExistingTarget(treePath, origin);
308
+ return state.exists && sameIdentity(state.identity, expectedIdentity) && isDeepStrictEqual(state.receipt, expectedReceipt);
309
+ } catch {
310
+ return false;
311
+ }
312
+ }
313
+
314
+ async function removeExactReceiptOwnedTree(treePath, expectedIdentity, expectedReceipt, origin) {
315
+ if (!await isExactReceiptOwnedTree(treePath, expectedIdentity, expectedReceipt, origin)) return false;
316
+ const quarantine = `${treePath}.formation-cloud-remove-${randomBytes(12).toString("hex")}`;
317
+ if (await maybeLstat(quarantine)) return false;
318
+ await rename(treePath, quarantine);
319
+ if (!await isExactReceiptOwnedTree(quarantine, expectedIdentity, expectedReceipt, origin)) {
320
+ if (!await maybeLstat(treePath)) await rename(quarantine, treePath).catch(() => {});
321
+ return false;
322
+ }
323
+ await rm(quarantine, { recursive: true, force: true });
324
+ return true;
325
+ }
326
+
327
+ export async function installSkill({
328
+ client,
329
+ artifactRevisionId,
330
+ target,
331
+ apply = false,
332
+ replace = false,
333
+ expectedInstalledManifest = null,
334
+ hooks = {},
335
+ }) {
336
+ assertExactTarget(target);
337
+ if (replace && (!apply || !expectedInstalledManifest || !SHA256.test(expectedInstalledManifest))) {
338
+ fail("invalid_replacement", "Replacement requires --apply, --replace, and one exact expected installed manifest digest.",
339
+ "Read the local receipt, then pass --apply --replace --expected-installed-manifest <sha256>.");
340
+ }
341
+ if (!replace && expectedInstalledManifest !== null) {
342
+ fail("invalid_replacement", "An expected installed manifest is valid only for an explicit replacement.",
343
+ "Remove the replacement option, or provide the complete replacement command.");
344
+ }
345
+ const parent = await safeParent(target);
346
+ const before = await inspectExistingTarget(target, client.origin.origin);
347
+ if (before.exists) {
348
+ const installedManifest = manifestFromOperation(await client.manifest(before.receipt.artifactRevisionId), {
349
+ expectedOrigin: client.origin.origin,
350
+ expectedRevisionId: before.receipt.artifactRevisionId,
351
+ });
352
+ if (!receiptMatchesManifest(before.receipt, installedManifest)) {
353
+ conflict("The local receipt does not match the immutable installed manifest.");
354
+ }
355
+ }
356
+ const bundle = await downloadSkillBundle(client, artifactRevisionId);
357
+ if (before.exists) {
358
+ if (before.receipt.artifactRevisionId === bundle.manifest.artifactRevisionId
359
+ && receiptMatchesManifest(before.receipt, bundle.manifest)) {
360
+ return { status: "current", action: "unchanged", applied: false, target, artifactRevisionId, manifestDigest: bundle.manifest.manifestDigest };
361
+ }
362
+ if (!replace) conflict("The target already contains a different receipt-owned Skill revision.", { installedManifestDigest: before.receipt.manifestDigest });
363
+ if (before.receipt.manifestDigest !== expectedInstalledManifest) conflict("The expected installed manifest digest does not match the local receipt.");
364
+ } else if (replace) {
365
+ conflict("Replacement requires an existing receipt-owned Skill target.");
366
+ }
367
+ if (!apply) {
368
+ return { status: "ready", action: "would_install", applied: false, target, artifactRevisionId, manifestDigest: bundle.manifest.manifestDigest };
369
+ }
370
+ const lock = await acquireLock(target, parent.identity, before, hooks.afterLockCreate);
371
+ let stage = null;
372
+ let stageIdentity = null;
373
+ let stageSafeToRemove = true;
374
+ let backup = null;
375
+ let backupIdentity = null;
376
+ let displaced = null;
377
+ let displacedIdentity = null;
378
+ let installedIdentity = null;
379
+ let installReceipt = null;
380
+ try {
381
+ await hooks.afterLock?.({ target, lockPath: lock.path });
382
+ await assertIdentity(parent.path, parent.identity, "The target parent");
383
+ const lockedState = await inspectExistingTarget(target, client.origin.origin);
384
+ if (lockedState.exists !== before.exists || (before.exists && !sameIdentity(lockedState.identity, before.identity))) conflict("The Skill target changed before the locked install.");
385
+ installReceipt = newReceipt(bundle, client.origin.origin);
386
+ const staged = await createStage(target, bundle, installReceipt, lock.token, hooks.afterStageDirectoryCreate);
387
+ stage = staged.path;
388
+ stageIdentity = staged.identity;
389
+ const assertStageComplete = async () => {
390
+ let stagedState;
391
+ try { stagedState = await inspectExistingTarget(stage, client.origin.origin); }
392
+ catch (error) { stageSafeToRemove = false; throw error; }
393
+ if (!stagedState.exists || !sameIdentity(stagedState.identity, stageIdentity)
394
+ || !isDeepStrictEqual(stagedState.receipt, installReceipt)) {
395
+ stageSafeToRemove = false;
396
+ conflict("The staged Skill changed before installation.", { stage });
397
+ }
398
+ };
399
+ await hooks.afterStage?.({ target, stage });
400
+ await assertStageComplete();
401
+ await revalidateCurrentManifest(client, bundle.manifest);
402
+ await assertIdentity(parent.path, parent.identity, "The target parent");
403
+ await assertStageComplete();
404
+ const finalState = await inspectExistingTarget(target, client.origin.origin);
405
+ if (finalState.exists !== before.exists || (before.exists && !sameIdentity(finalState.identity, before.identity))) conflict("The Skill target changed before the atomic swap.");
406
+ if (!before.exists) {
407
+ await hooks.beforeFirstRename?.({ target, stage });
408
+ await assertIdentity(parent.path, parent.identity, "The target parent");
409
+ await assertStageComplete();
410
+ if (await maybeLstat(target)) conflict("The Skill target appeared before the atomic install.");
411
+ installedIdentity = stageIdentity;
412
+ await rename(stage, target);
413
+ stage = null;
414
+ stageIdentity = null;
415
+ await fsyncDirectory(parent.path);
416
+ if (!await isExactReceiptOwnedTree(target, installedIdentity, installReceipt, client.origin.origin)) {
417
+ conflict("The installed target changed before the first install committed.", { target });
418
+ }
419
+ } else {
420
+ backup = path.join(parent.path, `.${path.basename(target)}.formation-cloud-backup-${lock.token}`);
421
+ displaced = path.join(parent.path, `.${path.basename(target)}.formation-cloud-failed-${lock.token}`);
422
+ await rename(target, backup);
423
+ backupIdentity = before.identity;
424
+ await fsyncDirectory(parent.path);
425
+ try {
426
+ await hooks.afterBackup?.({ target, stage, backup });
427
+ await assertIdentity(parent.path, parent.identity, "The target parent");
428
+ await assertIdentity(backup, backupIdentity, "The receipt-owned replacement backup");
429
+ await assertStageComplete();
430
+ if (await maybeLstat(target)) conflict("A competing target appeared during replacement.", { backup });
431
+ await rename(stage, target);
432
+ stage = null;
433
+ stageIdentity = null;
434
+ installedIdentity = staged.identity;
435
+ await fsyncDirectory(parent.path);
436
+ await hooks.afterSwap?.({ target, backup });
437
+ if (!await isExactReceiptOwnedTree(target, installedIdentity, installReceipt, client.origin.origin)) {
438
+ conflict("The installed target changed before replacement commit.", { target });
439
+ }
440
+ } catch (error) {
441
+ if (!await isExactReceiptOwnedTree(backup, backupIdentity, before.receipt, client.origin.origin)) {
442
+ conflict("The receipt-owned replacement backup changed, so rollback was refused.", { backup });
443
+ }
444
+ const current = await maybeLstat(target);
445
+ if (!current) {
446
+ await rename(backup, target);
447
+ backup = null;
448
+ backupIdentity = null;
449
+ await fsyncDirectory(parent.path);
450
+ } else if (installedIdentity && sameIdentity(identity(current), installedIdentity)) {
451
+ await rename(target, displaced);
452
+ displacedIdentity = installedIdentity;
453
+ await rename(backup, target);
454
+ backup = null;
455
+ backupIdentity = null;
456
+ await fsyncDirectory(parent.path);
457
+ if (!await removeExactReceiptOwnedTree(displaced, displacedIdentity, installReceipt, client.origin.origin)) {
458
+ conflict("The failed replacement tree changed before cleanup.", { displaced });
459
+ }
460
+ displaced = null;
461
+ displacedIdentity = null;
462
+ } else {
463
+ conflict("A competing target blocked replacement rollback. The prior receipt-owned install remains in its sibling backup.", { backup });
464
+ }
465
+ throw error;
466
+ }
467
+ if (!await removeExactReceiptOwnedTree(backup, backupIdentity, before.receipt, client.origin.origin)) {
468
+ conflict("The receipt-owned replacement backup changed before cleanup.", { backup });
469
+ }
470
+ backup = null;
471
+ backupIdentity = null;
472
+ await fsyncDirectory(parent.path);
473
+ }
474
+ return { status: "current", action: before.exists ? "replaced" : "installed", applied: true, target,
475
+ artifactRevisionId, manifestDigest: bundle.manifest.manifestDigest };
476
+ } finally {
477
+ if (stageSafeToRemove) await removeExactReceiptOwnedTree(stage, stageIdentity, installReceipt, client.origin.origin).catch(() => {});
478
+ if (backup) {
479
+ const current = await maybeLstat(target).catch(() => null);
480
+ if (!current && await isExactReceiptOwnedTree(backup, backupIdentity, before.receipt, client.origin.origin)) await rename(backup, target).catch(() => {});
481
+ }
482
+ await removeExactReceiptOwnedTree(displaced, displacedIdentity, installReceipt, client.origin.origin).catch(() => {});
483
+ await lock.release().catch(() => {});
484
+ }
485
+ }
486
+
487
+ export async function checkSkill({ client, target }) {
488
+ assertExactTarget(target);
489
+ try {
490
+ await safeParent(target);
491
+ const installed = await inspectExistingTarget(target, client.origin.origin);
492
+ if (!installed.exists) {
493
+ return { status: "conflict", target, reason: "not_installed", deleted: false };
494
+ }
495
+ const remote = manifestFromOperation(await client.manifest(installed.receipt.artifactRevisionId), {
496
+ expectedOrigin: client.origin.origin,
497
+ expectedRevisionId: installed.receipt.artifactRevisionId,
498
+ });
499
+ if (!receiptMatchesManifest(installed.receipt, remote)) {
500
+ return { status: "conflict", target, artifactRevisionId: installed.receipt.artifactRevisionId,
501
+ reason: "immutable_manifest_changed", deleted: false };
502
+ }
503
+ return {
504
+ status: remote.status === "superseded" ? "stale" : remote.status,
505
+ target,
506
+ artifactRevisionId: remote.artifactRevisionId,
507
+ manifestDigest: remote.manifestDigest,
508
+ ...(remote.status === "superseded" ? { currentArtifactRevisionId: remote.supersededByArtifactRevisionId } : {}),
509
+ ...(remote.status === "withdrawn" ? { withdrawalReason: remote.withdrawalReason } : {}),
510
+ deleted: false,
511
+ };
512
+ } catch (error) {
513
+ if (["install_conflict", "invalid_skill_manifest", "skill_artifact_not_found"].includes(error?.code)) {
514
+ return { status: "conflict", target, reason: error.message, deleted: false };
515
+ }
516
+ throw error;
517
+ }
518
+ }
519
+
520
+ export const installerInternals = Object.freeze({ assertExactTarget, assertOwnedSafeMetadata, inspectExistingTarget, receiptMatchesManifest, validateReceipt });