@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.
Files changed (43) hide show
  1. package/README.md +7 -0
  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/config.js +7 -0
  8. package/dist/execution-runner.js +2 -0
  9. package/dist/list-models.js +12 -12
  10. package/dist/local-executor.js +20 -48
  11. package/dist/local-runtime-environment.js +50 -0
  12. package/dist/machine-environment-cache.js +147 -0
  13. package/dist/machine-environment-contract.js +10 -0
  14. package/dist/machine-environment-control.js +27 -0
  15. package/dist/machine-environment-policy.js +75 -0
  16. package/dist/machine-environment-process.js +110 -0
  17. package/dist/machine-info.js +24 -16
  18. package/dist/machine-runtime-environment.js +189 -0
  19. package/dist/machine-shell-environment-capture.js +135 -0
  20. package/dist/machine-windows-environment-capture.js +215 -0
  21. package/dist/machine-windows-environment-script.js +158 -0
  22. package/dist/marketplace-controller.js +75 -0
  23. package/dist/marketplace-protocol.js +38 -0
  24. package/dist/marketplace-runner.js +234 -0
  25. package/dist/marketplace-serve.js +17 -0
  26. package/dist/message-file-preview.js +239 -0
  27. package/dist/provider-env.js +26 -12
  28. package/dist/runner.js +14 -0
  29. package/dist/runtime-environment-merge.js +16 -0
  30. package/dist/runtime-health-handler.js +10 -0
  31. package/dist/runtime-health.js +3 -1
  32. package/dist/runtime-path.js +22 -12
  33. package/dist/runtime-probe.js +2 -2
  34. package/dist/runtimes/claude-models.js +20 -9
  35. package/dist/runtimes/deepseek-harness.js +17 -12
  36. package/dist/runtimes/hermes-models.js +2 -1
  37. package/dist/runtimes/kimi-acp-runner.js +7 -6
  38. package/dist/runtimes/opencode.js +2 -2
  39. package/dist/serve.js +58 -105
  40. package/dist/token.js +0 -1
  41. package/dist/workspace-control-handler.js +27 -0
  42. package/dist/workspace-fs.js +66 -15
  43. package/package.json +2 -2
@@ -136,10 +136,10 @@ function deepSeekPermissionMode(permission) {
136
136
  return "workspace-write";
137
137
  return "danger-full-access";
138
138
  }
139
- function providerEnv(provider, permission) {
139
+ function providerEnv(provider, permission, env = process.env) {
140
140
  return provider === "deepseek-harness"
141
- ? { ...process.env, DSH_PERMISSION_MODE: deepSeekPermissionMode(permission) }
142
- : process.env;
141
+ ? { ...env, DSH_PERMISSION_MODE: deepSeekPermissionMode(permission) }
142
+ : env;
143
143
  }
144
144
  function providerDisplayName(provider) {
145
145
  if (provider === "deepseek-harness")
@@ -220,11 +220,12 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
220
220
  ...transport.fields(), spawn_confirmed_ms: spawnConfirmedMs, child_pid: child.pid,
221
221
  reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
222
222
  dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
223
- const launchEnv = options.launchEnv ?? providerEnv(provider, permission);
223
+ const sourceEnv = options.env ?? { ...process.env, PATH: augmentedPath() };
224
+ const launchEnv = options.launchEnv ?? providerEnv(provider, permission, sourceEnv);
224
225
  const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
225
226
  cwd: options.launchCwd ?? process.cwd(),
226
- // 显式 launchEnv 也决定 PATH/HOME;不能把它与父进程 PATH 混用。
227
- env: { ...launchEnv, PATH: augmentedPath(launchEnv) },
227
+ // 显式 launchEnv 已准备好 PATH/HOME,不能二次补 PATH 或混入父进程环境。
228
+ env: launchEnv,
228
229
  stdio: ["pipe", "pipe", "pipe"],
229
230
  });
230
231
  // spawn errors are emitted by ChildProcess, not its stdio streams. A missing
@@ -11,7 +11,7 @@ const REQUIRED_RUN_FLAGS = [
11
11
  "--variant",
12
12
  "--session",
13
13
  ];
