@nowcrew/daemon 0.6.88-beta.28 → 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 (39) 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 +20 -15
  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/main.js +0 -0
  23. package/dist/message-file-preview.js +239 -0
  24. package/dist/provider-env.js +26 -12
  25. package/dist/runner.js +14 -0
  26. package/dist/runtime-environment-merge.js +16 -0
  27. package/dist/runtime-health.js +3 -1
  28. package/dist/runtime-path.js +22 -12
  29. package/dist/runtime-probe.js +2 -2
  30. package/dist/runtimes/claude-models.js +20 -9
  31. package/dist/runtimes/deepseek-harness.js +17 -12
  32. package/dist/runtimes/hermes-models.js +2 -1
  33. package/dist/runtimes/kimi-acp-runner.js +7 -6
  34. package/dist/runtimes/opencode.js +2 -2
  35. package/dist/serve.js +50 -105
  36. package/dist/token.js +0 -1
  37. package/dist/workspace-control-handler.js +27 -0
  38. package/dist/workspace-fs.js +66 -15
  39. package/package.json +9 -10
@@ -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 = defaultDeps) {
34
- const version = normalizeClaudeVersion(await deps.probeVersion());
35
- const cached = version ? cacheByVersion.get(version) : undefined;
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 deps.resolveAlias(alias);
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
- cacheByVersion.set(version, models);
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
- ...process.env,
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[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
13
- const patchPath = env[DEEPSEEK_HARNESS_PATCH_ENV]?.trim();
14
- if (configPath && !isAbsolute(configPath))
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 && !isAbsolute(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[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
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: process.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: { ...process.env, PATH: augmentedPath() },
77
+ env,
77
78
  stdio: ["pipe", "pipe", "pipe"],
78
79
  });
79
80
  if (child.stdin === null || child.stdout === null || child.stderr === null)
@@ -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,6 @@ 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";
54
50
  import { createServeMarketplace } from "./marketplace-serve.js";
55
51
  import { buildControlPlaneUrl } from "./control-plane-url.js";
56
52
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
@@ -80,9 +76,6 @@ export function serve(config, opts = {}) {
80
76
  ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
81
77
  const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
82
78
  const executeProtocol = opts.execution?.runExecution ?? runExecution;
83
- let detectedExecutionRuntimes = [], executionRuntimesKnown = false;
84
- let runtimeFacts = null;
85
- const runtimeProbe = createRuntimeProbeCoordinator();
86
79
  const localHome = opts.localHome ?? resolveDaemonLocalStateRootFromEnv();
87
80
  const hostCoordinator = opts.execution?.hostCoordinator
88
81
  ?? createHostExecutionCoordinator({
@@ -171,6 +164,16 @@ export function serve(config, opts = {}) {
171
164
  },
172
165
  onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
173
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();
174
177
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
175
178
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
176
179
  const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
@@ -287,12 +290,20 @@ export function serve(config, opts = {}) {
287
290
  if (stopped)
288
291
  return;
289
292
  const wsUrl = (opts.buildControlPlaneUrl ?? buildControlPlaneUrl)(config.serverUrl, config.machineToken, process.platform, undefined, capabilities.controlPlaneUrl);
290
- 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
+ };
291
302
  let connectionEpochToken;
292
- ws.on("open", () => {
303
+ connectionSocket.on("open", () => {
293
304
  connectionEpochToken = projectSkillsController.beginConnectionEpoch();
294
305
  storageIdentityAdmission.beginIdentityBinding();
295
- const openedSocket = ws;
306
+ const openedSocket = connectionSocket;
296
307
  const connectionId = randomUUID();
297
308
  backoff = 1000;
298
309
  connectedAt = Date.now();
@@ -301,60 +312,7 @@ export function serve(config, opts = {}) {
301
312
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
302
313
  void drainSpool();
303
314
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
304
- const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
305
- const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
306
- ...(detectInstalled ? { detectInstalled } : {}),
307
- ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
308
- // First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
309
- detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
310
- additionalCapabilities: async () => managedDaemonCapabilities(updateEligibility),
311
- capabilities: capabilities.machineHello,
312
- });
313
- runtimeFacts = helloPromise
314
- .then(async (hello) => {
315
- if (hello.executionProtocol === undefined) {
316
- executionRuntimesKnown = true;
317
- dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
318
- connection_id: connectionId, reason: "protocol_unavailable",
319
- current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
320
- socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
321
- execution_runtimes: JSON.stringify(hello.executionRuntimes),
322
- });
323
- return [];
324
- }
325
- const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
326
- executionRuntimesKnown = true;
327
- const currentSocket = ws === openedSocket;
328
- const socketOpen = openedSocket.readyState === WebSocket.OPEN;
329
- const hasNewRuntime = detected.some((runtime) => !hello.executionRuntimes.includes(runtime));
330
- dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
331
- connection_id: connectionId, current_socket: currentSocket, socket_open: socketOpen,
332
- socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
333
- initial_execution_runtimes: JSON.stringify(hello.executionRuntimes), execution_runtimes: JSON.stringify(detected),
334
- reason: !currentSocket ? "stale_connection" : !socketOpen ? "socket_not_open" : !hasNewRuntime ? "unchanged" : "publish",
335
- });
336
- if (currentSocket && socketOpen && hasNewRuntime) {
337
- detectedExecutionRuntimes = detected;
338
- machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
339
- }
340
- return detected;
341
- })
342
- .catch(() => {
343
- executionRuntimesKnown = true;
344
- dslog("execution.runtime_detection_failed", "runtime 探测失败", {
345
- level: "ERROR", reason: "detection_threw", connection_id: connectionId,
346
- current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
347
- });
348
- return [];
349
- });
350
- void helloPromise
351
- .then(async (hello) => {
352
- if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
353
- return;
354
- detectedExecutionRuntimes = hello.executionRuntimes;
355
- machineHelloPublisher.publish(hello, openedSocket);
356
- })
357
- .catch(() => { });
315
+ machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
358
316
  opts.onOpen?.(ws);
359
317
  void sendSnapshot(`reconnect-${randomUUID()}`);
360
318
  void executionJournal.replay()
@@ -383,8 +341,8 @@ export function serve(config, opts = {}) {
383
341
  });
384
342
  });
385
343
  });
