@nowcrew/daemon 0.6.89 → 0.6.91-beta.1

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 (116) hide show
  1. package/README.md +64 -3
  2. package/dist/agent-ability/materializer.js +21 -10
  3. package/dist/agent-environment-contract.js +55 -0
  4. package/dist/agent-environment-control.js +24 -0
  5. package/dist/agent-environment-execution.js +98 -0
  6. package/dist/agent-environments.js +275 -0
  7. package/dist/automatic-daemon-installer.js +49 -1
  8. package/dist/computer-main.js +18 -0
  9. package/dist/computer-profile.js +17 -4
  10. package/dist/computer-setup-client.js +318 -0
  11. package/dist/computer-setup-orchestration.js +203 -0
  12. package/dist/computer-setup.js +244 -0
  13. package/dist/config.js +30 -12
  14. package/dist/console-formatter.js +3 -3
  15. package/dist/console.js +15 -2
  16. package/dist/daemon-update-eligibility.js +1 -1
  17. package/dist/daemon-updater.js +2 -12
  18. package/dist/execution-journal.js +9 -6
  19. package/dist/execution-protocol.js +1 -0
  20. package/dist/execution-runner.js +13 -8
  21. package/dist/i18n.js +19 -0
  22. package/dist/list-models.js +12 -12
  23. package/dist/local-execution-lease.js +22 -0
  24. package/dist/local-executor.js +57 -84
  25. package/dist/local-memory/candidate.js +43 -0
  26. package/dist/local-memory/index-budget.js +8 -0
  27. package/dist/local-memory/inventory-cli.js +61 -0
  28. package/dist/local-memory/inventory-preview.js +159 -0
  29. package/dist/local-memory/inventory-source.js +159 -0
  30. package/dist/local-memory/pending-match.js +29 -0
  31. package/dist/local-memory/pending-query.js +100 -0
  32. package/dist/local-memory/pending-schema.js +29 -0
  33. package/dist/local-memory/pending-source.js +126 -0
  34. package/dist/local-memory/pending.js +34 -84
  35. package/dist/local-memory/prompt.js +19 -12
  36. package/dist/local-memory/read-cli.js +97 -12
  37. package/dist/local-memory/rollout.js +82 -0
  38. package/dist/local-memory/runtime.js +23 -17
  39. package/dist/local-memory/work-log-execution.js +31 -0
  40. package/dist/local-memory/work-log.js +58 -0
  41. package/dist/local-memory/write.js +27 -58
  42. package/dist/local-runtime-environment.js +50 -0
  43. package/dist/machine-environment-cache.js +147 -0
  44. package/dist/machine-environment-contract.js +10 -0
  45. package/dist/machine-environment-control.js +27 -0
  46. package/dist/machine-environment-policy.js +75 -0
  47. package/dist/machine-environment-process.js +110 -0
  48. package/dist/machine-info.js +70 -25
  49. package/dist/machine-runtime-environment.js +189 -0
  50. package/dist/machine-shell-environment-capture.js +135 -0
  51. package/dist/machine-windows-environment-capture.js +215 -0
  52. package/dist/machine-windows-environment-script.js +158 -0
  53. package/dist/main.js +4 -69
  54. package/dist/marketplace-command.js +21 -0
  55. package/dist/marketplace-controller.js +80 -0
  56. package/dist/marketplace-protocol.js +38 -0
  57. package/dist/marketplace-remote.js +48 -0
  58. package/dist/marketplace-runner.js +235 -0
  59. package/dist/marketplace-serve.js +41 -0
  60. package/dist/marketplace-source.js +103 -0
  61. package/dist/message-file-preview.js +246 -0
  62. package/dist/pre-admission-rejection.js +64 -0
  63. package/dist/project-skills/execution-adapter.js +3 -2
  64. package/dist/project-skills/runtime-launch.js +14 -0
  65. package/dist/provider-env.js +26 -15
  66. package/dist/runner.js +17 -4
  67. package/dist/runtime-environment-merge.js +16 -0
  68. package/dist/runtime-health-handler.js +10 -0
  69. package/dist/runtime-health.js +3 -1
  70. package/dist/runtime-path.js +22 -12
  71. package/dist/runtime-probe.js +35 -6
  72. package/dist/runtimes/acp-probe-transport.js +69 -0
  73. package/dist/runtimes/claude-models.js +20 -9
  74. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  75. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  76. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  77. package/dist/runtimes/deepseek-harness.js +76 -15
  78. package/dist/runtimes/harness-probe-retry.js +42 -0
  79. package/dist/runtimes/hermes-models.js +2 -1
  80. package/dist/runtimes/kimi-acp-runner.js +78 -13
  81. package/dist/runtimes/opencode.js +2 -2
  82. package/dist/serve.js +88 -178
  83. package/dist/storage-identity.js +0 -3
  84. package/dist/storage-serve-adapter.js +6 -72
  85. package/dist/supervised-runtime.js +17 -2
  86. package/dist/token.js +0 -1
  87. package/dist/workspace-control-handler.js +27 -0
  88. package/dist/workspace-fs.js +66 -15
  89. package/dist/workspace.js +1 -1
  90. package/package.json +13 -12
  91. package/dist/codex-home-launch.js +0 -7
  92. package/dist/codex-home-mode-controller.js +0 -68
  93. package/dist/daemon-migration-controller.js +0 -211
  94. package/dist/daemon-migration-wiring.js +0 -22
  95. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  96. package/dist/runtimes/codex-home-migration.js +0 -112
  97. package/dist/runtimes/codex-home.js +0 -236
  98. package/dist/storage-control.js +0 -212
  99. package/dist/storage-copy.js +0 -628
  100. package/dist/storage-credential-guard.js +0 -21
  101. package/dist/storage-doctor.js +0 -228
  102. package/dist/storage-manifest.js +0 -1027
  103. package/dist/storage-migration-controller.js +0 -440
  104. package/dist/storage-migration-manifest.js +0 -105
  105. package/dist/storage-migration-policy.js +0 -100
  106. package/dist/storage-runtime-controller.js +0 -381
  107. package/dist/storage-runtime-readiness.js +0 -241
  108. package/dist/storage-safe-fs.js +0 -302
  109. package/dist/storage-status-delivery.js +0 -224
  110. package/dist/storage-status-replay.js +0 -140
  111. package/dist/storage-target-confirmation.js +0 -129
  112. package/dist/storage-worker-command.js +0 -37
  113. package/dist/storage-worker-executor.js +0 -447
  114. package/dist/storage-worker-handoff.js +0 -151
  115. package/dist/storage-worker-operation.js +0 -294
  116. package/dist/storage-worker-status.js +0 -259
