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