@nowcrew/daemon 0.5.50 → 0.5.52

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 CHANGED
@@ -222,7 +222,7 @@ the global compatible-machine pool.
222
222
 
223
223
  The daemon intersects requested permission with local policy, checks advertised resource limits, prepares
224
224
  the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, `kimi`,
225
- `hermes`, or `opencode`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
225
+ `hermes`, `opencode`, or `deepseek-harness`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
226
226
  behavior. Those are server responsibilities.
227
227
 
228
228
  The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
@@ -234,6 +234,7 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
234
234
  | Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
235
235
  | Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
236
236
  | OpenCode | `run --format json` | stdin | `--session` |
237
+ | DeepSeek Harness | ACP over stdio | `session/prompt` | none (fresh session per execution) |
237
238
 
238
239
  Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
239
240
  the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
@@ -254,16 +255,40 @@ only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissi
254
255
  `--model`, `--variant`, and `--session`; older installations remain visible as detected CLIs until
255
256
  they are upgraded, rather than falling back to a permission override that user config could weaken.
256
257
 
258
+ Install the pinned DeepSeek Harness ACP server on the daemon machine:
259
+
260
+ ```bash
261
+ npm install --global @deepseek-ai/dsh-acp-demo@0.0.1-rc.1
262
+ command -v dsh-acp-demo
263
+ ```
264
+
265
+ The executable is only the ACP app loader; it does not embed a runnable `cordis.yml`. Install or build
266
+ the plugins referenced by your DeepSeek Harness composition, then set both `DEEPSEEK_API_KEY` and an
267
+ absolute `NOWCREW_DEEPSEEK_HARNESS_CONFIG=/path/to/cordis.yml` in the daemon service environment. The
268
+ official repository's `examples/acp-agent/cordis.yml` is the reference composition. A missing or relative
269
+ config path keeps the runtime out of `executionRuntimes`.
270
+
271
+ `dsh-acp-demo --config <absolute-path>` is launched directly with ACP JSON-RPC on stdio; do not append
272
+ an `acp` subcommand and do not substitute `dsh --profile headless`.
273
+ Provider credentials and model selection remain machine-local. The daemon sets `DSH_PERMISSION_MODE`
274
+ from the effective NowWork permission: `sandboxed` -> `read-only`, `workspace_write` ->
275
+ `workspace-write`, and `full_access` -> `danger-full-access`. Outside full access, an ACP permission
276
+ request is rejected once when that option is unambiguous, otherwise it is cancelled. The current adapter
277
+ uses fresh sessions and committed text; it does not claim resume, images, MCP, reasoning, tool-progress,
278
+ or transcript support. Agent environment variables cannot override `DEEPSEEK_*`, `DSH_*`, or
279
+ `NOWCREW_DEEPSEEK_HARNESS_CONFIG`; those values always come from the daemon service environment.
280
+
257
281
  Codex, Kimi, Hermes, and OpenCode sessions use the same bounded per-task persistence and keyed lease as Claude when they
258
282
  run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
259
283
  semantically silent; after the first semantic event, the execution's configured total timeout remains
260
284
  authoritative so a legitimate long-running tool is not killed for quiet output.
285
+ DeepSeek Harness is execution-v1-only and intentionally skips native session persistence.
261
286
 
262
287
  Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
263
288
  found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
264
289
  adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
265
290
  The daemon sends a conservative first hello without waiting for third-party handshakes, then refreshes it
266
- after the optional Kimi/Hermes/OpenCode probes finish. Work targeting those optional runtimes waits for the
291
+ after the optional Kimi/Hermes/OpenCode/DeepSeek Harness probes finish. Work targeting those optional runtimes waits for the
267
292
  same full probe result; reconnects share one in-flight probe, and shutdown aborts any unfinished probe and
268
293
  its child process.
269
294
  Old daemons that omit `executionRuntimes` are conservatively interpreted as the intersection of installed
package/dist/console.js CHANGED
@@ -254,13 +254,16 @@ export function toConsoleLines(event) {
254
254
  const e = (event ?? {});
255
255
  const lang = detectDaemonLang();
256
256
  const td = (message) => translateDaemon(lang, message);
257
- if (e.type === "kimi.acp.text_delta" && e.text) {
257
+ if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
258
+ || e.type === "deepseek-harness.acp.text_delta") && e.text) {
258
259
  return [{ stream: "text", text: e.text }];
259
260
  }
