@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.
- package/README.md +57 -3
- package/dist/computer-cli.js +214 -0
- package/dist/computer-profile.js +195 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +2 -1
- package/dist/console.js +10 -0
- package/dist/execution-backend.js +11 -0
- package/dist/execution-protocol.js +8 -0
- package/dist/execution-runner.js +17 -10
- package/dist/execution-supervisor.js +48 -5
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/i18n.js +25 -0
- package/dist/local-executor.js +5 -2
- package/dist/machine-info.js +25 -4
- package/dist/main.js +15 -0
- package/dist/normalize.js +11 -0
- package/dist/prompt.js +21 -9
- package/dist/runner.js +1 -0
- package/dist/runtime-capabilities.js +5 -0
- package/dist/runtimes/kimi-acp-runner.js +264 -0
- package/dist/runtimes/kimi.js +10 -0
- package/dist/serve.js +45 -8
- package/package.json +3 -2
|
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
32
|
+
.replaceAll('"', """).replaceAll("'", "'");
|
|
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 ??
|
|
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,
|
package/dist/execution-runner.js
CHANGED
|
@@ -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 {
|
|
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, () =>
|
|
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:
|
|
219
|
-
args:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...(request.model === undefined ?
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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);
|