@nowcrew/daemon 0.6.88-beta.15 → 0.6.88-beta.16

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.
Files changed (38) hide show
  1. package/README.md +15 -0
  2. package/dist/daemon-update-eligibility.js +1 -1
  3. package/dist/local-executor.js +4 -2
  4. package/dist/machine-info.js +16 -4
  5. package/dist/main.js +4 -69
  6. package/dist/project-skills/execution-adapter.js +3 -2
  7. package/dist/project-skills/runtime-launch.js +14 -0
  8. package/dist/provider-env.js +3 -4
  9. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  10. package/dist/runtimes/deepseek-harness-runner.js +224 -0
  11. package/dist/runtimes/deepseek-harness.js +61 -10
  12. package/dist/runtimes/kimi-acp-runner.js +5 -5
  13. package/dist/serve.js +14 -62
  14. package/dist/storage-identity.js +0 -3
  15. package/dist/storage-serve-adapter.js +6 -72
  16. package/dist/supervised-runtime.js +17 -2
  17. package/package.json +1 -1
  18. package/dist/daemon-migration-controller.js +0 -211
  19. package/dist/daemon-migration-wiring.js +0 -22
  20. package/dist/storage-control.js +0 -212
  21. package/dist/storage-copy.js +0 -628
  22. package/dist/storage-credential-guard.js +0 -21
  23. package/dist/storage-doctor.js +0 -228
  24. package/dist/storage-manifest.js +0 -1027
  25. package/dist/storage-migration-controller.js +0 -440
  26. package/dist/storage-migration-manifest.js +0 -105
  27. package/dist/storage-migration-policy.js +0 -100
  28. package/dist/storage-runtime-controller.js +0 -381
  29. package/dist/storage-runtime-readiness.js +0 -240
  30. package/dist/storage-safe-fs.js +0 -302
  31. package/dist/storage-status-delivery.js +0 -224
  32. package/dist/storage-status-replay.js +0 -140
  33. package/dist/storage-target-confirmation.js +0 -129
  34. package/dist/storage-worker-command.js +0 -37
  35. package/dist/storage-worker-executor.js +0 -449
  36. package/dist/storage-worker-handoff.js +0 -151
  37. package/dist/storage-worker-operation.js +0 -294
  38. package/dist/storage-worker-status.js +0 -259
package/README.md CHANGED
@@ -3,6 +3,21 @@
3
3
  The daemon is NowWork's local execution adapter. It holds a machine credential, keeps one control-plane
4
4
  WebSocket open, and launches coding runtimes on that machine. Product policy remains on the server.
5
5
 
6
+ ## 已完成搬迁后的存储运行
7
+
8
+ NAS / `NOWWORK_HOME` 搬迁和旧 daemon 布局标准化入口已退役。daemon 不再提供内部
9
+ `storage-migration-worker` / `storage-migration-recover` 命令,不处理 `daemon:migrate`、
10
+ `daemon:storage-doctor`、`daemon:storage-migrate` 或迁移确认消息,也不再声明
11
+ `storage_migration_v1` / `storage_migration_status_v1` / `daemon_layout_migration_v1` 能力。
12
+
13
+ 现有 `installations.json` 即使仍有历史 `storageMigrationId`,也按当前 installation 解析
14
+ Agent、仓库和报告目录;启动、hello 和身份绑定不再读取迁移 receipt、outbox 或备份。
15
+ hello 继续报告布局、环境、Workspace、路径指纹与 ownership,但不再发送迁移 ID。
16
+ 每次连接仍须完成 Server 身份绑定后才能执行任务;Workspace 或 Machine 身份不匹配时拒绝执行。
17
+
18
+ 旧布局读取兼容、日常安装/升级、NAS 所有权校验、执行 journal 与 ACK/replay 继续保留。
19
+ 退役代码不会删除历史安装字段、迁移记录、receipt、备份或旧数据目录;这些数据的保留与清理另行处理。
20
+
6
21
  ## Run
7
22
 
8
23
  For a temporary foreground connection, run:
