@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.
- package/README.md +57 -3
- package/dist/bound-im-decision.js +22 -0
- 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 +11 -0
- package/dist/execution-runner.js +38 -11
- 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/origin-decision.js +1 -1
- package/dist/prompt.js +40 -21
- package/dist/runner.js +25 -1
- 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/scheduled-report.js +4 -0
- package/dist/scheduled-run-report.js +1 -0
- package/dist/serve.js +55 -10
- 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
|
+
}
|
|
@@ -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,
|
package/dist/execution-runner.js
CHANGED
|
@@ -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 {
|
|
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, () =>
|
|
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:
|
|
219
|
-
args:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...(request.model === undefined ?
|
|
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
|
-
|
|
240
|
-
|
|
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 }),
|