@nowcrew/daemon 0.6.89 → 0.6.90

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.
Files changed (115) hide show
  1. package/README.md +64 -3
  2. package/dist/agent-ability/materializer.js +21 -10
  3. package/dist/agent-environment-contract.js +55 -0
  4. package/dist/agent-environment-control.js +24 -0
  5. package/dist/agent-environment-execution.js +98 -0
  6. package/dist/agent-environments.js +275 -0
  7. package/dist/automatic-daemon-installer.js +49 -1
  8. package/dist/computer-main.js +18 -0
  9. package/dist/computer-profile.js +17 -4
  10. package/dist/computer-setup-client.js +318 -0
  11. package/dist/computer-setup-orchestration.js +203 -0
  12. package/dist/computer-setup.js +244 -0
  13. package/dist/config.js +30 -12
  14. package/dist/console-formatter.js +3 -3
  15. package/dist/console.js +15 -2
  16. package/dist/daemon-update-eligibility.js +1 -1
  17. package/dist/execution-journal.js +9 -6
  18. package/dist/execution-protocol.js +1 -0
  19. package/dist/execution-runner.js +13 -8
  20. package/dist/i18n.js +19 -0
  21. package/dist/list-models.js +12 -12
  22. package/dist/local-execution-lease.js +22 -0
  23. package/dist/local-executor.js +57 -84
  24. package/dist/local-memory/candidate.js +43 -0
  25. package/dist/local-memory/index-budget.js +8 -0
  26. package/dist/local-memory/inventory-cli.js +61 -0
  27. package/dist/local-memory/inventory-preview.js +159 -0
  28. package/dist/local-memory/inventory-source.js +159 -0
  29. package/dist/local-memory/pending-match.js +29 -0
  30. package/dist/local-memory/pending-query.js +100 -0
  31. package/dist/local-memory/pending-schema.js +29 -0
  32. package/dist/local-memory/pending-source.js +126 -0
  33. package/dist/local-memory/pending.js +34 -84
  34. package/dist/local-memory/prompt.js +19 -12
  35. package/dist/local-memory/read-cli.js +97 -12
  36. package/dist/local-memory/rollout.js +82 -0
  37. package/dist/local-memory/runtime.js +23 -17
  38. package/dist/local-memory/work-log-execution.js +31 -0
  39. package/dist/local-memory/work-log.js +58 -0
  40. package/dist/local-memory/write.js +27 -58
  41. package/dist/local-runtime-environment.js +50 -0
  42. package/dist/machine-environment-cache.js +147 -0
  43. package/dist/machine-environment-contract.js +10 -0
  44. package/dist/machine-environment-control.js +27 -0
  45. package/dist/machine-environment-policy.js +75 -0
  46. package/dist/machine-environment-process.js +110 -0
  47. package/dist/machine-info.js +70 -25
  48. package/dist/machine-runtime-environment.js +189 -0
  49. package/dist/machine-shell-environment-capture.js +135 -0
  50. package/dist/machine-windows-environment-capture.js +215 -0
  51. package/dist/machine-windows-environment-script.js +158 -0
  52. package/dist/main.js +4 -69
  53. package/dist/marketplace-command.js +21 -0
  54. package/dist/marketplace-controller.js +80 -0
  55. package/dist/marketplace-protocol.js +38 -0
  56. package/dist/marketplace-remote.js +48 -0
  57. package/dist/marketplace-runner.js +235 -0
  58. package/dist/marketplace-serve.js +17 -0
  59. package/dist/marketplace-source.js +103 -0
  60. package/dist/message-file-preview.js +246 -0
  61. package/dist/pre-admission-rejection.js +64 -0
  62. package/dist/project-skills/execution-adapter.js +3 -2
  63. package/dist/project-skills/runtime-launch.js +14 -0
  64. package/dist/provider-env.js +26 -15
  65. package/dist/runner.js +17 -4
  66. package/dist/runtime-environment-merge.js +16 -0
  67. package/dist/runtime-health-handler.js +10 -0
  68. package/dist/runtime-health.js +3 -1
  69. package/dist/runtime-path.js +22 -12
  70. package/dist/runtime-probe.js +35 -6
  71. package/dist/runtimes/acp-probe-transport.js +69 -0
  72. package/dist/runtimes/claude-models.js +20 -9
  73. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  74. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  75. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  76. package/dist/runtimes/deepseek-harness.js +76 -15
  77. package/dist/runtimes/harness-probe-retry.js +42 -0
  78. package/dist/runtimes/hermes-models.js +2 -1
  79. package/dist/runtimes/kimi-acp-runner.js +78 -13
  80. package/dist/runtimes/opencode.js +2 -2
  81. package/dist/serve.js +88 -176
  82. package/dist/storage-identity.js +0 -3
  83. package/dist/storage-serve-adapter.js +6 -72
  84. package/dist/supervised-runtime.js +17 -2
  85. package/dist/token.js +0 -1
  86. package/dist/workspace-control-handler.js +27 -0
  87. package/dist/workspace-fs.js +66 -15
  88. package/dist/workspace.js +1 -1
  89. package/package.json +13 -12
  90. package/dist/codex-home-launch.js +0 -7
  91. package/dist/codex-home-mode-controller.js +0 -68
  92. package/dist/daemon-migration-controller.js +0 -211
  93. package/dist/daemon-migration-wiring.js +0 -22
  94. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  95. package/dist/runtimes/codex-home-migration.js +0 -112
  96. package/dist/runtimes/codex-home.js +0 -236
  97. package/dist/storage-control.js +0 -212
  98. package/dist/storage-copy.js +0 -628
  99. package/dist/storage-credential-guard.js +0 -21
  100. package/dist/storage-doctor.js +0 -228
  101. package/dist/storage-manifest.js +0 -1027
  102. package/dist/storage-migration-controller.js +0 -440
  103. package/dist/storage-migration-manifest.js +0 -105
  104. package/dist/storage-migration-policy.js +0 -100
  105. package/dist/storage-runtime-controller.js +0 -381
  106. package/dist/storage-runtime-readiness.js +0 -241
  107. package/dist/storage-safe-fs.js +0 -302
  108. package/dist/storage-status-delivery.js +0 -224
  109. package/dist/storage-status-replay.js +0 -140
  110. package/dist/storage-target-confirmation.js +0 -129
  111. package/dist/storage-worker-command.js +0 -37
  112. package/dist/storage-worker-executor.js +0 -447
  113. package/dist/storage-worker-handoff.js +0 -151
  114. package/dist/storage-worker-operation.js +0 -294
  115. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,48 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
