@nowcrew/daemon 0.5.13 → 0.5.15

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.
@@ -0,0 +1,358 @@
1
+ import { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
5
+ import { promisify } from "node:util";
6
+ import { dirname, resolve } from "node:path";
7
+ const execFileAsync = promisify(execFile);
8
+ const WINDOWS_STOP_TIMEOUT_MS = 5_000;
9
+ const WINDOWS_STOP_POLL_MS = 100;
10
+ export const systemCommandRunner = async (command, args) => {
11
+ try {
12
+ const result = await execFileAsync(command, [...args], { encoding: "utf8" });
13
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
14
+ }
15
+ catch (error) {
16
+ const failure = error;
17
+ return {
18
+ exitCode: typeof failure.code === "number" ? failure.code : 127,
19
+ stdout: failure.stdout ?? "",
20
+ stderr: failure.stderr ?? failure.message,
21
+ };
22
+ }
23
+ };
24
+ function assertSingleLine(value, field) {
25
+ if (value.includes("\n") || value.includes("\r") || value.includes("\0")) {
26
+ throw new Error(`${field} must not contain control characters`);
27
+ }
28
+ return value;
29
+ }
30
+ function xml(value) {
31
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
32
+ .replaceAll('"', "&quot;").replaceAll("'", "&apos;");
33
+ }
34
+ function systemdArg(value) {
35
+ return `"${assertSingleLine(value, "service argument").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
36
+ }
37
+ export function windowsCommandArg(value) {
38
+ const input = assertSingleLine(value, "service argument");
39
+ return `"${input.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\*)$/g, "$1$1")}"`;
40
+ }
41
+ export function buildServiceSpec(input) {
42
+ const { platform, profile, userHome } = input;
43
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
44
+ throw new Error(`Computer services are not supported on platform '${platform}'`);
45
+ }
46
+ const daemonCommand = [
47
+ assertSingleLine(input.nodePath, "node path"),
48
+ assertSingleLine(input.entryPath, "daemon entry path"),
49
+ "serve",
50
+ "--profile",
51
+ assertSingleLine(profile, "profile"),
52
+ ...(input.profileHome ? ["--daemon-home", assertSingleLine(input.profileHome, "daemon home")] : []),
53
+ ];
54
+ if (platform === "darwin") {
55
+ if (input.uid === undefined)
56
+ throw new Error("Cannot install launchd service without a numeric uid");
57
+ const id = `com.nowcrew.daemon.${profile}`;
58
+ const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
59
+ return {
60
+ platform,
61
+ profile,
62
+ id,
63
+ descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
64
+ daemonCommand,
65
+ managerDomain: `gui/${input.uid}`,
66
+ descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
67
+ };
68
+ }
69
+ if (platform === "linux") {
70
+ const id = `nowcrew-daemon-${profile}.service`;
71
+ return {
72
+ platform,
73
+ profile,
74
+ id,
75
+ descriptorPath: resolve(userHome, ".config", "systemd", "user", id),
76
+ daemonCommand,
77
+ managerDomain: null,
78
+ descriptor: `[Unit]\nDescription=NowCrew daemon (${profile})\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${daemonCommand.map(systemdArg).join(" ")}\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`,
79
+ };
80
+ }
81
+ const id = `NowCrew Daemon (${profile})`;
82
+ return { platform, profile, id, descriptorPath: null, descriptor: null, daemonCommand, managerDomain: null };
83
+ }
84
+ async function pathExists(path) {
85
+ try {
86
+ await access(path, constants.F_OK);
87
+ return true;
88
+ }
89
+ catch (error) {
90
+ if (error.code === "ENOENT")
91
+ return false;
92
+ throw error;
93
+ }
94
+ }
95
+ async function writePrivateAtomic(path, content) {
96
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
97
+ await chmod(dirname(path), 0o700);
98
+ const temporary = `${path}.${randomUUID()}.tmp`;
99
+ try {
100
+ await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
101
+ await rename(temporary, path);
102
+ await chmod(path, 0o600);
103
+ }
104
+ finally {
105
+ await rm(temporary, { force: true });
106
+ }
107
+ }
108
+ async function checked(runner, command, args, action) {
109
+ const result = await runner(command, args);
110
+ if (result.exitCode !== 0) {
111
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`;
112
+ throw new Error(`${action} failed: ${detail}`);
113
+ }
114
+ return result;
115
+ }
116
+ async function windowsTaskState(spec, runner) {
117
+ const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
118
+ const script = [
119
+ "$ErrorActionPreference='Stop';",
120
+ "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
121
+ "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
122
+ "if($tasks.Count -eq 0){'missing'}elseif($tasks.Count -eq 1){'state:'+([int]$tasks[0].State)}else{throw 'duplicate scheduled tasks'}",
123
+ ].join("");
124
+ const result = await runner("powershell.exe", [
125
+ "-NoProfile", "-NonInteractive", "-Command", script, encodedName,
126
+ ]);
127
+ if (result.exitCode !== 0) {
128
+ throw new Error(`Unable to query scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
129
+ }
130
+ const value = result.stdout.trim();
131
+ if (value === "missing")
132
+ return { installed: false, state: null, result };
133
+ const match = /^state:([0-4])$/.exec(value);
134
+ if (!match)
135
+ throw new Error(`Scheduled task returned an unknown state '${value}'`);
136
+ return { installed: true, state: Number(match[1]), result };
137
+ }
138
+ async function windowsTaskExists(spec, runner) {
139
+ return (await windowsTaskState(spec, runner)).installed;
140
+ }
141
+ async function stopWindowsTask(spec, initial, runner) {
142
+ if (initial.state !== 2 && initial.state !== 4)
143
+ return initial.installed;
144
+ await checked(runner, "schtasks.exe", ["/End", "/TN", spec.id], "Scheduled task stop");
145
+ const deadline = Date.now() + WINDOWS_STOP_TIMEOUT_MS;
146
+ while (true) {
147
+ const current = await windowsTaskState(spec, runner);
148
+ if (!current.installed)
149
+ return false;
150
+ if (current.state !== 2 && current.state !== 4)
151
+ return true;
152
+ if (Date.now() >= deadline) {
153
+ throw new Error(`Scheduled task '${spec.id}' did not stop within ${WINDOWS_STOP_TIMEOUT_MS}ms`);
154
+ }
155
+ await new Promise((resolve) => setTimeout(resolve, WINDOWS_STOP_POLL_MS));
156
+ }
157
+ }
158
+ export async function installService(spec, runner = systemCommandRunner) {
159
+ if (spec.platform === "win32") {
160
+ if (await windowsTaskExists(spec, runner))
161
+ throw new Error(`Service '${spec.id}' is already installed`);
162
+ const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
163
+ const encodedExecutable = Buffer.from(spec.daemonCommand[0], "utf16le").toString("base64");
164
+ const encodedArguments = Buffer.from(spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "), "utf16le").toString("base64");
165
+ const script = [
166
+ "$ErrorActionPreference='Stop';",
167
+ "$name=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
168
+ "$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
169
+ "$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
170
+ "$identity=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
171
+ "$action=New-ScheduledTaskAction -Execute $exe -Argument $argv;",
172
+ "$trigger=New-ScheduledTaskTrigger -AtLogOn -User $identity;",
173
+ "$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1);",
174
+ "$principal=New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited;",
175
+ "Register-ScheduledTask -TaskName $name -TaskPath '\\' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -ErrorAction Stop|Out-Null;",
176
+ ].join("");
177
+ await checked(runner, "powershell.exe", [
178
+ "-NoProfile", "-NonInteractive", "-Command", script,
179
+ encodedName, encodedExecutable, encodedArguments,
180
+ ], "Scheduled task installation");
181
+ return;
182
+ }
183
+ if (spec.descriptorPath === null || spec.descriptor === null)
184
+ throw new Error("Missing service descriptor");
185
+ if (await pathExists(spec.descriptorPath))
186
+ throw new Error(`Service '${spec.id}' is already installed`);
187
+ await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
188
+ try {
189
+ if (spec.platform === "linux") {
190
+ await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
191
+ await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
192
+ }
193
+ }
194
+ catch (error) {
195
+ await rm(spec.descriptorPath, { force: true });
196
+ if (spec.platform === "linux") {
197
+ await runner("systemctl", ["--user", "disable", spec.id]);
198
+ await runner("systemctl", ["--user", "daemon-reload"]);
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+ export async function uninstallService(spec, runner = systemCommandRunner) {
204
+ if (spec.platform === "win32") {
205
+ const task = await windowsTaskState(spec, runner);
206
+ if (!task.installed)
207
+ throw new Error(`Service '${spec.id}' is not installed`);
208
+ if (!await stopWindowsTask(spec, task, runner))
209
+ return;
210
+ await checked(runner, "schtasks.exe", ["/Delete", "/TN", spec.id, "/F"], "Scheduled task removal");
211
+ return;
212
+ }
213
+ if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
214
+ throw new Error(`Service '${spec.id}' is not installed`);
215
+ }
216
+ if (spec.platform === "darwin") {
217
+ const status = await serviceStatus(spec, runner);
218
+ if (status.loaded) {
219
+ await checked(runner, "launchctl", ["bootout", `${spec.managerDomain}/${spec.id}`], "launchd bootout");
220
+ }
221
+ }
222
+ else {
223
+ const disable = await runner("systemctl", ["--user", "disable", "--now", spec.id]);
224
+ if (disable.exitCode !== 0 && !/not loaded|does not exist|not found/i.test(`${disable.stdout}\n${disable.stderr}`)) {
225
+ throw new Error(`systemd disable failed: ${disable.stderr.trim() || disable.stdout.trim()}`);
226
+ }
227
+ }
228
+ await rm(spec.descriptorPath);
229
+ if (spec.platform === "linux") {
230
+ await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
231
+ }
232
+ }
233
+ export async function serviceAction(spec, action, runner = systemCommandRunner) {
234
+ if (spec.platform !== "win32" && (spec.descriptorPath === null || !await pathExists(spec.descriptorPath))) {
235
+ throw new Error(`Service '${spec.id}' is not installed`);
236
+ }
237
+ if (spec.platform === "darwin") {
238
+ const target = `${spec.managerDomain}/${spec.id}`;
239
+ const status = await serviceStatus(spec, runner);
240
+ if (action === "start") {
241
+ if (status.loaded)
242
+ await checked(runner, "launchctl", ["kickstart", "-k", target], "launchd start");
243
+ else
244
+ await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
245
+ }
246
+ else if (action === "stop") {
247
+ if (status.loaded)
248
+ await checked(runner, "launchctl", ["bootout", target], "launchd stop");
249
+ }
250
+ else {
251
+ if (status.loaded)
252
+ await checked(runner, "launchctl", ["bootout", target], "launchd stop");
253
+ await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
254
+ }
255
+ }
256
+ else if (spec.platform === "linux") {
257
+ await checked(runner, "systemctl", ["--user", action, spec.id], `systemd ${action}`);
258
+ }
259
+ else {
260
+ const task = await windowsTaskState(spec, runner);
261
+ if (!task.installed)
262
+ throw new Error(`Service '${spec.id}' is not installed`);
263
+ if (action === "start")
264
+ await checked(runner, "schtasks.exe", ["/Run", "/TN", spec.id], "Scheduled task start");
265
+ else if (action === "stop") {
266
+ await stopWindowsTask(spec, task, runner);
267
+ }
268
+ else {
269
+ if (!await stopWindowsTask(spec, task, runner)) {
270
+ throw new Error(`Service '${spec.id}' disappeared while restarting`);
271
+ }
272
+ await checked(runner, "schtasks.exe", ["/Run", "/TN", spec.id], "Scheduled task start");
273
+ }
274
+ }
275
+ }
276
+ export async function serviceStatus(spec, runner = systemCommandRunner) {
277
+ if (spec.platform === "win32") {
278
+ const task = await windowsTaskState(spec, runner);
279
+ return {
280
+ ...task.result,
281
+ installed: task.installed,
282
+ loaded: task.installed,
283
+ running: task.state === 4,
284
+ };
285
+ }
286
+ if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
287
+ return { exitCode: 3, stdout: "", stderr: "service descriptor not found", installed: false, loaded: false, running: false };
288
+ }
289
+ const result = spec.platform === "darwin"
290
+ ? await runner("launchctl", ["print", `${spec.managerDomain}/${spec.id}`])
291
+ : await runner("systemctl", ["--user", "is-active", spec.id]);
292
+ const running = spec.platform === "darwin"
293
+ ? result.exitCode === 0 && /\bstate\s*=\s*running\b/.test(result.stdout)
294
+ : result.exitCode === 0;
295
+ return { ...result, installed: true, loaded: spec.platform === "darwin" ? result.exitCode === 0 : true, running };
296
+ }
297
+ export async function doctorService(spec, profilePrivate, runner = systemCommandRunner) {
298
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
299
+ const tool = spec.platform === "darwin" ? "launchctl" : spec.platform === "linux" ? "systemctl" : "schtasks.exe";
300
+ const probeArgs = spec.platform === "linux" ? ["--user", "--version"] : spec.platform === "darwin" ? ["help"] : ["/Query", "/?"];
301
+ const probe = await runner(tool, probeArgs);
302
+ const status = await serviceStatus(spec, runner);
303
+ return [
304
+ { name: "node", ok: nodeMajor >= 22, detail: `Node ${process.versions.node} (requires >=22)` },
305
+ { name: "profile-permissions", ok: profilePrivate, detail: profilePrivate ? "private" : "readable by other users" },
306
+ { name: "service-manager", ok: probe.exitCode === 0, detail: probe.exitCode === 0 ? tool : (probe.stderr.trim() || `${tool} unavailable`) },
307
+ { name: "service-installed", ok: status.installed, detail: status.installed ? spec.id : "not installed" },
308
+ { name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
309
+ ];
310
+ }
311
+ export function buildUpgradeCommand(platform) {
312
+ return [platform === "win32" ? "npm.cmd" : "npm", ["install", "--global", "@nowcrew/daemon@latest"]];
313
+ }
314
+ export async function upgradeDaemon(platform, runner = systemCommandRunner) {
315
+ const [command, args] = buildUpgradeCommand(platform);
316
+ await checked(runner, command, args, "Daemon upgrade");
317
+ }
318
+ const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
319
+ export async function hardenWindowsProfile(path, runner = systemCommandRunner) {
320
+ const identity = await checked(runner, "powershell.exe", [
321
+ "-NoProfile", "-NonInteractive", "-Command", WINDOWS_SID_SCRIPT,
322
+ ], "Windows identity lookup");
323
+ const sid = identity.stdout.trim();
324
+ if (!/^S-1-(?:\d+-)+\d+$/.test(sid))
325
+ throw new Error("Windows identity lookup returned an invalid SID");
326
+ await checked(runner, "icacls.exe", [path, "/inheritance:r", "/grant:r", `*${sid}:(F)`], "Profile ACL hardening");
327
+ }
328
+ export async function windowsProfileIsPrivate(path, runner = systemCommandRunner) {
329
+ const encodedPath = Buffer.from(path, "utf16le").toString("base64");
330
+ const script = [
331
+ "$p=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
332
+ "$me=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;",
333
+ "$safe=@($me,'S-1-5-18','S-1-5-32-544');",
334
+ "$rules=(Get-Acl -LiteralPath $p).Access;",
335
+ "$unsafe=$rules|Where-Object { $_.AccessControlType -eq 'Allow' -and $safe -notcontains $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value };",
336
+ "if($unsafe){'false'}else{'true'}",
337
+ ].join("");
338
+ const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, encodedPath]);
339
+ if (result.exitCode !== 0)
340
+ throw new Error(`Profile ACL inspection failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
341
+ if (result.stdout.trim() === "true")
342
+ return true;
343
+ if (result.stdout.trim() === "false")
344
+ return false;
345
+ throw new Error("Profile ACL inspection returned an invalid result");
346
+ }
347
+ export async function readServiceDescriptor(spec) {
348
+ if (spec.descriptorPath === null)
349
+ return null;
350
+ try {
351
+ return await readFile(spec.descriptorPath, "utf8");
352
+ }
353
+ catch (error) {
354
+ if (error.code === "ENOENT")
355
+ return null;
356
+ throw error;
357
+ }
358
+ }
package/dist/config.js CHANGED
@@ -47,8 +47,9 @@ function defaultCliPath() {
47
47
  return resolve(here, "../../cli/dist/main.js");
48
48
  }
49
49
  }
50
+ export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
50
51
  export function loadConfig(env = process.env) {
51
- const serverUrl = (env.CREW_SERVER_URL ?? "http://127.0.0.1:3000").replace(/\/+$/, "");
52
+ const serverUrl = (env.CREW_SERVER_URL ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
52
53
  const machineToken = env.CREW_MACHINE_TOKEN ?? "";
53
54
  if (!machineToken) {
54
55
  const lang = detectDaemonLang(env);
package/dist/console.js CHANGED
@@ -58,6 +58,16 @@ export function toConsoleLines(event) {
58
58
  const e = (event ?? {});
59
59
  const lang = detectDaemonLang();
60
60
  const td = (message) => translateDaemon(lang, message);
61
+ if (e.type === "kimi.acp.text_delta" && e.text) {
62
+ return [{ stream: "text", text: e.text }];
63
+ }
64
+ if (e.type === "kimi.acp.tool_call") {
65
+ return [{ stream: "tool", text: e.title ? `⏺ ${e.title}` : "⏺ Kimi tool" }];
66
+ }
67
+ if (e.type === "kimi.acp.tool_result") {
68
+ const text = typeof e.content === "string" ? e.content.trim() : "";
69
+ return text ? [{ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
70
+ }
61
71
  if (e.type === "system" && e.subtype === "init") {
62
72
  return [{ stream: "system", text: `● ${td("Claude session started")}` }];
63
73
  }
@@ -0,0 +1,11 @@
1
+ /** Durable execution needs ownership that survives daemon crashes, not only a best-effort kill. */
2
+ export function executionBackendCapability(platform = process.platform) {
3
+ if (platform === "win32") {
4
+ return {
5
+ supported: false,
6
+ backend: "windows-job-object-unavailable",
7
+ reason: "protocol-v1 is disabled until a Windows Job Object backend owns every runtime process",
8
+ };
9
+ }
10
+ return { supported: true, backend: "posix-process-group" };
11
+ }
@@ -136,6 +136,13 @@ export const ExecutionCompletionAckSchema = z.object({
136
136
  protocolVersion: ProtocolVersionSchema,
137
137
  executionId: ExecutionIdSchema,
138
138
  }).strict();
139
+ export const ExecutionEventAckSchema = z.object({
140
+ type: z.literal("execution:event-ack"),
141
+ protocolVersion: ProtocolVersionSchema,
142
+ executionId: ExecutionIdSchema,
143
+ kind: z.enum(["activity", "console"]),
144
+ seq: SequenceSchema,
145
+ }).strict();
139
146
  export const ExecutionAcceptedSchema = z.object({
140
147
  type: z.literal("execution:accepted"),
141
148
  protocolVersion: ProtocolVersionSchema,
@@ -294,6 +301,7 @@ export const ServerToDaemonExecutionFrameSchema = z.discriminatedUnion("type", [
294
301
  ExecutionCancelSchema,
295
302
  ExecutionSyncSchema,
296
303
  ExecutionCompletionAckSchema,
304
+ ExecutionEventAckSchema,
297
305
  ]);
298
306
  const RawDaemonToServerExecutionFrameSchema = z.discriminatedUnion("type", [
299
307
  ExecutionAcceptedSchema,
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
3
4
  import { JournalConflictError } from "./execution-journal.js";
4
5
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -7,7 +8,8 @@ import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
7
8
  import { startDormantSupervisor, } from "./execution-supervisor.js";
8
9
  import { buildClaudeArgs, CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
9
10
  import { buildCodexArgs, CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
10
- import { buildKimiArgs, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
11
+ import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
12
+ import { executionBackendCapability } from "./execution-backend.js";
11
13
  const ACTIVITY_KIND = {
12
14
  init: "working",
13
15
  text: "thinking",
@@ -38,7 +40,7 @@ async function reportBestEffort(report, frame, timeoutMs) {
38
40
  let timeout;
39
41
  try {
40
42
  return await Promise.race([
41
- Promise.resolve(report(frame)).then(() => true, () => true),
43
+ Promise.resolve(report(frame)).then(() => true, () => false),
42
44
  new Promise((resolve) => {
43
45
  timeout = setTimeout(() => resolve(false), timeoutMs);
44
46
  }),
@@ -214,15 +216,19 @@ function supervisorLaunch(request) {
214
216
  stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
215
217
  };
216
218
  }
219
+ if (request.effectivePermission !== "full_access") {
220
+ throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
221
+ }
217
222
  return {
218
- command: request.bin,
219
- args: buildKimiArgs({
220
- wakePrompt: `${request.systemPrompt}\n\n${request.wakePrompt}`,
221
- effectivePermission: request.effectivePermission,
222
- ...(request.model === undefined ? {} : { model: request.model }),
223
- }),
223
+ command: process.execPath,
224
+ args: [
225
+ fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
226
+ "--bin", request.bin,
227
+ ...(request.model === undefined ? [] : ["--model", request.model]),
228
+ ],
224
229
  cwd: request.cwd,
225
230
  env: request.env,
231
+ stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
226
232
  };
227
233
  }
228
234
  function rejection(executionId, reason, message, at) {
@@ -236,8 +242,9 @@ function rejection(executionId, reason, message, at) {
236
242
  });
237
243
  }
238
244
  function admission(spec, config, dependencies, at) {
239
- if (dependencies.platform === "win32" || (dependencies.platform === undefined && process.platform === "win32")) {
240
- return { rejected: rejection(spec.executionId, "capability_missing", "Execution supervisor is unavailable on Windows", at) };
245
+ const backend = executionBackendCapability(dependencies.platform ?? process.platform);
246
+ if (!backend.supported) {
247
+ return { rejected: rejection(spec.executionId, "capability_missing", backend.reason, at) };
241
248
  }
242
249
  const promptBytes = Buffer.byteLength(spec.instructions.systemPrompt, "utf8")
243
250
  + Buffer.byteLength(spec.instructions.wakePrompt, "utf8");
@@ -1,8 +1,10 @@
1
1
  import { fork } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { executionBackendCapability } from "./execution-backend.js";
3
4
  const DEFAULT_ABORT_TIMEOUT_MS = 5_000;
4
5
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
5
6
  const DEFAULT_TASKKILL_TIMEOUT_MS = 5_000;
7
+ const PROCESS_GROUP_POLL_MS = 10;
6
8
  function messageError(error) {
7
9
  return error instanceof Error ? error : new Error(String(error));
8
10
  }
@@ -21,6 +23,27 @@ async function waitForExit(exit, timeoutMs, pid) {
21
23
  clearTimeout(timer);
22
24
  }
23
25
  }
26
+ async function waitForProcessGroupExit(pid, timeoutMs) {
27
+ const deadline = Date.now() + timeoutMs;
28
+ while (true) {
29
+ try {
30
+ process.kill(-pid, 0);
31
+ }
32
+ catch (error) {
33
+ const code = error.code;
34
+ if (code === "ESRCH")
35
+ return;
36
+ // POSIX defines EPERM here as "the process group exists, but is not signalable".
37
+ // It is therefore an alive observation, not a completed cleanup or an API failure.
38
+ if (code !== "EPERM")
39
+ throw error;
40
+ }
41
+ if (Date.now() >= deadline) {
42
+ throw new Error(`Supervisor process group ${pid} did not exit within ${timeoutMs}ms`);
43
+ }
44
+ await new Promise((resolve) => setTimeout(resolve, PROCESS_GROUP_POLL_MS));
45
+ }
46
+ }
24
47
  async function withTimeout(promise, timeoutMs, phase) {
25
48
  let timer;
26
49
  try {
@@ -63,9 +86,9 @@ export async function signalSupervisorTree(pid, signal, platform = process.platf
63
86
  }
64
87
  export async function startDormantSupervisor(launch, options = {}) {
65
88
  const platform = options.platform ?? process.platform;
66
- if (platform === "win32") {
67
- throw new Error("Execution supervisor is unsupported on Windows without Job Object ownership");
68
- }
89
+ const backend = executionBackendCapability(platform);
90
+ if (!backend.supported)
91
+ throw new Error(backend.reason);
69
92
  const childEntry = options.childEntry
70
93
  ?? fileURLToPath(new URL("./execution-supervisor-child.js", import.meta.url));
71
94
  const abortTimeoutMs = options.abortTimeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
@@ -87,7 +110,7 @@ export async function startDormantSupervisor(launch, options = {}) {
87
110
  }
88
111
  let runtimeResult;
89
112
  let supervisorSpawnError;
90
- const exit = new Promise((resolve) => {
113
+ const supervisorExit = new Promise((resolve) => {
91
114
  child.once("error", (error) => { supervisorSpawnError = error.message; });
92
115
  child.once("close", (code, signal) => resolve(runtimeResult ?? {
93
116
  exitCode: supervisorSpawnError === undefined ? (code ?? 128) : -1,
@@ -95,6 +118,11 @@ export async function startDormantSupervisor(launch, options = {}) {
95
118
  ...(supervisorSpawnError === undefined && signal !== null ? { terminationSignal: signal } : {}),
96
119
  }));
97
120
  });
121
+ const exit = supervisorExit.then(async (result) => {
122
+ if (platform !== "win32")
123
+ await waitForProcessGroupExit(pid, abortTimeoutMs);
124
+ return result;
125
+ });
98
126
  const supervisorClosed = new Promise((resolve) => child.once("close", () => resolve()));
99
127
  let readyResolve;
100
128
  let readyReject;
@@ -158,6 +186,21 @@ export async function startDormantSupervisor(launch, options = {}) {
158
186
  await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
159
187
  }
160
188
  };
189
+ const cancel = async () => {
190
+ if (child.exitCode !== null || child.signalCode !== null) {
191
+ await supervisorClosed;
192
+ return;
193
+ }
194
+ try {
195
+ await new Promise((resolve, reject) => {
196
+ child.send({ type: "abort" }, (error) => error === null ? resolve() : reject(error));
197
+ });
198
+ await waitForExit(supervisorClosed.then(() => ({ exitCode: 0 })), abortTimeoutMs, pid);
199
+ }
200
+ catch {
201
+ await abort();
202
+ }
203
+ };
161
204
  try {
162
205
  await new Promise((resolve, reject) => {
163
206
  child.send({ type: "launch", launch: { ...launch, args: [...launch.args], env } }, (error) => {
@@ -204,6 +247,6 @@ export async function startDormantSupervisor(launch, options = {}) {
204
247
  }
205
248
  },
206
249
  abort,
207
- cancel: abort,
250
+ cancel,
208
251
  };
209
252
  }
@@ -0,0 +1,71 @@
1
+ import { access, open, mkdir, readdir, readFile, rename, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { DaemonToServerExecutionFrameSchema, } from "./execution-protocol.js";
5
+ const MAX_PENDING_FILES = 4_096;
6
+ const fileName = (frame) => `${frame.executionId}.${frame.type === "execution:activity" ? "activity" : "console"}.${frame.seq}.json`;
7
+ export class ExecutionTelemetryJournal {
8
+ directory;
9
+ constructor(agentsRoot) {
10
+ this.directory = join(agentsRoot, ".execution-telemetry");
11
+ }
12
+ async append(frame) {
13
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
14
+ const path = join(this.directory, fileName(frame));
15
+ if (await access(path).then(() => true, () => false))
16
+ return;
17
+ const pending = await readdir(this.directory);
18
+ if (pending.length >= MAX_PENDING_FILES) {
19
+ throw new Error(`execution telemetry journal is full (${MAX_PENDING_FILES} frames)`);
20
+ }
21
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
22
+ let handle;
23
+ try {
24
+ handle = await open(tempPath, "wx", 0o600);
25
+ await handle.writeFile(JSON.stringify(frame), "utf8");
26
+ await handle.sync();
27
+ await handle.close();
28
+ handle = undefined;
29
+ await rename(tempPath, path);
30
+ }
31
+ catch (error) {
32
+ await unlink(tempPath).catch(() => { });
33
+ if (error.code !== "EEXIST")
34
+ throw error;
35
+ }
36
+ finally {
37
+ await handle?.close();
38
+ }
39
+ }
40
+ async acknowledge(executionId, kind, seq) {
41
+ await unlink(join(this.directory, `${executionId}.${kind}.${seq}.json`)).catch((error) => {
42
+ if (error.code !== "ENOENT")
43
+ throw error;
44
+ });
45
+ }
46
+ async replay() {
47
+ const names = await readdir(this.directory).catch((error) => {
48
+ if (error.code === "ENOENT")
49
+ return [];
50
+ throw error;
51
+ });
52
+ const frames = [];
53
+ for (const name of names.filter((entry) => entry.endsWith(".json")).sort()) {
54
+ let raw;
55
+ try {
56
+ raw = JSON.parse(await readFile(join(this.directory, name), "utf8"));
57
+ }
58
+ catch (error) {
59
+ throw new Error(`invalid execution telemetry journal entry: ${name}`, { cause: error });
60
+ }
61
+ const parsed = DaemonToServerExecutionFrameSchema.safeParse(raw);
62
+ if (!parsed.success || (parsed.data.type !== "execution:activity" && parsed.data.type !== "execution:console")) {
63
+ throw new Error(`invalid execution telemetry journal entry: ${name}`);
64
+ }
65
+ frames.push(parsed.data);
66
+ }
67
+ return frames.sort((left, right) => left.at.localeCompare(right.at)
68
+ || left.type.localeCompare(right.type) || left.seq - right.seq);
69
+ }
70
+ }
71
+ export const createExecutionTelemetryJournal = (agentsRoot) => new ExecutionTelemetryJournal(agentsRoot);