@@ -14,7 +14,7 @@ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, man
14
14
  export async function managedDaemonCapabilities(eligibility) {
15
15
  try {
16
16
  return (await eligibility()).eligible
17
- ? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1", "daemon_layout_migration_v1"]
17
+ ? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"]
18
18
  : [];
19
19
  }
20
20
  catch {
@@ -1,3 +1,4 @@
1
+ import { deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
1
2
  import { createInterface } from "node:readline";
2
3
  import { readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { delimiter, join } from "node:path";
@@ -569,7 +570,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
569
570
  });
570
571
  }
571
572
  const trainingRoots = input.trainingProjectContext?.projects.map((project) => project.root) ?? [];
572
- const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
573
+ const { codexSkillRoots, claudeAdditionalDirectories, harnessSkillRoots } = await projectSkillRuntimeDirectories({
573
574
  runtime: runtime.name,
574
575
  ...(runtimeRoot === undefined ? {} : { runtimeRoot }),
575
576
  agentRoot: workspace.dir,
@@ -607,7 +608,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
607
608
  const workspaceRoots = [...legacyRoots, ...trainingRoots];
608
609
  const launchRequest = {
609
610
  runtime: runtime.name,
610
- bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
611
+ bin: runtime.name === "deepseek-harness" ? deepSeekHarnessLauncher(runtimeEnv).bin : runtime.name,
612
+ ...(runtime.name === "deepseek-harness" ? { harnessStateRoot: executionWorkspace.runDir, harnessSkillRoots: harnessSkillRoots ?? [] } : {}),
611
613
  cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
612
614
  ...(runtimeRoot !== undefined
613
615
  || (input.projectSkills === undefined && input.abilityRelease === undefined)
@@ -18,7 +18,7 @@ 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
20
  import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
21
- import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
21
+ import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
22
22
  import { daemonHome } from "./computer-profile.js";
23
23
  import { daemonReleaseProvenance } from "./release-provenance.js";
24
24
  export { executableRuntimes } from "./runtime-capabilities.js";
@@ -90,7 +90,7 @@ const RUNTIME_BINS = [
90
90
  ["gemini", "gemini"],
91
91
  ["hermes", "hermes"],
92
92
  ["opencode", "opencode"],
93
- ["deepseek-harness", "dsh-acp-demo"],
93
+ ["deepseek-harness", "dsh"],
94
94
  ["copilot", "copilot"],
95
95
  ["kimi", "kimi"],
96
96
  ["pi", "pi"],
@@ -107,7 +107,14 @@ async function isInstalled(bin) {
107
107
  }
108
108
  /** 并发探测所有候选 runtime,返回已安装的展示名列表。 */
109
109
  export async function detectRuntimes() {
110
- const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
110
+ const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => {
111
+ try {
112
+ return await isInstalled(name === "deepseek-harness" ? deepSeekHarnessLauncher().bin : bin) ? name : null;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }));
111
118
  return checks.filter((x) => x !== null);
112
119
  }
113
120
  async function supportsKimiAcp(signal) {
@@ -120,7 +127,12 @@ async function supportsOpenCodeRun(signal) {
120
127
  return probeOpenCodeRun(signal);
121
128
  }
122
129
  async function supportsDeepSeekHarnessAcp(signal) {
123
- return probeDeepSeekHarnessAcp({ bin: "dsh-acp-demo", ...(signal ? { signal } : {}) });
130
+ try {
131
+ return await probeDeepSeekHarnessAcp({ bin: deepSeekHarnessLauncher().bin, ...(signal ? { signal } : {}) });
132
+ }
133
+ catch {
134
+ return false;
135
+ }
124
136
  }
125
137
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
126
138
  export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, deepSeekHarnessProbe = supportsDeepSeekHarnessAcp, signal) {
package/dist/main.js CHANGED
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
9
  import { homedir } from "node:os";
10
- import { join } from "node:path";
11
10
  import { loadConfig, ConfigError } from "./config.js";
12
11
  import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
13
12
  import { cliVersion, daemonVersion } from "./machine-info.js";
@@ -25,19 +24,9 @@ import { assertManagedServiceStartup } from "./managed-service-startup.js";
25
24
  import { daemonGlobalInstallation } from "./daemon-installation.js";
26
25
  import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
27
26
  import { createStorageRuntimeOwnershipBinding, loadStorageRuntime } from "./storage-runtime.js";
28
- import { readStorageMigrationReceipt } from "./storage-migration-controller.js";
29
- import { storageMigrationStatusReplay } from "./storage-status-replay.js";
30
- import { runStorageWorkerCommand } from "./storage-worker-command.js";
31
- import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
32
- import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
33
27
  import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
34
28
  import { storageServiceEnvironment } from "./computer-service.js";
35
29
  async function main() {
36
- const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
37
- if (workerResult !== null) {
38
- process.exitCode = workerResult;
39
- return;
40
- }
41
30
  const computerResult = await runComputerCommand(process.argv.slice(2));
42
31
  if (computerResult !== null) {
43
32
  process.exitCode = computerResult;
@@ -74,7 +63,6 @@ async function main() {
74
63
  // 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
75
64
  if (values["daemon-home"])
76
65
  process.env.CREW_DAEMON_HOME = values["daemon-home"];
77
- let activeDaemonHome;
78
66
  const localHome = resolveDaemonLocalStateRoot({
79
67
  userHome: homedir(),
80
68
  platform: process.platform,
@@ -85,7 +73,6 @@ async function main() {
85
73
  });
86
74
  if (values.profile) {
87
75
  const home = daemonHome(process.env, values.profile);
88
- activeDaemonHome = home;
89
76
  const profile = await loadProfile(values.profile, home);
90
77
  if (cmd === "serve") {
91
78
  await assertProfileAgentsRootUnique(profile, home, homedir());
@@ -148,68 +135,16 @@ async function main() {
148
135
  const installationLease = installation === null
149
136
  ? null
150
137
  : await acquireDaemonInstallationLease(installation.npmPrefix, { localHome });
151
- const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
152
- ? { ready: false, reason: "not_legacy_source" }
153
- : await probeStorageMigrationRuntime({
154
- profile: values.profile,
155
- serverUrl: config.serverUrl,
156
- currentDaemonHome: activeDaemonHome,
157
- currentAgentsRoot: config.agentsRoot,
158
- userHome: homedir(),
159
- platform: process.platform,
160
- ...(process.getuid === undefined ? {} : { uid: process.getuid() }),
161
- environment: process.env,
162
- });
163
- const targetStorageBinding = storageRuntime === null
138
+ const storageBinding = storageRuntime === null
164
139
  ? null
165
140
  : createStorageRuntimeOwnershipBinding(storageRuntime);
166
- const targetConfirmation = storageRuntime?.identity.storageMigrationId === undefined
167
- ? null
168
- : createStorageTargetConfirmationHandler({
169
- backupsRoot: storageRuntime.local.backupsRoot,
170
- identity: storageRuntime.identity,
171
- profile: storageRuntime.installation.profile,
172
- completionTimeoutMs: 180_000,
173
- });
174
- const storageMigration = storageRuntime !== null
175
- ? {
176
- ready: false,
177
- statusOutboxPath: join(storageRuntime.local.stateRoot, "storage-status-outbox.json"),
178
- identity: targetStorageBinding.identity,
179
- bindServerIdentity: targetStorageBinding.bind,
180
- ...(targetConfirmation === null ? {} : { confirm: targetConfirmation.handle }),
181
- ...(storageRuntime.identity.storageMigrationId === undefined ? {} : {
182
- replayStatus: async (send) => {
183
- const migrationId = storageRuntime.identity.storageMigrationId;
184
- const receipt = await readStorageMigrationReceipt(storageRuntime.local.backupsRoot, migrationId);
185
- if (receipt === null)
186
- throw new Error(`storage migration receipt not found: ${migrationId}`);
187
- for (const frame of storageMigrationStatusReplay(receipt, {
188
- migrationId,
189
- environment: storageRuntime.identity.storageEnvironment,
190
- workspaceId: storageRuntime.identity.storageWorkspaceId,
191
- profile: storageRuntime.installation.profile,
192
- }))
193
- send(frame);
194
- },
195
- }),
196
- }
197
- : migrationRuntime.ready
198
- ? {
199
- ready: true,
200
- statusOutboxPath: migrationRuntime.runtime.statusOutboxPath,
201
- identity: migrationRuntime.runtime.identity,
202
- controller: migrationRuntime.runtime.controller,
203
- attach: migrationRuntime.runtime.attach,
204
- bindServerIdentity: migrationRuntime.runtime.bindServerIdentity,
205
- replayStatus: migrationRuntime.runtime.replayStatus,
206
- }
207
- : undefined;
208
141
  await runWithDaemonInstallationLease(installationLease, async () => {
209
142
  const service = serve(runtimeConfig, {
210
143
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
211
144
  localHome,
212
- ...(storageMigration === undefined ? {} : { storageMigration }),
145
+ ...(storageBinding === null ? {} : {
146
+ storageRuntime: { identity: storageBinding.identity, bindServerIdentity: storageBinding.bind },
147
+ }),
213
148
  });
214
149
  await runServeLifecycle(service);
215
150
  });
@@ -9,10 +9,11 @@ export function projectSkillExecutionProjection(bindings, generation) {
9
9
  });
10
10
  }
11
11
  /**
12
- * 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时是 codex/claude 的执行
12
+ * 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时支持受控目录的执行
13
13
  * 才会有投影目录,也才需要在停止无法证明时保留它。两处判定分开写过一次,结果 kimi + projectSkills
14
14
  * 会在取消路径上错抛 ownership 未确认。
15
15
  */
16
16
  export const projectSkillProjectionApplies = (spec) => spec.agent.projectSkills !== undefined
17
- && (spec.runtime.name === "codex" || spec.runtime.name === "claude");
17
+ && (spec.runtime.name === "codex" || spec.runtime.name === "claude"
18
+ || spec.runtime.name === "deepseek-harness");
18
19
  export const projectSkillProjectionErrorCode = (error) => error instanceof ProjectProjectionError ? error.code : null;
@@ -11,12 +11,26 @@ export function projectSkillRuntimeLaunchFacts(runtime, root) {
11
11
  if (runtime === "claude") {
12
12
  return Object.freeze({ claudeAdditionalDirectory: root.claudeAdditionalDirectory });
13
13
  }
14
+ if (runtime === "deepseek-harness") {
15
+ return Object.freeze({ harnessSkillRoot: root.codexSkillRoot });
16
+ }
14
17
  throw new ProjectProjectionError("skill_projection_failed");
15
18
  }
16
19
  /** Builds Runtime-native arrays while keeping v1 shared roots separate from v2 exact roots. */
17
20
  export async function projectSkillRuntimeDirectories(input) {
18
21
  const trainingProjectRoots = input.trainingProjectRoots ?? [];
19
22
  const facts = projectSkillRuntimeLaunchFacts(input.runtime, input.runtimeRoot);
23
+ if (input.runtime === "deepseek-harness") {
24
+ // Harness 的 local provider 接受直接包含 Skill 子目录的受控 root,不扫描项目/用户目录。
25
+ return Object.freeze({
26
+ codexSkillRoots: Object.freeze([]),
27
+ claudeAdditionalDirectories: Object.freeze([]),
28
+ harnessSkillRoots: Object.freeze([
29
+ ...(facts.harnessSkillRoot === undefined ? [] : [facts.harnessSkillRoot]),
30
+ ...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
31
+ ]),
32
+ });
33
+ }
20
34
  const codexSkillRoots = input.runtime === "codex"
21
35
  && (input.runtimeRoot !== undefined || input.projectContext !== undefined)
22
36
  ? Object.freeze([
@@ -10,7 +10,6 @@
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";
14
13
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
14
  const RESERVED_ENV = new Set([
16
15
  "PATH",
@@ -96,9 +95,9 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
96
95
  }
97
96
  return env;
98
97
  }
99
- const deepSeekHarnessMachineOwnedKey = (key) => (key.startsWith("DEEPSEEK_")
100
- || key === DEEPSEEK_HARNESS_CONFIG_ENV
101
- || key.startsWith("DSH_"));
98
+ const deepSeekHarnessMachineOwnedKey = (key) => (key.toUpperCase().startsWith("DEEPSEEK_")
99
+ || key.toUpperCase().startsWith("NOWCREW_DEEPSEEK_HARNESS_")
100
+ || key.toUpperCase().startsWith("DSH_"));
102
101
  /** Agent env remains available to tools, but cannot replace Harness process policy or credentials. */
103
102
  export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
104
103
  const env = { ...base };
@@ -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,224 @@
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 { prepareHarnessProfile } from "./deepseek-harness-profile.js";
11
+ import { mapAcpUpdate } from "./kimi-acp-runner.js";
12
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
13
+ const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
14
+ export const HarnessInputSchema = z
15
+ .object({
16
+ systemPrompt: z.string(),
17
+ wakePrompt: z.string().min(1),
18
+ stateRoot: absolutePath,
19
+ skillRoots: z.array(absolutePath).default([]),
20
+ effectivePermission: z.enum([
21
+ "sandboxed",
22
+ "workspace_write",
23
+ "full_access",
24
+ ]),
25
+ })
26
+ .strict();
27
+ const emit = async (event) => {
28
+ if (!process.stdout.write(`${JSON.stringify(event)}\n`))
29
+ await once(process.stdout, "drain");
30
+ };
31
+ async function settleWithin(promise, ms) {
32
+ let timer;
33
+ try {
34
+ return await Promise.race([
35
+ promise,
36
+ new Promise((resolve) => {
37
+ timer = setTimeout(() => resolve(undefined), ms);
38
+ }),
39
+ ]);
40
+ }
41
+ finally {
42
+ if (timer)
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+ export async function runHarness(input, bin, patchPath) {
47
+ const delivery = await prepareThreadDelivery({
48
+ env: process.env,
49
+ resumed: false,
50
+ sessionId: "cold-harness",
51
+ wakePrompt: input.wakePrompt,
52
+ });
53
+ const profile = await prepareHarnessProfile({
54
+ ...input,
55
+ cwd: process.cwd(),
56
+ env: process.env,
57
+ ...(patchPath ? { patchPath } : {}),
58
+ });
59
+ const child = spawn(bin, profile.args, {
60
+ cwd: profile.home,
61
+ env: profile.env,
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ });
64
+ // Boot away from the repository: the CLI must not load a project's .env into
65
+ // trusted process credentials. ACP session/new carries the real workspace.
66
+ let spawnError;
67
+ const closed = new Promise((resolve) => {
68
+ child.once("error", (error) => {
69
+ spawnError = error;
70
+ resolve();
71
+ });
72
+ child.once("close", () => resolve());
73
+ });
74
+ if (!child.stdin || !child.stdout || !child.stderr)
75
+ throw new Error("DeepSeek Harness stdio unavailable");
76
+ // Provider diagnostics may include secrets or prompts; do not stream raw stderr.
77
+ child.stderr.resume();
78
+ let context;
79
+ let sessionId;
80
+ let cancelled = false;
81
+ let stage = "initialize";
82
+ let exitCode = 1;
83
+ let stopping;
84
+ const stop = () => (stopping ??= (async () => {
85
+ child.stdin?.end();
86
+ if (await settleWithin(closed.then(() => true), 2000))
87
+ return;
88
+ child.kill("SIGTERM");
89
+ if (await settleWithin(closed.then(() => true), 1000))
90
+ return;
91
+ child.kill("SIGKILL");
92
+ if (!(await settleWithin(closed.then(() => true), 2000)))
93
+ throw new Error("DeepSeek Harness process stop unconfirmed");
94
+ })());
95
+ const cancel = () => {
96
+ cancelled = true;
97
+ void (async () => {
98
+ if (context && sessionId)
99
+ await settleWithin(context
100
+ .notify(methods.agent.session.cancel, { sessionId })
101
+ .catch(() => undefined), 1000);
102
+ await stop();
103
+ })().catch(() => {
104
+ /* finally reports stop failure; supervisor owns process tree */
105
+ });
106
+ };
107
+ process.once("SIGTERM", cancel);
108
+ process.once("SIGINT", cancel);
109
+ const app = client({ name: "nowwork-harness" })
110
+ .onRequest(methods.client.session.requestPermission, () => ({
111
+ outcome: { outcome: "cancelled" },
112
+ }))
113
+ .onNotification(methods.client.session.update, async ({ params }) => {
114
+ if (params.sessionId !== sessionId)
115
+ return;
116
+ for (const event of mapAcpUpdate("deepseek-harness", params.update))
117
+ await emit(event);
118
+ });
119
+ try {
120
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
121
+ const result = await app.connectWith(stream, async (ctx) => {
122
+ context = ctx;
123
+ const initialized = await ctx.request(methods.agent.initialize, {
124
+ protocolVersion: PROTOCOL_VERSION,
125
+ clientCapabilities: {},
126
+ clientInfo: { name: "nowwork", version: "1" },
127
+ });
128
+ if (initialized.protocolVersion !== PROTOCOL_VERSION)
129
+ throw new Error("Incompatible ACP protocol");
130
+ if (cancelled)
131
+ throw new Error("cancelled");
132
+ stage = "session_new";
133
+ const session = await ctx.request(methods.agent.session.new, {
134
+ cwd: process.cwd(),
135
+ mcpServers: [],
136
+ });
137
+ sessionId = session.sessionId;
138
+ if (!sessionId || cancelled)
139
+ throw new Error("Harness session unavailable");
140
+ await emit({ type: "thread.started", thread_id: sessionId });
141
+ stage = "prompt";
142
+ const response = await ctx.request(methods.agent.session.prompt, {
143
+ sessionId,
144
+ prompt: [{ type: "text", text: delivery.prompt }],
145
+ });
146
+ if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
147
+ stage = "session_close";
148
+ const close = await settleWithin(ctx
149
+ .request(methods.agent.session.close, { sessionId })
150
+ .then(() => true), 5000);
151
+ if (!close)
152
+ throw new Error("Harness session close timed out");
153
+ }
154
+ return response;
155
+ });
156
+ const usage = result.usage;
157
+ if (!cancelled)
158
+ await emit({
159
+ type: "turn.completed",
160
+ stop_reason: result.stopReason,
161
+ ...(usage
162
+ ? {
163
+ usage: {
164
+ input_tokens: usage.inputTokens,
165
+ output_tokens: usage.outputTokens,
166
+ cache_read_input_tokens: usage.cachedReadTokens ?? 0,
167
+ cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
168
+ },
169
+ }
170
+ : {}),
171
+ });
172
+ exitCode =
173
+ result.stopReason === "end_turn"
174
+ ? 0
175
+ : result.stopReason === "cancelled"
176
+ ? 130
177
+ : 1;
178
+ }
179
+ catch {
180
+ if (!cancelled)
181
+ process.stderr.write(spawnError
182
+ ? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
183
+ : `DeepSeek Harness ACP execution failed at ${stage}; verify the pinned version, machine credentials and profile patch.\n`);
184
+ }
185
+ finally {
186
+ try {
187
+ await stop();
188
+ }
189
+ finally {
190
+ process.off("SIGTERM", cancel);
191
+ process.off("SIGINT", cancel);
192
+ }
193
+ }
194
+ if (cancelled)
195
+ return 130;
196
+ return child.exitCode !== 0 ? 1 : exitCode;
197
+ }
198
+ async function main() {
199
+ const { values } = parseArgs({
200
+ options: { bin: { type: "string" }, patch: { type: "string" } },
201
+ });
202
+ if (!values.bin)
203
+ throw new Error("DeepSeek Harness --bin is required");
204
+ process.stdin.setEncoding("utf8");
205
+ let text = "";
206
+ for await (const chunk of process.stdin) {
207
+ text += String(chunk);
208
+ if (Buffer.byteLength(text) > 16 * 1024 * 1024)
209
+ throw new Error("Harness input exceeds 16 MiB");
210
+ }
211
+ const input = HarnessInputSchema.parse(JSON.parse(text));
212
+ return runHarness(input, values.bin, values.patch);
213
+ }
214
+ if (process.argv[1] &&
215
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
216
+ main()
217
+ .then((code) => {
218
+ process.exitCode = code;
219
+ })
220
+ .catch(() => {
221
+ process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
222
+ process.exitCode = 1;
223
+ });
224
+ }