@nowcrew/daemon 0.6.98-beta.6 → 0.6.98-beta.8
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 +29 -20
- package/dist/automatic-daemon-installer.js +15 -11
- package/dist/automatic-service-capture.js +27 -0
- package/dist/computer-main.js +0 -0
- package/dist/computer-service.js +30 -5
- package/dist/console.js +17 -2
- package/dist/execution-journal-lock.js +51 -2
- package/dist/execution-protocol.js +2 -0
- package/dist/execution-runner.js +19 -13
- package/dist/host-execution-coordinator.js +10 -0
- package/dist/local-executor.js +8 -1
- package/dist/machine-info.js +6 -1
- package/dist/machine-runtime-environment.js +1 -0
- package/dist/main.js +0 -0
- package/dist/managed-service-lifecycle.js +10 -0
- package/dist/marketplace-controller.js +21 -8
- package/dist/marketplace-maintenance-bridge.js +79 -0
- package/dist/marketplace-maintenance-protocol.js +40 -0
- package/dist/marketplace-serve.js +4 -2
- package/dist/project-skills/execution-projection.js +4 -5
- package/dist/project-skills/reconciler.js +8 -9
- package/dist/project-skills/scanner.js +9 -2
- package/dist/project-skills/source-directory.js +13 -0
- package/dist/runtime-observation.js +219 -0
- package/dist/runtime-tool-update-bridge.js +126 -0
- package/dist/runtime-tool-update-controller.js +151 -0
- package/dist/runtime-tool-update-protocol.js +24 -0
- package/dist/runtime-tool-update-serve.js +54 -0
- package/dist/runtimes/claude-observation-runner.js +55 -0
- package/dist/runtimes/claude-thread-runner.js +17 -3
- package/dist/runtimes/claude.js +5 -3
- package/dist/runtimes/codex-app-server-runner.js +16 -2
- package/dist/runtimes/deepseek-harness-runner.js +16 -0
- package/dist/runtimes/kimi-acp-runner.js +19 -0
- package/dist/runtimes/observation-context.js +18 -0
- package/dist/runtimes/observation-mapper.js +314 -0
- package/dist/runtimes/opencode-runner.js +18 -4
- package/dist/runtimes/pi-runner.js +18 -1
- package/dist/runtimes/pi.js +54 -20
- package/dist/serve.js +10 -1
- package/dist/token.js +4 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -68,10 +68,15 @@ on stderr and the final result on stdout. On Windows shells that block npm.ps1/n
|
|
|
68
68
|
the package entry; the setup arguments and workflow are unchanged.
|
|
69
69
|
|
|
70
70
|
Deployment requires `CREW_PUBLIC_URL` (or `CREW_SERVER_URL`) and
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
a compatible published installer (beta >= 0.6.98-beta.5; stable >= 0.6.99).
|
|
72
|
+
`CREW_COMPUTER_TOKEN_SETUP_VERSION` is an optional exact-version pin, not a feature switch.
|
|
73
|
+
When unset or blank, the server selects the latest beta for dev or stable for production from its refreshed release catalog.
|
|
74
|
+
Both the two-step installation command and the one-step npx command pin that exact version.
|
|
75
|
+
An explicit pin must be published, compatible and in the same channel; invalid pins do not fall back.
|
|
76
|
+
The command keeps `--machine` for target validation and `--setup-token` for the short-lived, one-time preapproval.
|
|
77
|
+
The server checks its published release catalog and channel before issuing a Token command. If no
|
|
78
|
+
compatible published version is available, it returns `computer_token_setup_unavailable` instead of
|
|
79
|
+
generating a command for an unsupported package. Existing clients retain browser setup.
|
|
75
80
|
|
|
76
81
|
## Browser-Approved Computer Setup (Compatibility)
|
|
77
82
|
|
|
@@ -429,23 +434,27 @@ path-free inventory and logical `(projectId, skillName)` bindings. The local pat
|
|
|
429
434
|
while an add command is forwarded to the selected online Computer; it is not written to the workspace
|
|
430
435
|
database, structured logs, inventory frames, or responses. Project Skills are disabled on Windows.
|
|
431
436
|
|
|
432
|
-
|
|
437
|
+
Skill entries may be real directories or directory symlinks, including relative, absolute, chained,
|
|
438
|
+
and shared directories outside the project. Only direct, non-hidden entries are scanned. Broken,
|
|
439
|
+
cyclic, inaccessible, and non-directory links count as invalid Skills without hiding healthy entries;
|
|
440
|
+
the existing frontmatter, size, name-conflict, and inventory limits apply to linked Skills too.
|
|
441
|
+
|
|
442
|
+
Each execution projects its binding set into a private Runtime discovery directory:
|
|
433
443
|
|
|
434
444
|
```text
|
|
435
|
-
<agentsRoot>/<handle>/.
|
|
436
|
-
<agentsRoot>/<handle>/.crew/claude-skills/.claude/skills/<name> # Claude symlink
|
|
445
|
+
<agentsRoot>/<handle>/.crew/project-skills/exec-<executionId>/runtime/.claude/skills/<name>
|
|
437
446
|
```
|
|
438
447
|
|
|
439
|
-
The
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
448
|
+
The scanner retains the entry path. Binding checks and execution startup resolve its current real
|
|
449
|
+
directory and check for `SKILL.md`; missing sources warn and are omitted without blocking other work.
|
|
450
|
+
Each execution links to that resolved directory, so retargeting an entry affects subsequent executions
|
|
451
|
+
while active executions keep their original target. Source file edits remain live. The projection is
|
|
452
|
+
removed after the runtime process tree stops; source directories and user-created links are preserved.
|
|
453
|
+
These are ordinary writable symlinks, not snapshots or a sandbox: an Agent running as the daemon user
|
|
454
|
+
can modify the source Skill through them. That write-through risk is intentionally accepted.
|
|
443
455
|
|
|
444
|
-
Codex
|
|
445
|
-
|
|
446
|
-
root and adds it to Codex `project_root_markers`, so upward discovery reaches `.agents/skills` without
|
|
447
|
-
changing the task cwd. Claude receives
|
|
448
|
-
`--add-dir <agentsRoot>/<handle>/.crew/claude-skills`; Claude treats those Skills as `projectSettings`,
|
|
456
|
+
Codex registers the execution's `runtime/.claude/skills` through `skills/extraRoots/set`. Claude receives
|
|
457
|
+
the execution's `runtime` directory through `--add-dir`; Claude treats those Skills as `projectSettings`,
|
|
449
458
|
so a machine policy that disables project settings also disables this source even when the projection is
|
|
450
459
|
healthy.
|
|
451
460
|
|
|
@@ -462,10 +471,10 @@ scope is merged. The policy version in Codex's session fingerprint cold-starts o
|
|
|
462
471
|
once, then subsequent runs can resume normally. Explicit `shell_environment_policy.set` entries, shell
|
|
463
472
|
startup scripts and Agent notes remain separate configuration; this change does not correct them.
|
|
464
473
|
|
|
465
|
-
Binding changes
|
|
466
|
-
complete binding set for every Agent assigned to that Computer, including empty sets
|
|
467
|
-
|
|
468
|
-
|
|
474
|
+
Binding changes are checked immediately when the Computer is online. On reconnect the server sends the
|
|
475
|
+
complete binding set for every Agent assigned to that Computer, including empty sets. Each execution
|
|
476
|
+
uses its server-snapshotted binding set and revalidates local source paths immediately before Runtime
|
|
477
|
+
launch. Skill file content changes
|
|
469
478
|
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
470
479
|
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
471
480
|
|
|
@@ -6,7 +6,8 @@ import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenS
|
|
|
6
6
|
import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
|
|
7
7
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
8
|
import { buildManagedServiceRecord, installManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
|
|
9
|
-
import {
|
|
9
|
+
import { readManagedServiceRegistry, } from "./managed-service-registry.js";
|
|
10
|
+
import { captureManagedServiceForSetup } from "./automatic-service-capture.js";
|
|
10
11
|
import { resolveAutomaticInstallTarget, } from "./automatic-install-target.js";
|
|
11
12
|
import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
12
13
|
import { daemonVersion } from "./machine-info.js";
|
|
@@ -80,7 +81,6 @@ function defaultDependencies() {
|
|
|
80
81
|
},
|
|
81
82
|
captureService: async (spec, target) => {
|
|
82
83
|
if (platform !== "win32") {
|
|
83
|
-
managedRecords = await readManagedServiceRegistry({ userHome, localHome: localHomeForTarget(target, platform) });
|
|
84
84
|
const entryPath = spec.daemonCommand[1];
|
|
85
85
|
const installation = daemonGlobalInstallation(entryPath, platform);
|
|
86
86
|
if (installation === null) {
|
|
@@ -94,14 +94,13 @@ function defaultDependencies() {
|
|
|
94
94
|
entryPath,
|
|
95
95
|
...installation,
|
|
96
96
|
});
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
97
|
+
const captured = await captureManagedServiceForSetup({
|
|
98
|
+
spec, record: candidate, runner,
|
|
99
|
+
registry: { userHome, localHome: localHomeForTarget(target, platform) },
|
|
100
|
+
});
|
|
101
|
+
managedRecords = captured.managedRecords;
|
|
102
|
+
previousManagedRecord = captured.previousManagedRecord;
|
|
103
|
+
return captured.snapshot;
|
|
105
104
|
}
|
|
106
105
|
return captureExactService(spec, runner);
|
|
107
106
|
},
|
|
@@ -175,7 +174,12 @@ function defaultDependencies() {
|
|
|
175
174
|
? localHome
|
|
176
175
|
: localHomeForNpmPrefix(previousManagedRecord.npmPrefix, platform);
|
|
177
176
|
const current = (await readManagedServiceRegistry({ userHome, localHome: restoreLocalHome })).find((record) => record.serviceId === spec.id);
|
|
178
|
-
|
|
177
|
+
const onlyHashChanged = current !== undefined && previousManagedRecord !== undefined
|
|
178
|
+
&& JSON.stringify({ ...current, descriptorSha256: previousManagedRecord.descriptorSha256 })
|
|
179
|
+
=== JSON.stringify(previousManagedRecord);
|
|
180
|
+
// Preflight may have captured the exact descriptor while leaving a stale
|
|
181
|
+
// registry hash untouched. Let installManagedService reconcile it in place.
|
|
182
|
+
if (current !== undefined && !onlyHashChanged && JSON.stringify(current) !== JSON.stringify(previousManagedRecord)) {
|
|
179
183
|
await uninstallManagedService({
|
|
180
184
|
spec,
|
|
181
185
|
record: current,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { captureExactService } from "./computer-service.js";
|
|
2
|
+
import { ManagedServiceConflictError, ManagedServiceRegistryError, managedServiceConflict, managedServiceIdentityMatches, readManagedServiceRegistry, } from "./managed-service-registry.js";
|
|
3
|
+
/** Read-only preflight; keep the record needed to restore the captured service. */
|
|
4
|
+
export async function captureManagedServiceForSetup(input) {
|
|
5
|
+
const managedRecords = await readManagedServiceRegistry(input.registry);
|
|
6
|
+
const { generation: _generation, createdAt: _createdAt, ...expected } = input.record;
|
|
7
|
+
const registered = managedRecords.find((record) => managedServiceIdentityMatches(record, {
|
|
8
|
+
...expected, descriptorSha256: record.descriptorSha256,
|
|
9
|
+
}));
|
|
10
|
+
if (registered === undefined) {
|
|
11
|
+
const conflict = managedServiceConflict(managedRecords, input.record);
|
|
12
|
+
if (conflict !== null) {
|
|
13
|
+
throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const snapshot = await captureExactService(input.spec, input.runner);
|
|
17
|
+
if (registered !== undefined && registered.descriptorSha256 !== expected.descriptorSha256
|
|
18
|
+
&& (!snapshot.installed || !snapshot.exact)) {
|
|
19
|
+
throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match the requested identity`);
|
|
20
|
+
}
|
|
21
|
+
// A stale hash may be reconciled later under the registry lock. Rollback must
|
|
22
|
+
// refer to the descriptor we actually captured, not the stale registry hash.
|
|
23
|
+
const previousManagedRecord = registered === undefined ? undefined : {
|
|
24
|
+
...registered, descriptorSha256: expected.descriptorSha256,
|
|
25
|
+
};
|
|
26
|
+
return { managedRecords, previousManagedRecord, snapshot };
|
|
27
|
+
}
|
package/dist/computer-main.js
CHANGED
|
File without changes
|
package/dist/computer-service.js
CHANGED
|
@@ -12,6 +12,8 @@ import { powershellCommandArgs } from "./powershell-command.js";
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
14
14
|
const WINDOWS_STOP_POLL_MS = 100;
|
|
15
|
+
const DARWIN_STOP_TIMEOUT_MS = 30_000;
|
|
16
|
+
const DARWIN_STOP_POLL_MS = 100;
|
|
15
17
|
/** Windows cannot spawn .cmd/.bat shims directly with shell:false; route them through cmd.exe explicitly. */
|
|
16
18
|
export function systemCommandInvocation(command, args, platform = process.platform, comSpec = process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe") {
|
|
17
19
|
if (platform === "win32" && /\.(?:cmd|bat)$/iu.test(command)) {
|
|
@@ -186,6 +188,32 @@ async function checked(runner, command, args, action) {
|
|
|
186
188
|
}
|
|
187
189
|
return result;
|
|
188
190
|
}
|
|
191
|
+
async function stopLaunchdService(spec, initial, runner) {
|
|
192
|
+
const assertQueryable = (result) => {
|
|
193
|
+
// launchctl uses 113 for an absent service. Other failures cannot prove it stopped.
|
|
194
|
+
if (result.exitCode !== 0 && result.exitCode !== 113) {
|
|
195
|
+
throw new Error(`launchd status failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
assertQueryable(initial);
|
|
199
|
+
if (initial.exitCode === 113)
|
|
200
|
+
return;
|
|
201
|
+
const target = `${spec.managerDomain}/${spec.id}`;
|
|
202
|
+
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
203
|
+
const deadline = Date.now() + DARWIN_STOP_TIMEOUT_MS;
|
|
204
|
+
while (true) {
|
|
205
|
+
const current = await runner("launchctl", ["print", target]);
|
|
206
|
+
assertQueryable(current);
|
|
207
|
+
if (current.exitCode === 113)
|
|
208
|
+
return;
|
|
209
|
+
// bootout returns while the job is SIGTERMed and still owns its installation lease.
|
|
210
|
+
// Wait for removal, not merely a non-running state, before installing or bootstrapping.
|
|
211
|
+
if (Date.now() >= deadline) {
|
|
212
|
+
throw new Error(`launchd service '${spec.id}' did not unload within ${DARWIN_STOP_TIMEOUT_MS}ms`);
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, DARWIN_STOP_POLL_MS));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
189
217
|
function sameWindowsPath(left, right) {
|
|
190
218
|
return left !== undefined
|
|
191
219
|
&& left.replaceAll("/", "\\").toLowerCase() === right.replaceAll("/", "\\").toLowerCase();
|
|
@@ -356,9 +384,7 @@ export async function uninstallService(spec, runner = systemCommandRunner) {
|
|
|
356
384
|
}
|
|
357
385
|
if (spec.platform === "darwin") {
|
|
358
386
|
const status = await serviceStatus(spec, runner);
|
|
359
|
-
|
|
360
|
-
await checked(runner, "launchctl", ["bootout", `${spec.managerDomain}/${spec.id}`], "launchd bootout");
|
|
361
|
-
}
|
|
387
|
+
await stopLaunchdService(spec, status, runner);
|
|
362
388
|
}
|
|
363
389
|
else {
|
|
364
390
|
const disable = await runner("systemctl", ["--user", "disable", "--now", spec.id]);
|
|
@@ -385,8 +411,7 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
|
|
|
385
411
|
await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
|
|
386
412
|
}
|
|
387
413
|
else if (action === "stop") {
|
|
388
|
-
|
|
389
|
-
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
414
|
+
await stopLaunchdService(spec, status, runner);
|
|
390
415
|
}
|
|
391
416
|
else {
|
|
392
417
|
if (status.loaded)
|
package/dist/console.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 会附带结构化 payload(diff/命令/todo 清单/计划…),供前端做富渲染(红绿 diff、清单勾选);
|
|
10
10
|
* 前端不识别 payload.kind 时降级为纯文本行,协议向后兼容。
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* kimi(OpenAI 消息风格行)。
|
|
12
|
+
* 覆盖 runtime 的流:claude(stream-json)、codex(exec --json 的 item.* 事件)、
|
|
13
|
+
* kimi(OpenAI 消息风格行)、ACP 适配器与 Pi 适配器(pi.* 块级事件)。
|
|
14
14
|
*/
|
|
15
15
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
16
16
|
import { buildFilePreview, buildSnippetDiff } from "./console-payload.js";
|
|
@@ -280,6 +280,21 @@ export function toConsoleLines(event, runtime) {
|
|
|
280
280
|
const text = typeof e.content === "string" ? e.content.trim() : "";
|
|
281
281
|
return text ? [{ stream: "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
|
|
282
282
|
}
|
|
283
|
+
if (e.type === "pi.text" && e.text)
|
|
284
|
+
return [{ stream: "text", text: e.text }];
|
|
285
|
+
if (e.type === "pi.thinking" && e.text)
|
|
286
|
+
return [{ stream: "thinking", text: e.text }];
|
|
287
|
+
if (e.type === "pi.tool_call") {
|
|
288
|
+
return [{ stream: "tool", text: e.title ? `⏺ ${clip(e.title, TOOL_INPUT_CAP)}` : "⏺ Pi tool" }];
|
|
289
|
+
}
|
|
290
|
+
if (e.type === "pi.tool_result") {
|
|
291
|
+
const text = typeof e.content === "string" ? e.content.trim() : "";
|
|
292
|
+
return text ? [{ stream: e.is_error ? "error" : "tool_result", text: clip(text, TOOL_RESULT_CAP) }] : [];
|
|
293
|
+
}
|
|
294
|
+
if (e.type === "pi.error") {
|
|
295
|
+
const message = event.message;
|
|
296
|
+
return typeof message === "string" && message.trim() ? [{ stream: "error", text: message.trim() }] : [];
|
|
297
|
+
}
|
|
283
298
|
if (e.type === "system" && e.subtype === "init") {
|
|
284
299
|
return [{ stream: "system", text: `● ${td("Claude Code session started")}` }];
|
|
285
300
|
}
|
|
@@ -3,10 +3,25 @@ import { renameSync } from "node:fs";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
const ProtectedGroupSchema = z.object({ pid: z.number().int().positive(), processIdentity: z.string().min(1) }).strict();
|
|
7
|
+
export async function inspectProtectedProcessGroup(pid) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(-pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === "ESRCH")
|
|
14
|
+
return false;
|
|
15
|
+
if (error.code === "EPERM")
|
|
16
|
+
return true;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
const OwnerSchema = z.object({
|
|
7
21
|
pid: z.number().int().positive(),
|
|
8
22
|
processIdentity: z.string().min(1),
|
|
9
23
|
token: z.string().uuid(),
|
|
24
|
+
protectedGroup: ProtectedGroupSchema.optional(),
|
|
10
25
|
}).strict();
|
|
11
26
|
export class JournalLockedError extends Error {
|
|
12
27
|
journalPath;
|
|
@@ -144,7 +159,8 @@ export async function inspectJournalLock(options) {
|
|
|
144
159
|
let inspection;
|
|
145
160
|
try {
|
|
146
161
|
const identity = await options.inspectIdentity(owner.pid);
|
|
147
|
-
|
|
162
|
+
const protectedAlive = owner.protectedGroup !== undefined && await (options.inspectProcessGroup ?? inspectProtectedProcessGroup)(owner.protectedGroup.pid);
|
|
163
|
+
inspection = identity === owner.processIdentity || protectedAlive
|
|
148
164
|
? {
|
|
149
165
|
status: "owned",
|
|
150
166
|
ownerPid: owner.pid,
|
|
@@ -333,7 +349,9 @@ export function createJournalLease(options) {
|
|
|
333
349
|
if (observed === null)
|
|
334
350
|
continue;
|
|
335
351
|
const identity = await options.inspectIdentity(observed.owner.pid);
|
|
336
|
-
|
|
352
|
+
const protectedAlive = observed.owner.protectedGroup !== undefined
|
|
353
|
+
&& await (options.inspectProcessGroup ?? inspectProtectedProcessGroup)(observed.owner.protectedGroup.pid);
|
|
354
|
+
if (identity === observed.owner.processIdentity || protectedAlive) {
|
|
337
355
|
throw new JournalLockedError(`Execution journal is locked by process ${observed.owner.pid}`, { journalPath, ownerPid: observed.owner.pid });
|
|
338
356
|
}
|
|
339
357
|
await options.hooks?.beforeRemoveObservedOwner?.(observed.owner);
|
|
@@ -408,6 +426,9 @@ export function createJournalLease(options) {
|
|
|
408
426
|
status = "closed";
|
|
409
427
|
return;
|
|
410
428
|
}
|
|
429
|
+
if (lease.owner.protectedGroup && await (options.inspectProcessGroup ?? inspectProtectedProcessGroup)(lease.owner.protectedGroup.pid)) {
|
|
430
|
+
throw new JournalLockedError("Protected maintenance process group is still alive", { journalPath, ownerPid: lease.owner.protectedGroup.pid });
|
|
431
|
+
}
|
|
411
432
|
status = "closing";
|
|
412
433
|
lease.releasing = true;
|
|
413
434
|
await awaitAbortable((async () => {
|
|
@@ -426,8 +447,36 @@ export function createJournalLease(options) {
|
|
|
426
447
|
status = "closed";
|
|
427
448
|
void cleanupReleasedDirectory(releasedDirectory);
|
|
428
449
|
};
|
|
450
|
+
const protectProcessGroup = async (raw) => {
|
|
451
|
+
const group = ProtectedGroupSchema.parse(raw);
|
|
452
|
+
const lease = registry.get(options.directory);
|
|
453
|
+
if (!attached || status !== "open" || !lease || lease.releasing || lease.refs !== 1)
|
|
454
|
+
throw new JournalLockedError("Lease cannot protect a worker", { journalPath });
|
|
455
|
+
await validateInstalledOwner(lease);
|
|
456
|
+
if (await options.inspectIdentity(group.pid) !== group.processIdentity
|
|
457
|
+
|| !await (options.inspectProcessGroup ?? inspectProtectedProcessGroup)(group.pid))
|
|
458
|
+
throw new JournalLockedError("Worker identity is absent", { journalPath });
|
|
459
|
+
if (lease.owner.protectedGroup) {
|
|
460
|
+
if (JSON.stringify(lease.owner.protectedGroup) === JSON.stringify(group))
|
|
461
|
+
return;
|
|
462
|
+
throw new JournalLockedError("Lease already protects a worker", { journalPath });
|
|
463
|
+
}
|
|
464
|
+
const owner = { ...lease.owner, protectedGroup: group };
|
|
465
|
+
const temp = join(options.directory, `.journal-owner.${lease.owner.token}.${randomUUID()}.tmp`);
|
|
466
|
+
try {
|
|
467
|
+
await fileSystem.writeDurableFile(temp, `${JSON.stringify(owner)}\n`);
|
|
468
|
+
await fileSystem.rename(temp, join(lockDirectory, lease.ownerFile));
|
|
469
|
+
lease.owner = owner;
|
|
470
|
+
await options.syncDirectory(lockDirectory);
|
|
471
|
+
await options.syncDirectory(options.directory);
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
await fileSystem.rm(temp, { force: true }).catch(() => undefined);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
429
477
|
return {
|
|
430
478
|
acquire,
|
|
479
|
+
protectProcessGroup,
|
|
431
480
|
assertUsable: () => {
|
|
432
481
|
if (status !== "open") {
|
|
433
482
|
throw new JournalLockedError(`Execution journal lease is ${status}`, { journalPath });
|
|
@@ -207,6 +207,8 @@ export const ExecutionStartSchema = z.object({
|
|
|
207
207
|
model: z.string().optional(),
|
|
208
208
|
reasoning: ReasoningSchema.optional(),
|
|
209
209
|
timeoutMs: z.number().int().positive().optional(),
|
|
210
|
+
// Server-owned policy; 0 disables the total runtime timer. Capability-gated.
|
|
211
|
+
taskTimeoutMs: z.number().int().min(0).max(2_145_600_000).optional(),
|
|
210
212
|
}).strict(),
|
|
211
213
|
permissions: z.object({
|
|
212
214
|
requested: PermissionSchema,
|
package/dist/execution-runner.js
CHANGED
|
@@ -416,7 +416,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
416
416
|
return { kind: "rejected", frame };
|
|
417
417
|
}
|
|
418
418
|
const { permission } = checked;
|
|
419
|
-
const effectiveTimeoutMs = spec.runtime.timeoutMs ?? config.executionLimits.maxTimeoutMs;
|
|
419
|
+
const effectiveTimeoutMs = spec.runtime.taskTimeoutMs ?? spec.runtime.timeoutMs ?? config.executionLimits.maxTimeoutMs;
|
|
420
420
|
const accepted = await dependencies.journal.accept(spec.executionId, specHash, {
|
|
421
421
|
runtime: spec.runtime.name,
|
|
422
422
|
...(spec.runtime.model === undefined ? {} : { model: spec.runtime.model }),
|
|
@@ -492,6 +492,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
492
492
|
let timeout;
|
|
493
493
|
let timedOut = false;
|
|
494
494
|
let firstOutputTimer;
|
|
495
|
+
let runtimeTimersStarted = false;
|
|
495
496
|
let firstOutputTimedOut = false;
|
|
496
497
|
let abandonedRuntimeRunningCompletion = null;
|
|
497
498
|
let completion;
|
|
@@ -617,7 +618,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
617
618
|
if (dependencies.cancellation?.isRequested())
|
|
618
619
|
return;
|
|
619
620
|
const cancel = runtimeCancel;
|
|
620
|
-
if (cancel !== null &&
|
|
621
|
+
if (cancel !== null && !runtimeTimersStarted && !dependencies.cancellation?.isRequested()) {
|
|
622
|
+
runtimeTimersStarted = true;
|
|
621
623
|
const firstOutputDeadline = config.executionLimits.maxFirstOutputWaitMs
|
|
622
624
|
+ config.executionLimits.firstOutputGraceMs;
|
|
623
625
|
firstOutputTimer = setTimeout(() => {
|
|
@@ -629,15 +631,16 @@ export async function runExecution(config, input, dependencies) {
|
|
|
629
631
|
rejectCancellationFailure(error);
|
|
630
632
|
}
|
|
631
633
|
}, firstOutputDeadline);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
634
|
+
if (effectiveTimeoutMs > 0)
|
|
635
|
+
timeout = setTimeout(() => {
|
|
636
|
+
timedOut = true;
|
|
637
|
+
try {
|
|
638
|
+
void cancel().catch(rejectCancellationFailure);
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
rejectCancellationFailure(error);
|
|
642
|
+
}
|
|
643
|
+
}, effectiveTimeoutMs);
|
|
641
644
|
}
|
|
642
645
|
dependencies.onRuntimePhase?.("running", spec.runtime.name);
|
|
643
646
|
const journalStartedAt = Date.now();
|
|
@@ -706,9 +709,11 @@ export async function runExecution(config, input, dependencies) {
|
|
|
706
709
|
catch { /* best-effort activity omitted when its envelope cannot fit */ }
|
|
707
710
|
},
|
|
708
711
|
} : {}),
|
|
709
|
-
...(spec.reporting.streamConsole ? {
|
|
712
|
+
...(spec.reporting.streamConsole || credential.observability ? {
|
|
710
713
|
onConsole: (chunk) => {
|
|
711
|
-
if (
|
|
714
|
+
if (!spec.reporting.streamConsole && chunk.payload?.kind !== "agent_observation")
|
|
715
|
+
return;
|
|
716
|
+
if (chunk.payload?.kind !== "agent_observation" && firstOutputTimer !== undefined) {
|
|
712
717
|
clearTimeout(firstOutputTimer);
|
|
713
718
|
firstOutputTimer = undefined;
|
|
714
719
|
}
|
|
@@ -856,6 +861,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
856
861
|
return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
|
|
857
862
|
};
|
|
858
863
|
const localInput = {
|
|
864
|
+
...(credential.observability ? { observability: credential.observability } : {}),
|
|
859
865
|
executionId: spec.executionId,
|
|
860
866
|
handle: spec.agent.handle,
|
|
861
867
|
channelId: spec.context.channelId,
|
|
@@ -302,6 +302,16 @@ export function createHostExecutionCoordinator(options = {}) {
|
|
|
302
302
|
}
|
|
303
303
|
let releasePromise = null;
|
|
304
304
|
return {
|
|
305
|
+
protectProcessGroup: async (pid) => {
|
|
306
|
+
const processIdentity = await defaultProcessController.inspectIdentity(pid);
|
|
307
|
+
if (processIdentity === null)
|
|
308
|
+
throw new Error("maintenance worker identity is absent");
|
|
309
|
+
for (const lease of leases) {
|
|
310
|
+
if (!lease.protectProcessGroup)
|
|
311
|
+
throw new Error("lease process-group protection is unavailable");
|
|
312
|
+
await lease.protectProcessGroup({ pid, processIdentity });
|
|
313
|
+
}
|
|
314
|
+
},
|
|
305
315
|
release: () => {
|
|
306
316
|
if (releasePromise !== null)
|
|
307
317
|
return releasePromise;
|
package/dist/local-executor.js
CHANGED
|
@@ -46,6 +46,7 @@ import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace,
|
|
|
46
46
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
47
47
|
import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
|
|
48
48
|
import { localMemoryV2Environment } from "./config.js";
|
|
49
|
+
import { observationEnvironment, safeRuntimeObservation } from "./runtime-observation.js";
|
|
49
50
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
50
51
|
constructor(runtime, timeoutMs) {
|
|
51
52
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -592,7 +593,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
592
593
|
systemPromptPath: workspace.systemPromptPath,
|
|
593
594
|
systemPrompt: effectiveSystemPrompt,
|
|
594
595
|
wakePrompt,
|
|
595
|
-
env: runtimeEnv,
|
|
596
|
+
env: observationEnvironment(runtimeEnv, input.observability),
|
|
596
597
|
effectivePermission: input.effectivePermission,
|
|
597
598
|
...(launchModel === undefined ? {} : { model: launchModel }),
|
|
598
599
|
...(launchReasoning === undefined ? {} : { reasoning: launchReasoning }),
|
|
@@ -787,6 +788,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
787
788
|
const event = parseLine(line);
|
|
788
789
|
if (!event)
|
|
789
790
|
return;
|
|
791
|
+
if (typeof event === "object" && "type" in event && event.type === "nowwork.observation") {
|
|
792
|
+
const payload = safeRuntimeObservation(event.observation, input.observability);
|
|
793
|
+
if (payload)
|
|
794
|
+
callbacks.onConsole?.({ stream: "system", text: "", payload });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
790
797
|
nativeThreadContext?.observe(event);
|
|
791
798
|
localMemoryTelemetry?.observe(event);
|
|
792
799
|
if (isRuntimeReadyEvent(runtime.name, event))
|
package/dist/machine-info.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RUNTIME_TOOL_UPDATES_CAPABILITY } from "./runtime-tool-update-protocol.js";
|
|
2
|
+
import { MAINTENANCE_CAPABILITY } from "./marketplace-maintenance-protocol.js";
|
|
1
3
|
import { retryHarnessProbe } from "./runtimes/harness-probe-retry.js";
|
|
2
4
|
import { MARKETPLACE_CAPABILITY } from "./marketplace-protocol.js";
|
|
3
5
|
/**
|
|
@@ -39,7 +41,10 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
|
|
|
39
41
|
nativeRuntimeAdaptersReady: true,
|
|
40
42
|
});
|
|
41
43
|
export const DAEMON_CAPABILITIES = Object.freeze([
|
|
44
|
+
RUNTIME_TOOL_UPDATES_CAPABILITY,
|
|
45
|
+
"execution_task_timeout_v1",
|
|
42
46
|
MARKETPLACE_CAPABILITY,
|
|
47
|
+
MAINTENANCE_CAPABILITY,
|
|
43
48
|
"scheduled_job_v1",
|
|
44
49
|
"reply_origin_v1",
|
|
45
50
|
"origin_decision_v1",
|
|
@@ -72,7 +77,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
|
|
|
72
77
|
const nativeWorkspaceReady = nativeWorkspacePlatform
|
|
73
78
|
&& readiness.resolverReady
|
|
74
79
|
&& readiness.nativeRuntimeAdaptersReady;
|
|
75
|
-
return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
|
|
80
|
+
return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => (((capability !== MARKETPLACE_CAPABILITY && capability !== MAINTENANCE_CAPABILITY && capability !== RUNTIME_TOOL_UPDATES_CAPABILITY) || nativeWorkspacePlatform)
|
|
76
81
|
&& (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
|
|
77
82
|
&& (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
|
|
78
83
|
&& (nativeWorkspacePlatform
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -103,6 +103,16 @@ export async function installManagedService(input) {
|
|
|
103
103
|
&& record.daemonHome === input.record.daemonHome);
|
|
104
104
|
if (registered !== undefined && sameManagedResource(registered, input.record)) {
|
|
105
105
|
const current = await readServiceDescriptor(input.spec);
|
|
106
|
+
// Descriptor replacement and registry commit can be interrupted separately.
|
|
107
|
+
// Adopt only the exact requested descriptor with all resource fields intact.
|
|
108
|
+
if (current === requestedDescriptor) {
|
|
109
|
+
assertRecordDescribesSpec(input);
|
|
110
|
+
await ensureServiceEnabled(input.spec, input.runner);
|
|
111
|
+
await replace(services.map((record) => record === registered
|
|
112
|
+
? { ...record, descriptorSha256: expectedDescriptorHash }
|
|
113
|
+
: record));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
106
116
|
if (current === null || serviceDescriptorSha256(current) !== registered.descriptorSha256) {
|
|
107
117
|
throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
|
|
108
118
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MaintenanceRequestSchema, MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
|
|
1
2
|
import { MarketplaceRequestSchema, marketplaceActive } from "./marketplace-protocol.js";
|
|
2
3
|
import { resolveMarketplaceScopes, runMarketplaceScope } from "./marketplace-runner.js";
|
|
3
4
|
export function createMarketplaceController(deps) {
|
|
@@ -11,22 +12,31 @@ export function createMarketplaceController(deps) {
|
|
|
11
12
|
const deadline = now() + 10 * 60_000;
|
|
12
13
|
let lease = null;
|
|
13
14
|
try {
|
|
14
|
-
const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
|
|
15
|
+
const scopes = request.type === "marketplace:operation" ? await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot) : [];
|
|
15
16
|
// 查询不使用任务执行槽;只有写入运行时共用市场/插件目录时才等待独占保护。
|
|
16
|
-
if (request.mode === "update") {
|
|
17
|
+
if (request.type === "marketplace:maintenance" || request.mode === "update") {
|
|
17
18
|
while (!stopped && now() < deadline && lease === null) {
|
|
18
19
|
lease = await deps.acquire();
|
|
19
20
|
if (!lease)
|
|
20
21
|
await wait();
|
|
21
22
|
}
|
|
22
23
|
if (!lease || stopped) {
|
|
23
|
-
operations.set(id, {
|
|
24
|
+
operations.set(id, { ...operations.get(id), status: "failed", errorCode: "runtime_busy" });
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
if (stopped || now() >= deadline)
|
|
28
29
|
throw new Error("operation_deadline");
|
|
29
|
-
operations.set(id, {
|
|
30
|
+
operations.set(id, { ...operations.get(id), status: "running" });
|
|
31
|
+
if (request.type === "marketplace:maintenance") {
|
|
32
|
+
if (!deps.maintain)
|
|
33
|
+
throw new Error("maintenance_unavailable");
|
|
34
|
+
const result = MaintenanceReportSchema.parse(await deps.maintain(request));
|
|
35
|
+
if (result.operationId !== id)
|
|
36
|
+
throw new Error("operation_mismatch");
|
|
37
|
+
operations.set(id, result);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
30
40
|
const results = [];
|
|
31
41
|
for (const scope of scopes) {
|
|
32
42
|
if (stopped || now() >= deadline)
|
|
@@ -41,7 +51,9 @@ export function createMarketplaceController(deps) {
|
|
|
41
51
|
}
|
|
42
52
|
catch {
|
|
43
53
|
const previous = operations.get(id);
|
|
44
|
-
operations.set(id,
|
|
54
|
+
operations.set(id, request.type === "marketplace:maintenance"
|
|
55
|
+
? { ...previous, status: "unknown", errorCode: "operation_unknown" }
|
|
56
|
+
: { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
|
|
45
57
|
}
|
|
46
58
|
finally {
|
|
47
59
|
try {
|
|
@@ -55,22 +67,23 @@ export function createMarketplaceController(deps) {
|
|
|
55
67
|
};
|
|
56
68
|
return {
|
|
57
69
|
handle(raw) {
|
|
58
|
-
const parsed = MarketplaceRequestSchema.safeParse(raw);
|
|
70
|
+
const parsed = MarketplaceRequestSchema.or(MaintenanceRequestSchema).safeParse(raw);
|
|
59
71
|
if (!parsed.success || stopped)
|
|
60
72
|
return { ok: false, error: "invalid_request" };
|
|
61
73
|
const request = parsed.data;
|
|
74
|
+
const version = request.type === "marketplace:maintenance" ? { version: 2 } : {};
|
|
62
75
|
const existing = operations.get(request.operationId);
|
|
63
76
|
if (existing)
|
|
64
77
|
return { ok: true, data: existing };
|
|
65
78
|
if (request.action === "status")
|
|
66
|
-
return { ok: true, data: { operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
|
|
79
|
+
return { ok: true, data: { ...version, operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
|
|
67
80
|
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
68
81
|
return { ok: false, error: "unsupported_platform" };
|
|
69
82
|
if (current !== null || [...operations.values()].some(x => marketplaceActive(x.status)))
|
|
70
83
|
return { ok: false, error: "runtime_busy" };
|
|
71
84
|
if (operations.size >= 100)
|
|
72
85
|
operations.delete(operations.keys().next().value);
|
|
73
|
-
const accepted = { operationId: request.operationId, status: "queued", results: [], errorCode: null };
|
|
86
|
+
const accepted = { ...version, operationId: request.operationId, status: "queued", results: [], errorCode: null };
|
|
74
87
|
operations.set(request.operationId, accepted);
|
|
75
88
|
current = execute(request).finally(() => { current = null; });
|
|
76
89
|
return { ok: true, data: accepted };
|