@nowcrew/daemon 0.5.44 → 0.5.46
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 -6
- package/dist/agent-memory/bridge.js +101 -10
- package/dist/computer-service.js +38 -5
- package/dist/daemon-installation.js +35 -18
- package/dist/daemon-update-eligibility.js +53 -12
- package/dist/daemon-updater.js +1 -1
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +3 -11
- package/dist/execution-runner.js +13 -4
- package/dist/external-output.js +4 -0
- package/dist/list-models.js +102 -8
- package/dist/local-executor.js +87 -14
- package/dist/local-memory-diagnostics.js +336 -0
- package/dist/local-memory-telemetry.js +224 -0
- package/dist/machine-info.js +22 -7
- package/dist/main.js +6 -1
- package/dist/normalize.js +14 -4
- package/dist/runtime-capabilities.js +11 -1
- package/dist/runtime-probe.js +26 -0
- package/dist/runtime-startup-gate.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -0
- package/dist/runtimes/hermes-models.js +117 -0
- package/dist/runtimes/hermes.js +6 -0
- package/dist/runtimes/kimi-acp-runner.js +116 -36
- package/dist/runtimes/opencode-runner.js +122 -0
- package/dist/runtimes/opencode.js +181 -0
- package/dist/serve.js +27 -4
- package/dist/slog.js +21 -3
- package/dist/supervised-runtime.js +22 -1
- package/dist/windows-scheduled-task.js +64 -0
- package/dist/workspace.js +34 -3
- package/package.json +8 -9
package/README.md
CHANGED
|
@@ -105,8 +105,10 @@ CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile dev
|
|
|
105
105
|
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile dev
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
If the descriptor has drifted
|
|
109
|
-
|
|
108
|
+
If the target descriptor has drifted or the profile is stopped, the daemon fails closed and remains
|
|
109
|
+
manually upgradeable. On macOS, another unregistered legacy LaunchAgent may coexist only when its plist
|
|
110
|
+
exactly matches a generated NowCrew service and resolves to a different npm prefix. An unverifiable or
|
|
111
|
+
same-prefix descriptor still fails closed.
|
|
110
112
|
|
|
111
113
|
The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
112
114
|
|
|
@@ -115,7 +117,7 @@ The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
|
115
117
|
| Platform | macOS LaunchAgent or Linux systemd user service |
|
|
116
118
|
| Entrypoint | global `@nowcrew/daemon/dist/main.js`, not npx or source/tsx |
|
|
117
119
|
| Startup | `crew-daemon serve --profile <name>` through the installed service |
|
|
118
|
-
| Registry |
|
|
120
|
+
| Registry | registered descriptors remain conflict-free; the exact target legacy descriptor may be adopted once; an exact macOS legacy descriptor on another npm prefix may coexist |
|
|
119
121
|
| Identity | service ID, descriptor, daemon home, Agent root, entrypoint, package root, and npm prefix match |
|
|
120
122
|
| Service | the selected profile is running and holds the installation lease |
|
|
121
123
|
| Install root | a standard writable Unix global npm prefix can be derived from the running entrypoint |
|
|
@@ -191,8 +193,8 @@ dispatch reports unavailable instead of running on another compatible daemon. An
|
|
|
191
193
|
the global compatible-machine pool.
|
|
192
194
|
|
|
193
195
|
The daemon intersects requested permission with local policy, checks advertised resource limits, prepares
|
|
194
|
-
the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`,
|
|
195
|
-
`
|
|
196
|
+
the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, `kimi`,
|
|
197
|
+
`hermes`, or `opencode`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
|
|
196
198
|
behavior. Those are server responsibilities.
|
|
197
199
|
|
|
198
200
|
The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
|
|
@@ -202,6 +204,8 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
202
204
|
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
203
205
|
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume` |
|
|
204
206
|
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
207
|
+
| Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
208
|
+
| OpenCode | `run --format json` | stdin | `--session` |
|
|
205
209
|
|
|
206
210
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
207
211
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
@@ -209,7 +213,20 @@ CLI and carries the same native session id; unrelated ACP failures stay failures
|
|
|
209
213
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
210
214
|
durable Job Object ownership is available.
|
|
211
215
|
|
|
212
|
-
|
|
216
|
+
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
217
|
+
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
218
|
+
machine-local. A missing resumed ACP session is retried once as a fresh session.
|
|
219
|
+
|
|
220
|
+
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
221
|
+
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
222
|
+
compatibility fallback). A missing resumed session is retried once without `--session`; malformed,
|
|
223
|
+
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
224
|
+
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
225
|
+
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
226
|
+
`--model`, `--variant`, and `--session`; older installations remain visible as detected CLIs until
|
|
227
|
+
they are upgraded, rather than falling back to a permission override that user config could weaken.
|
|
228
|
+
|
|
229
|
+
Codex, Kimi, Hermes, and OpenCode sessions use the same bounded per-task persistence and keyed lease as Claude when they
|
|
213
230
|
run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
|
|
214
231
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
215
232
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
@@ -217,6 +234,10 @@ authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
|
217
234
|
Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
|
|
218
235
|
found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
|
|
219
236
|
adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
|
|
237
|
+
The daemon sends a conservative first hello without waiting for third-party handshakes, then refreshes it
|
|
238
|
+
after the optional Kimi/Hermes/OpenCode probes finish. Work targeting those optional runtimes waits for the
|
|
239
|
+
same full probe result; reconnects share one in-flight probe, and shutdown aborts any unfinished probe and
|
|
240
|
+
its child process.
|
|
220
241
|
Old daemons that omit `executionRuntimes` are conservatively interpreted as the intersection of installed
|
|
221
242
|
CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
|
|
222
243
|
Protocol-0 `agent:start` remains only for the server-governed compatibility window.
|
|
@@ -1,35 +1,126 @@
|
|
|
1
1
|
import { createAgentMemoryClient } from "./client.js";
|
|
2
2
|
import { buildCaptureMessages, extractIncomingMessage, rankMemoryItems, renderMemoryContext, } from "./policy.js";
|
|
3
|
+
import { dslog } from "../slog.js";
|
|
4
|
+
function safeReport(report, eventType, message, fields) {
|
|
5
|
+
try {
|
|
6
|
+
report(eventType, message, fields);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Diagnostics must never affect optional memory recall or capture.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
export function createAgentMemoryBridge(config, dependencies = {}) {
|
|
4
13
|
const client = dependencies.client ?? createAgentMemoryClient(config);
|
|
14
|
+
const report = dependencies.report ?? dslog;
|
|
5
15
|
const blockId = `chat_memory-${config.teamId}-${config.agentId}`;
|
|
6
16
|
return {
|
|
7
|
-
recall: async (agentHandle, wakePrompt) => {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
17
|
+
recall: async (agentHandle, wakePrompt, executionId) => {
|
|
18
|
+
const startedAt = Date.now();
|
|
19
|
+
if (agentHandle !== config.agentHandle) {
|
|
20
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
|
|
21
|
+
execution_id: executionId,
|
|
22
|
+
agent_handle: agentHandle,
|
|
23
|
+
outcome: "skipped",
|
|
24
|
+
skip_reason: "handle_mismatch",
|
|
25
|
+
duration_ms: Date.now() - startedAt,
|
|
26
|
+
});
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
const incoming = extractIncomingMessage(wakePrompt);
|
|
30
|
+
if (incoming === null) {
|
|
31
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
|
|
32
|
+
execution_id: executionId,
|
|
33
|
+
agent_handle: agentHandle,
|
|
34
|
+
outcome: "skipped",
|
|
35
|
+
skip_reason: "wake_unparsed",
|
|
36
|
+
duration_ms: Date.now() - startedAt,
|
|
37
|
+
});
|
|
10
38
|
return "";
|
|
39
|
+
}
|
|
11
40
|
try {
|
|
12
41
|
const [core, atomic] = await Promise.all([
|
|
13
42
|
client.layer(blockId, "L3", 1),
|
|
14
43
|
client.layer(blockId, "L1", config.recallLimit),
|
|
15
44
|
]);
|
|
16
|
-
|
|
45
|
+
const selected = rankMemoryItems(incoming, atomic, config.recallLimit);
|
|
46
|
+
const rendered = renderMemoryContext(core, selected);
|
|
47
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已完成", {
|
|
48
|
+
execution_id: executionId,
|
|
49
|
+
agent_handle: agentHandle,
|
|
50
|
+
outcome: "succeeded",
|
|
51
|
+
core_count: core.length,
|
|
52
|
+
atomic_count: atomic.length,
|
|
53
|
+
selected_atomic_count: selected.length,
|
|
54
|
+
rendered_bytes: Buffer.byteLength(rendered, "utf8"),
|
|
55
|
+
duration_ms: Date.now() - startedAt,
|
|
56
|
+
});
|
|
57
|
+
return rendered;
|
|
17
58
|
}
|
|
18
|
-
catch {
|
|
59
|
+
catch (error) {
|
|
60
|
+
safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 失败并已放行", {
|
|
61
|
+
level: "WARN",
|
|
62
|
+
execution_id: executionId,
|
|
63
|
+
agent_handle: agentHandle,
|
|
64
|
+
outcome: "failed",
|
|
65
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
66
|
+
duration_ms: Date.now() - startedAt,
|
|
67
|
+
});
|
|
19
68
|
return "";
|
|
20
69
|
}
|
|
21
70
|
},
|
|
22
71
|
capture: async (agentHandle, executionId, wakePrompt, finalText) => {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
if (agentHandle !== config.agentHandle) {
|
|
74
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
75
|
+
execution_id: executionId,
|
|
76
|
+
agent_handle: agentHandle,
|
|
77
|
+
outcome: "skipped",
|
|
78
|
+
skip_reason: "handle_mismatch",
|
|
79
|
+
duration_ms: Date.now() - startedAt,
|
|
80
|
+
});
|
|
25
81
|
return;
|
|
82
|
+
}
|
|
83
|
+
const incoming = extractIncomingMessage(wakePrompt);
|
|
84
|
+
if (incoming === null) {
|
|
85
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
86
|
+
execution_id: executionId,
|
|
87
|
+
agent_handle: agentHandle,
|
|
88
|
+
outcome: "skipped",
|
|
89
|
+
skip_reason: "wake_unparsed",
|
|
90
|
+
duration_ms: Date.now() - startedAt,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
26
94
|
const messages = buildCaptureMessages(incoming, finalText);
|
|
27
|
-
if (messages === null)
|
|
95
|
+
if (messages === null) {
|
|
96
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
|
|
97
|
+
execution_id: executionId,
|
|
98
|
+
agent_handle: agentHandle,
|
|
99
|
+
outcome: "skipped",
|
|
100
|
+
skip_reason: "content_filtered",
|
|
101
|
+
duration_ms: Date.now() - startedAt,
|
|
102
|
+
});
|
|
28
103
|
return;
|
|
104
|
+
}
|
|
29
105
|
try {
|
|
30
|
-
await client.importConversation(`nowwork-${executionId}`, messages);
|
|
106
|
+
const result = await client.importConversation(`nowwork-${executionId}`, messages);
|
|
107
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已完成", {
|
|
108
|
+
execution_id: executionId,
|
|
109
|
+
agent_handle: agentHandle,
|
|
110
|
+
outcome: "succeeded",
|
|
111
|
+
accepted_count: result.acceptedCount,
|
|
112
|
+
duration_ms: Date.now() - startedAt,
|
|
113
|
+
});
|
|
31
114
|
}
|
|
32
|
-
catch {
|
|
115
|
+
catch (error) {
|
|
116
|
+
safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 失败并已放行", {
|
|
117
|
+
level: "WARN",
|
|
118
|
+
execution_id: executionId,
|
|
119
|
+
agent_handle: agentHandle,
|
|
120
|
+
outcome: "failed",
|
|
121
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
122
|
+
duration_ms: Date.now() - startedAt,
|
|
123
|
+
});
|
|
33
124
|
// External memory is an optional side effect and cannot alter execution completion.
|
|
34
125
|
}
|
|
35
126
|
},
|
package/dist/computer-service.js
CHANGED
|
@@ -315,8 +315,30 @@ setTimeout(() => {
|
|
|
315
315
|
let restarted;
|
|
316
316
|
if (spec.platform === "darwin") {
|
|
317
317
|
restarted = spawnSync("launchctl", ["kickstart", "-k", spec.target], { stdio: "ignore", shell: false });
|
|
318
|
-
} else {
|
|
318
|
+
} else if (spec.platform === "linux") {
|
|
319
319
|
restarted = spawnSync("systemctl", ["--user", "--no-block", "restart", spec.id], { stdio: "ignore", shell: false });
|
|
320
|
+
} else {
|
|
321
|
+
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
322
|
+
const encodedExecutable = Buffer.from(spec.executable, "utf16le").toString("base64");
|
|
323
|
+
const encodedArguments = Buffer.from(spec.arguments, "utf16le").toString("base64");
|
|
324
|
+
const script = [
|
|
325
|
+
"$ErrorActionPreference='Stop';",
|
|
326
|
+
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
327
|
+
"$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
|
|
328
|
+
"$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
|
|
329
|
+
"$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
|
|
330
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
331
|
+
"$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
|
|
332
|
+
"if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
|
|
333
|
+
"$p.Kill();",
|
|
334
|
+
"if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
|
|
335
|
+
"$deadline=[DateTime]::UtcNow.AddSeconds(10);",
|
|
336
|
+
"do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
|
|
337
|
+
"if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
|
|
338
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
339
|
+
"& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
|
|
340
|
+
].join("");
|
|
341
|
+
restarted = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, String(spec.daemonPid), encodedName, encodedExecutable, encodedArguments, spec.processIdentity], { stdio: "ignore", shell: false });
|
|
320
342
|
}
|
|
321
343
|
if (restarted.status !== 0) process.send?.({ type: "restart-failed", status: restarted.status });
|
|
322
344
|
setTimeout(() => {
|
|
@@ -326,16 +348,27 @@ setTimeout(() => {
|
|
|
326
348
|
}, 250);
|
|
327
349
|
`;
|
|
328
350
|
export function scheduleServiceRestart(spec, spawnDetached = systemDetachedSpawn) {
|
|
329
|
-
if (spec.platform
|
|
330
|
-
throw new Error("Windows detached restart is not supported");
|
|
331
|
-
if (spec.descriptorPath === null)
|
|
351
|
+
if (spec.platform !== "win32" && spec.descriptorPath === null) {
|
|
332
352
|
throw new Error("service descriptor is required for restart");
|
|
353
|
+
}
|
|
333
354
|
const payload = spec.platform === "darwin"
|
|
334
355
|
? {
|
|
335
356
|
platform: spec.platform,
|
|
336
357
|
target: `${spec.managerDomain}/${spec.id}`,
|
|
337
358
|
}
|
|
338
|
-
:
|
|
359
|
+
: spec.platform === "linux"
|
|
360
|
+
? { platform: spec.platform, id: spec.id }
|
|
361
|
+
: {
|
|
362
|
+
platform: spec.platform,
|
|
363
|
+
id: spec.id,
|
|
364
|
+
daemonPid: process.pid,
|
|
365
|
+
executable: spec.daemonCommand[0],
|
|
366
|
+
arguments: spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "),
|
|
367
|
+
processIdentity: spec.runtimeProcessIdentity,
|
|
368
|
+
};
|
|
369
|
+
if (spec.platform === "win32" && !payload.processIdentity) {
|
|
370
|
+
throw new Error("Windows daemon process identity is required for restart");
|
|
371
|
+
}
|
|
339
372
|
const child = spawnDetached(process.execPath, ["-e", RESTART_HELPER_SOURCE, JSON.stringify(payload)], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"], shell: false });
|
|
340
373
|
child.unref();
|
|
341
374
|
let settleFailure;
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
function canonicalPath(path, realpath) {
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
function canonicalPath(path, platform, realpath) {
|
|
4
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
4
5
|
try {
|
|
5
6
|
return realpath(path);
|
|
6
7
|
}
|
|
7
8
|
catch (error) {
|
|
8
9
|
const code = error.code;
|
|
9
|
-
return code === "ENOENT" || code === "ENOTDIR" ? resolve(path) : null;
|
|
10
|
+
return code === "ENOENT" || code === "ENOTDIR" ? paths.resolve(path) : null;
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
function samePath(left, right, platform) {
|
|
14
|
+
return platform === "win32"
|
|
15
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
16
|
+
: left === right;
|
|
17
|
+
}
|
|
12
18
|
export function daemonGlobalInstallation(entryPath, platform, realpath = realpathSync.native) {
|
|
13
|
-
if (platform !== "darwin" && platform !== "linux")
|
|
19
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32")
|
|
20
|
+
return null;
|
|
21
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
22
|
+
const packageRoot = paths.resolve(paths.dirname(entryPath), "..");
|
|
23
|
+
const globalNodeModules = paths.resolve(packageRoot, "..", "..");
|
|
24
|
+
const modulesParent = paths.dirname(globalNodeModules);
|
|
25
|
+
if (paths.basename(globalNodeModules).toLowerCase() !== "node_modules")
|
|
14
26
|
return null;
|
|
15
|
-
|
|
16
|
-
const globalNodeModules = resolve(packageRoot, "..", "..");
|
|
17
|
-
const libDirectory = dirname(globalNodeModules);
|
|
18
|
-
if (basename(globalNodeModules) !== "node_modules" || basename(libDirectory) !== "lib")
|
|
27
|
+
if (platform !== "win32" && paths.basename(modulesParent) !== "lib")
|
|
19
28
|
return null;
|
|
20
|
-
if (resolve(entryPath)
|
|
29
|
+
if (!samePath(paths.resolve(entryPath), paths.resolve(packageRoot, "dist", "main.js"), platform))
|
|
21
30
|
return null;
|
|
22
|
-
if (resolve(packageRoot)
|
|
31
|
+
if (!samePath(paths.resolve(packageRoot), paths.resolve(globalNodeModules, "@nowcrew", "daemon"), platform))
|
|
23
32
|
return null;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
33
|
+
const npmPrefix = platform === "win32" ? modulesParent : paths.dirname(modulesParent);
|
|
34
|
+
const canonicalPackageRoot = canonicalPath(packageRoot, platform, realpath);
|
|
35
|
+
const canonicalGlobalNodeModules = canonicalPath(globalNodeModules, platform, realpath);
|
|
36
|
+
const canonicalNpmPrefix = canonicalPath(npmPrefix, platform, realpath);
|
|
27
37
|
if (canonicalPackageRoot === null || canonicalGlobalNodeModules === null || canonicalNpmPrefix === null)
|
|
28
38
|
return null;
|
|
29
|
-
|
|
39
|
+
const normalizeCanonical = (path) => paths.resolve(path);
|
|
40
|
+
const normalizedPackageRoot = normalizeCanonical(canonicalPackageRoot);
|
|
41
|
+
const normalizedGlobalNodeModules = normalizeCanonical(canonicalGlobalNodeModules);
|
|
42
|
+
const normalizedNpmPrefix = normalizeCanonical(canonicalNpmPrefix);
|
|
43
|
+
if (!samePath(normalizedPackageRoot, paths.resolve(normalizedGlobalNodeModules, "@nowcrew", "daemon"), platform))
|
|
30
44
|
return null;
|
|
31
|
-
|
|
45
|
+
const expectedModules = platform === "win32"
|
|
46
|
+
? paths.resolve(normalizedNpmPrefix, "node_modules")
|
|
47
|
+
: paths.resolve(normalizedNpmPrefix, "lib", "node_modules");
|
|
48
|
+
if (!samePath(normalizedGlobalNodeModules, expectedModules, platform))
|
|
32
49
|
return null;
|
|
33
50
|
return {
|
|
34
|
-
packageRoot:
|
|
35
|
-
globalNodeModules:
|
|
36
|
-
npmPrefix:
|
|
51
|
+
packageRoot: normalizedPackageRoot,
|
|
52
|
+
globalNodeModules: normalizedGlobalNodeModules,
|
|
53
|
+
npmPrefix: normalizedNpmPrefix,
|
|
37
54
|
};
|
|
38
55
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { daemonHome, loadProfile, resolveAgentsRoot } from "./computer-profile.js";
|
|
5
6
|
import { builtDaemonEntry } from "./computer-cli.js";
|
|
6
7
|
import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
|
|
7
8
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
9
|
import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
|
|
10
|
+
import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
|
|
11
|
+
import { inspectWindowsServiceUpdateScope } from "./windows-scheduled-task.js";
|
|
9
12
|
import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
|
|
10
13
|
function defaults() {
|
|
11
14
|
return {
|
|
@@ -22,16 +25,35 @@ function defaults() {
|
|
|
22
25
|
serviceStatus,
|
|
23
26
|
assertWritable: (path) => access(path, constants.W_OK),
|
|
24
27
|
installationLeaseHeld: daemonInstallationLeaseHeld,
|
|
28
|
+
installationRealpath: realpathSync.native,
|
|
29
|
+
inspectWindowsServiceUpdateScope,
|
|
30
|
+
inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
|
|
25
31
|
};
|
|
26
32
|
}
|
|
33
|
+
async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
|
|
34
|
+
for (const descriptorPath of descriptorPaths) {
|
|
35
|
+
if (descriptorPath === targetDescriptorPath)
|
|
36
|
+
continue;
|
|
37
|
+
try {
|
|
38
|
+
assertRegistryCoversDescriptors(services, [descriptorPath]);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
const legacy = await inspectLegacyDescriptorInstallation(descriptorPath).catch(() => null);
|
|
43
|
+
if (legacy === null || legacy.npmPrefix === npmPrefix)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
27
49
|
export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
|
|
28
50
|
const deps = { ...defaults(), ...overrides };
|
|
29
|
-
if (deps.platform !== "darwin" && deps.platform !== "linux") {
|
|
51
|
+
if (deps.platform !== "darwin" && deps.platform !== "linux" && deps.platform !== "win32") {
|
|
30
52
|
return { eligible: false, reason: "unsupported_platform" };
|
|
31
53
|
}
|
|
32
54
|
if (!profileName)
|
|
33
55
|
return { eligible: false, reason: "profile_required" };
|
|
34
|
-
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
|
|
56
|
+
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform, deps.installationRealpath);
|
|
35
57
|
if (installation === null)
|
|
36
58
|
return { eligible: false, reason: "global_install_required" };
|
|
37
59
|
let profile;
|
|
@@ -53,6 +75,33 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
53
75
|
entryPath: deps.entryPath,
|
|
54
76
|
profileHome: deps.profileHome,
|
|
55
77
|
});
|
|
78
|
+
if (deps.platform === "win32") {
|
|
79
|
+
const scope = await deps.inspectWindowsServiceUpdateScope(spec, installation.npmPrefix)
|
|
80
|
+
.catch(() => ({ valid: false }));
|
|
81
|
+
if (!scope.valid) {
|
|
82
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
83
|
+
}
|
|
84
|
+
if (!scope.running)
|
|
85
|
+
return { eligible: false, reason: "service_not_running" };
|
|
86
|
+
if (!scope.processIdentity) {
|
|
87
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
88
|
+
}
|
|
89
|
+
if (!deps.installationLeaseHeld(installation.npmPrefix)) {
|
|
90
|
+
return { eligible: false, reason: "installation_lease_missing" };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await deps.assertWritable(installation.globalNodeModules);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { eligible: false, reason: "global_root_not_writable" };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
eligible: true,
|
|
100
|
+
profileName,
|
|
101
|
+
...installation,
|
|
102
|
+
serviceSpec: { ...spec, runtimeProcessIdentity: scope.processIdentity },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
56
105
|
let selectedSpec = spec;
|
|
57
106
|
let status = await deps.serviceStatus(spec);
|
|
58
107
|
let services;
|
|
@@ -65,10 +114,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
65
114
|
const registered = services.find((record) => record.serviceId === spec.id
|
|
66
115
|
|| (record.daemonHome === deps.profileHome && record.profile === profileName));
|
|
67
116
|
if (registered !== undefined) {
|
|
68
|
-
|
|
69
|
-
assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
117
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), spec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
72
118
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
73
119
|
}
|
|
74
120
|
const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
|
|
@@ -109,12 +155,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
109
155
|
if (legacyDescriptor !== legacySpec.descriptor) {
|
|
110
156
|
return { eligible: false, reason: "managed_service_not_registered" };
|
|
111
157
|
}
|
|
112
|
-
|
|
113
|
-
const descriptorPaths = await deps.listManagedDescriptorPaths();
|
|
114
|
-
const targetPath = legacySpec.descriptorPath;
|
|
115
|
-
assertRegistryCoversDescriptors(services, descriptorPaths.filter((descriptorPath) => descriptorPath !== targetPath));
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
158
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), legacySpec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
118
159
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
119
160
|
}
|
|
120
161
|
status = await deps.serviceStatus(legacySpec);
|
package/dist/daemon-updater.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function installExactDaemonUpdate(input) {
|
|
|
48
48
|
await release();
|
|
49
49
|
return { ok: false, errorCode: "install_failed" };
|
|
50
50
|
}
|
|
51
|
-
const result = await runner(process.platform === "win32" ? "npm.cmd" : "npm", [
|
|
51
|
+
const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
|
|
52
52
|
"install",
|
|
53
53
|
"--global",
|
|
54
54
|
"--prefix",
|
|
@@ -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"]);
|
|
10
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
|
|
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"]);
|
|
11
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
|
|
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 !== "."
|
|
@@ -22,16 +22,8 @@ const ProjectSkillRefSchema = z.object({
|
|
|
22
22
|
projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
|
|
23
23
|
skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
|
|
24
24
|
}).strict();
|
|
25
|
-
export const ReasoningSchema = z.
|
|
26
|
-
"
|
|
27
|
-
"none",
|
|
28
|
-
"minimal",
|
|
29
|
-
"low",
|
|
30
|
-
"medium",
|
|
31
|
-
"high",
|
|
32
|
-
"xhigh",
|
|
33
|
-
"max",
|
|
34
|
-
]);
|
|
25
|
+
export const ReasoningSchema = z.string().min(1).max(64)
|
|
26
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u, "invalid runtime reasoning token");
|
|
35
27
|
export const PermissionSchema = z.enum([
|
|
36
28
|
"local_default",
|
|
37
29
|
"sandboxed",
|
package/dist/execution-runner.js
CHANGED
|
@@ -169,7 +169,10 @@ function validReasoning(spec) {
|
|
|
169
169
|
if (spec.runtime.name === "codex") {
|
|
170
170
|
return CODEX_EFFORT_LEVELS.includes(reasoning);
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
if (spec.runtime.name === "kimi") {
|
|
173
|
+
return KIMI_EFFORT_LEVELS.includes(reasoning);
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
173
176
|
}
|
|
174
177
|
function launchProviderConfig(config) {
|
|
175
178
|
if (config === undefined)
|
|
@@ -241,8 +244,14 @@ function admission(spec, config, dependencies, at) {
|
|
|
241
244
|
return { rejected: rejection(spec.executionId, "resource_limit", "Local execution capacity is exhausted", at) };
|
|
242
245
|
}
|
|
243
246
|
const permission = effectivePermission(spec, config);
|
|
244
|
-
if (spec.runtime.name === "kimi"
|
|
245
|
-
|
|
247
|
+
if ((spec.runtime.name === "kimi" || spec.runtime.name === "hermes" || spec.runtime.name === "opencode")
|
|
248
|
+
&& permission !== "full_access") {
|
|
249
|
+
const displayName = spec.runtime.name === "kimi"
|
|
250
|
+
? "Kimi"
|
|
251
|
+
: spec.runtime.name === "hermes" ? "Hermes" : "OpenCode";
|
|
252
|
+
return {
|
|
253
|
+
rejected: rejection(spec.executionId, "local_policy_denied", `${displayName} cannot enforce ${permission} permission`, at),
|
|
254
|
+
};
|
|
246
255
|
}
|
|
247
256
|
return { spec, permission };
|
|
248
257
|
}
|
|
@@ -404,7 +413,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
404
413
|
let recalledMemory = "";
|
|
405
414
|
if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
|
|
406
415
|
try {
|
|
407
|
-
recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt), dependencies.cancellation);
|
|
416
|
+
recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt, spec.executionId), dependencies.cancellation);
|
|
408
417
|
}
|
|
409
418
|
catch (error) {
|
|
410
419
|
if (error instanceof ExecutionCancelledError)
|
package/dist/external-output.js
CHANGED
|
@@ -62,6 +62,10 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
|
62
62
|
? decoder.push(candidate.item.text)
|
|
63
63
|
: [];
|
|
64
64
|
}
|
|
65
|
+
if ((runtime === "hermes" && candidate.type === "hermes.acp.text_delta")
|
|
66
|
+
|| (runtime === "opencode" && candidate.type === "opencode.text_delta")) {
|
|
67
|
+
return typeof candidate.text === "string" ? decoder.push(candidate.text) : [];
|
|
68
|
+
}
|
|
65
69
|
return candidate.type === undefined
|
|
66
70
|
&& candidate.role === "assistant"
|
|
67
71
|
&& typeof candidate.content === "string"
|