@nowcrew/daemon 0.5.14 → 0.5.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.
@@ -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
+ }
@@ -55,6 +55,7 @@ export const LegacyAgentStartSchema = z.object({
55
55
  runId: z.string().min(1),
56
56
  title: z.string().optional(),
57
57
  outputPolicy: z.unknown().optional(),
58
+ externalNotificationPolicy: z.unknown().optional(),
58
59
  }).passthrough().optional(),
59
60
  silent: z.boolean().optional(),
60
61
  }).passthrough();
@@ -119,6 +120,7 @@ export const ExecutionStartSchema = z.object({
119
120
  captureFinal: z.boolean(),
120
121
  streamActivity: z.boolean(),
121
122
  streamConsole: z.boolean(),
123
+ allowBoundImDecision: z.boolean().optional(),
122
124
  }).strict(),
123
125
  }).strict();
124
126
  export const ExecutionCancelSchema = z.object({
@@ -136,6 +138,13 @@ export const ExecutionCompletionAckSchema = z.object({
136
138
  protocolVersion: ProtocolVersionSchema,
137
139
  executionId: ExecutionIdSchema,
138
140
  }).strict();
141
+ export const ExecutionEventAckSchema = z.object({
142
+ type: z.literal("execution:event-ack"),
143
+ protocolVersion: ProtocolVersionSchema,
144
+ executionId: ExecutionIdSchema,
145
+ kind: z.enum(["activity", "console"]),
146
+ seq: SequenceSchema,
147
+ }).strict();
139
148
  export const ExecutionAcceptedSchema = z.object({
140
149
  type: z.literal("execution:accepted"),
141
150
  protocolVersion: ProtocolVersionSchema,
@@ -196,6 +205,7 @@ const RawExecutionCompletedSchema = z.object({
196
205
  model: z.string().optional(),
197
206
  resumed: z.boolean(),
198
207
  finalText: z.string().optional(),
208
+ boundImDecision: z.enum(["notify", "silent"]).optional(),
199
209
  usage: ExecutionUsageSchema.optional(),
200
210
  startedAt: TimestampSchema,
201
211
  finishedAt: TimestampSchema,
@@ -294,6 +304,7 @@ export const ServerToDaemonExecutionFrameSchema = z.discriminatedUnion("type", [
294
304
  ExecutionCancelSchema,
295
305
  ExecutionSyncSchema,
296
306
  ExecutionCompletionAckSchema,
307
+ ExecutionEventAckSchema,
297
308
  ]);
298
309
  const RawDaemonToServerExecutionFrameSchema = z.discriminatedUnion("type", [
299
310
  ExecutionAcceptedSchema,
@@ -1,4 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
3
5
  import { JournalConflictError } from "./execution-journal.js";
4
6
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -7,7 +9,9 @@ import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
7
9
  import { startDormantSupervisor, } from "./execution-supervisor.js";
8
10
  import { buildClaudeArgs, CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
9
11
  import { buildCodexArgs, CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
10
- import { buildKimiArgs, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
12
+ import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
13
+ import { executionBackendCapability } from "./execution-backend.js";
14
+ import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
11
15
  const ACTIVITY_KIND = {
12
16
  init: "working",
13
17
  text: "thinking",
@@ -38,7 +42,7 @@ async function reportBestEffort(report, frame, timeoutMs) {
38
42
  let timeout;
39
43
  try {
40
44
  return await Promise.race([
41
- Promise.resolve(report(frame)).then(() => true, () => true),
45
+ Promise.resolve(report(frame)).then(() => true, () => false),
42
46
  new Promise((resolve) => {
43
47
  timeout = setTimeout(() => resolve(false), timeoutMs);
44
48
  }),
@@ -214,15 +218,19 @@ function supervisorLaunch(request) {
214
218
  stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
215
219
  };
216
220
  }
221
+ if (request.effectivePermission !== "full_access") {
222
+ throw new Error(`Kimi ACP cannot enforce ${request.effectivePermission} permission`);
223
+ }
217
224
  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
- }),
225
+ command: process.execPath,
226
+ args: [
227
+ fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
228
+ "--bin", request.bin,
229
+ ...(request.model === undefined ? [] : ["--model", request.model]),
230
+ ],
224
231
  cwd: request.cwd,
225
232
  env: request.env,
233
+ stdinText: `${request.systemPrompt}\n\n${request.wakePrompt}`,
226
234
  };
227
235
  }
228
236
  function rejection(executionId, reason, message, at) {
@@ -236,8 +244,9 @@ function rejection(executionId, reason, message, at) {
236
244
  });
237
245
  }
238
246
  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) };
247
+ const backend = executionBackendCapability(dependencies.platform ?? process.platform);
248
+ if (!backend.supported) {
249
+ return { rejected: rejection(spec.executionId, "capability_missing", backend.reason, at) };
241
250
  }
242
251
  const promptBytes = Buffer.byteLength(spec.instructions.systemPrompt, "utf8")
243
252
  + Buffer.byteLength(spec.instructions.wakePrompt, "utf8");
@@ -404,6 +413,8 @@ export async function runExecution(config, input, dependencies) {
404
413
  const mint = dependencies.mintAgentToken ?? mintAgentToken;
405
414
  const execute = dependencies.executeLocal ?? executeLocal;
406
415
  const startSupervisor = dependencies.startSupervisor ?? startDormantSupervisor;
416
+ const readBoundImDecision = dependencies.readBoundImDecision ?? readBoundImDecisionFile;
417
+ const resetBoundImDecision = dependencies.resetBoundImDecision ?? resetBoundImDecisionFile;
407
418
  const telemetry = new TelemetryQueue(dependencies.report, bestEffortTimeoutMs, positiveTelemetryLimit(dependencies.telemetryMaxPendingFrames, DEFAULT_TELEMETRY_MAX_PENDING_FRAMES), Math.max(config.executionLimits.maxEventBytes, positiveTelemetryLimit(dependencies.telemetryMaxPendingBytes, DEFAULT_TELEMETRY_MAX_PENDING_BYTES)));
408
419
  const supervisorState = { active: null, abortOnce: null };
409
420
  let launchClosed = false;
@@ -416,6 +427,9 @@ export async function runExecution(config, input, dependencies) {
416
427
  let timeout;
417
428
  let timedOut = false;
418
429
  let completion;
430
+ let boundImDecision = spec.reporting.allowBoundImDecision
431
+ ? "silent"
432
+ : undefined;
419
433
  let rejectCancellationFailure;
420
434
  const cancellationFailure = new Promise((_resolve, reject) => {
421
435
  rejectCancellationFailure = reject;
@@ -424,7 +438,7 @@ export async function runExecution(config, input, dependencies) {
424
438
  if (dependencies.slot !== undefined) {
425
439
  await cancellable(dependencies.slot.ready, dependencies.cancellation);
426
440
  }
427
- const credential = await cancellable(mint(config.serverUrl, config.machineToken, spec.agent.handle, undefined, { executionId: spec.executionId }), dependencies.cancellation);
441
+ const credential = await cancellable(mint(config.serverUrl, config.machineToken, spec.agent.handle, undefined, { executionId: spec.executionId, agentRunId: spec.executionId }), dependencies.cancellation);
428
442
  const providerConfig = launchProviderConfig(credential.config);
429
443
  let activitySequence = 0;
430
444
  let consoleSequence = 0;
@@ -547,6 +561,11 @@ export async function runExecution(config, input, dependencies) {
547
561
  cliPath: config.cliPath,
548
562
  providerConfig,
549
563
  ...(providerConfig.description ? { description: providerConfig.description } : {}),
564
+ ...(spec.reporting.allowBoundImDecision ? {
565
+ systemEnv: {
566
+ CREW_BOUND_IM_DECISION_FILE: `.bound-im-decision-${spec.executionId}.json`,
567
+ },
568
+ } : {}),
550
569
  },
551
570
  session: {
552
571
  enabled: config.resume,
@@ -563,6 +582,12 @@ export async function runExecution(config, input, dependencies) {
563
582
  if (timeout !== undefined)
564
583
  clearTimeout(timeout);
565
584
  const finishedAt = now().toISOString();
585
+ if (spec.reporting.allowBoundImDecision) {
586
+ const path = join(result.workspaceRunDir, `.bound-im-decision-${spec.executionId}.json`);
587
+ const selected = await readBoundImDecision(path);
588
+ await resetBoundImDecision(path);
589
+ boundImDecision = selected?.decision ?? "silent";
590
+ }
566
591
  completion = ExecutionCompletedSchema.parse(timedOut ? {
567
592
  type: "execution:completed",
568
593
  protocolVersion: 1,
@@ -573,6 +598,7 @@ export async function runExecution(config, input, dependencies) {
573
598
  runtime: result.runtime,
574
599
  ...(result.model === null ? {} : { model: result.model }),
575
600
  resumed: result.resumed,
601
+ ...(boundImDecision ? { boundImDecision } : {}),
576
602
  startedAt,
577
603
  finishedAt,
578
604
  } : {
@@ -589,6 +615,7 @@ export async function runExecution(config, input, dependencies) {
589
615
  runtime: result.runtime,
590
616
  ...(result.model === null ? {} : { model: result.model }),
591
617
  resumed: result.resumed,
618
+ ...(boundImDecision ? { boundImDecision } : {}),
592
619
  ...(!spec.reporting.captureFinal || result.finalText === null
593
620
  ? {}
594
621
  : { finalText: result.finalText }),