@nowcrew/daemon 0.5.49 → 0.5.51
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 +27 -2
- package/dist/computer-profile.js +9 -4
- package/dist/console.js +7 -4
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +2 -0
- package/dist/external-output.js +1 -0
- package/dist/local-executor.js +15 -6
- package/dist/machine-info.js +11 -4
- package/dist/normalize.js +4 -2
- package/dist/provider-env.js +17 -0
- package/dist/runtime-capabilities.js +8 -1
- package/dist/runtime-startup-gate.js +1 -0
- package/dist/runtimes/deepseek-harness.js +20 -0
- package/dist/runtimes/kimi-acp-runner.js +54 -9
- package/dist/supervised-runtime.js +22 -0
- package/package.json +1 -1
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 `
|
|
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/computer-profile.js
CHANGED
|
@@ -131,8 +131,13 @@ export function validateProfileName(name) {
|
|
|
131
131
|
export function profilePath(name, home = daemonHome()) {
|
|
132
132
|
return resolve(home, "profiles", `${validateProfileName(name)}.json`);
|
|
133
133
|
}
|
|
134
|
-
const
|
|
135
|
-
const
|
|
134
|
+
const POWERSHELL_SECURITY_ASSEMBLY = "[void](Add-Type -AssemblyName System.Security);";
|
|
135
|
+
const DPAPI_PROTECT = `${POWERSHELL_SECURITY_ASSEMBLY}$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)`;
|
|
136
|
+
const DPAPI_UNPROTECT = `${POWERSHELL_SECURITY_ASSEMBLY}$p=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))`;
|
|
137
|
+
export const windowsDpapiScripts = {
|
|
138
|
+
protect: DPAPI_PROTECT,
|
|
139
|
+
unprotect: DPAPI_UNPROTECT,
|
|
140
|
+
};
|
|
136
141
|
async function powershellStdin(script, input) {
|
|
137
142
|
return new Promise((resolvePromise, reject) => {
|
|
138
143
|
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
@@ -155,8 +160,8 @@ async function powershellStdin(script, input) {
|
|
|
155
160
|
});
|
|
156
161
|
}
|
|
157
162
|
export const windowsDpapiProtector = {
|
|
158
|
-
protect: (plaintext) => powershellStdin(
|
|
159
|
-
unprotect: (ciphertext) => powershellStdin(
|
|
163
|
+
protect: (plaintext) => powershellStdin(windowsDpapiScripts.protect, plaintext),
|
|
164
|
+
unprotect: (ciphertext) => powershellStdin(windowsDpapiScripts.unprotect, ciphertext),
|
|
160
165
|
};
|
|
161
166
|
function storageOptions(options) {
|
|
162
167
|
const platform = options.platform ?? process.platform;
|
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"
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 !== "."
|
package/dist/execution-runner.js
CHANGED
|
@@ -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) {
|
package/dist/external-output.js
CHANGED
|
@@ -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
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -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"
|
|
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
|
|
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"
|
|
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)) {
|
package/dist/machine-info.js
CHANGED
|
@@ -17,6 +17,7 @@ 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 { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
|
|
20
21
|
export { executableRuntimes } from "./runtime-capabilities.js";
|
|
21
22
|
const execFileP = promisify(execFile);
|
|
22
23
|
export const DAEMON_CAPABILITIES = [
|
|
@@ -43,6 +44,7 @@ const RUNTIME_BINS = [
|
|
|
43
44
|
["gemini", "gemini"],
|
|
44
45
|
["hermes", "hermes"],
|
|
45
46
|
["opencode", "opencode"],
|
|
47
|
+
["deepseek-harness", "dsh-acp-demo"],
|
|
46
48
|
["copilot", "copilot"],
|
|
47
49
|
["kimi", "kimi"],
|
|
48
50
|
["pi", "pi"],
|
|
@@ -71,20 +73,25 @@ async function supportsHermesAcp(signal) {
|
|
|
71
73
|
async function supportsOpenCodeRun(signal) {
|
|
72
74
|
return probeOpenCodeRun(signal);
|
|
73
75
|
}
|
|
76
|
+
async function supportsDeepSeekHarnessAcp(signal) {
|
|
77
|
+
return probeDeepSeekHarnessAcp({ bin: "dsh-acp-demo", ...(signal ? { signal } : {}) });
|
|
78
|
+
}
|
|
74
79
|
/** 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) {
|
|
80
|
+
export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal) {
|
|
76
81
|
const present = await installed;
|
|
77
|
-
const [kimiReady, hermesReady, openCodeReady] = await Promise.all([
|
|
82
|
+
const [kimiReady, hermesReady, openCodeReady, deepSeekHarnessReady] = await Promise.all([
|
|
78
83
|
present.includes("kimi") ? kimiAcpProbe(signal) : false,
|
|
79
84
|
present.includes("hermes") ? hermesAcpProbe(signal) : false,
|
|
80
85
|
present.includes("opencode") ? openCodeProbe(signal) : false,
|
|
86
|
+
present.includes("deepseek-harness") ? deepSeekHarnessProbe(signal) : false,
|
|
81
87
|
]);
|
|
82
88
|
return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
|
|
83
89
|
: runtime === "hermes" ? hermesReady
|
|
84
90
|
: runtime === "opencode" ? openCodeReady
|
|
85
|
-
:
|
|
91
|
+
: runtime === "deepseek-harness" ? deepSeekHarnessReady
|
|
92
|
+
: true));
|
|
86
93
|
}
|
|
87
|
-
export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, signal);
|
|
94
|
+
export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, supportsDeepSeekHarnessAcp, signal);
|
|
88
95
|
export function daemonVersion() {
|
|
89
96
|
try {
|
|
90
97
|
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") {
|
package/dist/provider-env.js
CHANGED
|
@@ -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 = [
|
|
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];
|
|
@@ -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
|
|
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: { ...
|
|
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
|
|
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,
|
|
266
|
+
const child = spawn(options.bin, providerArgs(provider, options.configPath), {
|
|
230
267
|
cwd: process.cwd(),
|
|
231
|
-
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
|
-
|
|
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) {
|
|
@@ -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`);
|