@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.
- package/README.md +66 -4
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +13 -8
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +12 -12
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -84
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +19 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +23 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-command.js +21 -0
- package/dist/marketplace-controller.js +80 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +235 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/marketplace-source.js +103 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +17 -4
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +78 -13
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +88 -176
- package/dist/server-url.js +25 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +6 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { MACHINE_ENVIRONMENT_MAX_BYTES, MACHINE_ENVIRONMENT_MAX_ENTRIES, MachineEnvironmentCaptureError, } from "./machine-environment-contract.js";
|
|
4
|
+
import { WINDOWS_DAEMON_SID_SCRIPT, WINDOWS_ENVIRONMENT_CAPTURE_SCRIPT } from "./machine-windows-environment-script.js";
|
|
5
|
+
const CAPTURE_TIMEOUT_MS = 8_000;
|
|
6
|
+
const CLEANUP_TIMEOUT_MS = 2_000;
|
|
7
|
+
const validSid = (value) => typeof value === "string" && value.length <= 184 && /^S-1-(?:\d+-)+\d+$/.test(value);
|
|
8
|
+
const unavailable = () => new MachineEnvironmentCaptureError("windows_environment_unavailable");
|
|
9
|
+
const validKey = (key) => key.length > 0 && !/[=\0]/.test(key);
|
|
10
|
+
function systemExecutable(startupEnv, relative) {
|
|
11
|
+
const roots = Object.entries(startupEnv).filter(([key]) => key.toUpperCase() === "SYSTEMROOT");
|
|
12
|
+
const root = roots[0]?.[1];
|
|
13
|
+
if (roots.length !== 1 || !root || !/^[A-Za-z]:[\\/]/.test(root) || /[\0\r\n]/.test(root)
|
|
14
|
+
|| root.slice(2).includes(":") || root.split(/[\\/]/).some((part) => part === "." || part === ".."))
|
|
15
|
+
throw unavailable();
|
|
16
|
+
return win32.join(root, "System32", relative);
|
|
17
|
+
}
|
|
18
|
+
/** 仅终止本调用仍持有的活 helper PID;根进程已退出时不按旧 PID 杀进程。 */
|
|
19
|
+
export const runWindowsEnvironmentProcess = (request) => new Promise((resolve, reject) => {
|
|
20
|
+
if (request.signal?.aborted || request.deadline <= Date.now()) {
|
|
21
|
+
reject(new MachineEnvironmentCaptureError("shell_capture_timeout"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let child;
|
|
25
|
+
try {
|
|
26
|
+
child = spawn(request.file, [...request.args], {
|
|
27
|
+
env: { ...request.env }, stdio: ["ignore", "pipe", "pipe"], shell: false, windowsHide: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
reject(unavailable());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let settled = false;
|
|
35
|
+
let failure;
|
|
36
|
+
let cleanupTimer;
|
|
37
|
+
let killer;
|
|
38
|
+
let killerClosed = true;
|
|
39
|
+
let helperClosed = false;
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
function finish(error) {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(captureTimer);
|
|
47
|
+
if (cleanupTimer !== undefined)
|
|
48
|
+
clearTimeout(cleanupTimer);
|
|
49
|
+
request.signal?.removeEventListener("abort", abort);
|
|
50
|
+
if (error) {
|
|
51
|
+
chunks.length = 0;
|
|
52
|
+
reject(new MachineEnvironmentCaptureError(error));
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
resolve(Buffer.concat(chunks));
|
|
56
|
+
}
|
|
57
|
+
function finishCleanup() {
|
|
58
|
+
if (helperClosed && killerClosed)
|
|
59
|
+
finish(failure);
|
|
60
|
+
}
|
|
61
|
+
function stop(code) {
|
|
62
|
+
if (failure || settled)
|
|
63
|
+
return;
|
|
64
|
+
failure = code;
|
|
65
|
+
chunks.length = 0;
|
|
66
|
+
clearTimeout(captureTimer);
|
|
67
|
+
cleanupTimer = setTimeout(() => {
|
|
68
|
+
// Node 的 kill 使用本 ChildProcess 的句柄;这里不使用已退出 helper 的陈旧 PID。
|
|
69
|
+
if (killer && killer.exitCode === null && killer.signalCode === null)
|
|
70
|
+
killer.kill();
|
|
71
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
72
|
+
child.kill();
|
|
73
|
+
child.stdout?.destroy();
|
|
74
|
+
child.stderr?.destroy();
|
|
75
|
+
child.unref();
|
|
76
|
+
finish("shell_cleanup_failed");
|
|
77
|
+
}, CLEANUP_TIMEOUT_MS);
|
|
78
|
+
if (child.pid && child.exitCode === null && child.signalCode === null) {
|
|
79
|
+
try {
|
|
80
|
+
killerClosed = false;
|
|
81
|
+
killer = spawn(systemExecutable(request.env, "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"], {
|
|
82
|
+
env: { ...request.env }, stdio: "ignore", shell: false, windowsHide: true,
|
|
83
|
+
});
|
|
84
|
+
killer.once("error", () => { killerClosed = true; failure = "shell_cleanup_failed"; child.kill(); finishCleanup(); });
|
|
85
|
+
killer.once("close", (exitCode) => {
|
|
86
|
+
killerClosed = true;
|
|
87
|
+
if (exitCode !== 0)
|
|
88
|
+
failure = "shell_cleanup_failed";
|
|
89
|
+
finishCleanup();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
killerClosed = true;
|
|
94
|
+
failure = "shell_cleanup_failed";
|
|
95
|
+
child.kill();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finishCleanup();
|
|
99
|
+
}
|
|
100
|
+
const abort = () => stop("shell_capture_timeout");
|
|
101
|
+
const captureTimer = setTimeout(abort, Math.max(1, request.deadline - Date.now()));
|
|
102
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
103
|
+
child.stdout?.on("data", (data) => {
|
|
104
|
+
if (settled || failure)
|
|
105
|
+
return;
|
|
106
|
+
bytes += data.length;
|
|
107
|
+
if (bytes > request.maxBytes)
|
|
108
|
+
stop("shell_capture_too_large");
|
|
109
|
+
else
|
|
110
|
+
chunks.push(Buffer.from(data));
|
|
111
|
+
});
|
|
112
|
+
child.stderr?.on("data", (data) => {
|
|
113
|
+
if (settled || failure)
|
|
114
|
+
return;
|
|
115
|
+
bytes += data.length;
|
|
116
|
+
if (bytes > request.maxBytes)
|
|
117
|
+
stop("shell_capture_too_large");
|
|
118
|
+
});
|
|
119
|
+
child.once("error", () => {
|
|
120
|
+
if (failure)
|
|
121
|
+
return;
|
|
122
|
+
finish("windows_environment_unavailable");
|
|
123
|
+
});
|
|
124
|
+
child.once("close", (code) => {
|
|
125
|
+
helperClosed = true;
|
|
126
|
+
if (failure)
|
|
127
|
+
finishCleanup();
|
|
128
|
+
else if (code === 0)
|
|
129
|
+
finish();
|
|
130
|
+
else
|
|
131
|
+
finish(code === 12 ? "shell_capture_too_large" : "windows_environment_unavailable");
|
|
132
|
+
});
|
|
133
|
+
if (request.signal?.aborted)
|
|
134
|
+
abort();
|
|
135
|
+
});
|
|
136
|
+
async function runScript(options, script) {
|
|
137
|
+
const deadline = Math.min(options.deadline ?? Date.now() + CAPTURE_TIMEOUT_MS, Date.now() + CAPTURE_TIMEOUT_MS);
|
|
138
|
+
if (options.signal?.aborted || deadline <= Date.now())
|
|
139
|
+
throw new MachineEnvironmentCaptureError("shell_capture_timeout");
|
|
140
|
+
const maxBytes = Math.min(options.maxBytes ?? MACHINE_ENVIRONMENT_MAX_BYTES, MACHINE_ENVIRONMENT_MAX_BYTES);
|
|
141
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
142
|
+
throw unavailable();
|
|
143
|
+
const file = systemExecutable(options.startupEnv, "WindowsPowerShell\\v1.0\\powershell.exe");
|
|
144
|
+
let output;
|
|
145
|
+
try {
|
|
146
|
+
output = await (options.runner ?? runWindowsEnvironmentProcess)({
|
|
147
|
+
file, args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")],
|
|
148
|
+
env: options.startupEnv, deadline, maxBytes, ...(options.signal ? { signal: options.signal } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof MachineEnvironmentCaptureError)
|
|
153
|
+
throw new MachineEnvironmentCaptureError(error.code);
|
|
154
|
+
throw unavailable();
|
|
155
|
+
}
|
|
156
|
+
if (output.length > maxBytes)
|
|
157
|
+
throw new MachineEnvironmentCaptureError("shell_capture_too_large");
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(output));
|
|
160
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
161
|
+
throw unavailable();
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw unavailable();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function captureWindowsEnvironment(options) {
|
|
169
|
+
if (!validSid(options.expectedSid))
|
|
170
|
+
throw unavailable();
|
|
171
|
+
const payload = await runScript(options, WINDOWS_ENVIRONMENT_CAPTURE_SCRIPT);
|
|
172
|
+
if (!validSid(payload.accountSid))
|
|
173
|
+
throw unavailable();
|
|
174
|
+
if (payload.accountSid !== options.expectedSid)
|
|
175
|
+
throw new MachineEnvironmentCaptureError("windows_environment_identity_mismatch");
|
|
176
|
+
if (!payload.env || typeof payload.env !== "object" || Array.isArray(payload.env) || !Array.isArray(payload.sourceOwnedKeys))
|
|
177
|
+
throw unavailable();
|
|
178
|
+
const entries = Object.entries(payload.env);
|
|
179
|
+
if (entries.length > MACHINE_ENVIRONMENT_MAX_ENTRIES || payload.sourceOwnedKeys.length > MACHINE_ENVIRONMENT_MAX_ENTRIES) {
|
|
180
|
+
throw new MachineEnvironmentCaptureError("shell_capture_too_large");
|
|
181
|
+
}
|
|
182
|
+
const env = Object.create(null);
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const [key, value] of entries) {
|
|
185
|
+
const folded = key.toUpperCase();
|
|
186
|
+
if (!validKey(key) || seen.has(folded) || typeof value !== "string" || value.includes("\0"))
|
|
187
|
+
throw unavailable();
|
|
188
|
+
seen.add(folded);
|
|
189
|
+
env[key] = value;
|
|
190
|
+
}
|
|
191
|
+
seen.clear();
|
|
192
|
+
const sourceOwnedKeys = [];
|
|
193
|
+
for (const key of payload.sourceOwnedKeys) {
|
|
194
|
+
if (typeof key !== "string" || !validKey(key) || seen.has(key.toUpperCase()))
|
|
195
|
+
throw unavailable();
|
|
196
|
+
seen.add(key.toUpperCase());
|
|
197
|
+
sourceOwnedKeys.push(key);
|
|
198
|
+
}
|
|
199
|
+
return Object.freeze({ env: Object.freeze(env), sourceOwnedKeys: Object.freeze(sourceOwnedKeys), accountSid: payload.accountSid });
|
|
200
|
+
}
|
|
201
|
+
export async function resolveWindowsDaemonSid(options) {
|
|
202
|
+
const payload = await runScript(options, WINDOWS_DAEMON_SID_SCRIPT);
|
|
203
|
+
if (!validSid(payload.accountSid))
|
|
204
|
+
throw unavailable();
|
|
205
|
+
return payload.accountSid;
|
|
206
|
+
}
|
|
207
|
+
export const captureWindowsMachineEnvironment = async (request) => {
|
|
208
|
+
const options = { startupEnv: request.startupEnv, signal: request.signal, deadline: request.deadline };
|
|
209
|
+
const expectedSid = request.account.sid ?? await resolveWindowsDaemonSid(options);
|
|
210
|
+
const result = await captureWindowsEnvironment({ ...options, expectedSid });
|
|
211
|
+
return {
|
|
212
|
+
...result,
|
|
213
|
+
sourceLocations: ["HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "HKCU\\Environment"],
|
|
214
|
+
};
|
|
215
|
+
};
|