@nowcrew/daemon 0.6.88 → 0.6.90

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 (122) hide show
  1. package/README.md +66 -4
  2. package/dist/agent-ability/controller.js +11 -1
  3. package/dist/agent-ability/git-errors.js +22 -0
  4. package/dist/agent-ability/materializer.js +21 -10
  5. package/dist/agent-ability/resolver.js +12 -4
  6. package/dist/agent-environment-contract.js +55 -0
  7. package/dist/agent-environment-control.js +24 -0
  8. package/dist/agent-environment-execution.js +98 -0
  9. package/dist/agent-environments.js +275 -0
  10. package/dist/automatic-daemon-installer.js +51 -2
  11. package/dist/automatic-install-target.js +8 -0
  12. package/dist/computer-main.js +18 -0
  13. package/dist/computer-profile.js +17 -4
  14. package/dist/computer-setup-client.js +318 -0
  15. package/dist/computer-setup-orchestration.js +203 -0
  16. package/dist/computer-setup.js +244 -0
  17. package/dist/config.js +30 -12
  18. package/dist/console-formatter.js +3 -3
  19. package/dist/console.js +15 -2
  20. package/dist/daemon-update-eligibility.js +1 -1
  21. package/dist/execution-journal.js +9 -6
  22. package/dist/execution-protocol.js +1 -0
  23. package/dist/execution-runner.js +13 -8
  24. package/dist/i18n.js +19 -0
  25. package/dist/list-models.js +12 -12
  26. package/dist/local-execution-lease.js +22 -0
  27. package/dist/local-executor.js +57 -84
  28. package/dist/local-memory/candidate.js +43 -0
  29. package/dist/local-memory/index-budget.js +8 -0
  30. package/dist/local-memory/inventory-cli.js +61 -0
  31. package/dist/local-memory/inventory-preview.js +159 -0
  32. package/dist/local-memory/inventory-source.js +159 -0
  33. package/dist/local-memory/pending-match.js +29 -0
  34. package/dist/local-memory/pending-query.js +100 -0
  35. package/dist/local-memory/pending-schema.js +29 -0
  36. package/dist/local-memory/pending-source.js +126 -0
  37. package/dist/local-memory/pending.js +34 -84
  38. package/dist/local-memory/prompt.js +19 -12
  39. package/dist/local-memory/read-cli.js +97 -12
  40. package/dist/local-memory/rollout.js +82 -0
  41. package/dist/local-memory/runtime.js +23 -17
  42. package/dist/local-memory/work-log-execution.js +31 -0
  43. package/dist/local-memory/work-log.js +58 -0
  44. package/dist/local-memory/write.js +27 -58
  45. package/dist/local-runtime-environment.js +50 -0
  46. package/dist/machine-environment-cache.js +147 -0
  47. package/dist/machine-environment-contract.js +10 -0
  48. package/dist/machine-environment-control.js +27 -0
  49. package/dist/machine-environment-policy.js +75 -0
  50. package/dist/machine-environment-process.js +110 -0
  51. package/dist/machine-info.js +70 -25
  52. package/dist/machine-runtime-environment.js +189 -0
  53. package/dist/machine-shell-environment-capture.js +135 -0
  54. package/dist/machine-windows-environment-capture.js +215 -0
  55. package/dist/machine-windows-environment-script.js +158 -0
  56. package/dist/main.js +4 -69
  57. package/dist/marketplace-command.js +21 -0
  58. package/dist/marketplace-controller.js +80 -0
  59. package/dist/marketplace-protocol.js +38 -0
  60. package/dist/marketplace-remote.js +48 -0
  61. package/dist/marketplace-runner.js +235 -0
  62. package/dist/marketplace-serve.js +17 -0
  63. package/dist/marketplace-source.js +103 -0
  64. package/dist/message-file-preview.js +246 -0
  65. package/dist/pre-admission-rejection.js +64 -0
  66. package/dist/project-skills/execution-adapter.js +3 -2
  67. package/dist/project-skills/runtime-launch.js +14 -0
  68. package/dist/provider-env.js +26 -15
  69. package/dist/runner.js +17 -4
  70. package/dist/runtime-environment-merge.js +16 -0
  71. package/dist/runtime-health-handler.js +10 -0
  72. package/dist/runtime-health.js +3 -1
  73. package/dist/runtime-path.js +22 -12
  74. package/dist/runtime-probe.js +35 -6
  75. package/dist/runtimes/acp-probe-transport.js +69 -0
  76. package/dist/runtimes/claude-models.js +20 -9
  77. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  78. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  79. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  80. package/dist/runtimes/deepseek-harness.js +76 -15
  81. package/dist/runtimes/harness-probe-retry.js +42 -0
  82. package/dist/runtimes/hermes-models.js +2 -1
  83. package/dist/runtimes/kimi-acp-runner.js +78 -13
  84. package/dist/runtimes/opencode.js +2 -2
  85. package/dist/serve.js +88 -176
  86. package/dist/server-url.js +25 -0
  87. package/dist/slog.js +2 -1
  88. package/dist/storage-identity.js +0 -3
  89. package/dist/storage-runtime.js +2 -2
  90. package/dist/storage-serve-adapter.js +6 -72
  91. package/dist/supervised-runtime.js +17 -2
  92. package/dist/token.js +0 -1
  93. package/dist/workspace-control-handler.js +27 -0
  94. package/dist/workspace-fs.js +66 -15
  95. package/dist/workspace.js +1 -1
  96. package/package.json +6 -4
  97. package/dist/codex-home-launch.js +0 -7
  98. package/dist/codex-home-mode-controller.js +0 -68
  99. package/dist/daemon-migration-controller.js +0 -211
  100. package/dist/daemon-migration-wiring.js +0 -22
  101. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  102. package/dist/runtimes/codex-home-migration.js +0 -112
  103. package/dist/runtimes/codex-home.js +0 -236
  104. package/dist/storage-control.js +0 -212
  105. package/dist/storage-copy.js +0 -628
  106. package/dist/storage-credential-guard.js +0 -21
  107. package/dist/storage-doctor.js +0 -228
  108. package/dist/storage-manifest.js +0 -1027
  109. package/dist/storage-migration-controller.js +0 -440
  110. package/dist/storage-migration-manifest.js +0 -105
  111. package/dist/storage-migration-policy.js +0 -100
  112. package/dist/storage-runtime-controller.js +0 -381
  113. package/dist/storage-runtime-readiness.js +0 -240
  114. package/dist/storage-safe-fs.js +0 -302
  115. package/dist/storage-status-delivery.js +0 -224
  116. package/dist/storage-status-replay.js +0 -140
  117. package/dist/storage-target-confirmation.js +0 -129
  118. package/dist/storage-worker-command.js +0 -37
  119. package/dist/storage-worker-executor.js +0 -449
  120. package/dist/storage-worker-handoff.js +0 -151
  121. package/dist/storage-worker-operation.js +0 -294
  122. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,50 @@
