@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.
- package/README.md +15 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/local-executor.js +4 -2
- package/dist/machine-info.js +16 -4
- package/dist/main.js +4 -69
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +3 -4
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +224 -0
- package/dist/runtimes/deepseek-harness.js +61 -10
- package/dist/runtimes/kimi-acp-runner.js +5 -5
- package/dist/serve.js +14 -62
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/package.json +1 -1
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/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"
|
|
17
|
+
? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"]
|
|
18
18
|
: [];
|
|
19
19
|
}
|
|
20
20
|
catch {
|
package/dist/local-executor.js
CHANGED
|
@@ -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" ?
|
|
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)
|
package/dist/machine-info.js
CHANGED
|
@@ -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
|
|
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]) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
|
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([
|
package/dist/provider-env.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|