@nowcrew/daemon 0.6.89 → 0.6.91-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -3
- 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/automatic-daemon-installer.js +49 -1
- 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/daemon-updater.js +2 -12
- 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 +41 -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 -178
- package/dist/storage-identity.js +0 -3
- 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 +13 -12
- 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 -241
- 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 -447
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/dist/runtime-probe.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { dslog } from "./slog.js";
|
|
1
3
|
import { executableRuntimes } from "./runtime-capabilities.js";
|
|
2
4
|
export function conservativeExecutionRuntimes(installed) {
|
|
3
5
|
return executableRuntimes(installed)
|
|
@@ -7,16 +9,43 @@ export function createRuntimeProbeCoordinator() {
|
|
|
7
9
|
const abort = new AbortController();
|
|
8
10
|
const flights = new Map();
|
|
9
11
|
return {
|
|
10
|
-
detect(installed, detector) {
|
|
11
|
-
const key = [...installed].sort()
|
|
12
|
+
detect(installed, detector, connectionId, environmentGeneration = "") {
|
|
13
|
+
const key = JSON.stringify([environmentGeneration, [...installed].sort()]);
|
|
12
14
|
const existing = flights.get(key);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const probeId = existing?.probeId ?? randomUUID();
|
|
16
|
+
const fields = { probe_id: probeId, connection_id: connectionId, installed_runtimes: JSON.stringify(installed) };
|
|
17
|
+
dslog("runtime_probe.request", "请求 runtime 执行能力探测", fields);
|
|
18
|
+
if (existing) {
|
|
19
|
+
dslog("runtime_probe.cache_hit", "复用 runtime 探测结果", { ...fields, cache_state: existing.state });
|
|
20
|
+
return existing.promise;
|
|
21
|
+
}
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
const complete = (outcome, executionRuntimes) => {
|
|
24
|
+
dslog("runtime_probe.complete", "runtime 探测聚合结束", {
|
|
25
|
+
...fields, outcome, duration_ms: Date.now() - startedAt, aborted: abort.signal.aborted,
|
|
26
|
+
...(executionRuntimes === undefined ? {} : { execution_runtimes: JSON.stringify(executionRuntimes) }),
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
let detection;
|
|
30
|
+
try {
|
|
31
|
+
detection = detector(installed, abort.signal, probeId);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
complete("threw");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const pending = detection.then((result) => {
|
|
38
|
+
const flight = flights.get(key);
|
|
39
|
+
if (flight)
|
|
40
|
+
flights.set(key, { ...flight, state: "resolved" });
|
|
41
|
+
complete("resolved", result);
|
|
42
|
+
return result;
|
|
43
|
+
}, (error) => {
|
|
16
44
|
flights.delete(key);
|
|
45
|
+
complete("threw");
|
|
17
46
|
throw error;
|
|
18
47
|
});
|
|
19
|
-
flights.set(key, pending);
|
|
48
|
+
flights.set(key, { promise: pending, probeId, state: "pending" });
|
|
20
49
|
return pending;
|
|
21
50
|
},
|
|
22
51
|
stop() {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Readable as NodeReadable } from "node:stream";
|
|
2
|
+
import { ReadableStream, WritableStream } from "node:stream/web";
|
|
3
|
+
/** Probe-only counters. Never retain/log chunks, JSON-RPC params, stderr or env values. */
|
|
4
|
+
export class AcpProbeTransport {
|
|
5
|
+
elapsed;
|
|
6
|
+
initializeRequestedMs;
|
|
7
|
+
initializeResponseMs;
|
|
8
|
+
firstStdinWriteMs;
|
|
9
|
+
firstStdoutMs;
|
|
10
|
+
stdinBytesWritten = 0;
|
|
11
|
+
stdoutBytes = 0;
|
|
12
|
+
stdoutEnded = false;
|
|
13
|
+
constructor(elapsed) {
|
|
14
|
+
this.elapsed = elapsed;
|
|
15
|
+
}
|
|
16
|
+
initializeRequested() { this.initializeRequestedMs = this.elapsed(); }
|
|
17
|
+
initializeResponse() { this.initializeResponseMs = this.elapsed(); }
|
|
18
|
+
fields() {
|
|
19
|
+
return {
|
|
20
|
+
initialize_requested_ms: this.initializeRequestedMs,
|
|
21
|
+
initialize_response_ms: this.initializeResponseMs,
|
|
22
|
+
first_stdin_write_ms: this.firstStdinWriteMs,
|
|
23
|
+
first_stdout_ms: this.firstStdoutMs,
|
|
24
|
+
stdin_bytes_written: this.stdinBytesWritten,
|
|
25
|
+
stdout_bytes: this.stdoutBytes,
|
|
26
|
+
stdout_ended: this.stdoutEnded,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A write callback confirms acceptance by the pipe, NOT receipt by the agent. */
|
|
30
|
+
writable(stdin) {
|
|
31
|
+
return new WritableStream({
|
|
32
|
+
start: (controller) => { stdin.once("error", (error) => controller.error(error)); },
|
|
33
|
+
write: (chunk) => new Promise((resolve, reject) => {
|
|
34
|
+
stdin.write(chunk, (error) => {
|
|
35
|
+
if (error) {
|
|
36
|
+
reject(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.firstStdinWriteMs ??= this.elapsed();
|
|
40
|
+
this.stdinBytesWritten += chunk.byteLength;
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
close: () => new Promise((resolve, reject) => {
|
|
45
|
+
stdin.end((error) => error ? reject(error) : resolve());
|
|
46
|
+
}),
|
|
47
|
+
abort: () => { stdin.destroy(); },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Observe before NDJSON decoding, without adding a competing 'data' listener. */
|
|
51
|
+
readable(stdout) {
|
|
52
|
+
const reader = NodeReadable.toWeb(stdout).getReader();
|
|
53
|
+
return new ReadableStream({
|
|
54
|
+
pull: async (controller) => {
|
|
55
|
+
const item = await reader.read();
|
|
56
|
+
if (item.done) {
|
|
57
|
+
this.stdoutEnded = true;
|
|
58
|
+
controller.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const chunk = item.value;
|
|
62
|
+
this.firstStdoutMs ??= this.elapsed();
|
|
63
|
+
this.stdoutBytes += chunk.byteLength;
|
|
64
|
+
controller.enqueue(chunk);
|
|
65
|
+
},
|
|
66
|
+
cancel: (reason) => reader.cancel(reason),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { isWin } from "../platform.js";
|
|
17
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
17
18
|
const execFileP = promisify(execFile);
|
|
18
19
|
const PROBE_TIMEOUT_MS = 8_000;
|
|
19
20
|
// 本机丢弃端口:连不上也无所谓,init 事件在任何网络请求之前就已输出。
|
|
@@ -26,18 +27,27 @@ const CLAUDE_ALIASES = [
|
|
|
26
27
|
];
|
|
27
28
|
const DEFAULT_ALIAS = "sonnet";
|
|
28
29
|
const cacheByVersion = new Map();
|
|
30
|
+
let cacheByEnvironment = new WeakMap();
|
|
29
31
|
/** "2.1.258 (Claude Code)" → "2.1.258";空/异常输出 → ""。 */
|
|
30
32
|
export function normalizeClaudeVersion(raw) {
|
|
31
33
|
return raw.replace(/\s*\(Claude Code\)\s*$/, "").trim();
|
|
32
34
|
}
|
|
33
|
-
export async function listClaudeModels(deps =
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
export async function listClaudeModels(deps, env, environmentGeneration = "") {
|
|
36
|
+
const probes = deps ?? { probeVersion: () => probeVersion(env), resolveAlias: (alias) => resolveAlias(alias, env) };
|
|
37
|
+
let cache = env ? cacheByEnvironment.get(env) : cacheByVersion;
|
|
38
|
+
if (!cache) {
|
|
39
|
+
cache = new Map();
|
|
40
|
+
if (env)
|
|
41
|
+
cacheByEnvironment.set(env, cache);
|
|
42
|
+
}
|
|
43
|
+
const version = normalizeClaudeVersion(await probes.probeVersion());
|
|
44
|
+
const cacheKey = JSON.stringify([environmentGeneration, version]);
|
|
45
|
+
const cached = version ? cache.get(cacheKey) : undefined;
|
|
36
46
|
if (cached)
|
|
37
47
|
return cached;
|
|
38
48
|
const resolved = await Promise.all(CLAUDE_ALIASES.map(async ([alias]) => {
|
|
39
49
|
try {
|
|
40
|
-
return await
|
|
50
|
+
return await probes.resolveAlias(alias);
|
|
41
51
|
}
|
|
42
52
|
catch {
|
|
43
53
|
return null;
|
|
@@ -53,22 +63,24 @@ export async function listClaudeModels(deps = defaultDeps) {
|
|
|
53
63
|
};
|
|
54
64
|
});
|
|
55
65
|
if (version && resolved.every((model) => model !== null))
|
|
56
|
-
|
|
66
|
+
cache.set(cacheKey, models);
|
|
57
67
|
return models;
|
|
58
68
|
}
|
|
59
69
|
/** 仅测试用:清空版本缓存。 */
|
|
60
70
|
export function resetClaudeModelCache() {
|
|
61
71
|
cacheByVersion.clear();
|
|
72
|
+
cacheByEnvironment = new WeakMap();
|
|
62
73
|
}
|
|
63
|
-
async function probeVersion() {
|
|
74
|
+
async function probeVersion(env = { ...process.env, PATH: augmentedPath() }) {
|
|
64
75
|
const { stdout } = await execFileP("claude", ["--version"], {
|
|
65
76
|
shell: isWin(),
|
|
66
77
|
timeout: PROBE_TIMEOUT_MS,
|
|
67
78
|
killSignal: "SIGKILL",
|
|
79
|
+
env,
|
|
68
80
|
});
|
|
69
81
|
return stdout;
|
|
70
82
|
}
|
|
71
|
-
function resolveAlias(alias) {
|
|
83
|
+
function resolveAlias(alias, env = { ...process.env, PATH: augmentedPath() }) {
|
|
72
84
|
return new Promise((resolve) => {
|
|
73
85
|
const child = spawn("claude", [
|
|
74
86
|
"--model", alias,
|
|
@@ -78,7 +90,7 @@ function resolveAlias(alias) {
|
|
|
78
90
|
], {
|
|
79
91
|
cwd: tmpdir(),
|
|
80
92
|
env: {
|
|
81
|
-
...
|
|
93
|
+
...env,
|
|
82
94
|
ANTHROPIC_BASE_URL: UNROUTABLE_API_BASE,
|
|
83
95
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
84
96
|
},
|
|
@@ -120,4 +132,3 @@ export function initModelOf(line) {
|
|
|
120
132
|
return null;
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
|
-
const defaultDeps = { probeVersion, resolveAlias };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launchd Background policy can stall dsh's synchronous startup reads before ACP.
|
|
3
|
+
* Apply application resource policy to this short-lived child, never the daemon.
|
|
4
|
+
* taskpolicy execs the target; no shell, alternate PATH, or protocol bypass.
|
|
5
|
+
*/
|
|
6
|
+
export function deepSeekHarnessLaunch(bin, args, platform = process.platform) {
|
|
7
|
+
return platform === "darwin"
|
|
8
|
+
? { bin: "/usr/sbin/taskpolicy", args: ["-a", "--", bin, ...args] }
|
|
9
|
+
: { bin, args: [...args] };
|
|
10
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** 短命执行配置:不读取全局 Harness home,不新增 daemon 状态。 */
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, access } from "node:fs/promises";
|
|
3
|
+
import { join, isAbsolute } from "node:path";
|
|
4
|
+
import { stringify } from "yaml";
|
|
5
|
+
export const harnessPermissionMode = (permission) => permission === "sandboxed"
|
|
6
|
+
? "read-only"
|
|
7
|
+
: permission === "workspace_write"
|
|
8
|
+
? "workspace-write"
|
|
9
|
+
: "danger-full-access";
|
|
10
|
+
// A variable's returned value is interpolated once: literal {{...}} in NowWork
|
|
11
|
+
// instructions must not become unknown Harness template variables.
|
|
12
|
+
const instructionsPlugin = `export const name = 'nowwork-instructions';
|
|
13
|
+
export const inject = ['systemPrompt'];
|
|
14
|
+
export function apply(ctx, config) {
|
|
15
|
+
ctx.systemPrompt.variable('nowwork_instructions', () => config.text);
|
|
16
|
+
ctx.systemPrompt.section({name: 'nowwork-instructions', order: 100000, text: '{{nowwork_instructions}}'});
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
export async function prepareHarnessProfile(input) {
|
|
20
|
+
if (![input.stateRoot, input.cwd, ...input.skillRoots].every(isAbsolute)) {
|
|
21
|
+
throw new Error("DeepSeek Harness execution paths must be absolute");
|
|
22
|
+
}
|
|
23
|
+
if (input.patchPath !== undefined) {
|
|
24
|
+
if (!isAbsolute(input.patchPath))
|
|
25
|
+
throw new Error("DeepSeek Harness patch must be absolute");
|
|
26
|
+
await access(input.patchPath);
|
|
27
|
+
}
|
|
28
|
+
await mkdir(input.stateRoot, { recursive: true, mode: 0o700 });
|
|
29
|
+
const home = await mkdtemp(join(input.stateRoot, ".deepseek-harness-"));
|
|
30
|
+
const pluginPath = join(home, "nowwork-instructions.mjs");
|
|
31
|
+
const policyPath = join(home, "nowwork-policy.yml");
|
|
32
|
+
const mode = harnessPermissionMode(input.effectivePermission);
|
|
33
|
+
await writeFile(pluginPath, instructionsPlugin, { mode: 0o600 });
|
|
34
|
+
await writeFile(policyPath, stringify([
|
|
35
|
+
{
|
|
36
|
+
id: "sandbox-policy",
|
|
37
|
+
disabled: false,
|
|
38
|
+
config: { mode, workspaceRoot: input.cwd },
|
|
39
|
+
},
|
|
40
|
+
{ id: "approval", disabled: false, config: { policy: "never" } },
|
|
41
|
+
{
|
|
42
|
+
id: "permission",
|
|
43
|
+
disabled: false,
|
|
44
|
+
config: {
|
|
45
|
+
defaultPreset: "nowwork",
|
|
46
|
+
presets: { nowwork: { sandbox: mode, approval: "never" } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "skill-filesystem",
|
|
51
|
+
disabled: false,
|
|
52
|
+
config: {
|
|
53
|
+
includeDefaultRoots: false,
|
|
54
|
+
customSkillDirs: [...input.skillRoots],
|
|
55
|
+
watch: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "session-persistence-jsonl",
|
|
60
|
+
disabled: false,
|
|
61
|
+
config: { root: join(home, "sessions") },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
insert: [
|
|
65
|
+
{
|
|
66
|
+
id: "nowwork-instructions",
|
|
67
|
+
name: pluginPath,
|
|
68
|
+
config: { text: input.systemPrompt },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]), { mode: 0o600 });
|
|
73
|
+
const env = Object.fromEntries(Object.entries(input.env).filter(([key]) => !key.toUpperCase().startsWith("DSH_")));
|
|
74
|
+
return {
|
|
75
|
+
home,
|
|
76
|
+
policyPath,
|
|
77
|
+
args: [
|
|
78
|
+
"--profile",
|
|
79
|
+
"acp",
|
|
80
|
+
...(input.patchPath ? ["--patch", input.patchPath] : []),
|
|
81
|
+
"--patch",
|
|
82
|
+
policyPath,
|
|
83
|
+
],
|
|
84
|
+
env: {
|
|
85
|
+
...env,
|
|
86
|
+
DSH_HOME: home,
|
|
87
|
+
DSH_AGENTS_HOME: join(home, "agents-home"),
|
|
88
|
+
DSH_PERMISSION_MODE: mode,
|
|
89
|
+
DSH_TELEMETRY_DISABLED: "1",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** 新版 Harness 的短命 ACP 桥:产品状态与重试仍属于 server。 */
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { Readable, Writable } from "node:stream";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { isAbsolute } from "node:path";
|
|
7
|
+
import spawn from "cross-spawn";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { client, methods, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
10
|
+
import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
|
|
11
|
+
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
12
|
+
import { mapAcpUpdate } from "./kimi-acp-runner.js";
|
|
13
|
+
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
14
|
+
const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
|
|
15
|
+
export const HarnessInputSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
systemPrompt: z.string(),
|
|
18
|
+
wakePrompt: z.string().min(1),
|
|
19
|
+
stateRoot: absolutePath,
|
|
20
|
+
skillRoots: z.array(absolutePath).default([]),
|
|
21
|
+
effectivePermission: z.enum([
|
|
22
|
+
"sandboxed",
|
|
23
|
+
"workspace_write",
|
|
24
|
+
"full_access",
|
|
25
|
+
]),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
const emit = async (event) => {
|
|
29
|
+
if (!process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
30
|
+
await once(process.stdout, "drain");
|
|
31
|
+
};
|
|
32
|
+
async function settleWithin(promise, ms) {
|
|
33
|
+
let timer;
|
|
34
|
+
try {
|
|
35
|
+
return await Promise.race([
|
|
36
|
+
promise,
|
|
37
|
+
new Promise((resolve) => {
|
|
38
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (timer)
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runHarness(input, bin, patchPath) {
|
|
48
|
+
const delivery = await prepareThreadDelivery({
|
|
49
|
+
env: process.env,
|
|
50
|
+
resumed: false,
|
|
51
|
+
sessionId: "cold-harness",
|
|
52
|
+
wakePrompt: input.wakePrompt,
|
|
53
|
+
});
|
|
54
|
+
const profile = await prepareHarnessProfile({
|
|
55
|
+
...input,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
env: process.env,
|
|
58
|
+
...(patchPath ? { patchPath } : {}),
|
|
59
|
+
});
|
|
60
|
+
const launch = deepSeekHarnessLaunch(bin, profile.args);
|
|
61
|
+
const child = spawn(launch.bin, launch.args, {
|
|
62
|
+
cwd: profile.home,
|
|
63
|
+
env: profile.env,
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
+
});
|
|
66
|
+
// Boot away from the repository: the CLI must not load a project's .env into
|
|
67
|
+
// trusted process credentials. ACP session/new carries the real workspace.
|
|
68
|
+
let spawnError;
|
|
69
|
+
const closed = new Promise((resolve) => {
|
|
70
|
+
child.once("error", (error) => {
|
|
71
|
+
spawnError = error;
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
child.once("close", () => resolve());
|
|
75
|
+
});
|
|
76
|
+
if (!child.stdin || !child.stdout || !child.stderr)
|
|
77
|
+
throw new Error("DeepSeek Harness stdio unavailable");
|
|
78
|
+
// Provider diagnostics may include secrets or prompts; do not stream raw stderr.
|
|
79
|
+
child.stderr.resume();
|
|
80
|
+
let context;
|
|
81
|
+
let sessionId;
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
let stage = "initialize";
|
|
84
|
+
let exitCode = 1;
|
|
85
|
+
let stopping;
|
|
86
|
+
const stop = () => (stopping ??= (async () => {
|
|
87
|
+
child.stdin?.end();
|
|
88
|
+
if (await settleWithin(closed.then(() => true), 2000))
|
|
89
|
+
return;
|
|
90
|
+
child.kill("SIGTERM");
|
|
91
|
+
if (await settleWithin(closed.then(() => true), 1000))
|
|
92
|
+
return;
|
|
93
|
+
child.kill("SIGKILL");
|
|
94
|
+
if (!(await settleWithin(closed.then(() => true), 2000)))
|
|
95
|
+
throw new Error("DeepSeek Harness process stop unconfirmed");
|
|
96
|
+
})());
|
|
97
|
+
const cancel = () => {
|
|
98
|
+
cancelled = true;
|
|
99
|
+
void (async () => {
|
|
100
|
+
if (context && sessionId)
|
|
101
|
+
await settleWithin(context
|
|
102
|
+
.notify(methods.agent.session.cancel, { sessionId })
|
|
103
|
+
.catch(() => undefined), 1000);
|
|
104
|
+
await stop();
|
|
105
|
+
})().catch(() => {
|
|
106
|
+
/* finally reports stop failure; supervisor owns process tree */
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
process.once("SIGTERM", cancel);
|
|
110
|
+
process.once("SIGINT", cancel);
|
|
111
|
+
const app = client({ name: "nowwork-harness" })
|
|
112
|
+
.onRequest(methods.client.session.requestPermission, () => ({
|
|
113
|
+
outcome: { outcome: "cancelled" },
|
|
114
|
+
}))
|
|
115
|
+
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
116
|
+
if (params.sessionId !== sessionId)
|
|
117
|
+
return;
|
|
118
|
+
for (const event of mapAcpUpdate("deepseek-harness", params.update))
|
|
119
|
+
await emit(event);
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
123
|
+
const result = await app.connectWith(stream, async (ctx) => {
|
|
124
|
+
context = ctx;
|
|
125
|
+
const initialized = await ctx.request(methods.agent.initialize, {
|
|
126
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
127
|
+
clientCapabilities: {},
|
|
128
|
+
clientInfo: { name: "nowwork", version: "1" },
|
|
129
|
+
});
|
|
130
|
+
if (initialized.protocolVersion !== PROTOCOL_VERSION)
|
|
131
|
+
throw new Error("Incompatible ACP protocol");
|
|
132
|
+
if (cancelled)
|
|
133
|
+
throw new Error("cancelled");
|
|
134
|
+
stage = "session_new";
|
|
135
|
+
const session = await ctx.request(methods.agent.session.new, {
|
|
136
|
+
cwd: process.cwd(),
|
|
137
|
+
mcpServers: [],
|
|
138
|
+
});
|
|
139
|
+
sessionId = session.sessionId;
|
|
140
|
+
if (!sessionId || cancelled)
|
|
141
|
+
throw new Error("Harness session unavailable");
|
|
142
|
+
await emit({ type: "thread.started", thread_id: sessionId });
|
|
143
|
+
stage = "prompt";
|
|
144
|
+
const response = await ctx.request(methods.agent.session.prompt, {
|
|
145
|
+
sessionId,
|
|
146
|
+
prompt: [{ type: "text", text: delivery.prompt }],
|
|
147
|
+
});
|
|
148
|
+
if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
|
|
149
|
+
stage = "session_close";
|
|
150
|
+
const close = await settleWithin(ctx
|
|
151
|
+
.request(methods.agent.session.close, { sessionId })
|
|
152
|
+
.then(() => true), 5000);
|
|
153
|
+
if (!close)
|
|
154
|
+
throw new Error("Harness session close timed out");
|
|
155
|
+
}
|
|
156
|
+
return response;
|
|
157
|
+
});
|
|
158
|
+
const usage = result.usage;
|
|
159
|
+
if (!cancelled)
|
|
160
|
+
await emit({
|
|
161
|
+
type: "turn.completed",
|
|
162
|
+
stop_reason: result.stopReason,
|
|
163
|
+
...(usage
|
|
164
|
+
? {
|
|
165
|
+
usage: {
|
|
166
|
+
input_tokens: usage.inputTokens,
|
|
167
|
+
output_tokens: usage.outputTokens,
|
|
168
|
+
cache_read_input_tokens: usage.cachedReadTokens ?? 0,
|
|
169
|
+
cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
});
|
|
174
|
+
exitCode =
|
|
175
|
+
result.stopReason === "end_turn"
|
|
176
|
+
? 0
|
|
177
|
+
: result.stopReason === "cancelled"
|
|
178
|
+
? 130
|
|
179
|
+
: 1;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
if (!cancelled)
|
|
183
|
+
process.stderr.write(spawnError
|
|
184
|
+
? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
|
|
185
|
+
: `DeepSeek Harness ACP execution failed at ${stage}; verify DEEPSEEK_API_KEY, model access, the pinned version and profile patch.\n`);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
try {
|
|
189
|
+
await stop();
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
process.off("SIGTERM", cancel);
|
|
193
|
+
process.off("SIGINT", cancel);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (cancelled)
|
|
197
|
+
return 130;
|
|
198
|
+
return child.exitCode !== 0 ? 1 : exitCode;
|
|
199
|
+
}
|
|
200
|
+
async function main() {
|
|
201
|
+
const { values } = parseArgs({
|
|
202
|
+
options: { bin: { type: "string" }, patch: { type: "string" } },
|
|
203
|
+
});
|
|
204
|
+
if (!values.bin)
|
|
205
|
+
throw new Error("DeepSeek Harness --bin is required");
|
|
206
|
+
process.stdin.setEncoding("utf8");
|
|
207
|
+
let text = "";
|
|
208
|
+
for await (const chunk of process.stdin) {
|
|
209
|
+
text += String(chunk);
|
|
210
|
+
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
211
|
+
throw new Error("Harness input exceeds 16 MiB");
|
|
212
|
+
}
|
|
213
|
+
const input = HarnessInputSchema.parse(JSON.parse(text));
|
|
214
|
+
return runHarness(input, values.bin, values.patch);
|
|
215
|
+
}
|
|
216
|
+
if (process.argv[1] &&
|
|
217
|
+
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
218
|
+
main()
|
|
219
|
+
.then((code) => {
|
|
220
|
+
process.exitCode = code;
|
|
221
|
+
})
|
|
222
|
+
.catch(() => {
|
|
223
|
+
process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -1,20 +1,81 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
-
import { isAbsolute } from "node:path";
|
|
3
|
-
import {
|
|
2
|
+
import { isAbsolute, win32, join } from "node:path";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
|
|
6
|
+
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
7
|
+
import { probeKimiAcp, AcpProbeCleanupError } from "./kimi-acp-runner.js";
|
|
8
|
+
import { runtimeEnvironmentValue } from "../runtime-environment-merge.js";
|
|
9
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
4
10
|
export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
|
|
12
|
+
export function deepSeekHarnessLauncher(env = process.env, platform = process.platform) {
|
|
13
|
+
const configPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
|
|
14
|
+
const patchPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_PATCH_ENV, platform)?.trim();
|
|
15
|
+
const absolute = platform === "win32" ? win32.isAbsolute : isAbsolute;
|
|
16
|
+
if (configPath && !absolute(configPath))
|
|
17
|
+
throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
|
|
18
|
+
if (patchPath && !absolute(patchPath))
|
|
19
|
+
throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
|
|
20
|
+
if (configPath && patchPath)
|
|
21
|
+
throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
|
|
22
|
+
return configPath
|
|
23
|
+
? { kind: "legacy", bin: "dsh-acp-demo", configPath }
|
|
24
|
+
: { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
|
|
25
|
+
}
|
|
26
|
+
export function deepSeekHarnessConfigPath(env = process.env, platform = process.platform) {
|
|
27
|
+
const path = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
|
|
28
|
+
return path && (platform === "win32" ? win32.isAbsolute(path) : isAbsolute(path)) ? path : null;
|
|
8
29
|
}
|
|
9
30
|
/** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
|
|
10
|
-
export function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
32
|
+
if (options.signal?.aborted)
|
|
33
|
+
return false;
|
|
34
|
+
const sourceEnv = options.env ?? { ...process.env, PATH: augmentedPath() };
|
|
35
|
+
let root;
|
|
36
|
+
try {
|
|
37
|
+
const launcher = options.configPath === undefined
|
|
38
|
+
? deepSeekHarnessLauncher(sourceEnv)
|
|
39
|
+
: deepSeekHarnessLauncher({
|
|
40
|
+
...sourceEnv,
|
|
41
|
+
[DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
|
|
42
|
+
});
|
|
43
|
+
if (launcher.kind === "legacy")
|
|
44
|
+
return await probeKimiAcp({
|
|
45
|
+
...options,
|
|
46
|
+
env: sourceEnv,
|
|
47
|
+
provider: "deepseek-harness",
|
|
48
|
+
configPath: launcher.configPath,
|
|
49
|
+
effectivePermission: "sandboxed",
|
|
50
|
+
}, spawnProcess);
|
|
51
|
+
root = await mkdtemp(join(tmpdir(), "nowwork-dsh-probe-"));
|
|
52
|
+
const profile = await prepareHarnessProfile({
|
|
53
|
+
stateRoot: root,
|
|
54
|
+
cwd: root,
|
|
55
|
+
systemPrompt: "",
|
|
56
|
+
skillRoots: [],
|
|
57
|
+
effectivePermission: "sandboxed",
|
|
58
|
+
env: sourceEnv,
|
|
59
|
+
...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
|
|
60
|
+
});
|
|
61
|
+
const launch = deepSeekHarnessLaunch(options.bin, profile.args);
|
|
62
|
+
return await probeKimiAcp({
|
|
63
|
+
...options,
|
|
64
|
+
bin: launch.bin,
|
|
65
|
+
provider: "deepseek-harness",
|
|
66
|
+
effectivePermission: "sandboxed",
|
|
67
|
+
launchArgs: launch.args,
|
|
68
|
+
launchEnv: profile.env,
|
|
69
|
+
launchCwd: root,
|
|
70
|
+
}, spawnProcess);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof AcpProbeCleanupError)
|
|
74
|
+
throw error;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (root)
|
|
79
|
+
await rm(root, { recursive: true, force: true }).catch(() => undefined);
|
|
80
|
+
}
|
|
20
81
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dslog } from "../slog.js";
|
|
2
|
+
const RETRY_DELAYS_MS = [1_000, 2_000];
|
|
3
|
+
/** 只延迟当前启动探测,不设后台定时任务;stop 立即清除等待。 */
|
|
4
|
+
function waitForRetry(ms, signal) {
|
|
5
|
+
if (signal?.aborted)
|
|
6
|
+
return Promise.resolve(false);
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const finish = (ready) => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11
|
+
resolve(ready);
|
|
12
|
+
};
|
|
13
|
+
const onAbort = () => finish(false);
|
|
14
|
+
const timer = setTimeout(() => finish(true), ms);
|
|
15
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** 成功才公布能力;每次 probe 返回前已完成进程清理,异常保持原有传播。 */
|
|
19
|
+
export async function retryHarnessProbe(probe, signal, probeId) {
|
|
20
|
+
for (let attempt = 1; attempt <= RETRY_DELAYS_MS.length + 1; attempt++) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
return false;
|
|
23
|
+
const ready = await probe();
|
|
24
|
+
dslog("runtime_probe.harness_attempt", "Harness 启动探测尝试结束", {
|
|
25
|
+
probe_id: probeId, runtime: "deepseek-harness", attempt, ready,
|
|
26
|
+
aborted: signal?.aborted ?? false,
|
|
27
|
+
});
|
|
28
|
+
if (signal?.aborted)
|
|
29
|
+
return false;
|
|
30
|
+
if (ready)
|
|
31
|
+
return true;
|
|
32
|
+
const delayMs = RETRY_DELAYS_MS[attempt - 1];
|
|
33
|
+
if (delayMs === undefined)
|
|
34
|
+
return false;
|
|
35
|
+
dslog("runtime_probe.harness_retry", "延迟重试 Harness 启动探测", {
|
|
36
|
+
probe_id: probeId, runtime: "deepseek-harness", next_attempt: attempt + 1, delay_ms: delayMs,
|
|
37
|
+
});
|
|
38
|
+
if (!await waitForRetry(delayMs, signal))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|