6
+ import { MarketplaceFailure } from "./marketplace-command.js";
7
+ import { validateMarketplaceSource } from "./marketplace-source.js";
8
+ const Manifest = z.object({ name: z.literal(MARKETPLACE), plugins: z.array(z.object({ name: z.string() }).passthrough()) });
9
+ const Target = z.object({
10
+ version: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/),
11
+ source: z.object({ source: z.literal("git-subdir"),
12
+ url: z.literal("git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git"),
13
+ path: z.literal("plugins/AIinterview-plugin"), sha: z.string().regex(/^[a-f0-9]{40}$/) }),
14
+ });
15
+ export async function readRemoteMarketplace(runtime, registered, env, command) {
16
+ const source = validateMarketplaceSource(registered.url, registered.ref);
17
+ const temporary = await mkdtemp(join(tmpdir(), "nowwork-marketplace-query-"));
18
+ const options = { env, cwd: temporary };
19
+ try {
20
+ await command("git", ["init", "--bare", "--template=", "."], options);
21
+ await command("git", ["fetch", "--depth=1", "--no-tags", source.url, source.ref], options);
22
+ const revision = (await command("git", ["rev-parse", "--verify", "FETCH_HEAD"], options)).trim();
23
+ if (!/^[a-f0-9]{40}$/.test(revision))
24
+ throw new MarketplaceFailure("verification_failed");
25
+ const path = runtime === "claude" ? ".claude-plugin/marketplace.json" : ".agents/plugins/marketplace.json";
26
+ const raw = await command("git", ["show", `${revision}:${path}`], options);
27
+ let value;
28
+ try {
29
+ value = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ throw new MarketplaceFailure("invalid_inventory");
33
+ }
34
+ const manifest = Manifest.safeParse(value);
35
+ if (!manifest.success)
36
+ throw new MarketplaceFailure("invalid_inventory");
37
+ const matches = manifest.data.plugins.filter(plugin => plugin.name === PLUGIN);
38
+ if (matches.length !== 1)
39
+ throw new MarketplaceFailure("invalid_inventory");
40
+ const target = Target.safeParse(matches[0]);
41
+ if (!target.success)
42
+ throw new MarketplaceFailure("unsupported_installation");
43
+ return { marketRevision: revision, targetVersion: target.data.version };
44
+ }
45
+ finally {
46
+ await rm(temporary, { recursive: true, force: true });
47
+ }
48
+ }
@@ -0,0 +1,235 @@
1
+ /** 固定市场操作的短命 CLI 适配;原始命令输出不离开设备。 */
2
+ import { createHash } from "node:crypto";
3
+ import { readFile, readdir, realpath, lstat, mkdtemp, rm } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { join, resolve, isAbsolute, relative, sep } from "node:path";
6
+ import { applyProviderEnv } from "./provider-env.js";
7
+ import { augmentedPath } from "./runtime-path.js";
8
+ import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
9
+ import { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
10
+ import { readRemoteMarketplace } from "./marketplace-remote.js";
11
+ import { readMarketplaceSource } from "./marketplace-source.js";
12
+ export { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
13
+ const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
14
+ const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
15
+ const SOURCE_PATH = "plugins/AIinterview-plugin";
16
+ const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
17
+ const parse = (text) => { try {
18
+ return JSON.parse(text);
19
+ }
20
+ catch {
21
+ throw new MarketplaceFailure("invalid_inventory");
22
+ } };
23
+ const version = (x) => typeof x === "string" && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/.test(x) ? x : null;
24
+ const sha = (x) => typeof x === "string" && /^[a-f0-9]{40}$/.test(x) ? x : null;
25
+ const within = (root, candidate) => {
26
+ const child = relative(root, candidate);
27
+ return child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child));
28
+ };
29
+ const canonical = async (path) => realpath(path).catch(() => resolve(path));
30
+ export async function resolveMarketplaceScopes(targets, agentsRoot, inherited = process.env) {
31
+ const scopes = new Map();
32
+ const trustedHome = await canonical(inherited.HOME ?? homedir());
33
+ for (const target of targets) {
34
+ const home = target.handle ? join(agentsRoot, target.handle, ".home") : homedir();
35
+ const agentHome = target.handle ? await canonical(home) : null;
36
+ const allowedHomeRoots = [trustedHome, ...(agentHome === null ? [] : [agentHome])];
37
+ const sharedConfigRoot = join(trustedHome, target.runtime === "codex" ? ".codex" : ".claude");
38
+ const agentConfigRoot = agentHome === null ? null : join(agentHome, target.runtime === "codex" ? ".codex" : ".claude");
39
+ const allowedConfigRoots = [sharedConfigRoot, ...(agentConfigRoot === null ? [] : [agentConfigRoot])];
40
+ const suppliedHome = target.paths.HOME;
41
+ if (suppliedHome !== undefined && !within(allowedHomeRoots[0], await canonical(suppliedHome))
42
+ && (agentHome === null || !within(agentHome, await canonical(suppliedHome))))
43
+ throw new MarketplaceFailure("scope_unavailable");
44
+ const suppliedConfig = target.runtime === "codex" ? target.paths.CODEX_HOME : target.paths.CLAUDE_CONFIG_DIR;
45
+ const suppliedConfigPath = suppliedConfig === undefined ? null : await canonical(suppliedConfig);
46
+ if (suppliedConfigPath !== null && !allowedConfigRoots.some(root => within(root, suppliedConfigPath)))
47
+ throw new MarketplaceFailure("scope_unavailable");
48
+ // 与 local-executor 使用同一 provider 解析;凭据存在性只用于选择已配置的 provider,不传递凭据内容。
49
+ const env = applyProviderEnv({ ...inherited, ...target.paths }, target.runtime, target.handle ? {
50
+ provider: target.provider, providerApiKey: target.hasProviderKey ? "configured" : null,
51
+ } : {}, home);
52
+ for (const key of Object.keys(env))
53
+ if (/^(CREW_|ANTHROPIC_|OPENAI_|DEEPSEEK_)/.test(key))
54
+ delete env[key];
55
+ for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"])
56
+ delete env[key];
57
+ env.PATH = augmentedPath(inherited);
58
+ env.GIT_TERMINAL_PROMPT = "0";
59
+ env.CI = "1";
60
+ const raw = target.runtime === "codex" ? env.CODEX_HOME ?? join(env.HOME ?? homedir(), ".codex")
61
+ : env.CLAUDE_CONFIG_DIR ?? join(env.HOME ?? homedir(), ".claude");
62
+ if (!isAbsolute(raw))
63
+ throw new MarketplaceFailure("scope_unavailable");
64
+ const configHome = await realpath(raw).catch(() => resolve(raw));
65
+ if (!allowedConfigRoots.some(root => within(root, configHome)))
66
+ throw new MarketplaceFailure("scope_unavailable");
67
+ if (target.runtime === "codex")
68
+ env.CODEX_HOME = configHome;
69
+ else
70
+ env.CLAUDE_CONFIG_DIR = configHome;
71
+ const key = `${target.runtime}:${configHome}`;
72
+ const prior = scopes.get(key);
73
+ if (prior) {
74
+ if (target.handle && !prior.agents.includes(target.handle))
75
+ prior.agents.push(target.handle);
76
+ prior.shared ||= !target.handle;
77
+ }
78
+ else
79
+ scopes.set(key, { runtime: target.runtime, configHome, env, agents: target.handle ? [target.handle] : [], shared: !target.handle });
80
+ }
81
+ return [...scopes.values()];
82
+ }
83
+ export function parsePluginInventory(runtime, value, configHome) {
84
+ const rows = Array.isArray(value) ? value : obj(value).installed;
85
+ if (!Array.isArray(rows))
86
+ throw new MarketplaceFailure("invalid_inventory");
87
+ const found = rows.map(obj).filter(x => (x.id ?? x.pluginId) === SELECTOR);
88
+ if (found.length === 0)
89
+ return null;
90
+ if (found.length !== 1)
91
+ throw new MarketplaceFailure("unsupported_installation");
92
+ const item = found[0];
93
+ const v = version(item.version);
94
+ if (!v || typeof item.enabled !== "boolean")
95
+ throw new MarketplaceFailure("invalid_inventory");
96
+ if (runtime === "claude" && item.scope !== "user")
97
+ throw new MarketplaceFailure("unsupported_installation");
98
+ const path = runtime === "claude" ? item.installPath : join(configHome, "plugins/cache", MARKETPLACE, PLUGIN, v);
99
+ if (typeof path !== "string" || !isAbsolute(path))
100
+ throw new MarketplaceFailure("invalid_inventory");
101
+ return { version: v, enabled: item.enabled, path, revision: sha(obj(item.source).sha) };
102
+ }
103
+ /** 查询与任务并行;外部更新改变安装清单时重读一次,避免混合两代安装事实。 */
104
+ async function readInstalledSnapshot(runtime, inventory) {
105
+ for (let attempt = 0; attempt < 2; attempt += 1) {
106
+ const before = await inventory();
107
+ let valid = before === null;
108
+ if (before !== null) {
109
+ try {
110
+ const manifest = obj(parse(await readFile(join(before.path, runtime === "claude" ? ".claude-plugin" : ".codex-plugin", "plugin.json"), "utf8")));
111
+ valid = manifest.name === PLUGIN && version(manifest.version) === before.version;
112
+ }
113
+ catch { /* 清单变化则重读;稳定但不可读的安装仍报告核验失败。 */ }
114
+ }
115
+ if (JSON.stringify(before) !== JSON.stringify(await inventory()))
116
+ continue;
117
+ if (!valid)
118
+ throw new MarketplaceFailure("verification_failed");
119
+ return before;
120
+ }
121
+ throw new MarketplaceFailure("configuration_changed");
122
+ }
123
+ /** 逐字节比对固定 Git 源子目录,不能只凭 CLI 退出码或版本字符串证明安装完成。 */
124
+ export async function verifyInstalledContent(path, revision, env, command) {
125
+ const temporary = await mkdtemp(join(tmpdir(), "nowwork-plugin-verify-"));
126
+ const options = { env, cwd: temporary };
127
+ try {
128
+ await command("git", ["init", "--bare", "."], options);
129
+ await command("git", ["fetch", "--depth=1", SOURCE, revision], options);
130
+ const tree = await command("git", ["ls-tree", "-rz", `${revision}:${SOURCE_PATH}`], options);
131
+ const expected = new Map();
132
+ for (const entry of tree.split("\0").filter(Boolean)) {
133
+ const match = /^(100644|100755) blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
134
+ if (!match || match[3].split("/").some(x => x === ".." || x === "."))
135
+ throw new MarketplaceFailure("verification_failed");
136
+ expected.set(match[3], match[2]);
137
+ }
138
+ if (!expected.size || expected.size > 2000)
139
+ throw new MarketplaceFailure("verification_failed");
140
+ const actual = new Map();
141
+ let bytes = 0;
142
+ const walk = async (dir, prefix = "") => {
143
+ for (const name of await readdir(dir)) {
144
+ if (name === "__pycache__" || name === ".DS_Store")
145
+ continue;
146
+ const entry = join(dir, name);
147
+ const key = prefix + name;
148
+ const info = await lstat(entry);
149
+ if (info.isSymbolicLink())
150
+ throw new MarketplaceFailure("verification_failed");
151
+ if (info.isDirectory())
152
+ await walk(entry, key + "/");
153
+ else if (info.isFile()) {
154
+ bytes += info.size;
155
+ if (bytes > 100 * 1024 * 1024 || actual.size > 2000)
156
+ throw new MarketplaceFailure("verification_failed");
157
+ const data = await readFile(entry);
158
+ actual.set(key, createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex"));
159
+ }
160
+ else
161
+ throw new MarketplaceFailure("verification_failed");
162
+ }
163
+ };
164
+ await walk(path);
165
+ if (actual.size !== expected.size || [...expected].some(([name, hash]) => actual.get(name) !== hash))
166
+ throw new MarketplaceFailure("verification_failed");
167
+ }
168
+ finally {
169
+ await rm(temporary, { recursive: true, force: true });
170
+ }
171
+ }
172
+ export async function runMarketplaceScope(scope, mode, command = runMarketplaceCommand, verify = verifyInstalledContent) {
173
+ let result = { runtime: scope.runtime, scopeId: createHash("sha256").update(`${scope.runtime}:${scope.configHome}`).digest("hex").slice(0, 16),
174
+ agents: scope.agents, shared: scope.shared, status: "failed", beforeVersion: null, afterVersion: null, targetVersion: null,
175
+ marketRevision: null, installedRevision: null, enabled: null, activation: "unverified", errorCode: null };
176
+ const options = { env: scope.env, cwd: scope.configHome };
177
+ const run = (args) => command(scope.runtime, args, options);
178
+ const inventory = async () => parsePluginInventory(scope.runtime, parse(await run(["plugin", "list", "--json"])), scope.configHome);
179
+ try {
180
+ if (!(await lstat(scope.configHome)).isDirectory())
181
+ throw new MarketplaceFailure("scope_unavailable");
182
+ const markets = parse(await run(["plugin", "marketplace", "list", "--json"]));
183
+ const rows = Array.isArray(markets) ? markets : obj(markets).marketplaces;
184
+ if (!Array.isArray(rows))
185
+ throw new MarketplaceFailure("invalid_inventory");
186
+ const market = rows.map(obj).filter(x => x.name === MARKETPLACE);
187
+ if (market.length !== 1)
188
+ throw new MarketplaceFailure("market_not_registered");
189
+ const root = market[0].root ?? market[0].installLocation;
190
+ if (typeof root !== "string" || !isAbsolute(root))
191
+ throw new MarketplaceFailure("invalid_inventory");
192
+ const registered = await readMarketplaceSource(scope.runtime, scope.configHome, market[0]);
193
+ const before = mode === "update" ? await inventory() : await readInstalledSnapshot(scope.runtime, inventory);
194
+ result = { ...result, beforeVersion: before?.version ?? null, afterVersion: before?.version ?? null, enabled: before?.enabled ?? null };
195
+ if (mode === "inspect")
196
+ return { ...result, status: before ? "checked" : "not_installed", activation: "unverified" };
197
+ if (mode === "refresh") {
198
+ const latest = await readRemoteMarketplace(scope.runtime, registered, scope.env, command);
199
+ const current = await readMarketplaceSource(scope.runtime, scope.configHome, market[0]);
200
+ if (current.url !== registered.url || current.ref !== registered.ref)
201
+ throw new MarketplaceFailure("configuration_changed");
202
+ return { ...result, ...latest, status: "refreshed", activation: "not_changed" };
203
+ }
204
+ await run(["plugin", "marketplace", scope.runtime === "claude" ? "update" : "upgrade", MARKETPLACE]);
205
+ const revision = sha((await command("git", ["-C", root, "rev-parse", "HEAD"], options)).trim());
206
+ if (!revision)
207
+ throw new MarketplaceFailure("verification_failed");
208
+ const manifest = obj(parse(await readFile(join(root, scope.runtime === "claude" ? ".claude-plugin" : ".agents/plugins", "marketplace.json"), "utf8")));
209
+ const plugins = manifest.plugins;
210
+ const target = Array.isArray(plugins) ? plugins.map(obj).find(x => x.name === PLUGIN) : undefined;
211
+ const source = obj(target?.source);
212
+ const targetVersion = version(target?.version);
213
+ const targetSha = sha(source.sha);
214
+ result = { ...result, marketRevision: revision, targetVersion };
215
+ if (!before)
216
+ return { ...result, status: "not_installed", activation: "not_changed" };
217
+ if (!targetVersion || !targetSha || source.url !== SOURCE || source.path !== SOURCE_PATH || source.source !== "git-subdir")
218
+ throw new MarketplaceFailure("unsupported_installation");
219
+ // Codex add 会启用插件;不偷偷改变用户已禁用的状态。
220
+ if (!before.enabled)
221
+ throw new MarketplaceFailure("unsupported_installation");
222
+ await run(scope.runtime === "claude" ? ["plugin", "update", SELECTOR, "--scope", "user", "--json"] : ["plugin", "add", SELECTOR, "--json"]);
223
+ const after = await inventory();
224
+ result = { ...result, afterVersion: after?.version ?? null, enabled: after?.enabled ?? null };
225
+ if (!after || after.version !== targetVersion || after.enabled !== before.enabled)
226
+ throw new MarketplaceFailure("verification_failed");
227
+ await verify(after.path, targetSha, scope.env, command);
228
+ return { ...result, status: before.version === after.version && before.revision === targetSha ? "unchanged" : "updated",
229
+ installedRevision: targetSha, activation: "reload_required" };
230
+ }
231
+ catch (error) {
232
+ const code = error instanceof MarketplaceFailure ? error.code : "scope_unavailable";
233
+ return { ...result, status: code === "cli_missing" ? "missing_cli" : "failed", errorCode: code };
234
+ }
235
+ }
@@ -0,0 +1,17 @@
1
+ import { createMarketplaceController } from "./marketplace-controller.js";
2
+ import { prepareDaemonRestart } from "./daemon-updater.js";
3
+ export function createServeMarketplace(config, localSlots, hostCoordinator) {
4
+ const controller = createMarketplaceController({ agentsRoot: config.agentsRoot,
5
+ acquire: async () => { const result = await prepareDaemonRestart({ localSlots, hostCoordinator }); return result.ok ? result : null; } });
6
+ return {
7
+ handle(raw, send) {
8
+ if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "marketplace:operation")
9
+ return false;
10
+ const { reqId, ...request } = raw;
11
+ if (typeof reqId === "string")
12
+ send({ type: "fs:result", reqId, ...controller.handle(request) });
13
+ return true;
14
+ },
15
+ stop: controller.stop,
16
+ };
17
+ }
@@ -0,0 +1,103 @@
1
+ import { readFile, realpath, stat } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { parse as parseToml } from "smol-toml";
4
+ import { MARKETPLACE } from "./marketplace-protocol.js";
5
+ import { MarketplaceFailure } from "./marketplace-command.js";
6
+ const REPOSITORY_PATH = "6128d2e757e7cd986dfae5ab/pm/nowwork-pm-skills-marketplace";
7
+ const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
8
+ /** 固定仓库身份,但保留用户登记的 SSH/HTTPS 传输方式;不允许 URL 内嵌凭据。 */
9
+ function allowedRepository(value) {
10
+ if (typeof value !== "string" || value.length > 4096 || /[\s\u0000-\u001f\u007f\\]/u.test(value))
11
+ return false;
12
+ let host;
13
+ let path;
14
+ const scp = /^git@([^/:]+):(.+)$/u.exec(value);
15
+ if (scp) {
16
+ host = scp[1];
17
+ path = scp[2];
18
+ }
19
+ else {
20
+ let url;
21
+ try {
22
+ url = new URL(value);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ if (!["https:", "ssh:"].includes(url.protocol) || url.search || url.hash || url.password)
28
+ return false;
29
+ if (url.protocol === "https:" && (url.username || url.port))
30
+ return false;
31
+ if (url.protocol === "ssh:" && (url.username !== "git" || (url.port !== "" && url.port !== "22")))
32
+ return false;
33
+ host = url.hostname;
34
+ path = url.pathname.replace(/^\//u, "");
35
+ }
36
+ return host.toLowerCase() === "codeup.aliyun.com" && path.replace(/\/$/u, "").replace(/\.git$/u, "") === REPOSITORY_PATH;
37
+ }
38
+ export function validateMarketplaceSource(url, ref) {
39
+ const selectedRef = ref === undefined ? "HEAD" : ref;
40
+ if (!allowedRepository(url) || typeof selectedRef !== "string" || !selectedRef || selectedRef.length > 1024
41
+ || /^[+-]/u.test(selectedRef) || selectedRef === "@" || selectedRef.includes("..") || selectedRef.includes("@{")
42
+ || /[\s\u0000-\u001f\u007f~^:?*\[\\]/u.test(selectedRef)
43
+ || selectedRef.split("/").some(part => !part || part.startsWith(".") || part.endsWith(".") || part.endsWith(".lock"))) {
44
+ throw new MarketplaceFailure("unsupported_installation");
45
+ }
46
+ return { url, ref: selectedRef };
47
+ }
48
+ async function registrationFile(path) {
49
+ try {
50
+ if ((await stat(path)).size > 1024 * 1024)
51
+ throw new MarketplaceFailure("invalid_inventory");
52
+ return await readFile(path, "utf8");
53
+ }
54
+ catch (error) {
55
+ if (error instanceof MarketplaceFailure)
56
+ throw error;
57
+ throw new MarketplaceFailure("configuration_changed");
58
+ }
59
+ }
60
+ /** 列表会省略 ref;以同一配置目录内的原生登记文件为准,不从已安装缓存猜分支。 */
61
+ export async function readMarketplaceSource(runtime, configHome, listed) {
62
+ let source;
63
+ let listedUrl;
64
+ if (runtime === "claude") {
65
+ let registered;
66
+ const text = await registrationFile(join(configHome, "plugins/known_marketplaces.json"));
67
+ try {
68
+ registered = obj(obj(JSON.parse(text))[MARKETPLACE]);
69
+ }
70
+ catch {
71
+ throw new MarketplaceFailure("invalid_inventory");
72
+ }
73
+ const entry = obj(registered.source);
74
+ if (entry.source !== "git" || listed.source !== "git")
75
+ throw new MarketplaceFailure("unsupported_installation");
76
+ source = validateMarketplaceSource(entry.url, entry.ref);
77
+ listedUrl = listed.url ?? listed.repo;
78
+ if (typeof registered.installLocation !== "string" || typeof listed.installLocation !== "string")
79
+ throw new MarketplaceFailure("configuration_changed");
80
+ const canonical = (path) => realpath(path).catch(() => resolve(path));
81
+ if (await canonical(registered.installLocation) !== await canonical(listed.installLocation))
82
+ throw new MarketplaceFailure("configuration_changed");
83
+ }
84
+ else {
85
+ let entry;
86
+ const text = await registrationFile(join(configHome, "config.toml"));
87
+ try {
88
+ entry = obj(obj(parseToml(text).marketplaces)[MARKETPLACE]);
89
+ }
90
+ catch {
91
+ throw new MarketplaceFailure("invalid_inventory");
92
+ }
93
+ const origin = obj(listed.marketplaceSource);
94
+ if (entry.source_type !== "git" || origin.sourceType !== "git")
95
+ throw new MarketplaceFailure("unsupported_installation");
96
+ source = validateMarketplaceSource(entry.source, entry.ref);
97
+ listedUrl = origin.source;
98
+ }
99
+ // 同一轮清单与登记文件来源不一致,可能正被重新登记;不擅自改查另一来源。
100
+ if (listedUrl !== source.url)
101
+ throw new MarketplaceFailure("configuration_changed");
102
+ return source;
103
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * 消息授权后的按需单文件读取。Server 决定消息权限、原执行设备及项目 ID;
3
+ * 本模块只解析本机受控根、拒绝别名与越界,并有界读取,不持久化状态。
4
+ */
5
+ import { constants } from "node:fs";
6
+ import { lstat, open, realpath } from "node:fs/promises";
7
+ import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
8
+ import { z } from "zod";
9
+ import { isInternalEnvironmentPath } from "./workspace-fs.js";
10
+ import { opaqueKey, safeKey } from "./workspace.js";
11
+ import { AgentHandleSchema } from "./execution-protocol.js";
12
+ const MAX_BYTES = 256 * 1024;
13
+ const MAX_BINARY_BYTES = 8 * 1024 * 1024;
14
+ const MAX_CONCURRENT_READS = 4;
15
+ let activeReads = 0;
16
+ export const SharedFileRequestSchema = z.object({
17
+ type: z.literal("fs:read-shared"),
18
+ reqId: z.string().min(1).max(128),
19
+ handle: AgentHandleSchema.refine((handle) => !handle.startsWith(".")),
20
+ path: z.string().min(1).max(4096),
21
+ fileContext: z.object({
22
+ taskKey: z.string().min(1).max(200),
23
+ projectIds: z.array(z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/)).max(100),
24
+ primaryProjectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/).optional(),
25
+ }).strict().refine((value) => !value.primaryProjectId || value.projectIds.includes(value.primaryProjectId)),
26
+ }).strict();
27
+ export class SharedFileError extends Error {
28
+ code;
29
+ constructor(code) {
30
+ super(code);
31
+ this.code = code;
32
+ this.name = "SharedFileError";
33
+ }
34
+ }
35
+ const inside = (root, target) => {
36
+ const rel = relative(root, target);
37
+ return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
38
+ };
39
+ const parts = (path) => path.split(/[\\/]/).filter(Boolean);
40
+ const privatePart = (part) => part.startsWith(".")
41
+ || /^(?:credentials?|secrets?|tokens?)(?:[._-]|$)/i.test(part)
42
+ || /^(?:id_rsa|id_ed25519|id_ecdsa|id_dsa|passwd|shadow|authorized_keys|known_hosts)$/i.test(part)
43
+ || /\.(?:pem|key|p12|pfx|keystore)$/i.test(part);
44
+ function validatePath(path) {
45
+ // path 已由 Server 从 Markdown 解码,禁止再次解释编码、协议或设备路径。
46
+ if (/[\x00-\x1f\x7f]/.test(path) || /%[0-9a-f]{2}/i.test(path)
47
+ || path.startsWith("\\\\") || path.startsWith("//") || path.startsWith("~")
48
+ || parts(path).some((part) => part === "..")
49
+ || (process.platform !== "win32" && (/^[A-Za-z]:/.test(path) || path.includes("\\")))
50
+ || (process.platform === "win32" && path.slice(2).includes(":"))) {
51
+ throw new SharedFileError("file_not_viewable");
52
+ }
53
+ }
54
+ function restrictAgentTask(agentsRoot, agentRoot, path, taskNames) {
55
+ if (!inside(agentsRoot, path))
56
+ return;
57
+ if (!inside(agentRoot, path)) {
58
+ // 旧版托管项目位于 agentsRoot/repos;仍必须经过下方登记项目根检查。
59
+ if (inside(join(agentsRoot, "repos"), path))
60
+ return;
61
+ throw new SharedFileError("file_scope_unavailable");
62
+ }
63
+ const local = parts(relative(agentRoot, path));
64
+ const identity = (value) => process.platform === "win32" ? value.toLowerCase() : value;
65
+ if (identity(local[0] ?? "") === "tasks" && (!local[1] || !taskNames.map(identity).includes(identity(local[1])))) {
66
+ throw new SharedFileError("file_scope_unavailable");
67
+ }
68
+ }
69
+ async function checkedCandidate(candidate) {
70
+ const rel = relative(candidate.root, candidate.path);
71
+ if (!inside(candidate.root, candidate.path) || parts(rel).some(privatePart) || isInternalEnvironmentPath(candidate.path)) {
72
+ throw new SharedFileError("file_not_viewable");
73
+ }
74
+ try {
75
+ const before = await lstat(candidate.root, { bigint: true });
76
+ if (!before.isDirectory())
77
+ throw new SharedFileError("file_not_viewable");
78
+ const root = await realpath(candidate.root);
79
+ if (isInternalEnvironmentPath(root))
80
+ throw new SharedFileError("file_not_viewable");
81
+ const [current, canonical] = await Promise.all([
82
+ lstat(candidate.root, { bigint: true }), lstat(root, { bigint: true }),
83
+ ]);
84
+ if (![current, canonical].every((entry) => entry.isDirectory() && entry.dev === before.dev && entry.ino === before.ino)) {
85
+ throw new SharedFileError("file_changed");
86
+ }
87
+ const path = resolve(root, rel);
88
+ await assertRegularPath(root, path);
89
+ return { root, path };
90
+ }
91
+ catch (error) {
92
+ if (error.code === "ENOENT")
93
+ return null;
94
+ throw error;
95
+ }
96
+ }
97
+ async function assertRegularPath(root, target) {
98
+ let current = root;
99
+ const segments = parts(relative(root, target));
100
+ if (segments.length === 0)
101
+ throw new SharedFileError("file_not_viewable");
102
+ for (let index = 0; index < segments.length; index++) {
103
+ const segment = segments[index];
104
+ if (privatePart(segment))
105
+ throw new SharedFileError("file_not_viewable");
106
+ current = join(current, segment);
107
+ const entry = await lstat(current);
108
+ if (entry.isSymbolicLink())
109
+ throw new SharedFileError("file_not_viewable");
110
+ if (index < segments.length - 1 ? !entry.isDirectory() : !entry.isFile() || entry.nlink !== 1) {
111
+ throw new SharedFileError("file_not_viewable");
112
+ }
113
+ }
114
+ if (await realpath(target) !== target)
115
+ throw new SharedFileError("file_not_viewable");
116
+ }
117
+ async function resolveCandidate(agentsRoot, request, registry) {
118
+ const agentRoot = resolve(agentsRoot, request.handle);
119
+ // 根本身不能是符号链接;缺失任务文件由候选阶段归一为 file_not_found。
120
+ try {
121
+ if (!(await lstat(agentRoot)).isDirectory())
122
+ throw new SharedFileError("file_not_viewable");
123
+ }
124
+ catch (error) {
125
+ if (error.code !== "ENOENT")
126
+ throw error;
127
+ }
128
+ const taskNames = [...new Set([opaqueKey(request.fileContext.taskKey), safeKey(request.fileContext.taskKey)])];
129
+ const taskRoots = taskNames.map((name) => join(agentRoot, "tasks", name));
130
+ let candidates;
131
+ if (isAbsolute(request.path)) {
132
+ const target = resolve(request.path);
133
+ restrictAgentTask(resolve(agentsRoot), agentRoot, target, taskNames);
134
+ if (inside(agentRoot, target)) {
135
+ candidates = [{ root: agentRoot, path: target }];
136
+ }
137
+ else {
138
+ const registrations = await registry.list().catch(() => []);
139
+ const roots = registrations.filter((project) => request.fileContext.projectIds.includes(project.projectId)
140
+ && isAbsolute(project.root) && resolve(project.root) !== parse(resolve(project.root)).root
141
+ && inside(resolve(project.root), target));
142
+ if (roots.length === 0)
143
+ throw new SharedFileError("file_scope_unavailable");
144
+ // 嵌套项目取最具体的已授权根;同一实际文件不构成相对路径歧义。
145
+ const root = roots.map((project) => resolve(project.root)).sort((a, b) => b.length - a.length)[0];
146
+ candidates = [{ root, path: target }];
147
+ }
148
+ }
149
+ else {
150
+ const rel = request.path.replace(/^\.\//, "");
151
+ if (parts(rel).some(privatePart))
152
+ throw new SharedFileError("file_not_viewable");
153
+ const target = resolve(agentRoot, rel);
154
+ restrictAgentTask(resolve(agentsRoot), agentRoot, target, taskNames);
155
+ candidates = [{ root: agentRoot, path: target }, ...taskRoots.map((root) => ({ root: agentRoot, path: resolve(root, rel) }))];
156
+ }
157
+ const resolved = (await Promise.all(candidates.map(checkedCandidate))).filter((v) => v !== null);
158
+ const unique = [...new Map(resolved.map((value) => [value.path, value])).values()];
159
+ if (unique.length === 0)
160
+ throw new SharedFileError("file_not_found");
161
+ if (unique.length !== 1)
162
+ throw new SharedFileError("file_path_ambiguous");
163
+ return unique[0];
164
+ }
165
+ async function readCandidate(candidate) {
166
+ const before = await lstat(candidate.path, { bigint: true });
167
+ // Darwin SDK sys/fcntl.h 的 O_NOFOLLOW_ANY 原子拒绝任何父目录符号链接。
168
+ // Node 尚未导出该常量;Linux 则在下方通过已打开句柄的真实路径核验。
169
+ // Darwin 不允许 O_NOFOLLOW_ANY 与 O_NOFOLLOW 同时使用。
170
+ const noFollow = process.platform === "darwin" ? 0x20000000 : (constants.O_NOFOLLOW ?? 0);
171
+ const handle = await open(candidate.path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | noFollow);
172
+ try {
173
+ const opened = await handle.stat({ bigint: true });
174
+ // 对照打开前身份并重新检查整条路径,拒绝检查与读取之间的替换。
175
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino) {
176
+ throw new SharedFileError("file_changed");
177
+ }
178
+ await assertRegularPath(candidate.root, candidate.path);
179
+ if (process.platform === "linux" && await realpath(`/proc/self/fd/${handle.fd}`) !== candidate.path) {
180
+ throw new SharedFileError("file_changed");
181
+ }
182
+ const current = await lstat(candidate.path, { bigint: true });
183
+ if (current.dev !== opened.dev || current.ino !== opened.ino || !current.isFile() || current.nlink !== 1n) {
184
+ throw new SharedFileError("file_changed");
185
+ }
186
+ const knownBinary = /\.(?:xlsx|xls|pdf|png|jpe?g|gif|webp|zip|docx|pptx)$/iu.test(candidate.path);
187
+ if (knownBinary && opened.size > BigInt(MAX_BINARY_BYTES))
188
+ throw new SharedFileError("file_too_large");
189
+ const buffer = Buffer.alloc(knownBinary ? Number(opened.size) : MAX_BYTES);
190
+ let length = 0;
191
+ while (length < buffer.length) {
192
+ const read = await handle.read(buffer, length, buffer.length - length, length);
193
+ if (read.bytesRead === 0)
194
+ break;
195
+ length += read.bytesRead;
196
+ }
197
+ const after = await handle.stat({ bigint: true });
198
+ const currentAfter = await lstat(candidate.path, { bigint: true });
199
+ if (after.size !== opened.size || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs
200
+ || currentAfter.dev !== opened.dev || currentAfter.ino !== opened.ino || !currentAfter.isFile()) {
201
+ throw new SharedFileError("file_changed");
202
+ }
203
+ const bytes = buffer.subarray(0, length);
204
+ const truncated = opened.size > BigInt(length);
205
+ if (bytes.includes(0) || knownBinary) {
206
+ if (truncated)
207
+ throw new SharedFileError("file_too_large");
208
+ return { path: relative(candidate.root, candidate.path).split(sep).join("/"), content: bytes.toString("base64"), encoding: "base64", size: Number(opened.size), truncated };
209
+ }
210
+ let content;
211
+ try {
212
+ content = new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: truncated });
213
+ }
214
+ catch {
215
+ throw new SharedFileError("file_binary");
216
+ }
217
+ return { path: relative(candidate.root, candidate.path).split(sep).join("/"), content, size: Number(opened.size), truncated };
218
+ }
219
+ finally {
220
+ await handle.close();
221
+ }
222
+ }
223
+ export async function readSharedMessageFile(agentsRoot, rawRequest, registry) {
224
+ const parsed = SharedFileRequestSchema.safeParse(rawRequest);
225
+ if (!parsed.success)
226
+ throw new SharedFileError("file_not_viewable");
227
+ if (process.platform !== "darwin" && process.platform !== "linux")
228
+ throw new SharedFileError("file_scope_unavailable");
229
+ validatePath(parsed.data.path);
230
+ if (activeReads >= MAX_CONCURRENT_READS)
231
+ throw new SharedFileError("file_preview_busy");
232
+ activeReads++;
233
+ try {
234
+ return await readCandidate(await resolveCandidate(agentsRoot, parsed.data, registry));
235
+ }
236
+ catch (error) {
237
+ if (error instanceof SharedFileError)
238
+ throw error;
239
+ if (error.code === "ENOENT")
240
+ throw new SharedFileError("file_not_found");
241
+ throw new SharedFileError("file_not_viewable");
242
+ }
243
+ finally {
244
+ activeReads--;
245
+ }
246
+ }