14
- export async function probeOpenCodeRun(signal) {
14
+ export async function probeOpenCodeRun(signal, env = { ...process.env, PATH: augmentedPath() }) {
15
15
  try {
16
16
  const { stdout, stderr } = await execFileP("opencode", ["run", "--help"], {
17
17
  shell: isWin(),
@@ -19,7 +19,7 @@ export async function probeOpenCodeRun(signal) {
19
19
  killSignal: "SIGKILL",
20
20
  maxBuffer: 1024 * 1024,
21
21
  signal,
22
- env: { ...process.env, PATH: augmentedPath() },
22
+ env,
23
23
  });
24
24
  const help = `${stdout}\n${stderr}`;
25
25
  return REQUIRED_RUN_FLAGS.every((flag) => help.includes(flag));
package/dist/serve.js CHANGED
@@ -1,7 +1,7 @@
1
- /**
2
- * 常驻模式 (M3c):连接 server 控制面 WS,注册机器,监听 agent:start 自动唤醒 agent。
3
- * 断线指数退避重连。一个 agent:start 进来就跑一次现有的 runAgent。
4
- */
1
+ /** 常驻控制面桥接;按请求拉起本地 runtime。 */
2
+ import { handleAgentEnvironmentFrame } from "./agent-environment-control.js";
3
+ import { handleMachineEnvironmentFrame } from "./machine-environment-control.js";
4
+ import { createMachineRuntimeEnvironment } from "./machine-runtime-environment.js";
5
5
  import { WebSocket } from "ws";
6
6
  import { persistPreAdmissionRejection, rejectQueuedAdmission } from "./pre-admission-rejection.js";
7
7
  import { join } from "node:path";
@@ -9,14 +9,11 @@ import { randomUUID } from "node:crypto";
9
9
  import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
10
10
  import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
11
11
  import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
12
- import { collectMachineHello, daemonCapabilities, daemonCapabilityBindings, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
12
+ import { daemonCapabilities, daemonCapabilityBindings, } from "./machine-info.js";
13
13
  import { reconcileExecutionJournal } from "./execution-recovery.js";
14
14
  import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
15
- import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
16
- import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
17
- import { listSkills } from "./skills.js";
15
+ import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
18
16
  import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
19
- import { listRuntimeModels } from "./list-models.js";
20
17
  import { normalizeScheduledContext } from "./scheduled-report.js";
21
18
  import { formatDaemonLogLine } from "./log-format.js";
22
19
  import { reportAgentRunComplete } from "./scheduled-run-report.js";
@@ -50,7 +47,7 @@ import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
50
47
  import { initializedProjectSkillsReconciler } from "./project-skills/initialized-reconciler.js";
51
48
  import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
52
49
  import { probeRuntimeHealth } from "./runtime-health.js";
53
- import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
50
+ import { createServeMarketplace } from "./marketplace-serve.js";
54
51
  import { buildControlPlaneUrl } from "./control-plane-url.js";
55
52
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
56
53
  import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
@@ -79,9 +76,6 @@ export function serve(config, opts = {}) {
79
76
  ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
80
77
  const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
81
78
  const executeProtocol = opts.execution?.runExecution ?? runExecution;
82
- let detectedExecutionRuntimes = [], executionRuntimesKnown = false;
83
- let runtimeFacts = null;
84
- const runtimeProbe = createRuntimeProbeCoordinator();
85
79
  const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
86
80
  const hostCoordinator = opts.execution?.hostCoordinator
87
81
  ?? createHostExecutionCoordinator({
@@ -126,6 +120,7 @@ export function serve(config, opts = {}) {
126
120
  catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
127
121
  },
128
122
  });
123
+ const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
129
124
  const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
130
125
  const projectionCoordinator = createAgentProjectionCoordinator();
131
126
  const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
@@ -169,6 +164,16 @@ export function serve(config, opts = {}) {
169
164
  },
170
165
  onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
171
166
  });
167
+ const machineRuntime = createMachineRuntimeEnvironment({
168
+ config,
169
+ ...(opts.machineEnvironment === undefined ? {} : { provider: opts.machineEnvironment }),
170
+ ...(opts.machineInfo === undefined ? {} : { machineInfo: opts.machineInfo }),
171
+ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
172
+ additionalCapabilities: async () => managedDaemonCapabilities(updateEligibility),
173
+ publish: machineHelloPublisher.publish,
174
+ onError: (code, context) => dslog("execution.runtime_detection_failed", "机器环境或 runtime 探测失败", { level: "ERROR", error_code: code, ...context }),
175
+ });
176
+ machineRuntime.prewarm();
172
177
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
173
178
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
174
179
  const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
@@ -285,12 +290,20 @@ export function serve(config, opts = {}) {
285
290
  if (stopped)
286
291
  return;
287
292
  const wsUrl = (opts.buildControlPlaneUrl ?? buildControlPlaneUrl)(config.serverUrl, config.machineToken, process.platform, undefined, capabilities.controlPlaneUrl);
288
- ws = createWebSocket(wsUrl);
293
+ const connectionSocket = createWebSocket(wsUrl);
294
+ ws = connectionSocket;
295
+ const sendToConnection = (frame) => {
296
+ try {
297
+ if (!stopped && ws === connectionSocket && connectionSocket.readyState === WebSocket.OPEN)
298
+ connectionSocket.send(JSON.stringify(frame));
299
+ }
300
+ catch { /* 旧连接请求由服务端超时处理。 */ }
301
+ };
289
302
  let connectionEpochToken;
290
- ws.on("open", () => {
303
+ connectionSocket.on("open", () => {
291
304
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
292
305
  storageIdentityAdmission.beginIdentityBinding();
293
- const openedSocket = ws;
306
+ const openedSocket = connectionSocket;
294
307
  const connectionId = randomUUID();
295
308
  backoff = 1000;
296
309
  connectedAt = Date.now();
@@ -299,60 +312,7 @@ export function serve(config, opts = {}) {
299
312
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
300
313
  void drainSpool();
301
314
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
302
- const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
303
- const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
304
- ...(detectInstalled ? { detectInstalled } : {}),
305
- ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
306
- // First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
307
- detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
308
- additionalCapabilities: async () => managedDaemonCapabilities(updateEligibility),
309
- capabilities: capabilities.machineHello,
310
- });
311
- runtimeFacts = helloPromise
312
- .then(async (hello) => {
313
- if (hello.executionProtocol === undefined) {
314
- executionRuntimesKnown = true;
315
- dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
316
- connection_id: connectionId, reason: "protocol_unavailable",
317
- current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
318
- socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
319
- execution_runtimes: JSON.stringify(hello.executionRuntimes),
320
- });
321
- return [];
322
- }
323
- const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
324
- executionRuntimesKnown = true;
325
- const currentSocket = ws === openedSocket;
326
- const socketOpen = openedSocket.readyState === WebSocket.OPEN;
327
- const hasNewRuntime = detected.some((runtime) => !hello.executionRuntimes.includes(runtime));
328
- dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
329
- connection_id: connectionId, current_socket: currentSocket, socket_open: socketOpen,
330
- socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
331
- initial_execution_runtimes: JSON.stringify(hello.executionRuntimes), execution_runtimes: JSON.stringify(detected),
332
- reason: !currentSocket ? "stale_connection" : !socketOpen ? "socket_not_open" : !hasNewRuntime ? "unchanged" : "publish",
333
- });
334
- if (currentSocket && socketOpen && hasNewRuntime) {
335
- detectedExecutionRuntimes = detected;
336
- machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
337
- }
338
- return detected;
339
- })
340
- .catch(() => {
341
- executionRuntimesKnown = true;
342
- dslog("execution.runtime_detection_failed", "runtime 探测失败", {
343
- level: "ERROR", reason: "detection_threw", connection_id: connectionId,
344
- current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
345
- });
346
- return [];
347
- });
348
- void helloPromise
349
- .then(async (hello) => {
350
- if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
351
- return;
352
- detectedExecutionRuntimes = hello.executionRuntimes;
353
- machineHelloPublisher.publish(hello, openedSocket);
354
- })
355
- .catch(() => { });
315
+ machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
356
316
  opts.onOpen?.(ws);
357
317
  void sendSnapshot(`reconnect-${randomUUID()}`);
358
318
  void executionJournal.replay()
@@ -381,8 +341,8 @@ export function serve(config, opts = {}) {
381
341
  });
382
342
  });
383
343
  });
