@nowcrew/daemon 0.6.88-beta.27 → 0.6.88-beta.29
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 +7 -0
- package/dist/agent-ability/materializer.js +21 -10
- 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/config.js +7 -0
- package/dist/execution-runner.js +2 -0
- package/dist/list-models.js +12 -12
- package/dist/local-executor.js +20 -48
- 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 +24 -16
- 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/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 +239 -0
- package/dist/provider-env.js +26 -12
- package/dist/runner.js +14 -0
- 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 +2 -2
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness.js +17 -12
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +7 -6
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +58 -105
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/package.json +2 -2
|
@@ -0,0 +1,239 @@
|
|
|
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_CONCURRENT_READS = 4;
|
|
14
|
+
let activeReads = 0;
|
|
15
|
+
export const SharedFileRequestSchema = z.object({
|
|
16
|
+
type: z.literal("fs:read-shared"),
|
|
17
|
+
reqId: z.string().min(1).max(128),
|
|
18
|
+
handle: AgentHandleSchema.refine((handle) => !handle.startsWith(".")),
|
|
19
|
+
path: z.string().min(1).max(4096),
|
|
20
|
+
fileContext: z.object({
|
|
21
|
+
taskKey: z.string().min(1).max(200),
|
|
22
|
+
projectIds: z.array(z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/)).max(100),
|
|
23
|
+
primaryProjectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/).optional(),
|
|
24
|
+
}).strict().refine((value) => !value.primaryProjectId || value.projectIds.includes(value.primaryProjectId)),
|
|
25
|
+
}).strict();
|
|
26
|
+
export class SharedFileError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
constructor(code) {
|
|
29
|
+
super(code);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = "SharedFileError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const inside = (root, target) => {
|
|
35
|
+
const rel = relative(root, target);
|
|
36
|
+
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
|
|
37
|
+
};
|
|
38
|
+
const parts = (path) => path.split(/[\\/]/).filter(Boolean);
|
|
39
|
+
const privatePart = (part) => part.startsWith(".")
|
|
40
|
+
|| /^(?:credentials?|secrets?|tokens?)(?:[._-]|$)/i.test(part)
|
|
41
|
+
|| /^(?:id_rsa|id_ed25519|id_ecdsa|id_dsa|passwd|shadow|authorized_keys|known_hosts)$/i.test(part)
|
|
42
|
+
|| /\.(?:pem|key|p12|pfx|keystore)$/i.test(part);
|
|
43
|
+
function validatePath(path) {
|
|
44
|
+
// path 已由 Server 从 Markdown 解码,禁止再次解释编码、协议或设备路径。
|
|
45
|
+
if (/[\x00-\x1f\x7f]/.test(path) || /%[0-9a-f]{2}/i.test(path)
|
|
46
|
+
|| path.startsWith("\\\\") || path.startsWith("//") || path.startsWith("~")
|
|
47
|
+
|| parts(path).some((part) => part === "..")
|
|
48
|
+
|| (process.platform !== "win32" && (/^[A-Za-z]:/.test(path) || path.includes("\\")))
|
|
49
|
+
|| (process.platform === "win32" && path.slice(2).includes(":"))) {
|
|
50
|
+
throw new SharedFileError("file_not_viewable");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function restrictAgentTask(agentsRoot, agentRoot, path, taskNames) {
|
|
54
|
+
if (!inside(agentsRoot, path))
|
|
55
|
+
return;
|
|
56
|
+
if (!inside(agentRoot, path)) {
|
|
57
|
+
// 旧版托管项目位于 agentsRoot/repos;仍必须经过下方登记项目根检查。
|
|
58
|
+
if (inside(join(agentsRoot, "repos"), path))
|
|
59
|
+
return;
|
|
60
|
+
throw new SharedFileError("file_scope_unavailable");
|
|
61
|
+
}
|
|
62
|
+
const local = parts(relative(agentRoot, path));
|
|
63
|
+
const identity = (value) => process.platform === "win32" ? value.toLowerCase() : value;
|
|
64
|
+
if (identity(local[0] ?? "") === "tasks" && (!local[1] || !taskNames.map(identity).includes(identity(local[1])))) {
|
|
65
|
+
throw new SharedFileError("file_scope_unavailable");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function checkedCandidate(candidate) {
|
|
69
|
+
const rel = relative(candidate.root, candidate.path);
|
|
70
|
+
if (!inside(candidate.root, candidate.path) || parts(rel).some(privatePart) || isInternalEnvironmentPath(candidate.path)) {
|
|
71
|
+
throw new SharedFileError("file_not_viewable");
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const before = await lstat(candidate.root, { bigint: true });
|
|
75
|
+
if (!before.isDirectory())
|
|
76
|
+
throw new SharedFileError("file_not_viewable");
|
|
77
|
+
const root = await realpath(candidate.root);
|
|
78
|
+
if (isInternalEnvironmentPath(root))
|
|
79
|
+
throw new SharedFileError("file_not_viewable");
|
|
80
|
+
const [current, canonical] = await Promise.all([
|
|
81
|
+
lstat(candidate.root, { bigint: true }), lstat(root, { bigint: true }),
|
|
82
|
+
]);
|
|
83
|
+
if (![current, canonical].every((entry) => entry.isDirectory() && entry.dev === before.dev && entry.ino === before.ino)) {
|
|
84
|
+
throw new SharedFileError("file_changed");
|
|
85
|
+
}
|
|
86
|
+
const path = resolve(root, rel);
|
|
87
|
+
await assertRegularPath(root, path);
|
|
88
|
+
return { root, path };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === "ENOENT")
|
|
92
|
+
return null;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function assertRegularPath(root, target) {
|
|
97
|
+
let current = root;
|
|
98
|
+
const segments = parts(relative(root, target));
|
|
99
|
+
if (segments.length === 0)
|
|
100
|
+
throw new SharedFileError("file_not_viewable");
|
|
101
|
+
for (let index = 0; index < segments.length; index++) {
|
|
102
|
+
const segment = segments[index];
|
|
103
|
+
if (privatePart(segment))
|
|
104
|
+
throw new SharedFileError("file_not_viewable");
|
|
105
|
+
current = join(current, segment);
|
|
106
|
+
const entry = await lstat(current);
|
|
107
|
+
if (entry.isSymbolicLink())
|
|
108
|
+
throw new SharedFileError("file_not_viewable");
|
|
109
|
+
if (index < segments.length - 1 ? !entry.isDirectory() : !entry.isFile() || entry.nlink !== 1) {
|
|
110
|
+
throw new SharedFileError("file_not_viewable");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (await realpath(target) !== target)
|
|
114
|
+
throw new SharedFileError("file_not_viewable");
|
|
115
|
+
}
|
|
116
|
+
async function resolveCandidate(agentsRoot, request, registry) {
|
|
117
|
+
const agentRoot = resolve(agentsRoot, request.handle);
|
|
118
|
+
// 根本身不能是符号链接;缺失任务文件由候选阶段归一为 file_not_found。
|
|
119
|
+
try {
|
|
120
|
+
if (!(await lstat(agentRoot)).isDirectory())
|
|
121
|
+
throw new SharedFileError("file_not_viewable");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code !== "ENOENT")
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const taskNames = [...new Set([opaqueKey(request.fileContext.taskKey), safeKey(request.fileContext.taskKey)])];
|
|
128
|
+
const taskRoots = taskNames.map((name) => join(agentRoot, "tasks", name));
|
|
129
|
+
let candidates;
|
|
130
|
+
if (isAbsolute(request.path)) {
|
|
131
|
+
const target = resolve(request.path);
|
|
132
|
+
restrictAgentTask(resolve(agentsRoot), agentRoot, target, taskNames);
|
|
133
|
+
if (inside(agentRoot, target)) {
|
|
134
|
+
candidates = [{ root: agentRoot, path: target }];
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const registrations = await registry.list().catch(() => []);
|
|
138
|
+
const roots = registrations.filter((project) => request.fileContext.projectIds.includes(project.projectId)
|
|
139
|
+
&& isAbsolute(project.root) && resolve(project.root) !== parse(resolve(project.root)).root
|
|
140
|
+
&& inside(resolve(project.root), target));
|
|
141
|
+
if (roots.length === 0)
|
|
142
|
+
throw new SharedFileError("file_scope_unavailable");
|
|
143
|
+
// 嵌套项目取最具体的已授权根;同一实际文件不构成相对路径歧义。
|
|
144
|
+
const root = roots.map((project) => resolve(project.root)).sort((a, b) => b.length - a.length)[0];
|
|
145
|
+
candidates = [{ root, path: target }];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const rel = request.path.replace(/^\.\//, "");
|
|
150
|
+
if (parts(rel).some(privatePart))
|
|
151
|
+
throw new SharedFileError("file_not_viewable");
|
|
152
|
+
const target = resolve(agentRoot, rel);
|
|
153
|
+
restrictAgentTask(resolve(agentsRoot), agentRoot, target, taskNames);
|
|
154
|
+
candidates = [{ root: agentRoot, path: target }, ...taskRoots.map((root) => ({ root: agentRoot, path: resolve(root, rel) }))];
|
|
155
|
+
}
|
|
156
|
+
const resolved = (await Promise.all(candidates.map(checkedCandidate))).filter((v) => v !== null);
|
|
157
|
+
const unique = [...new Map(resolved.map((value) => [value.path, value])).values()];
|
|
158
|
+
if (unique.length === 0)
|
|
159
|
+
throw new SharedFileError("file_not_found");
|
|
160
|
+
if (unique.length !== 1)
|
|
161
|
+
throw new SharedFileError("file_path_ambiguous");
|
|
162
|
+
return unique[0];
|
|
163
|
+
}
|
|
164
|
+
async function readCandidate(candidate) {
|
|
165
|
+
const before = await lstat(candidate.path, { bigint: true });
|
|
166
|
+
// Darwin SDK sys/fcntl.h 的 O_NOFOLLOW_ANY 原子拒绝任何父目录符号链接。
|
|
167
|
+
// Node 尚未导出该常量;Linux 则在下方通过已打开句柄的真实路径核验。
|
|
168
|
+
// Darwin 不允许 O_NOFOLLOW_ANY 与 O_NOFOLLOW 同时使用。
|
|
169
|
+
const noFollow = process.platform === "darwin" ? 0x20000000 : (constants.O_NOFOLLOW ?? 0);
|
|
170
|
+
const handle = await open(candidate.path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | noFollow);
|
|
171
|
+
try {
|
|
172
|
+
const opened = await handle.stat({ bigint: true });
|
|
173
|
+
// 对照打开前身份并重新检查整条路径,拒绝检查与读取之间的替换。
|
|
174
|
+
if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
175
|
+
throw new SharedFileError("file_changed");
|
|
176
|
+
}
|
|
177
|
+
await assertRegularPath(candidate.root, candidate.path);
|
|
178
|
+
if (process.platform === "linux" && await realpath(`/proc/self/fd/${handle.fd}`) !== candidate.path) {
|
|
179
|
+
throw new SharedFileError("file_changed");
|
|
180
|
+
}
|
|
181
|
+
const current = await lstat(candidate.path, { bigint: true });
|
|
182
|
+
if (current.dev !== opened.dev || current.ino !== opened.ino || !current.isFile() || current.nlink !== 1n) {
|
|
183
|
+
throw new SharedFileError("file_changed");
|
|
184
|
+
}
|
|
185
|
+
const buffer = Buffer.alloc(MAX_BYTES);
|
|
186
|
+
let length = 0;
|
|
187
|
+
while (length < buffer.length) {
|
|
188
|
+
const read = await handle.read(buffer, length, buffer.length - length, length);
|
|
189
|
+
if (read.bytesRead === 0)
|
|
190
|
+
break;
|
|
191
|
+
length += read.bytesRead;
|
|
192
|
+
}
|
|
193
|
+
const after = await handle.stat({ bigint: true });
|
|
194
|
+
const currentAfter = await lstat(candidate.path, { bigint: true });
|
|
195
|
+
if (after.size !== opened.size || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs
|
|
196
|
+
|| currentAfter.dev !== opened.dev || currentAfter.ino !== opened.ino || !currentAfter.isFile()) {
|
|
197
|
+
throw new SharedFileError("file_changed");
|
|
198
|
+
}
|
|
199
|
+
const bytes = buffer.subarray(0, length);
|
|
200
|
+
if (bytes.includes(0))
|
|
201
|
+
throw new SharedFileError("file_binary");
|
|
202
|
+
const truncated = opened.size > BigInt(length);
|
|
203
|
+
let content;
|
|
204
|
+
try {
|
|
205
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: truncated });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new SharedFileError("file_binary");
|
|
209
|
+
}
|
|
210
|
+
return { path: relative(candidate.root, candidate.path).split(sep).join("/"), content, size: Number(opened.size), truncated };
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await handle.close();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function readSharedMessageFile(agentsRoot, rawRequest, registry) {
|
|
217
|
+
const parsed = SharedFileRequestSchema.safeParse(rawRequest);
|
|
218
|
+
if (!parsed.success)
|
|
219
|
+
throw new SharedFileError("file_not_viewable");
|
|
220
|
+
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
221
|
+
throw new SharedFileError("file_scope_unavailable");
|
|
222
|
+
validatePath(parsed.data.path);
|
|
223
|
+
if (activeReads >= MAX_CONCURRENT_READS)
|
|
224
|
+
throw new SharedFileError("file_preview_busy");
|
|
225
|
+
activeReads++;
|
|
226
|
+
try {
|
|
227
|
+
return await readCandidate(await resolveCandidate(agentsRoot, parsed.data, registry));
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error instanceof SharedFileError)
|
|
231
|
+
throw error;
|
|
232
|
+
if (error.code === "ENOENT")
|
|
233
|
+
throw new SharedFileError("file_not_found");
|
|
234
|
+
throw new SharedFileError("file_not_viewable");
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
activeReads--;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/dist/provider-env.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
13
|
+
import { mergeRuntimeEnvironments } from "./runtime-environment-merge.js";
|
|
13
14
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
14
15
|
const RESERVED_ENV = new Set([
|
|
15
16
|
"PATH",
|
|
@@ -56,11 +57,12 @@ const CONFLICTING_CODEX_ENV = [
|
|
|
56
57
|
DEEPSEEK_CODEX_KEY_ENV,
|
|
57
58
|
];
|
|
58
59
|
/** 返回叠加了 provider 隔离的新 env(不改入参);不支持的 runtime/provider 组合原样返回。 */
|
|
59
|
-
export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverride) {
|
|
60
|
+
export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverride, platform = process.platform) {
|
|
60
61
|
if (runtime === "codex" && cfg.provider === "deepseek") {
|
|
61
62
|
const env = { ...base };
|
|
62
|
-
for (const key of
|
|
63
|
-
|
|
63
|
+
for (const key of Object.keys(env))
|
|
64
|
+
if (CONFLICTING_CODEX_ENV.includes(platform === "win32" ? key.toUpperCase() : key))
|
|
65
|
+
delete env[key];
|
|
64
66
|
env.CODEX_HOME = deepSeekHomeOverride ?? deepSeekCodexHome(homeDir);
|
|
65
67
|
if (cfg.providerApiKey)
|
|
66
68
|
env[DEEPSEEK_CODEX_KEY_ENV] = cfg.providerApiKey;
|
|
@@ -72,8 +74,9 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
|
|
|
72
74
|
if (runtime !== "claude" || cfg.provider !== "custom")
|
|
73
75
|
return base;
|
|
74
76
|
const env = { ...base };
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
+
for (const key of Object.keys(env))
|
|
78
|
+
if (CONFLICTING_ENV.includes(platform === "win32" ? key.toUpperCase() : key))
|
|
79
|
+
delete env[key];
|
|
77
80
|
if (cfg.providerBaseUrl)
|
|
78
81
|
env.ANTHROPIC_BASE_URL = cfg.providerBaseUrl;
|
|
79
82
|
if (cfg.providerApiKey) {
|
|
@@ -91,20 +94,31 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
|
|
|
91
94
|
env.ANTHROPIC_SMALL_FAST_MODEL = cfg.providerSmallFastModel;
|
|
92
95
|
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = cfg.providerSmallFastModel;
|
|
93
96
|
}
|
|
94
|
-
return env;
|
|
97
|
+
return mergeRuntimeEnvironments([env], platform);
|
|
98
|
+
}
|
|
99
|
+
const deepSeekHarnessMachineOwnedKey = (key, platform) => {
|
|
100
|
+
const identity = platform === "win32" ? key.toUpperCase() : key;
|
|
101
|
+
return identity.startsWith("DEEPSEEK_") || identity.startsWith("NOWCREW_DEEPSEEK_HARNESS_") || identity.startsWith("DSH_");
|
|
102
|
+
};
|
|
103
|
+
/** 子命令切换目录后仍必须保留 provider 的鉴权/路由边界,包括被清除的键。 */
|
|
104
|
+
export function providerOwnedEnvironmentKeys(runtime, cfg, candidateKeys, platform = process.platform) {
|
|
105
|
+
if (runtime === "claude" && cfg.provider === "custom")
|
|
106
|
+
return [...CONFLICTING_ENV, "CLAUDE_CONFIG_DIR"];
|
|
107
|
+
if (runtime === "codex" && cfg.provider === "deepseek")
|
|
108
|
+
return CONFLICTING_CODEX_ENV;
|
|
109
|
+
if (runtime === "deepseek-harness")
|
|
110
|
+
return candidateKeys.filter(key => deepSeekHarnessMachineOwnedKey(key, platform));
|
|
111
|
+
return [];
|
|
95
112
|
}
|
|
96
|
-
const deepSeekHarnessMachineOwnedKey = (key) => (key.toUpperCase().startsWith("DEEPSEEK_")
|
|
97
|
-
|| key.toUpperCase().startsWith("NOWCREW_DEEPSEEK_HARNESS_")
|
|
98
|
-
|| key.toUpperCase().startsWith("DSH_"));
|
|
99
113
|
/** Agent env remains available to tools, but cannot replace Harness process policy or credentials. */
|
|
100
|
-
export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
|
|
114
|
+
export function applyDeepSeekHarnessMachineEnv(base, machineEnv, platform = process.platform) {
|
|
101
115
|
const env = { ...base };
|
|
102
116
|
for (const key of Object.keys(env)) {
|
|
103
|
-
if (deepSeekHarnessMachineOwnedKey(key))
|
|
117
|
+
if (deepSeekHarnessMachineOwnedKey(key, platform))
|
|
104
118
|
delete env[key];
|
|
105
119
|
}
|
|
106
120
|
for (const [key, value] of Object.entries(machineEnv)) {
|
|
107
|
-
if (deepSeekHarnessMachineOwnedKey(key) && value !== undefined)
|
|
121
|
+
if (deepSeekHarnessMachineOwnedKey(key, platform) && value !== undefined)
|
|
108
122
|
env[key] = value;
|
|
109
123
|
}
|
|
110
124
|
return env;
|
package/dist/runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMachineEnvironmentProvider } from "./machine-environment-cache.js";
|
|
1
2
|
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -24,6 +25,18 @@ function runtimeName(value) {
|
|
|
24
25
|
throw new Error(`unsupported runtime: ${value}`);
|
|
25
26
|
}
|
|
26
27
|
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
28
|
+
const owned = dependencies.machineEnvironment === undefined;
|
|
29
|
+
const machineEnvironment = dependencies.machineEnvironment
|
|
30
|
+
?? createMachineEnvironmentProvider({ mode: config.machineEnvironmentMode ?? "off" });
|
|
31
|
+
try {
|
|
32
|
+
return await runAgentWithMachineEnvironment(config, input, onActivity, onConsole, { ...dependencies, machineEnvironment });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (owned)
|
|
36
|
+
await machineEnvironment.stop();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function runAgentWithMachineEnvironment(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
27
40
|
const threadId = input.scheduled ? undefined : input.threadId;
|
|
28
41
|
const taskAnchor = threadId ?? input.wakeMessageId;
|
|
29
42
|
// Old servers have no typed maintenance purpose. A legacy marker can only
|
|
@@ -118,6 +131,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
118
131
|
onRuntimeStarting: () => dependencies.onRuntimePhase?.("starting", runtime),
|
|
119
132
|
onRuntimeRunning: () => dependencies.onRuntimePhase?.("running", runtime),
|
|
120
133
|
}, {
|
|
134
|
+
...(dependencies.machineEnvironment === undefined ? {} : { machineEnvironment: dependencies.machineEnvironment }),
|
|
121
135
|
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
122
136
|
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
123
137
|
...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Keep layer order authoritative even when Windows keys use different casing. */
|
|
2
|
+
export function mergeRuntimeEnvironments(layers, platform = process.platform) {
|
|
3
|
+
const entries = new Map();
|
|
4
|
+
for (const layer of layers) {
|
|
5
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
entries.set(platform === "win32" ? key.toUpperCase() : key, [key, value]);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return Object.fromEntries(entries.values());
|
|
11
|
+
}
|
|
12
|
+
export function runtimeEnvironmentValue(env, key, platform = process.platform) {
|
|
13
|
+
if (platform !== "win32")
|
|
14
|
+
return env?.[key];
|
|
15
|
+
return Object.entries(env ?? {}).find(([candidate]) => candidate.toUpperCase() === key.toUpperCase())?.[1];
|
|
16
|
+
}
|
|
@@ -3,3 +3,13 @@ export const runtimeHealthProbeHandler = (configuredProbe, executionRuntimesKnow
|
|
|
3
3
|
...(executionRuntimesKnown ? { availableRuntimes: detectedExecutionRuntimes } : {}),
|
|
4
4
|
}));
|
|
5
5
|
export const handleRuntimeHealthProbeFrame = (decoded, configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes) => handleRuntimeProbeFrame(decoded, runtimeHealthProbeHandler(configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes));
|
|
6
|
+
/** 将探针结果按既有 RPC 帧返回,serve 只保留路由选择。 */
|
|
7
|
+
export async function replyRuntimeHealthProbe(decoded, configuredProbe, known, runtimes, send) {
|
|
8
|
+
const result = await handleRuntimeHealthProbeFrame(decoded, configuredProbe, known, runtimes);
|
|
9
|
+
if (result !== null) {
|
|
10
|
+
try {
|
|
11
|
+
send({ type: "fs:result", ...result });
|
|
12
|
+
}
|
|
13
|
+
catch { /* Server 查询超时处理断连 */ }
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/runtime-health.js
CHANGED
|
@@ -123,7 +123,9 @@ export async function probeRuntimeHealth(request, dependencies = {}) {
|
|
|
123
123
|
const now = dependencies.now ?? (() => new Date());
|
|
124
124
|
const monotonicNow = dependencies.monotonicNow ?? (() => performance.now());
|
|
125
125
|
const sourceEnv = dependencies.env ?? process.env;
|
|
126
|
-
const env =
|
|
126
|
+
const env = dependencies.env === undefined
|
|
127
|
+
? { ...sourceEnv, PATH: augmentedPath(sourceEnv) }
|
|
128
|
+
: { ...sourceEnv };
|
|
127
129
|
const availableRuntimes = dependencies.availableRuntimes ?? parsed.availableRuntimes;
|
|
128
130
|
const results = [];
|
|
129
131
|
for (const target of parsed.runtimes) {
|
package/dist/runtime-path.js
CHANGED
|
@@ -8,24 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { win32, posix } from "node:path";
|
|
11
|
+
import { runtimeEnvironmentValue } from "./runtime-environment-merge.js";
|
|
11
12
|
const pathApi = (platform) => (platform === "win32" ? win32 : posix);
|
|
12
13
|
/** 该平台常见 CLI 安装目录候选(是否真实存在稍后由 augmentedPath 校验)。 */
|
|
13
14
|
export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
14
15
|
const p = pathApi(platform);
|
|
16
|
+
const value = (key) => runtimeEnvironmentValue(env, key, platform);
|
|
15
17
|
if (platform === "win32") {
|
|
16
18
|
const dirs = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
p.join(
|
|
19
|
+
const userProfile = value("USERPROFILE"), appData = value("APPDATA"), localAppData = value("LOCALAPPDATA");
|
|
20
|
+
if (userProfile) {
|
|
21
|
+
dirs.push(p.join(userProfile, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
22
|
+
p.join(userProfile, ".local", "bin"));
|
|
20
23
|
}
|
|
21
|
-
if (
|
|
22
|
-
dirs.push(p.join(
|
|
23
|
-
if (
|
|
24
|
-
dirs.push(p.join(
|
|
24
|
+
if (appData)
|
|
25
|
+
dirs.push(p.join(appData, "npm"));
|
|
26
|
+
if (localAppData)
|
|
27
|
+
dirs.push(p.join(localAppData, "Microsoft", "WindowsApps"));
|
|
25
28
|
return dirs;
|
|
26
29
|
}
|
|
27
30
|
const dirs = [];
|
|
28
|
-
const home =
|
|
31
|
+
const home = value("HOME");
|
|
29
32
|
if (home) {
|
|
30
33
|
dirs.push(p.join(home, ".kimi-code", "bin"), // Kimi 官方安装器默认落点
|
|
31
34
|
p.join(home, ".local", "bin"), // Claude 官方原生安装器默认落点
|
|
@@ -41,19 +44,26 @@ export function commonBinDirs(env = process.env, platform = process.platform) {
|
|
|
41
44
|
* 在 `env.PATH` 基础上,把候选目录里「真实存在且尚未在 PATH 中」的去重后 prepend,返回新的 PATH 字符串。
|
|
42
45
|
* 用户装的目录优先命中(prepend);全部已存在或都不在磁盘上时,原样返回 `env.PATH`。
|
|
43
46
|
*/
|
|
44
|
-
export function augmentedPath(env = process.env, deps = {}, platform = process.platform) {
|
|
47
|
+
export function augmentedPath(env = process.env, deps = {}, platform = process.platform, placement = "prepend") {
|
|
45
48
|
const dirExists = deps.dirExists ?? existsSync;
|
|
46
49
|
const sep = pathApi(platform).delimiter;
|
|
47
|
-
const current = env
|
|
48
|
-
const
|
|
50
|
+
const current = runtimeEnvironmentValue(env, "PATH", platform) ?? "";
|
|
51
|
+
const keyOf = (entry) => platform === "win32" ? entry.replace(/\//gu, "\\").toLowerCase() : entry;
|
|
52
|
+
const currentEntries = current.split(sep).filter((entry) => entry.length > 0);
|
|
53
|
+
const existing = new Set(currentEntries.map(keyOf));
|
|
49
54
|
const extra = [];
|
|
50
55
|
for (const dir of commonBinDirs(env, platform)) {
|
|
51
|
-
if (existing.has(dir) || extra.
|
|
56
|
+
if (existing.has(keyOf(dir)) || extra.some(entry => keyOf(entry) === keyOf(dir)))
|
|
52
57
|
continue;
|
|
53
58
|
if (!dirExists(dir))
|
|
54
59
|
continue;
|
|
55
60
|
extra.push(dir);
|
|
56
61
|
}
|
|
62
|
+
if (placement === "append") {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
return [...currentEntries, ...extra].filter(entry => { const key = keyOf(entry); if (seen.has(key))
|
|
65
|
+
return false; seen.add(key); return true; }).join(sep);
|
|
66
|
+
}
|
|
57
67
|
if (extra.length === 0)
|
|
58
68
|
return current;
|
|
59
69
|
return current.length > 0 ? `${extra.join(sep)}${sep}${current}` : extra.join(sep);
|
package/dist/runtime-probe.js
CHANGED
|
@@ -9,8 +9,8 @@ export function createRuntimeProbeCoordinator() {
|
|
|
9
9
|
const abort = new AbortController();
|
|
10
10
|
const flights = new Map();
|
|
11
11
|
return {
|
|
12
|
-
detect(installed, detector, connectionId) {
|
|
13
|
-
const key = [...installed].sort()
|
|
12
|
+
detect(installed, detector, connectionId, environmentGeneration = "") {
|
|
13
|
+
const key = JSON.stringify([environmentGeneration, [...installed].sort()]);
|
|
14
14
|
const existing = flights.get(key);
|
|
15
15
|
const probeId = existing?.probeId ?? randomUUID();
|
|
16
16
|
const fields = { probe_id: probeId, connection_id: connectionId, installed_runtimes: JSON.stringify(installed) };
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { isWin } from "../platform.js";
|
|
17
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
17
18
|
const execFileP = promisify(execFile);
|
|
18
19
|
const PROBE_TIMEOUT_MS = 8_000;
|
|
19
20
|
// 本机丢弃端口:连不上也无所谓,init 事件在任何网络请求之前就已输出。
|
|
@@ -26,18 +27,27 @@ const CLAUDE_ALIASES = [
|
|
|
26
27
|
];
|
|
27
28
|
const DEFAULT_ALIAS = "sonnet";
|
|
28
29
|
const cacheByVersion = new Map();
|
|
30
|
+
let cacheByEnvironment = new WeakMap();
|
|
29
31
|
/** "2.1.258 (Claude Code)" → "2.1.258";空/异常输出 → ""。 */
|
|
30
32
|
export function normalizeClaudeVersion(raw) {
|
|
31
33
|
return raw.replace(/\s*\(Claude Code\)\s*$/, "").trim();
|
|
32
34
|
}
|
|
33
|
-
export async function listClaudeModels(deps =
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
export async function listClaudeModels(deps, env, environmentGeneration = "") {
|
|
36
|
+
const probes = deps ?? { probeVersion: () => probeVersion(env), resolveAlias: (alias) => resolveAlias(alias, env) };
|
|
37
|
+
let cache = env ? cacheByEnvironment.get(env) : cacheByVersion;
|
|
38
|
+
if (!cache) {
|
|
39
|
+
cache = new Map();
|
|
40
|
+
if (env)
|
|
41
|
+
cacheByEnvironment.set(env, cache);
|
|
42
|
+
}
|
|
43
|
+
const version = normalizeClaudeVersion(await probes.probeVersion());
|
|
44
|
+
const cacheKey = JSON.stringify([environmentGeneration, version]);
|
|
45
|
+
const cached = version ? cache.get(cacheKey) : undefined;
|
|
36
46
|
if (cached)
|
|
37
47
|
return cached;
|
|
38
48
|
const resolved = await Promise.all(CLAUDE_ALIASES.map(async ([alias]) => {
|
|
39
49
|
try {
|
|
40
|
-
return await
|
|
50
|
+
return await probes.resolveAlias(alias);
|
|
41
51
|
}
|
|
42
52
|
catch {
|
|
43
53
|
return null;
|
|
@@ -53,22 +63,24 @@ export async function listClaudeModels(deps = defaultDeps) {
|
|
|
53
63
|
};
|
|
54
64
|
});
|
|
55
65
|
if (version && resolved.every((model) => model !== null))
|
|
56
|
-
|
|
66
|
+
cache.set(cacheKey, models);
|
|
57
67
|
return models;
|
|
58
68
|
}
|
|
59
69
|
/** 仅测试用:清空版本缓存。 */
|
|
60
70
|
export function resetClaudeModelCache() {
|
|
61
71
|
cacheByVersion.clear();
|
|
72
|
+
cacheByEnvironment = new WeakMap();
|
|
62
73
|
}
|
|
63
|
-
async function probeVersion() {
|
|
74
|
+
async function probeVersion(env = { ...process.env, PATH: augmentedPath() }) {
|
|
64
75
|
const { stdout } = await execFileP("claude", ["--version"], {
|
|
65
76
|
shell: isWin(),
|
|
66
77
|
timeout: PROBE_TIMEOUT_MS,
|
|
67
78
|
killSignal: "SIGKILL",
|
|
79
|
+
env,
|
|
68
80
|
});
|
|
69
81
|
return stdout;
|
|
70
82
|
}
|
|
71
|
-
function resolveAlias(alias) {
|
|
83
|
+
function resolveAlias(alias, env = { ...process.env, PATH: augmentedPath() }) {
|
|
72
84
|
return new Promise((resolve) => {
|
|
73
85
|
const child = spawn("claude", [
|
|
74
86
|
"--model", alias,
|
|
@@ -78,7 +90,7 @@ function resolveAlias(alias) {
|
|
|
78
90
|
], {
|
|
79
91
|
cwd: tmpdir(),
|
|
80
92
|
env: {
|
|
81
|
-
...
|
|
93
|
+
...env,
|
|
82
94
|
ANTHROPIC_BASE_URL: UNROUTABLE_API_BASE,
|
|
83
95
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
84
96
|
},
|
|
@@ -120,4 +132,3 @@ export function initModelOf(line) {
|
|
|
120
132
|
return null;
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
|
-
const defaultDeps = { probeVersion, resolveAlias };
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
-
import { isAbsolute } from "node:path";
|
|
2
|
+
import { isAbsolute, win32, join } from "node:path";
|
|
3
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
5
|
import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
|
|
7
6
|
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
8
7
|
import { probeKimiAcp, AcpProbeCleanupError } from "./kimi-acp-runner.js";
|
|
8
|
+
import { runtimeEnvironmentValue } from "../runtime-environment-merge.js";
|
|
9
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
9
10
|
export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
|
|
10
11
|
export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
|
|
11
|
-
export function deepSeekHarnessLauncher(env = process.env) {
|
|
12
|
-
const configPath = env
|
|
13
|
-
const patchPath = env
|
|
14
|
-
|
|
12
|
+
export function deepSeekHarnessLauncher(env = process.env, platform = process.platform) {
|
|
13
|
+
const configPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
|
|
14
|
+
const patchPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_PATCH_ENV, platform)?.trim();
|
|
15
|
+
const absolute = platform === "win32" ? win32.isAbsolute : isAbsolute;
|
|
16
|
+
if (configPath && !absolute(configPath))
|
|
15
17
|
throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
|
|
16
|
-
if (patchPath && !
|
|
18
|
+
if (patchPath && !absolute(patchPath))
|
|
17
19
|
throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
|
|
18
20
|
if (configPath && patchPath)
|
|
19
21
|
throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
|
|
@@ -21,24 +23,27 @@ export function deepSeekHarnessLauncher(env = process.env) {
|
|
|
21
23
|
? { kind: "legacy", bin: "dsh-acp-demo", configPath }
|
|
22
24
|
: { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
|
|
23
25
|
}
|
|
24
|
-
export function deepSeekHarnessConfigPath(env = process.env) {
|
|
25
|
-
const path = env
|
|
26
|
-
return path && isAbsolute(path) ? path : null;
|
|
26
|
+
export function deepSeekHarnessConfigPath(env = process.env, platform = process.platform) {
|
|
27
|
+
const path = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
|
|
28
|
+
return path && (platform === "win32" ? win32.isAbsolute(path) : isAbsolute(path)) ? path : null;
|
|
27
29
|
}
|
|
28
30
|
/** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
|
|
29
31
|
export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
30
32
|
if (options.signal?.aborted)
|
|
31
33
|
return false;
|
|
34
|
+
const sourceEnv = options.env ?? { ...process.env, PATH: augmentedPath() };
|
|
32
35
|
let root;
|
|
33
36
|
try {
|
|
34
37
|
const launcher = options.configPath === undefined
|
|
35
|
-
? deepSeekHarnessLauncher()
|
|
38
|
+
? deepSeekHarnessLauncher(sourceEnv)
|
|
36
39
|
: deepSeekHarnessLauncher({
|
|
40
|
+
...sourceEnv,
|
|
37
41
|
[DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
|
|
38
42
|
});
|
|
39
43
|
if (launcher.kind === "legacy")
|
|
40
44
|
return await probeKimiAcp({
|
|
41
45
|
...options,
|
|
46
|
+
env: sourceEnv,
|
|
42
47
|
provider: "deepseek-harness",
|
|
43
48
|
configPath: launcher.configPath,
|
|
44
49
|
effectivePermission: "sandboxed",
|
|
@@ -50,7 +55,7 @@ export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
|
50
55
|
systemPrompt: "",
|
|
51
56
|
skillRoots: [],
|
|
52
57
|
effectivePermission: "sandboxed",
|
|
53
|
-
env:
|
|
58
|
+
env: sourceEnv,
|
|
54
59
|
...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
|
|
55
60
|
});
|
|
56
61
|
const launch = deepSeekHarnessLaunch(options.bin, profile.args);
|
|
@@ -71,9 +71,10 @@ async function stopChild(child) {
|
|
|
71
71
|
}
|
|
72
72
|
export async function listHermesModels(options = {}) {
|
|
73
73
|
const spawnProcess = options.spawnProcess ?? spawn;
|
|
74
|
+
const env = options.env ?? { ...process.env, PATH: augmentedPath() };
|
|
74
75
|
const child = spawnProcess("hermes", ["acp"], {
|
|
75
76
|
cwd: process.cwd(),
|
|
76
|
-
env
|
|
77
|
+
env,
|
|
77
78
|
stdio: ["pipe", "pipe", "pipe"],
|
|
78
79
|
});
|
|
79
80
|
if (child.stdin === null || child.stdout === null || child.stderr === null)
|