@@ -1,7 +1,11 @@
1
+ import { retryHarnessProbe } from "./runtimes/harness-probe-retry.js";
2
+ import { MARKETPLACE_CAPABILITY } from "./marketplace-protocol.js";
1
3
  /**
2
4
  * 采集本机信息上报给控制面 (machine:hello):hostname / os / daemon 版本 / 已装 runtimes。
3
5
  * runtimes 探测靠 `which <bin>`(win32 用 `where`),只报真实可执行的 CLI(用于展示 Detected Runtimes)。
4
6
  */
7
+ import { randomUUID } from "node:crypto";
8
+ import { dslog } from "./slog.js";
5
9
  import { hostname, arch, platform as osPlatform } from "node:os";
6
10
  import { lookupCmd } from "./platform.js";
7
11
  import { augmentedPath } from "./runtime-path.js";
@@ -14,11 +18,11 @@ import { createRequire } from "node:module";
14
18
  import { dirname, resolve } from "node:path";
15
19
  import { executableRuntimes } from "./runtime-capabilities.js";
16
20
  import { executionBackendCapability } from "./execution-backend.js";
17
- import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
21
+ import { probeKimiAcp, AcpProbeCleanupError } from "./runtimes/kimi-acp-runner.js";
18
22
  import { probeHermesAcp } from "./runtimes/hermes.js";
19
23
  import { probeOpenCodeRun } from "./runtimes/opencode.js";
20
24
  import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
21
- import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
25
+ import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
22
26
  import { daemonHome } from "./computer-profile.js";
23
27
  import { daemonReleaseProvenance } from "./release-provenance.js";
24
28
  export { executableRuntimes } from "./runtime-capabilities.js";
@@ -28,17 +32,20 @@ export const TRAINING_PROJECT_DEPENDENCIES_CAPABILITY = "training_project_depend
28
32
  export const SELF_TRAINING_EXECUTION_CAPABILITY = "self_training_execution_v1";
29
33
  export const EXECUTION_LARGE_ATTACHMENTS_CAPABILITY = "execution_attachments_300m_v1";
30
34
  export const PROJECT_SKILL_PROJECTION_CAPABILITY = "project_skill_projection_v2";
35
+ export const MACHINE_ENVIRONMENT_CAPABILITY = "machine_environment_v1";
31
36
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
32
37
  resolverReady: true,
