@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.30
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 +89 -14
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/agent-memory/policy.js +3 -1
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +16 -10
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +13 -11
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +88 -92
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +20 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +31 -19
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +84 -15
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +95 -184
- package/dist/server-url.js +25 -0
- package/dist/session.js +4 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +5 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** 固定市场操作的短命 CLI 适配;原始命令输出不离开设备。 */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFile, readdir, realpath, lstat, mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { join, resolve, isAbsolute, relative, sep } from "node:path";
|
|
8
|
+
import { applyProviderEnv } from "./provider-env.js";
|
|
9
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
10
|
+
import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
|
|
13
|
+
const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
|
|
14
|
+
const SOURCE_PATH = "plugins/AIinterview-plugin";
|
|
15
|
+
export class MarketplaceFailure extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code) {
|
|
18
|
+
super(code);
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const runMarketplaceCommand = async (bin, args, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await exec(bin, [...args], { ...options, timeout: 120_000, maxBuffer: 4 * 1024 * 1024,
|
|
25
|
+
killSignal: "SIGKILL", encoding: "utf8" });
|
|
26
|
+
return result.stdout;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const value = error;
|
|
30
|
+
throw new MarketplaceFailure(value.code === "ENOENT" ? "cli_missing" : value.killed ? "command_timeout" : "command_failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
34
|
+
const parse = (text) => { try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
39
|
+
} };
|
|
40
|
+
const version = (x) => typeof x === "string" && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/.test(x) ? x : null;
|
|
41
|
+
const sha = (x) => typeof x === "string" && /^[a-f0-9]{40}$/.test(x) ? x : null;
|
|
42
|
+
const within = (root, candidate) => {
|
|
43
|
+
const child = relative(root, candidate);
|
|
44
|
+
return child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child));
|
|
45
|
+
};
|
|
46
|
+
const canonical = async (path) => realpath(path).catch(() => resolve(path));
|
|
47
|
+
export async function resolveMarketplaceScopes(targets, agentsRoot, inherited = process.env) {
|
|
48
|
+
const scopes = new Map();
|
|
49
|
+
const trustedHome = await canonical(inherited.HOME ?? homedir());
|
|
50
|
+
for (const target of targets) {
|
|
51
|
+
const home = target.handle ? join(agentsRoot, target.handle, ".home") : homedir();
|
|
52
|
+
const agentHome = target.handle ? await canonical(home) : null;
|
|
53
|
+
const allowedHomeRoots = [trustedHome, ...(agentHome === null ? [] : [agentHome])];
|
|
54
|
+
const sharedConfigRoot = join(trustedHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
55
|
+
const agentConfigRoot = agentHome === null ? null : join(agentHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
56
|
+
const allowedConfigRoots = [sharedConfigRoot, ...(agentConfigRoot === null ? [] : [agentConfigRoot])];
|
|
57
|
+
const suppliedHome = target.paths.HOME;
|
|
58
|
+
if (suppliedHome !== undefined && !within(allowedHomeRoots[0], await canonical(suppliedHome))
|
|
59
|
+
&& (agentHome === null || !within(agentHome, await canonical(suppliedHome))))
|
|
60
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
61
|
+
const suppliedConfig = target.runtime === "codex" ? target.paths.CODEX_HOME : target.paths.CLAUDE_CONFIG_DIR;
|
|
62
|
+
const suppliedConfigPath = suppliedConfig === undefined ? null : await canonical(suppliedConfig);
|
|
63
|
+
if (suppliedConfigPath !== null && !allowedConfigRoots.some(root => within(root, suppliedConfigPath)))
|
|
64
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
65
|
+
// 与 local-executor 使用同一 provider 解析;凭据存在性只用于选择已配置的 provider,不传递凭据内容。
|
|
66
|
+
const env = applyProviderEnv({ ...inherited, ...target.paths }, target.runtime, target.handle ? {
|
|
67
|
+
provider: target.provider, providerApiKey: target.hasProviderKey ? "configured" : null,
|
|
68
|
+
} : {}, home);
|
|
69
|
+
for (const key of Object.keys(env))
|
|
70
|
+
if (/^(CREW_|ANTHROPIC_|OPENAI_|DEEPSEEK_)/.test(key))
|
|
71
|
+
delete env[key];
|
|
72
|
+
for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"])
|
|
73
|
+
delete env[key];
|
|
74
|
+
env.PATH = augmentedPath(inherited);
|
|
75
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
76
|
+
env.CI = "1";
|
|
77
|
+
const raw = target.runtime === "codex" ? env.CODEX_HOME ?? join(env.HOME ?? homedir(), ".codex")
|
|
78
|
+
: env.CLAUDE_CONFIG_DIR ?? join(env.HOME ?? homedir(), ".claude");
|
|
79
|
+
if (!isAbsolute(raw))
|
|
80
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
81
|
+
const configHome = await realpath(raw).catch(() => resolve(raw));
|
|
82
|
+
if (!allowedConfigRoots.some(root => within(root, configHome)))
|
|
83
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
84
|
+
if (target.runtime === "codex")
|
|
85
|
+
env.CODEX_HOME = configHome;
|
|
86
|
+
else
|
|
87
|
+
env.CLAUDE_CONFIG_DIR = configHome;
|
|
88
|
+
const key = `${target.runtime}:${configHome}`;
|
|
89
|
+
const prior = scopes.get(key);
|
|
90
|
+
if (prior) {
|
|
91
|
+
if (target.handle && !prior.agents.includes(target.handle))
|
|
92
|
+
prior.agents.push(target.handle);
|
|
93
|
+
prior.shared ||= !target.handle;
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
scopes.set(key, { runtime: target.runtime, configHome, env, agents: target.handle ? [target.handle] : [], shared: !target.handle });
|
|
97
|
+
}
|
|
98
|
+
return [...scopes.values()];
|
|
99
|
+
}
|
|
100
|
+
export function parsePluginInventory(runtime, value, configHome) {
|
|
101
|
+
const rows = Array.isArray(value) ? value : obj(value).installed;
|
|
102
|
+
if (!Array.isArray(rows))
|
|
103
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
104
|
+
const found = rows.map(obj).filter(x => (x.id ?? x.pluginId) === SELECTOR);
|
|
105
|
+
if (found.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
if (found.length !== 1)
|
|
108
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
109
|
+
const item = found[0];
|
|
110
|
+
const v = version(item.version);
|
|
111
|
+
if (!v || typeof item.enabled !== "boolean")
|
|
112
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
113
|
+
if (runtime === "claude" && item.scope !== "user")
|
|
114
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
115
|
+
const path = runtime === "claude" ? item.installPath : join(configHome, "plugins/cache", MARKETPLACE, PLUGIN, v);
|
|
116
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
117
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
118
|
+
return { version: v, enabled: item.enabled, path, revision: sha(obj(item.source).sha) };
|
|
119
|
+
}
|
|
120
|
+
/** 逐字节比对固定 Git 源子目录,不能只凭 CLI 退出码或版本字符串证明安装完成。 */
|
|
121
|
+
export async function verifyInstalledContent(path, revision, env, command) {
|
|
122
|
+
const temporary = await mkdtemp(join(tmpdir(), "nowwork-plugin-verify-"));
|
|
123
|
+
const options = { env, cwd: temporary };
|
|
124
|
+
try {
|
|
125
|
+
await command("git", ["init", "--bare", "."], options);
|
|
126
|
+
await command("git", ["fetch", "--depth=1", SOURCE, revision], options);
|
|
127
|
+
const tree = await command("git", ["ls-tree", "-rz", `${revision}:${SOURCE_PATH}`], options);
|
|
128
|
+
const expected = new Map();
|
|
129
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
130
|
+
const match = /^(100644|100755) blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
|
|
131
|
+
if (!match || match[3].split("/").some(x => x === ".." || x === "."))
|
|
132
|
+
throw new MarketplaceFailure("verification_failed");
|
|
133
|
+
expected.set(match[3], match[2]);
|
|
134
|
+
}
|
|
135
|
+
if (!expected.size || expected.size > 2000)
|
|
136
|
+
throw new MarketplaceFailure("verification_failed");
|
|
137
|
+
const actual = new Map();
|
|
138
|
+
let bytes = 0;
|
|
139
|
+
const walk = async (dir, prefix = "") => {
|
|
140
|
+
for (const name of await readdir(dir)) {
|
|
141
|
+
if (name === "__pycache__" || name === ".DS_Store")
|
|
142
|
+
continue;
|
|
143
|
+
const entry = join(dir, name);
|
|
144
|
+
const key = prefix + name;
|
|
145
|
+
const info = await lstat(entry);
|
|
146
|
+
if (info.isSymbolicLink())
|
|
147
|
+
throw new MarketplaceFailure("verification_failed");
|
|
148
|
+
if (info.isDirectory())
|
|
149
|
+
await walk(entry, key + "/");
|
|
150
|
+
else if (info.isFile()) {
|
|
151
|
+
bytes += info.size;
|
|
152
|
+
if (bytes > 100 * 1024 * 1024 || actual.size > 2000)
|
|
153
|
+
throw new MarketplaceFailure("verification_failed");
|
|
154
|
+
const data = await readFile(entry);
|
|
155
|
+
actual.set(key, createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex"));
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
throw new MarketplaceFailure("verification_failed");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
await walk(path);
|
|
162
|
+
if (actual.size !== expected.size || [...expected].some(([name, hash]) => actual.get(name) !== hash))
|
|
163
|
+
throw new MarketplaceFailure("verification_failed");
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await rm(temporary, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function runMarketplaceScope(scope, mode, command = runMarketplaceCommand, verify = verifyInstalledContent) {
|
|
170
|
+
let result = { runtime: scope.runtime, scopeId: createHash("sha256").update(`${scope.runtime}:${scope.configHome}`).digest("hex").slice(0, 16),
|
|
171
|
+
agents: scope.agents, shared: scope.shared, status: "failed", beforeVersion: null, afterVersion: null, targetVersion: null,
|
|
172
|
+
marketRevision: null, installedRevision: null, enabled: null, activation: "unverified", errorCode: null };
|
|
173
|
+
const options = { env: scope.env, cwd: scope.configHome };
|
|
174
|
+
const run = (args) => command(scope.runtime, args, options);
|
|
175
|
+
const inventory = async () => parsePluginInventory(scope.runtime, parse(await run(["plugin", "list", "--json"])), scope.configHome);
|
|
176
|
+
try {
|
|
177
|
+
if (!(await lstat(scope.configHome)).isDirectory())
|
|
178
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
179
|
+
const markets = parse(await run(["plugin", "marketplace", "list", "--json"]));
|
|
180
|
+
const rows = Array.isArray(markets) ? markets : obj(markets).marketplaces;
|
|
181
|
+
if (!Array.isArray(rows))
|
|
182
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
183
|
+
const market = rows.map(obj).filter(x => x.name === MARKETPLACE);
|
|
184
|
+
if (market.length !== 1)
|
|
185
|
+
throw new MarketplaceFailure("market_not_registered");
|
|
186
|
+
const root = market[0].root ?? market[0].installLocation;
|
|
187
|
+
if (typeof root !== "string" || !isAbsolute(root))
|
|
188
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
189
|
+
const before = await inventory();
|
|
190
|
+
result = { ...result, beforeVersion: before?.version ?? null, afterVersion: before?.version ?? null, enabled: before?.enabled ?? null };
|
|
191
|
+
if (mode !== "inspect")
|
|
192
|
+
await run(["plugin", "marketplace", scope.runtime === "claude" ? "update" : "upgrade", MARKETPLACE]);
|
|
193
|
+
const revision = sha((await command("git", ["-C", root, "rev-parse", "HEAD"], options)).trim());
|
|
194
|
+
if (!revision)
|
|
195
|
+
throw new MarketplaceFailure("verification_failed");
|
|
196
|
+
const manifest = obj(parse(await readFile(join(root, scope.runtime === "claude" ? ".claude-plugin" : ".agents/plugins", "marketplace.json"), "utf8")));
|
|
197
|
+
const plugins = manifest.plugins;
|
|
198
|
+
const target = Array.isArray(plugins) ? plugins.map(obj).find(x => x.name === PLUGIN) : undefined;
|
|
199
|
+
const source = obj(target?.source);
|
|
200
|
+
const targetVersion = version(target?.version);
|
|
201
|
+
const targetSha = sha(source.sha);
|
|
202
|
+
result = { ...result, marketRevision: revision, targetVersion };
|
|
203
|
+
if (mode === "refresh")
|
|
204
|
+
return { ...result, status: "refreshed", activation: "not_changed" };
|
|
205
|
+
if (!before)
|
|
206
|
+
return { ...result, status: "not_installed", activation: "not_changed" };
|
|
207
|
+
if (mode === "inspect") {
|
|
208
|
+
const installed = obj(parse(await readFile(join(before.path, scope.runtime === "claude" ? ".claude-plugin" : ".codex-plugin", "plugin.json"), "utf8")));
|
|
209
|
+
const actualVersion = version(installed.version);
|
|
210
|
+
result = { ...result, afterVersion: actualVersion };
|
|
211
|
+
if (installed.name !== PLUGIN || actualVersion !== before.version)
|
|
212
|
+
throw new MarketplaceFailure("verification_failed");
|
|
213
|
+
// 清单可能来自市场元数据;没有文件内容核验就不报告已安装源提交。
|
|
214
|
+
return { ...result, status: "checked", installedRevision: null, activation: "unverified" };
|
|
215
|
+
}
|
|
216
|
+
if (!targetVersion || !targetSha || source.url !== SOURCE || source.path !== SOURCE_PATH || source.source !== "git-subdir")
|
|
217
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
218
|
+
// Codex add 会启用插件;不偷偷改变用户已禁用的状态。
|
|
219
|
+
if (!before.enabled)
|
|
220
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
221
|
+
await run(scope.runtime === "claude" ? ["plugin", "update", SELECTOR, "--scope", "user", "--json"] : ["plugin", "add", SELECTOR, "--json"]);
|
|
222
|
+
const after = await inventory();
|
|
223
|
+
result = { ...result, afterVersion: after?.version ?? null, enabled: after?.enabled ?? null };
|
|
224
|
+
if (!after || after.version !== targetVersion || after.enabled !== before.enabled)
|
|
225
|
+
throw new MarketplaceFailure("verification_failed");
|
|
226
|
+
await verify(after.path, targetSha, scope.env, command);
|
|
227
|
+
return { ...result, status: before.version === after.version && before.revision === targetSha ? "unchanged" : "updated",
|
|
228
|
+
installedRevision: targetSha, activation: "reload_required" };
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const code = error instanceof MarketplaceFailure ? error.code : "scope_unavailable";
|
|
232
|
+
return { ...result, status: code === "cli_missing" ? "missing_cli" : "failed", errorCode: code };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
export function isolateMaintenanceSession(input) {
|
|
3
|
+
if (input.maintenancePurpose === undefined)
|
|
4
|
+
return input;
|
|
5
|
+
const { resumeKey: _ignored, ...rest } = input;
|
|
6
|
+
return { ...rest, session: { ...input.session, enabled: false } };
|
|
7
|
+
}
|
|
8
|
+
/** A per-execution runtime handoff, not a transcript or daemon business store. */
|
|
9
|
+
export class NativeThreadContext {
|
|
10
|
+
path;
|
|
11
|
+
threadRoot;
|
|
12
|
+
runtime;
|
|
13
|
+
requestedSessionId;
|
|
14
|
+
prior;
|
|
15
|
+
confirmed = false;
|
|
16
|
+
constructor(path, threadRoot, runtime, requestedSessionId, prior) {
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.threadRoot = threadRoot;
|
|
19
|
+
this.runtime = runtime;
|
|
20
|
+
this.requestedSessionId = requestedSessionId;
|
|
21
|
+
this.prior = prior;
|
|
22
|
+
}
|
|
23
|
+
snapshot() {
|
|
24
|
+
const reusable = this.requestedSessionId !== null && this.prior?.sessionId === this.requestedSessionId
|
|
25
|
+
&& this.prior?.threadRoot === this.threadRoot && this.prior?.runtime === this.runtime;
|
|
26
|
+
return { schemaVersion: 1, threadRoot: this.threadRoot, resumeConfirmed: this.confirmed,
|
|
27
|
+
historyComplete: false, readSeq: 0,
|
|
28
|
+
resumeCandidate: reusable ? { sessionId: this.requestedSessionId,
|
|
29
|
+
historyComplete: this.prior?.threadHistoryComplete === true, readSeq: this.prior?.threadContextSeq ?? 0 } : null };
|
|
30
|
+
}
|
|
31
|
+
async initialize() { await writeFile(this.path, JSON.stringify(this.snapshot()), { mode: 0o600 }); }
|
|
32
|
+
observe(raw) {
|
|
33
|
+
if (this.confirmed || !this.requestedSessionId || !raw || typeof raw !== "object")
|
|
34
|
+
return;
|
|
35
|
+
const event = raw;
|
|
36
|
+
const confirmed = (this.runtime === "claude" || this.runtime === "codex")
|
|
37
|
+
&& event.type === "nowwork.native_resume" && event.session_id === this.requestedSessionId
|
|
38
|
+
&& (event.runtime === undefined || event.runtime === this.runtime);
|
|
39
|
+
if (!confirmed)
|
|
40
|
+
return;
|
|
41
|
+
this.confirmed = true;
|
|
42
|
+
// Telemetry only. The native adapter verifies its handshake directly and
|
|
43
|
+
// commits accepted input itself. Writing here could race that commit and
|
|
44
|
+
// reset the delivered cursor when parent stdout consumption is delayed.
|
|
45
|
+
}
|
|
46
|
+
async checkpoint() {
|
|
47
|
+
try {
|
|
48
|
+
const state = JSON.parse(await readFile(this.path, "utf8"));
|
|
49
|
+
if (state.schemaVersion !== 1 || state.threadRoot !== this.threadRoot
|
|
50
|
+
|| !Number.isSafeInteger(state.readSeq) || state.readSeq < 0)
|
|
51
|
+
return null;
|
|
52
|
+
return { threadContextSeq: state.readSeq, threadHistoryComplete: state.historyComplete === true };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async cleanup() { await rm(this.path, { force: true }); }
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-protocol.js";
|
|
2
|
+
import { hashExecutionSpec } from "./execution-runner.js";
|
|
3
|
+
import { dslog } from "./slog.js";
|
|
4
|
+
/** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
|
|
5
|
+
export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
|
|
6
|
+
const id = spec.executionId;
|
|
7
|
+
const hash = hashExecutionSpec(spec);
|
|
8
|
+
if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
|
|
9
|
+
// A same-ID request won while the caller awaited journal.get. Its journal may
|
|
10
|
+
// still be pending; silence lets its real result arrive instead of rejecting it.
|
|
11
|
+
const existing = await context.journal.get(id);
|
|
12
|
+
if (existing?.specHash === hash)
|
|
13
|
+
context.replay(existing);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
context.knownExecutionHashes.set(id, hash);
|
|
17
|
+
const rejected = ExecutionRejectedSchema.parse({
|
|
18
|
+
type: "execution:rejected", protocolVersion: 1, executionId: id,
|
|
19
|
+
reason: "resource_limit", message: "Local machine execution queue is full", at: new Date().toISOString(),
|
|
20
|
+
});
|
|
21
|
+
dslog("execution.machine_queue_rejected", "机器执行队列已满", {
|
|
22
|
+
level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
|
|
23
|
+
});
|
|
24
|
+
const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
|
|
25
|
+
.then(async (entry) => {
|
|
26
|
+
if (entry)
|
|
27
|
+
await context.report({ ...rejected, preAdmissionProof: { specHash: hash } });
|
|
28
|
+
else {
|
|
29
|
+
const existing = await context.journal.get(id);
|
|
30
|
+
if (existing?.specHash === hash)
|
|
31
|
+
context.replay(existing);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.catch(error => {
|
|
35
|
+
// A failed durable write has no safe rejection proof; a restart/sync will
|
|
36
|
+
// reconcile an accepted tombstone if this admission created one.
|
|
37
|
+
dslog("execution.admission_rejection_failed", "准入拒绝未取得持久停止证明", {
|
|
38
|
+
level: "ERROR", execution_id: id, error_message: String(error),
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
if (context.executionRuns.get(id) === rejecting) {
|
|
43
|
+
context.executionRuns.delete(id);
|
|
44
|
+
context.knownExecutionHashes.delete(id);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
context.executionRuns.set(id, rejecting);
|
|
48
|
+
await rejecting;
|
|
49
|
+
}
|
|
50
|
+
/** The caller synchronously owns this admission ID. Never complete somebody else's record. */
|
|
51
|
+
export async function persistPreAdmissionRejection(journal, spec, rejection) {
|
|
52
|
+
const accepted = await journal.accept(spec.executionId, hashExecutionSpec(spec), {
|
|
53
|
+
runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
|
|
54
|
+
});
|
|
55
|
+
if (accepted.kind !== "created")
|
|
56
|
+
return null;
|
|
57
|
+
return journal.complete(spec.executionId, ExecutionCompletedSchema.parse({
|
|
58
|
+
type: "execution:completed", protocolVersion: 1, executionId: spec.executionId,
|
|
59
|
+
outcome: "failed", errorCode: rejection.reason, errorMessage: rejection.message ?? "Admission rejected before runtime start",
|
|
60
|
+
runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
|
|
61
|
+
startedAt: accepted.acceptedAt,
|
|
62
|
+
finishedAt: new Date(Math.max(Date.now(), Date.parse(accepted.acceptedAt))).toISOString(),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -9,10 +9,11 @@ export function projectSkillExecutionProjection(bindings, generation) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills
|
|
12
|
+
* 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时支持受控目录的执行
|
|
13
13
|
* 才会有投影目录,也才需要在停止无法证明时保留它。两处判定分开写过一次,结果 kimi + projectSkills
|
|
14
14
|
* 会在取消路径上错抛 ownership 未确认。
|
|
15
15
|
*/
|
|
16
16
|
export const projectSkillProjectionApplies = (spec) => spec.agent.projectSkills !== undefined
|
|
17
|
-
&& (spec.runtime.name === "codex" || spec.runtime.name === "claude"
|
|
17
|
+
&& (spec.runtime.name === "codex" || spec.runtime.name === "claude"
|
|
18
|
+
|| spec.runtime.name === "deepseek-harness");
|
|
18
19
|
export const projectSkillProjectionErrorCode = (error) => error instanceof ProjectProjectionError ? error.code : null;
|