@nggaigc/cli 0.1.10 → 0.1.12

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,690 @@
1
+ import { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
2
+ import { open, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile, } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const ID = /^skill\.[a-z0-9][a-z0-9.-]{0,79}$/;
5
+ const VERSION = /^\d+\.\d+\.\d+$/;
6
+ const DIGEST = /^[a-f0-9]{64}$/;
7
+ const MAX_PACKAGE_BYTES = 4 * 1024 * 1024;
8
+ const MAX_FILES = 32;
9
+ export class IndependentSkillError extends Error {
10
+ code;
11
+ constructor(code) {
12
+ super(code);
13
+ this.code = code;
14
+ this.name = "IndependentSkillError";
15
+ }
16
+ }
17
+ function fail(code) {
18
+ throw new IndependentSkillError(code);
19
+ }
20
+ function record(value) {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+ function sha256(bytes) {
24
+ return createHash("sha256").update(bytes).digest("hex");
25
+ }
26
+ function canonical(value) {
27
+ if (Array.isArray(value))
28
+ return `[${value.map(canonical).join(",")}]`;
29
+ if (value !== null && typeof value === "object")
30
+ return `{${Object.entries(value)
31
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
32
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`)
33
+ .join(",")}}`;
34
+ return JSON.stringify(value);
35
+ }
36
+ function compareVersion(left, right) {
37
+ if (!VERSION.test(left) || !VERSION.test(right))
38
+ fail("PACKAGE_INCOMPATIBLE");
39
+ const a = left.split(".").map(Number);
40
+ const b = right.split(".").map(Number);
41
+ for (let index = 0; index < 3; index++) {
42
+ if (a[index] !== b[index])
43
+ return a[index] - b[index];
44
+ }
45
+ return 0;
46
+ }
47
+ function compatible(version, range) {
48
+ return (compareVersion(version, range.minimum) >= 0 &&
49
+ compareVersion(version, range.maximumExclusive) < 0);
50
+ }
51
+ function resolve(index, id, version, rootVersion, platformVersion) {
52
+ if (index.schemaVersion !== "nggaigc-independent-skill-index/v1" ||
53
+ !ID.test(id) ||
54
+ !index.releaseId ||
55
+ !Array.isArray(index.entries))
56
+ fail("CATALOG_INVALID");
57
+ const state = new Map();
58
+ const ordered = [];
59
+ const visit = (skillId, requiredVersion) => {
60
+ if (!ID.test(skillId))
61
+ fail("CATALOG_INVALID");
62
+ const matches = index.entries.filter((entry) => entry.id === skillId &&
63
+ (requiredVersion === undefined || entry.version === requiredVersion));
64
+ if (matches.length !== 1)
65
+ fail("DEPENDENCY_UNAVAILABLE");
66
+ const entry = matches[0];
67
+ const key = `${entry.id}@${entry.version}`;
68
+ if (state.get(key) === "visiting")
69
+ fail("DEPENDENCY_CYCLE");
70
+ if (state.get(key) === "done")
71
+ return;
72
+ if (entry.state !== "active" ||
73
+ entry.manifest.lifecycle.status !== "published" ||
74
+ entry.manifest.id !== entry.id ||
75
+ entry.manifest.version !== entry.version ||
76
+ !DIGEST.test(entry.manifestSha256) ||
77
+ sha256(canonical(entry.sourceManifest ?? entry.manifest)) !==
78
+ entry.manifestSha256)
79
+ fail("CATALOG_INVALID");
80
+ const metadata = entry.manifest.package;
81
+ if (!DIGEST.test(metadata.sha256) ||
82
+ !Number.isSafeInteger(metadata.sizeBytes) ||
83
+ metadata.sizeBytes < 1 ||
84
+ metadata.sizeBytes > MAX_PACKAGE_BYTES ||
85
+ !compatible(rootVersion, metadata.compatibility.rootSkill) ||
86
+ !compatible(platformVersion, metadata.compatibility.platform))
87
+ fail("PACKAGE_INCOMPATIBLE");
88
+ const dependencies = entry.manifest.requires;
89
+ if (!Array.isArray(dependencies) ||
90
+ dependencies.length > 16 ||
91
+ !dependencies.every((item) => record(item) &&
92
+ typeof item.id === "string" &&
93
+ typeof item.version === "string" &&
94
+ VERSION.test(item.version)) ||
95
+ ordered.length + state.size > 32)
96
+ fail("DEPENDENCY_INVALID");
97
+ state.set(key, "visiting");
98
+ for (const dependency of dependencies)
99
+ visit(dependency.id, dependency.version);
100
+ state.set(key, "done");
101
+ ordered.push(entry);
102
+ };
103
+ if (!VERSION.test(version))
104
+ fail("CATALOG_INVALID");
105
+ visit(id, version);
106
+ return ordered;
107
+ }
108
+ function exactKeys(value, expected) {
109
+ return (JSON.stringify(Object.keys(value).sort()) ===
110
+ JSON.stringify([...expected].sort()));
111
+ }
112
+ function crc32(bytes) {
113
+ let crc = 0xffffffff;
114
+ for (const byte of bytes) {
115
+ crc ^= byte;
116
+ for (let bit = 0; bit < 8; bit++)
117
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
118
+ }
119
+ return (crc ^ 0xffffffff) >>> 0;
120
+ }
121
+ /** Projects the one active public catalog; this is not a second release index. */
122
+ export function projectActiveSkillCatalog(snapshot) {
123
+ if (!record(snapshot) ||
124
+ typeof snapshot.releaseId !== "string" ||
125
+ !Array.isArray(snapshot.entries))
126
+ fail("CATALOG_INVALID");
127
+ const publicEntries = [];
128
+ for (const candidate of snapshot.entries) {
129
+ if (!record(candidate) ||
130
+ typeof candidate.kind !== "string" ||
131
+ typeof candidate.id !== "string" ||
132
+ typeof candidate.version !== "string" ||
133
+ typeof candidate.manifestSha256 !== "string" ||
134
+ !record(candidate.manifest) ||
135
+ candidate.manifest.kind !== candidate.kind ||
136
+ candidate.manifest.id !== candidate.id ||
137
+ candidate.manifest.version !== candidate.version ||
138
+ sha256(canonical(candidate.manifest)) !== candidate.manifestSha256)
139
+ fail("CATALOG_INVALID");
140
+ publicEntries.push(candidate);
141
+ }
142
+ const entries = [];
143
+ for (const item of publicEntries) {
144
+ if (item.kind !== "skill")
145
+ continue;
146
+ const manifest = item.manifest;
147
+ if (manifest.role === "root_skill")
148
+ continue;
149
+ if (!record(manifest.lifecycle) ||
150
+ !record(manifest.package) ||
151
+ !Array.isArray(manifest.dependencies))
152
+ fail("CATALOG_INVALID");
153
+ const pkg = manifest.package;
154
+ if (pkg.version !== item.version ||
155
+ typeof pkg.artifactPath !== "string" ||
156
+ !/^packages\/skills\/[a-z0-9.-]+\.zip$/.test(pkg.artifactPath) ||
157
+ !record(pkg.signature) ||
158
+ !record(pkg.compatibility) ||
159
+ !record(pkg.compatibility.rootSkill) ||
160
+ !record(pkg.compatibility.platform))
161
+ fail("CATALOG_INVALID");
162
+ const requires = [];
163
+ for (const dependency of manifest.dependencies) {
164
+ if (!record(dependency) ||
165
+ typeof dependency.kind !== "string" ||
166
+ typeof dependency.id !== "string" ||
167
+ typeof dependency.version !== "string" ||
168
+ dependency.required !== true)
169
+ fail("DEPENDENCY_INVALID");
170
+ const matches = publicEntries.filter((candidate) => candidate.kind === dependency.kind &&
171
+ candidate.id === dependency.id &&
172
+ candidate.version === dependency.version &&
173
+ record(candidate.manifest) &&
174
+ record(candidate.manifest.lifecycle) &&
175
+ candidate.manifest.lifecycle.status === "published");
176
+ if (matches.length !== 1)
177
+ fail("DEPENDENCY_UNAVAILABLE");
178
+ if (dependency.kind === "skill")
179
+ requires.push({ id: dependency.id, version: dependency.version });
180
+ else if (dependency.kind !== "capability")
181
+ fail("DEPENDENCY_INVALID");
182
+ }
183
+ entries.push({
184
+ id: item.id,
185
+ version: item.version,
186
+ manifestSha256: item.manifestSha256,
187
+ state: "active",
188
+ sourceManifest: manifest,
189
+ manifest: {
190
+ id: item.id,
191
+ version: item.version,
192
+ lifecycle: { status: manifest.lifecycle.status },
193
+ requires,
194
+ package: {
195
+ sha256: pkg.sha256,
196
+ sizeBytes: pkg.sizeBytes,
197
+ signature: pkg.signature,
198
+ compatibility: pkg.compatibility,
199
+ },
200
+ },
201
+ });
202
+ }
203
+ return {
204
+ schemaVersion: "nggaigc-independent-skill-index/v1",
205
+ releaseId: snapshot.releaseId,
206
+ entries,
207
+ };
208
+ }
209
+ function storedZipEntries(input) {
210
+ const bytes = Buffer.from(input);
211
+ const end = bytes.length - 22;
212
+ if (end < 0 ||
213
+ bytes.readUInt32LE(end) !== 0x06054b50 ||
214
+ bytes.readUInt16LE(end + 4) !== 0 ||
215
+ bytes.readUInt16LE(end + 6) !== 0 ||
216
+ bytes.readUInt16LE(end + 20) !== 0)
217
+ fail("PACKAGE_VERIFICATION_FAILED");
218
+ const count = bytes.readUInt16LE(end + 10);
219
+ const centralSize = bytes.readUInt32LE(end + 12);
220
+ const centralOffset = bytes.readUInt32LE(end + 16);
221
+ if (count < 2 || count > MAX_FILES + 1 || centralOffset + centralSize !== end)
222
+ fail("PACKAGE_VERIFICATION_FAILED");
223
+ const result = new Map();
224
+ let cursor = centralOffset;
225
+ let total = 0;
226
+ for (let index = 0; index < count; index++) {
227
+ if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50)
228
+ fail("PACKAGE_VERIFICATION_FAILED");
229
+ const flags = bytes.readUInt16LE(cursor + 8);
230
+ const method = bytes.readUInt16LE(cursor + 10);
231
+ const checksum = bytes.readUInt32LE(cursor + 16);
232
+ const compressed = bytes.readUInt32LE(cursor + 20);
233
+ const uncompressed = bytes.readUInt32LE(cursor + 24);
234
+ const nameLength = bytes.readUInt16LE(cursor + 28);
235
+ const extraLength = bytes.readUInt16LE(cursor + 30);
236
+ const commentLength = bytes.readUInt16LE(cursor + 32);
237
+ const disk = bytes.readUInt16LE(cursor + 34);
238
+ const externalAttributes = bytes.readUInt32LE(cursor + 38);
239
+ const localOffset = bytes.readUInt32LE(cursor + 42);
240
+ const next = cursor + 46 + nameLength + extraLength + commentLength;
241
+ if (next > end ||
242
+ (flags !== 0 && flags !== 0x0800) ||
243
+ method !== 0 ||
244
+ compressed !== uncompressed ||
245
+ uncompressed > MAX_PACKAGE_BYTES ||
246
+ disk !== 0 ||
247
+ localOffset + 30 > centralOffset)
248
+ fail("PACKAGE_VERIFICATION_FAILED");
249
+ const unixMode = externalAttributes >>> 16;
250
+ if (unixMode !== 0 && (unixMode & 0xf000) !== 0x8000)
251
+ fail("PACKAGE_VERIFICATION_FAILED");
252
+ const nameBytes = bytes.subarray(cursor + 46, cursor + 46 + nameLength);
253
+ let name;
254
+ try {
255
+ name = new TextDecoder("utf-8", { fatal: true }).decode(nameBytes);
256
+ }
257
+ catch {
258
+ fail("PACKAGE_VERIFICATION_FAILED");
259
+ }
260
+ if (!name ||
261
+ result.has(name) ||
262
+ bytes.readUInt32LE(localOffset) !== 0x04034b50 ||
263
+ bytes.readUInt16LE(localOffset + 6) !== flags ||
264
+ bytes.readUInt16LE(localOffset + 8) !== method)
265
+ fail("PACKAGE_VERIFICATION_FAILED");
266
+ const localNameLength = bytes.readUInt16LE(localOffset + 26);
267
+ const localExtraLength = bytes.readUInt16LE(localOffset + 28);
268
+ const localName = bytes.subarray(localOffset + 30, localOffset + 30 + localNameLength);
269
+ const start = localOffset + 30 + localNameLength + localExtraLength;
270
+ if (!localName.equals(nameBytes) ||
271
+ start + compressed > centralOffset ||
272
+ bytes.readUInt32LE(localOffset + 14) !== checksum)
273
+ fail("PACKAGE_VERIFICATION_FAILED");
274
+ total += uncompressed;
275
+ if (total > MAX_PACKAGE_BYTES)
276
+ fail("PACKAGE_VERIFICATION_FAILED");
277
+ const content = bytes.subarray(start, start + compressed);
278
+ if (crc32(content) !== checksum)
279
+ fail("PACKAGE_VERIFICATION_FAILED");
280
+ result.set(name, content);
281
+ cursor = next;
282
+ }
283
+ if (cursor !== end || result.size !== count)
284
+ fail("PACKAGE_VERIFICATION_FAILED");
285
+ return result;
286
+ }
287
+ function decodePackage(bytes, entry, trust) {
288
+ const metadata = entry.manifest.package;
289
+ if (bytes.length !== metadata.sizeBytes ||
290
+ sha256(bytes) !== metadata.sha256 ||
291
+ metadata.signature.algorithm !== "ed25519" ||
292
+ metadata.signature.keyId !== trust.keyId ||
293
+ !DIGEST.test(trust.spkiSha256))
294
+ fail("PACKAGE_VERIFICATION_FAILED");
295
+ try {
296
+ const key = createPublicKey(trust.publicKeyPem);
297
+ const fingerprint = sha256(key.export({ type: "spki", format: "der" }));
298
+ if (fingerprint !== trust.spkiSha256 ||
299
+ !verify(null, bytes, key, Buffer.from(metadata.signature.value, "base64")))
300
+ fail("PACKAGE_VERIFICATION_FAILED");
301
+ }
302
+ catch {
303
+ fail("PACKAGE_VERIFICATION_FAILED");
304
+ }
305
+ const archive = storedZipEntries(bytes);
306
+ let payload;
307
+ try {
308
+ payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(archive.get("skill-package.v1.json")));
309
+ }
310
+ catch {
311
+ fail("PACKAGE_VERIFICATION_FAILED");
312
+ }
313
+ if (!record(payload) ||
314
+ payload.schemaVersion !== "ngg-skill-package/v1" ||
315
+ payload.artifactId !== entry.id ||
316
+ payload.packageVersion !== entry.version ||
317
+ canonical(payload.compatibility) !== canonical(metadata.compatibility) ||
318
+ !Array.isArray(payload.files) ||
319
+ payload.files.length < 1 ||
320
+ payload.files.length > MAX_FILES ||
321
+ !exactKeys(payload, [
322
+ "schemaVersion",
323
+ "artifactId",
324
+ "packageVersion",
325
+ "compatibility",
326
+ "files",
327
+ ]))
328
+ fail("PACKAGE_VERIFICATION_FAILED");
329
+ const seen = new Set();
330
+ let total = 0;
331
+ for (const file of payload.files) {
332
+ if (!record(file) ||
333
+ !exactKeys(file, ["path", "sha256", "sizeBytes", "executable"]))
334
+ fail("PACKAGE_VERIFICATION_FAILED");
335
+ if (typeof file.path !== "string" ||
336
+ file.path !== file.path.normalize("NFC") ||
337
+ file.path.length > 160 ||
338
+ file.path.includes("\\") ||
339
+ file.path.includes(":") ||
340
+ file.path.startsWith("/") ||
341
+ file.path
342
+ .split("/")
343
+ .some((part) => !part || part === "." || part === ".." || /[. ]$/.test(part)) ||
344
+ !(file.path === "SKILL.md" ||
345
+ /^references\/[A-Za-z0-9._/-]+\.(md|json)$/.test(file.path)))
346
+ fail("PACKAGE_VERIFICATION_FAILED");
347
+ const folded = file.path.toLowerCase();
348
+ if (seen.has(folded) ||
349
+ !DIGEST.test(file.sha256) ||
350
+ file.executable !== false ||
351
+ !Number.isSafeInteger(file.sizeBytes))
352
+ fail("PACKAGE_VERIFICATION_FAILED");
353
+ seen.add(folded);
354
+ const content = archive.get(file.path);
355
+ if (!content)
356
+ fail("PACKAGE_VERIFICATION_FAILED");
357
+ total += content.length;
358
+ if (total > MAX_PACKAGE_BYTES ||
359
+ content.length !== file.sizeBytes ||
360
+ sha256(content) !== file.sha256)
361
+ fail("PACKAGE_VERIFICATION_FAILED");
362
+ }
363
+ if (!seen.has("skill.md") || archive.size !== payload.files.length + 1)
364
+ fail("PACKAGE_VERIFICATION_FAILED");
365
+ return payload.files.map((file) => ({
366
+ path: file.path,
367
+ sha256: file.sha256,
368
+ contentBase64: archive.get(file.path).toString("base64"),
369
+ }));
370
+ }
371
+ /** Read-only release gate: use the installer's exact archive and file-list verifier. */
372
+ export function verifyIndependentSkillPackage(bytes, entry, trust) {
373
+ return decodePackage(bytes, entry, trust).map(({ path, sha256 }) => ({
374
+ path,
375
+ sha256,
376
+ }));
377
+ }
378
+ async function existingReceipt(target, entry, trust) {
379
+ let receipt;
380
+ try {
381
+ const facts = await lstat(target);
382
+ if (!facts.isDirectory() || facts.isSymbolicLink())
383
+ fail("INSTALL_CONFLICT");
384
+ const value = JSON.parse(await readFile(path.join(target, ".nggaigc-package.json"), "utf8"));
385
+ if (!record(value) ||
386
+ typeof value.id !== "string" ||
387
+ typeof value.version !== "string" ||
388
+ typeof value.sha256 !== "string" ||
389
+ typeof value.manifestSha256 !== "string" ||
390
+ !record(value.files))
391
+ return false;
392
+ receipt = value;
393
+ }
394
+ catch (error) {
395
+ if (error instanceof IndependentSkillError)
396
+ throw error;
397
+ return false;
398
+ }
399
+ if (receipt.id !== entry.id ||
400
+ receipt.version !== entry.version ||
401
+ receipt.sha256 !== entry.manifest.package.sha256 ||
402
+ receipt.manifestSha256 !== entry.manifestSha256 ||
403
+ !receipt.files ||
404
+ typeof receipt.files !== "object")
405
+ return false;
406
+ try {
407
+ const packageBytes = await readFile(path.join(target, ".nggaigc-package.bin"));
408
+ const signedFiles = decodePackage(packageBytes, entry, trust);
409
+ if (JSON.stringify(Object.fromEntries(signedFiles.map((file) => [file.path, file.sha256]))) !== JSON.stringify(receipt.files))
410
+ return false;
411
+ const names = [];
412
+ const walk = async (directory, prefix = "") => {
413
+ for (const child of await readdir(directory, { withFileTypes: true })) {
414
+ const name = prefix ? `${prefix}/${child.name}` : child.name;
415
+ if (child.isSymbolicLink())
416
+ throw new Error();
417
+ if (child.isDirectory())
418
+ await walk(path.join(directory, child.name), name);
419
+ else if (child.isFile())
420
+ names.push(name);
421
+ else
422
+ throw new Error();
423
+ }
424
+ };
425
+ await walk(target);
426
+ if (JSON.stringify(names.sort()) !==
427
+ JSON.stringify([
428
+ ...Object.keys(receipt.files),
429
+ ".nggaigc-package.json",
430
+ ".nggaigc-package.bin",
431
+ ].sort()))
432
+ return false;
433
+ for (const [name, digest] of Object.entries(receipt.files)) {
434
+ if (!(name === "SKILL.md" ||
435
+ /^references\/[A-Za-z0-9._/-]+\.(md|json)$/.test(name)) ||
436
+ name.split("/").some((part) => part === "." || part === ".."))
437
+ return false;
438
+ const file = path.join(target, ...name.split("/"));
439
+ const facts = await lstat(file);
440
+ if (!facts.isFile() ||
441
+ facts.isSymbolicLink() ||
442
+ sha256(await readFile(file)) !== digest)
443
+ return false;
444
+ }
445
+ return Object.hasOwn(receipt.files, "SKILL.md");
446
+ }
447
+ catch {
448
+ return false;
449
+ }
450
+ }
451
+ async function download(entry, releaseId, origin, fetcher) {
452
+ const base = new URL(origin);
453
+ if (base.protocol !== "https:" ||
454
+ base.origin !== "https://api.nggaigc.com" ||
455
+ base.pathname !== "/")
456
+ fail("PACKAGE_SOURCE_REJECTED");
457
+ const url = new URL(`/v1/skill-packages/${encodeURIComponent(entry.id)}/${encodeURIComponent(entry.version)}`, base);
458
+ url.searchParams.set("releaseId", releaseId);
459
+ let response;
460
+ try {
461
+ response = await fetcher(url, {
462
+ method: "GET",
463
+ redirect: "error",
464
+ credentials: "omit",
465
+ cache: "no-store",
466
+ signal: AbortSignal.timeout(15000),
467
+ });
468
+ }
469
+ catch {
470
+ fail("PACKAGE_DOWNLOAD_FAILED");
471
+ }
472
+ if (!response.ok ||
473
+ response.redirected ||
474
+ response.url !== url.href ||
475
+ Number(response.headers.get("content-length") ?? 0) > MAX_PACKAGE_BYTES)
476
+ fail("PACKAGE_DOWNLOAD_FAILED");
477
+ let bytes;
478
+ try {
479
+ bytes = new Uint8Array(await response.arrayBuffer());
480
+ }
481
+ catch {
482
+ fail("PACKAGE_DOWNLOAD_FAILED");
483
+ }
484
+ if (bytes.length > MAX_PACKAGE_BYTES)
485
+ fail("PACKAGE_DOWNLOAD_FAILED");
486
+ return bytes;
487
+ }
488
+ export async function prepareIndependentSkill(options) {
489
+ const ordered = resolve(options.index, options.skillId, options.skillVersion, options.rootSkillVersion, options.platformVersion);
490
+ if (!path.isAbsolute(options.codexHome))
491
+ fail("INSTALL_PATH_UNSAFE");
492
+ const skillsRoot = path.join(options.codexHome, "skills");
493
+ const stateRoot = path.join(options.codexHome, ".nggaigc-managed");
494
+ await mkdir(skillsRoot, { recursive: true });
495
+ await mkdir(stateRoot, { recursive: true });
496
+ if ((await realpath(skillsRoot)) !== path.resolve(skillsRoot))
497
+ fail("INSTALL_PATH_UNSAFE");
498
+ if ((await realpath(stateRoot)) !== path.resolve(stateRoot))
499
+ fail("INSTALL_PATH_UNSAFE");
500
+ const fetcher = options.fetcher ?? fetch;
501
+ let result;
502
+ for (const entry of ordered) {
503
+ const slug = entry.id.slice("skill.".length);
504
+ if (slug === "nggaigc")
505
+ fail("INSTALL_CONFLICT");
506
+ const target = path.join(skillsRoot, slug);
507
+ const lock = path.join(stateRoot, `.nggaigc-${slug}.lock`);
508
+ let handle;
509
+ try {
510
+ handle = await open(lock, "wx", 0o600);
511
+ }
512
+ catch {
513
+ fail("INSTALL_BUSY");
514
+ }
515
+ try {
516
+ const backup = path.join(stateRoot, `.nggaigc-backup-${slug}`);
517
+ if ((await lstat(backup).then(() => true, () => false)) &&
518
+ !(await lstat(target).then(() => true, () => false))) {
519
+ let version;
520
+ try {
521
+ const receipt = JSON.parse(await readFile(path.join(backup, ".nggaigc-package.json"), "utf8"));
522
+ if (!record(receipt) || typeof receipt.version !== "string")
523
+ fail("ROLLBACK_PENDING");
524
+ version = receipt.version;
525
+ }
526
+ catch {
527
+ fail("ROLLBACK_PENDING");
528
+ }
529
+ const previous = options.index.entries.find((candidate) => candidate.id === entry.id &&
530
+ candidate.version === version &&
531
+ candidate.state === "active");
532
+ if (!previous ||
533
+ !(await existingReceipt(backup, previous, options.trust)))
534
+ fail("ROLLBACK_PENDING");
535
+ await rename(backup, target);
536
+ }
537
+ if (await existingReceipt(target, entry, options.trust)) {
538
+ result = {
539
+ id: entry.id,
540
+ version: entry.version,
541
+ skillPath: path.join(target, "SKILL.md"),
542
+ reused: true,
543
+ };
544
+ continue;
545
+ }
546
+ const bytes = await download(entry, options.index.releaseId, options.origin ?? "https://api.nggaigc.com/", fetcher);
547
+ const files = decodePackage(bytes, entry, options.trust);
548
+ const stage = path.join(stateRoot, `.nggaigc-stage-${slug}-${randomUUID()}`);
549
+ await mkdir(stage);
550
+ try {
551
+ for (const file of files) {
552
+ const destination = path.join(stage, ...file.path.split("/"));
553
+ await mkdir(path.dirname(destination), { recursive: true });
554
+ await writeFile(destination, Buffer.from(file.contentBase64, "base64"), { flag: "wx", mode: 0o600 });
555
+ }
556
+ await writeFile(path.join(stage, ".nggaigc-package.json"), `${JSON.stringify({ id: entry.id, version: entry.version, sha256: entry.manifest.package.sha256, manifestSha256: entry.manifestSha256, releaseId: options.index.releaseId, files: Object.fromEntries(files.map((file) => [file.path, file.sha256])) })}\n`, { flag: "wx", mode: 0o600 });
557
+ await writeFile(path.join(stage, ".nggaigc-package.bin"), bytes, {
558
+ flag: "wx",
559
+ mode: 0o600,
560
+ });
561
+ const targetExists = await lstat(target).then(() => true, () => false);
562
+ if (targetExists) {
563
+ let oldReceipt;
564
+ try {
565
+ const value = JSON.parse(await readFile(path.join(target, ".nggaigc-package.json"), "utf8"));
566
+ if (!record(value) ||
567
+ typeof value.id !== "string" ||
568
+ typeof value.version !== "string")
569
+ fail("INSTALL_CONFLICT");
570
+ oldReceipt = { id: value.id, version: value.version };
571
+ }
572
+ catch {
573
+ fail("INSTALL_CONFLICT");
574
+ }
575
+ const oldEntry = options.index.entries.find((candidate) => candidate.id === oldReceipt.id &&
576
+ candidate.version === oldReceipt.version);
577
+ if (oldReceipt.id !== entry.id ||
578
+ !oldEntry ||
579
+ !(await existingReceipt(target, oldEntry, options.trust)))
580
+ fail("INSTALL_CONFLICT");
581
+ if (await lstat(backup).then(() => true, () => false))
582
+ fail("ROLLBACK_PENDING");
583
+ await rename(target, backup);
584
+ }
585
+ try {
586
+ await options.beforeActivate?.();
587
+ await rename(stage, target);
588
+ }
589
+ catch {
590
+ if (targetExists)
591
+ await rename(backup, target);
592
+ fail("INSTALL_FAILED");
593
+ }
594
+ result = {
595
+ id: entry.id,
596
+ version: entry.version,
597
+ skillPath: path.join(target, "SKILL.md"),
598
+ reused: false,
599
+ };
600
+ }
601
+ finally {
602
+ await rm(stage, { recursive: true, force: true });
603
+ }
604
+ }
605
+ finally {
606
+ await handle.close();
607
+ await rm(lock, { force: true });
608
+ }
609
+ }
610
+ return result;
611
+ }
612
+ /** Official start path: online server admission must precede any local preparation. */
613
+ export async function startIndependentSkill(options) {
614
+ const target = resolve(options.index, options.skillId, options.skillVersion, options.rootSkillVersion, options.platformVersion).at(-1);
615
+ const identity = {
616
+ kind: "skill",
617
+ id: target.id,
618
+ version: target.version,
619
+ manifestSha256: target.manifestSha256,
620
+ releaseId: options.index.releaseId,
621
+ };
622
+ let admission;
623
+ try {
624
+ admission = await options.admit(identity);
625
+ }
626
+ catch (error) {
627
+ if (record(error) && error.code === "INSUFFICIENT_CREDITS")
628
+ fail("INSUFFICIENT_CREDITS");
629
+ fail("START_ADMISSION_UNAVAILABLE");
630
+ }
631
+ if (admission.status !== "admitted" ||
632
+ admission.kind !== identity.kind ||
633
+ admission.id !== identity.id ||
634
+ admission.version !== identity.version ||
635
+ admission.manifestSha256 !== identity.manifestSha256 ||
636
+ admission.releaseId !== identity.releaseId)
637
+ fail("START_ADMISSION_REJECTED");
638
+ return prepareIndependentSkill(options);
639
+ }
640
+ export async function rollbackIndependentSkill(options) {
641
+ const previous = resolve(options.index, options.skillId, options.skillVersion, options.rootSkillVersion, options.platformVersion).at(-1);
642
+ const current = options.index.entries.find((entry) => entry.id === options.skillId &&
643
+ entry.version === options.installedVersion);
644
+ if (!current)
645
+ fail("ROLLBACK_UNAVAILABLE");
646
+ if (!path.isAbsolute(options.codexHome))
647
+ fail("INSTALL_PATH_UNSAFE");
648
+ const skillsRoot = path.join(options.codexHome, "skills");
649
+ const stateRoot = path.join(options.codexHome, ".nggaigc-managed");
650
+ if ((await realpath(skillsRoot).catch(() => "")) !== path.resolve(skillsRoot))
651
+ fail("INSTALL_PATH_UNSAFE");
652
+ if ((await realpath(stateRoot).catch(() => "")) !== path.resolve(stateRoot))
653
+ fail("INSTALL_PATH_UNSAFE");
654
+ const slug = options.skillId.slice("skill.".length);
655
+ const target = path.join(skillsRoot, slug);
656
+ const backup = path.join(stateRoot, `.nggaigc-backup-${slug}`);
657
+ const lock = path.join(stateRoot, `.nggaigc-${slug}.lock`);
658
+ let handle;
659
+ try {
660
+ handle = await open(lock, "wx", 0o600);
661
+ }
662
+ catch {
663
+ fail("INSTALL_BUSY");
664
+ }
665
+ const retired = path.join(stateRoot, `.nggaigc-retired-${slug}-${randomUUID()}`);
666
+ try {
667
+ if (!(await existingReceipt(target, current, options.trust)) ||
668
+ !(await existingReceipt(backup, previous, options.trust)))
669
+ fail("ROLLBACK_UNAVAILABLE");
670
+ await rename(target, retired);
671
+ try {
672
+ await rename(backup, target);
673
+ }
674
+ catch {
675
+ await rename(retired, target);
676
+ fail("INSTALL_FAILED");
677
+ }
678
+ await rm(retired, { recursive: true });
679
+ return {
680
+ id: previous.id,
681
+ version: previous.version,
682
+ skillPath: path.join(target, "SKILL.md"),
683
+ reused: false,
684
+ };
685
+ }
686
+ finally {
687
+ await handle.close();
688
+ await rm(lock, { force: true });
689
+ }
690
+ }