33
38
  nativeRuntimeAdaptersReady: true,
34
39
  });
35
40
  export const DAEMON_CAPABILITIES = Object.freeze([
41
+ MARKETPLACE_CAPABILITY,
36
42
  "scheduled_job_v1",
37
43
  "reply_origin_v1",
38
44
  "origin_decision_v1",
39
45
  "execution_telemetry_ack_v1",
40
46
  "execution_external_output_v1",
41
47
  "execution_attachments_v1",
48
+ "message_file_preview_v1",
42
49
  EXECUTION_LARGE_ATTACHMENTS_CAPABILITY,
43
50
  "execution_answer_stream_v1",
44
51
  "execution_machine_queue_v1",
@@ -53,6 +60,8 @@ export const DAEMON_CAPABILITIES = Object.freeze([
53
60
  "agent_ability_release_v1",
54
61
  "agent_ability_workspace_v1",
55
62
  "agent_ability_runtime_assets_v1",
63
+ "agent_environments_v1",
64
+ MACHINE_ENVIRONMENT_CAPABILITY,
56
65
  ]);
57
66
  /** 能力只按平台过滤,进程存活期间静态(CLAUDE.md 红线)。Windows 在 junction/copy 真机 smoke 通过前不宣告 Project Skills。 */
58
67
  export const daemonCapabilities = (runtimePlatform = process.platform, readiness = NATIVE_PROJECT_WORKSPACE_READINESS) => {
@@ -61,7 +70,9 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
61
70
  const nativeWorkspaceReady = nativeWorkspacePlatform
62
71
  && readiness.resolverReady
63
72
  && readiness.nativeRuntimeAdaptersReady;
64
- return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
73
+ return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
74
+ && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
75
+ && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
65
76
  && (nativeWorkspacePlatform
66
77
  || (capability !== "project_skills_v1"
67
78
  && capability !== "project_skill_projection_v2"
@@ -90,15 +101,15 @@ const RUNTIME_BINS = [
90
101
  ["gemini", "gemini"],
91
102
  ["hermes", "hermes"],
92
103
  ["opencode", "opencode"],
93
- ["deepseek-harness", "dsh-acp-demo"],
104
+ ["deepseek-harness", "dsh"],
94
105
  ["copilot", "copilot"],
95
106
  ["kimi", "kimi"],
96
107
  ["pi", "pi"],
97
108
  ];
98
- async function isInstalled(bin) {
109
+ async function isInstalled(bin, env) {
99
110
  try {
100
111
  // 用增强 PATH 探测:daemon 继承的 PATH 可能缺 ~/.local/bin 等用户级目录(Claude 原生安装器落点)。
101
- await execFileP(lookupCmd(), [bin], { env: { ...process.env, PATH: augmentedPath() } });
112
+ await execFileP(lookupCmd(), [bin], { env });
102
113
  return true;
103
114
  }
104
115
  catch {
@@ -106,30 +117,65 @@ async function isInstalled(bin) {
106
117
  }
107
118
  }
108
119
  /** 并发探测所有候选 runtime,返回已安装的展示名列表。 */
109
- export async function detectRuntimes() {
110
- const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
120
+ export async function detectRuntimes(env = { ...process.env, PATH: augmentedPath() }) {
121
+ const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => {
122
+ try {
123
+ return await isInstalled(name === "deepseek-harness" ? deepSeekHarnessLauncher(env).bin : bin, env) ? name : null;
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }));
111
129
  return checks.filter((x) => x !== null);
112
130
  }
113
- async function supportsKimiAcp(signal) {
114
- return probeKimiAcp({ bin: "kimi", ...(signal ? { signal } : {}) });
131
+ async function supportsKimiAcp(signal, probeId, env) {
132
+ return probeKimiAcp({ bin: "kimi", ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}), ...(env ? { env } : {}) });
115
133
  }
116
- async function supportsHermesAcp(signal) {
117
- return probeHermesAcp({ bin: "hermes", ...(signal ? { signal } : {}) });
134
+ async function supportsHermesAcp(signal, probeId, env) {
135
+ return probeHermesAcp({ bin: "hermes", ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}), ...(env ? { env } : {}) });
118
136
  }
119
- async function supportsOpenCodeRun(signal) {
120
- return probeOpenCodeRun(signal);
137
+ async function supportsOpenCodeRun(signal, _probeId, env) {
138
+ return probeOpenCodeRun(signal, env);
121
139
  }
122
- async function supportsDeepSeekHarnessAcp(signal) {
123
- return probeDeepSeekHarnessAcp({ bin: "dsh-acp-demo", ...(signal ? { signal } : {}) });
140
+ async function supportsDeepSeekHarnessAcp(signal, probeId, env) {
141
+ try {
142
+ return await probeDeepSeekHarnessAcp({ bin: deepSeekHarnessLauncher(env).bin, ...(probeId ? { probeId } : {}), ...(signal ? { signal } : {}), ...(env ? { env } : {}) });
143
+ }
144
+ catch (error) {
145
+ if (error instanceof AcpProbeCleanupError)
146
+ throw error;
147
+ return false;
148
+ }
124
149
  }
125
150
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
126
- export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal) {
151
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal, probeId = randomUUID()) {
127
152
  const present = await installed;
153
+ const probe = (runtime, detect) => {
154
+ const startedAt = Date.now();
155
+ dslog("runtime_probe.optional_start", "开始可选 runtime 探测", { probe_id: probeId, runtime, aborted: signal?.aborted ?? false });
156
+ const complete = (outcome, ready) => {
157
+ dslog("runtime_probe.optional_complete", "可选 runtime 探测结束", {
158
+ probe_id: probeId, runtime, outcome, duration_ms: Date.now() - startedAt,
159
+ aborted: signal?.aborted ?? false, ...(ready === undefined ? {} : { ready }),
160
+ });
161
+ };
162
+ let pending;
163
+ try {
164
+ pending = detect(signal, probeId);
165
+ }
166
+ catch (error) {
167
+ complete("threw");
168
+ throw error;
169
+ }
170
+ return pending.then((ready) => { complete("resolved", ready); return ready; }, (error) => { complete("threw"); throw error; });
171
+ };
128
172
  const [kimiReady, hermesReady, openCodeReady, deepSeekHarnessReady] = await Promise.all([
129
- present.includes("kimi") ? kimiAcpProbe(signal) : false,
130
- present.includes("hermes") ? hermesAcpProbe(signal) : false,
131
- present.includes("opencode") ? openCodeProbe(signal) : false,
132
- present.includes("deepseek-harness") ? deepSeekHarnessProbe(signal) : false,
173
+ present.includes("kimi") ? probe("kimi", kimiAcpProbe) : false,
174
+ present.includes("hermes") ? probe("hermes", hermesAcpProbe) : false,
175
+ present.includes("opencode") ? probe("opencode", openCodeProbe) : false,
176
+ present.includes("deepseek-harness")
177
+ ? retryHarnessProbe(() => probe("deepseek-harness", deepSeekHarnessProbe), signal, probeId)
178
+ : false,
133
179
  ]);
134
180
  return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
135
181
  : runtime === "hermes" ? hermesReady
@@ -137,7 +183,7 @@ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimi
137
183
  : runtime === "deepseek-harness" ? deepSeekHarnessReady
138
184
  : true));
139
185
  }
140
- export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, supportsDeepSeekHarnessAcp, signal);
186
+ export const detectExecutionRuntimesWithSignal = (installed, signal, probeId, env) => detectExecutionRuntimes(installed, (probeSignal, id) => supportsKimiAcp(probeSignal, id, env), (probeSignal, id) => supportsHermesAcp(probeSignal, id, env), (probeSignal, id) => supportsOpenCodeRun(probeSignal, id, env), (probeSignal, id) => supportsDeepSeekHarnessAcp(probeSignal, id, env), signal, probeId);
141
187
  export function daemonVersion() {
142
188
  try {
143
189
  const here = dirname(fileURLToPath(import.meta.url));
@@ -174,13 +220,13 @@ async function listAgentHandles(agentsRoot) {
174
220
  }
175
221
  export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform, dependencies = {}) {
176
222
  const [runtimes, agentHandles, additionalCapabilities] = await Promise.all([
177
- (dependencies.detectInstalled ?? detectRuntimes)(),
223
+ (dependencies.detectInstalled ?? (() => detectRuntimes(dependencies.env)))(),
178
224
  listAgentHandles(agentsRoot),
179
225
  dependencies.additionalCapabilities?.() ?? Promise.resolve([]),
180
226
  ]);
181
227
  const backend = executionBackendCapability(runtimePlatform, dependencies.jobObjectProbe);
182
228
  const executionRuntimes = backend.supported
183
- ? await (dependencies.detectExecutable ?? detectExecutionRuntimes)(runtimes)
229
+ ? await (dependencies.detectExecutable ?? ((installed) => detectExecutionRuntimesWithSignal(installed, undefined, undefined, dependencies.env)))(runtimes)
184
230
  : [];
185
231
  const baseCapabilities = dependencies.capabilities ?? daemonCapabilities(runtimePlatform);
186
232
  const capabilities = additionalCapabilities.length === 0 && Object.isFrozen(baseCapabilities)
@@ -203,7 +249,6 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
203
249
  runtimes,
204
250
  executionRuntimes,
205
251
  capabilities,
206
- ...(dependencies.codexHomeMode === undefined ? {} : { codexHomeMode: dependencies.codexHomeMode }),
207
252
  ...(dependencies.profileName === undefined ? {} : {
208
253
  layoutVersion: 1,
209
254
  profileName: dependencies.profileName,
@@ -0,0 +1,189 @@
1
+ import { WebSocket } from "ws";
2
+ import { createMachineEnvironmentProvider, createStaticMachineEnvironmentProvider } from "./machine-environment-cache.js";
3
+ import { copyMachineEnvironment } from "./machine-environment-policy.js";
4
+ import { collectMachineHello, detectRuntimes, detectExecutionRuntimesWithSignal } from "./machine-info.js";
5
+ import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator } from "./runtime-probe.js";
6
+ import { augmentedPath } from "./runtime-path.js";
7
+ import { mergeRuntimeEnvironments, runtimeEnvironmentValue } from "./runtime-environment-merge.js";
8
+ import { listRuntimeModels } from "./list-models.js";
9
+ import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js";
10
+ import { dslog } from "./slog.js";
11
+ const version = (snapshot) => `${snapshot.status.bootId}:${snapshot.status.generation}`;
12
+ /** 启动 hello 不等待 shell;每次成功环境代次以独立探测和连接围栏更新运行能力。 */
13
+ export function createMachineRuntimeEnvironment(options) {
14
+ const startupEnv = copyMachineEnvironment(options.startupEnv ?? process.env);
15
+ let stopped = false;
16
+ let connection;
17
+ let cycle;
18
+ let accepted;
19
+ let latest;
20
+ let stopPromise;
21
+ let warmed = false;
22
+ const retiredBoots = new Set();
23
+ const preparedEnvironments = new WeakMap();
24
+ const prepare = (snapshot) => {
25
+ const existing = preparedEnvironments.get(snapshot.env);
26
+ if (existing)
27
+ return existing;
28
+ const source = { ...snapshot.env, PATH: runtimeEnvironmentValue(snapshot.env, "PATH") ?? "" };
29
+ const env = Object.freeze(mergeRuntimeEnvironments([snapshot.env, { PATH: augmentedPath(source, {}, process.platform, "append") }]));
30
+ preparedEnvironments.set(snapshot.env, env);
31
+ return env;
32
+ };
33
+ const current = (candidate) => !stopped && cycle === candidate
34
+ && connection === candidate.connection && connection.socket.readyState === WebSocket.OPEN
35
+ && version(latest) === version(candidate.snapshot);
36
+ const inactiveReason = (candidate) => connection !== candidate.connection
37
+ ? "stale_connection" : candidate.connection.socket.readyState !== WebSocket.OPEN
38
+ ? "socket_not_open" : "stale_environment";
39
+ const refreshRuntimes = () => {
40
+ if (stopped || !connection || connection.socket.readyState !== WebSocket.OPEN)
41
+ return;
42
+ cycle?.probe.stop();
43
+ const next = { connection, snapshot: latest, probe: createRuntimeProbeCoordinator(), facts: Promise.resolve([]), runtimes: [], known: false };
44
+ cycle = next;
45
+ const env = prepare(next.snapshot);
46
+ const collect = options.machineInfo?.collect ?? collectMachineHello;
47
+ const helloPromise = collect(options.config.agentsRoot, options.config.executionLimits, process.platform, {
48
+ env,
49
+ detectInstalled: () => (options.machineInfo?.detectInstalled ?? detectRuntimes)(env),
50
+ detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
51
+ additionalCapabilities: options.additionalCapabilities,
52
+ capabilities: next.connection.capabilities,
53
+ ...(options.profileName === undefined ? {} : { profileName: options.profileName }),
54
+ });
55
+ next.facts = helloPromise.then(async (hello) => {
56
+ if (!current(next))
57
+ return [];
58
+ next.runtimes = hello.executionRuntimes;
59
+ const initialPublished = next.connection.beforePublish.then(() => {
60
+ if (current(next))
61
+ options.publish(hello, next.connection.socket);
62
+ });
63
+ const logDecision = (reason, runtimes) => dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
64
+ connection_id: next.connection.connectionId, reason, current_socket: connection === next.connection,
65
+ socket_open: next.connection.socket.readyState === WebSocket.OPEN, socket_ready_state: next.connection.socket.readyState,
66
+ installed_runtimes: JSON.stringify(hello.runtimes), initial_execution_runtimes: JSON.stringify(hello.executionRuntimes),
67
+ execution_runtimes: JSON.stringify(runtimes), environment_boot_id: next.snapshot.status.bootId, environment_generation: next.snapshot.status.generation,
68
+ });
69
+ if (hello.executionProtocol === undefined) {
70
+ next.known = true;
71
+ logDecision("protocol_unavailable", []);
72
+ await initialPublished;
73
+ return [];
74
+ }
75
+ const detector = options.machineInfo?.detectExecutable ?? detectExecutionRuntimesWithSignal;
76
+ const detected = await next.probe.detect(hello.runtimes, (installed, signal, probeId) => detector(installed, signal, probeId, env), next.connection.connectionId, version(next.snapshot));
77
+ if (!current(next)) {
78
+ logDecision(inactiveReason(next), detected);
79
+ return [];
80
+ }
81
+ next.runtimes = detected;
82
+ next.known = true;
83
+ await initialPublished;
84
+ const changed = JSON.stringify(detected) !== JSON.stringify(hello.executionRuntimes);
85
+ logDecision(!current(next) ? inactiveReason(next) : changed ? "publish" : "unchanged", detected);
86
+ if (current(next) && changed)
87
+ options.publish({ ...hello, executionRuntimes: [...detected] }, next.connection.socket);
88
+ return detected;
89
+ }).catch(() => {
90
+ if (current(next)) {
91
+ next.known = true;
92
+ options.onError("runtime_detection_failed", {
93
+ reason: "detection_threw", ...(next.connection.connectionId === undefined ? {} : { connection_id: next.connection.connectionId }),
94
+ current_socket: connection === next.connection, socket_open: next.connection.socket.readyState === WebSocket.OPEN,
95
+ });
96
+ }
97
+ return [];
98
+ });
99
+ };
100
+ const observe = (snapshot) => {
101
+ if (stopped)
102
+ return;
103
+ if (snapshot.status.state !== "ready") {
104
+ // 预热可先给出账号归属已恢复的基线;loading/disabled 本身不触发重新探测。
105
+ if (!accepted && !connection)
106
+ latest = snapshot;
107
+ return;
108
+ }
109
+ if (accepted) {
110
+ if (retiredBoots.has(snapshot.status.bootId))
111
+ return;
112
+ if (snapshot.status.bootId === accepted.status.bootId && snapshot.status.generation <= accepted.status.generation)
113
+ return;
114
+ if (snapshot.status.bootId !== accepted.status.bootId)
115
+ retiredBoots.add(accepted.status.bootId);
116
+ }
117
+ accepted = snapshot;
118
+ latest = snapshot;
119
+ refreshRuntimes();
120
+ };
121
+ const source = options.provider ?? (options.config.machineEnvironmentMode === undefined || options.config.machineEnvironmentMode === "off"
122
+ ? createStaticMachineEnvironmentProvider(startupEnv)
123
+ : createMachineEnvironmentProvider({ startupEnv, mode: options.config.machineEnvironmentMode, onChange: observe }));
124
+ latest = Object.freeze({ env: startupEnv, status: source.status() });
125
+ const stop = () => {
126
+ if (stopPromise)
127
+ return stopPromise;
128
+ stopped = true;
129
+ cycle?.probe.stop();
130
+ connection = undefined;
131
+ stopPromise = Promise.resolve().then(() => source.stop()).catch(() => { throw new Error("machine_environment_stop_failed"); });
132
+ return stopPromise;
133
+ };
134
+ const provider = {
135
+ async acquire(input) { if (stopped)
136
+ return latest; const snapshot = await source.acquire(input); observe(snapshot); return snapshot; },
137
+ status: () => source.status(), stop,
138
+ };
139
+ return {
140
+ provider,
141
+ prewarm: () => {
142
+ if (stopped || warmed)
143
+ return;
144
+ warmed = true;
145
+ void provider.acquire().catch(() => { if (!stopped)
146
+ options.onError("machine_environment_capture_failed"); });
147
+ },
148
+ open: (socket, input) => { connection = { socket, ...input }; refreshRuntimes(); },
149
+ close: (socket) => { if (connection?.socket === socket) {
150
+ connection = undefined;
151
+ cycle?.probe.stop();
152
+ cycle = undefined;
153
+ } },
154
+ async availableRuntimes(runtime) {
155
+ while (!stopped && cycle) {
156
+ const active = cycle;
157
+ if (active.runtimes.includes(runtime))
158
+ return active.runtimes;
159
+ await active.facts;
160
+ if (cycle === active)
161
+ return active.runtimes;
162
+ }
163
+ return [];
164
+ },
165
+ async models(runtime) {
166
+ const snapshot = await provider.acquire();
167
+ if (stopped || version(latest) !== version(snapshot))
168
+ throw new Error("runtime_environment_changed");
169
+ const models = await (options.models ?? listRuntimeModels)(runtime, { env: prepare(snapshot), environmentGeneration: version(snapshot) });
170
+ if (stopped || version(latest) !== version(snapshot))
171
+ throw new Error("runtime_environment_changed");
172
+ return models;
173
+ },
174
+ async health(decoded, probe = probeRuntimeHealth) {
175
+ return handleRuntimeProbeFrame(decoded, async (request) => {
176
+ const snapshot = await provider.acquire();
177
+ if (stopped || version(latest) !== version(snapshot))
178
+ throw new Error("runtime_environment_changed");
179
+ const active = cycle;
180
+ const known = active?.known === true && version(active.snapshot) === version(snapshot);
181
+ const result = await probe(request, { env: prepare(snapshot), ...(known ? { availableRuntimes: active.runtimes } : {}) });
182
+ if (stopped || version(latest) !== version(snapshot))
183
+ throw new Error("runtime_environment_changed");
184
+ return result;
185
+ });
186
+ },
187
+ stop,
188
+ };
189
+ }
@@ -0,0 +1,135 @@
1
+ import { access, lstat, realpath, stat } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { MachineEnvironmentCaptureError } from "./machine-environment-contract.js";
5
+ import { isMachineTransientKey, machineCaptureStartupEnvironment, validateMachineEnvironment } from "./machine-environment-policy.js";
6
+ import { runMachineEnvironmentProcess } from "./machine-environment-process.js";
7
+ const quote = (value) => `'${value.replace(/'/gu, `'"'"'`)}'`;
8
+ const ENV_PROGRAM = 'require("node:fs").writeFileSync(3,JSON.stringify(process.env))';
9
+ /** stderr 只在私有有界管道中短暂保留;返回实际入口路径,不返回 trace 或错误文本。 */
10
+ async function tracedZshEntrypoints(bytes, accountHome) {
11
+ if (!bytes)
12
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
13
+ const entries = new Set();
14
+ for (const line of bytes.toString("utf8").split("\n")) {
15
+ if (!line.endsWith("<sourcetrace>"))
16
+ continue;
17
+ const match = /^(.*):\d+> <sourcetrace>$/u.exec(line);
18
+ // 自定义 PS4 隐藏了来源时无法证明入口,不能把未检查的半份环境发布为 ready。
19
+ if (!match?.[1] || match[1].includes("\0"))
20
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
21
+ // printf >&2 可不换行。只接受原生 +path:line> 尾部中唯一真实入口,
22
+ // 不能把前缀噪声(可能含秘密)拼进 sourceLocations。
23
+ const starts = [...match[1].matchAll(/\+/gu)].map((part) => part.index + 1);
24
+ if (starts.length === 0 || starts.length > 32)
25
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
26
+ const paths = [...new Set(starts.map((start) => resolve(accountHome, match[1].slice(start))))]
27
+ .filter((path) => /\/(?:\.zshenv|\.zshrc)$/u.test(path)
28
+ || ["/etc/zshenv", "/etc/zsh/zshenv", "/etc/zshrc", "/etc/zsh/zshrc"].includes(path));
29
+ if (paths.length === 0)
30
+ continue;
31
+ const existing = (await Promise.all(paths.map(async (path) => {
32
+ try {
33
+ return (await stat(path)).isFile() ? path : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }))).filter((path) => path !== null);
39
+ if (existing.length !== 1)
40
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
41
+ entries.add(existing[0]);
42
+ }
43
+ return [...entries];
44
+ }
45
+ export function machineShellSourceLocations(request, effectiveDirectory) {
46
+ if (request.source === "bash")
47
+ return [join(request.account.home, ".bashrc")];
48
+ if (request.source !== "zsh")
49
+ return [];
50
+ const initial = resolve(request.account.home, request.startupEnv.ZDOTDIR ?? request.account.home);
51
+ const effective = effectiveDirectory ?? initial;
52
+ return [...new Set(["/etc/zshenv", "/etc/zsh/zshenv", "/etc/zshrc", "/etc/zsh/zshrc", join(initial, ".zshenv"), join(initial, ".zshrc"), join(effective, ".zshrc")])].slice(0, 8);
53
+ }
54
+ export async function machineEnvironmentSourceFingerprint(locations) {
55
+ return JSON.stringify(await Promise.all(locations.map(async (path) => {
56
+ try {
57
+ const link = await lstat(path);
58
+ const target = await stat(path);
59
+ return [path, link.dev, link.ino, link.mtimeMs, link.ctimeMs, link.size, await realpath(path), target.dev, target.ino, target.mtimeMs, target.ctimeMs, target.size];
60
+ }
61
+ catch (error) {
62
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
63
+ return [path, "missing"];
64
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
65
+ }
66
+ })));
67
+ }
68
+ export const captureShellMachineEnvironment = async (request) => {
69
+ if (request.source !== "bash" && request.source !== "zsh")
70
+ throw new MachineEnvironmentCaptureError("shell_unavailable");
71
+ const file = request.shellPath;
72
+ if (!file || !isAbsolute(file))
73
+ throw new MachineEnvironmentCaptureError("shell_unavailable");
74
+ try {
75
+ await access(file, constants.X_OK);
76
+ }
77
+ catch {
78
+ throw new MachineEnvironmentCaptureError("shell_unavailable");
79
+ }
80
+ const env = machineCaptureStartupEnvironment(request.startupEnv, request.account, process.platform);
81
+ const processOptions = { file, env, cwd: request.account.home, signal: request.signal, deadline: request.deadline };
82
+ const syntaxCheck = async (path) => {
83
+ try {
84
+ await access(path, constants.R_OK);
85
+ }
86
+ catch (error) {
87
+ if (error.code === "ENOENT")
88
+ return;
89
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
90
+ }
91
+ await runMachineEnvironmentProcess({ ...processOptions, args: request.source === "bash" ? ["--noprofile", "--norc", "-n", path] : ["-f", "-n", path], exitErrorCode: "shell_syntax_invalid" });
92
+ };
93
+ let locations = machineShellSourceLocations(request);
94
+ if (request.source === "bash")
95
+ await syntaxCheck(join(request.account.home, ".bashrc"));
96
+ const command = `${request.source === "zsh" ? "[[ -o sourcetrace ]] || exit 73; " : ""}exec ${quote(process.execPath)} -e ${quote(ENV_PROGRAM)}`;
97
+ const args = request.source === "zsh" ? ["-o", "SOURCE_TRACE", "-ic", command] : ["--noprofile", "--rcfile", join(request.account.home, ".bashrc"), "-ic", command];
98
+ for (let discovery = 0; discovery < 2; discovery++) {
99
+ const before = await machineEnvironmentSourceFingerprint(locations);
100
+ let sourceTraceBytes;
101
+ const bytes = await runMachineEnvironmentProcess({ ...processOptions, args, collectFd: 3, ...(request.source === "zsh" ? { metadataFd: 2, onMetadata: (value) => { sourceTraceBytes = value; } } : {}) });
102
+ if (before !== await machineEnvironmentSourceFingerprint(locations))
103
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
104
+ if (request.source === "zsh") {
105
+ const actualEntrypoints = await tracedZshEntrypoints(sourceTraceBytes, request.account.home);
106
+ sourceTraceBytes = undefined;
107
+ const discovered = actualEntrypoints.some((path) => !locations.includes(path));
108
+ const nextLocations = [...new Set([...locations, ...actualEntrypoints])];
109
+ if (nextLocations.length > 8)
110
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
111
+ const actualBefore = await machineEnvironmentSourceFingerprint(nextLocations);
112
+ for (const path of actualEntrypoints)
113
+ await syntaxCheck(path);
114
+ if (actualBefore !== await machineEnvironmentSourceFingerprint(nextLocations))
115
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
116
+ if (discovered) {
117
+ if (discovery > 0)
118
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
119
+ // 新发现的入口在本次执行前没有指纹;丢弃发现轮,按原截止时间重采一次。
120
+ locations = nextLocations;
121
+ continue;
122
+ }
123
+ }
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(bytes.toString("utf8"));
127
+ }
128
+ catch {
129
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
130
+ }
131
+ const exported = validateMachineEnvironment(parsed, bytes.length);
132
+ return { env: Object.freeze(Object.fromEntries(Object.entries(exported).filter(([key]) => !isMachineTransientKey(key)))), sourceLocations: locations };
133
+ }
134
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
135
+ };