386
- ws.on("message", async (data) => {
387
- if (stopped)
344
+ connectionSocket.on("message", async (data) => {
345
+ if (stopped || ws !== connectionSocket)
388
346
  return;
389
347
  let decoded;
390
348
  try {
@@ -396,6 +354,10 @@ export function serve(config, opts = {}) {
396
354
  if (typeof decoded !== "object" || decoded === null)
397
355
  return;
398
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
+ }
399
361
  if (rawType === "daemon:update" || rawType === "daemon:restart") {
400
362
  void updateController.handle(decoded).catch((error) => {
401
363
  dslog("daemon.control_failed", "daemon 控制操作处理失败", {
@@ -667,9 +629,7 @@ export function serve(config, opts = {}) {
667
629
  let availableRuntimes;
668
630
  try {
669
631
  availableRuntimes = opts.execution?.availableRuntimes?.()
670
- ?? (detectedExecutionRuntimes.includes(spec.runtime.name)
671
- ? detectedExecutionRuntimes
672
- : await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes)));
632
+ ?? await machineRuntime.availableRuntimes(spec.runtime.name);
673
633
  }
674
634
  catch (error) {
675
635
  cleanupExecutionReservation("runtime_detection_failed");
@@ -686,6 +646,7 @@ export function serve(config, opts = {}) {
686
646
  }
687
647
  const execution = executeProtocol(config, spec, {
688
648
  ...opts.execution?.dependencies,
649
+ machineEnvironment: machineRuntime.provider,
689
650
  capabilities: capabilities.executionRunner,
690
651
  projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
691
652
  abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
@@ -831,12 +792,9 @@ export function serve(config, opts = {}) {
831
792
  return;
832
793
  }
833
794
  if (msg.type === "runtime-health:probe") {
834
- const result = await handleRuntimeHealthProbeFrame(msg, opts.runtimeHealth?.probe, executionRuntimesKnown, detectedExecutionRuntimes);
795
+ const result = await machineRuntime.health(msg, opts.runtimeHealth?.probe);
835
796
  if (result !== null) {
836
- try {
837
- ws?.send(JSON.stringify({ type: "fs:result", ...result }));
838
- }
839
- catch { /* server 会超时 */ }
797
+ sendToConnection({ type: "fs:result", ...result });
840
798
  }
841
799
  return;
842
800
  }
@@ -870,26 +828,11 @@ export function serve(config, opts = {}) {
870
828
  }
871
829
  return;
872
830
  }
873
- // workspace 文件浏览 / skills 枚举请求 (只读、沙箱;见 workspace-fs.ts / skills.ts)
874
- if (msg.type === "fs:list" || msg.type === "fs:read" || msg.type === "skills:list" || msg.type === "probe-models") {
875
- const req = msg;
876
- const root = join(config.agentsRoot, req.handle);
877
- const reply = (r) => {
878
- try {
879
- ws?.send(JSON.stringify({ type: "fs:result", reqId: req.reqId, ...r }));
880
- }
881
- catch { /* 忽略 */ }
882
- };
883
- try {
884
- const data2 = req.type === "fs:list" ? await listWorkspace(root, req.path)
885
- : req.type === "fs:read" ? await readWorkspaceFile(root, req.path)
886
- : req.type === "skills:list" ? await listSkills(req.runtime)
887
- : { models: await listRuntimeModels(req.type === "probe-models" ? (req.runtime ?? "") : "") };
888
- reply({ ok: true, data: data2 });
889
- }
890
- catch (e) {
891
- reply({ ok: false, error: e.message });
892
- }
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);
893
836
  return;
894
837
  }
895
838
  if (msg.type !== "agent:start")
@@ -1097,6 +1040,7 @@ export function serve(config, opts = {}) {
1097
1040
  ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
1098
1041
  ...(attemptWake ? { wake: attemptWake } : {}),
1099
1042
  }, reportActivity, reportConsole, {
1043
+ machineEnvironment: machineRuntime.provider,
1100
1044
  cancellation: controller.cancellation,
1101
1045
  startupGate: runtimeStartupGate,
1102
1046
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
@@ -1259,8 +1203,9 @@ export function serve(config, opts = {}) {
1259
1203
  void flushSlog(); // 每轮收尾冲一次,保证 run.end 尽快可查
1260
1204
  }
1261
1205
  });
1262
- ws.on("close", (code) => {
1263
- if (stopped)
1206
+ connectionSocket.on("close", (code) => {
1207
+ machineRuntime.close(connectionSocket);
1208
+ if (stopped || ws !== connectionSocket)
1264
1209
  return;
1265
1210
  // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
1266
1211
  serverCapabilities = new Set();
@@ -1284,9 +1229,9 @@ export function serve(config, opts = {}) {
1284
1229
  }, backoff);
1285
1230
  backoff = Math.min(backoff * 2, maxBackoff);
1286
1231
  });
1287
- ws.on("error", () => {
1232
+ connectionSocket.on("error", () => {
1288
1233
  if (!stopped)
1289
- ws?.close();
1234
+ connectionSocket.close();
1290
1235
  });
1291
1236
  }
1292
1237
  const ready = (async () => {
@@ -1324,7 +1269,6 @@ export function serve(config, opts = {}) {
1324
1269
  stopPromise = (async () => {
1325
1270
  stopped = true;
1326
1271
  concurrencyTelemetry.stop();
1327
- runtimeProbe.stop();
1328
1272
  completionRetransmitter.stop();
1329
1273
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1330
1274
  if (reconnectTimer !== null) {
@@ -1333,6 +1277,7 @@ export function serve(config, opts = {}) {
1333
1277
  }
1334
1278
  const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
1335
1279
  const pending = [
1280
+ machineRuntime.stop(),
1336
1281
  webSocketClosed,
1337
1282
  updateController.drain(),
1338
1283
  marketplace.stop(),
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
+ }