@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.
Files changed (130) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +2 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +3 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/attach.d.ts +2 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +73 -6
  15. package/dist/attach.js.map +1 -1
  16. package/dist/auth-login.d.ts +24 -1
  17. package/dist/auth-login.d.ts.map +1 -1
  18. package/dist/auth-login.js +132 -4
  19. package/dist/auth-login.js.map +1 -1
  20. package/dist/central-mcp.d.ts +2 -0
  21. package/dist/central-mcp.d.ts.map +1 -1
  22. package/dist/central-mcp.js +90 -3
  23. package/dist/central-mcp.js.map +1 -1
  24. package/dist/claude-code-cli-isolation.d.ts +53 -0
  25. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  26. package/dist/claude-code-cli-isolation.js +277 -0
  27. package/dist/claude-code-cli-isolation.js.map +1 -0
  28. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  29. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  30. package/dist/claude-code-cli-lifecycle.js +402 -0
  31. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  32. package/dist/claude-code-cli-runner.d.ts +89 -0
  33. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  34. package/dist/claude-code-cli-runner.js +659 -0
  35. package/dist/claude-code-cli-runner.js.map +1 -0
  36. package/dist/claude-seat-input.d.ts +47 -0
  37. package/dist/claude-seat-input.d.ts.map +1 -0
  38. package/dist/claude-seat-input.js +272 -0
  39. package/dist/claude-seat-input.js.map +1 -0
  40. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  41. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  42. package/dist/claude-seat-mesh-contract.js +13 -0
  43. package/dist/claude-seat-mesh-contract.js.map +1 -0
  44. package/dist/cli-help-groups.js +1 -1
  45. package/dist/cli-help-groups.js.map +1 -1
  46. package/dist/crash-log.d.ts +112 -0
  47. package/dist/crash-log.d.ts.map +1 -0
  48. package/dist/crash-log.js +560 -0
  49. package/dist/crash-log.js.map +1 -0
  50. package/dist/delegate.d.ts +10 -0
  51. package/dist/delegate.d.ts.map +1 -1
  52. package/dist/delegate.js +29 -0
  53. package/dist/delegate.js.map +1 -1
  54. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  55. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  56. package/dist/gateway-host/claude-seat-client.js +38 -0
  57. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  58. package/dist/gateway-host/host.d.ts +6 -0
  59. package/dist/gateway-host/host.d.ts.map +1 -1
  60. package/dist/gateway-host/host.js +24 -3
  61. package/dist/gateway-host/host.js.map +1 -1
  62. package/dist/gateway-host/live-declarations.d.ts +58 -0
  63. package/dist/gateway-host/live-declarations.d.ts.map +1 -0
  64. package/dist/gateway-host/live-declarations.js +429 -0
  65. package/dist/gateway-host/live-declarations.js.map +1 -0
  66. package/dist/gateway-host/live-github.d.ts +11 -0
  67. package/dist/gateway-host/live-github.d.ts.map +1 -0
  68. package/dist/gateway-host/live-github.js +374 -0
  69. package/dist/gateway-host/live-github.js.map +1 -0
  70. package/dist/gateway-host/live-object-store.d.ts +35 -0
  71. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  72. package/dist/gateway-host/live-object-store.js +447 -0
  73. package/dist/gateway-host/live-object-store.js.map +1 -0
  74. package/dist/gateway-host/live-registration.d.ts +162 -0
  75. package/dist/gateway-host/live-registration.d.ts.map +1 -0
  76. package/dist/gateway-host/live-registration.js +455 -0
  77. package/dist/gateway-host/live-registration.js.map +1 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +290 -23
  80. package/dist/index.js.map +1 -1
  81. package/dist/llm-mesh-accounts.d.ts +70 -4
  82. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  83. package/dist/llm-mesh-accounts.js +296 -9
  84. package/dist/llm-mesh-accounts.js.map +1 -1
  85. package/dist/native-host.d.ts +1 -1
  86. package/dist/native-host.d.ts.map +1 -1
  87. package/dist/native-host.js +30 -17
  88. package/dist/native-host.js.map +1 -1
  89. package/dist/native-terminal/host.d.ts +6 -1
  90. package/dist/native-terminal/host.d.ts.map +1 -1
  91. package/dist/native-terminal/host.js +6 -25
  92. package/dist/native-terminal/host.js.map +1 -1
  93. package/dist/native-terminal/journal.d.ts +5 -0
  94. package/dist/native-terminal/journal.d.ts.map +1 -1
  95. package/dist/native-terminal/journal.js +28 -16
  96. package/dist/native-terminal/journal.js.map +1 -1
  97. package/dist/native-terminal/op.d.ts +13 -0
  98. package/dist/native-terminal/op.d.ts.map +1 -1
  99. package/dist/native-terminal/op.js +64 -5
  100. package/dist/native-terminal/op.js.map +1 -1
  101. package/dist/proc-identity.d.ts +31 -0
  102. package/dist/proc-identity.d.ts.map +1 -1
  103. package/dist/proc-identity.js +54 -0
  104. package/dist/proc-identity.js.map +1 -1
  105. package/dist/prompt-delivery.d.ts +3 -0
  106. package/dist/prompt-delivery.d.ts.map +1 -1
  107. package/dist/prompt-delivery.js +8 -3
  108. package/dist/prompt-delivery.js.map +1 -1
  109. package/dist/run.d.ts.map +1 -1
  110. package/dist/run.js +79 -16
  111. package/dist/run.js.map +1 -1
  112. package/dist/terminal-reset.d.ts +104 -0
  113. package/dist/terminal-reset.d.ts.map +1 -0
  114. package/dist/terminal-reset.js +229 -0
  115. package/dist/terminal-reset.js.map +1 -0
  116. package/dist/tmux.d.ts +26 -4
  117. package/dist/tmux.d.ts.map +1 -1
  118. package/dist/tmux.js +60 -25
  119. package/dist/tmux.js.map +1 -1
  120. package/dist/vendor/engram-live/contracts.d.ts +825 -0
  121. package/dist/vendor/engram-live/contracts.js +1734 -0
  122. package/dist/vendor/engram-live/integration.d.ts +113 -0
  123. package/dist/vendor/engram-live/integration.js +254 -0
  124. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  125. package/dist/vendor/engram-live/pr-sync.js +411 -0
  126. package/dist/vendor/engram-live/registration.d.ts +138 -0
  127. package/dist/vendor/engram-live/registration.js +358 -0
  128. package/dist/vendor/engram-live/retention.d.ts +56 -0
  129. package/dist/vendor/engram-live/retention.js +179 -0
  130. 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