@nowcrew/daemon 0.6.98-beta.7 → 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.
Files changed (35) hide show
  1. package/README.md +20 -16
  2. package/dist/automatic-daemon-installer.js +15 -11
  3. package/dist/automatic-service-capture.js +27 -0
  4. package/dist/computer-main.js +0 -0
  5. package/dist/execution-journal-lock.js +51 -2
  6. package/dist/execution-protocol.js +2 -0
  7. package/dist/execution-runner.js +19 -13
  8. package/dist/host-execution-coordinator.js +10 -0
  9. package/dist/local-executor.js +8 -1
  10. package/dist/machine-info.js +4 -1
  11. package/dist/machine-runtime-environment.js +1 -0
  12. package/dist/main.js +0 -0
  13. package/dist/managed-service-lifecycle.js +10 -0
  14. package/dist/project-skills/execution-projection.js +4 -5
  15. package/dist/project-skills/reconciler.js +8 -9
  16. package/dist/project-skills/scanner.js +9 -2
  17. package/dist/project-skills/source-directory.js +13 -0
  18. package/dist/runtime-observation.js +219 -0
  19. package/dist/runtime-tool-update-bridge.js +126 -0
  20. package/dist/runtime-tool-update-controller.js +151 -0
  21. package/dist/runtime-tool-update-protocol.js +24 -0
  22. package/dist/runtime-tool-update-serve.js +54 -0
  23. package/dist/runtimes/claude-observation-runner.js +55 -0
  24. package/dist/runtimes/claude-thread-runner.js +17 -3
  25. package/dist/runtimes/claude.js +5 -3
  26. package/dist/runtimes/codex-app-server-runner.js +16 -2
  27. package/dist/runtimes/deepseek-harness-runner.js +16 -0
  28. package/dist/runtimes/kimi-acp-runner.js +19 -0
  29. package/dist/runtimes/observation-context.js +18 -0
  30. package/dist/runtimes/observation-mapper.js +314 -0
  31. package/dist/runtimes/opencode-runner.js +18 -4
  32. package/dist/runtimes/pi-runner.js +17 -1
  33. package/dist/serve.js +9 -0
  34. package/dist/token.js +4 -1
  35. package/package.json +9 -10