260
- if (e.type === "kimi.acp.tool_call") {
261
- return [{ stream: "tool", text: e.title ? `⏺ ${e.title}` : " Kimi tool" }];
261
+ if (e.type === "kimi.acp.tool_call" || e.type === "hermes.acp.tool_call"
262
+ || e.type === "deepseek-harness.acp.tool_call") {
263
+ return [{ stream: "tool", text: e.title ? `⏺ ${e.title}` : "⏺ ACP tool" }];
262
264
  }
263
- if (e.type === "kimi.acp.tool_result") {
265
+ if (e.type === "kimi.acp.tool_result" || e.type === "hermes.acp.tool_result"
266
+ || e.type === "deepseek-harness.acp.tool_result") {
264
267
  const text = typeof e.content === "string" ? e.content.trim() : "";
265
268
  return text ? [{ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
266
269
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -7,7 +7,7 @@ import { JournalLockedError, createJournalLease, } from "./execution-journal-loc
7
7
  export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
8
8
  const ExecutionIdSchema = z.string().uuid();
9
9
  const TimestampSchema = z.string().datetime({ offset: true });
10
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
10
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"]);
11
11
  const RUNTIME_READY_SUFFIX = ".runtime-ready";
12
12
  const RawJournalEntrySchema = z.object({
13
13
  executionId: ExecutionIdSchema,
@@ -8,7 +8,7 @@ const MIN_SIGNED_32_INTEGER = -2_147_483_648;
8
8
  const SequenceSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
9
9
  const TokenCountSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
10
10
  const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_INTEGER);
11
- const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"]);
12
12
  const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
13
13
  const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
14
14
  export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
@@ -172,6 +172,8 @@ function validReasoning(spec) {
172
172
  if (spec.runtime.name === "kimi") {
173
173
  return KIMI_EFFORT_LEVELS.includes(reasoning);
174
174
  }
175
+ if (spec.runtime.name === "deepseek-harness")
176
+ return false;
175
177
  return true;
176
178
  }
177
179
  function launchProviderConfig(config) {
@@ -63,6 +63,7 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
63
63
  : [];
64
64
  }
65
65
  if ((runtime === "hermes" && candidate.type === "hermes.acp.text_delta")
66
+ || (runtime === "deepseek-harness" && candidate.type === "deepseek-harness.acp.text_delta")
66
67
  || (runtime === "opencode" && candidate.type === "opencode.text_delta")) {
67
68
  return typeof candidate.text === "string" ? decoder.push(candidate.text) : [];
68
69
  }
@@ -6,7 +6,7 @@ import { spawnClaude } from "./runtimes/claude.js";
6
6
  import { spawnCodex } from "./runtimes/codex.js";
7
7
  import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
8
8
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
9
- import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
9
+ import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
10
10
  import { augmentedPath } from "./runtime-path.js";
11
11
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
12
12
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
@@ -115,14 +115,15 @@ export function awaitExit(child) {
115
115
  }
116
116
  export function exitActivity(runtime, exitCode, stderrTail) {
117
117
  if ((runtime === "codex" || runtime === "kimi" || runtime === "hermes"
118
- || runtime === "opencode" || exitCode === -1) && exitCode !== 0) {
118
+ || runtime === "opencode" || runtime === "deepseek-harness" || exitCode === -1) && exitCode !== 0) {
119
119
  return {
120
120
  kind: "error",
121
121
  label: "运行出错",
122
122
  detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
123
123
  };
124
124
  }
125
- if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode") && exitCode === 0) {
125
+ if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode"
126
+ || runtime === "deepseek-harness") && exitCode === 0) {
126
127
  return { kind: "done", label: "本轮结束" };
127
128
  }
128
129
  return null;
@@ -136,6 +137,9 @@ function wrapChild(child) {
136
137
  };
137
138
  }
138
139
  async function launchLegacyRuntime(request) {
140
+ if (request.runtime === "deepseek-harness") {
141
+ throw new Error("DeepSeek Harness requires execution protocol v1");
142
+ }
139
143
  const common = {
140
144
  bin: request.bin,
141
145
  cwd: request.cwd,
@@ -463,7 +467,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
463
467
  ? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
464
468
  : {}),
465
469
  };
466
- const childEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir);
470
+ const providerEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir);
471
+ const childEnv = runtime.name === "deepseek-harness"
472
+ ? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
473
+ : providerEnv;
467
474
  const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
468
475
  if (dependencies.cancellation?.isRequested())
469
476
  throw new RuntimeCancelledError();
@@ -492,7 +499,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
492
499
  memoryPruneFailurePhase = "runtime_launch";
493
500
  const launchRequest = {
494
501
  runtime: runtime.name,
495
- bin: runtime.name,
502
+ bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
496
503
  cwd: executionWorkspace.runDir,
497
504
  ...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
498
505
  systemPromptPath: workspace.systemPromptPath,
@@ -572,7 +579,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
572
579
  if (extracted) {
573
580
  const incremental = typeof event === "object" && event !== null
574
581
  && "type" in event && (event.type === "kimi.acp.text_delta"
575
- || event.type === "hermes.acp.text_delta" || event.type === "opencode.text_delta");
582
+ || event.type === "hermes.acp.text_delta"
583
+ || event.type === "deepseek-harness.acp.text_delta"
584
+ || event.type === "opencode.text_delta");
576
585
  finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
577
586
  }
578
587
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
@@ -17,6 +17,8 @@ import { executionBackendCapability } from "./execution-backend.js";
17
17
  import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
18
18
  import { probeHermesAcp } from "./runtimes/hermes.js";
19
19
  import { probeOpenCodeRun } from "./runtimes/opencode.js";
20
+ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
21
+ import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
20
22
  export { executableRuntimes } from "./runtime-capabilities.js";
21
23
  const execFileP = promisify(execFile);
22
24
  export const DAEMON_CAPABILITIES = [
@@ -30,6 +32,7 @@ export const DAEMON_CAPABILITIES = [
30
32
  "execution_machine_queue_v1",
31
33
  "execution_agent_memory_policy_v1",
32
34
  "project_skills_v1",
35
+ RUNTIME_HEALTH_PROBE_CAPABILITY,
33
36
  ];
34
37
  export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
35
38
  ? DAEMON_CAPABILITIES
@@ -43,6 +46,7 @@ const RUNTIME_BINS = [
43
46
  ["gemini", "gemini"],
44
47
  ["hermes", "hermes"],
45
48
  ["opencode", "opencode"],
49
+ ["deepseek-harness", "dsh-acp-demo"],
46
50
  ["copilot", "copilot"],
47
51
  ["kimi", "kimi"],
48
52
  ["pi", "pi"],
@@ -71,20 +75,25 @@ async function supportsHermesAcp(signal) {
71
75
  async function supportsOpenCodeRun(signal) {
72
76
  return probeOpenCodeRun(signal);
73
77
  }
78
+ async function supportsDeepSeekHarnessAcp(signal) {
79
+ return probeDeepSeekHarnessAcp({ bin: "dsh-acp-demo", ...(signal ? { signal } : {}) });
80
+ }
74
81
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
75
- export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, signal) {
82
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal) {
76
83
  const present = await installed;
77
- const [kimiReady, hermesReady, openCodeReady] = await Promise.all([
84
+ const [kimiReady, hermesReady, openCodeReady, deepSeekHarnessReady] = await Promise.all([
78
85
  present.includes("kimi") ? kimiAcpProbe(signal) : false,
79
86
  present.includes("hermes") ? hermesAcpProbe(signal) : false,
80
87
  present.includes("opencode") ? openCodeProbe(signal) : false,
88
+ present.includes("deepseek-harness") ? deepSeekHarnessProbe(signal) : false,
81
89
  ]);
82
90
  return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
83
91
  : runtime === "hermes" ? hermesReady
84
92
  : runtime === "opencode" ? openCodeReady
85
- : true));
93
+ : runtime === "deepseek-harness" ? deepSeekHarnessReady
94
+ : true));
86
95
  }
87
- export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, signal);
96
+ export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, supportsDeepSeekHarnessAcp, signal);
88
97
  export function daemonVersion() {
89
98
  try {
90
99
  const here = dirname(fileURLToPath(import.meta.url));
package/dist/normalize.js CHANGED
@@ -35,6 +35,7 @@ export function classifyCommand(command) {
35
35
  export function extractFinalText(event) {
36
36
  const e = (event ?? {});
37
37
  if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
38
+ || e.type === "deepseek-harness.acp.text_delta"
38
39
  || e.type === "opencode.text_delta") && e.text)
39
40
  return e.text;
40
41
  if (e.type === "result" && !e.is_error && e.result?.trim())
@@ -63,15 +64,16 @@ function parseKimiBashCommand(args) {
63
64
  export function normalizeEvent(event) {
64
65
  const e = (event ?? {});
65
66
  if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
67
+ || e.type === "deepseek-harness.acp.text_delta"
66
68
  || e.type === "opencode.text_delta") && e.text?.trim()) {
67
69
  return [{ kind: "text", label: "思考/说明", detail: e.text }];
68
70
  }
69
71
  if (e.type === "kimi.acp.tool_call" || e.type === "hermes.acp.tool_call"
70
- || e.type === "opencode.tool_call") {
72
+ || e.type === "deepseek-harness.acp.tool_call" || e.type === "opencode.tool_call") {
71
73
  return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
72
74
  }
73
75
  if (e.type === "kimi.acp.tool_result" || e.type === "hermes.acp.tool_result"
74
- || e.type === "opencode.tool_result") {
76
+ || e.type === "deepseek-harness.acp.tool_result" || e.type === "opencode.tool_result") {
75
77
  return [{ kind: "tool_result", label: "工具返回" }];
76
78
  }
77
79
  if (e.type === "system" && e.subtype === "init") {
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { join } from "node:path";
12
12
  import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
13
+ import { DEEPSEEK_HARNESS_CONFIG_ENV } from "./runtimes/deepseek-harness.js";
13
14
  /** custom 模式下必须从继承 env 中剔除的全局路由/凭证变量(避免与注入值叠加或抢优先级)。 */
14
15
  const CONFLICTING_ENV = [
15
16
  "ANTHROPIC_BASE_URL",
@@ -72,6 +73,22 @@ export function applyProviderEnv(base, runtime, cfg, homeDir) {
72
73
  }
73
74
  return env;
74
75
  }
76
+ const deepSeekHarnessMachineOwnedKey = (key) => (key.startsWith("DEEPSEEK_")
77
+ || key === DEEPSEEK_HARNESS_CONFIG_ENV
78
+ || key.startsWith("DSH_"));
79
+ /** Agent env remains available to tools, but cannot replace Harness process policy or credentials. */
80
+ export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
81
+ const env = { ...base };
82
+ for (const key of Object.keys(env)) {
83
+ if (deepSeekHarnessMachineOwnedKey(key))
84
+ delete env[key];
85
+ }
86
+ for (const [key, value] of Object.entries(machineEnv)) {
87
+ if (deepSeekHarnessMachineOwnedKey(key) && value !== undefined)
88
+ env[key] = value;
89
+ }
90
+ return env;
91
+ }
75
92
  /**
76
93
  * provider 关键配置指纹:Claude Custom 路由和 Codex DeepSeek key 有无都会改变会话边界,
77
94
  * 供 pickResumeId 判定冷启动。default 恒为 null;不含 key 明文(同 Provider 换 key 可续)。
@@ -1,4 +1,6 @@
1
- export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi", "hermes", "opencode"];
1
+ export const LOCAL_EXECUTION_RUNTIMES = [
2
+ "claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness",
3
+ ];
2
4
  export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
3
5
  claude: Object.freeze({
4
6
  transport: "claude-stream-json",
@@ -25,6 +27,11 @@ export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
25
27
  nativeResume: true,
26
28
  systemPromptTransport: "file",
27
29
  }),
30
+ "deepseek-harness": Object.freeze({
31
+ transport: "deepseek-harness-acp",
32
+ nativeResume: false,
33
+ systemPromptTransport: "protocol",
34
+ }),
28
35
  });
29
36
  export function runtimeCapability(runtime) {
30
37
  return LOCAL_RUNTIME_CAPABILITIES[runtime];
@@ -0,0 +1,158 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { z } from "zod";
4
+ import { augmentedPath } from "./runtime-path.js";
5
+ export const RUNTIME_HEALTH_PROBE_CAPABILITY = "runtime_health_probe_v1";
6
+ export const RUNTIME_HEALTH_NAMES = ["claude", "codex", "opencode", "kimi"];
7
+ const RuntimeProbeTargetSchema = z.object({
8
+ runtime: z.enum(RUNTIME_HEALTH_NAMES),
9
+ modelId: z.string().min(1).max(200).nullable(),
10
+ configFingerprint: z.string().min(1).max(200).nullable(),
11
+ }).strict();
12
+ export const RuntimeProbeRequestSchema = z.object({
13
+ runtimes: z.array(RuntimeProbeTargetSchema).min(1).max(RUNTIME_HEALTH_NAMES.length),
14
+ timeoutMs: z.number().int().min(10).max(60_000),
15
+ }).strict().superRefine((value, context) => {
16
+ const seen = new Set();
17
+ for (let index = 0; index < value.runtimes.length; index += 1) {
18
+ const runtime = value.runtimes[index]?.runtime;
19
+ if (runtime === undefined || !seen.has(runtime)) {
20
+ if (runtime !== undefined)
21
+ seen.add(runtime);
22
+ continue;
23
+ }
24
+ context.addIssue({
25
+ code: z.ZodIssueCode.custom,
26
+ path: ["runtimes", index, "runtime"],
27
+ message: "duplicate runtime",
28
+ });
29
+ }
30
+ });
31
+ export const RuntimeProbeFrameSchema = z.object({
32
+ type: z.literal("runtime-health:probe"),
33
+ reqId: z.string().min(1).max(128),
34
+ runtimes: z.array(RuntimeProbeTargetSchema).min(1).max(RUNTIME_HEALTH_NAMES.length),
35
+ timeoutMs: z.number().int().min(10).max(60_000),
36
+ }).strict();
37
+ const execFileAsync = promisify(execFile);
38
+ const defaultExecutor = async (file, args, options) => {
39
+ const result = await execFileAsync(file, [...args], options);
40
+ return { stdout: String(result.stdout), stderr: String(result.stderr) };
41
+ };
42
+ const adapterOf = (_runtime) => "executable";
43
+ function modelArgs(runtime, modelId) {
44
+ const model = modelId === null ? [] : ["--model", modelId];
45
+ if (runtime === "claude")
46
+ return ["--print", "--output-format", "json", ...model, "Reply only OK."];
47
+ if (runtime === "codex")
48
+ return ["exec", "--json", "--skip-git-repo-check", ...model, "Reply only OK."];
49
+ if (runtime === "opencode")
50
+ return ["run", "--format", "json", ...model, "Reply only OK."];
51
+ if (runtime === "kimi")
52
+ return ["--print", "--output-format", "json", ...model, "Reply only OK."];
53
+ return [];
54
+ }
55
+ function failureText(error) {
56
+ if (!error || typeof error !== "object")
57
+ return String(error);
58
+ const value = error;
59
+ return [value.message, value.stdout, value.stderr].filter((part) => typeof part === "string")
60
+ .join("\n").toLowerCase().slice(0, 32_000);
61
+ }
62
+ function failureCode(error) {
63
+ const text = failureText(error);
64
+ if (/quota|insufficient[_ ]quota|credit balance/.test(text))
65
+ return "quota_exhausted";
66
+ if (/\b401\b|unauthori[sz]ed|invalid api[_ -]?key|authentication failed/.test(text))
67
+ return "auth_failed";
68
+ if (/model.{0,80}(not found|does not exist|unknown)/.test(text))
69
+ return "model_not_found";
70
+ if (/model.{0,80}(mapping|alias).{0,40}(invalid|missing|failed)/.test(text))
71
+ return "mapping_invalid";
72
+ if (/\b429\b|rate.?limit|too many requests/.test(text))
73
+ return "rate_limited";
74
+ if (/\b5\d\d\b|upstream|bad gateway|service unavailable/.test(text))
75
+ return "upstream_error";
76
+ return "upstream_error";
77
+ }
78
+ const isMissing = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
79
+ const isAbort = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
80
+ async function runBounded(execute, file, args, timeoutMs, env) {
81
+ const controller = new AbortController();
82
+ let timer;
83
+ try {
84
+ return await Promise.race([
85
+ execute(file, args, {
86
+ env,
87
+ signal: controller.signal,
88
+ timeout: timeoutMs,
89
+ maxBuffer: 64 * 1024,
90
+ }),
91
+ new Promise((_resolve, reject) => {
92
+ timer = setTimeout(() => {
93
+ controller.abort();
94
+ reject(Object.assign(new Error("runtime health probe timed out"), { name: "AbortError" }));
95
+ }, timeoutMs);
96
+ }),
97
+ ]);
98
+ }
99
+ finally {
100
+ if (timer !== undefined)
101
+ clearTimeout(timer);
102
+ }
103
+ }
104
+ export async function probeRuntimeHealth(request, dependencies = {}) {
105
+ const parsed = RuntimeProbeRequestSchema.parse(request);
106
+ const execute = dependencies.execute ?? defaultExecutor;
107
+ const now = dependencies.now ?? (() => new Date());
108
+ const monotonicNow = dependencies.monotonicNow ?? (() => performance.now());
109
+ const sourceEnv = dependencies.env ?? process.env;
110
+ const env = { ...sourceEnv, PATH: augmentedPath(sourceEnv) };
111
+ return Promise.all(parsed.runtimes.map(async (target) => {
112
+ const started = monotonicNow();
113
+ const checkedAt = now().toISOString();
114
+ const base = {
115
+ runtime: target.runtime,
116
+ adapter: adapterOf(target.runtime),
117
+ modelId: target.modelId,
118
+ configFingerprint: target.configFingerprint,
119
+ checkedAt,
120
+ };
121
+ const latencyMs = () => Math.max(0, Math.round(monotonicNow() - started));
122
+ try {
123
+ await runBounded(execute, target.runtime, ["--version"], parsed.timeoutMs, env);
124
+ }
125
+ catch (error) {
126
+ return {
127
+ ...base,
128
+ installation: isMissing(error) ? "missing" : "timeout",
129
+ model: "not_checked",
130
+ latencyMs: latencyMs(),
131
+ detailCode: isMissing(error) ? "executable_missing" : "version_timeout",
132
+ };
133
+ }
134
+ if (base.adapter !== "executable") {
135
+ return { ...base, installation: "available", model: "not_checked", latencyMs: latencyMs(), detailCode: null };
136
+ }
137
+ try {
138
+ await runBounded(execute, target.runtime, modelArgs(target.runtime, target.modelId), parsed.timeoutMs, env);
139
+ return { ...base, installation: "available", model: "ok", latencyMs: latencyMs(), detailCode: null };
140
+ }
141
+ catch (error) {
142
+ const code = isAbort(error) ? "timeout" : failureCode(error);
143
+ return { ...base, installation: "available", model: code, latencyMs: latencyMs(), detailCode: code };
144
+ }
145
+ }));
146
+ }
147
+ export async function handleRuntimeProbeFrame(decoded, probe = probeRuntimeHealth) {
148
+ const frame = RuntimeProbeFrameSchema.safeParse(decoded);
149
+ if (!frame.success)
150
+ return null;
151
+ try {
152
+ const data = await probe({ runtimes: frame.data.runtimes, timeoutMs: frame.data.timeoutMs });
153
+ return { reqId: frame.data.reqId, ok: true, data };
154
+ }
155
+ catch {
156
+ return { reqId: frame.data.reqId, ok: false, error: "runtime_health_probe_failed" };
157
+ }
158
+ }
@@ -79,6 +79,7 @@ export function createRuntimeStartupGate(limits, now = Date.now) {
79
79
  kimi: activeByRuntime.get("kimi") ?? 0,
80
80
  hermes: activeByRuntime.get("hermes") ?? 0,
81
81
  opencode: activeByRuntime.get("opencode") ?? 0,
82
+ "deepseek-harness": activeByRuntime.get("deepseek-harness") ?? 0,
82
83
  },
83
84
  }),
84
85
  };
@@ -0,0 +1,20 @@
1
+ import spawn from "cross-spawn";
2
+ import { isAbsolute } from "node:path";
3
+ import { probeKimiAcp } from "./kimi-acp-runner.js";
4
+ export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
5
+ export function deepSeekHarnessConfigPath(env = process.env) {
6
+ const path = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
7
+ return path && isAbsolute(path) ? path : null;
8
+ }
9
+ /** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
10
+ export function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
11
+ const configPath = options.configPath ?? deepSeekHarnessConfigPath();
12
+ if (!configPath || !isAbsolute(configPath))
13
+ return Promise.resolve(false);
14
+ return probeKimiAcp({
15
+ ...options,
16
+ provider: "deepseek-harness",
17
+ configPath,
18
+ effectivePermission: "sandboxed",
19
+ }, spawnProcess);
20
+ }
@@ -1,4 +1,5 @@
1
1
  import { once } from "node:events";
2
+ import { isAbsolute } from "node:path";
2
3
  import { parseArgs } from "node:util";
3
4
  import { Readable, Writable } from "node:stream";
4
5
  import { pathToFileURL } from "node:url";
@@ -92,7 +93,13 @@ export function kimiResumeMethod(capabilities) {
92
93
  return null;
93
94
  }
94
95
  /** Full access may approve an operation, but it must never fabricate an answer to an agent question. */
95
- export function selectKimiPermission(params) {
96
+ export function selectKimiPermission(params, permission = "full_access") {
97
+ if (permission !== "full_access") {
98
+ const rejected = params.options.filter((option) => option.kind === "reject_once");
99
+ return rejected.length === 1
100
+ ? { outcome: { outcome: "selected", optionId: rejected[0].optionId } }
101
+ : { outcome: { outcome: "cancelled" } };
102
+ }
96
103
  const allowOnce = params.options.filter((option) => option.kind === "allow_once");
97
104
  if (params.toolCall.title === "AskUserQuestion" || allowOnce.length > 1) {
98
105
  return { outcome: { outcome: "cancelled" } };
@@ -105,6 +112,31 @@ export function selectKimiPermission(params) {
105
112
  ? { outcome: { outcome: "cancelled" } }
106
113
  : { outcome: { outcome: "selected", optionId: allowed.optionId } };
107
114
  }
115
+ function providerArgs(provider, configPath) {
116
+ if (provider !== "deepseek-harness")
117
+ return ["acp"];
118
+ if (!configPath || !isAbsolute(configPath)) {
119
+ throw new Error("DeepSeek Harness ACP requires an absolute config path");
120
+ }
121
+ return ["--config", configPath];
122
+ }
123
+ function deepSeekPermissionMode(permission) {
124
+ if (permission === "sandboxed")
125
+ return "read-only";
126
+ if (permission === "workspace_write")
127
+ return "workspace-write";
128
+ return "danger-full-access";
129
+ }
130
+ function providerEnv(provider, permission) {
131
+ return provider === "deepseek-harness"
132
+ ? { ...process.env, DSH_PERMISSION_MODE: deepSeekPermissionMode(permission) }
133
+ : process.env;
134
+ }
135
+ function providerDisplayName(provider) {
136
+ if (provider === "deepseek-harness")
137
+ return "DeepSeek Harness";
138
+ return provider === "kimi" ? "Kimi" : "Hermes";
139
+ }
108
140
  function selectConfigId(options, category) {
109
141
  const normalizedNames = category === "model"
110
142
  ? new Set(["model"])
@@ -164,10 +196,11 @@ async function stopChild(child) {
164
196
  /** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
165
197
  export async function probeKimiAcp(options, spawnProcess = spawn) {
166
198
  const provider = options.provider ?? "kimi";
167
- const child = spawnProcess(options.bin, ["acp"], {
199
+ const permission = options.effectivePermission ?? "sandboxed";
200
+ const child = spawnProcess(options.bin, providerArgs(provider, options.configPath), {
168
201
  cwd: process.cwd(),
169
202
  // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
170
- env: { ...process.env, PATH: augmentedPath() },
203
+ env: { ...providerEnv(provider, permission), PATH: augmentedPath() },
171
204
  stdio: ["pipe", "pipe", "pipe"],
172
205
  });
173
206
  if (child.stdin === null || child.stdout === null || child.stderr === null)
@@ -224,11 +257,15 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
224
257
  }
225
258
  export async function runKimiAcp(options) {
226
259
  const provider = options.provider ?? "kimi";
227
- const displayName = provider === "kimi" ? "Kimi" : "Hermes";
260
+ const displayName = providerDisplayName(provider);
261
+ const permission = options.effectivePermission ?? "full_access";
262
+ if (provider === "deepseek-harness" && options.resume) {
263
+ throw new Error("DeepSeek Harness ACP does not support session resume");
264
+ }
228
265
  const prompt = await readPrompt();
229
- const child = spawn(options.bin, ["acp"], {
266
+ const child = spawn(options.bin, providerArgs(provider, options.configPath), {
230
267
  cwd: process.cwd(),
231
- env: process.env,
268
+ env: providerEnv(provider, permission),
232
269
  stdio: ["pipe", "pipe", "pipe"],
233
270
  });
234
271
  let runtimeChild = child;
@@ -256,7 +293,7 @@ export async function runKimiAcp(options) {
256
293
  process.once("SIGTERM", onSignal);
257
294
  process.once("SIGINT", onSignal);
258
295
  const app = client({ name: `nowcrew-daemon-${provider}` })
259
- .onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
296
+ .onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params, permission))
260
297
  .onNotification(methods.client.session.update, async ({ params }) => {
261
298
  acpSemanticProgress = true;
262
299
  firstProgress.observe();
@@ -416,13 +453,19 @@ function optionsFromArgv(argv) {
416
453
  reasoning: { type: "string" },
417
454
  session: { type: "string" },
418
455
  resume: { type: "boolean", default: false },
456
+ permission: { type: "string", default: "full_access" },
457
+ config: { type: "string" },
419
458
  },
420
459
  });
421
460
  if (!values.bin)
422
461
  throw new Error("--bin is required");
423
- if (values.provider !== "kimi" && values.provider !== "hermes") {
424
- throw new Error("--provider must be kimi or hermes");
462
+ if (values.provider !== "kimi" && values.provider !== "hermes"
463
+ && values.provider !== "deepseek-harness") {
464
+ throw new Error("--provider must be kimi, hermes, or deepseek-harness");
425
465
  }
466
+ if (values.permission !== "sandboxed" && values.permission !== "workspace_write"
467
+ && values.permission !== "full_access")
468
+ throw new Error("invalid --permission");
426
469
  if (values.resume && !values.session)
427
470
  throw new Error("--resume requires --session");
428
471
  return {
@@ -432,6 +475,8 @@ function optionsFromArgv(argv) {
432
475
  ...(values.reasoning ? { reasoning: values.reasoning } : {}),
433
476
  ...(values.session ? { sessionId: values.session } : {}),
434
477
  ...(values.resume ? { resume: true } : {}),
478
+ effectivePermission: values.permission,
479
+ ...(values.config ? { configPath: values.config } : {}),
435
480
  };
436
481
  }
437
482
  if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/dist/serve.js CHANGED
@@ -42,6 +42,7 @@ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
42
42
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
43
43
  import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
44
44
  import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
45
+ import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js";
45
46
  // normalize.ts 的活动种类 → activity 枚举
46
47
  const ACTIVITY_MAP = {
47
48
  init: "working", text: "thinking", reading: "reading", sending: "sending",
@@ -698,6 +699,16 @@ export function serve(config, opts = {}) {
698
699
  catch { /* server 会按请求超时处理 */ }
699
700
  return;
700
701
  }
702
+ if (msg.type === "runtime-health:probe") {
703
+ const result = await handleRuntimeProbeFrame(msg, opts.runtimeHealth?.probe ?? probeRuntimeHealth);
704
+ if (result !== null) {
705
+ try {
706
+ ws?.send(JSON.stringify({ type: "fs:result", ...result }));
707
+ }
708
+ catch { /* server 会超时 */ }
709
+ }
710
+ return;
711
+ }
701
712
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
702
713
  if (msg.type === "raft:inspect" || msg.type === "raft:import") {
703
714
  const req = msg;
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
2
2
  import { join } from "node:path";
3
3
  import { startDormantSupervisor, } from "./execution-supervisor.js";
4
4
  import { buildClaudeArgs } from "./runtimes/claude.js";
5
+ import { DEEPSEEK_HARNESS_CONFIG_ENV, deepSeekHarnessConfigPath, } from "./runtimes/deepseek-harness.js";
5
6
  import { RuntimeCancelledError } from "./runtime-cancellation.js";
6
7
  export function supervisorLaunch(request) {
7
8
  const common = {
@@ -74,6 +75,27 @@ export function supervisorLaunch(request) {
74
75
  stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
75
76
  };
76
77
  }
78
+ if (request.runtime === "deepseek-harness") {
79
+ if (request.resume)
80
+ throw new Error("DeepSeek Harness ACP does not support session resume");
81
+ const configPath = deepSeekHarnessConfigPath();
82
+ if (configPath === null) {
83
+ throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
84
+ }
85
+ return {
86
+ command: process.execPath,
87
+ args: [
88
+ fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
89
+ "--provider", "deepseek-harness",
90
+ "--bin", request.bin,
91
+ "--config", configPath,
92
+ "--permission", request.effectivePermission,
93
+ ],
94
+ cwd: request.cwd,
95
+ env: request.env,
96
+ stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
97
+ };
98
+ }
77
99
  if (request.effectivePermission !== "full_access") {
78
100
  const displayName = request.runtime === "hermes" ? "Hermes" : "Kimi";
79
101
  throw new Error(`${displayName} ACP cannot enforce ${request.effectivePermission} permission`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.50",
3
+ "version": "0.5.52",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",