384
- ws.on("message", async (data) => {
385
- if (stopped)
344
+ connectionSocket.on("message", async (data) => {
345
+ if (stopped || ws !== connectionSocket)
386
346
  return;
387
347
  let decoded;
388
348
  try {
@@ -394,6 +354,10 @@ export function serve(config, opts = {}) {
394
354
  if (typeof decoded !== "object" || decoded === null)
395
355
  return;
396
356
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
357
+ if (rawType === "machine:environment:get" || rawType === "machine:environment:refresh") {
358
+ await handleMachineEnvironmentFrame(decoded, machineRuntime.provider, sendToConnection);
359
+ return;
360
+ }
397
361
  if (rawType === "daemon:update" || rawType === "daemon:restart") {
398
362
  void updateController.handle(decoded).catch((error) => {
399
363
  dslog("daemon.control_failed", "daemon 控制操作处理失败", {
@@ -665,9 +629,7 @@ export function serve(config, opts = {}) {
665
629
  let availableRuntimes;
666
630
  try {
667
631
  availableRuntimes = opts.execution?.availableRuntimes?.()
668
- ?? (detectedExecutionRuntimes.includes(spec.runtime.name)
669
- ? detectedExecutionRuntimes
670
- : await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes)));
632
+ ?? await machineRuntime.availableRuntimes(spec.runtime.name);
671
633
  }
672
634
  catch (error) {
673
635
  cleanupExecutionReservation("runtime_detection_failed");
@@ -684,6 +646,7 @@ export function serve(config, opts = {}) {
684
646
  }
685
647
  const execution = executeProtocol(config, spec, {
686
648
  ...opts.execution?.dependencies,
649
+ machineEnvironment: machineRuntime.provider,
687
650
  capabilities: capabilities.executionRunner,
688
651
  projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
689
652
  abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
@@ -829,15 +792,17 @@ export function serve(config, opts = {}) {
829
792
  return;
830
793
  }
831
794
  if (msg.type === "runtime-health:probe") {
832
- const result = await handleRuntimeHealthProbeFrame(msg, opts.runtimeHealth?.probe, executionRuntimesKnown, detectedExecutionRuntimes);
795
+ const result = await machineRuntime.health(msg, opts.runtimeHealth?.probe);
833
796
  if (result !== null) {
834
- try {
835
- ws?.send(JSON.stringify({ type: "fs:result", ...result }));
836
- }
837
- catch { /* server 会超时 */ }
797
+ sendToConnection({ type: "fs:result", ...result });
838
798
  }
839
799
  return;
840
800
  }
801
+ if (marketplace.handle(msg, frame => { try {
802
+ ws?.send(JSON.stringify(frame));
803
+ }
804
+ catch { /* server 会按请求超时处理 */ } }))
805
+ return;
841
806
  if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
842
807
  return;
843
808
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
@@ -863,26 +828,11 @@ export function serve(config, opts = {}) {
863
828
  }
864
829
  return;
865
830
  }
866
- // workspace 文件浏览 / skills 枚举请求 (只读、沙箱;见 workspace-fs.ts / skills.ts)
867
- if (msg.type === "fs:list" || msg.type === "fs:read" || msg.type === "skills:list" || msg.type === "probe-models") {
868
- const req = msg;
869
- const root = join(config.agentsRoot, req.handle);
870
- const reply = (r) => {
871
- try {
872
- ws?.send(JSON.stringify({ type: "fs:result", reqId: req.reqId, ...r }));
873
- }
874
- catch { /* 忽略 */ }
875
- };
876
- try {
877
- const data2 = req.type === "fs:list" ? await listWorkspace(root, req.path)
878
- : req.type === "fs:read" ? await readWorkspaceFile(root, req.path)
879
- : req.type === "skills:list" ? await listSkills(req.runtime)
880
- : { models: await listRuntimeModels(req.type === "probe-models" ? (req.runtime ?? "") : "") };
881
- reply({ ok: true, data: data2 });
882
- }
883
- catch (e) {
884
- reply({ ok: false, error: e.message });
885
- }
831
+ if (await handleAgentEnvironmentFrame(msg, config.agentsRoot, sendToConnection))
832
+ return;
833
+ const fileResult = await handleWorkspaceControlRequest(msg, config.agentsRoot, projectWorkspaceRegistry, machineRuntime.models);
834
+ if (fileResult) {
835
+ sendToConnection(fileResult);
886
836
  return;
887
837
  }
888
838
  if (msg.type !== "agent:start")
@@ -1090,6 +1040,7 @@ export function serve(config, opts = {}) {
1090
1040
  ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
1091
1041
  ...(attemptWake ? { wake: attemptWake } : {}),
1092
1042
  }, reportActivity, reportConsole, {
1043
+ machineEnvironment: machineRuntime.provider,
1093
1044
  cancellation: controller.cancellation,
1094
1045
  startupGate: runtimeStartupGate,
1095
1046
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
@@ -1252,8 +1203,9 @@ export function serve(config, opts = {}) {
1252
1203
  void flushSlog(); // 每轮收尾冲一次,保证 run.end 尽快可查
1253
1204
  }
1254
1205
  });
1255
- ws.on("close", (code) => {
1256
- if (stopped)
1206
+ connectionSocket.on("close", (code) => {
1207
+ machineRuntime.close(connectionSocket);
1208
+ if (stopped || ws !== connectionSocket)
1257
1209
  return;
1258
1210
  // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
1259
1211
  serverCapabilities = new Set();
@@ -1277,9 +1229,9 @@ export function serve(config, opts = {}) {
1277
1229
  }, backoff);
1278
1230
  backoff = Math.min(backoff * 2, maxBackoff);
1279
1231
  });
1280
- ws.on("error", () => {
1232
+ connectionSocket.on("error", () => {
1281
1233
  if (!stopped)
1282
- ws?.close();
1234
+ connectionSocket.close();
1283
1235
  });
1284
1236
  }
1285
1237
  const ready = (async () => {
@@ -1317,7 +1269,6 @@ export function serve(config, opts = {}) {
1317
1269
  stopPromise = (async () => {
1318
1270
  stopped = true;
1319
1271
  concurrencyTelemetry.stop();
1320
- runtimeProbe.stop();
1321
1272
  completionRetransmitter.stop();
1322
1273
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1323
1274
  if (reconnectTimer !== null) {
@@ -1326,8 +1277,10 @@ export function serve(config, opts = {}) {
1326
1277
  }
1327
1278
  const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
1328
1279
  const pending = [
1280
+ machineRuntime.stop(),
1329
1281
  webSocketClosed,
1330
1282
  updateController.drain(),
1283
+ marketplace.stop(),
1331
1284
  executionFrameQueue,
1332
1285
  ...executionRuns.values(),
1333
1286
  ...[...legacyRuns.values()].map((run) => run.done),
package/dist/token.js CHANGED
@@ -1,4 +1,3 @@
1
- /** 用机器令牌为某 agent 换取 per-launch 的 sk_agent_*。 */
2
1
  export async function mintAgentToken(serverUrl, machineToken, handle, displayName, options = {}, fetchImpl = fetch) {
3
2
  const res = await fetchImpl(`${serverUrl}/daemon/agents/token`, {
4
3
  method: "POST",
@@ -0,0 +1,27 @@
1
+ /** 控制面文件/技能查看的按需处理,独立于执行生命周期。 */
2
+ import { join } from "node:path";
3
+ import { readSharedMessageFile } from "./message-file-preview.js";
4
+ import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
5
+ import { listSkills } from "./skills.js";
6
+ import { listRuntimeModels } from "./list-models.js";
7
+ export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, models = listRuntimeModels) {
8
+ if (!raw || typeof raw !== "object")
9
+ return null;
10
+ const request = raw;
11
+ if (!["fs:list", "fs:read", "fs:read-shared", "skills:list", "probe-models"].includes(request.type))
12
+ return null;
13
+ if (typeof request.reqId !== "string" || request.reqId.length > 128)
14
+ return null;
15
+ try {
16
+ const root = join(agentsRoot, request.handle);
17
+ const data = request.type === "fs:read-shared" ? await readSharedMessageFile(agentsRoot, raw, registry)
18
+ : request.type === "fs:list" ? await listWorkspace(root, request.path)
19
+ : request.type === "fs:read" ? await readWorkspaceFile(root, request.path)
20
+ : request.type === "skills:list" ? await listSkills(request.runtime)
21
+ : { models: await models(request.runtime ?? "") };
22
+ return { type: "fs:result", reqId: request.reqId, ok: true, data };
23
+ }
24
+ catch (error) {
25
+ return { type: "fs:result", reqId: request.reqId, ok: false, error: request.type === "probe-models" ? "runtime_model_probe_failed" : error.message };
26
+ }
27
+ }
@@ -3,21 +3,31 @@
3
3
  *
4
4
  * 安全铁律:
5
5
  * - 所有路径都 resolve 后必须仍落在 agent 根内 (防 `..` / 符号链接逃逸)。
6
- * - 隐藏内部目录与点文件 (.crew / .git / 任何 . 开头)。
6
+ * - 列表隐藏点文件;内部环境快照始终不可列出或读取,包括链接别名。
7
7
  * - 只回文本文件,二进制拒绝;单文件大小上限,超出截断。
8
8
  * - 仅 list / read,无写删。
9
9
  */
10
- import { realpath, readdir, readFile, stat } from "node:fs/promises";
11
- import { join, resolve, sep } from "node:path";
10
+ import { constants } from "node:fs";
11
+ import { realpath, readdir, lstat, open, stat } from "node:fs/promises";
12
+ import { join, relative, resolve, sep } from "node:path";
12
13
  export class FsError extends Error {
13
14
  }
14
15
  const MAX_FILE_BYTES = 256 * 1024; // 单文件查看上限 256KB
15
16
  const HIDDEN = new Set([".crew", ".git", ".slock"]);
16
17
  const isHidden = (name) => name.startsWith(".") || HIDDEN.has(name);
18
+ /** 该命名空间包含完整机器环境,只允许本地执行 harness 使用。 */
19
+ export const isInternalEnvironmentPath = (path) => path.split(/[\\/]/u)
20
+ .some((part) => part.toLowerCase().startsWith(".environment-"));
21
+ const assertViewablePath = (path) => {
22
+ if (isInternalEnvironmentPath(path))
23
+ throw new FsError("file_not_viewable");
24
+ };
17
25
  /** 把相对路径安全解析到 root 内的绝对路径;越界抛错。realpath 兜底符号链接逃逸。 */
18
26
  async function safeResolve(root, rel) {
27
+ assertViewablePath(rel);
19
28
  const rootResolved = await realpath(root);
20
29
  const target = resolve(rootResolved, rel.replace(/^\/+/, ""));
30
+ assertViewablePath(target);
21
31
  const withSep = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
22
32
  if (target !== rootResolved && !target.startsWith(withSep)) {
23
33
  throw new FsError("path escapes workspace");
@@ -33,6 +43,7 @@ async function safeResolve(root, rel) {
33
43
  if (real !== rootResolved && !real.startsWith(withSep)) {
34
44
  throw new FsError("path escapes workspace (symlink)");
35
45
  }
46
+ assertViewablePath(real);
36
47
  return real;
37
48
  }
38
49
  /** 列出某目录 (相对 root)。隐藏点文件/内部目录;目录在前、按名排序。 */
@@ -51,13 +62,15 @@ export async function listWorkspace(root, rel) {
51
62
  for (const name of names) {
52
63
  if (isHidden(name))
53
64
  continue;
54
- const full = join(dir, name);
65
+ const full = await safeResolve(root, relative(rootResolved, join(dir, name))).catch(() => null);
66
+ if (full === null)
67
+ continue;
55
68
  const s = await stat(full).catch(() => null);
56
69
  if (!s)
57
70
  continue;
58
71
  if (s.isDirectory())
59
72
  entries.push({ name, type: "dir" });
60
- else if (s.isFile())
73
+ else if (s.isFile() && s.nlink === 1)
61
74
  entries.push({ name, type: "file", size: s.size });
62
75
  }
63
76
  entries.sort((a, b) => a.type !== b.type ? (a.type === "dir" ? -1 : 1) : a.name.localeCompare(b.name));
@@ -66,15 +79,53 @@ export async function listWorkspace(root, rel) {
66
79
  /** 读取某文本文件 (相对 root)。二进制/超大拒绝或截断。 */
67
80
  export async function readWorkspaceFile(root, rel) {
68
81
  const file = await safeResolve(root, rel);
69
- const s = await stat(file).catch(() => null);
70
- if (!s || !s.isFile())
82
+ const before = await lstat(file, { bigint: true }).catch(() => null);
83
+ if (!before?.isFile())
71
84
  throw new FsError("not a file");
72
- const buf = await readFile(file);
73
- // 二进制探测:含 NUL 即视为二进制
74
- const sample = buf.subarray(0, Math.min(buf.length, 8192));
75
- if (sample.includes(0))
76
- throw new FsError("binary file not viewable");
77
- const truncated = buf.length > MAX_FILE_BYTES;
78
- const content = buf.subarray(0, MAX_FILE_BYTES).toString("utf8");
79
- return { path: rel.replace(/^\/+/, ""), content, truncated, size: buf.length };
85
+ // 真实路径不能辨认硬链接的私有来源,因此拒绝多链接文件。
86
+ if (before.nlink !== 1n)
87
+ throw new FsError("file_not_viewable");
88
+ // macOS 原子拒绝父目录链接;Linux 在打开后额外核对句柄的真实路径。
89
+ const noFollow = process.platform === "darwin" ? 0x20000000 : (constants.O_NOFOLLOW ?? 0);
90
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | noFollow)
91
+ .catch(() => { throw new FsError("file_not_viewable"); });
92
+ try {
93
+ const opened = await handle.stat({ bigint: true });
94
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino)
95
+ throw new FsError("file_changed");
96
+ if (process.platform === "linux") {
97
+ const actual = await realpath(`/proc/self/fd/${handle.fd}`);
98
+ assertViewablePath(actual);
99
+ if (actual !== file)
100
+ throw new FsError("file_changed");
101
+ }
102
+ if (await safeResolve(root, rel) !== file)
103
+ throw new FsError("file_changed");
104
+ const buffer = Buffer.alloc(MAX_FILE_BYTES);
105
+ let length = 0;
106
+ while (length < buffer.length) {
107
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
108
+ if (bytesRead === 0)
109
+ break;
110
+ length += bytesRead;
111
+ }
112
+ const after = await handle.stat({ bigint: true });
113
+ const current = await lstat(file, { bigint: true });
114
+ if (await safeResolve(root, rel) !== file || !current.isFile() || current.nlink !== 1n
115
+ || current.dev !== opened.dev || current.ino !== opened.ino || after.nlink !== 1n
116
+ || after.size !== opened.size || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs)
117
+ throw new FsError("file_changed");
118
+ const bytes = buffer.subarray(0, length);
119
+ if (bytes.subarray(0, Math.min(length, 8192)).includes(0))
120
+ throw new FsError("binary file not viewable");
121
+ return { path: rel.replace(/^\/+/, ""), content: bytes.toString("utf8"), truncated: opened.size > BigInt(length), size: Number(opened.size) };
122
+ }
123
+ catch (error) {
124
+ if (error instanceof FsError)
125
+ throw error;
126
+ throw new FsError("file_changed");
127
+ }
128
+ finally {
129
+ await handle.close();
130
+ }
80
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.27",
3
+ "version": "0.6.88-beta.29",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@agentclientprotocol/sdk": "1.2.1",
21
- "@nowcrew/cli": "0.4.14",
21
+ "@nowcrew/cli": "0.4.17",
22
22
  "cross-spawn": "^7.0.6",
23
23
  "semver": "7.8.5",
24
24
  "ws": "^8",