@@ -0,0 +1,219 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ export const OBSERVABILITY_ENV = "NOWWORK_OBSERVABILITY_POLICY";
4
+ export const ObservabilityPolicySchema = z.object({
5
+ version: z.literal(1), captureId: z.string().min(1).max(128),
6
+ contentMode: z.enum(["metadata", "redacted"]),
7
+ }).strict();
8
+ const Id = z.string().min(1).max(200);
9
+ const Count = z.number().int().nonnegative().safe();
10
+ export const RuntimeObservationSchema = z.object({
11
+ kind: z.literal("agent_observation"), version: z.literal(1), captureId: z.string().min(1).max(128),
12
+ sourceEventIndex: z.number().int().positive().safe(),
13
+ runtime: z.enum(["codex", "claude", "kimi", "hermes", "deepseek-harness", "opencode", "pi"]),
14
+ observationId: Id, parentObservationId: Id.optional(), nativeSessionId: Id.optional(),
15
+ nativeTurnId: Id.optional(), nativeCallId: Id.optional(),
16
+ type: z.enum(["agent", "tool", "generation", "event", "span", "retriever"]),
17
+ phase: z.enum(["start", "end", "event"]), name: z.string().min(1).max(200),
18
+ observedAt: z.string().datetime({ offset: true }), status: z.enum(["ok", "error"]).optional(),
19
+ input: z.string().max(8192).optional(), output: z.string().max(8192).optional(), inputBytes: Count.optional(), outputBytes: Count.optional(), model: z.string().min(1).max(200).optional(),
20
+ usage: z.object({ inputTokens: Count.optional(), outputTokens: Count.optional(), cacheReadTokens: Count.optional(),
21
+ cacheCreationTokens: Count.optional(), reasoningTokens: Count.optional(), costUsd: z.number().finite().nonnegative().optional() }).strict().optional(),
22
+ usageScope: z.enum(["generation", "turn", "execution"]).optional(),
23
+ unavailable: z.array(z.string().min(1).max(120)).max(30).optional(), truncated: z.boolean().optional(),
24
+ expectedCount: z.number().int().nonnegative().safe().optional(), dropped: z.number().int().nonnegative().safe().optional(),
25
+ }).strict().superRefine((event, ctx) => {
26
+ if (event.name === "capture.summary") {
27
+ if (event.phase !== "event" || event.type !== "event" || event.expectedCount === undefined
28
+ || event.sourceEventIndex !== event.expectedCount + 1)
29
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid capture summary" });
30
+ }
31
+ else if (event.expectedCount !== undefined || event.dropped !== undefined) {
32
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Only capture summary carries counts" });
33
+ }
34
+ });
35
+ export function parseObservabilityPolicy(raw) {
36
+ const parsed = ObservabilityPolicySchema.safeParse(raw);
37
+ return parsed.success ? parsed.data : undefined;
38
+ }
39
+ export function observationPolicyFromEnvironment(env) {
40
+ try {
41
+ return parseObservabilityPolicy(JSON.parse(env[OBSERVABILITY_ENV] ?? "null"));
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** 最后应用,清掉继承/用户配置中的同名变量,旧执行默认关闭。 */
48
+ export function observationEnvironment(env, policy) {
49
+ const clean = Object.fromEntries(Object.entries(env).filter(([key]) => key.toUpperCase() !== OBSERVABILITY_ENV));
50
+ const parsed = parseObservabilityPolicy(policy);
51
+ return parsed ? { ...clean, [OBSERVABILITY_ENV]: JSON.stringify(parsed) } : clean;
52
+ }
53
+ const REDACTED = "[REDACTED]";
54
+ // 与 Server 保持相同的边界规则;原 env/spec 不能先进入本地 journal 再由 Server 清洗。
55
+ const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/gu, "");
56
+ const forbiddenContainer = (key) => /^(?:env|envvars|runtimeenv|childenv|inheritedenv|environmentvariables|processenv|environmentplan|spec|executionspec|runtimeconfig|providerconfig)$/u.test(normalizeKey(key));
57
+ const sensitiveKey = (key) => /(?:apikey|accesskey(?:id)?|secretkey|secret|password|passwd|token|authorization|cookie|credential|privatekey|signature)$/u.test(normalizeKey(key))
58
+ || /^(?:key|sig|session|auth|authkey|authstring)$/u.test(normalizeKey(key));
59
+ function clip(value, limit) {
60
+ const end = value.charCodeAt(limit - 1);
61
+ return value.slice(0, end >= 0xd800 && end <= 0xdbff ? limit - 1 : limit);
62
+ }
63
+ function cleanUrl(raw) {
64
+ try {
65
+ const url = new URL(raw);
66
+ if (url.username || url.password) {
67
+ url.username = REDACTED;
68
+ url.password = "";
69
+ }
70
+ for (const name of [...url.searchParams.keys()]) {
71
+ if (sensitiveKey(name) || /^(?:code|ticket|sessionid|sas)$/iu.test(name) || /^(?:x-amz-|x-oss-|x-goog-)/iu.test(name))
72
+ url.searchParams.set(name, REDACTED);
73
+ }
74
+ if (url.hash && /(?:token|secret|password|credential|key|signature)/iu.test(url.hash))
75
+ url.hash = REDACTED;
76
+ return url.toString();
77
+ }
78
+ catch {
79
+ return REDACTED;
80
+ }
81
+ }
82
+ export function redactObservationText(value) {
83
+ const urls = [];
84
+ const text = value.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/giu, raw => {
85
+ const index = urls.push(cleanUrl(raw)) - 1;
86
+ return `__NOWWORK_TRACE_URL_${index}__`;
87
+ });
88
+ return text
89
+ .replace(/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/gu, REDACTED)
90
+ .replace(/\b(?:sk|pk)[-_]lf[-_][A-Za-z0-9_-]+\b/gu, REDACTED)
91
+ .replace(/\b(?:sk[-_][A-Za-z0-9_-]{6,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|npm_[A-Za-z0-9]{12,})\b/gu, REDACTED)
92
+ .replace(/\b(authorization\s*[:=]\s*)(?:(?:bearer|basic)\s+)?[^\r\n,;]+/giu, `$1${REDACTED}`)
93
+ .replace(/((?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?key|secret|password|passwd|token|cookie|credential|signature)|\b(?:key|session))\s*["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+)/giu, `$1${REDACTED}`)
94
+ .replace(/(\/\/[^\s"'`]+\/:_auth(?:token)?\s*=)\s*[^\s"'`]+/giu, `$1${REDACTED}`)
95
+ .replace(/__NOWWORK_TRACE_URL_(\d+)__/gu, (_match, index) => urls[Number(index)] ?? REDACTED);
96
+ }
97
+ function safeIdentity(value) {
98
+ // 含敏感值的 ID 用稳定别名,不能把不同调用都改成同一个 [REDACTED]。
99
+ return redactObservationText(value) === value ? value : `redacted-id:${createHash("sha256").update(value).digest("hex")}`;
100
+ }
101
+ function content(value) {
102
+ let truncated = false;
103
+ let remaining = 2000;
104
+ const visit = (item, depth) => {
105
+ if (--remaining < 0 || depth > 12) {
106
+ truncated = true;
107
+ return "[TRUNCATED]";
108
+ }
109
+ if (typeof item === "string") {
110
+ const text = redactObservationText(item);
111
+ if (text.length > 8192)
112
+ truncated = true;
113
+ return clip(text, 8192);
114
+ }
115
+ if (item === null || typeof item === "number" || typeof item === "boolean")
116
+ return item;
117
+ if (Array.isArray(item)) {
118
+ if (item.length > 100)
119
+ truncated = true;
120
+ return item.slice(0, 100).map(child => visit(child, depth + 1));
121
+ }
122
+ if (typeof item === "object") {
123
+ const entries = Object.entries(item);
124
+ if (entries.length > 100)
125
+ truncated = true;
126
+ return Object.fromEntries(entries.slice(0, 100).map(([key, child]) => [redactObservationText(key), forbiddenContainer(key) || sensitiveKey(key)
127
+ || (normalizeKey(key) === "environment" && typeof child === "object") ? REDACTED : visit(child, depth + 1)]));
128
+ }
129
+ return String(item);
130
+ };
131
+ let structured = value;
132
+ if (typeof value === "string" && /^[\s]*[\[{]/u.test(value)) {
133
+ try {
134
+ structured = JSON.parse(value);
135
+ }
136
+ catch { /* 不是完整JSON则走文本清洗 */ }
137
+ }
138
+ const safe = visit(structured, 0);
139
+ const text = typeof safe === "string" ? safe : JSON.stringify(safe);
140
+ return { text: clip(text, 8192), truncated: truncated || text.length > 8192 };
141
+ }
142
+ /** 校验与本地内容策略在进入现有 telemetry journal 前再执行一次。 */
143
+ export function safeRuntimeObservation(raw, policy) {
144
+ if (!policy)
145
+ return undefined;
146
+ const result = RuntimeObservationSchema.safeParse(raw);
147
+ if (!result.success || result.data.captureId !== policy.captureId)
148
+ return undefined;
149
+ const { input, output, ...metadata } = result.data;
150
+ const cleanMetadata = { ...metadata, name: redactObservationText(metadata.name),
151
+ ...(metadata.model ? { model: redactObservationText(metadata.model) } : {}),
152
+ ...(metadata.unavailable ? { unavailable: metadata.unavailable.map(value => clip(redactObservationText(value), 120)) } : {}),
153
+ ...Object.fromEntries(["observationId", "parentObservationId", "nativeSessionId", "nativeTurnId", "nativeCallId"]
154
+ .flatMap(key => metadata[key] === undefined ? [] : [[key, safeIdentity(metadata[key])]])),
155
+ };
156
+ const inputContent = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
157
+ const outputContent = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
158
+ const safe = policy.contentMode === "metadata" ? cleanMetadata : {
159
+ ...cleanMetadata,
160
+ ...(inputContent ? { input: inputContent.text } : {}),
161
+ ...(outputContent ? { output: outputContent.text } : {}),
162
+ ...(inputContent?.truncated || outputContent?.truncated ? { truncated: true } : {}),
163
+ };
164
+ const checked = RuntimeObservationSchema.safeParse(safe);
165
+ return checked.success && Buffer.byteLength(JSON.stringify(checked.data)) <= 24 * 1024 ? checked.data : undefined;
166
+ }
167
+ /** 仅短命 runner 使用;不持久化、不联网、不影响业务事件流。 */
168
+ export function createRuntimeObservationCapture(runtime, env = process.env, emit = event => { process.stdout.write(`${JSON.stringify(event)}\n`); }) {
169
+ const policy = observationPolicyFromEnvironment(env);
170
+ let index = 0;
171
+ let dropped = 0;
172
+ let finished = false;
173
+ const send = (fields, summary = false) => {
174
+ if (!policy || finished)
175
+ return;
176
+ if (!summary && index >= 20_000) {
177
+ dropped += 1;
178
+ return;
179
+ }
180
+ try {
181
+ const { input, output, ...metadata } = fields;
182
+ const bytes = (value) => Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value));
183
+ const inputText = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
184
+ const outputText = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
185
+ let candidate = {
186
+ ...metadata, kind: "agent_observation", version: 1, captureId: policy.captureId,
187
+ runtime, sourceEventIndex: index + 1, observedAt: fields.observedAt ?? new Date().toISOString(),
188
+ ...(input === undefined ? {} : { inputBytes: bytes(input) }), ...(output === undefined ? {} : { outputBytes: bytes(output) }),
189
+ ...(inputText ? { input: inputText.text } : {}), ...(outputText ? { output: outputText.text } : {}),
190
+ ...(inputText?.truncated || outputText?.truncated ? { truncated: true } : {}),
191
+ };
192
+ if (Buffer.byteLength(JSON.stringify(candidate)) > 24 * 1024) {
193
+ candidate = { ...candidate, ...(inputText ? { input: clip(inputText.text, 1500) } : {}),
194
+ ...(outputText ? { output: clip(outputText.text, 1500) } : {}), truncated: true };
195
+ }
196
+ const safe = safeRuntimeObservation(candidate, policy);
197
+ if (!safe) {
198
+ dropped += 1;
199
+ return;
200
+ }
201
+ index += 1;
202
+ emit({ type: "nowwork.observation", observation: safe });
203
+ }
204
+ catch {
205
+ dropped += 1; /* 诊断永远不改变 runtime 结果 */
206
+ }
207
+ };
208
+ return {
209
+ enabled: policy !== undefined,
210
+ observe: fields => send(fields),
211
+ finish() {
212
+ if (finished)
213
+ return;
214
+ send({ observationId: "capture.summary", type: "event", phase: "event", name: "capture.summary",
215
+ expectedCount: index, dropped, ...(index === 0 ? { unavailable: ["runtime_events"] } : {}) }, true);
216
+ finished = true;
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { RuntimeToolResponseSchema } from "./runtime-tool-update-protocol.js";
7
+ export function runtimeToolEnvironment(env) {
8
+ // Keep only machine identity and lookup settings. Never pass runtime/control-plane credentials or Node injection flags.
9
+ return Object.fromEntries(Object.entries(env).filter(([key]) => /^(PATH|HOME|USER|LOGNAME|SHELL|TMPDIR|TMP|TEMP|LANG|LC_[A-Z_]+|XDG_CONFIG_HOME|XDG_CACHE_HOME)$/u.test(key)));
10
+ }
11
+ async function readReport(dir) {
12
+ const file = join(dir, "report.json");
13
+ if ((await stat(file)).size > 256_000)
14
+ throw new Error("invalid_report");
15
+ return RuntimeToolResponseSchema.parse(JSON.parse(await readFile(file, "utf8")));
16
+ }
17
+ async function stopGroup(pid) {
18
+ if (!pid)
19
+ return true;
20
+ try {
21
+ process.kill(-pid, "SIGKILL");
22
+ }
23
+ catch (error) {
24
+ return error.code === "ESRCH";
25
+ }
26
+ const deadline = Date.now() + 3000;
27
+ while (Date.now() < deadline) {
28
+ try {
29
+ process.kill(-pid, 0);
30
+ }
31
+ catch (error) {
32
+ return error.code === "ESRCH";
33
+ }
34
+ await new Promise(resolve => setTimeout(resolve, 50));
35
+ }
36
+ return false;
37
+ }
38
+ export function createRuntimeToolProgressPoller(read, progress, intervalMs = 250) {
39
+ let active = true;
40
+ let pending = null;
41
+ const interval = setInterval(() => {
42
+ if (!active || pending || !progress)
43
+ return;
44
+ pending = read().then(async (data) => { if (active)
45
+ await progress(data); })
46
+ .catch(() => { }).finally(() => { pending = null; });
47
+ }, intervalMs);
48
+ return { async stop() { active = false; clearInterval(interval); await pending; } };
49
+ }
50
+ async function supervise(dir, env, start, progress, protectProcessGroup) {
51
+ const child = spawn(process.execPath, [join(dir, "runner.mjs"), "--managed-worker"], { cwd: dir, env, detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"] });
52
+ const closed = new Promise(resolve => { child.once("error", () => resolve(null)); child.once("close", resolve); });
53
+ const polling = createRuntimeToolProgressPoller(() => readReport(dir), progress);
54
+ let expired = false;
55
+ const timer = setTimeout(() => { expired = true; void stopGroup(child.pid); }, start ? 12 * 60_000 : 120_000);
56
+ let launchFailed = false;
57
+ try {
58
+ if (start) {
59
+ if (!protectProcessGroup || !child.pid)
60
+ throw new Error("lease_protection_unavailable");
61
+ await protectProcessGroup(child.pid);
62
+ }
63
+ await new Promise((resolve, reject) => child.send({ type: "runtime-tools:go" }, error => error ? reject(error) : resolve()));
64
+ }
65
+ catch {
66
+ launchFailed = true;
67
+ await stopGroup(child.pid);
68
+ }
69
+ const code = await closed;
70
+ clearTimeout(timer);
71
+ await polling.stop();
72
+ // A successful parent exit does not prove its package-manager descendants stopped.
73
+ const stopped = await stopGroup(child.pid);
74
+ return { stopped, success: stopped && !expired && !launchFailed && code === 0 };
75
+ }
76
+ export async function runRuntimeToolBridge(config, request, acquireEnv, progress, deps = {}) {
77
+ let dir;
78
+ let started = false;
79
+ let processStopped = true;
80
+ const failure = (errorCode) => ({ processStopped, data: request.action === "start" ? { operation: {
81
+ operationId: request.operationId, state: started ? "unknown" : "failed", currentIndex: 0, targets: request.targets,
82
+ items: request.targets.map(t => ({ runtime: t.installation.runtime, state: started ? "unknown" : "failed", errorCode })),
83
+ updatedAt: new Date().toISOString(), errorCode,
84
+ } } : {} });
85
+ try {
86
+ const response = await (deps.fetch ?? fetch)(`${config.serverUrl.replace(/\/+$/, "")}/machine/runtime-tool-updates/runner/${request.runner.releaseId}`, {
87
+ headers: { authorization: `Bearer ${config.machineToken}` }, redirect: "error", signal: AbortSignal.timeout(15_000),
88
+ });
89
+ if (!response.ok || !response.body)
90
+ return failure("runner_unavailable");
91
+ const chunks = [];
92
+ let bytes = 0;
93
+ for await (const chunk of response.body) {
94
+ bytes += chunk.length;
95
+ if (bytes > 512_000)
96
+ return failure("runner_unavailable");
97
+ chunks.push(chunk);
98
+ }
99
+ const content = Buffer.concat(chunks);
100
+ if (createHash("sha256").update(content).digest("hex") !== request.runner.sha256)
101
+ return failure("runner_unavailable");
102
+ const env = runtimeToolEnvironment(await acquireEnv());
103
+ dir = await mkdtemp(join(tmpdir(), "nowwork-runtime-update-"));
104
+ await writeFile(join(dir, "runner.mjs"), content, { mode: 0o600 });
105
+ await writeFile(join(dir, "request.json"), JSON.stringify(request), { mode: 0o600 });
106
+ started = true;
107
+ processStopped = false;
108
+ const outcome = await supervise(dir, env, request.action === "start", progress, deps.protectProcessGroup);
109
+ processStopped = outcome.stopped;
110
+ if (!outcome.success)
111
+ return failure("operation_unknown");
112
+ const data = await readReport(dir);
113
+ if (request.action === "start" && data.operation?.operationId !== request.operationId)
114
+ return failure("operation_unknown");
115
+ if (request.action === "inspect" && !data.snapshot)
116
+ return failure("inspection_failed");
117
+ return { data, processStopped };
118
+ }
119
+ catch {
120
+ return failure(started ? "operation_unknown" : "runner_unavailable");
121
+ }
122
+ finally {
123
+ if (dir && processStopped)
124
+ await rm(dir, { recursive: true, force: true });
125
+ }
126
+ }
@@ -0,0 +1,151 @@
1
+ import { RuntimeToolRequestSchema, RuntimeToolResponseSchema } from "./runtime-tool-update-protocol.js";
2
+ const finalStates = new Set(["completed", "failed", "unknown", "cancelled"]);
3
+ const phaseRank = (state) => ({ queued: 0, preflight: 1, installing: 2, verifying: 3, completed: 4, failed: 4, unknown: 4, cancelled: 4 })[state];
4
+ function advancesProgress(previous, next) {
5
+ if (finalStates.has(previous.state) || finalStates.has(next.state)
6
+ || next.operationId !== previous.operationId || JSON.stringify(next.targets) !== JSON.stringify(previous.targets)
7
+ || next.currentIndex < previous.currentIndex || next.currentIndex >= previous.targets.length
8
+ || (next.currentIndex === previous.currentIndex && phaseRank(next.state) < phaseRank(previous.state)))
9
+ return false;
10
+ return previous.items.every((item, index) => {
11
+ const candidate = next.items[index];
12
+ if (!candidate || candidate.runtime !== item.runtime)
13
+ return false;
14
+ return finalStates.has(item.state) ? candidate.state === item.state && candidate.errorCode === item.errorCode
15
+ : phaseRank(candidate.state) >= phaseRank(item.state);
16
+ });
17
+ }
18
+ export function createRuntimeToolUpdateController(deps) {
19
+ const entries = new Map();
20
+ let current = null;
21
+ let stopped = false;
22
+ let orphaned = false;
23
+ const now = deps.now ?? Date.now;
24
+ const date = () => new Date(now()).toISOString();
25
+ const wait = deps.wait ?? (() => new Promise(resolve => setTimeout(resolve, 500)));
26
+ const fail = (error) => ({ ok: false, error });
27
+ const result = (data) => ({ ok: true, data });
28
+ const set = (entry, patch) => { entry.operation = { ...entry.operation, ...patch, updatedAt: date() }; };
29
+ async function execute(entry) {
30
+ const deadline = now() + 10 * 60_000;
31
+ let acceptingProgress = false;
32
+ let lease = null;
33
+ try {
34
+ while (!stopped && entry.operation.state === "queued" && now() < deadline && !lease) {
35
+ lease = await deps.acquire();
36
+ if (!lease)
37
+ await wait();
38
+ }
39
+ if (entry.operation.state === "cancelled")
40
+ return;
41
+ if (!lease || stopped || now() >= deadline) {
42
+ set(entry, { state: "failed", errorCode: "runtime_busy" });
43
+ return;
44
+ }
45
+ set(entry, { state: "preflight" });
46
+ // Mark unsafe before spawning: an exception or lost response cannot authorize another writer.
47
+ entry.processStopped = false;
48
+ acceptingProgress = true;
49
+ const output = await deps.run(entry.request, raw => {
50
+ if (!acceptingProgress)
51
+ return;
52
+ const parsed = RuntimeToolResponseSchema.safeParse(raw);
53
+ if (parsed.success && parsed.data.operation && advancesProgress(entry.operation, parsed.data.operation))
54
+ entry.operation = parsed.data.operation;
55
+ }, lease.protectProcessGroup);
56
+ acceptingProgress = false;
57
+ const data = RuntimeToolResponseSchema.parse(output.data);
58
+ entry.processStopped = output.processStopped;
59
+ if (!data.operation || data.operation.operationId !== entry.operation.operationId
60
+ || JSON.stringify(data.operation.targets) !== JSON.stringify(entry.request.targets)
61
+ || data.operation.currentIndex < entry.operation.currentIndex || !finalStates.has(data.operation.state))
62
+ throw new Error("operation_mismatch");
63
+ entry.operation = data.operation;
64
+ if (!entry.processStopped)
65
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
66
+ }
67
+ catch {
68
+ acceptingProgress = false;
69
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
70
+ }
71
+ finally {
72
+ acceptingProgress = false;
73
+ // Retain the barrier if process-group termination could not be proved.
74
+ if (entry.processStopped) {
75
+ try {
76
+ await lease?.release();
77
+ }
78
+ catch {
79
+ entry.processStopped = false;
80
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ async handle(raw) {
87
+ const parsed = RuntimeToolRequestSchema.safeParse(raw);
88
+ if (!parsed.success || stopped)
89
+ return fail("invalid_request");
90
+ const request = parsed.data;
91
+ if (request.action === "inspect") {
92
+ try {
93
+ const data = RuntimeToolResponseSchema.parse((await deps.run(request)).data);
94
+ return data.snapshot ? result(data) : fail("inspection_failed");
95
+ }
96
+ catch {
97
+ return fail("inspection_failed");
98
+ }
99
+ }
100
+ const entry = entries.get(request.operationId);
101
+ if (request.action === "start") {
102
+ if (entry)
103
+ return result({ operation: entry.operation });
104
+ if (orphaned || [...entries.values()].some(x => x.operation.state === "unknown"))
105
+ return fail("operation_unknown");
106
+ if (current)
107
+ return fail("runtime_busy");
108
+ if (entries.size >= 100)
109
+ return fail("operation_limit");
110
+ if (new Set(request.targets.map(t => t.installation.runtime)).size !== request.targets.length)
111
+ return fail("invalid_request");
112
+ const operation = { operationId: request.operationId, state: "queued", currentIndex: 0, targets: request.targets,
113
+ items: request.targets.map(t => ({ runtime: t.installation.runtime, state: "queued", errorCode: null })), updatedAt: date(), errorCode: null };
114
+ const next = { operation, request, processStopped: true };
115
+ entries.set(request.operationId, next);
116
+ current = execute(next).finally(() => { current = null; });
117
+ return result({ operation });
118
+ }
119
+ if (!entry) {
120
+ orphaned = true;
121
+ return fail("operation_unknown");
122
+ }
123
+ if (request.action === "status")
124
+ return result({ operation: entry.operation });
125
+ if (request.action === "cancel") {
126
+ if (entry.operation.state !== "queued")
127
+ return fail("already_started");
128
+ set(entry, { state: "cancelled", items: entry.operation.items.map(x => ({ ...x, state: "cancelled" })) });
129
+ return result({ operation: entry.operation });
130
+ }
131
+ if (!entry.processStopped || current)
132
+ return fail("operation_unknown");
133
+ try {
134
+ const output = await deps.run({ action: "inspect", runner: entry.request.runner });
135
+ const snapshot = RuntimeToolResponseSchema.parse(output.data).snapshot;
136
+ if (!snapshot || !output.processStopped)
137
+ return fail("operation_unknown");
138
+ const matches = entry.operation.targets.every(target => snapshot.tools.some(tool => tool.runtime === target.installation.runtime
139
+ && tool.version === target.targetVersion && tool.path === target.installation.path && tool.source === target.installation.source
140
+ && tool.prefix === target.installation.prefix && tool.brewPath === target.installation.brewPath && tool.automatic));
141
+ set(entry, { state: matches ? "completed" : "failed", errorCode: matches ? null : "verification_failed", snapshot,
142
+ items: entry.operation.items.map(x => ({ ...x, state: matches ? "completed" : "failed", errorCode: matches ? null : "verification_failed" })) });
143
+ return result({ operation: entry.operation, snapshot });
144
+ }
145
+ catch {
146
+ return fail("inspection_failed");
147
+ }
148
+ },
149
+ async stop() { stopped = true; await current; },
150
+ };
151
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ export const RUNTIME_TOOL_UPDATES_CAPABILITY = "runtime_tool_updates_v1";
3
+ const boundedPath = z.string().min(1).max(4096);
4
+ export const RuntimeToolRunnerSchema = z.object({ releaseId: z.string().regex(/^[a-f0-9]{64}$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
5
+ export const RuntimeToolInstallationSchema = z.object({
6
+ runtime: z.enum(["codex", "claude"]), path: boundedPath, realPath: boundedPath,
7
+ version: z.string().max(100).nullable(), source: z.enum(["npm", "homebrew", "native", "unknown"]),
8
+ nodeVersion: z.string().max(100).nullable(), nodePath: boundedPath.nullable(), npmPath: boundedPath.nullable(), prefix: boundedPath.nullable(),
9
+ brewPath: boundedPath.nullable(), brewKind: z.enum(["formula", "cask"]).nullable(), brewName: z.enum(["codex", "claude-code"]).nullable(),
10
+ fingerprint: z.string().regex(/^[a-f0-9]{64}$/), alternatives: z.array(boundedPath).max(32), automatic: z.boolean(), reason: z.string().max(100).nullable(),
11
+ }).strict();
12
+ export const RuntimeToolSnapshotSchema = z.object({ checkedAt: z.string().datetime(), tools: z.array(RuntimeToolInstallationSchema).max(2) }).strict();
13
+ export const RuntimeToolTargetSchema = z.object({ installation: RuntimeToolInstallationSchema, targetVersion: z.string().regex(/^\d+\.\d+\.\d+$/).max(100) }).strict();
14
+ export const RuntimeToolStateSchema = z.enum(["queued", "preflight", "installing", "verifying", "completed", "failed", "unknown", "cancelled"]);
15
+ export const RuntimeToolOperationSchema = z.object({ operationId: z.string().uuid(), state: RuntimeToolStateSchema, currentIndex: z.number().int().min(0).max(2),
16
+ targets: z.array(RuntimeToolTargetSchema).min(1).max(2), items: z.array(z.object({ runtime: z.enum(["codex", "claude"]), state: RuntimeToolStateSchema, errorCode: z.string().max(100).nullable() }).strict()).max(2),
17
+ updatedAt: z.string().datetime(), errorCode: z.string().max(100).nullable(), snapshot: RuntimeToolSnapshotSchema.optional(),
18
+ }).strict();
19
+ export const RuntimeToolRequestSchema = z.discriminatedUnion("action", [
20
+ z.object({ action: z.literal("inspect"), runner: RuntimeToolRunnerSchema }).strict(),
21
+ z.object({ action: z.literal("start"), operationId: z.string().uuid(), targets: z.array(RuntimeToolTargetSchema).min(1).max(2), runner: RuntimeToolRunnerSchema }).strict(),
22
+ ...["status", "cancel", "verify"].map(action => z.object({ action: z.literal(action), operationId: z.string().uuid() }).strict()),
23
+ ]);
24
+ export const RuntimeToolResponseSchema = z.object({ snapshot: RuntimeToolSnapshotSchema.optional(), operation: RuntimeToolOperationSchema.optional() }).strict();
@@ -0,0 +1,54 @@
1
+ import { prepareDaemonRestart } from "./daemon-updater.js";
2
+ import { createRuntimeToolUpdateController } from "./runtime-tool-update-controller.js";
3
+ import { augmentedPath } from "./runtime-path.js";
4
+ import { runRuntimeToolBridge } from "./runtime-tool-update-bridge.js";
5
+ export async function acquireRuntimeToolLease(localSlots, host) {
6
+ const local = await prepareDaemonRestart({ localSlots });
7
+ if (!local.ok)
8
+ return null;
9
+ try {
10
+ const shared = await host.tryAcquireExclusiveExecution();
11
+ if (!shared) {
12
+ await local.release();
13
+ return null;
14
+ }
15
+ let released = false;
16
+ return { protectProcessGroup: async (pid) => {
17
+ if (!shared.protectProcessGroup)
18
+ throw new Error("lease protection unavailable");
19
+ await shared.protectProcessGroup(pid);
20
+ }, async release() { if (released)
21
+ return; await shared.release(); await local.release(); released = true; } };
22
+ }
23
+ catch (error) {
24
+ await local.release();
25
+ throw error;
26
+ }
27
+ }
28
+ export function createServeRuntimeToolUpdates(config, localSlots, host, acquireEnv, refresh) {
29
+ const controller = createRuntimeToolUpdateController({ acquire: () => acquireRuntimeToolLease(localSlots, host),
30
+ run: async (request, progress, protectProcessGroup) => {
31
+ const result = await runRuntimeToolBridge(config, request, async () => {
32
+ const env = await acquireEnv();
33
+ return { ...env, PATH: augmentedPath(env, {}, process.platform, "append") };
34
+ }, progress, { ...(protectProcessGroup ? { protectProcessGroup } : {}) });
35
+ if (request.action === "start" && result.processStopped)
36
+ refresh();
37
+ return result;
38
+ } });
39
+ return {
40
+ handle(raw, send) {
41
+ if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "runtime-tools:operation")
42
+ return false;
43
+ const { reqId, type: _type, ...request } = raw;
44
+ if (typeof reqId === "string") {
45
+ if (process.platform !== "linux" && process.platform !== "darwin")
46
+ send({ type: "fs:result", reqId, ok: false, error: "unsupported_platform" });
47
+ else
48
+ void controller.handle(request).then(result => send({ type: "fs:result", reqId, ...result })).catch(() => send({ type: "fs:result", reqId, ok: false, error: "operation_unknown" }));
49
+ }
50
+ return true;
51
+ },
52
+ stop: controller.stop,
53
+ };
54
+ }
@@ -0,0 +1,55 @@
1
+ /** 无线程 Claude 的短命观察桥:保留原命令/参数,不引入线程或原生会话约束。 */
2
+ import { createInterface } from "node:readline";
3
+ import { pathToFileURL } from "node:url";
4
+ import spawn from "cross-spawn";
5
+ import { z } from "zod";
6
+ import { createRuntimeObservationCapture } from "../runtime-observation.js";
7
+ import { createObservationMapper } from "./observation-mapper.js";
8
+ import { observeClaudeContext } from "./observation-context.js";
9
+ const Input = z.object({ bin: z.string().min(1), args: z.array(z.string()) }).strict();
10
+ export async function runClaudeObservation(bin, args) {
11
+ const capture = createRuntimeObservationCapture("claude");
12
+ const observer = createObservationMapper("claude", capture);
13
+ await observeClaudeContext(observer, args, args.at(-1) ?? "");
14
+ const child = spawn(bin, [...args], { cwd: process.cwd(), env: process.env, stdio: ["ignore", "pipe", "pipe"], detached: false });
15
+ const stopped = new Promise((resolve, reject) => {
16
+ child.once("error", reject);
17
+ child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
18
+ });
19
+ void stopped.catch(() => undefined);
20
+ const cancel = () => { child.kill("SIGTERM"); };
21
+ process.once("SIGTERM", cancel);
22
+ process.once("SIGINT", cancel);
23
+ try {
24
+ if (!child.stdout || !child.stderr)
25
+ throw new Error("Claude observation runner requires output streams");
26
+ child.stderr.pipe(process.stderr, { end: false });
27
+ for await (const line of createInterface({ input: child.stdout })) {
28
+ try {
29
+ observer.observe(JSON.parse(line));
30
+ }
31
+ catch { /* 原非 JSON 输出仍透传 */ }
32
+ process.stdout.write(`${line}\n`);
33
+ }
34
+ return await stopped;
35
+ }
36
+ finally {
37
+ observer.finish();
38
+ capture.finish();
39
+ process.off("SIGTERM", cancel);
40
+ process.off("SIGINT", cancel);
41
+ }
42
+ }
43
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
44
+ let raw = "";
45
+ for await (const chunk of process.stdin) {
46
+ raw += String(chunk);
47
+ if (Buffer.byteLength(raw) > 8 * 1024 * 1024)
48
+ throw new Error("Claude observation input too large");
49
+ }
50
+ const input = Input.parse(JSON.parse(raw));
51
+ runClaudeObservation(input.bin, input.args).then(code => { process.exitCode = code; }).catch(() => {
52
+ process.stderr.write("Claude observation runner failed\n");
53
+ process.exitCode = 1;
54
+ });
55
+ }