@sentropic/h2a-runtime 0.98.6 → 0.98.7-rc.2
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/dist/agent-launch-args.d.ts.map +1 -1
- package/dist/agent-launch-args.js +5 -5
- package/dist/agent-launch-args.js.map +1 -1
- package/dist/agy-claude.d.ts +2 -0
- package/dist/agy-claude.d.ts.map +1 -0
- package/dist/agy-claude.js +3 -0
- package/dist/agy-claude.js.map +1 -0
- package/dist/agy-headless.d.ts +11 -0
- package/dist/agy-headless.d.ts.map +1 -0
- package/dist/agy-headless.js +104 -0
- package/dist/agy-headless.js.map +1 -0
- package/dist/attach.d.ts +2 -0
- package/dist/attach.d.ts.map +1 -1
- package/dist/attach.js +73 -6
- package/dist/attach.js.map +1 -1
- package/dist/auth-login.d.ts +24 -1
- package/dist/auth-login.d.ts.map +1 -1
- package/dist/auth-login.js +132 -4
- package/dist/auth-login.js.map +1 -1
- package/dist/central-mcp.d.ts +2 -0
- package/dist/central-mcp.d.ts.map +1 -1
- package/dist/central-mcp.js +90 -3
- package/dist/central-mcp.js.map +1 -1
- package/dist/claude-code-cli-isolation.d.ts +53 -0
- package/dist/claude-code-cli-isolation.d.ts.map +1 -0
- package/dist/claude-code-cli-isolation.js +277 -0
- package/dist/claude-code-cli-isolation.js.map +1 -0
- package/dist/claude-code-cli-lifecycle.d.ts +80 -0
- package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
- package/dist/claude-code-cli-lifecycle.js +402 -0
- package/dist/claude-code-cli-lifecycle.js.map +1 -0
- package/dist/claude-code-cli-runner.d.ts +89 -0
- package/dist/claude-code-cli-runner.d.ts.map +1 -0
- package/dist/claude-code-cli-runner.js +659 -0
- package/dist/claude-code-cli-runner.js.map +1 -0
- package/dist/claude-seat-input.d.ts +47 -0
- package/dist/claude-seat-input.d.ts.map +1 -0
- package/dist/claude-seat-input.js +272 -0
- package/dist/claude-seat-input.js.map +1 -0
- package/dist/claude-seat-mesh-contract.d.ts +88 -0
- package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
- package/dist/claude-seat-mesh-contract.js +13 -0
- package/dist/claude-seat-mesh-contract.js.map +1 -0
- package/dist/cli-help-groups.js +1 -1
- package/dist/cli-help-groups.js.map +1 -1
- package/dist/crash-log.d.ts +112 -0
- package/dist/crash-log.d.ts.map +1 -0
- package/dist/crash-log.js +560 -0
- package/dist/crash-log.js.map +1 -0
- package/dist/delegate.d.ts +10 -0
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +29 -0
- package/dist/delegate.js.map +1 -1
- package/dist/gateway-host/claude-seat-client.d.ts +32 -0
- package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
- package/dist/gateway-host/claude-seat-client.js +38 -0
- package/dist/gateway-host/claude-seat-client.js.map +1 -0
- package/dist/gateway-host/host.d.ts +6 -0
- package/dist/gateway-host/host.d.ts.map +1 -1
- package/dist/gateway-host/host.js +24 -3
- package/dist/gateway-host/host.js.map +1 -1
- package/dist/gateway-host/live-declarations.d.ts +58 -0
- package/dist/gateway-host/live-declarations.d.ts.map +1 -0
- package/dist/gateway-host/live-declarations.js +429 -0
- package/dist/gateway-host/live-declarations.js.map +1 -0
- package/dist/gateway-host/live-github.d.ts +11 -0
- package/dist/gateway-host/live-github.d.ts.map +1 -0
- package/dist/gateway-host/live-github.js +374 -0
- package/dist/gateway-host/live-github.js.map +1 -0
- package/dist/gateway-host/live-object-store.d.ts +35 -0
- package/dist/gateway-host/live-object-store.d.ts.map +1 -0
- package/dist/gateway-host/live-object-store.js +447 -0
- package/dist/gateway-host/live-object-store.js.map +1 -0
- package/dist/gateway-host/live-registration.d.ts +162 -0
- package/dist/gateway-host/live-registration.d.ts.map +1 -0
- package/dist/gateway-host/live-registration.js +455 -0
- package/dist/gateway-host/live-registration.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +290 -23
- package/dist/index.js.map +1 -1
- package/dist/llm-mesh-accounts.d.ts +70 -4
- package/dist/llm-mesh-accounts.d.ts.map +1 -1
- package/dist/llm-mesh-accounts.js +296 -9
- package/dist/llm-mesh-accounts.js.map +1 -1
- package/dist/native-host.d.ts +1 -1
- package/dist/native-host.d.ts.map +1 -1
- package/dist/native-host.js +30 -17
- package/dist/native-host.js.map +1 -1
- package/dist/native-terminal/host.d.ts +6 -1
- package/dist/native-terminal/host.d.ts.map +1 -1
- package/dist/native-terminal/host.js +6 -25
- package/dist/native-terminal/host.js.map +1 -1
- package/dist/native-terminal/journal.d.ts +5 -0
- package/dist/native-terminal/journal.d.ts.map +1 -1
- package/dist/native-terminal/journal.js +28 -16
- package/dist/native-terminal/journal.js.map +1 -1
- package/dist/native-terminal/op.d.ts +13 -0
- package/dist/native-terminal/op.d.ts.map +1 -1
- package/dist/native-terminal/op.js +64 -5
- package/dist/native-terminal/op.js.map +1 -1
- package/dist/proc-identity.d.ts +31 -0
- package/dist/proc-identity.d.ts.map +1 -1
- package/dist/proc-identity.js +54 -0
- package/dist/proc-identity.js.map +1 -1
- package/dist/prompt-delivery.d.ts +3 -0
- package/dist/prompt-delivery.d.ts.map +1 -1
- package/dist/prompt-delivery.js +8 -3
- package/dist/prompt-delivery.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +79 -16
- package/dist/run.js.map +1 -1
- package/dist/terminal-reset.d.ts +104 -0
- package/dist/terminal-reset.d.ts.map +1 -0
- package/dist/terminal-reset.js +229 -0
- package/dist/terminal-reset.js.map +1 -0
- package/dist/tmux.d.ts +26 -4
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +60 -25
- package/dist/tmux.js.map +1 -1
- package/dist/vendor/engram-live/contracts.d.ts +825 -0
- package/dist/vendor/engram-live/contracts.js +1734 -0
- package/dist/vendor/engram-live/integration.d.ts +113 -0
- package/dist/vendor/engram-live/integration.js +254 -0
- package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
- package/dist/vendor/engram-live/pr-sync.js +411 -0
- package/dist/vendor/engram-live/registration.d.ts +138 -0
- package/dist/vendor/engram-live/registration.js +358 -0
- package/dist/vendor/engram-live/retention.d.ts +56 -0
- package/dist/vendor/engram-live/retention.js +179 -0
- package/package.json +2 -2
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code CLI Runner: executes the official `claude` CLI (resolved
|
|
3
|
+
* from `PATH` at each attempt) with NDJSON streaming, strict isolation,
|
|
4
|
+
* process reaping, credential read-back, and static error handling.
|
|
5
|
+
* No version pin, no digest fingerprint, no attestation: no other seat
|
|
6
|
+
* carries such machinery, and none is claimed here.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
10
|
+
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
11
|
+
import { buildIsolatedEnvironment, cleanClaudeRunSandbox, createClaudeRunSandbox, recordChildPidInSandbox, verifyCredentialNotRewritten, } from "./claude-code-cli-isolation.js";
|
|
12
|
+
import { checkTokenTtlAdmission, reapProcessTree, sweepOrphanSandboxes, } from "./claude-code-cli-lifecycle.js";
|
|
13
|
+
/**
|
|
14
|
+
* Returns true for a regular file with the execute bit set. A directory or
|
|
15
|
+
* a non-executable file named `claude` earlier on PATH must never mask the
|
|
16
|
+
* real CLI (the failure would otherwise surface only at spawn).
|
|
17
|
+
*/
|
|
18
|
+
function isExecutableRegularFile(candidate) {
|
|
19
|
+
try {
|
|
20
|
+
accessSync(candidate, constants.X_OK);
|
|
21
|
+
return statSync(candidate).isFile();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the official `claude` CLI from `PATH` (Linux-first; Windows
|
|
29
|
+
* resolves `claude` and `claude.exe` only — `.cmd`/`.bat` shims are
|
|
30
|
+
* excluded because spawn launches without a shell). Returns the first
|
|
31
|
+
* absolute regular executable candidate, or undefined when none is found.
|
|
32
|
+
* Never throws, never executes anything.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveClaudeBinaryFromPath(env = process.env) {
|
|
35
|
+
const pathValue = env.PATH ?? "";
|
|
36
|
+
if (!pathValue)
|
|
37
|
+
return undefined;
|
|
38
|
+
const extensions = process.platform === "win32" ? ["", ".exe"] : [""];
|
|
39
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
40
|
+
if (!dir || !isAbsolute(dir))
|
|
41
|
+
continue;
|
|
42
|
+
for (const extension of extensions) {
|
|
43
|
+
const candidate = join(dir, `claude${extension}`);
|
|
44
|
+
try {
|
|
45
|
+
if (isExecutableRegularFile(candidate))
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Unreadable directory entry: try the next candidate.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export const DEFAULT_MAX_FRAME_BYTES = 65536; // 64 KiB
|
|
56
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
57
|
+
export const DEFAULT_RUN_DEADLINE_MS = 120_000; // 2 minutes
|
|
58
|
+
/**
|
|
59
|
+
* Strict allowlist of typed tool identifiers permitted for the CLI --tools flag.
|
|
60
|
+
* Caller-provided tool names are never passed to --tools.
|
|
61
|
+
*/
|
|
62
|
+
export const ALLOWED_TYPED_TOOL_IDENTIFIERS = [];
|
|
63
|
+
/**
|
|
64
|
+
* Known built-in tools of the Claude CLI. Callers cannot request these names.
|
|
65
|
+
*/
|
|
66
|
+
export const BUILTIN_CLI_TOOLS = new Set([
|
|
67
|
+
"Bash",
|
|
68
|
+
"BashOutput",
|
|
69
|
+
"Edit",
|
|
70
|
+
"MultiEdit",
|
|
71
|
+
"Read",
|
|
72
|
+
"Write",
|
|
73
|
+
"Glob",
|
|
74
|
+
"Grep",
|
|
75
|
+
"WebFetch",
|
|
76
|
+
"WebSearch",
|
|
77
|
+
"Agent",
|
|
78
|
+
"Skill",
|
|
79
|
+
"TodoWrite",
|
|
80
|
+
"NotebookEdit",
|
|
81
|
+
"KillShell",
|
|
82
|
+
"ExitPlanMode",
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* Top-level NDJSON frame types emitted by the official CLI stream-json
|
|
86
|
+
* output that are known transport or lifecycle events and should not be failed as unknown.
|
|
87
|
+
*/
|
|
88
|
+
export const KNOWN_IGNORED_TOP_LEVEL_TYPES = new Set([
|
|
89
|
+
"system",
|
|
90
|
+
"rate_limit_event",
|
|
91
|
+
"tool_use_summary",
|
|
92
|
+
"user",
|
|
93
|
+
"auth_status",
|
|
94
|
+
"keep_alive",
|
|
95
|
+
"control_request",
|
|
96
|
+
]);
|
|
97
|
+
export class OfficialClaudeCodeCliRunner {
|
|
98
|
+
binaryPath;
|
|
99
|
+
capabilities;
|
|
100
|
+
maxFrameBytes;
|
|
101
|
+
maxTotalBytes;
|
|
102
|
+
timeoutMs;
|
|
103
|
+
spawnFn;
|
|
104
|
+
now;
|
|
105
|
+
sandboxRootDir;
|
|
106
|
+
allowedCliTools;
|
|
107
|
+
killProcessTreeFn;
|
|
108
|
+
reapProcessTreeFn;
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.binaryPath = options.binaryPath ?? "";
|
|
111
|
+
this.capabilities = options.capabilities;
|
|
112
|
+
this.maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
113
|
+
this.maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
114
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_RUN_DEADLINE_MS;
|
|
115
|
+
this.spawnFn = options.spawnFn ?? spawn;
|
|
116
|
+
this.now = options.now ?? Date.now;
|
|
117
|
+
this.sandboxRootDir = options.sandboxRootDir;
|
|
118
|
+
this.allowedCliTools = options.allowedCliTools;
|
|
119
|
+
this.killProcessTreeFn = options.killProcessTreeFn;
|
|
120
|
+
this.reapProcessTreeFn = options.reapProcessTreeFn;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the spawn candidate: the explicit override when set,
|
|
124
|
+
* otherwise the official `claude` CLI from `PATH` (re-resolved at each
|
|
125
|
+
* attempt, so a later CLI install is picked up without a restart).
|
|
126
|
+
* Generic spawn safety only — absolute resolved path, regular executable
|
|
127
|
+
* file, supported protocol. No version pin, no fingerprint, no attestation.
|
|
128
|
+
*/
|
|
129
|
+
resolveBinary() {
|
|
130
|
+
const candidate = this.binaryPath || resolveClaudeBinaryFromPath() || "";
|
|
131
|
+
if (!candidate || !isAbsolute(candidate)) {
|
|
132
|
+
throw new Error("Claude CLI candidate unqualified: official claude CLI not found on PATH");
|
|
133
|
+
}
|
|
134
|
+
if (!existsSync(candidate)) {
|
|
135
|
+
throw new Error("Claude CLI candidate unqualified: binary does not exist");
|
|
136
|
+
}
|
|
137
|
+
if (!isExecutableRegularFile(candidate)) {
|
|
138
|
+
throw new Error("Claude CLI candidate unqualified: binary not executable");
|
|
139
|
+
}
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Refuse to spawn unless the candidate resolves and the mesh protocol
|
|
144
|
+
* matches. Anything less is refused before spawn.
|
|
145
|
+
*/
|
|
146
|
+
qualifyBinary() {
|
|
147
|
+
this.resolveBinary();
|
|
148
|
+
if (!this.capabilities || this.capabilities.protocol !== "claude-code-stream-json-v1") {
|
|
149
|
+
throw new Error("Claude CLI candidate unqualified: unsupported protocol");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
run(input) {
|
|
153
|
+
const runController = new AbortController();
|
|
154
|
+
const abortHandler = () => runController.abort();
|
|
155
|
+
input.signal.addEventListener("abort", abortHandler, { once: true });
|
|
156
|
+
const runState = { earlyClosed: false };
|
|
157
|
+
const gen = this.runInternal(input, runController, abortHandler, runState);
|
|
158
|
+
const iterator = {
|
|
159
|
+
[Symbol.asyncIterator]() {
|
|
160
|
+
return this;
|
|
161
|
+
},
|
|
162
|
+
next() {
|
|
163
|
+
return gen.next();
|
|
164
|
+
},
|
|
165
|
+
return(val) {
|
|
166
|
+
runState.earlyClosed = true;
|
|
167
|
+
runController.abort();
|
|
168
|
+
return gen.return(val);
|
|
169
|
+
},
|
|
170
|
+
throw(err) {
|
|
171
|
+
runState.earlyClosed = true;
|
|
172
|
+
runController.abort();
|
|
173
|
+
return gen.throw(err);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return iterator;
|
|
177
|
+
}
|
|
178
|
+
async *runInternal(input, runController, abortHandler, runState) {
|
|
179
|
+
// 1. Candidate qualification preflight
|
|
180
|
+
this.qualifyBinary();
|
|
181
|
+
// Caller abort before any work: refuse without spawning.
|
|
182
|
+
if (input.signal.aborted || runController.signal.aborted) {
|
|
183
|
+
yield { type: "error" };
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Tool preflight: requests with tools are refused because caller tools would be silently
|
|
187
|
+
// ignored without client-side tool transport (local MCP qualification).
|
|
188
|
+
if (input.request.tools && input.request.tools.length > 0) {
|
|
189
|
+
for (const t of input.request.tools) {
|
|
190
|
+
if (BUILTIN_CLI_TOOLS.has(t.name)) {
|
|
191
|
+
throw new Error("Claude seat spawn refused: built-in tool names cannot be requested by caller");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw new Error("Claude seat spawn refused: requests with tools are rejected (client tool transport is not available; tools would be ignored)");
|
|
195
|
+
}
|
|
196
|
+
// Orphan sweep at each runner start (brief invariant 6): stale dead
|
|
197
|
+
// runs are reaped before a new sandbox is created. Best-effort: a
|
|
198
|
+
// sweep failure never blocks the run.
|
|
199
|
+
try {
|
|
200
|
+
const sweepResult = await sweepOrphanSandboxes(this.sandboxRootDir, {
|
|
201
|
+
onUnprovenChild: (unproven) => {
|
|
202
|
+
process.stderr.write(`[claude-orphan-sweep] unproven child ${unproven.childPid} in ${unproven.runDir} skipped: ${unproven.reason}\n`);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
if (sweepResult.skippedUnprovenChild) {
|
|
206
|
+
process.stderr.write(`[claude-orphan-sweep] skipped ${sweepResult.skippedUnprovenChild} unproven child process(es)\n`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Sweep is hygiene, not admission.
|
|
211
|
+
}
|
|
212
|
+
// 2. Admission and TTL checks
|
|
213
|
+
const admission = checkTokenTtlAdmission(input.access.expiresAt, this.now());
|
|
214
|
+
if (!admission.admitted) {
|
|
215
|
+
yield { type: "error" };
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// 3. Create isolated sandbox
|
|
219
|
+
const binaryPath = this.resolveBinary();
|
|
220
|
+
const sandbox = createClaudeRunSandbox({
|
|
221
|
+
access: input.access,
|
|
222
|
+
...(this.sandboxRootDir ? { rootDir: this.sandboxRootDir } : {}),
|
|
223
|
+
});
|
|
224
|
+
const env = buildIsolatedEnvironment(sandbox, process.env, {
|
|
225
|
+
binaryPath,
|
|
226
|
+
});
|
|
227
|
+
// 4. Construct candidate arguments. No --include-partial-messages:
|
|
228
|
+
// partial stream_event deltas would duplicate the final assistant
|
|
229
|
+
// message text. The parser still accepts stream_event envelopes
|
|
230
|
+
// defensively, but steady state carries each text once via assistant
|
|
231
|
+
// message.content[].
|
|
232
|
+
let toolsArg = "";
|
|
233
|
+
if (this.allowedCliTools && this.allowedCliTools.length > 0) {
|
|
234
|
+
const allowed = this.allowedCliTools.filter((t) => ALLOWED_TYPED_TOOL_IDENTIFIERS.includes(t));
|
|
235
|
+
toolsArg = allowed.join(",");
|
|
236
|
+
}
|
|
237
|
+
const args = [
|
|
238
|
+
"-p",
|
|
239
|
+
"--input-format", "stream-json",
|
|
240
|
+
"--output-format", "stream-json",
|
|
241
|
+
"--verbose",
|
|
242
|
+
"--no-session-persistence",
|
|
243
|
+
"--setting-sources", "",
|
|
244
|
+
"--settings", sandbox.settingsPath,
|
|
245
|
+
"--strict-mcp-config",
|
|
246
|
+
"--mcp-config", sandbox.mcpConfigPath,
|
|
247
|
+
"--tools", toolsArg,
|
|
248
|
+
"--disable-slash-commands",
|
|
249
|
+
"--permission-mode", "dontAsk",
|
|
250
|
+
"--model", input.request.modelId,
|
|
251
|
+
];
|
|
252
|
+
let childPid = 0;
|
|
253
|
+
let terminalResultEmitted = false;
|
|
254
|
+
let streamFailed = false;
|
|
255
|
+
let onRunAbort = null;
|
|
256
|
+
// Check for abort that fired during preflight, sweep, or sandbox creation
|
|
257
|
+
if (input.signal.aborted || runController.signal.aborted) {
|
|
258
|
+
runController.abort();
|
|
259
|
+
try {
|
|
260
|
+
cleanClaudeRunSandbox(sandbox);
|
|
261
|
+
}
|
|
262
|
+
catch { }
|
|
263
|
+
yield { type: "error" };
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
let timeoutTimer = null;
|
|
267
|
+
if (this.timeoutMs > 0 && Number.isFinite(this.timeoutMs)) {
|
|
268
|
+
timeoutTimer = setTimeout(() => {
|
|
269
|
+
runController.abort();
|
|
270
|
+
}, this.timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const child = this.spawnFn(binaryPath, args, {
|
|
274
|
+
cwd: sandbox.cwdDir,
|
|
275
|
+
env,
|
|
276
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
277
|
+
detached: true,
|
|
278
|
+
});
|
|
279
|
+
childPid = child.pid ?? 0;
|
|
280
|
+
if (!childPid) {
|
|
281
|
+
yield { type: "error" };
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
recordChildPidInSandbox(sandbox, childPid);
|
|
285
|
+
// Drain stderr so a verbose child can never block on a full pipe.
|
|
286
|
+
// Stderr is bounded and never forwarded to the caller.
|
|
287
|
+
let stderrBytes = 0;
|
|
288
|
+
child.stderr?.on("data", (chunk) => {
|
|
289
|
+
stderrBytes += chunk.length;
|
|
290
|
+
if (stderrBytes > this.maxTotalBytes) {
|
|
291
|
+
try {
|
|
292
|
+
child.kill("SIGKILL");
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Already dead; reaping below settles the run.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const queue = [];
|
|
300
|
+
let notifyNext = null;
|
|
301
|
+
const pushQueue = (item) => {
|
|
302
|
+
queue.push(item);
|
|
303
|
+
if (notifyNext) {
|
|
304
|
+
const fn = notifyNext;
|
|
305
|
+
notifyNext = null;
|
|
306
|
+
fn();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
// Abort, deadline, and early-close must end work even while a read is
|
|
310
|
+
// pending: kill the whole child tree and wake the consumer loop.
|
|
311
|
+
const killChildTree = () => {
|
|
312
|
+
if (this.killProcessTreeFn && childPid > 0) {
|
|
313
|
+
this.killProcessTreeFn(childPid, "SIGKILL");
|
|
314
|
+
try {
|
|
315
|
+
child.kill("SIGKILL");
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Already dead
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (childPid > 0) {
|
|
323
|
+
try {
|
|
324
|
+
process.kill(-childPid, "SIGKILL");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Not our group leader (mocks) or already dead: fall through.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
child.kill("SIGKILL");
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Already dead; reaping below settles the run.
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
onRunAbort = () => {
|
|
339
|
+
killChildTree();
|
|
340
|
+
if (notifyNext) {
|
|
341
|
+
const wake = notifyNext;
|
|
342
|
+
notifyNext = null;
|
|
343
|
+
wake();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
runController.signal.addEventListener("abort", onRunAbort);
|
|
347
|
+
// Stream request NDJSON onto stdin: one stream-json line per message
|
|
348
|
+
// ({type:"user",message:{role,content}}), the documented --input-format
|
|
349
|
+
// stream-json shape (binary shows message.role user + content; the
|
|
350
|
+
// assistant leg is symmetric). Never argv, never env.
|
|
351
|
+
for (const message of input.request.messages) {
|
|
352
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
353
|
+
child.stdin?.write(JSON.stringify({ type: role, message: { role, content: message.content } }) + "\n");
|
|
354
|
+
}
|
|
355
|
+
child.stdin?.end();
|
|
356
|
+
// Read stdout stream
|
|
357
|
+
let stdoutBuffer = "";
|
|
358
|
+
let totalBytesReceived = 0;
|
|
359
|
+
let pendingResult = null;
|
|
360
|
+
let terminalSeen = false;
|
|
361
|
+
let toolUseSeen = false;
|
|
362
|
+
const partialToolInputs = new Map();
|
|
363
|
+
const blockKinds = new Map();
|
|
364
|
+
// Frame shapes of the official CLI --output-format stream-json
|
|
365
|
+
// (verified statically against the local binary: stream_event x72 with
|
|
366
|
+
// content_block_start/delta/stop and message_start/delta/stop inner
|
|
367
|
+
// events, assistant frames carrying message.content[], result frames
|
|
368
|
+
// carrying subtype/is_error/usage/total_cost_usd; no live call made).
|
|
369
|
+
// Unknown top-level types, second terminals, and post-terminal frames
|
|
370
|
+
// are refused: an invented shape is never silently accepted.
|
|
371
|
+
const failFrame = () => {
|
|
372
|
+
streamFailed = true;
|
|
373
|
+
pushQueue({ event: { type: "error" } });
|
|
374
|
+
};
|
|
375
|
+
const parseAssistantMessage = (message) => {
|
|
376
|
+
if (!message || typeof message !== "object")
|
|
377
|
+
return failFrame();
|
|
378
|
+
const content = message.content;
|
|
379
|
+
if (!Array.isArray(content))
|
|
380
|
+
return failFrame();
|
|
381
|
+
for (const block of content) {
|
|
382
|
+
if (!block || typeof block !== "object")
|
|
383
|
+
return failFrame();
|
|
384
|
+
const typed = block;
|
|
385
|
+
if (typed.type === "text" && typeof typed.text === "string") {
|
|
386
|
+
if (typed.text)
|
|
387
|
+
pushQueue({ event: { type: "text_delta", text: typed.text } });
|
|
388
|
+
}
|
|
389
|
+
else if (typed.type === "tool_use" &&
|
|
390
|
+
typeof typed.id === "string" &&
|
|
391
|
+
typeof typed.name === "string") {
|
|
392
|
+
toolUseSeen = true;
|
|
393
|
+
pushQueue({
|
|
394
|
+
event: { type: "tool_use", id: typed.id, name: typed.name, input: typed.input ?? {} },
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
else if (typed.type === "thinking" || typed.type === "redacted_thinking") {
|
|
398
|
+
// Reasoning traces are never forwarded.
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
return failFrame();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const parseStreamEvent = (event) => {
|
|
406
|
+
if (!event || typeof event !== "object")
|
|
407
|
+
return failFrame();
|
|
408
|
+
const inner = event;
|
|
409
|
+
if (inner.type === "content_block_delta") {
|
|
410
|
+
const delta = inner.delta;
|
|
411
|
+
const index = typeof inner.index === "number" ? inner.index : -1;
|
|
412
|
+
if (!delta || typeof delta.type !== "string")
|
|
413
|
+
return;
|
|
414
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
415
|
+
if (delta.text)
|
|
416
|
+
pushQueue({ event: { type: "text_delta", text: delta.text } });
|
|
417
|
+
}
|
|
418
|
+
else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
419
|
+
const slot = partialToolInputs.get(index);
|
|
420
|
+
if (slot)
|
|
421
|
+
slot.json += delta.partial_json;
|
|
422
|
+
}
|
|
423
|
+
// thinking/signature/citation deltas are hints only; the terminal
|
|
424
|
+
// result frame governs the outcome.
|
|
425
|
+
}
|
|
426
|
+
else if (inner.type === "content_block_start") {
|
|
427
|
+
const block = inner.content_block;
|
|
428
|
+
const index = typeof inner.index === "number" ? inner.index : -1;
|
|
429
|
+
if (!block || typeof block.type !== "string" || index < 0)
|
|
430
|
+
return;
|
|
431
|
+
blockKinds.set(index, block.type);
|
|
432
|
+
if (block.type === "tool_use" &&
|
|
433
|
+
typeof block.id === "string" &&
|
|
434
|
+
typeof block.name === "string") {
|
|
435
|
+
partialToolInputs.set(index, { id: block.id, name: block.name, json: "" });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else if (inner.type === "content_block_stop") {
|
|
439
|
+
const index = typeof inner.index === "number" ? inner.index : -1;
|
|
440
|
+
const slot = index >= 0 ? partialToolInputs.get(index) : undefined;
|
|
441
|
+
if (slot) {
|
|
442
|
+
partialToolInputs.delete(index);
|
|
443
|
+
// Empty streamed input means no partials were observed (partials
|
|
444
|
+
// stay off at this seam): the assistant message carries the
|
|
445
|
+
// complete block, so nothing is emitted here.
|
|
446
|
+
if (slot.json) {
|
|
447
|
+
let parsed;
|
|
448
|
+
try {
|
|
449
|
+
parsed = JSON.parse(slot.json);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return failFrame();
|
|
453
|
+
}
|
|
454
|
+
toolUseSeen = true;
|
|
455
|
+
pushQueue({ event: { type: "tool_use", id: slot.id, name: slot.name, input: parsed } });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (index >= 0)
|
|
459
|
+
blockKinds.delete(index);
|
|
460
|
+
}
|
|
461
|
+
else if (inner.type === "message_start" ||
|
|
462
|
+
inner.type === "message_delta" ||
|
|
463
|
+
inner.type === "message_stop") {
|
|
464
|
+
// Transport bookkeeping (incremental usage lives here); the
|
|
465
|
+
// terminal result frame carries the canonical outcome.
|
|
466
|
+
}
|
|
467
|
+
// Unknown inner SSE events are ignored: they are hints, never the
|
|
468
|
+
// terminal contract. Unknown TOP-LEVEL types are refused below.
|
|
469
|
+
};
|
|
470
|
+
const parseResult = (frame) => {
|
|
471
|
+
if (terminalSeen)
|
|
472
|
+
return failFrame();
|
|
473
|
+
terminalSeen = true;
|
|
474
|
+
if (frame.subtype !== "success" || frame.is_error === true) {
|
|
475
|
+
return failFrame();
|
|
476
|
+
}
|
|
477
|
+
let usage;
|
|
478
|
+
if (frame.usage !== undefined) {
|
|
479
|
+
const raw = frame.usage;
|
|
480
|
+
if (!raw || typeof raw !== "object")
|
|
481
|
+
return failFrame();
|
|
482
|
+
usage = {};
|
|
483
|
+
if (typeof raw.input_tokens === "number")
|
|
484
|
+
usage.inputTokens = raw.input_tokens;
|
|
485
|
+
if (typeof raw.output_tokens === "number")
|
|
486
|
+
usage.outputTokens = raw.output_tokens;
|
|
487
|
+
if (Object.keys(usage).length === 0)
|
|
488
|
+
return failFrame();
|
|
489
|
+
}
|
|
490
|
+
// Stage pending terminal result: emitted only on exit agreement.
|
|
491
|
+
pendingResult = {
|
|
492
|
+
type: "result",
|
|
493
|
+
finishReason: toolUseSeen ? "tool_calls" : "stop",
|
|
494
|
+
...(usage ? { usage } : {}),
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
const parseLine = (line) => {
|
|
498
|
+
const trimmed = line.trim();
|
|
499
|
+
if (!trimmed)
|
|
500
|
+
return;
|
|
501
|
+
let frame;
|
|
502
|
+
try {
|
|
503
|
+
frame = JSON.parse(trimmed);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// Malformed frame rejects
|
|
507
|
+
failFrame();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (!frame || typeof frame !== "object") {
|
|
511
|
+
failFrame();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const typed = frame;
|
|
515
|
+
if (terminalSeen) {
|
|
516
|
+
// No frame of any kind is legal after the terminal result.
|
|
517
|
+
failFrame();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (KNOWN_IGNORED_TOP_LEVEL_TYPES.has(typed.type)) {
|
|
521
|
+
// Known lifecycle/transport frames: accepted as valid, not forwarded.
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
else if (typed.type === "assistant") {
|
|
525
|
+
parseAssistantMessage(typed.message);
|
|
526
|
+
}
|
|
527
|
+
else if (typed.type === "stream_event") {
|
|
528
|
+
parseStreamEvent(typed.event);
|
|
529
|
+
}
|
|
530
|
+
else if (typed.type === "result") {
|
|
531
|
+
parseResult(typed);
|
|
532
|
+
}
|
|
533
|
+
else if (typed.type === "error") {
|
|
534
|
+
failFrame();
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Unknown top-level frame (including invented delta shapes): refuse.
|
|
538
|
+
failFrame();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
child.stdout?.on("data", (chunk) => {
|
|
542
|
+
totalBytesReceived += chunk.length;
|
|
543
|
+
if (totalBytesReceived > this.maxTotalBytes) {
|
|
544
|
+
streamFailed = true;
|
|
545
|
+
pushQueue({ event: { type: "error" } });
|
|
546
|
+
child.kill("SIGKILL");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
550
|
+
const lines = stdoutBuffer.split("\n");
|
|
551
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
552
|
+
for (const line of lines) {
|
|
553
|
+
if (Buffer.byteLength(line, "utf8") > this.maxFrameBytes) {
|
|
554
|
+
streamFailed = true;
|
|
555
|
+
pushQueue({ event: { type: "error" } });
|
|
556
|
+
child.kill("SIGKILL");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
parseLine(line);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
child.on("error", () => {
|
|
563
|
+
streamFailed = true;
|
|
564
|
+
pushQueue({ event: { type: "error" } });
|
|
565
|
+
});
|
|
566
|
+
child.on("close", async (exitCode) => {
|
|
567
|
+
if (stdoutBuffer.trim()) {
|
|
568
|
+
parseLine(stdoutBuffer);
|
|
569
|
+
}
|
|
570
|
+
// Verify exit agreement, credential integrity, and reap process tree
|
|
571
|
+
const credentialIntact = verifyCredentialNotRewritten(sandbox, input.access);
|
|
572
|
+
let cleanupOk = true;
|
|
573
|
+
if (childPid > 0) {
|
|
574
|
+
if (this.reapProcessTreeFn) {
|
|
575
|
+
await this.reapProcessTreeFn(childPid, "SIGTERM", 50);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
await reapProcessTree(childPid, "SIGTERM", 50);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
cleanClaudeRunSandbox(sandbox);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
cleanupOk = false;
|
|
586
|
+
}
|
|
587
|
+
const exitAgreement = exitCode === 0 && !streamFailed && pendingResult !== null && cleanupOk;
|
|
588
|
+
if (exitAgreement && credentialIntact && pendingResult) {
|
|
589
|
+
pushQueue({ event: pendingResult });
|
|
590
|
+
terminalResultEmitted = true;
|
|
591
|
+
}
|
|
592
|
+
else if (!terminalResultEmitted && !streamFailed) {
|
|
593
|
+
pushQueue({ event: { type: "error" } });
|
|
594
|
+
}
|
|
595
|
+
pushQueue({ done: true });
|
|
596
|
+
});
|
|
597
|
+
// Consumer generator loop
|
|
598
|
+
while (true) {
|
|
599
|
+
if (runState.earlyClosed) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (runController.signal.aborted) {
|
|
603
|
+
yield { type: "error" };
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (queue.length === 0) {
|
|
607
|
+
await new Promise((r) => {
|
|
608
|
+
notifyNext = r;
|
|
609
|
+
// Abort may fire between the check above and this wait.
|
|
610
|
+
if (runController.signal.aborted || runState.earlyClosed) {
|
|
611
|
+
notifyNext = null;
|
|
612
|
+
r();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
while (queue.length > 0) {
|
|
617
|
+
const item = queue.shift();
|
|
618
|
+
if ("done" in item) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if ("event" in item) {
|
|
622
|
+
yield item.event;
|
|
623
|
+
if (item.event.type === "result" || item.event.type === "error") {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
if (timeoutTimer) {
|
|
632
|
+
clearTimeout(timeoutTimer);
|
|
633
|
+
}
|
|
634
|
+
input.signal.removeEventListener("abort", abortHandler);
|
|
635
|
+
if (onRunAbort) {
|
|
636
|
+
runController.signal.removeEventListener("abort", onRunAbort);
|
|
637
|
+
}
|
|
638
|
+
runController.abort();
|
|
639
|
+
if (childPid > 0) {
|
|
640
|
+
if (this.killProcessTreeFn) {
|
|
641
|
+
this.killProcessTreeFn(childPid, "SIGKILL");
|
|
642
|
+
}
|
|
643
|
+
if (this.reapProcessTreeFn) {
|
|
644
|
+
await this.reapProcessTreeFn(childPid, "SIGKILL", 50);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
await reapProcessTree(childPid, "SIGKILL", 50);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
cleanClaudeRunSandbox(sandbox);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Terminal outcome already decided in the close handler; retry is best-effort.
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
//# sourceMappingURL=claude-code-cli-runner.js.map
|