1
+ import { delimiter, join } from "node:path";
2
+ import { environmentForExecution } from "./agent-environments.js";
3
+ import { augmentedPath } from "./runtime-path.js";
4
+ import { sanitizeEnvVars } from "./runtime-env.js";
5
+ import { machinePackageManagerEnv } from "./package-manager-env.js";
6
+ import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
7
+ import { mergeRuntimeEnvironments } from "./runtime-environment-merge.js";
8
+ /** 机器、目录与受控运行字段分层合并,Windows 不依赖对象中键名的偶然顺序。 */
9
+ export function buildLocalRuntimeEnvironment(context) {
10
+ const { input, workspace, executionWorkspace, inheritedEnv, providerConfig, crewHome, contextThreadId, nativeThreadContextPath, reportContext, runtime } = context;
11
+ const agentEnvironment = sanitizeEnvVars(providerConfig.environmentPlan === undefined ? providerConfig.envVars
12
+ : environmentForExecution(providerConfig.environmentPlan, workspace.dir, input.projectContext?.primary?.root ?? executionWorkspace.runDir, input.projectContext?.primary?.projectId));
13
+ return mergeRuntimeEnvironments([
14
+ inheritedEnv, agentEnvironment,
15
+ input.trainingProjectContext === undefined ? {} : machinePackageManagerEnv(inheritedEnv),
16
+ input.launch.systemEnv ?? {}, {
17
+ PATH: `${workspace.crewDir}${delimiter}${augmentedPath(inheritedEnv, {}, process.platform, "append")}`,
18
+ CREW_SERVER_URL: input.launch.serverUrl,
19
+ CREW_TOKEN: input.launch.token,
20
+ CREW_CHANNEL: input.channelId,
21
+ CREW_HOME: crewHome,
22
+ // Empty values override any stale inherited hints from an enclosing runtime.
23
+ CREW_THREAD_ROOT: contextThreadId ?? "",
24
+ CREW_THREAD_CONTEXT_FILE: nativeThreadContextPath ?? "",
25
+ // Keep the daemon-selected storage root authoritative for runtimes.
26
+ // In profile-backed NowWork sessions the daemon config may differ from
27
+ // the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
28
+ // sufficient for QA/project diagnostics.
29
+ CREW_AGENTS_ROOT: input.launch.agentsRoot,
30
+ ...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
31
+ ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
32
+ // Expose only the training root; ability-specific workspace layout stays
33
+ // owned by the training package rather than the NowWork daemon.
34
+ QA_RUNTIME_ROOT: join(crewHome, "training"),
35
+ QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
36
+ CREW_TASK_DIR: executionWorkspace.runDir,
37
+ CREW_TASK_LOG: executionWorkspace.workLogPath,
38
+ ...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
39
+ XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
40
+ XDG_DATA_HOME: join(workspace.homeDir, ".local", "share"),
41
+ XDG_CACHE_HOME: join(workspace.homeDir, ".cache"),
42
+ GH_CONFIG_DIR: join(workspace.homeDir, ".config", "gh"),
43
+ CLOUDSDK_CONFIG: join(workspace.homeDir, ".config", "gcloud"),
44
+ ...(runtime.name === "kimi" && runtime.reasoning
45
+ && KIMI_EFFORT_LEVELS.includes(runtime.reasoning)
46
+ ? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
47
+ : {}),
48
+ },
49
+ ]);
50
+ }
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { MachineEnvironmentCaptureError } from "./machine-environment-contract.js";
3
+ import { copyMachineEnvironment, machineCaptureStartupEnvironment, resolveMachineAccount, restoreMachineEnvironmentOwnership, selectMachineEnvironmentSource, validateMachineEnvironment } from "./machine-environment-policy.js";
4
+ import { captureShellMachineEnvironment, machineEnvironmentSourceFingerprint, machineShellSourceLocations } from "./machine-shell-environment-capture.js";
5
+ function frozenStatus(status) {
6
+ return Object.freeze({ ...status, sourceLocations: Object.freeze([...new Set(status.sourceLocations)].slice(0, 8)) });
7
+ }
8
+ /** 测试或明确关闭机器环境时使用;永远不会启动 shell,也不读取真实用户 rc。 */
9
+ export function createStaticMachineEnvironmentProvider(startupEnv = {}) {
10
+ const snapshot = Object.freeze({ env: copyMachineEnvironment(startupEnv), status: frozenStatus({ bootId: randomUUID(), generation: 0, state: "disabled", source: null, sourceLocations: [], capturedAt: null, attemptedAt: null, errorCode: null }) });
11
+ return { acquire: async () => snapshot, status: () => snapshot.status, stop: async () => { } };
12
+ }
13
+ const sameEnvironment = (left, right) => {
14
+ const keys = Object.keys(left);
15
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && left[key] === right[key]);
16
+ };
17
+ export function createMachineEnvironmentProvider(options = {}) {
18
+ const startup = copyMachineEnvironment(options.startupEnv ?? process.env);
19
+ const mode = options.mode ?? "auto";
20
+ const platform = options.platform ?? process.platform;
21
+ const now = options.now ?? Date.now;
22
+ let account = Object.freeze({ ...(options.account ?? resolveMachineAccount()) });
23
+ const selected = selectMachineEnvironmentSource(mode, account, startup, platform);
24
+ const baseline = startup;
25
+ const captureStartup = machineCaptureStartupEnvironment(startup, account, platform);
26
+ let watchedLocations = machineShellSourceLocations({ source: selected.source ?? "windows-registry", account, startupEnv: captureStartup });
27
+ let sourceFingerprint = null;
28
+ let snapshot = Object.freeze({ env: baseline, status: frozenStatus({ bootId: randomUUID(), generation: 0, state: mode === "off" ? "disabled" : selected.source === null ? "unsupported" : "loading", source: selected.source, sourceLocations: watchedLocations, capturedAt: null, attemptedAt: null, errorCode: null }) });
29
+ let lastGood = null;
30
+ let lastSuccessTime = -Infinity;
31
+ let lastFailureTime = -Infinity;
32
+ let stopped = false;
33
+ let unsafeCleanup = false;
34
+ let flight;
35
+ let controller;
36
+ let captureEpoch = 0;
37
+ let capturing = false;
38
+ let forcedFollowup;
39
+ const sourceOwnedKeys = new Set();
40
+ const publish = (env, patch) => {
41
+ snapshot = Object.freeze({ env, status: frozenStatus({ ...snapshot.status, ...patch }) });
42
+ try {
43
+ options.onChange?.(snapshot);
44
+ }
45
+ catch { /* 观察者失败不能污染已获取的机器环境。 */ }
46
+ return snapshot;
47
+ };
48
+ const captureWindows = options.captureWindows ?? (async (request) => {
49
+ const module = await import("./machine-windows-environment-capture.js");
50
+ return module.captureWindowsMachineEnvironment(request);
51
+ });
52
+ const refresh = async (force, signal) => {
53
+ if (!selected.source || stopped || unsafeCleanup)
54
+ return snapshot;
55
+ if (!force && now() - lastFailureTime < (options.backoffMs ?? 30_000))
56
+ return snapshot;
57
+ const attemptedAt = new Date(now()).toISOString();
58
+ try {
59
+ if (!force && lastGood && now() - lastSuccessTime < (options.ttlMs ?? 60_000)) {
60
+ if (selected.source === "windows-registry" || sourceFingerprint === await machineEnvironmentSourceFingerprint(watchedLocations))
61
+ return snapshot;
62
+ }
63
+ if (stopped || signal.aborted)
64
+ return snapshot;
65
+ publish(snapshot.env, { state: "loading", attemptedAt, errorCode: null });
66
+ capturing = true;
67
+ const deadline = Date.now() + (options.captureTimeoutMs ?? (selected.source === "windows-registry" ? 8_000 : 5_000));
68
+ const capture = selected.source === "windows-registry" ? captureWindows : options.captureShell ?? captureShellMachineEnvironment;
69
+ for (let attempt = 0; attempt < 2; attempt++) {
70
+ try {
71
+ const before = selected.source === "windows-registry" ? null : await machineEnvironmentSourceFingerprint(watchedLocations);
72
+ captureEpoch++;
73
+ const captured = await capture({ startupEnv: captureStartup, account, source: selected.source, ...(selected.shellPath ? { shellPath: selected.shellPath } : {}), signal, deadline });
74
+ if (signal.aborted)
75
+ throw new MachineEnvironmentCaptureError("shell_capture_timeout");
76
+ if (before !== null && before !== await machineEnvironmentSourceFingerprint(watchedLocations))
77
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
78
+ let env = validateMachineEnvironment(captured.env);
79
+ let currentOwned;
80
+ if (selected.source === "windows-registry") {
81
+ currentOwned = new Set((captured.sourceOwnedKeys ?? []).map((key) => key.toUpperCase()));
82
+ const merged = new Map();
83
+ for (const [key, value] of [...Object.entries(startup), ...Object.entries(env)]) {
84
+ if (!sourceOwnedKeys.has(key.toUpperCase()) || currentOwned.has(key.toUpperCase()))
85
+ merged.set(key.toUpperCase(), [key, value]);
86
+ }
87
+ env = Object.fromEntries(merged.values());
88
+ }
89
+ env = validateMachineEnvironment(restoreMachineEnvironmentOwnership(env, startup, account, platform));
90
+ const unchanged = lastGood !== null && sameEnvironment(lastGood.env, env);
91
+ const generation = unchanged && lastGood ? lastGood.status.generation : (lastGood?.status.generation ?? 0) + 1;
92
+ const locations = [...new Set(captured.sourceLocations)].slice(0, 8);
93
+ watchedLocations = locations;
94
+ sourceFingerprint = selected.source === "windows-registry" ? null : await machineEnvironmentSourceFingerprint(locations);
95
+ for (const key of currentOwned ?? [])
96
+ sourceOwnedKeys.add(key);
97
+ if (selected.source === "windows-registry" && captured.accountSid)
98
+ account = Object.freeze({ ...account, sid: captured.accountSid });
99
+ lastSuccessTime = now();
100
+ lastFailureTime = -Infinity;
101
+ lastGood = publish(unchanged && lastGood ? lastGood.env : env, { state: "ready", generation, sourceLocations: locations, capturedAt: new Date(lastSuccessTime).toISOString(), errorCode: null });
102
+ return lastGood;
103
+ }
104
+ catch (error) {
105
+ if (error instanceof MachineEnvironmentCaptureError && error.code === "shell_files_changed" && attempt === 0 && Date.now() < deadline && !signal.aborted)
106
+ continue;
107
+ throw error;
108
+ }
109
+ }
110
+ throw new MachineEnvironmentCaptureError("shell_files_changed");
111
+ }
112
+ catch (error) {
113
+ const errorCode = error instanceof MachineEnvironmentCaptureError ? error.code : selected.source === "windows-registry" ? "windows_environment_unavailable" : "shell_capture_failed";
114
+ if (errorCode === "shell_cleanup_failed")
115
+ unsafeCleanup = true;
116
+ lastFailureTime = now();
117
+ return publish(lastGood?.env ?? baseline, { state: lastGood ? "stale" : "fallback", errorCode, attemptedAt });
118
+ }
119
+ finally {
120
+ capturing = false;
121
+ }
122
+ };
123
+ const acquire = (acquireOptions) => {
124
+ if (flight) {
125
+ if (acquireOptions?.force && !capturing) {
126
+ const requestedEpoch = captureEpoch;
127
+ forcedFollowup ??= flight.then((value) => captureEpoch > requestedEpoch ? value : acquire({ force: true })).finally(() => { forcedFollowup = undefined; });
128
+ return forcedFollowup;
129
+ }
130
+ return flight;
131
+ }
132
+ if (stopped || unsafeCleanup || selected.source === null)
133
+ return Promise.resolve(snapshot);
134
+ controller = new AbortController();
135
+ flight = refresh(acquireOptions?.force === true, controller.signal).finally(() => { flight = undefined; controller = undefined; });
136
+ return flight;
137
+ };
138
+ return {
139
+ acquire,
140
+ status: () => snapshot.status,
141
+ async stop() {
142
+ stopped = true;
143
+ controller?.abort();
144
+ await flight;
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,10 @@
1
+ export class MachineEnvironmentCaptureError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super(code);
5
+ this.code = code;
6
+ this.name = "MachineEnvironmentCaptureError";
7
+ }
8
+ }
9
+ export const MACHINE_ENVIRONMENT_MAX_BYTES = 1024 * 1024;
10
+ export const MACHINE_ENVIRONMENT_MAX_ENTRIES = 4096;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ const Request = z.object({ type: z.enum(["machine:environment:get", "machine:environment:refresh"]), reqId: z.string().min(1).max(200) }).strict();
3
+ /** 发送回调由连接持有,刷新完成后不得把旧请求回复到新 socket。 */
4
+ export async function handleMachineEnvironmentFrame(decoded, provider, send) {
5
+ if (typeof decoded !== "object" || decoded === null || !("type" in decoded)
6
+ || (decoded.type !== "machine:environment:get" && decoded.type !== "machine:environment:refresh"))
7
+ return false;
8
+ const reqId = "reqId" in decoded && typeof decoded.reqId === "string" ? decoded.reqId.slice(0, 200) : "invalid";
9
+ const reply = (result) => { try {
10
+ send({ type: "fs:result", reqId, ...result });
11
+ }
12
+ catch { /* 连接失效由服务端超时处理。 */ } };
13
+ const parsed = Request.safeParse(decoded);
14
+ if (!parsed.success) {
15
+ reply({ ok: false, error: "machine_environment_request_invalid" });
16
+ return true;
17
+ }
18
+ try {
19
+ if (parsed.data.type === "machine:environment:refresh")
20
+ await provider.acquire({ force: true });
21
+ reply({ ok: true, data: provider.status() });
22
+ }
23
+ catch {
24
+ reply({ ok: false, error: "machine_environment_request_failed" });
25
+ }
26
+ return true;
27
+ }
@@ -0,0 +1,75 @@
1
+ import { userInfo } from "node:os";
2
+ import { basename, isAbsolute, join } from "node:path";
3
+ import { MACHINE_ENVIRONMENT_MAX_BYTES, MACHINE_ENVIRONMENT_MAX_ENTRIES, MachineEnvironmentCaptureError } from "./machine-environment-contract.js";
4
+ const ACCOUNT_KEYS = new Set(["HOME", "USER", "LOGNAME", "SHELL", "USERPROFILE", "USERNAME", "USERDOMAIN", "USERDOMAIN_ROAMINGPROFILE", "HOMEDRIVE", "HOMEPATH", "SYSTEMROOT", "WINDIR", "COMSPEC"]);
5
+ const CONFIGURED_KEYS = new Set(["QA_REPORT_HUB", "QA_RUNTIME_ROOT", "QA_REPORT_DELIVERY_CONTEXT"]);
6
+ export const isMachineControlKey = (key) => /^(CREW_|NOWWORK_)/iu.test(key);
7
+ export const isMachineTransientKey = (key) => ["_", "SHLVL", "PWD", "OLDPWD"].includes(key) || key.startsWith("BASH_FUNC_");
8
+ export function copyMachineEnvironment(env) {
9
+ return Object.freeze(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string")));
10
+ }
11
+ export function resolveMachineAccount() {
12
+ const info = userInfo();
13
+ return Object.freeze({ home: info.homedir, username: info.username, shell: info.shell, uid: info.uid });
14
+ }
15
+ /** 账号来自操作系统,不受 daemon 的 profile HOME 或 rc 改写影响。 */
16
+ export function restoreMachineEnvironmentOwnership(captured, startup, account, platform) {
17
+ const protectedKeys = new Set(Object.keys(startup).filter((key) => CONFIGURED_KEYS.has(key.toUpperCase())).map((key) => key.toUpperCase()));
18
+ const owns = (key) => isMachineControlKey(key) || ACCOUNT_KEYS.has(key.toUpperCase()) || protectedKeys.has(key.toUpperCase());
19
+ const entries = Object.entries(captured).filter(([key]) => !owns(key) && !isMachineTransientKey(key));
20
+ entries.push(...Object.entries(startup).filter(([key]) => owns(key)));
21
+ const env = Object.fromEntries(entries);
22
+ if (platform === "win32") {
23
+ for (const key of Object.keys(env))
24
+ if (["HOME", "USERPROFILE", "USERNAME"].includes(key.toUpperCase()))
25
+ delete env[key];
26
+ env.USERPROFILE = account.home;
27
+ env.USERNAME = account.username;
28
+ if (startup.HOME !== undefined)
29
+ env.HOME = account.home;
30
+ }
31
+ else {
32
+ env.HOME = account.home;
33
+ env.USER = account.username;
34
+ env.LOGNAME = account.username;
35
+ const shell = account.shell || startup.SHELL;
36
+ if (shell)
37
+ env.SHELL = shell;
38
+ else
39
+ delete env.SHELL;
40
+ }
41
+ return Object.freeze(env);
42
+ }
43
+ export function machineCaptureStartupEnvironment(startup, account, platform) {
44
+ const owned = restoreMachineEnvironmentOwnership(startup, startup, account, platform);
45
+ return Object.freeze(Object.fromEntries(Object.entries(owned).filter(([key]) => !isMachineControlKey(key) && !isMachineTransientKey(key))));
46
+ }
47
+ export function selectMachineEnvironmentSource(mode, account, startup, platform) {
48
+ if (mode === "off")
49
+ return { source: null };
50
+ if (platform === "win32")
51
+ return { source: mode === "auto" || mode === "windows" ? "windows-registry" : null };
52
+ if (mode === "windows")
53
+ return { source: null };
54
+ const loginShell = account.shell || startup.SHELL;
55
+ const name = mode === "auto" ? basename(loginShell ?? "") : mode;
56
+ if (name !== "bash" && name !== "zsh")
57
+ return { source: null };
58
+ const shellPath = loginShell && isAbsolute(loginShell) && basename(loginShell) === name ? loginShell : join("/bin", name);
59
+ return { source: name, shellPath };
60
+ }
61
+ export function validateMachineEnvironment(value, bytes) {
62
+ if (bytes !== undefined && bytes > MACHINE_ENVIRONMENT_MAX_BYTES)
63
+ throw new MachineEnvironmentCaptureError("shell_capture_too_large");
64
+ if (typeof value !== "object" || value === null || Array.isArray(value))
65
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
66
+ const entries = Object.entries(value);
67
+ if (entries.length > MACHINE_ENVIRONMENT_MAX_ENTRIES)
68
+ throw new MachineEnvironmentCaptureError("shell_capture_too_large");
69
+ if (entries.some(([key, item]) => !key || key.includes("=") || key.includes("\0") || typeof item !== "string" || item.includes("\0"))) {
70
+ throw new MachineEnvironmentCaptureError("shell_capture_failed");
71
+ }
72
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > MACHINE_ENVIRONMENT_MAX_BYTES)
73
+ throw new MachineEnvironmentCaptureError("shell_capture_too_large");
74
+ return Object.freeze(Object.fromEntries(entries));
75
+ }
@@ -0,0 +1,110 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { MACHINE_ENVIRONMENT_MAX_BYTES, MachineEnvironmentCaptureError } from "./machine-environment-contract.js";
4
+ /** 只接收本模块 detached spawn 所创建的进程组,绝不复用 supervisor 的 PID 清理接口。 */
5
+ async function cleanupOwnedGroup(pid) {
6
+ const exists = () => {
7
+ try {
8
+ process.kill(-pid, 0);
9
+ return true;
10
+ }
11
+ catch (error) {
12
+ if (error.code === "ESRCH")
13
+ return false;
14
+ throw new MachineEnvironmentCaptureError("shell_cleanup_failed");
15
+ }
16
+ };
17
+ const signal = (value) => {
18
+ try {
19
+ process.kill(-pid, value);
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "ESRCH")
23
+ throw new MachineEnvironmentCaptureError("shell_cleanup_failed");
24
+ }
25
+ };
26
+ if (!exists())
27
+ return;
28
+ for (const value of ["SIGTERM", "SIGKILL"]) {
29
+ signal(value);
30
+ const until = Date.now() + 1_000;
31
+ while (exists() && Date.now() < until)
32
+ await delay(20);
33
+ if (!exists())
34
+ return;
35
+ }
36
+ throw new MachineEnvironmentCaptureError("shell_cleanup_failed");
37
+ }
38
+ export async function runMachineEnvironmentProcess(options) {
39
+ if (options.signal.aborted || Date.now() >= options.deadline)
40
+ throw new MachineEnvironmentCaptureError("shell_capture_timeout");
41
+ const collectFd = options.collectFd;
42
+ const stdio = Array.from({ length: Math.max(3, (collectFd ?? 2) + 1, (options.metadataFd ?? 2) + 1) }, () => "ignore");
43
+ if (collectFd !== undefined)
44
+ stdio[collectFd] = "pipe";
45
+ if (options.metadataFd !== undefined)
46
+ stdio[options.metadataFd] = "pipe";
47
+ const child = spawn(options.file, [...options.args], { cwd: options.cwd, env: options.env, detached: true, stdio });
48
+ const chunks = [];
49
+ let size = 0;
50
+ const metadata = [];
51
+ let metadataSize = 0;
52
+ let timer;
53
+ let abort;
54
+ let cleanup;
55
+ const cleanupOnce = () => cleanup ??= child.pid === undefined ? Promise.resolve() : cleanupOwnedGroup(child.pid);
56
+ try {
57
+ return await new Promise((resolve, reject) => {
58
+ const fail = (code) => reject(new MachineEnvironmentCaptureError(code));
59
+ abort = () => fail("shell_capture_timeout");
60
+ options.signal.addEventListener("abort", abort, { once: true });
61
+ timer = setTimeout(abort, Math.max(0, options.deadline - Date.now()));
62
+ const channel = collectFd === undefined ? undefined : child.stdio[collectFd];
63
+ channel?.on("data", (chunk) => {
64
+ size += chunk.length;
65
+ if (size > (options.maxBytes ?? MACHINE_ENVIRONMENT_MAX_BYTES))
66
+ fail("shell_capture_too_large");
67
+ else
68
+ chunks.push(chunk);
69
+ });
70
+ channel?.on("error", () => fail("shell_capture_failed"));
71
+ const metadataChannel = options.metadataFd === undefined ? undefined : child.stdio[options.metadataFd];
72
+ metadataChannel?.on("data", (chunk) => {
73
+ metadataSize += chunk.length;
74
+ if (metadataSize > 16_384)
75
+ fail("shell_capture_too_large");
76
+ else
77
+ metadata.push(chunk);
78
+ });
79
+ metadataChannel?.on("error", () => fail("shell_capture_failed"));
80
+ child.once("error", () => fail("shell_unavailable"));
81
+ child.once("exit", () => {
82
+ // rc 的后台任务会继承 fd3;主进程退出即清理,不能等所有继承者自行 close。
83
+ void cleanupOnce().catch(() => fail("shell_cleanup_failed"));
84
+ });
85
+ child.once("close", (code) => {
86
+ if (code === 0) {
87
+ options.onMetadata?.(Buffer.concat(metadata));
88
+ resolve(Buffer.concat(chunks));
89
+ }
90
+ else
91
+ fail(options.exitErrorCode ?? "shell_capture_failed");
92
+ });
93
+ if (options.signal.aborted)
94
+ abort();
95
+ });
96
+ }
97
+ finally {
98
+ if (timer)
99
+ clearTimeout(timer);
100
+ if (abort)
101
+ options.signal.removeEventListener("abort", abort);
102
+ try {
103
+ await cleanupOnce();
104
+ }
105
+ finally {
106
+ for (const stream of child.stdio)
107
+ stream?.destroy();
108
+ }
109
+ }
110
+ }
@@ -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,