@nggaigc/cli 0.1.53 → 0.1.55

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 CHANGED
@@ -12,7 +12,13 @@ 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.53` 随包安装已签名的 NGGAIGC 根 Skill `1.1.49`,保留豆包工作识别和 Intel Mac 安装放行;后续独立工作流仍经当前目录解析、在线准入和签名验签加载。
15
+ CLI `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
+ 旧的显式 `start-skill skill.<id> <version>` 只有在官方受签、精确绑定当前目录的有限迁移许可内,才可转到同一 Skill 的当前版本;结果分别返回 `requestedVersion` 与 `resolvedVersion`,在线准入使用真实新版本及哈希。未列版本、目录变化、停用或验签失败均停止。确需固定版本可加 `--exact-version`,此时绝不迁移到其他版本。无版本调用仍要求唯一 active 入口,不激活旧版本。
20
+
21
+ 此许可仅用于新启动。已经受理或结果未知的任务保留原请求和任务身份;`client` 将原参数交给已验证启动器,不为恢复任务重新准入、提交或付费。
16
22
 
17
23
  ## 统一安装入口(0.1.46 起)
18
24
 
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 !== "1.1.49" ||
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 bytes = await readFile(path.resolve(directory, ...name.split("/")));
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";
@@ -9,7 +10,7 @@ import { resolveSkillLocation, } from "./skill-location.js";
9
10
  function defaultBundle() {
10
11
  const vendor = fileURLToPath(new URL("../vendor/", import.meta.url));
11
12
  return {
12
- archive: path.join(vendor, "nggaigc-plugin-1.1.49.zip"),
13
+ archive: path.join(vendor, "nggaigc-plugin-1.1.50.zip"),
13
14
  manifest: path.join(vendor, "manifest.json"),
14
15
  trust: path.join(vendor, "trusted-keys.json"),
15
16
  };
@@ -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 = await installSkill(installer, path.join(extracted, "skills", "nggaigc"), profile, skillsDirectory, bundle.version, bundle.files);
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.slice(1));
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], location);
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,172 @@
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
+ const fd = await open(file, "r");
93
+ try {
94
+ await fd.sync();
95
+ }
96
+ finally {
97
+ await fd.close();
98
+ }
99
+ }
100
+ await syncDirectory(temp);
101
+ await rename(temp, dest);
102
+ await syncDirectory(parent);
103
+ }
104
+ export async function trustedRoot(skills, vendor = rootVendor) {
105
+ const root = path.join(skills, "nggaigc");
106
+ await realDirectory(root);
107
+ let version;
108
+ try {
109
+ version = JSON.parse(await readFile(path.join(root, "references/source.json"), "utf8")).version;
110
+ }
111
+ catch {
112
+ throw new CliError("ROOT_UNAVAILABLE");
113
+ }
114
+ if (typeof version !== "string" || !versionPattern.test(version))
115
+ throw new CliError("ROOT_UNAVAILABLE");
116
+ const bundle = await knownBundle(skills, version, vendor);
117
+ await verifyInstalledSkill(root, bundle.files, bundle.executableFiles);
118
+ // This binds source.json and protocol to the signed payload, regardless of marker.version.
119
+ return { version: bundle.version, bundle, directory: root };
120
+ }
121
+ /** Creation holds the install lock; verified immutable reuse is read-only. */
122
+ export async function trustedRuntime(skills, version, vendor = rootVendor, create = true) {
123
+ if (!["1.1.49", "1.1.50"].includes(version))
124
+ throw new CliError("ROOT_UNAVAILABLE");
125
+ await trustedRoot(skills, vendor);
126
+ // Delegates always use an immutable runtime, including the currently installed version.
127
+ const bundle = await knownBundle(skills, version, vendor);
128
+ const parent = path.join(stateDirectoryForSkills(skills), "root-runtimes");
129
+ const directory = path.join(parent, version);
130
+ if (await exists(directory)) {
131
+ if ((await realpath(directory)) !== path.resolve(directory))
132
+ throw new CliError("INSTALL_PATH_UNSAFE");
133
+ await verifyInstalledSkill(directory, bundle.files, bundle.executableFiles);
134
+ return { version, bundle, directory };
135
+ }
136
+ if (!create)
137
+ throw new CliError("ROOT_RUNTIME_UNAVAILABLE");
138
+ await privateDirectory(parent);
139
+ const temporary = path.join(parent, ".runtime-" + randomBytes(16).toString("hex"));
140
+ try {
141
+ await extractBundle(bundle.archive, temporary, bundle.sha256);
142
+ await verifyExtractedBundle(temporary, bundle.files);
143
+ const root = path.join(temporary, "skills/nggaigc");
144
+ const marker = await open(path.join(root, ".nggaigc-install.json"), "wx", 0o600);
145
+ try {
146
+ await marker.writeFile(JSON.stringify({
147
+ schemaVersion: "nggaigc-local-install/v1",
148
+ version,
149
+ installedAt: new Date().toISOString(),
150
+ }) + "\n");
151
+ await marker.sync();
152
+ }
153
+ finally {
154
+ await marker.close();
155
+ }
156
+ await verifyInstalledSkill(root, bundle.files, bundle.executableFiles);
157
+ if (await exists(directory))
158
+ throw new CliError("ROOT_RECOVERY_REQUIRED");
159
+ await rename(root, directory);
160
+ await syncDirectory(parent);
161
+ await verifyInstalledSkill(directory, bundle.files, bundle.executableFiles);
162
+ return { version, bundle, directory };
163
+ }
164
+ finally {
165
+ await rm(temporary, { recursive: true, force: true });
166
+ }
167
+ }
168
+ export async function bytesHash(file) {
169
+ return createHash("sha256")
170
+ .update(await readFile(file))
171
+ .digest("hex");
172
+ }