@nggaigc/cli 0.1.54 → 0.1.56
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/README.md +9 -1
- package/dist/bundle.js +49 -4
- package/dist/install.js +4 -1
- package/dist/main.js +13 -4
- package/dist/root-store.js +174 -0
- package/dist/root-upgrade.js +366 -0
- package/dist/skill-command.js +174 -69
- package/dist/start-compatibility.js +123 -0
- package/package.json +1 -1
- package/vendor/roots/1.1.49/manifest.json +47 -0
- package/vendor/roots/1.1.49/nggaigc-plugin-1.1.49.zip +0 -0
- package/vendor/start-compatibility.json +124 -0
package/README.md
CHANGED
|
@@ -12,7 +12,15 @@ CLI 保留 macOS npm symlink 入口与 packed-TGZ launcher 检查,并验证随
|
|
|
12
12
|
|
|
13
13
|
从 CLI `0.1.52` 起,Intel Mac 仅放开安装限制,安装的签名包与 macOS 辅助程序字节保持不变;未做 Intel 兼容性适配或真机验收,不保证登录及工作流可用,运行失败按原有机制报告。实际公开版本和 `@latest` 状态以 npm registry 为准,Apple Silicon 真机验收需单独进行。
|
|
14
14
|
|
|
15
|
-
CLI `0.1.
|
|
15
|
+
CLI `0.1.56` 延续 `0.1.55` 的版本兼容修复,实际公开版本与 `latest` 以 registry 为准。它通过官方签名 ZIP 的完整文件闭包识别现有根 Skill `1.1.49`/`1.1.50`;本地安装标记仅作提示。兼容时直接复用现有启动器,当前目录确实要求新版根时才安全升级;不会降级已验证的新版安装。旧目录仍使用根149时,保留已装150,并在不可发现为 active Skill 的私有缓存内验证、使用官方149兼容运行文件;目录切到新版后使用150,启动回执指向实际受信运行文件。保留豆包工作识别和 Intel Mac 安装放行的原边界。
|
|
16
|
+
|
|
17
|
+
升级在原安装锁内先验证私有临时目录的文件类型、哈希和系统可表达的执行位,再原子替换并复验。失败时恢复已验证旧安装;进程中断后先按原事务读回恢复,不盲目开始新升级。Windows 不把 POSIX 执行位当作其系统权限证明,真机验收另记。
|
|
18
|
+
|
|
19
|
+
CLI `0.1.56` 修复 Windows 写盘句柄:校验缓存和升级暂存文件使用不截断文件的读写句柄执行 `fsync`,保留验签和失败停止。更新已发布安装器时,使用 `npx --yes @nggaigc/cli@0.1.56 install`;项目级或指定宿主安装保留原命令中的 `--skills-dir` 或 `--agent`,不要另换目录、删除配置或重提已有付费任务。发布状态以 npm 为准。本次原生 Windows 实测尚未完成,不能把模拟回归或 Mac 验收视为 Windows 已通过。维护者在原生 Windows 的 Node 22、24 上构建后,必须分别运行 `node --test packages/cli/scripts/windows-root-flush.integration.test.mjs`,覆盖新装、149→150 升级、重复安装及失败恢复;非 Windows 环境会明确拒绝该验收。模拟约束的回归测试不能代替此项。
|
|
20
|
+
|
|
21
|
+
旧的显式 `start-skill skill.<id> <version>` 只有在官方受签、精确绑定当前目录的有限迁移许可内,才可转到同一 Skill 的当前版本;结果分别返回 `requestedVersion` 与 `resolvedVersion`,在线准入使用真实新版本及哈希。未列版本、目录变化、停用或验签失败均停止。确需固定版本可加 `--exact-version`,此时绝不迁移到其他版本。无版本调用仍要求唯一 active 入口,不激活旧版本。
|
|
22
|
+
|
|
23
|
+
此许可仅用于新启动。已经受理或结果未知的任务保留原请求和任务身份;`client` 将原参数交给已验证启动器,不为恢复任务重新准入、提交或付费。
|
|
16
24
|
|
|
17
25
|
## 统一安装入口(0.1.46 起)
|
|
18
26
|
|
package/dist/bundle.js
CHANGED
|
@@ -36,8 +36,13 @@ async function json(file) {
|
|
|
36
36
|
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
export async function verifyBundle(input) {
|
|
39
|
+
export async function verifyBundle(input, expectedVersion = "1.1.50") {
|
|
40
40
|
try {
|
|
41
|
+
for (const file of [input.archive, input.manifest, input.trust]) {
|
|
42
|
+
const info = await lstat(file);
|
|
43
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
44
|
+
throw new Error();
|
|
45
|
+
}
|
|
41
46
|
const manifestValue = await json(input.manifest);
|
|
42
47
|
const trustValue = await json(input.trust);
|
|
43
48
|
const signature = record(manifestValue)
|
|
@@ -47,7 +52,7 @@ export async function verifyBundle(input) {
|
|
|
47
52
|
!exactKeys(manifestValue, manifestKeys) ||
|
|
48
53
|
manifestValue.schemaVersion !== "nggaigc-plugin-package/v1" ||
|
|
49
54
|
manifestValue.artifactId !== "plugin.nggaigc" ||
|
|
50
|
-
manifestValue.version !==
|
|
55
|
+
manifestValue.version !== expectedVersion ||
|
|
51
56
|
manifestValue.artifactPath !==
|
|
52
57
|
`packages/plugins/nggaigc-plugin-${manifestValue.version}.zip` ||
|
|
53
58
|
typeof manifestValue.sourceCommit !== "string" ||
|
|
@@ -120,6 +125,22 @@ export async function verifyBundle(input) {
|
|
|
120
125
|
if (!record(pluginMetadata) ||
|
|
121
126
|
pluginMetadata.version !== manifestValue.version)
|
|
122
127
|
throw new Error();
|
|
128
|
+
const rootSource = contents.find((entry) => entry.name === "skills/nggaigc/references/source.json");
|
|
129
|
+
const rootBootstrap = contents.find((entry) => entry.name === "skills/nggaigc/scripts/bootstrap-public.json");
|
|
130
|
+
const sourceIdentity = rootSource
|
|
131
|
+
? JSON.parse(rootSource.bytes.toString("utf8"))
|
|
132
|
+
: null;
|
|
133
|
+
const bootstrapIdentity = rootBootstrap
|
|
134
|
+
? JSON.parse(rootBootstrap.bytes.toString("utf8"))
|
|
135
|
+
: null;
|
|
136
|
+
if (!record(sourceIdentity) ||
|
|
137
|
+
sourceIdentity.id !== "skill.nggaigc" ||
|
|
138
|
+
sourceIdentity.version !== manifestValue.version ||
|
|
139
|
+
sourceIdentity.clientProtocol !== 1 ||
|
|
140
|
+
!record(bootstrapIdentity) ||
|
|
141
|
+
bootstrapIdentity.version !== manifestValue.version ||
|
|
142
|
+
bootstrapIdentity.credentialTarget !== "NGGAIGC/Codex/v1")
|
|
143
|
+
throw new Error();
|
|
123
144
|
const supported = contents.find((entry) => entry.name === "skills/nggaigc/scripts/supported-platforms.json");
|
|
124
145
|
if (!supported)
|
|
125
146
|
throw new Error();
|
|
@@ -139,6 +160,9 @@ export async function verifyBundle(input) {
|
|
|
139
160
|
version: manifestValue.version,
|
|
140
161
|
platforms: manifestValue.platforms,
|
|
141
162
|
sha256: manifestValue.sha256,
|
|
163
|
+
executableFiles: contents
|
|
164
|
+
.filter((entry) => entry.executable)
|
|
165
|
+
.map((entry) => entry.name),
|
|
142
166
|
};
|
|
143
167
|
}
|
|
144
168
|
catch (error) {
|
|
@@ -213,18 +237,39 @@ export async function verifyExtractedBundle(directory, expected) {
|
|
|
213
237
|
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
214
238
|
}
|
|
215
239
|
}
|
|
216
|
-
export async function verifyInstalledSkill(directory, bundleFiles) {
|
|
240
|
+
export async function verifyInstalledSkill(directory, bundleFiles, executableFiles) {
|
|
217
241
|
const prefix = "skills/nggaigc/";
|
|
218
242
|
const expected = Object.fromEntries(Object.entries(bundleFiles)
|
|
219
243
|
.filter(([name]) => name.startsWith(prefix))
|
|
220
244
|
.map(([name, digest]) => [name.slice(prefix.length), digest]));
|
|
221
245
|
try {
|
|
246
|
+
const rootFacts = await lstat(directory);
|
|
247
|
+
if (!rootFacts.isDirectory() || rootFacts.isSymbolicLink())
|
|
248
|
+
throw new Error();
|
|
222
249
|
const actual = await entries(directory);
|
|
223
250
|
const allowed = [...Object.keys(expected), ".nggaigc-install.json"].sort();
|
|
224
251
|
if (JSON.stringify(actual) !== JSON.stringify(allowed))
|
|
225
252
|
throw new Error();
|
|
253
|
+
const checkDirectories = async (parent, prefix = "") => {
|
|
254
|
+
for (const child of await readdir(parent, { withFileTypes: true }))
|
|
255
|
+
if (child.isDirectory()) {
|
|
256
|
+
const relative = prefix + child.name + "/";
|
|
257
|
+
if (!Object.keys(expected).some((name) => name.startsWith(relative)))
|
|
258
|
+
throw new Error();
|
|
259
|
+
await checkDirectories(path.join(parent, child.name), relative);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
await checkDirectories(directory);
|
|
226
263
|
for (const [name, digest] of Object.entries(expected)) {
|
|
227
|
-
const
|
|
264
|
+
const absolute = path.resolve(directory, ...name.split("/"));
|
|
265
|
+
const facts = await lstat(absolute);
|
|
266
|
+
if (!facts.isFile() || facts.isSymbolicLink())
|
|
267
|
+
throw new Error();
|
|
268
|
+
if (executableFiles &&
|
|
269
|
+
process.platform !== "win32" &&
|
|
270
|
+
Boolean(facts.mode & 0o111) !== executableFiles.includes(prefix + name))
|
|
271
|
+
throw new Error();
|
|
272
|
+
const bytes = await readFile(absolute);
|
|
228
273
|
if (createHash("sha256").update(bytes).digest("hex") !== digest)
|
|
229
274
|
throw new Error();
|
|
230
275
|
}
|
package/dist/install.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withRootLock, installVerifiedRootLocked } from "./root-upgrade.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
@@ -242,7 +243,9 @@ export async function runInstall(options = {}) {
|
|
|
242
243
|
const installer = require(path.join(extracted, "installer", "install.cjs"));
|
|
243
244
|
if (!installer || typeof installer.main !== "function")
|
|
244
245
|
throw new CliError("INSTALLATION_FAILED");
|
|
245
|
-
const installation =
|
|
246
|
+
const installation = options.bundle
|
|
247
|
+
? await installSkill(installer, path.join(extracted, "skills/nggaigc"), profile, skillsDirectory, bundle.version, bundle.files)
|
|
248
|
+
: await withRootLock(skillsDirectory, () => installVerifiedRootLocked(skillsDirectory, bundle));
|
|
246
249
|
const installedScripts = path.join(skillsDirectory, "nggaigc", "scripts");
|
|
247
250
|
if (options.connect === false)
|
|
248
251
|
return { status: "installed", installation, version: bundle.version };
|
package/dist/main.js
CHANGED
|
@@ -27,6 +27,9 @@ const errorMessages = {
|
|
|
27
27
|
LOCAL_SESSION_FAILED: "本地凭据会话未能完成;请先处理本地错误,不要重复网页授权。",
|
|
28
28
|
LOCAL_SESSION_TIMEOUT: "本地凭据会话超时;请先检查当前本地进程,不要重复网页授权。",
|
|
29
29
|
POWERSHELL_EXECUTION_POLICY_BLOCKED: "Windows 脚本执行受到系统或组织策略限制,设备连接未完成。安装器没有更改执行策略。",
|
|
30
|
+
ROOT_UPGRADE_RESTORED: "上次升级中断,已恢复并验证旧安装;本次未重新升级或提交任务。请确认恢复结果后再发起新的升级。",
|
|
31
|
+
ROOT_RECOVERY_REQUIRED: "本地升级状态无法安全恢复,已停止;请保留安装目录和事务记录并联系支持。",
|
|
32
|
+
INSTALL_BUSY: "同一安装目录正在使用或更新,请等待原操作完成。",
|
|
30
33
|
INSTALLATION_FAILED: "本地安装未完成,请检查现有文件状态后重试。",
|
|
31
34
|
SKILL_PAIR_UPGRADE_REQUIRED: "检测到无法安全配套迁移的本地 Seedream Skill,已停止升级。请保留旧 Skill 和任务文件并联系支持。",
|
|
32
35
|
SKILL_PAIR_RECOVERY_REQUIRED: "检测到本地组合安装中断或目录变化,已停止自动操作。请保留相关目录和任务文件并联系支持。",
|
|
@@ -42,7 +45,7 @@ export async function main(args, runtime = {}) {
|
|
|
42
45
|
output([
|
|
43
46
|
"NGGAIGC CLI",
|
|
44
47
|
"install [--agent codex|workbuddy|doubao-work|universal | --skills-dir <absolute-directory>] [--no-login]",
|
|
45
|
-
"start-skill skill.<id> [version] [--agent codex|workbuddy|doubao-work|universal | --skills-dir <absolute-directory>]",
|
|
48
|
+
"start-skill skill.<id> [version] [--exact-version] [--agent codex|workbuddy|doubao-work|universal | --skills-dir <absolute-directory>]",
|
|
46
49
|
"client [--agent codex|workbuddy|doubao-work|universal | --skills-dir <absolute-directory>] -- <command> [arguments]",
|
|
47
50
|
"Default: automatically detect Codex, WorkBuddy or Doubao Work. If unknown or conflicting, select one; non-interactive calls return selection-required (exit 3).",
|
|
48
51
|
"Codex: existing CODEX_HOME is preserved, otherwise ~/.codex/skills. WorkBuddy: its runtime config directory/skills, otherwise ~/.workbuddy/skills; existing legacy ~/.codebuddy installs are reused. Doubao Work: ~/DoubaoWork/skills. Explicit legacy universal/custom targets remain supported.",
|
|
@@ -86,7 +89,9 @@ export async function main(args, runtime = {}) {
|
|
|
86
89
|
}
|
|
87
90
|
let parsed;
|
|
88
91
|
try {
|
|
89
|
-
parsed = parseSkillLocation(args
|
|
92
|
+
parsed = parseSkillLocation(args
|
|
93
|
+
.slice(1)
|
|
94
|
+
.filter((value) => args[0] !== "start-skill" || value !== "--exact-version"));
|
|
90
95
|
}
|
|
91
96
|
catch {
|
|
92
97
|
errorOutput(`${errorMessages.INVALID_INPUT}\n`);
|
|
@@ -94,10 +99,14 @@ export async function main(args, runtime = {}) {
|
|
|
94
99
|
}
|
|
95
100
|
const parameters = parsed.positional;
|
|
96
101
|
if (args[0] === "start-skill" &&
|
|
97
|
-
(parameters.length === 1 || parameters.length === 2)
|
|
102
|
+
(parameters.length === 1 || parameters.length === 2) &&
|
|
103
|
+
(!args.includes("--exact-version") || parameters.length === 2)) {
|
|
98
104
|
try {
|
|
99
105
|
const location = await selectCommandLocation({ ...runtime.skillOptions, ...parsed.location }, runtime.agentSelection);
|
|
100
|
-
const result = await (runtime.startSkill ?? runSkillStartCommand)(parameters[0], parameters[1],
|
|
106
|
+
const result = await (runtime.startSkill ?? runSkillStartCommand)(parameters[0], parameters[1], {
|
|
107
|
+
...location,
|
|
108
|
+
...(args.includes("--exact-version") ? { exactVersion: true } : {}),
|
|
109
|
+
});
|
|
101
110
|
output(`${JSON.stringify({ status: "ready", ...result })}\n`);
|
|
102
111
|
return 0;
|
|
103
112
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/** Official root material is authenticity evidence; a local marker is only a hint. */
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readFile, rename, copyFile, open, realpath, rm, } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { verifyBundle, verifyInstalledSkill, extractBundle, verifyExtractedBundle, } from "./bundle.js";
|
|
7
|
+
import { stateDirectoryForSkills } from "./skill-location.js";
|
|
8
|
+
import { CliError } from "./errors.js";
|
|
9
|
+
export const rootVendor = fileURLToPath(new URL("../vendor/", import.meta.url));
|
|
10
|
+
const versionPattern = /^\d+\.\d+\.\d+$/;
|
|
11
|
+
export async function exists(file) {
|
|
12
|
+
try {
|
|
13
|
+
await lstat(file);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (e.code === "ENOENT")
|
|
18
|
+
return false;
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function realDirectory(directory) {
|
|
23
|
+
const info = await lstat(directory);
|
|
24
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
25
|
+
throw new CliError("INSTALL_PATH_UNSAFE");
|
|
26
|
+
}
|
|
27
|
+
export async function privateDirectory(directory) {
|
|
28
|
+
if (!(await exists(directory))) {
|
|
29
|
+
await privateDirectory(path.dirname(directory));
|
|
30
|
+
await mkdir(directory, { mode: 0o700 });
|
|
31
|
+
}
|
|
32
|
+
await realDirectory(directory);
|
|
33
|
+
if ((await realpath(directory)) !== path.resolve(directory))
|
|
34
|
+
throw new CliError("INSTALL_PATH_UNSAFE");
|
|
35
|
+
}
|
|
36
|
+
export async function syncDirectory(directory) {
|
|
37
|
+
if (process.platform === "win32")
|
|
38
|
+
return; // Windows has no portable fsync(directory).
|
|
39
|
+
const fd = await open(directory, "r");
|
|
40
|
+
try {
|
|
41
|
+
await fd.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await fd.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function proofDirectory(skills, version) {
|
|
48
|
+
if (!versionPattern.test(version))
|
|
49
|
+
throw new CliError("ROOT_UNAVAILABLE");
|
|
50
|
+
return path.join(stateDirectoryForSkills(skills), "root-proofs", version);
|
|
51
|
+
}
|
|
52
|
+
export function bundleFiles(directory, version, vendor = rootVendor) {
|
|
53
|
+
return {
|
|
54
|
+
archive: path.join(directory, `nggaigc-plugin-${version}.zip`),
|
|
55
|
+
manifest: path.join(directory, "manifest.json"),
|
|
56
|
+
trust: path.join(vendor, "trusted-keys.json"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function knownBundle(skills, version, vendor = rootVendor) {
|
|
60
|
+
const cache = proofDirectory(skills, version);
|
|
61
|
+
// An existing corrupt cache is rejected; never silently replace its evidence.
|
|
62
|
+
if (await exists(cache)) {
|
|
63
|
+
await realDirectory(cache);
|
|
64
|
+
if ((await realpath(cache)) !== path.resolve(cache))
|
|
65
|
+
throw new CliError("INSTALL_PATH_UNSAFE");
|
|
66
|
+
return verifyBundle(bundleFiles(cache, version, vendor), version);
|
|
67
|
+
}
|
|
68
|
+
return verifyBundle(bundleFiles(version === "1.1.50" ? vendor : path.join(vendor, "roots", version), version, vendor), version);
|
|
69
|
+
}
|
|
70
|
+
export async function saveProof(skills, bundle, vendor = rootVendor) {
|
|
71
|
+
const dest = proofDirectory(skills, bundle.version);
|
|
72
|
+
if (await exists(dest)) {
|
|
73
|
+
const prior = await knownBundle(skills, bundle.version, vendor);
|
|
74
|
+
if (prior.sha256 !== bundle.sha256)
|
|
75
|
+
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const parent = path.dirname(dest);
|
|
79
|
+
await privateDirectory(parent);
|
|
80
|
+
const temp = path.join(parent, `.proof-${bundle.version}-${randomBytes(16).toString("hex")}`);
|
|
81
|
+
await mkdir(temp, { mode: 0o700 });
|
|
82
|
+
await copyFile(bundle.archive, path.join(temp, `nggaigc-plugin-${bundle.version}.zip`), 1);
|
|
83
|
+
// The archive-adjacent manifest is already cryptographically validated above.
|
|
84
|
+
await copyFile(path.join(path.dirname(bundle.archive), "manifest.json"), path.join(temp, "manifest.json"), 1);
|
|
85
|
+
const verified = await verifyBundle(bundleFiles(temp, bundle.version, vendor), bundle.version);
|
|
86
|
+
if (verified.sha256 !== bundle.sha256)
|
|
87
|
+
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
88
|
+
for (const file of [
|
|
89
|
+
path.join(temp, `nggaigc-plugin-${bundle.version}.zip`),
|
|
90
|
+
path.join(temp, "manifest.json"),
|
|
91
|
+
]) {
|
|
92
|
+
// Windows FlushFileBuffers requires a writable handle. r+ preserves bytes
|
|
93
|
+
// and fails if the already-verified staging file is missing (unlike w).
|
|
94
|
+
const fd = await open(file, "r+");
|
|
95
|
+
try {
|
|
96
|
+
await fd.sync();
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await fd.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await syncDirectory(temp);
|
|
103
|
+
await rename(temp, dest);
|
|
104
|
+
await syncDirectory(parent);
|
|
105
|
+
}
|
|
106
|
+
export async function trustedRoot(skills, vendor = rootVendor) {
|
|
107
|
+
const root = path.join(skills, "nggaigc");
|
|
108
|
+
await realDirectory(root);
|
|
109
|
+
let version;
|
|
110
|
+
try {
|
|
111
|
+
version = JSON.parse(await readFile(path.join(root, "references/source.json"), "utf8")).version;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new CliError("ROOT_UNAVAILABLE");
|
|
115
|
+
}
|
|
116
|
+
if (typeof version !== "string" || !versionPattern.test(version))
|
|
117
|
+
throw new CliError("ROOT_UNAVAILABLE");
|
|
118
|
+
const bundle = await knownBundle(skills, version, vendor);
|
|
119
|
+
await verifyInstalledSkill(root, bundle.files, bundle.executableFiles);
|
|
120
|
+
// This binds source.json and protocol to the signed payload, regardless of marker.version.
|
|
121
|
+
return { version: bundle.version, bundle, directory: root };
|
|
122
|
+
}
|
|
123
|
+
/** Creation holds the install lock; verified immutable reuse is read-only. */
|
|
124
|
+
export async function trustedRuntime(skills, version, vendor = rootVendor, create = true) {
|
|
125
|
+
if (!["1.1.49", "1.1.50"].includes(version))
|
|
126
|
+
throw new CliError("ROOT_UNAVAILABLE");
|
|
127
|
+
await trustedRoot(skills, vendor);
|
|
128
|
+
// Delegates always use an immutable runtime, including the currently installed version.
|
|
129
|
+
const bundle = await knownBundle(skills, version, vendor);
|
|
130
|
+
const parent = path.join(stateDirectoryForSkills(skills), "root-runtimes");
|
|
131
|
+
const directory = path.join(parent, version);
|
|
132
|
+
if (await exists(directory)) {
|
|
133
|
+
if ((await realpath(directory)) !== path.resolve(directory))
|
|
134
|
+
throw new CliError("INSTALL_PATH_UNSAFE");
|
|
135
|
+
await verifyInstalledSkill(directory, bundle.files, bundle.executableFiles);
|
|
136
|
+
return { version, bundle, directory };
|
|
137
|
+
}
|
|
138
|
+
if (!create)
|
|
139
|
+
throw new CliError("ROOT_RUNTIME_UNAVAILABLE");
|
|
140
|
+
await privateDirectory(parent);
|
|
141
|
+
const temporary = path.join(parent, ".runtime-" + randomBytes(16).toString("hex"));
|
|
142
|
+
try {
|
|
143
|
+
await extractBundle(bundle.archive, temporary, bundle.sha256);
|
|
144
|
+
await verifyExtractedBundle(temporary, bundle.files);
|
|
145
|
+
const root = path.join(temporary, "skills/nggaigc");
|
|
146
|
+
const marker = await open(path.join(root, ".nggaigc-install.json"), "wx", 0o600);
|
|
147
|
+
try {
|
|
148
|
+
await marker.writeFile(JSON.stringify({
|
|
149
|
+
schemaVersion: "nggaigc-local-install/v1",
|
|
150
|
+
version,
|
|
151
|
+
installedAt: new Date().toISOString(),
|
|
152
|
+
}) + "\n");
|
|
153
|
+
await marker.sync();
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await marker.close();
|
|
157
|
+
}
|
|
158
|
+
await verifyInstalledSkill(root, bundle.files, bundle.executableFiles);
|
|
159
|
+
if (await exists(directory))
|
|
160
|
+
throw new CliError("ROOT_RECOVERY_REQUIRED");
|
|
161
|
+
await rename(root, directory);
|
|
162
|
+
await syncDirectory(parent);
|
|
163
|
+
await verifyInstalledSkill(directory, bundle.files, bundle.executableFiles);
|
|
164
|
+
return { version, bundle, directory };
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await rm(temporary, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function bytesHash(file) {
|
|
171
|
+
return createHash("sha256")
|
|
172
|
+
.update(await readFile(file))
|
|
173
|
+
.digest("hex");
|
|
174
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/** A finite root transaction. It shares the signed installer's lock and keeps verified backups. */
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, open, readFile, rmdir, rename, rm, readdir, } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { CliError } from "./errors.js";
|
|
7
|
+
import { extractBundle, verifyExtractedBundle, verifyInstalledSkill, } from "./bundle.js";
|
|
8
|
+
import { exists, knownBundle, privateDirectory, rootVendor, saveProof, syncDirectory, trustedRoot, } from "./root-store.js";
|
|
9
|
+
const journalName = ".nggaigc-root-upgrade.json";
|
|
10
|
+
const noncePattern = /^[a-f0-9]{32}$/;
|
|
11
|
+
const supported = ["1.1.49", "1.1.50"];
|
|
12
|
+
function fail() {
|
|
13
|
+
throw new CliError("ROOT_RECOVERY_REQUIRED");
|
|
14
|
+
}
|
|
15
|
+
async function durableJson(file, value) {
|
|
16
|
+
const fd = await open(file, "wx", 0o600);
|
|
17
|
+
try {
|
|
18
|
+
await fd.writeFile(JSON.stringify(value) + "\n");
|
|
19
|
+
await fd.sync();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await fd.close();
|
|
23
|
+
}
|
|
24
|
+
await syncDirectory(path.dirname(file));
|
|
25
|
+
}
|
|
26
|
+
const acquisitionGateName = ".nggaigc-install-acquire";
|
|
27
|
+
/** No stale-gate reclamation: an interrupted acquisition requires explicit readback. */
|
|
28
|
+
async function acquisitionGate(skills, own) {
|
|
29
|
+
const directory = path.join(skills, acquisitionGateName);
|
|
30
|
+
const ownerFile = path.join(directory, "owner.json");
|
|
31
|
+
try {
|
|
32
|
+
await mkdir(directory, { mode: 0o700 });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== "EEXIST")
|
|
36
|
+
throw error;
|
|
37
|
+
try {
|
|
38
|
+
const facts = await lstat(directory);
|
|
39
|
+
const ownerFacts = await lstat(ownerFile);
|
|
40
|
+
if (!facts.isDirectory() ||
|
|
41
|
+
facts.isSymbolicLink() ||
|
|
42
|
+
!ownerFacts.isFile() ||
|
|
43
|
+
ownerFacts.isSymbolicLink())
|
|
44
|
+
fail();
|
|
45
|
+
const prior = JSON.parse(await readFile(ownerFile, "utf8"));
|
|
46
|
+
if (Object.keys(prior).sort().join(",") !== "nonce,pid,schemaVersion" ||
|
|
47
|
+
prior.schemaVersion !== "nggaigc-install-acquire/v1" ||
|
|
48
|
+
!Number.isSafeInteger(prior.pid) ||
|
|
49
|
+
prior.pid < 1 ||
|
|
50
|
+
typeof prior.nonce !== "string" ||
|
|
51
|
+
!noncePattern.test(prior.nonce))
|
|
52
|
+
fail();
|
|
53
|
+
try {
|
|
54
|
+
process.kill(prior.pid, 0);
|
|
55
|
+
}
|
|
56
|
+
catch (signal) {
|
|
57
|
+
if (signal.code === "ESRCH")
|
|
58
|
+
fail();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (cause) {
|
|
62
|
+
if (cause instanceof CliError)
|
|
63
|
+
throw cause;
|
|
64
|
+
fail();
|
|
65
|
+
}
|
|
66
|
+
throw new CliError("INSTALL_BUSY");
|
|
67
|
+
}
|
|
68
|
+
const identity = await lstat(directory);
|
|
69
|
+
// A failure before this write completes leaves the gate in place, never guessed away.
|
|
70
|
+
await durableJson(ownerFile, {
|
|
71
|
+
...own,
|
|
72
|
+
schemaVersion: "nggaigc-install-acquire/v1",
|
|
73
|
+
});
|
|
74
|
+
await syncDirectory(skills);
|
|
75
|
+
return async () => {
|
|
76
|
+
const facts = await lstat(directory);
|
|
77
|
+
const ownerFacts = await lstat(ownerFile);
|
|
78
|
+
const owner = JSON.parse(await readFile(ownerFile, "utf8"));
|
|
79
|
+
if (!facts.isDirectory() ||
|
|
80
|
+
facts.isSymbolicLink() ||
|
|
81
|
+
facts.ino !== identity.ino ||
|
|
82
|
+
!ownerFacts.isFile() ||
|
|
83
|
+
ownerFacts.isSymbolicLink() ||
|
|
84
|
+
owner.schemaVersion !== "nggaigc-install-acquire/v1" ||
|
|
85
|
+
owner.pid !== own.pid ||
|
|
86
|
+
owner.nonce !== own.nonce ||
|
|
87
|
+
(await readdir(directory)).join(",") !== "owner.json")
|
|
88
|
+
fail();
|
|
89
|
+
await rm(ownerFile);
|
|
90
|
+
await rmdir(directory);
|
|
91
|
+
await syncDirectory(skills);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function withRootLock(skills, action) {
|
|
95
|
+
await privateDirectory(skills);
|
|
96
|
+
const lock = path.join(skills, ".nggaigc-install.lock");
|
|
97
|
+
const own = {
|
|
98
|
+
schemaVersion: "nggaigc-install-lock/v2",
|
|
99
|
+
pid: process.pid,
|
|
100
|
+
nonce: randomBytes(16).toString("hex"),
|
|
101
|
+
};
|
|
102
|
+
// Every 055 creator/reclaimer takes this atomic gate before touching the main
|
|
103
|
+
// lock. A delayed stale snapshot cannot unlink another 055 owner's live lock.
|
|
104
|
+
const releaseGate = await acquisitionGate(skills, own);
|
|
105
|
+
let fd;
|
|
106
|
+
try {
|
|
107
|
+
try {
|
|
108
|
+
try {
|
|
109
|
+
fd = await open(lock, "wx", 0o600);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (e.code !== "EEXIST")
|
|
113
|
+
throw e;
|
|
114
|
+
const before = await lstat(lock);
|
|
115
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
116
|
+
throw new CliError("INSTALL_BUSY");
|
|
117
|
+
let prior;
|
|
118
|
+
try {
|
|
119
|
+
prior = JSON.parse(await readFile(lock, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new CliError("INSTALL_BUSY");
|
|
123
|
+
}
|
|
124
|
+
if (Object.keys(prior).sort().join(",") !== "nonce,pid,schemaVersion" ||
|
|
125
|
+
!["nggaigc-install-lock/v1", own.schemaVersion].includes(prior.schemaVersion) ||
|
|
126
|
+
!Number.isSafeInteger(prior.pid) ||
|
|
127
|
+
prior.pid < 1 ||
|
|
128
|
+
typeof prior.nonce !== "string" ||
|
|
129
|
+
!noncePattern.test(prior.nonce))
|
|
130
|
+
throw new CliError("INSTALL_BUSY");
|
|
131
|
+
try {
|
|
132
|
+
process.kill(prior.pid, 0);
|
|
133
|
+
throw new CliError("INSTALL_BUSY");
|
|
134
|
+
}
|
|
135
|
+
catch (signal) {
|
|
136
|
+
if (signal.code !== "ESRCH")
|
|
137
|
+
throw new CliError("INSTALL_BUSY");
|
|
138
|
+
}
|
|
139
|
+
// A v1 observer may already hold a stale snapshot and ignores our gate.
|
|
140
|
+
// Never replace a dead v1 lock automatically; require quiescent recovery.
|
|
141
|
+
if (prior.schemaVersion !== own.schemaVersion)
|
|
142
|
+
fail();
|
|
143
|
+
const again = await lstat(lock);
|
|
144
|
+
if (again.ino !== before.ino ||
|
|
145
|
+
again.size !== before.size ||
|
|
146
|
+
again.mtimeMs !== before.mtimeMs)
|
|
147
|
+
throw new CliError("INSTALL_BUSY");
|
|
148
|
+
await rm(lock);
|
|
149
|
+
try {
|
|
150
|
+
fd = await open(lock, "wx", 0o600);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new CliError("INSTALL_BUSY");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await fd.writeFile(JSON.stringify(own) + "\n");
|
|
157
|
+
await fd.sync();
|
|
158
|
+
await syncDirectory(skills);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// Release only the short acquisition gate. The main lock covers action.
|
|
162
|
+
// SIGKILL during action therefore leaves the original recoverable lock.
|
|
163
|
+
await releaseGate();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
await fd?.close();
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await action();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await fd.close();
|
|
175
|
+
try {
|
|
176
|
+
const v = JSON.parse(await readFile(lock, "utf8"));
|
|
177
|
+
if (v.pid === own.pid && v.nonce === own.nonce) {
|
|
178
|
+
await rm(lock);
|
|
179
|
+
await syncDirectory(skills);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* Never remove an untrusted replacement. */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function paths(skills, j) {
|
|
188
|
+
return {
|
|
189
|
+
target: path.join(skills, "nggaigc"),
|
|
190
|
+
stage: path.join(skills, `.nggaigc-verified-stage-${j.nonce}`),
|
|
191
|
+
backup: path.join(skills, `.nggaigc-verified-backup-${j.nonce}`),
|
|
192
|
+
rejected: path.join(skills, `.nggaigc-verified-rejected-${j.nonce}`),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function valid(directory, bundle) {
|
|
196
|
+
try {
|
|
197
|
+
await verifyInstalledSkill(directory, bundle.files, bundle.executableFiles);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function finish(skills, j, outcome) {
|
|
205
|
+
const done = path.join(skills, `.nggaigc-root-${outcome}-${j.nonce}.json`);
|
|
206
|
+
if (await exists(done))
|
|
207
|
+
fail();
|
|
208
|
+
await rename(path.join(skills, journalName), done);
|
|
209
|
+
await syncDirectory(skills);
|
|
210
|
+
}
|
|
211
|
+
/** Call only while holding withRootLock. Recovery observes the original journal, never starts a new transaction. */
|
|
212
|
+
export async function recoverRoot(skills, vendor = rootVendor) {
|
|
213
|
+
const file = path.join(skills, journalName);
|
|
214
|
+
if (!(await exists(file)))
|
|
215
|
+
return;
|
|
216
|
+
const info = await lstat(file);
|
|
217
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
218
|
+
fail();
|
|
219
|
+
let j;
|
|
220
|
+
try {
|
|
221
|
+
j = JSON.parse(await readFile(file, "utf8"));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
fail();
|
|
225
|
+
}
|
|
226
|
+
if (Object.keys(j).sort().join(",") !==
|
|
227
|
+
"archiveSha256,from,nonce,schemaVersion,to" ||
|
|
228
|
+
j.schemaVersion !== "nggaigc-root-upgrade/v1" ||
|
|
229
|
+
!noncePattern.test(j.nonce) ||
|
|
230
|
+
!supported.includes(j.to) ||
|
|
231
|
+
(j.from !== null && !supported.includes(j.from)) ||
|
|
232
|
+
j.from === j.to ||
|
|
233
|
+
!/^[a-f0-9]{64}$/.test(j.archiveSha256))
|
|
234
|
+
fail();
|
|
235
|
+
const p = paths(skills, j), next = await knownBundle(skills, j.to, vendor);
|
|
236
|
+
if (next.sha256 !== j.archiveSha256)
|
|
237
|
+
fail();
|
|
238
|
+
if (await valid(p.target, next)) {
|
|
239
|
+
await finish(skills, j, "committed");
|
|
240
|
+
return "committed";
|
|
241
|
+
}
|
|
242
|
+
if (j.from !== null) {
|
|
243
|
+
const previous = await knownBundle(skills, j.from, vendor);
|
|
244
|
+
if (await valid(p.target, previous)) {
|
|
245
|
+
await finish(skills, j, "restored");
|
|
246
|
+
return "restored";
|
|
247
|
+
}
|
|
248
|
+
if (!(await valid(p.backup, previous)))
|
|
249
|
+
fail();
|
|
250
|
+
if (await exists(p.target)) {
|
|
251
|
+
if (await exists(p.rejected))
|
|
252
|
+
fail();
|
|
253
|
+
await rename(p.target, p.rejected);
|
|
254
|
+
await syncDirectory(skills);
|
|
255
|
+
}
|
|
256
|
+
await rename(p.backup, p.target);
|
|
257
|
+
await syncDirectory(skills);
|
|
258
|
+
await verifyInstalledSkill(p.target, previous.files, previous.executableFiles);
|
|
259
|
+
await finish(skills, j, "restored");
|
|
260
|
+
return "restored";
|
|
261
|
+
}
|
|
262
|
+
// A first installation has no previous state. Only its fully verified stage may be committed.
|
|
263
|
+
if ((await exists(p.target)) || !(await valid(p.stage, next)))
|
|
264
|
+
fail();
|
|
265
|
+
await rename(p.stage, p.target);
|
|
266
|
+
await syncDirectory(skills);
|
|
267
|
+
await verifyInstalledSkill(p.target, next.files, next.executableFiles);
|
|
268
|
+
await finish(skills, j, "committed");
|
|
269
|
+
return "committed";
|
|
270
|
+
}
|
|
271
|
+
export async function recoverRootForUse(skills, vendor = rootVendor) {
|
|
272
|
+
if ((await recoverRoot(skills, vendor)) === "restored")
|
|
273
|
+
throw new CliError("ROOT_UPGRADE_RESTORED");
|
|
274
|
+
}
|
|
275
|
+
/** Caller holds the shared lock. No signed source, credential or run state is changed. */
|
|
276
|
+
export async function installVerifiedRootLocked(skills, bundle, options = {}) {
|
|
277
|
+
const vendor = options.vendor ?? rootVendor;
|
|
278
|
+
await recoverRootForUse(skills, vendor);
|
|
279
|
+
for (const name of await readdir(skills))
|
|
280
|
+
if (/^\.nggaigc-pair(?:-v[23])?-(?:transaction|stage)/.test(name))
|
|
281
|
+
throw new CliError("SKILL_PAIR_RECOVERY_REQUIRED");
|
|
282
|
+
const target = path.join(skills, "nggaigc");
|
|
283
|
+
const old = (await exists(target)) ? await trustedRoot(skills, vendor) : null;
|
|
284
|
+
if (old?.version === bundle.version)
|
|
285
|
+
return "unchanged";
|
|
286
|
+
if (!supported.includes(bundle.version) ||
|
|
287
|
+
(old && !supported.includes(old.version)))
|
|
288
|
+
throw new CliError("ROOT_UNAVAILABLE");
|
|
289
|
+
if (old)
|
|
290
|
+
await saveProof(skills, old.bundle, vendor);
|
|
291
|
+
await saveProof(skills, bundle, vendor);
|
|
292
|
+
const j = {
|
|
293
|
+
schemaVersion: "nggaigc-root-upgrade/v1",
|
|
294
|
+
nonce: randomBytes(16).toString("hex"),
|
|
295
|
+
from: old?.version ?? null,
|
|
296
|
+
to: bundle.version,
|
|
297
|
+
archiveSha256: bundle.sha256,
|
|
298
|
+
};
|
|
299
|
+
const p = paths(skills, j), extracted = path.join(skills, `.nggaigc-verified-source-${j.nonce}`);
|
|
300
|
+
try {
|
|
301
|
+
await extractBundle(bundle.archive, extracted, bundle.sha256);
|
|
302
|
+
await verifyExtractedBundle(extracted, bundle.files);
|
|
303
|
+
const require = createRequire(import.meta.url);
|
|
304
|
+
const installer = require(path.join(extracted, "installer/install.cjs"));
|
|
305
|
+
if (typeof installer.copyTree !== "function")
|
|
306
|
+
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
307
|
+
await mkdir(p.stage, { mode: 0o700 });
|
|
308
|
+
await installer.copyTree(path.join(extracted, "skills/nggaigc"), p.stage);
|
|
309
|
+
await durableJson(path.join(p.stage, ".nggaigc-install.json"), {
|
|
310
|
+
schemaVersion: "nggaigc-local-install/v1",
|
|
311
|
+
version: bundle.version,
|
|
312
|
+
installedAt: new Date().toISOString(),
|
|
313
|
+
});
|
|
314
|
+
await options.step?.("stage-copied", p);
|
|
315
|
+
await verifyInstalledSkill(p.stage, bundle.files, bundle.executableFiles);
|
|
316
|
+
// Flush all payloads before recording intent; directory renames then survive process interruption.
|
|
317
|
+
for (const name of Object.keys(bundle.files).filter((x) => x.startsWith("skills/nggaigc/"))) {
|
|
318
|
+
// Flush existing staged payloads without truncating or creating files.
|
|
319
|
+
const fd = await open(path.join(p.stage, name.slice(15)), "r+");
|
|
320
|
+
try {
|
|
321
|
+
await fd.sync();
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await fd.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const syncTree = async (directory) => {
|
|
328
|
+
for (const entry of await readdir(directory, { withFileTypes: true }))
|
|
329
|
+
if (entry.isDirectory())
|
|
330
|
+
await syncTree(path.join(directory, entry.name));
|
|
331
|
+
await syncDirectory(directory);
|
|
332
|
+
};
|
|
333
|
+
await syncTree(p.stage);
|
|
334
|
+
await durableJson(path.join(skills, journalName), j);
|
|
335
|
+
if (old) {
|
|
336
|
+
await verifyInstalledSkill(target, old.bundle.files, old.bundle.executableFiles);
|
|
337
|
+
await rename(target, p.backup);
|
|
338
|
+
await syncDirectory(skills);
|
|
339
|
+
await options.step?.("old-renamed", p);
|
|
340
|
+
}
|
|
341
|
+
await rename(p.stage, target);
|
|
342
|
+
await syncDirectory(skills);
|
|
343
|
+
await options.step?.("new-renamed", p);
|
|
344
|
+
await verifyInstalledSkill(target, bundle.files, bundle.executableFiles);
|
|
345
|
+
await options.step?.("post-verified", p);
|
|
346
|
+
await finish(skills, j, "committed");
|
|
347
|
+
return old ? "updated" : "installed";
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (await exists(path.join(skills, journalName))) {
|
|
351
|
+
try {
|
|
352
|
+
await recoverRoot(skills, vendor);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
throw new CliError("ROOT_RECOVERY_REQUIRED");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await rm(extracted, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export async function ensureCurrentRootLocked(skills, vendor = rootVendor) {
|
|
365
|
+
await installVerifiedRootLocked(skills, await knownBundle(skills, "1.1.50", vendor), { vendor });
|
|
366
|
+
}
|
package/dist/skill-command.js
CHANGED
|
@@ -4,30 +4,32 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { resolveSkillLocation, } from "./skill-location.js";
|
|
7
|
-
import {
|
|
7
|
+
import { CliError } from "./errors.js";
|
|
8
|
+
import { trustedKeyId, trustedKeySpkiSha256 } from "./bundle.js";
|
|
9
|
+
import { trustedRoot, trustedRuntime, exists } from "./root-store.js";
|
|
10
|
+
import { withRootLock, recoverRootForUse, ensureCurrentRootLocked, } from "./root-upgrade.js";
|
|
11
|
+
import { canonical, resolveStartAlias } from "./start-compatibility.js";
|
|
8
12
|
import { IndependentSkillError, projectActiveSkillCatalog, startIndependentSkill, } from "./independent-skill.js";
|
|
13
|
+
function compare(a, b) {
|
|
14
|
+
const x = a.split(".").map(Number), y = b.split(".").map(Number);
|
|
15
|
+
for (let i = 0; i < 3; i++) {
|
|
16
|
+
const d = x[i] - y[i];
|
|
17
|
+
if (d)
|
|
18
|
+
return d;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
9
22
|
function fail(code) {
|
|
10
23
|
throw new IndependentSkillError(code);
|
|
11
24
|
}
|
|
12
25
|
function record(value) {
|
|
13
26
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
27
|
}
|
|
15
|
-
|
|
28
|
+
async function productionDependenciesLocked(skillsDirectory, verifiedLauncher, runtimeVersion, createRuntime = true) {
|
|
16
29
|
const vendor = fileURLToPath(new URL("../vendor/", import.meta.url));
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
trust: path.join(vendor, "trusted-keys.json"),
|
|
21
|
-
});
|
|
22
|
-
const root = path.join(skillsDirectory, "nggaigc");
|
|
23
|
-
await verifyInstalledSkill(root, bundle.files);
|
|
24
|
-
const source = JSON.parse(await readFile(path.join(root, "references", "source.json"), "utf8"));
|
|
25
|
-
if (!record(source) ||
|
|
26
|
-
source.id !== "skill.nggaigc" ||
|
|
27
|
-
typeof source.version !== "string" ||
|
|
28
|
-
!/^\d+\.\d+\.\d+$/.test(source.version) ||
|
|
29
|
-
source.clientProtocol !== 1)
|
|
30
|
-
fail("ROOT_UNAVAILABLE");
|
|
30
|
+
const current = await trustedRoot(skillsDirectory);
|
|
31
|
+
const installed = await trustedRuntime(skillsDirectory, runtimeVersion ?? current.version, undefined, createRuntime);
|
|
32
|
+
const root = installed.directory;
|
|
31
33
|
const keys = JSON.parse(await readFile(path.join(vendor, "trusted-keys.json"), "utf8"));
|
|
32
34
|
if (!record(keys) ||
|
|
33
35
|
keys.schemaVersion !== "ngg-trusted-package-keys/v1" ||
|
|
@@ -45,30 +47,108 @@ export async function productionDependencies(skillsDirectory) {
|
|
|
45
47
|
if (fingerprint !== trustedKeySpkiSha256)
|
|
46
48
|
fail("TRUST_UNAVAILABLE");
|
|
47
49
|
const require = createRequire(import.meta.url);
|
|
50
|
+
for (const cached of Object.keys(require.cache))
|
|
51
|
+
if (cached.startsWith(root + path.sep))
|
|
52
|
+
delete require.cache[cached];
|
|
48
53
|
const launcher = require(path.join(root, "scripts", "launch-ngg-client.cjs"));
|
|
49
54
|
if (!record(launcher) || typeof launcher.main !== "function")
|
|
50
55
|
fail("ROOT_UNAVAILABLE");
|
|
51
56
|
return {
|
|
52
|
-
launcher:
|
|
57
|
+
launcher: verifiedLauncher?.({ version: installed.version, directory: root }) ??
|
|
58
|
+
launcher,
|
|
53
59
|
trust: {
|
|
54
60
|
keyId: trustedKeyId,
|
|
55
61
|
publicKeyPem: match.publicKeyPem,
|
|
56
62
|
spkiSha256: fingerprint,
|
|
57
63
|
},
|
|
58
|
-
rootSkillVersion:
|
|
64
|
+
rootSkillVersion: installed.version,
|
|
65
|
+
rootLauncherPath: path.join(root, "scripts/launch-ngg-client.cjs"),
|
|
59
66
|
};
|
|
60
67
|
}
|
|
68
|
+
/** Read-only fast path for existing immutable runtimes; only recovery/cache creation locks. */
|
|
69
|
+
export async function productionDependencies(skillsDirectory, verifiedLauncher, runtimeVersion) {
|
|
70
|
+
if (!(await exists(path.join(skillsDirectory, ".nggaigc-root-upgrade.json")))) {
|
|
71
|
+
try {
|
|
72
|
+
return await productionDependenciesLocked(skillsDirectory, verifiedLauncher, runtimeVersion, false);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof CliError) ||
|
|
76
|
+
error.code !== "ROOT_RUNTIME_UNAVAILABLE")
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return withRootLock(skillsDirectory, async () => {
|
|
81
|
+
await recoverRootForUse(skillsDirectory);
|
|
82
|
+
return productionDependenciesLocked(skillsDirectory, verifiedLauncher, runtimeVersion, true);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function upgradeRoot(skillsDirectory) {
|
|
86
|
+
await withRootLock(skillsDirectory, () => ensureCurrentRootLocked(skillsDirectory));
|
|
87
|
+
}
|
|
88
|
+
function catalogRootVersion(snapshot) {
|
|
89
|
+
if (!record(snapshot) || !Array.isArray(snapshot.entries))
|
|
90
|
+
fail("CATALOG_INVALID");
|
|
91
|
+
const roots = snapshot.entries.filter((e) => record(e) &&
|
|
92
|
+
e.id === "skill.nggaigc" &&
|
|
93
|
+
record(e.manifest) &&
|
|
94
|
+
e.manifest.role === "root_skill" &&
|
|
95
|
+
record(e.manifest.lifecycle) &&
|
|
96
|
+
e.manifest.lifecycle.status === "published");
|
|
97
|
+
if (roots.length !== 1 ||
|
|
98
|
+
!["1.1.49", "1.1.50"].includes(String(roots[0].version)))
|
|
99
|
+
fail("ROOT_UNAVAILABLE");
|
|
100
|
+
return String(roots[0].version);
|
|
101
|
+
}
|
|
61
102
|
/** Generic bridge only: the verified launcher owns command and API contracts. */
|
|
62
103
|
export async function runClientCommand(args, options = {}, launcherOptions = {}) {
|
|
63
104
|
if (args.length === 0)
|
|
64
105
|
fail("INVALID_INPUT");
|
|
65
106
|
const { skillsDirectory } = resolveSkillLocation(options);
|
|
66
|
-
const
|
|
67
|
-
|
|
107
|
+
const execute = async () => {
|
|
108
|
+
let { launcher, rootSkillVersion } = await productionDependencies(skillsDirectory, options.verifiedLauncher);
|
|
109
|
+
if ([
|
|
110
|
+
"preflight",
|
|
111
|
+
"connect",
|
|
112
|
+
"reconnect",
|
|
113
|
+
"login",
|
|
114
|
+
"relogin",
|
|
115
|
+
"status",
|
|
116
|
+
"disconnect",
|
|
117
|
+
].includes(args[0]))
|
|
118
|
+
return launcher.main(args, launcherOptions);
|
|
119
|
+
// Catalog read is a generic bootstrap operation. Login/offline diagnostics may
|
|
120
|
+
// continue when it is unavailable; all remote actions retain server admission.
|
|
121
|
+
let snapshot;
|
|
122
|
+
try {
|
|
123
|
+
snapshot = await launcher.main(["catalog-snapshot"], launcherOptions);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* Keep the existing connection/recovery contract. */
|
|
127
|
+
}
|
|
128
|
+
if (snapshot !== undefined) {
|
|
129
|
+
projectActiveSkillCatalog(snapshot);
|
|
130
|
+
if (!record(snapshot) || !Array.isArray(snapshot.entries))
|
|
131
|
+
fail("CATALOG_INVALID");
|
|
132
|
+
const target = catalogRootVersion(snapshot);
|
|
133
|
+
if (target !== rootSkillVersion) {
|
|
134
|
+
if (compare(target, rootSkillVersion) > 0)
|
|
135
|
+
await upgradeRoot(skillsDirectory);
|
|
136
|
+
const updated = await productionDependencies(skillsDirectory, options.verifiedLauncher, target);
|
|
137
|
+
launcher = updated.launcher;
|
|
138
|
+
const fresh = await launcher.main(["catalog-snapshot"], launcherOptions);
|
|
139
|
+
projectActiveSkillCatalog(fresh);
|
|
140
|
+
if (canonical(fresh) !== canonical(snapshot))
|
|
141
|
+
fail("CATALOG_CHANGED");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return launcher.main(args, launcherOptions);
|
|
145
|
+
};
|
|
146
|
+
return execute();
|
|
68
147
|
}
|
|
69
148
|
/** Candidate npm entry: active catalog → server admission → verified local Skill path. */
|
|
70
149
|
export async function runSkillStartCommand(skillId, skillVersion, options = {}) {
|
|
71
|
-
if (
|
|
150
|
+
if ((options.exactVersion && skillVersion === undefined) ||
|
|
151
|
+
!/^skill\.[a-z0-9][a-z0-9.-]{0,79}$/.test(skillId) ||
|
|
72
152
|
(skillVersion !== undefined && !/^\d+\.\d+\.\d+$/.test(skillVersion)))
|
|
73
153
|
fail("INVALID_INPUT");
|
|
74
154
|
const configuredDirectory = resolveSkillLocation(options).skillsDirectory;
|
|
@@ -77,53 +157,78 @@ export async function runSkillStartCommand(skillId, skillVersion, options = {})
|
|
|
77
157
|
? configuredDirectory
|
|
78
158
|
: fail("INSTALL_PATH_UNSAFE"))
|
|
79
159
|
: configuredDirectory;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
160
|
+
const execute = async () => {
|
|
161
|
+
const production = options.launcher && options.trust && options.rootSkillVersion
|
|
162
|
+
? null
|
|
163
|
+
: await productionDependencies(skillsDirectory, options.verifiedLauncher);
|
|
164
|
+
let launcher = options.launcher ?? production.launcher;
|
|
165
|
+
const trust = options.trust ?? production.trust;
|
|
166
|
+
let rootSkillVersion = options.rootSkillVersion ?? production.rootSkillVersion;
|
|
167
|
+
const platformVersion = options.platformVersion ??
|
|
168
|
+
JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
169
|
+
let snapshot;
|
|
170
|
+
try {
|
|
171
|
+
snapshot = await launcher.main(["catalog-snapshot"], options.launcherOptions);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
fail("CATALOG_UNAVAILABLE");
|
|
175
|
+
}
|
|
176
|
+
const index = projectActiveSkillCatalog(snapshot);
|
|
177
|
+
const migration = await resolveStartAlias(snapshot, index, skillId, skillVersion, options.migrationTrust ?? trust, options);
|
|
178
|
+
const resolvedVersion = migration?.toVersion ?? skillVersion;
|
|
179
|
+
const matches = index.entries.filter((entry) => entry.id === skillId &&
|
|
180
|
+
entry.state === "active" &&
|
|
181
|
+
entry.sourceManifest?.role === "independent_skill" &&
|
|
182
|
+
(resolvedVersion === undefined || entry.version === resolvedVersion));
|
|
183
|
+
if (matches.length !== 1)
|
|
184
|
+
fail("SKILL_UNAVAILABLE");
|
|
185
|
+
const selected = matches[0];
|
|
186
|
+
let rootLauncherPath = production?.rootLauncherPath ??
|
|
187
|
+
path.join(skillsDirectory, "nggaigc/scripts/launch-ngg-client.cjs");
|
|
188
|
+
if (production) {
|
|
189
|
+
const target = catalogRootVersion(snapshot);
|
|
190
|
+
if (target !== rootSkillVersion) {
|
|
191
|
+
if (compare(target, rootSkillVersion) > 0)
|
|
192
|
+
await upgradeRoot(skillsDirectory);
|
|
193
|
+
const updated = await productionDependencies(skillsDirectory, options.verifiedLauncher, target);
|
|
194
|
+
launcher = updated.launcher;
|
|
195
|
+
rootSkillVersion = updated.rootSkillVersion;
|
|
196
|
+
rootLauncherPath = updated.rootLauncherPath;
|
|
197
|
+
const fresh = await launcher.main(["catalog-snapshot"], options.launcherOptions);
|
|
198
|
+
projectActiveSkillCatalog(fresh);
|
|
199
|
+
if (canonical(fresh) !== canonical(snapshot))
|
|
200
|
+
fail("CATALOG_CHANGED");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const result = await startIndependentSkill({
|
|
204
|
+
index,
|
|
205
|
+
skillId: selected.id,
|
|
206
|
+
skillVersion: selected.version,
|
|
207
|
+
rootSkillVersion,
|
|
208
|
+
platformVersion,
|
|
209
|
+
skillsDirectory,
|
|
210
|
+
trust,
|
|
211
|
+
fetcher: options.fetcher,
|
|
212
|
+
admit: async (identity) => {
|
|
213
|
+
const answer = await launcher.main([
|
|
214
|
+
"skill-admit",
|
|
215
|
+
identity.kind,
|
|
216
|
+
identity.id,
|
|
217
|
+
identity.version,
|
|
218
|
+
identity.manifestSha256,
|
|
219
|
+
identity.releaseId,
|
|
220
|
+
], options.launcherOptions);
|
|
221
|
+
return answer;
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
...(skillVersion
|
|
227
|
+
? { requestedVersion: skillVersion, resolvedVersion: selected.version }
|
|
228
|
+
: {}),
|
|
229
|
+
skillsDirectory,
|
|
230
|
+
rootLauncherPath,
|
|
231
|
+
};
|
|
128
232
|
};
|
|
233
|
+
return execute();
|
|
129
234
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Signed, finite aliases for new starts only. Never used for accepted run recovery. */
|
|
2
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { IndependentSkillError, } from "./independent-skill.js";
|
|
5
|
+
export const startCompatibilityDomain = "NGGAIGC/START-COMPATIBILITY/v1\0";
|
|
6
|
+
export function canonical(value) {
|
|
7
|
+
if (value === null || typeof value !== "object")
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
11
|
+
return `{${Object.keys(value)
|
|
12
|
+
.sort()
|
|
13
|
+
.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`)
|
|
14
|
+
.join(",")}}`;
|
|
15
|
+
}
|
|
16
|
+
export function startCompatibilityInput(payload) {
|
|
17
|
+
return Buffer.from(startCompatibilityDomain + canonical(payload), "utf8");
|
|
18
|
+
}
|
|
19
|
+
function record(v) {
|
|
20
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
function keys(v, expected) {
|
|
23
|
+
return Object.keys(v).sort().join(",") === expected.sort().join(",");
|
|
24
|
+
}
|
|
25
|
+
function fail() {
|
|
26
|
+
throw new IndependentSkillError("START_COMPATIBILITY_INVALID");
|
|
27
|
+
}
|
|
28
|
+
export function verifyStartCompatibility(value, trust) {
|
|
29
|
+
if (!record(value) ||
|
|
30
|
+
!keys(value, ["payload", "signature"]) ||
|
|
31
|
+
!record(value.payload) ||
|
|
32
|
+
!record(value.signature))
|
|
33
|
+
fail();
|
|
34
|
+
const p = value.payload, s = value.signature;
|
|
35
|
+
if (!keys(p, ["schemaVersion", "catalog", "contract", "aliases"]) ||
|
|
36
|
+
p.schemaVersion !== "nggaigc-start-compatibility/v1" ||
|
|
37
|
+
p.contract !== "same-skill-inputs-pricing-permissions" ||
|
|
38
|
+
!record(p.catalog) ||
|
|
39
|
+
!keys(p.catalog, ["releaseId", "catalogVersion", "releaseSha256"]) ||
|
|
40
|
+
typeof p.catalog.releaseId !== "string" ||
|
|
41
|
+
typeof p.catalog.catalogVersion !== "string" ||
|
|
42
|
+
typeof p.catalog.releaseSha256 !== "string" ||
|
|
43
|
+
!/^[a-f0-9]{64}$/.test(p.catalog.releaseSha256) ||
|
|
44
|
+
!Array.isArray(p.aliases) ||
|
|
45
|
+
!p.aliases.length ||
|
|
46
|
+
p.aliases.length > 128)
|
|
47
|
+
fail();
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const a of p.aliases) {
|
|
50
|
+
if (!record(a) ||
|
|
51
|
+
!keys(a, [
|
|
52
|
+
"id",
|
|
53
|
+
"fromVersion",
|
|
54
|
+
"fromManifestSha256",
|
|
55
|
+
"toVersion",
|
|
56
|
+
"toManifestSha256",
|
|
57
|
+
]) ||
|
|
58
|
+
typeof a.id !== "string" ||
|
|
59
|
+
!/^skill\.[a-z0-9][a-z0-9.-]{0,79}$/.test(a.id) ||
|
|
60
|
+
typeof a.fromVersion !== "string" ||
|
|
61
|
+
typeof a.toVersion !== "string" ||
|
|
62
|
+
!/^\d+\.\d+\.\d+$/.test(a.fromVersion) ||
|
|
63
|
+
!/^\d+\.\d+\.\d+$/.test(a.toVersion) ||
|
|
64
|
+
a.fromVersion === a.toVersion ||
|
|
65
|
+
typeof a.fromManifestSha256 !== "string" ||
|
|
66
|
+
typeof a.toManifestSha256 !== "string" ||
|
|
67
|
+
!/^[a-f0-9]{64}$/.test(a.fromManifestSha256) ||
|
|
68
|
+
!/^[a-f0-9]{64}$/.test(a.toManifestSha256) ||
|
|
69
|
+
seen.has(`${a.id}@${a.fromVersion}`))
|
|
70
|
+
fail();
|
|
71
|
+
seen.add(`${a.id}@${a.fromVersion}`);
|
|
72
|
+
}
|
|
73
|
+
if (!keys(s, ["algorithm", "keyId", "scope", "value"]) ||
|
|
74
|
+
s.algorithm !== "ed25519" ||
|
|
75
|
+
s.keyId !== trust.keyId ||
|
|
76
|
+
s.scope !== "nggaigc-start-compatibility/v1" ||
|
|
77
|
+
typeof s.value !== "string" ||
|
|
78
|
+
!/^[A-Za-z0-9+/]{86}==$/.test(s.value))
|
|
79
|
+
fail();
|
|
80
|
+
try {
|
|
81
|
+
const key = createPublicKey(trust.publicKeyPem);
|
|
82
|
+
if (createHash("sha256")
|
|
83
|
+
.update(key.export({ type: "spki", format: "der" }))
|
|
84
|
+
.digest("hex") !== trust.spkiSha256 ||
|
|
85
|
+
!verify(null, startCompatibilityInput(p), key, Buffer.from(s.value, "base64")))
|
|
86
|
+
fail();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
fail();
|
|
90
|
+
}
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
export async function resolveStartAlias(snapshot, index, id, version, trust, options = {}) {
|
|
94
|
+
if (version === undefined ||
|
|
95
|
+
index.entries.some((e) => e.id === id && e.version === version && e.state === "active"))
|
|
96
|
+
return;
|
|
97
|
+
if (options.exactVersion)
|
|
98
|
+
throw new IndependentSkillError("SKILL_UNAVAILABLE");
|
|
99
|
+
let value = options.migrationPermit;
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
try {
|
|
102
|
+
value = JSON.parse(await readFile(new URL("../vendor/start-compatibility.json", import.meta.url), "utf8"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new IndependentSkillError("SKILL_UNAVAILABLE");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const permit = verifyStartCompatibility(value, trust);
|
|
109
|
+
if (!record(snapshot) ||
|
|
110
|
+
permit.catalog.releaseId !== snapshot.releaseId ||
|
|
111
|
+
permit.catalog.catalogVersion !== snapshot.catalogVersion ||
|
|
112
|
+
permit.catalog.releaseSha256 !== snapshot.releaseSha256)
|
|
113
|
+
throw new IndependentSkillError("START_COMPATIBILITY_CATALOG_MISMATCH");
|
|
114
|
+
const aliases = permit.aliases.filter((a) => a.id === id && a.fromVersion === version);
|
|
115
|
+
if (aliases.length !== 1)
|
|
116
|
+
throw new IndependentSkillError("SKILL_UNAVAILABLE");
|
|
117
|
+
const alias = aliases[0], targets = index.entries.filter((e) => e.id === id && e.state === "active");
|
|
118
|
+
if (targets.length !== 1 ||
|
|
119
|
+
targets[0].version !== alias.toVersion ||
|
|
120
|
+
targets[0].manifestSha256 !== alias.toManifestSha256)
|
|
121
|
+
fail();
|
|
122
|
+
return alias;
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "nggaigc-plugin-package/v1",
|
|
3
|
+
"artifactId": "plugin.nggaigc",
|
|
4
|
+
"version": "1.1.49",
|
|
5
|
+
"sourceCommit": "69db1c015defab063dc6ac590c3191c3f2accf13",
|
|
6
|
+
"artifactPath": "packages/plugins/nggaigc-plugin-1.1.49.zip",
|
|
7
|
+
"sha256": "8f102ea49ea1123c44d2e199fb6f03a520a85890735a3dc88bbbf9718ec8dc65",
|
|
8
|
+
"sizeBytes": 508722,
|
|
9
|
+
"fileCount": 24,
|
|
10
|
+
"platforms": [
|
|
11
|
+
"win32-x64",
|
|
12
|
+
"win32-arm64",
|
|
13
|
+
"darwin-arm64"
|
|
14
|
+
],
|
|
15
|
+
"files": {
|
|
16
|
+
".codex-plugin/plugin.json": "ff760760b31838bcf605fc6239c02dd5efbe08e5c66ca2999ab63a75cf535ef0",
|
|
17
|
+
"installer/install.cjs": "76f4ed73233fd39900fb0bedbc434dea39e52e6b6b4622f3fd388a5ca09c6f62",
|
|
18
|
+
"installer/legacy-root-digests.json": "922552646a5aa6cff8616c372912a37b1d5f263351206b85785ed9b2eae31641",
|
|
19
|
+
"installer/signed-pair-1.1.16.json": "ccb22a90302cc5c9c585067ebc211a27b0c9d8a177c2740a5678a58a3d70bfe9",
|
|
20
|
+
"installer/signed-pair-1.1.17.json": "a3697506c929aaa085a6d994fc3a1b9a87e522ec6e1e32f3f760e6a534effa2b",
|
|
21
|
+
"migration/seedream-1.1.3/agents/openai.yaml": "3cbf034ddccd64a7972acd37e7433683a3944f3d8c7b5bdccbbd70e517bf5a8c",
|
|
22
|
+
"migration/seedream-1.1.3/references/image-generation.md": "208389b3ea4647040232c65a9b91ebcc20787818f57535f16664a7c4db842b3f",
|
|
23
|
+
"migration/seedream-1.1.3/references/source.json": "75998d079a229435d1097b00bd008703bd38026fc6485c312254925bf55398a8",
|
|
24
|
+
"migration/seedream-1.1.3/scripts/seedream.mjs": "dcc6af4d945790d9dfd7fac21c60934759d7ac2576a14507d0299f4833942a96",
|
|
25
|
+
"migration/seedream-1.1.3/SKILL.md": "5c5cf3c2d99376f246425e873c5566436ed528256372ac3208abb60364c4517c",
|
|
26
|
+
"skills/nggaigc/agents/openai.yaml": "8936d97b9821e687e224e9349cc42be2ee5fc20988562efffbcb70080aedf87c",
|
|
27
|
+
"skills/nggaigc/references/catalog-and-updates.md": "a43bee94a480aa2bf0b1e7038cee6f27fb86d2bd8fa88a28a63678963d7c1530",
|
|
28
|
+
"skills/nggaigc/references/source.json": "64ac80a785146b993aace78f00e88bdd6a571957d019f8ac093264f8c59b9210",
|
|
29
|
+
"skills/nggaigc/scripts/api_client.mjs": "e16bf750a297ef7e88e4843d2fba9c33b412a66516bfe76822d7deec6c153069",
|
|
30
|
+
"skills/nggaigc/scripts/api_package.mjs": "824ea8f935e2c3aec7ad5ea5c6ad043f1b77ccd9918c86687a9bd61558954fe4",
|
|
31
|
+
"skills/nggaigc/scripts/bootstrap-public.json": "1c5ef86f600dc35543db9f20fe0142fc97ff7f2940b585eb2a27860e3328e83f",
|
|
32
|
+
"skills/nggaigc/scripts/launch-ngg-client.cjs": "97c3fb740eba0c0a3dc3ef9f24f3cbb201cd2e1b3209d14b782744bcc083e232",
|
|
33
|
+
"skills/nggaigc/scripts/macos-keychain": "b0bdf29d14219d38ca18c7e5c599deb0daa8c8ac70b0babbc7d4fb23673079ec",
|
|
34
|
+
"skills/nggaigc/scripts/macos-keychain.swift": "1b57e74fde66c824fe970547dd8ca0883a3334c34c065e85a8a2a90d4556a7f5",
|
|
35
|
+
"skills/nggaigc/scripts/ngg_client.mjs": "43ce6697bd15f2bfef089031e33eab73ef1f3883cc724962f1e82731c676ef0c",
|
|
36
|
+
"skills/nggaigc/scripts/ngg-session.mjs": "dbb69158307e3726a567ae65987f61d6ad15f75e3c6e2d41bf43bc50f15d91ec",
|
|
37
|
+
"skills/nggaigc/scripts/supported-platforms.json": "ec8b0754c489e04534a1d9880dd6664ba9c5bcd07789e06d138e0923f23575ff",
|
|
38
|
+
"skills/nggaigc/scripts/windows-credential.ps1": "641888d8c6016bb84f18df19315322ed802c5709fbe5209250eaf1b0ce498aec",
|
|
39
|
+
"skills/nggaigc/SKILL.md": "8cb3eb8a03cf144ce15fa01d7d342495c2b11ba0f6a0cf61a493e1e43c83a11f"
|
|
40
|
+
},
|
|
41
|
+
"signature": {
|
|
42
|
+
"algorithm": "ed25519",
|
|
43
|
+
"keyId": "ngg-signing-skill-release-1",
|
|
44
|
+
"scope": "artifact-bytes",
|
|
45
|
+
"value": "OTRlG+kA7/YmFdLHqdeGfoDftbYidQFCfZay4RSPWdaNmgpuIy32fT7s/u2GWbkAbUz8L1attssM6IPZUraZAg=="
|
|
46
|
+
}
|
|
47
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"payload": {
|
|
3
|
+
"schemaVersion": "nggaigc-start-compatibility/v1",
|
|
4
|
+
"catalog": {
|
|
5
|
+
"releaseId": "catalog-2026.11.02.3",
|
|
6
|
+
"catalogVersion": "1.10.168",
|
|
7
|
+
"releaseSha256": "11fa89a45efc9b859d26ae6398b97b3a7559c1d614eefae3ff59a9403f481950"
|
|
8
|
+
},
|
|
9
|
+
"contract": "same-skill-inputs-pricing-permissions",
|
|
10
|
+
"aliases": [
|
|
11
|
+
{
|
|
12
|
+
"id": "skill.h3-aqing-video",
|
|
13
|
+
"fromVersion": "0.1.3",
|
|
14
|
+
"fromManifestSha256": "aa9bfafce6e01c47b4d1c55cb4222374b7c86dfd0a7eb9b7fd4f75b8ce6725b3",
|
|
15
|
+
"toVersion": "0.1.4",
|
|
16
|
+
"toManifestSha256": "8a6f2c926938bd5e39f06ee8618f6a3618176aebc5d2fe891d0f2f57bb623df2"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "skill.h3-broll-video-assets",
|
|
20
|
+
"fromVersion": "0.1.9",
|
|
21
|
+
"fromManifestSha256": "d4f3a01ce66879e668935af9ccba225ca08ca9b123296e8f7e6144a2beb71674",
|
|
22
|
+
"toVersion": "0.1.10",
|
|
23
|
+
"toManifestSha256": "14b2d3996d927f2b3c995c3e827a33e990bcca923aa64f84c905b24b3badcfe4"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "skill.image-singing-digital-human",
|
|
27
|
+
"fromVersion": "0.1.3",
|
|
28
|
+
"fromManifestSha256": "e9ae6ca29c8711afceccd423f260b249c6744582ba98cbcbc5bcfc89187a2c11",
|
|
29
|
+
"toVersion": "0.1.4",
|
|
30
|
+
"toManifestSha256": "941ca6ea1cd72540adef38f57f6b09b7e7635c72760f9c4eaa4a92e4fc86465a"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "skill.image-singing-video",
|
|
34
|
+
"fromVersion": "0.1.2",
|
|
35
|
+
"fromManifestSha256": "6ef089b81c8da8a1039faeaff80a4559815e07bf46a11482003a191d8656187d",
|
|
36
|
+
"toVersion": "0.1.3",
|
|
37
|
+
"toManifestSha256": "a4ec99e3478b100c4abc1662e6cc01b8e41bb01f6653643d20e9910b8ff8b6c8"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "skill.knowledge-narration-video",
|
|
41
|
+
"fromVersion": "0.1.7",
|
|
42
|
+
"fromManifestSha256": "076755d1414537fc46899cab8bae79c25e3607a923cff9df35c2b55c3f5c1c0b",
|
|
43
|
+
"toVersion": "0.1.8",
|
|
44
|
+
"toManifestSha256": "32be1dd0d3b59b575239eaf61a4b78226a17a9db757c1b1eb51559a01f946b15"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "skill.ngg-depth-dance-copy",
|
|
48
|
+
"fromVersion": "0.2.1",
|
|
49
|
+
"fromManifestSha256": "efb34854dd2d5ab5e8d33363ca1ce93c69c7743cac143c16c34c360a8bab89fd",
|
|
50
|
+
"toVersion": "0.2.2",
|
|
51
|
+
"toManifestSha256": "6eca6fc0897875ba56c0cb460052f7f490f6833c61164daf3dfc22b4e9530fd9"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "skill.ngg-drama-workflow",
|
|
55
|
+
"fromVersion": "0.3.4",
|
|
56
|
+
"fromManifestSha256": "04fdfac21e7010b4268e02b3d2424d9fb1b9359e88461464967565a09e8ec196",
|
|
57
|
+
"toVersion": "0.3.5",
|
|
58
|
+
"toManifestSha256": "1e94cabce4f46f7c4dec439e18ec66b19274a71516384379f92477e67275ca88"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "skill.video-lipsync",
|
|
62
|
+
"fromVersion": "0.1.1",
|
|
63
|
+
"fromManifestSha256": "4d4ed19d17c56a0de549e1516c76b1c359749bfcd39dd52b7b24f216d5b68733",
|
|
64
|
+
"toVersion": "0.1.2",
|
|
65
|
+
"toManifestSha256": "cce3409eba007acb6901cf01cf5bf57167fa04b22f903aaa600de71ee77d7e4d"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "skill.video-lipsync-workflow",
|
|
69
|
+
"fromVersion": "0.1.1",
|
|
70
|
+
"fromManifestSha256": "b22a9f7b107d9dd1d10be530cd31d6b207f5efa96628793987d7c3aeb3b26b1f",
|
|
71
|
+
"toVersion": "0.1.2",
|
|
72
|
+
"toManifestSha256": "a5f8e045e01dcff66cf1f65ab0f831d5349909606a47b4e2fe793c6d6a5cd5c2"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "skill.voice-clone",
|
|
76
|
+
"fromVersion": "0.1.5",
|
|
77
|
+
"fromManifestSha256": "99220323b89b7412d4eda6b9a169965627744d0f32da9d3a30b8155b49799785",
|
|
78
|
+
"toVersion": "0.1.6",
|
|
79
|
+
"toManifestSha256": "7778a8e8cef0b426293c0f73c690a1cd464a0a10d8b9154e558e48ff0b2dbb25"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "skill.w2-1-digital-human",
|
|
83
|
+
"fromVersion": "0.1.4",
|
|
84
|
+
"fromManifestSha256": "280038085869beb851f85189c66f3785b78436cb356f3d4e998f3a7f29f84c06",
|
|
85
|
+
"toVersion": "0.1.5",
|
|
86
|
+
"toManifestSha256": "5ed03ba9ea025f8327daae1bf4b56b6b36496eb6ae32bc09b9fe708b7d4bd1db"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "skill.w2-1-image-digital-human",
|
|
90
|
+
"fromVersion": "0.1.6",
|
|
91
|
+
"fromManifestSha256": "12a470f19dd1115cd07b4ea65f7bfe03f2b4a7b1288f2535628a673bcb4a86b7",
|
|
92
|
+
"toVersion": "0.1.7",
|
|
93
|
+
"toManifestSha256": "80bbb639b345fd0dd91fba08243755f15bc9d186873cd33bde176603314e2e48"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "skill.w3-multimodal-video",
|
|
97
|
+
"fromVersion": "1.2.9",
|
|
98
|
+
"fromManifestSha256": "393a95738fb1f8eda4bcf048afc695663c00f2d844cce76adb25c22db2eb2c47",
|
|
99
|
+
"toVersion": "1.2.10",
|
|
100
|
+
"toManifestSha256": "63aa79081179d5032a6b3dee796fc3c9e137b4edada312029d63ec1d52ff3514"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "skill.w3-video-copy",
|
|
104
|
+
"fromVersion": "0.1.36",
|
|
105
|
+
"fromManifestSha256": "bbb3b9869b3a50b4724c30465e9b4addd7dde1514b4e4814e8d91664d01f64e8",
|
|
106
|
+
"toVersion": "0.1.37",
|
|
107
|
+
"toManifestSha256": "1a5c06a3dc14f612d2423b47701fb88f492ee2046741126b03d310c33907fb8a"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "skill.w3g-video-generation",
|
|
111
|
+
"fromVersion": "0.1.2",
|
|
112
|
+
"fromManifestSha256": "d75ecfb49ba264601b360bcec6bc9e5ab19f6e9294ae01b9957da2fd8f41950b",
|
|
113
|
+
"toVersion": "0.1.3",
|
|
114
|
+
"toManifestSha256": "08ced5591a854739a7bd6594969d5bcf4ce6d392cf7423e72ac81dceffcb2489"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"signature": {
|
|
119
|
+
"algorithm": "ed25519",
|
|
120
|
+
"keyId": "ngg-signing-skill-release-1",
|
|
121
|
+
"scope": "nggaigc-start-compatibility/v1",
|
|
122
|
+
"value": "TieQh/DsCu0K6ViZsYATqXFnBIFtnJxk97Nd3j9Jp8F4VfeacC7IE2vOlG5VLz/gWvxXqALy6J9OOiBJi45PDQ=="
|
|
123
|
+
}
|
|
124
|
+
}
|