@ai-sdk/harness 1.0.133 → 1.0.135

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.
@@ -1,799 +1,667 @@
1
- // src/utils/sandbox-channel.ts
2
- import {
3
- safeParseJSON,
4
- safeValidateTypes
5
- } from "@ai-sdk/provider-utils";
6
-
7
- // src/utils/sleep.ts
8
- function sleep({
9
- ms,
10
- abortSignal
11
- }) {
12
- return new Promise((resolve) => {
13
- if (abortSignal?.aborted) {
14
- resolve();
15
- return;
16
- }
17
- let settled = false;
18
- let onAbort = () => {
19
- };
20
- const finish = () => {
21
- if (settled) return;
22
- settled = true;
23
- abortSignal?.removeEventListener("abort", onAbort);
24
- resolve();
25
- };
26
- const timer = setTimeout(finish, ms);
27
- timer.unref?.();
28
- onAbort = () => {
29
- clearTimeout(timer);
30
- finish();
31
- };
32
- abortSignal?.addEventListener("abort", onAbort, { once: true });
33
- });
34
- }
35
-
36
- // src/utils/sandbox-channel.ts
37
- var sandboxChannelEventCheckpointSymbol = /* @__PURE__ */ Symbol.for(
38
- "vercel.ai.harness.sandboxChannelEventCheckpoint"
39
- );
40
- function getAbortReason({
41
- abortSignal
42
- }) {
43
- return abortSignal.reason ?? new Error("SandboxChannel connection aborted");
1
+ import { isRecord, safeParseJSON, safeValidateTypes } from "@ai-sdk/provider-utils";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import path, { posix } from "node:path";
6
+ import { z } from "zod/v4";
7
+ import { readFile } from "node:fs/promises";
8
+ //#region src/utils/sleep.ts
9
+ function sleep({ ms, abortSignal }) {
10
+ return new Promise((resolve) => {
11
+ if (abortSignal?.aborted) {
12
+ resolve();
13
+ return;
14
+ }
15
+ let settled = false;
16
+ let onAbort = () => {};
17
+ const finish = () => {
18
+ if (settled) return;
19
+ settled = true;
20
+ abortSignal?.removeEventListener("abort", onAbort);
21
+ resolve();
22
+ };
23
+ const timer = setTimeout(finish, ms);
24
+ timer.unref?.();
25
+ onAbort = () => {
26
+ clearTimeout(timer);
27
+ finish();
28
+ };
29
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
30
+ });
31
+ }
32
+ //#endregion
33
+ //#region src/utils/sandbox-channel.ts
34
+ const sandboxChannelEventCheckpointSymbol = Symbol.for("vercel.ai.harness.sandboxChannelEventCheckpoint");
35
+ function getAbortReason({ abortSignal }) {
36
+ return abortSignal.reason ?? /* @__PURE__ */ new Error("SandboxChannel connection aborted");
44
37
  }
45
38
  function closeWebSocket({ ws }) {
46
- try {
47
- const terminable = ws;
48
- if (terminable.terminate != null) {
49
- terminable.terminate();
50
- } else {
51
- ws.close();
52
- }
53
- } catch {
54
- try {
55
- ws.close();
56
- } catch {
57
- }
58
- }
59
- }
60
- async function awaitWebSocketConnection({
61
- connection,
62
- abortSignal
63
- }) {
64
- if (abortSignal.aborted) {
65
- void connection.then((ws) => closeWebSocket({ ws })).catch(() => {
66
- });
67
- throw getAbortReason({ abortSignal });
68
- }
69
- let onAbort;
70
- const aborted = new Promise((_, reject) => {
71
- onAbort = () => reject(getAbortReason({ abortSignal }));
72
- abortSignal.addEventListener("abort", onAbort, { once: true });
73
- });
74
- try {
75
- const ws = await Promise.race([connection, aborted]);
76
- if (abortSignal.aborted) {
77
- closeWebSocket({ ws });
78
- throw getAbortReason({ abortSignal });
79
- }
80
- return ws;
81
- } catch (error) {
82
- if (abortSignal.aborted) {
83
- void connection.then((ws) => closeWebSocket({ ws })).catch(() => {
84
- });
85
- }
86
- throw error;
87
- } finally {
88
- if (onAbort != null) {
89
- abortSignal.removeEventListener("abort", onAbort);
90
- }
91
- }
92
- }
39
+ try {
40
+ const terminable = ws;
41
+ if (terminable.terminate != null) terminable.terminate();
42
+ else ws.close();
43
+ } catch {
44
+ try {
45
+ ws.close();
46
+ } catch {}
47
+ }
48
+ }
49
+ async function awaitWebSocketConnection({ connection, abortSignal }) {
50
+ if (abortSignal.aborted) {
51
+ connection.then((ws) => closeWebSocket({ ws })).catch(() => {});
52
+ throw getAbortReason({ abortSignal });
53
+ }
54
+ let onAbort;
55
+ const aborted = new Promise((_, reject) => {
56
+ onAbort = () => reject(getAbortReason({ abortSignal }));
57
+ abortSignal.addEventListener("abort", onAbort, { once: true });
58
+ });
59
+ try {
60
+ const ws = await Promise.race([connection, aborted]);
61
+ if (abortSignal.aborted) {
62
+ closeWebSocket({ ws });
63
+ throw getAbortReason({ abortSignal });
64
+ }
65
+ return ws;
66
+ } catch (error) {
67
+ if (abortSignal.aborted) connection.then((ws) => closeWebSocket({ ws })).catch(() => {});
68
+ throw error;
69
+ } finally {
70
+ if (onAbort != null) abortSignal.removeEventListener("abort", onAbort);
71
+ }
72
+ }
73
+ /**
74
+ * Host-side typed wrapper around the bridge WebSocket connection.
75
+ *
76
+ * Buffers inbound messages in arrival order while listeners attach, so callers
77
+ * do not miss or reorder early frames. Listener registration drains the
78
+ * ordered prefix synchronously. Selective listeners are given through the
79
+ * current task (including its microtasks) to attach before unhandled event
80
+ * types are retained independently, so they cannot block subscribed event
81
+ * types indefinitely. A listener that claims an already-buffered event still
82
+ * receives it if it unsubscribes before that ordered drain completes. Use
83
+ * {@link SandboxChannel.beginListenerAttachment} to define an explicit
84
+ * attachment boundary that spans asynchronous work. Inbound dispatch is
85
+ * serialised through a promise chain so a `close` event that arrives on the
86
+ * same microtask as the final `finish` message does not fire close handlers
87
+ * until the message has been dispatched.
88
+ *
89
+ * Survives transient disconnects. The bridge keeps running and
90
+ * accumulates events in an in-memory log keyed by a monotonic `seq`; on an
91
+ * unexpected socket drop this channel re-invokes `connect`, re-wires the new
92
+ * socket, and asks the bridge to replay everything past `lastSeenEventId`. The
93
+ * in-flight turn never observes the blip — `onClose` fires only after a
94
+ * host-initiated close or once the reconnect budget is exhausted.
95
+ */
93
96
  var SandboxChannel = class {
94
- constructor(options) {
95
- this.listeners = /* @__PURE__ */ new Map();
96
- this.buffered = [];
97
- this.bufferedByType = /* @__PURE__ */ new Map();
98
- this.bufferedOffset = 0;
99
- this.flushingBuffered = false;
100
- this.bufferedFlushScheduled = false;
101
- this.listenerAttachmentDepth = 0;
102
- this.onCloseHandlers = /* @__PURE__ */ new Set();
103
- this.onReconnectHandlers = /* @__PURE__ */ new Set();
104
- this.connected = false;
105
- /** Host has begun teardown; suppresses reconnect so a bridge-side close finalises. */
106
- this.closing = false;
107
- /**
108
- * Host has gracefully suspended (slice boundary). Inbound frames are ignored
109
- * from this point so the cursor stops advancing exactly at the last delivered
110
- * event — the bridge keeps the turn running and the not-yet-delivered tail is
111
- * replayed to the next process on `resume`.
112
- */
113
- this.suspended = false;
114
- /** Channel is fully torn down; `send` throws and `onClose` has fired. */
115
- this.terminal = false;
116
- this._lastSeenEventId = 0;
117
- this.pendingSends = [];
118
- this.dispatchChain = Promise.resolve();
119
- this.connectThunk = options.connect;
120
- this.outboundSchema = options.outboundSchema;
121
- this.onDebug = options.onDebug;
122
- this.onDiagnostic = options.onDiagnostic;
123
- this.onBridgeError = options.onBridgeError;
124
- this.maxElapsedMs = options.reconnect?.maxElapsedMs ?? 3e4;
125
- this.initialDelayMs = options.reconnect?.initialDelayMs ?? 50;
126
- this.maxDelayMs = options.reconnect?.maxDelayMs ?? 2e3;
127
- this._lastSeenEventId = options.initialLastSeenEventId ?? 0;
128
- }
129
- /**
130
- * Highest bridge event `seq` this channel has observed. Persist it (e.g. via
131
- * the adapter's resume handle) so a future process can seed
132
- * {@link SandboxChannelOptions.initialLastSeenEventId} and attach.
133
- */
134
- get lastSeenEventId() {
135
- return this._lastSeenEventId;
136
- }
137
- /**
138
- * Establish the initial connection. A single attempt — startup failures
139
- * reject so the caller can fail `doStart` cleanly. Reconnect retries apply
140
- * only to drops after a successful open.
141
- *
142
- * Pass `{ resume: true }` to attach to a bridge that is already mid-session:
143
- * after the socket opens, the channel sends `{ type: 'resume', lastSeenEventId }`
144
- * so the bridge replays everything past the seeded cursor. This is the
145
- * cross-process attach handshake — identical to what a transient reconnect
146
- * does, but triggered by the initial open from a new process.
147
- */
148
- async open(opts) {
149
- if (this.terminal) {
150
- throw new Error("SandboxChannel: cannot open a closed channel.");
151
- }
152
- const abortController = new AbortController();
153
- this.activeConnectAbortController = abortController;
154
- try {
155
- const ws = await this.connect({
156
- abortSignal: abortController.signal
157
- });
158
- this.wire(ws);
159
- this.ws = ws;
160
- this.connected = true;
161
- if (opts?.resume) {
162
- this.rawSend(
163
- JSON.stringify({
164
- type: "resume",
165
- lastSeenEventId: this._lastSeenEventId
166
- })
167
- );
168
- }
169
- } finally {
170
- if (this.activeConnectAbortController === abortController) {
171
- this.activeConnectAbortController = void 0;
172
- }
173
- }
174
- }
175
- on(type, listener) {
176
- let set = this.listeners.get(type);
177
- if (!set) {
178
- set = /* @__PURE__ */ new Set();
179
- this.listeners.set(type, set);
180
- }
181
- set.add(listener);
182
- this.captureBufferedListeners(type);
183
- if (this.listenerAttachmentDepth > 0) {
184
- return () => {
185
- set.delete(listener);
186
- };
187
- }
188
- this.flushBufferedType(type);
189
- this.flushBuffered();
190
- this.scheduleSelectiveBufferedFlush();
191
- return () => {
192
- set.delete(listener);
193
- };
194
- }
195
- /**
196
- * Hold buffered event delivery while a consumer attaches a related set of
197
- * listeners, including across asynchronous boundaries. Call the returned
198
- * function once registration is complete. Buffered events are then replayed
199
- * in arrival order; event types that remain unhandled are retained
200
- * independently so they do not block subscribed types.
201
- */
202
- beginListenerAttachment() {
203
- this.listenerAttachmentDepth++;
204
- let finished = false;
205
- return () => {
206
- if (finished) return;
207
- finished = true;
208
- this.listenerAttachmentDepth--;
209
- if (this.listenerAttachmentDepth > 0) return;
210
- for (const type of this.listeners.keys()) {
211
- this.flushBufferedType(type);
212
- }
213
- this.flushBuffered();
214
- this.scheduleSelectiveBufferedFlush();
215
- };
216
- }
217
- onClose(handler) {
218
- this.onCloseHandlers.add(handler);
219
- }
220
- onReconnect(handler) {
221
- this.onReconnectHandlers.add(handler);
222
- return () => {
223
- this.onReconnectHandlers.delete(handler);
224
- };
225
- }
226
- send(message) {
227
- if (this.terminal) {
228
- throw new Error(
229
- `SandboxChannel: cannot send ${message.type} \u2014 channel is closed.`
230
- );
231
- }
232
- this.rawSend(JSON.stringify(message));
233
- }
234
- /**
235
- * Mark that the host is tearing the session down. The next socket close is
236
- * then treated as terminal rather than triggering a reconnect. Call before
237
- * sending a `stop` / `destroy` message whose completion closes the bridge
238
- * socket.
239
- */
240
- beginClose() {
241
- this.closing = true;
242
- this.abortActiveConnect();
243
- }
244
- close() {
245
- if (this.terminal) return;
246
- this.closing = true;
247
- this.abortActiveConnect();
248
- try {
249
- this.ws?.close();
250
- } catch {
251
- }
252
- this.enqueue(() => this.finalizeClose(1e3, "closed"));
253
- }
254
- /**
255
- * Gracefully suspend at a slice boundary: stop processing inbound frames
256
- * (so the cursor freezes at the last delivered event), drain any frames
257
- * already queued for dispatch, then close the socket and finalise with the
258
- * close reason `'suspended'`. Resolves with the final `lastSeenEventId`.
259
- *
260
- * The bridge keeps the in-flight turn running (a host socket close never
261
- * aborts it) and accumulates events past the cursor for the next process to
262
- * `resume`. Unlike {@link close}, the consumer's active turn is wound down
263
- * cleanly — adapters distinguish a suspend from an unexpected drop via the
264
- * `'suspended'` close reason and resolve `done` successfully. When an event
265
- * checkpoint is pinned, the returned cursor points to that event so any
266
- * already-dispatched tail is replayed by the next process.
267
- */
268
- suspend() {
269
- return new Promise((resolve) => {
270
- const pinnedSuspensionCursor = this.pinnedSuspensionCursor?.eventId;
271
- if (this.terminal) {
272
- resolve(pinnedSuspensionCursor ?? this._lastSeenEventId);
273
- return;
274
- }
275
- this.suspended = true;
276
- this.closing = true;
277
- this.abortActiveConnect();
278
- this.onClose(
279
- () => resolve(pinnedSuspensionCursor ?? this._lastSeenEventId)
280
- );
281
- this.enqueue(() => {
282
- try {
283
- this.ws?.close();
284
- } catch {
285
- }
286
- this.finalizeClose(1e3, "suspended");
287
- });
288
- });
289
- }
290
- isClosed() {
291
- return this.terminal;
292
- }
293
- // ─── internals ──────────────────────────────────────────────────────
294
- wire(ws) {
295
- let dropped = false;
296
- const onDrop = (code, reason) => {
297
- if (dropped) return;
298
- dropped = true;
299
- if (ws !== this.ws) return;
300
- this.connected = false;
301
- if (this.closing) {
302
- this.enqueue(() => this.finalizeClose(code, reason));
303
- } else {
304
- this.enqueue(() => this.reconnectLoop());
305
- }
306
- };
307
- ws.on("message", (raw) => {
308
- if (this.suspended) return;
309
- const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
310
- this.enqueue(() => this.handleIncoming(text));
311
- });
312
- ws.on(
313
- "close",
314
- (code, reason) => onDrop(code, reason?.toString?.("utf8") ?? "")
315
- );
316
- ws.on("error", () => onDrop(1006, "socket error"));
317
- }
318
- async reconnectLoop() {
319
- if (this.terminal || this.closing) return;
320
- const deadline = Date.now() + this.maxElapsedMs;
321
- let attempt = 0;
322
- let delay = this.initialDelayMs;
323
- const reconnectAbortController = new AbortController();
324
- this.reconnectAbortController = reconnectAbortController;
325
- try {
326
- while (!this.terminal && !this.closing) {
327
- const remaining = deadline - Date.now();
328
- if (remaining <= 0) {
329
- this.failReconnect({
330
- attempts: attempt,
331
- cause: new Error("Reconnect deadline expired")
332
- });
333
- return;
334
- }
335
- attempt++;
336
- this.onDebug?.({
337
- event: "reconnect-attempt",
338
- attempt,
339
- lastSeenEventId: this._lastSeenEventId
340
- });
341
- const abortController = new AbortController();
342
- this.activeConnectAbortController = abortController;
343
- const abortConnect = () => abortController.abort(reconnectAbortController.signal.reason);
344
- reconnectAbortController.signal.addEventListener(
345
- "abort",
346
- abortConnect,
347
- {
348
- once: true
349
- }
350
- );
351
- const timeout = setTimeout(() => {
352
- abortController.abort(new Error("Reconnect deadline expired"));
353
- }, remaining);
354
- timeout.unref?.();
355
- try {
356
- const ws = await this.connect({
357
- abortSignal: abortController.signal
358
- });
359
- if (this.terminal || this.closing) {
360
- closeWebSocket({ ws });
361
- return;
362
- }
363
- this.wire(ws);
364
- this.ws = ws;
365
- this.connected = true;
366
- this.rawSend(
367
- JSON.stringify({
368
- type: "resume",
369
- lastSeenEventId: this._lastSeenEventId
370
- })
371
- );
372
- this.flushPending();
373
- for (const handler of this.onReconnectHandlers) handler();
374
- this.onDebug?.({
375
- event: "reconnected",
376
- attempt,
377
- lastSeenEventId: this._lastSeenEventId
378
- });
379
- return;
380
- } catch (cause) {
381
- if (this.terminal || this.closing || reconnectAbortController.signal.aborted) {
382
- return;
383
- }
384
- const remainingAfterFailure = deadline - Date.now();
385
- if (remainingAfterFailure <= 0) {
386
- this.failReconnect({ attempts: attempt, cause });
387
- return;
388
- }
389
- await sleep({
390
- ms: Math.min(delay, remainingAfterFailure),
391
- abortSignal: reconnectAbortController.signal
392
- });
393
- delay = Math.min(delay * 1.5, this.maxDelayMs);
394
- } finally {
395
- clearTimeout(timeout);
396
- reconnectAbortController.signal.removeEventListener(
397
- "abort",
398
- abortConnect
399
- );
400
- if (this.activeConnectAbortController === abortController) {
401
- this.activeConnectAbortController = void 0;
402
- }
403
- }
404
- }
405
- } finally {
406
- if (this.reconnectAbortController === reconnectAbortController) {
407
- this.reconnectAbortController = void 0;
408
- }
409
- }
410
- }
411
- connect({
412
- abortSignal
413
- }) {
414
- return awaitWebSocketConnection({
415
- connection: Promise.resolve().then(
416
- () => this.connectThunk({ abortSignal })
417
- ),
418
- abortSignal
419
- });
420
- }
421
- abortActiveConnect() {
422
- const controller = this.activeConnectAbortController;
423
- this.activeConnectAbortController = void 0;
424
- controller?.abort(new Error("SandboxChannel connection aborted"));
425
- this.reconnectAbortController?.abort(
426
- new Error("SandboxChannel connection aborted")
427
- );
428
- }
429
- failReconnect({
430
- attempts,
431
- cause
432
- }) {
433
- this.finalizeClose(1006, "reconnect failed");
434
- this.onDebug?.({
435
- event: "reconnect-failed",
436
- attempts,
437
- lastSeenEventId: this._lastSeenEventId,
438
- cause
439
- });
440
- }
441
- rawSend(text) {
442
- if (this.connected && this.ws) {
443
- this.ws.send(text);
444
- } else {
445
- this.pendingSends.push(text);
446
- }
447
- }
448
- flushPending() {
449
- if (!this.connected || !this.ws) return;
450
- const queued = this.pendingSends.splice(0);
451
- for (const text of queued) this.ws.send(text);
452
- }
453
- enqueue(work) {
454
- this.dispatchChain = this.dispatchChain.then(work);
455
- }
456
- async handleIncoming(text) {
457
- const raw = await safeParseJSON({ text });
458
- let seq;
459
- if (raw.success && raw.value != null && typeof raw.value === "object" && typeof raw.value.seq === "number") {
460
- seq = raw.value.seq;
461
- }
462
- const validated = await safeValidateTypes({
463
- value: raw.success ? raw.value : void 0,
464
- schema: this.outboundSchema
465
- });
466
- if (validated.success) {
467
- if (seq !== void 0) {
468
- this.attachEventCheckpoint({ event: validated.value, eventId: seq });
469
- }
470
- this.dispatch(validated.value);
471
- } else {
472
- this.dispatch({
473
- type: "error",
474
- error: validated.error
475
- });
476
- }
477
- if (seq !== void 0 && seq > this._lastSeenEventId) {
478
- this._lastSeenEventId = seq;
479
- }
480
- }
481
- dispatch(message) {
482
- if (message.type === "sandbox-log" || message.type === "debug-event") {
483
- this.onDiagnostic?.(
484
- message
485
- );
486
- return;
487
- }
488
- if (message.type === "error") {
489
- this.onBridgeError?.(message);
490
- }
491
- const type = message.type;
492
- const set = this.listeners.get(type);
493
- if (this.listenerAttachmentDepth > 0 || this.bufferedOffset < this.buffered.length || !set || set.size === 0) {
494
- this.buffered.push({
495
- message,
496
- ...set != null && set.size > 0 ? { listeners: Array.from(set) } : {}
497
- });
498
- this.flushBuffered();
499
- this.scheduleSelectiveBufferedFlush();
500
- return;
501
- }
502
- for (const listener of set) {
503
- listener(message);
504
- }
505
- }
506
- flushBuffered({
507
- selective = false
508
- } = {}) {
509
- if (this.flushingBuffered || this.listenerAttachmentDepth > 0) return;
510
- this.flushingBuffered = true;
511
- try {
512
- while (this.bufferedOffset < this.buffered.length) {
513
- const bufferedEvent = this.buffered[this.bufferedOffset];
514
- const message = bufferedEvent.message;
515
- const type = message.type;
516
- const set = this.listeners.get(type);
517
- const listeners = bufferedEvent.listeners ?? (set != null && set.size > 0 ? Array.from(set) : void 0);
518
- if (!listeners || listeners.length === 0) {
519
- if (!selective) return;
520
- this.bufferedOffset++;
521
- const buffered = this.bufferedByType.get(type);
522
- if (buffered) {
523
- buffered.push(message);
524
- } else {
525
- this.bufferedByType.set(type, [message]);
526
- }
527
- continue;
528
- }
529
- this.bufferedOffset++;
530
- for (const listener of listeners) {
531
- listener(message);
532
- }
533
- }
534
- } finally {
535
- if (this.bufferedOffset > 0) {
536
- this.buffered.splice(0, this.bufferedOffset);
537
- this.bufferedOffset = 0;
538
- }
539
- this.flushingBuffered = false;
540
- }
541
- }
542
- captureBufferedListeners(type) {
543
- const set = this.listeners.get(type);
544
- if (!set || set.size === 0) return;
545
- for (let i = this.bufferedOffset; i < this.buffered.length; i++) {
546
- const bufferedEvent = this.buffered[i];
547
- if (bufferedEvent.listeners == null && bufferedEvent.message.type === type) {
548
- bufferedEvent.listeners = Array.from(set);
549
- }
550
- }
551
- }
552
- flushBufferedType(type) {
553
- const buffered = this.bufferedByType.get(type);
554
- if (!buffered) return;
555
- let offset = 0;
556
- try {
557
- while (offset < buffered.length) {
558
- const set = this.listeners.get(type);
559
- if (!set || set.size === 0) return;
560
- const message = buffered[offset++];
561
- for (const listener of set) {
562
- listener(message);
563
- }
564
- }
565
- } finally {
566
- if (offset === buffered.length) {
567
- this.bufferedByType.delete(type);
568
- } else if (offset > 0) {
569
- buffered.splice(0, offset);
570
- }
571
- }
572
- }
573
- scheduleSelectiveBufferedFlush() {
574
- if (this.bufferedFlushScheduled || this.bufferedOffset >= this.buffered.length || !this.hasListeners() || this.listenerAttachmentDepth > 0) {
575
- return;
576
- }
577
- this.bufferedFlushScheduled = true;
578
- setTimeout(() => {
579
- this.bufferedFlushScheduled = false;
580
- if (this.listenerAttachmentDepth > 0) return;
581
- this.flushBuffered({ selective: true });
582
- }, 0);
583
- }
584
- hasListeners() {
585
- for (const listeners of this.listeners.values()) {
586
- if (listeners.size > 0) return true;
587
- }
588
- return false;
589
- }
590
- attachEventCheckpoint(options) {
591
- if (!Object.isExtensible(options.event)) return;
592
- Object.defineProperty(options.event, sandboxChannelEventCheckpointSymbol, {
593
- value: {
594
- pin: () => {
595
- const token = {};
596
- this.pinnedSuspensionCursor = { eventId: options.eventId, token };
597
- return () => {
598
- if (this.pinnedSuspensionCursor?.token === token) {
599
- this.pinnedSuspensionCursor = void 0;
600
- }
601
- };
602
- }
603
- }
604
- });
605
- }
606
- finalizeClose(code, reason) {
607
- if (this.terminal) return;
608
- this.abortActiveConnect();
609
- this.terminal = true;
610
- this.connected = false;
611
- for (const h of this.onCloseHandlers) h(code, reason);
612
- }
97
+ constructor(options) {
98
+ this.listeners = /* @__PURE__ */ new Map();
99
+ this.buffered = [];
100
+ this.bufferedByType = /* @__PURE__ */ new Map();
101
+ this.bufferedOffset = 0;
102
+ this.flushingBuffered = false;
103
+ this.bufferedFlushScheduled = false;
104
+ this.listenerAttachmentDepth = 0;
105
+ this.onCloseHandlers = /* @__PURE__ */ new Set();
106
+ this.onReconnectHandlers = /* @__PURE__ */ new Set();
107
+ this.connected = false;
108
+ this.closing = false;
109
+ this.suspended = false;
110
+ this.terminal = false;
111
+ this._lastSeenEventId = 0;
112
+ this.pendingSends = [];
113
+ this.dispatchChain = Promise.resolve();
114
+ this.connectThunk = options.connect;
115
+ this.outboundSchema = options.outboundSchema;
116
+ this.onDebug = options.onDebug;
117
+ this.onDiagnostic = options.onDiagnostic;
118
+ this.onBridgeError = options.onBridgeError;
119
+ this.maxElapsedMs = options.reconnect?.maxElapsedMs ?? 3e4;
120
+ this.initialDelayMs = options.reconnect?.initialDelayMs ?? 50;
121
+ this.maxDelayMs = options.reconnect?.maxDelayMs ?? 2e3;
122
+ this._lastSeenEventId = options.initialLastSeenEventId ?? 0;
123
+ }
124
+ /**
125
+ * Highest bridge event `seq` this channel has observed. Persist it (e.g. via
126
+ * the adapter's resume handle) so a future process can seed
127
+ * {@link SandboxChannelOptions.initialLastSeenEventId} and attach.
128
+ */
129
+ get lastSeenEventId() {
130
+ return this._lastSeenEventId;
131
+ }
132
+ /**
133
+ * Establish the initial connection. A single attempt — startup failures
134
+ * reject so the caller can fail `doStart` cleanly. Reconnect retries apply
135
+ * only to drops after a successful open.
136
+ *
137
+ * Pass `{ resume: true }` to attach to a bridge that is already mid-session:
138
+ * after the socket opens, the channel sends `{ type: 'resume', lastSeenEventId }`
139
+ * so the bridge replays everything past the seeded cursor. This is the
140
+ * cross-process attach handshake — identical to what a transient reconnect
141
+ * does, but triggered by the initial open from a new process.
142
+ */
143
+ async open(opts) {
144
+ if (this.terminal) throw new Error("SandboxChannel: cannot open a closed channel.");
145
+ const abortController = new AbortController();
146
+ this.activeConnectAbortController = abortController;
147
+ try {
148
+ const ws = await this.connect({ abortSignal: abortController.signal });
149
+ this.wire(ws);
150
+ this.ws = ws;
151
+ this.connected = true;
152
+ if (opts?.resume) this.rawSend(JSON.stringify({
153
+ type: "resume",
154
+ lastSeenEventId: this._lastSeenEventId
155
+ }));
156
+ } finally {
157
+ if (this.activeConnectAbortController === abortController) this.activeConnectAbortController = void 0;
158
+ }
159
+ }
160
+ on(type, listener) {
161
+ let set = this.listeners.get(type);
162
+ if (!set) {
163
+ set = /* @__PURE__ */ new Set();
164
+ this.listeners.set(type, set);
165
+ }
166
+ set.add(listener);
167
+ this.captureBufferedListeners(type);
168
+ if (this.listenerAttachmentDepth > 0) return () => {
169
+ set.delete(listener);
170
+ };
171
+ this.flushBufferedType(type);
172
+ this.flushBuffered();
173
+ this.scheduleSelectiveBufferedFlush();
174
+ return () => {
175
+ set.delete(listener);
176
+ };
177
+ }
178
+ /**
179
+ * Hold buffered event delivery while a consumer attaches a related set of
180
+ * listeners, including across asynchronous boundaries. Call the returned
181
+ * function once registration is complete. Buffered events are then replayed
182
+ * in arrival order; event types that remain unhandled are retained
183
+ * independently so they do not block subscribed types.
184
+ */
185
+ beginListenerAttachment() {
186
+ this.listenerAttachmentDepth++;
187
+ let finished = false;
188
+ return () => {
189
+ if (finished) return;
190
+ finished = true;
191
+ this.listenerAttachmentDepth--;
192
+ if (this.listenerAttachmentDepth > 0) return;
193
+ for (const type of this.listeners.keys()) this.flushBufferedType(type);
194
+ this.flushBuffered();
195
+ this.scheduleSelectiveBufferedFlush();
196
+ };
197
+ }
198
+ onClose(handler) {
199
+ this.onCloseHandlers.add(handler);
200
+ }
201
+ onReconnect(handler) {
202
+ this.onReconnectHandlers.add(handler);
203
+ return () => {
204
+ this.onReconnectHandlers.delete(handler);
205
+ };
206
+ }
207
+ send(message) {
208
+ if (this.terminal) throw new Error(`SandboxChannel: cannot send ${message.type} — channel is closed.`);
209
+ this.rawSend(JSON.stringify(message));
210
+ }
211
+ /**
212
+ * Mark that the host is tearing the session down. The next socket close is
213
+ * then treated as terminal rather than triggering a reconnect. Call before
214
+ * sending a `stop` / `destroy` message whose completion closes the bridge
215
+ * socket.
216
+ */
217
+ beginClose() {
218
+ this.closing = true;
219
+ this.abortActiveConnect();
220
+ }
221
+ close() {
222
+ if (this.terminal) return;
223
+ this.closing = true;
224
+ this.abortActiveConnect();
225
+ try {
226
+ this.ws?.close();
227
+ } catch {}
228
+ this.enqueue(() => this.finalizeClose(1e3, "closed"));
229
+ }
230
+ /**
231
+ * Gracefully suspend at a slice boundary: stop processing inbound frames
232
+ * (so the cursor freezes at the last delivered event), drain any frames
233
+ * already queued for dispatch, then close the socket and finalise with the
234
+ * close reason `'suspended'`. Resolves with the final `lastSeenEventId`.
235
+ *
236
+ * The bridge keeps the in-flight turn running (a host socket close never
237
+ * aborts it) and accumulates events past the cursor for the next process to
238
+ * `resume`. Unlike {@link close}, the consumer's active turn is wound down
239
+ * cleanly — adapters distinguish a suspend from an unexpected drop via the
240
+ * `'suspended'` close reason and resolve `done` successfully. When an event
241
+ * checkpoint is pinned, the returned cursor points to that event so any
242
+ * already-dispatched tail is replayed by the next process.
243
+ */
244
+ suspend() {
245
+ return new Promise((resolve) => {
246
+ const pinnedSuspensionCursor = this.pinnedSuspensionCursor?.eventId;
247
+ if (this.terminal) {
248
+ resolve(pinnedSuspensionCursor ?? this._lastSeenEventId);
249
+ return;
250
+ }
251
+ this.suspended = true;
252
+ this.closing = true;
253
+ this.abortActiveConnect();
254
+ this.onClose(() => resolve(pinnedSuspensionCursor ?? this._lastSeenEventId));
255
+ this.enqueue(() => {
256
+ try {
257
+ this.ws?.close();
258
+ } catch {}
259
+ this.finalizeClose(1e3, "suspended");
260
+ });
261
+ });
262
+ }
263
+ isClosed() {
264
+ return this.terminal;
265
+ }
266
+ wire(ws) {
267
+ let dropped = false;
268
+ const onDrop = (code, reason) => {
269
+ if (dropped) return;
270
+ dropped = true;
271
+ if (ws !== this.ws) return;
272
+ this.connected = false;
273
+ if (this.closing) this.enqueue(() => this.finalizeClose(code, reason));
274
+ else this.enqueue(() => this.reconnectLoop());
275
+ };
276
+ ws.on("message", (raw) => {
277
+ if (this.suspended) return;
278
+ const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
279
+ this.enqueue(() => this.handleIncoming(text));
280
+ });
281
+ ws.on("close", (code, reason) => onDrop(code, reason?.toString?.("utf8") ?? ""));
282
+ ws.on("error", () => onDrop(1006, "socket error"));
283
+ }
284
+ async reconnectLoop() {
285
+ if (this.terminal || this.closing) return;
286
+ const deadline = Date.now() + this.maxElapsedMs;
287
+ let attempt = 0;
288
+ let delay = this.initialDelayMs;
289
+ const reconnectAbortController = new AbortController();
290
+ this.reconnectAbortController = reconnectAbortController;
291
+ try {
292
+ while (!this.terminal && !this.closing) {
293
+ const remaining = deadline - Date.now();
294
+ if (remaining <= 0) {
295
+ this.failReconnect({
296
+ attempts: attempt,
297
+ cause: /* @__PURE__ */ new Error("Reconnect deadline expired")
298
+ });
299
+ return;
300
+ }
301
+ attempt++;
302
+ this.onDebug?.({
303
+ event: "reconnect-attempt",
304
+ attempt,
305
+ lastSeenEventId: this._lastSeenEventId
306
+ });
307
+ const abortController = new AbortController();
308
+ this.activeConnectAbortController = abortController;
309
+ const abortConnect = () => abortController.abort(reconnectAbortController.signal.reason);
310
+ reconnectAbortController.signal.addEventListener("abort", abortConnect, { once: true });
311
+ const timeout = setTimeout(() => {
312
+ abortController.abort(/* @__PURE__ */ new Error("Reconnect deadline expired"));
313
+ }, remaining);
314
+ timeout.unref?.();
315
+ try {
316
+ const ws = await this.connect({ abortSignal: abortController.signal });
317
+ if (this.terminal || this.closing) {
318
+ closeWebSocket({ ws });
319
+ return;
320
+ }
321
+ this.wire(ws);
322
+ this.ws = ws;
323
+ this.connected = true;
324
+ this.rawSend(JSON.stringify({
325
+ type: "resume",
326
+ lastSeenEventId: this._lastSeenEventId
327
+ }));
328
+ this.flushPending();
329
+ for (const handler of this.onReconnectHandlers) handler();
330
+ this.onDebug?.({
331
+ event: "reconnected",
332
+ attempt,
333
+ lastSeenEventId: this._lastSeenEventId
334
+ });
335
+ return;
336
+ } catch (cause) {
337
+ if (this.terminal || this.closing || reconnectAbortController.signal.aborted) return;
338
+ const remainingAfterFailure = deadline - Date.now();
339
+ if (remainingAfterFailure <= 0) {
340
+ this.failReconnect({
341
+ attempts: attempt,
342
+ cause
343
+ });
344
+ return;
345
+ }
346
+ await sleep({
347
+ ms: Math.min(delay, remainingAfterFailure),
348
+ abortSignal: reconnectAbortController.signal
349
+ });
350
+ delay = Math.min(delay * 1.5, this.maxDelayMs);
351
+ } finally {
352
+ clearTimeout(timeout);
353
+ reconnectAbortController.signal.removeEventListener("abort", abortConnect);
354
+ if (this.activeConnectAbortController === abortController) this.activeConnectAbortController = void 0;
355
+ }
356
+ }
357
+ } finally {
358
+ if (this.reconnectAbortController === reconnectAbortController) this.reconnectAbortController = void 0;
359
+ }
360
+ }
361
+ connect({ abortSignal }) {
362
+ return awaitWebSocketConnection({
363
+ connection: Promise.resolve().then(() => this.connectThunk({ abortSignal })),
364
+ abortSignal
365
+ });
366
+ }
367
+ abortActiveConnect() {
368
+ const controller = this.activeConnectAbortController;
369
+ this.activeConnectAbortController = void 0;
370
+ controller?.abort(/* @__PURE__ */ new Error("SandboxChannel connection aborted"));
371
+ this.reconnectAbortController?.abort(/* @__PURE__ */ new Error("SandboxChannel connection aborted"));
372
+ }
373
+ failReconnect({ attempts, cause }) {
374
+ this.finalizeClose(1006, "reconnect failed");
375
+ this.onDebug?.({
376
+ event: "reconnect-failed",
377
+ attempts,
378
+ lastSeenEventId: this._lastSeenEventId,
379
+ cause
380
+ });
381
+ }
382
+ rawSend(text) {
383
+ if (this.connected && this.ws) this.ws.send(text);
384
+ else this.pendingSends.push(text);
385
+ }
386
+ flushPending() {
387
+ if (!this.connected || !this.ws) return;
388
+ const queued = this.pendingSends.splice(0);
389
+ for (const text of queued) this.ws.send(text);
390
+ }
391
+ enqueue(work) {
392
+ this.dispatchChain = this.dispatchChain.then(work);
393
+ }
394
+ async handleIncoming(text) {
395
+ const raw = await safeParseJSON({ text });
396
+ let seq;
397
+ if (raw.success && raw.value != null && typeof raw.value === "object" && typeof raw.value.seq === "number") seq = raw.value.seq;
398
+ const validated = await safeValidateTypes({
399
+ value: raw.success ? raw.value : void 0,
400
+ schema: this.outboundSchema
401
+ });
402
+ if (validated.success) {
403
+ if (seq !== void 0) this.attachEventCheckpoint({
404
+ event: validated.value,
405
+ eventId: seq
406
+ });
407
+ this.dispatch(validated.value);
408
+ } else this.dispatch({
409
+ type: "error",
410
+ error: validated.error
411
+ });
412
+ if (seq !== void 0 && seq > this._lastSeenEventId) this._lastSeenEventId = seq;
413
+ }
414
+ dispatch(message) {
415
+ if (message.type === "sandbox-log" || message.type === "debug-event") {
416
+ this.onDiagnostic?.(message);
417
+ return;
418
+ }
419
+ if (message.type === "error") this.onBridgeError?.(message);
420
+ const type = message.type;
421
+ const set = this.listeners.get(type);
422
+ if (this.listenerAttachmentDepth > 0 || this.bufferedOffset < this.buffered.length || !set || set.size === 0) {
423
+ this.buffered.push({
424
+ message,
425
+ ...set != null && set.size > 0 ? { listeners: Array.from(set) } : {}
426
+ });
427
+ this.flushBuffered();
428
+ this.scheduleSelectiveBufferedFlush();
429
+ return;
430
+ }
431
+ for (const listener of set) listener(message);
432
+ }
433
+ flushBuffered({ selective = false } = {}) {
434
+ if (this.flushingBuffered || this.listenerAttachmentDepth > 0) return;
435
+ this.flushingBuffered = true;
436
+ try {
437
+ while (this.bufferedOffset < this.buffered.length) {
438
+ const bufferedEvent = this.buffered[this.bufferedOffset];
439
+ const message = bufferedEvent.message;
440
+ const type = message.type;
441
+ const set = this.listeners.get(type);
442
+ const listeners = bufferedEvent.listeners ?? (set != null && set.size > 0 ? Array.from(set) : void 0);
443
+ if (!listeners || listeners.length === 0) {
444
+ if (!selective) return;
445
+ this.bufferedOffset++;
446
+ const buffered = this.bufferedByType.get(type);
447
+ if (buffered) buffered.push(message);
448
+ else this.bufferedByType.set(type, [message]);
449
+ continue;
450
+ }
451
+ this.bufferedOffset++;
452
+ for (const listener of listeners) listener(message);
453
+ }
454
+ } finally {
455
+ if (this.bufferedOffset > 0) {
456
+ this.buffered.splice(0, this.bufferedOffset);
457
+ this.bufferedOffset = 0;
458
+ }
459
+ this.flushingBuffered = false;
460
+ }
461
+ }
462
+ captureBufferedListeners(type) {
463
+ const set = this.listeners.get(type);
464
+ if (!set || set.size === 0) return;
465
+ for (let i = this.bufferedOffset; i < this.buffered.length; i++) {
466
+ const bufferedEvent = this.buffered[i];
467
+ if (bufferedEvent.listeners == null && bufferedEvent.message.type === type) bufferedEvent.listeners = Array.from(set);
468
+ }
469
+ }
470
+ flushBufferedType(type) {
471
+ const buffered = this.bufferedByType.get(type);
472
+ if (!buffered) return;
473
+ let offset = 0;
474
+ try {
475
+ while (offset < buffered.length) {
476
+ const set = this.listeners.get(type);
477
+ if (!set || set.size === 0) return;
478
+ const message = buffered[offset++];
479
+ for (const listener of set) listener(message);
480
+ }
481
+ } finally {
482
+ if (offset === buffered.length) this.bufferedByType.delete(type);
483
+ else if (offset > 0) buffered.splice(0, offset);
484
+ }
485
+ }
486
+ scheduleSelectiveBufferedFlush() {
487
+ if (this.bufferedFlushScheduled || this.bufferedOffset >= this.buffered.length || !this.hasListeners() || this.listenerAttachmentDepth > 0) return;
488
+ this.bufferedFlushScheduled = true;
489
+ setTimeout(() => {
490
+ this.bufferedFlushScheduled = false;
491
+ if (this.listenerAttachmentDepth > 0) return;
492
+ this.flushBuffered({ selective: true });
493
+ }, 0);
494
+ }
495
+ hasListeners() {
496
+ for (const listeners of this.listeners.values()) if (listeners.size > 0) return true;
497
+ return false;
498
+ }
499
+ attachEventCheckpoint(options) {
500
+ if (!Object.isExtensible(options.event)) return;
501
+ Object.defineProperty(options.event, sandboxChannelEventCheckpointSymbol, { value: { pin: () => {
502
+ const token = {};
503
+ this.pinnedSuspensionCursor = {
504
+ eventId: options.eventId,
505
+ token
506
+ };
507
+ return () => {
508
+ if (this.pinnedSuspensionCursor?.token === token) this.pinnedSuspensionCursor = void 0;
509
+ };
510
+ } } });
511
+ }
512
+ finalizeClose(code, reason) {
513
+ if (this.terminal) return;
514
+ this.abortActiveConnect();
515
+ this.terminal = true;
516
+ this.connected = false;
517
+ for (const h of this.onCloseHandlers) h(code, reason);
518
+ }
613
519
  };
614
-
615
- // src/utils/bridge-user-message-submitter.ts
520
+ //#endregion
521
+ //#region src/utils/bridge-user-message-submitter.ts
616
522
  function experimental_createBridgeUserMessageSubmitter(options) {
617
- const pending = /* @__PURE__ */ new Map();
618
- let closed = false;
619
- const send = (request) => {
620
- try {
621
- options.send(request);
622
- } catch (error) {
623
- const entry = pending.get(request.messageId);
624
- if (entry == null) return;
625
- pending.delete(request.messageId);
626
- entry.reject(error);
627
- }
628
- };
629
- const unsubscribeResponse = options.onResponse((response) => {
630
- const entry = pending.get(response.messageId);
631
- if (entry == null) return;
632
- pending.delete(response.messageId);
633
- if (response.accepted) {
634
- entry.resolve();
635
- } else {
636
- entry.reject(
637
- new Error(
638
- response.error?.message ?? "The runtime rejected the user message."
639
- )
640
- );
641
- }
642
- });
643
- const unsubscribeReconnect = options.onReconnect(() => {
644
- for (const entry of pending.values()) send(entry.request);
645
- });
646
- return {
647
- submit: (text) => {
648
- if (closed) {
649
- return Promise.reject(
650
- new Error("The bridge turn is no longer accepting user messages.")
651
- );
652
- }
653
- const messageId = crypto.randomUUID();
654
- const request = {
655
- type: "user-message",
656
- messageId,
657
- text
658
- };
659
- const promise = new Promise((resolve, reject) => {
660
- pending.set(messageId, { request, resolve, reject });
661
- });
662
- send(request);
663
- return promise;
664
- },
665
- close: (error) => {
666
- if (closed) return;
667
- closed = true;
668
- unsubscribeResponse();
669
- unsubscribeReconnect();
670
- const reason = error ?? new Error("The bridge turn ended before accepting the user message.");
671
- for (const entry of pending.values()) entry.reject(reason);
672
- pending.clear();
673
- }
674
- };
675
- }
676
-
677
- // src/utils/classify-disk-log.ts
678
- import { safeParseJSON as safeParseJSON2 } from "@ai-sdk/provider-utils";
523
+ const pending = /* @__PURE__ */ new Map();
524
+ let closed = false;
525
+ const send = (request) => {
526
+ try {
527
+ options.send(request);
528
+ } catch (error) {
529
+ const entry = pending.get(request.messageId);
530
+ if (entry == null) return;
531
+ pending.delete(request.messageId);
532
+ entry.reject(error);
533
+ }
534
+ };
535
+ const unsubscribeResponse = options.onResponse((response) => {
536
+ const entry = pending.get(response.messageId);
537
+ if (entry == null) return;
538
+ pending.delete(response.messageId);
539
+ if (response.accepted) entry.resolve();
540
+ else entry.reject(new Error(response.error?.message ?? "The runtime rejected the user message."));
541
+ });
542
+ const unsubscribeReconnect = options.onReconnect(() => {
543
+ for (const entry of pending.values()) send(entry.request);
544
+ });
545
+ return {
546
+ submit: (text) => {
547
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("The bridge turn is no longer accepting user messages."));
548
+ const messageId = crypto.randomUUID();
549
+ const request = {
550
+ type: "user-message",
551
+ messageId,
552
+ text
553
+ };
554
+ const promise = new Promise((resolve, reject) => {
555
+ pending.set(messageId, {
556
+ request,
557
+ resolve,
558
+ reject
559
+ });
560
+ });
561
+ send(request);
562
+ return promise;
563
+ },
564
+ close: (error) => {
565
+ if (closed) return;
566
+ closed = true;
567
+ unsubscribeResponse();
568
+ unsubscribeReconnect();
569
+ const reason = error ?? /* @__PURE__ */ new Error("The bridge turn ended before accepting the user message.");
570
+ for (const entry of pending.values()) entry.reject(reason);
571
+ pending.clear();
572
+ }
573
+ };
574
+ }
575
+ //#endregion
576
+ //#region src/utils/classify-disk-log.ts
577
+ /**
578
+ * Decide whether a respawned bridge can `replay` a turn from its persisted
579
+ * `event-log.ndjson`, or must `rerun` it from scratch.
580
+ *
581
+ * A turn is replayable only when its log ends in a terminal `finish` event —
582
+ * a log that is missing, empty, or ends mid-turn means the bridge died before
583
+ * completing the turn, so there is no coherent tail to deliver and the runtime
584
+ * must re-run it (continuing its own thread from the sandbox snapshot).
585
+ *
586
+ * @param eventLog Raw contents of `event-log.ndjson` (newline-delimited JSON),
587
+ * or `null`/`undefined`/empty when the file is absent.
588
+ */
679
589
  async function classifyDiskLog(eventLog) {
680
- if (!eventLog) return "rerun";
681
- const lines = eventLog.split("\n").map((line) => line.trim()).filter(Boolean);
682
- const lastLine = lines.at(-1);
683
- if (lastLine == null) return "rerun";
684
- const parsed = await safeParseJSON2({ text: lastLine });
685
- if (parsed.success && parsed.value != null && typeof parsed.value === "object" && parsed.value.type === "finish") {
686
- return "replay";
687
- }
688
- return "rerun";
689
- }
690
-
691
- // src/utils/ai-gateway-auth.ts
692
- var DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
693
- function getAiGatewayAuthFromEnv({
694
- env
695
- }) {
696
- return {
697
- apiKey: env.AI_GATEWAY_API_KEY || env.VERCEL_OIDC_TOKEN || void 0,
698
- baseUrl: env.AI_GATEWAY_BASE_URL ?? DEFAULT_AI_GATEWAY_BASE_URL
699
- };
700
- }
701
-
702
- // src/utils/authentication-environment.ts
590
+ if (!eventLog) return "rerun";
591
+ const lastLine = eventLog.split("\n").map((line) => line.trim()).filter(Boolean).at(-1);
592
+ if (lastLine == null) return "rerun";
593
+ const parsed = await safeParseJSON({ text: lastLine });
594
+ if (parsed.success && parsed.value != null && typeof parsed.value === "object" && parsed.value.type === "finish") return "replay";
595
+ return "rerun";
596
+ }
597
+ //#endregion
598
+ //#region src/utils/ai-gateway-auth.ts
599
+ const DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
600
+ function getAiGatewayAuthFromEnv({ env }) {
601
+ return {
602
+ apiKey: env.AI_GATEWAY_API_KEY || env.VERCEL_OIDC_TOKEN || void 0,
603
+ baseUrl: env.AI_GATEWAY_BASE_URL ?? DEFAULT_AI_GATEWAY_BASE_URL
604
+ };
605
+ }
606
+ //#endregion
607
+ //#region src/utils/authentication-environment.ts
703
608
  function isHarnessAuthenticationEnvironment(value) {
704
- if (value == null || typeof value !== "object") {
705
- return false;
706
- }
707
- if (Array.isArray(value) || Object.values(value).some((entry) => typeof entry !== "string")) {
708
- throw new TypeError(
709
- "Invalid auth: expected an authentication mode or a flat record with string values."
710
- );
711
- }
712
- return true;
713
- }
714
-
715
- // src/utils/native-subscription/jwt.ts
716
- import { isRecord, safeParseJSON as safeParseJSON3 } from "@ai-sdk/provider-utils";
717
- async function parseJwtPayload({
718
- token
719
- }) {
720
- const segments = token.split(".");
721
- if (segments.length !== 3) return void 0;
722
- try {
723
- const parsed = await safeParseJSON3({
724
- text: Buffer.from(segments[1], "base64url").toString("utf8")
725
- });
726
- return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
727
- } catch {
728
- return void 0;
729
- }
730
- }
731
- async function getJwtExpiresAt({
732
- token
733
- }) {
734
- const payload = await parseJwtPayload({ token });
735
- const expiresAt = payload?.exp;
736
- return typeof expiresAt === "number" && Number.isFinite(expiresAt) ? expiresAt * 1e3 : void 0;
737
- }
738
-
739
- // src/utils/native-subscription/linux-secret-service.ts
740
- import { execFile } from "child_process";
741
- import { promisify } from "util";
742
- var execFileAsync = promisify(execFile);
743
- async function readLinuxSecretServicePassword({
744
- attributes
745
- }) {
746
- try {
747
- const result = await execFileAsync("secret-tool", [
748
- "lookup",
749
- ...Object.entries(attributes).flatMap(([attribute, value]) => [
750
- attribute,
751
- value
752
- ])
753
- ]);
754
- return result.stdout || void 0;
755
- } catch {
756
- return void 0;
757
- }
758
- }
759
-
760
- // src/utils/native-subscription/macos-keychain.ts
761
- import { execFile as execFile2 } from "child_process";
762
- import { promisify as promisify2 } from "util";
763
- var execFileAsync2 = promisify2(execFile2);
764
- async function readMacOSKeychainPassword({
765
- service,
766
- account
767
- }) {
768
- try {
769
- const result = await execFileAsync2("/usr/bin/security", [
770
- "find-generic-password",
771
- "-s",
772
- service,
773
- "-a",
774
- account,
775
- "-w"
776
- ]);
777
- return result.stdout.trim() || void 0;
778
- } catch {
779
- return void 0;
780
- }
781
- }
782
-
783
- // src/utils/native-subscription/should-resolve-native.ts
784
- function shouldResolveNativeSubscription({
785
- auth,
786
- env,
787
- hasDirectCredential
788
- }) {
789
- return auth !== "ai-gateway" && !hasDirectCredential && (auth === "direct" || getAiGatewayAuthFromEnv({ env }).apiKey == null);
790
- }
791
-
792
- // src/utils/native-subscription/windows-credential-manager.ts
793
- import { execFile as execFile3 } from "child_process";
794
- import { promisify as promisify3 } from "util";
795
- var execFileAsync3 = promisify3(execFile3);
796
- var credentialManagerSource = `
609
+ if (value == null || typeof value !== "object") return false;
610
+ if (Array.isArray(value) || Object.values(value).some((entry) => typeof entry !== "string")) throw new TypeError("Invalid auth: expected an authentication mode or a flat record with string values.");
611
+ return true;
612
+ }
613
+ //#endregion
614
+ //#region src/utils/native-subscription/jwt.ts
615
+ async function parseJwtPayload({ token }) {
616
+ const segments = token.split(".");
617
+ if (segments.length !== 3) return void 0;
618
+ try {
619
+ const parsed = await safeParseJSON({ text: Buffer.from(segments[1], "base64url").toString("utf8") });
620
+ return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
621
+ } catch {
622
+ return;
623
+ }
624
+ }
625
+ async function getJwtExpiresAt({ token }) {
626
+ const expiresAt = (await parseJwtPayload({ token }))?.exp;
627
+ return typeof expiresAt === "number" && Number.isFinite(expiresAt) ? expiresAt * 1e3 : void 0;
628
+ }
629
+ //#endregion
630
+ //#region src/utils/native-subscription/linux-secret-service.ts
631
+ const execFileAsync$2 = promisify(execFile);
632
+ async function readLinuxSecretServicePassword({ attributes }) {
633
+ try {
634
+ return (await execFileAsync$2("secret-tool", ["lookup", ...Object.entries(attributes).flatMap(([attribute, value]) => [attribute, value])])).stdout || void 0;
635
+ } catch {
636
+ return;
637
+ }
638
+ }
639
+ //#endregion
640
+ //#region src/utils/native-subscription/macos-keychain.ts
641
+ const execFileAsync$1 = promisify(execFile);
642
+ async function readMacOSKeychainPassword({ service, account }) {
643
+ try {
644
+ return (await execFileAsync$1("/usr/bin/security", [
645
+ "find-generic-password",
646
+ "-s",
647
+ service,
648
+ "-a",
649
+ account,
650
+ "-w"
651
+ ])).stdout.trim() || void 0;
652
+ } catch {
653
+ return;
654
+ }
655
+ }
656
+ //#endregion
657
+ //#region src/utils/native-subscription/should-resolve-native.ts
658
+ function shouldResolveNativeSubscription({ auth, env, hasDirectCredential }) {
659
+ return auth !== "ai-gateway" && !hasDirectCredential && (auth === "direct" || getAiGatewayAuthFromEnv({ env }).apiKey == null);
660
+ }
661
+ //#endregion
662
+ //#region src/utils/native-subscription/windows-credential-manager.ts
663
+ const execFileAsync = promisify(execFile);
664
+ const credentialManagerSource = `
797
665
  using System;
798
666
  using System.Runtime.InteropServices;
799
667
  using System.Text;
@@ -820,1758 +688,1335 @@ public static class AISDKCredentialManager {
820
688
  } finally { CredFree(pointer); }
821
689
  }
822
690
  }`;
823
- async function readWindowsCredentialManagerPassword({
824
- targetName
825
- }) {
826
- try {
827
- const result = await execFileAsync3(
828
- "powershell.exe",
829
- [
830
- "-NoProfile",
831
- "-NonInteractive",
832
- "-Command",
833
- `Add-Type -TypeDefinition $env:AI_SDK_WINDOWS_CREDENTIAL_MANAGER_SOURCE; $value = [AISDKCredentialManager]::Read($env:AI_SDK_WINDOWS_CREDENTIAL_MANAGER_TARGET); if ($null -ne $value) { [Console]::Out.Write($value) }`
834
- ],
835
- {
836
- env: {
837
- ...process.env,
838
- AI_SDK_WINDOWS_CREDENTIAL_MANAGER_SOURCE: credentialManagerSource,
839
- AI_SDK_WINDOWS_CREDENTIAL_MANAGER_TARGET: targetName
840
- },
841
- windowsHide: true
842
- }
843
- );
844
- return result.stdout || void 0;
845
- } catch {
846
- return void 0;
847
- }
848
- }
849
-
850
- // src/utils/oauth-access-token.ts
851
- import { isRecord as isRecord2, safeParseJSON as safeParseJSON4 } from "@ai-sdk/provider-utils";
852
- var DEFAULT_REFRESH_WINDOW_MS = 3e5;
853
- function isAccessTokenExpiringSoon({
854
- expiresAt,
855
- now = Date.now(),
856
- refreshWindowMs = DEFAULT_REFRESH_WINDOW_MS
857
- }) {
858
- return expiresAt <= now + refreshWindowMs;
859
- }
860
- async function refreshOAuthAccessToken({
861
- tokenUrl,
862
- clientId,
863
- refreshToken,
864
- requestFormat = "form",
865
- headers,
866
- fetch: fetchImplementation = globalThis.fetch
867
- }) {
868
- const values = {
869
- grant_type: "refresh_token",
870
- client_id: clientId,
871
- refresh_token: refreshToken
872
- };
873
- const response = await fetchImplementation(tokenUrl, {
874
- method: "POST",
875
- headers: {
876
- "content-type": requestFormat === "json" ? "application/json" : "application/x-www-form-urlencoded",
877
- ...headers
878
- },
879
- body: requestFormat === "json" ? JSON.stringify(values) : new URLSearchParams(values).toString()
880
- });
881
- const responseText = await response.text();
882
- if (!response.ok) {
883
- throw new Error(
884
- `OAuth access token refresh failed with status ${response.status}.`
885
- );
886
- }
887
- const parsed = await safeParseJSON4({ text: responseText });
888
- if (!parsed.success || !isRecord2(parsed.value)) {
889
- throw new Error("OAuth access token refresh returned invalid JSON.");
890
- }
891
- const accessToken = parsed.value.access_token;
892
- if (typeof accessToken !== "string" || accessToken.length === 0) {
893
- throw new Error(
894
- "OAuth access token refresh response is missing access_token."
895
- );
896
- }
897
- const expiresAt = await resolveExpiresAt({
898
- accessToken,
899
- expiresIn: parsed.value.expires_in
900
- });
901
- const rotatedRefreshToken = parsed.value.refresh_token;
902
- if (rotatedRefreshToken != null && (typeof rotatedRefreshToken !== "string" || rotatedRefreshToken.length === 0)) {
903
- throw new Error(
904
- "OAuth access token refresh response contains an invalid refresh_token."
905
- );
906
- }
907
- return {
908
- accessToken,
909
- expiresAt,
910
- ...typeof rotatedRefreshToken === "string" ? { refreshToken: rotatedRefreshToken } : {}
911
- };
912
- }
913
- async function resolveExpiresAt({
914
- accessToken,
915
- expiresIn
916
- }) {
917
- if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn >= 0) {
918
- return Date.now() + expiresIn * 1e3;
919
- }
920
- const jwtExpiresAt = await getJwtExpiresAt({ token: accessToken });
921
- if (jwtExpiresAt != null) return jwtExpiresAt;
922
- throw new Error(
923
- "OAuth access token refresh response does not include a usable expiry."
924
- );
925
- }
926
-
927
- // src/utils/os.ts
691
+ async function readWindowsCredentialManagerPassword({ targetName }) {
692
+ try {
693
+ return (await execFileAsync("powershell.exe", [
694
+ "-NoProfile",
695
+ "-NonInteractive",
696
+ "-Command",
697
+ `Add-Type -TypeDefinition $env:AI_SDK_WINDOWS_CREDENTIAL_MANAGER_SOURCE; $value = [AISDKCredentialManager]::Read($env:AI_SDK_WINDOWS_CREDENTIAL_MANAGER_TARGET); if ($null -ne $value) { [Console]::Out.Write($value) }`
698
+ ], {
699
+ env: {
700
+ ...process.env,
701
+ AI_SDK_WINDOWS_CREDENTIAL_MANAGER_SOURCE: credentialManagerSource,
702
+ AI_SDK_WINDOWS_CREDENTIAL_MANAGER_TARGET: targetName
703
+ },
704
+ windowsHide: true
705
+ })).stdout || void 0;
706
+ } catch {
707
+ return;
708
+ }
709
+ }
710
+ //#endregion
711
+ //#region src/utils/oauth-access-token.ts
712
+ const DEFAULT_REFRESH_WINDOW_MS = 3e5;
713
+ function isAccessTokenExpiringSoon({ expiresAt, now = Date.now(), refreshWindowMs = DEFAULT_REFRESH_WINDOW_MS }) {
714
+ return expiresAt <= now + refreshWindowMs;
715
+ }
716
+ async function refreshOAuthAccessToken({ tokenUrl, clientId, refreshToken, requestFormat = "form", headers, fetch: fetchImplementation = globalThis.fetch }) {
717
+ const values = {
718
+ grant_type: "refresh_token",
719
+ client_id: clientId,
720
+ refresh_token: refreshToken
721
+ };
722
+ const response = await fetchImplementation(tokenUrl, {
723
+ method: "POST",
724
+ headers: {
725
+ "content-type": requestFormat === "json" ? "application/json" : "application/x-www-form-urlencoded",
726
+ ...headers
727
+ },
728
+ body: requestFormat === "json" ? JSON.stringify(values) : new URLSearchParams(values).toString()
729
+ });
730
+ const responseText = await response.text();
731
+ if (!response.ok) throw new Error(`OAuth access token refresh failed with status ${response.status}.`);
732
+ const parsed = await safeParseJSON({ text: responseText });
733
+ if (!parsed.success || !isRecord(parsed.value)) throw new Error("OAuth access token refresh returned invalid JSON.");
734
+ const accessToken = parsed.value.access_token;
735
+ if (typeof accessToken !== "string" || accessToken.length === 0) throw new Error("OAuth access token refresh response is missing access_token.");
736
+ const expiresAt = await resolveExpiresAt({
737
+ accessToken,
738
+ expiresIn: parsed.value.expires_in
739
+ });
740
+ const rotatedRefreshToken = parsed.value.refresh_token;
741
+ if (rotatedRefreshToken != null && (typeof rotatedRefreshToken !== "string" || rotatedRefreshToken.length === 0)) throw new Error("OAuth access token refresh response contains an invalid refresh_token.");
742
+ return {
743
+ accessToken,
744
+ expiresAt,
745
+ ...typeof rotatedRefreshToken === "string" ? { refreshToken: rotatedRefreshToken } : {}
746
+ };
747
+ }
748
+ async function resolveExpiresAt({ accessToken, expiresIn }) {
749
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn >= 0) return Date.now() + expiresIn * 1e3;
750
+ const jwtExpiresAt = await getJwtExpiresAt({ token: accessToken });
751
+ if (jwtExpiresAt != null) return jwtExpiresAt;
752
+ throw new Error("OAuth access token refresh response does not include a usable expiry.");
753
+ }
754
+ //#endregion
755
+ //#region src/utils/os.ts
928
756
  function isMacOS(platform) {
929
- return platform === "darwin";
757
+ return platform === "darwin";
930
758
  }
931
759
  function isLinux(platform) {
932
- return platform === "linux";
760
+ return platform === "linux";
933
761
  }
934
762
  function isWindows(platform) {
935
- return platform === "win32";
763
+ return platform === "win32";
936
764
  }
937
-
938
- // src/utils/sandbox-credential-brokering.ts
939
- import { randomBytes } from "crypto";
940
- var SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX = "aisdkhc_";
765
+ //#endregion
766
+ //#region src/utils/sandbox-credential-brokering.ts
767
+ const SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX = "aisdkhc_";
941
768
  function generateSandboxCredentialPlaceholder() {
942
- return `${SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX}${randomBytes(32).toString("base64url")}`;
769
+ return `${SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX}${randomBytes(32).toString("base64url")}`;
943
770
  }
944
771
  function isSandboxCredentialPlaceholder(value) {
945
- return /^aisdkhc_[A-Za-z0-9_-]{43}$/.test(value);
772
+ return /^aisdkhc_[A-Za-z0-9_-]{43}$/.test(value);
946
773
  }
774
+ /**
775
+ * Warns when credential brokering is unavailable, but only if real credentials
776
+ * remain among the credentials forwarded into the sandbox.
777
+ */
947
778
  function warnCredentialBrokeringUnavailable(options) {
948
- const credentialEnvironmentVariables = [
949
- ...new Set(options.credentialEnvironmentVariables)
950
- ];
951
- const credentials = credentialEnvironmentVariables.map((name) => options.environment[name]).filter(
952
- (credential) => credential != null && credential.length > 0
953
- );
954
- const forwardedCredentials = credentialEnvironmentVariables.map((name) => options.forwardedEnvironment[name]).filter((credential) => credential != null);
955
- if (!credentials.some(
956
- (credential) => forwardedCredentials.some(
957
- (forwardedCredential) => forwardedCredential.includes(credential)
958
- )
959
- )) {
960
- return;
961
- }
962
- console.warn(
963
- "The sandbox implementation does not support configuring request transformations, so credential brokering does not work. Falling back to less secure credential forwarding."
964
- );
965
- }
966
- function maskSandboxCredentials({
967
- environment,
968
- credentialEnvironmentVariables
969
- }) {
970
- const maskedEnvironment = { ...environment };
971
- for (const name of credentialEnvironmentVariables) {
972
- if (maskedEnvironment[name] != null) {
973
- maskedEnvironment[name] = name;
974
- }
975
- }
976
- return maskedEnvironment;
977
- }
978
- function createCredentialRequestTransformation({
979
- matchUrl,
980
- matchHeaders,
981
- transformHeaders
982
- }) {
983
- const url = new URL(matchUrl);
984
- const pathname = url.pathname.replace(/\/+$/, "");
985
- return {
986
- match: {
987
- host: url.hostname,
988
- ...pathname.length === 0 ? {} : { path: { startsWith: pathname } },
989
- headers: Object.entries(matchHeaders).map(([key, value]) => ({
990
- key: { exact: key },
991
- value: { exact: value }
992
- }))
993
- },
994
- transform: { headers: transformHeaders }
995
- };
996
- }
997
-
998
- // src/utils/credential-forwarding.ts
999
- async function applyCredentialForwarding({
1000
- environment,
1001
- credentialEnvironmentVariables,
1002
- credentialForwarding
1003
- }) {
1004
- const forwardedEnvironment = { ...environment };
1005
- if (credentialForwarding == null) return forwardedEnvironment;
1006
- for (const environmentVariableName of new Set(
1007
- credentialEnvironmentVariables
1008
- )) {
1009
- const credential = forwardedEnvironment[environmentVariableName];
1010
- if (credential == null) continue;
1011
- forwardedEnvironment[environmentVariableName] = await credentialForwarding({
1012
- credential,
1013
- environmentVariableName
1014
- });
1015
- }
1016
- return forwardedEnvironment;
1017
- }
1018
- async function resolveSandboxCredentialEnvironment({
1019
- environment,
1020
- credentialEnvironmentVariables,
1021
- credentialForwarding,
1022
- previousSandboxCredentialEnvironment
1023
- }) {
1024
- const sandboxCredentialEnvironment = {};
1025
- for (const environmentVariableName of new Set(
1026
- credentialEnvironmentVariables
1027
- )) {
1028
- if (environment[environmentVariableName] == null) continue;
1029
- if (previousSandboxCredentialEnvironment != null && Object.prototype.hasOwnProperty.call(
1030
- previousSandboxCredentialEnvironment,
1031
- environmentVariableName
1032
- )) {
1033
- sandboxCredentialEnvironment[environmentVariableName] = previousSandboxCredentialEnvironment[environmentVariableName];
1034
- continue;
1035
- }
1036
- const placeholder = generateSandboxCredentialPlaceholder();
1037
- sandboxCredentialEnvironment[environmentVariableName] = credentialForwarding == null ? placeholder : await credentialForwarding({
1038
- credential: placeholder,
1039
- environmentVariableName
1040
- });
1041
- }
1042
- return sandboxCredentialEnvironment;
1043
- }
1044
-
1045
- // src/utils/sandbox-home-dir.ts
1046
- import path from "path";
1047
- async function resolveSandboxHomeDir({
1048
- sandbox,
1049
- abortSignal
1050
- }) {
1051
- const result = await sandbox.run({
1052
- command: 'printf "%s" "$HOME"',
1053
- abortSignal
1054
- });
1055
- const homeDir = result.stdout.trim();
1056
- if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
1057
- throw new Error(
1058
- `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
1059
- );
1060
- }
1061
- return homeDir;
1062
- }
1063
-
1064
- // src/utils/shell-quote.ts
779
+ const credentialEnvironmentVariables = [...new Set(options.credentialEnvironmentVariables)];
780
+ const credentials = credentialEnvironmentVariables.map((name) => options.environment[name]).filter((credential) => credential != null && credential.length > 0);
781
+ const forwardedCredentials = credentialEnvironmentVariables.map((name) => options.forwardedEnvironment[name]).filter((credential) => credential != null);
782
+ if (!credentials.some((credential) => forwardedCredentials.some((forwardedCredential) => forwardedCredential.includes(credential)))) return;
783
+ console.warn("The sandbox implementation does not support configuring request transformations, so credential brokering does not work. Falling back to less secure credential forwarding.");
784
+ }
785
+ function maskSandboxCredentials({ environment, credentialEnvironmentVariables }) {
786
+ const maskedEnvironment = { ...environment };
787
+ for (const name of credentialEnvironmentVariables) if (maskedEnvironment[name] != null) maskedEnvironment[name] = name;
788
+ return maskedEnvironment;
789
+ }
790
+ function createCredentialRequestTransformation({ matchUrl, matchHeaders, transformHeaders }) {
791
+ const url = new URL(matchUrl);
792
+ const pathname = url.pathname.replace(/\/+$/, "");
793
+ return {
794
+ match: {
795
+ host: url.hostname,
796
+ ...pathname.length === 0 ? {} : { path: { startsWith: pathname } },
797
+ headers: Object.entries(matchHeaders).map(([key, value]) => ({
798
+ key: { exact: key },
799
+ value: { exact: value }
800
+ }))
801
+ },
802
+ transform: { headers: transformHeaders }
803
+ };
804
+ }
805
+ //#endregion
806
+ //#region src/utils/credential-forwarding.ts
807
+ async function applyCredentialForwarding({ environment, credentialEnvironmentVariables, credentialForwarding }) {
808
+ const forwardedEnvironment = { ...environment };
809
+ if (credentialForwarding == null) return forwardedEnvironment;
810
+ for (const environmentVariableName of new Set(credentialEnvironmentVariables)) {
811
+ const credential = forwardedEnvironment[environmentVariableName];
812
+ if (credential == null) continue;
813
+ forwardedEnvironment[environmentVariableName] = await credentialForwarding({
814
+ credential,
815
+ environmentVariableName
816
+ });
817
+ }
818
+ return forwardedEnvironment;
819
+ }
820
+ async function resolveSandboxCredentialEnvironment({ environment, credentialEnvironmentVariables, credentialForwarding, previousSandboxCredentialEnvironment }) {
821
+ const sandboxCredentialEnvironment = {};
822
+ for (const environmentVariableName of new Set(credentialEnvironmentVariables)) {
823
+ if (environment[environmentVariableName] == null) continue;
824
+ if (previousSandboxCredentialEnvironment != null && Object.prototype.hasOwnProperty.call(previousSandboxCredentialEnvironment, environmentVariableName)) {
825
+ sandboxCredentialEnvironment[environmentVariableName] = previousSandboxCredentialEnvironment[environmentVariableName];
826
+ continue;
827
+ }
828
+ const placeholder = generateSandboxCredentialPlaceholder();
829
+ sandboxCredentialEnvironment[environmentVariableName] = credentialForwarding == null ? placeholder : await credentialForwarding({
830
+ credential: placeholder,
831
+ environmentVariableName
832
+ });
833
+ }
834
+ return sandboxCredentialEnvironment;
835
+ }
836
+ //#endregion
837
+ //#region src/utils/sandbox-home-dir.ts
838
+ async function resolveSandboxHomeDir({ sandbox, abortSignal }) {
839
+ const result = await sandbox.run({
840
+ command: "printf \"%s\" \"$HOME\"",
841
+ abortSignal
842
+ });
843
+ const homeDir = result.stdout.trim();
844
+ if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) throw new Error(`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`);
845
+ return homeDir;
846
+ }
847
+ //#endregion
848
+ //#region src/utils/shell-quote.ts
1065
849
  function shellQuote(value) {
1066
- return `'${value.replace(/'/g, `'\\''`)}'`;
1067
- }
1068
-
1069
- // src/utils/write-instructions.ts
1070
- import path2 from "path";
1071
- import {
1072
- safeParseJSON as safeParseJSON5
1073
- } from "@ai-sdk/provider-utils";
1074
- var INSTRUCTIONS_METADATA_VERSION = 1;
1075
- async function writeInstructions({
1076
- sandbox,
1077
- homePath,
1078
- instructionsFile,
1079
- instructions,
1080
- abortSignal
1081
- }) {
1082
- const { targetPath, metadataPath } = resolveInstructionsFilePath({
1083
- homePath,
1084
- instructionsFile
1085
- });
1086
- const hasInstructions = typeof instructions === "string" && instructions.trim().length > 0;
1087
- const trimmedInstructions = hasInstructions ? instructions.trim() : "";
1088
- const currentDiskContent = await sandbox.readTextFile({
1089
- path: targetPath,
1090
- abortSignal
1091
- });
1092
- const existingMetadata = await readInstructionsMetadata({
1093
- sandbox,
1094
- metadataPath,
1095
- abortSignal
1096
- });
1097
- if (hasInstructions) {
1098
- const originalContent = deriveOriginalContent({
1099
- currentDiskContent,
1100
- existingMetadata
1101
- });
1102
- const targetContent = originalContent != null && originalContent.trim().length > 0 ? `${originalContent.replace(/\n+$/, "")}
1103
-
1104
- ${trimmedInstructions}
1105
- ` : `${trimmedInstructions}
1106
- `;
1107
- if (currentDiskContent === targetContent && existingMetadata != null && existingMetadata.instructions === trimmedInstructions && existingMetadata.originalContent === originalContent) {
1108
- return { changed: false, filePath: targetPath };
1109
- }
1110
- await sandbox.writeTextFile({
1111
- path: targetPath,
1112
- content: targetContent,
1113
- abortSignal
1114
- });
1115
- await writeInstructionsMetadata({
1116
- sandbox,
1117
- metadataPath,
1118
- metadata: {
1119
- version: INSTRUCTIONS_METADATA_VERSION,
1120
- originalContent,
1121
- instructions: trimmedInstructions,
1122
- appliedContent: targetContent
1123
- },
1124
- abortSignal
1125
- });
1126
- return { changed: true, filePath: targetPath };
1127
- }
1128
- if (existingMetadata == null) {
1129
- return { changed: false, filePath: targetPath };
1130
- }
1131
- const restoredContent = deriveRestoredContent({
1132
- currentDiskContent,
1133
- existingMetadata
1134
- });
1135
- if (restoredContent != null) {
1136
- const contentToWrite = `${restoredContent.replace(/\n+$/, "")}
1137
- `;
1138
- await sandbox.writeTextFile({
1139
- path: targetPath,
1140
- content: contentToWrite,
1141
- abortSignal
1142
- });
1143
- } else {
1144
- await removeTargetFile({
1145
- sandbox,
1146
- targetPath,
1147
- abortSignal
1148
- });
1149
- }
1150
- await removeMetadataFile({
1151
- sandbox,
1152
- metadataPath,
1153
- abortSignal
1154
- });
1155
- return { changed: true, filePath: targetPath };
1156
- }
1157
- function deriveOriginalContent({
1158
- currentDiskContent,
1159
- existingMetadata
1160
- }) {
1161
- if (existingMetadata == null) {
1162
- return currentDiskContent;
1163
- }
1164
- if (currentDiskContent == null) {
1165
- return existingMetadata.originalContent;
1166
- }
1167
- if (currentDiskContent === existingMetadata.appliedContent) {
1168
- return existingMetadata.originalContent;
1169
- }
1170
- const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
1171
- const expectedSuffix = `
1172
-
1173
- ${existingMetadata.instructions}`;
1174
- if (trimmedDisk.endsWith(expectedSuffix)) {
1175
- const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
1176
- return userBase.length > 0 ? userBase : null;
1177
- }
1178
- if (trimmedDisk === existingMetadata.instructions) {
1179
- return null;
1180
- }
1181
- return existingMetadata.originalContent ?? currentDiskContent;
1182
- }
1183
- function deriveRestoredContent({
1184
- currentDiskContent,
1185
- existingMetadata
1186
- }) {
1187
- if (currentDiskContent == null) {
1188
- return null;
1189
- }
1190
- if (currentDiskContent === existingMetadata.appliedContent) {
1191
- return existingMetadata.originalContent != null && existingMetadata.originalContent.trim().length > 0 ? existingMetadata.originalContent : null;
1192
- }
1193
- const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
1194
- const expectedSuffix = `
1195
-
1196
- ${existingMetadata.instructions}`;
1197
- if (trimmedDisk.endsWith(expectedSuffix)) {
1198
- const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
1199
- return userBase.length > 0 ? userBase : null;
1200
- }
1201
- if (trimmedDisk === existingMetadata.instructions) {
1202
- return null;
1203
- }
1204
- return currentDiskContent;
1205
- }
1206
- function resolveInstructionsFilePath({
1207
- homePath,
1208
- instructionsFile
1209
- }) {
1210
- if (typeof homePath !== "string" || homePath.trim().length === 0) {
1211
- throw new Error("Invalid homePath: expected a non-empty string.");
1212
- }
1213
- if (!path2.posix.isAbsolute(homePath)) {
1214
- throw new Error(
1215
- `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`
1216
- );
1217
- }
1218
- if (typeof instructionsFile !== "string" || instructionsFile.trim().length === 0) {
1219
- throw new Error(
1220
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`
1221
- );
1222
- }
1223
- const containsTraversal = instructionsFile.split(/[\\/]/).some((segment) => segment === "..");
1224
- const normalizedInstructionsFile = path2.posix.normalize(
1225
- instructionsFile.trim()
1226
- );
1227
- const normalizedNoTrailingSlash = normalizedInstructionsFile.replace(
1228
- /\/+$/,
1229
- ""
1230
- );
1231
- if (instructionsFile.includes("\\") || path2.posix.isAbsolute(instructionsFile) || path2.win32.isAbsolute(instructionsFile) || containsTraversal || instructionsFile.endsWith("/") || instructionsFile.endsWith("\\") || instructionsFile.endsWith("/.") || instructionsFile.endsWith("/..") || normalizedNoTrailingSlash === "" || normalizedNoTrailingSlash === "." || normalizedInstructionsFile.startsWith("../") || normalizedInstructionsFile.includes("/../") || normalizedInstructionsFile.endsWith("/..")) {
1232
- throw new Error(
1233
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`
1234
- );
1235
- }
1236
- const targetPath = path2.posix.join(homePath, normalizedNoTrailingSlash);
1237
- const relative = path2.posix.relative(homePath, targetPath);
1238
- if (relative === "" || relative.startsWith("..") || path2.posix.isAbsolute(relative)) {
1239
- throw new Error(
1240
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`
1241
- );
1242
- }
1243
- const dir = path2.posix.dirname(targetPath);
1244
- const base = path2.posix.basename(targetPath);
1245
- const metadataPath = path2.posix.join(
1246
- dir,
1247
- `.${base}.ai-sdk-harness-instructions.json`
1248
- );
1249
- return { targetPath, metadataPath };
1250
- }
1251
- async function readInstructionsMetadata({
1252
- sandbox,
1253
- metadataPath,
1254
- abortSignal
1255
- }) {
1256
- const content = await sandbox.readTextFile({
1257
- path: metadataPath,
1258
- abortSignal
1259
- });
1260
- if (content == null) return void 0;
1261
- const parsed = await safeParseJSON5({ text: content });
1262
- if (!parsed.success || !isInstructionsMetadata(parsed.value)) {
1263
- throw new Error(
1264
- `Invalid AI SDK harness instructions metadata: ${metadataPath}`
1265
- );
1266
- }
1267
- return parsed.value;
850
+ return `'${value.replace(/'/g, `'\\''`)}'`;
851
+ }
852
+ //#endregion
853
+ //#region src/utils/write-instructions.ts
854
+ const INSTRUCTIONS_METADATA_VERSION = 1;
855
+ async function writeInstructions({ sandbox, homePath, instructionsFile, instructions, abortSignal }) {
856
+ const { targetPath, metadataPath } = resolveInstructionsFilePath({
857
+ homePath,
858
+ instructionsFile
859
+ });
860
+ const hasInstructions = typeof instructions === "string" && instructions.trim().length > 0;
861
+ const trimmedInstructions = hasInstructions ? instructions.trim() : "";
862
+ const currentDiskContent = await sandbox.readTextFile({
863
+ path: targetPath,
864
+ abortSignal
865
+ });
866
+ const existingMetadata = await readInstructionsMetadata({
867
+ sandbox,
868
+ metadataPath,
869
+ abortSignal
870
+ });
871
+ if (hasInstructions) {
872
+ const originalContent = deriveOriginalContent({
873
+ currentDiskContent,
874
+ existingMetadata
875
+ });
876
+ const targetContent = originalContent != null && originalContent.trim().length > 0 ? `${originalContent.replace(/\n+$/, "")}\n\n${trimmedInstructions}\n` : `${trimmedInstructions}\n`;
877
+ if (currentDiskContent === targetContent && existingMetadata != null && existingMetadata.instructions === trimmedInstructions && existingMetadata.originalContent === originalContent) return {
878
+ changed: false,
879
+ filePath: targetPath
880
+ };
881
+ await sandbox.writeTextFile({
882
+ path: targetPath,
883
+ content: targetContent,
884
+ abortSignal
885
+ });
886
+ await writeInstructionsMetadata({
887
+ sandbox,
888
+ metadataPath,
889
+ metadata: {
890
+ version: INSTRUCTIONS_METADATA_VERSION,
891
+ originalContent,
892
+ instructions: trimmedInstructions,
893
+ appliedContent: targetContent
894
+ },
895
+ abortSignal
896
+ });
897
+ return {
898
+ changed: true,
899
+ filePath: targetPath
900
+ };
901
+ }
902
+ if (existingMetadata == null) return {
903
+ changed: false,
904
+ filePath: targetPath
905
+ };
906
+ const restoredContent = deriveRestoredContent({
907
+ currentDiskContent,
908
+ existingMetadata
909
+ });
910
+ if (restoredContent != null) {
911
+ const contentToWrite = `${restoredContent.replace(/\n+$/, "")}\n`;
912
+ await sandbox.writeTextFile({
913
+ path: targetPath,
914
+ content: contentToWrite,
915
+ abortSignal
916
+ });
917
+ } else await removeTargetFile({
918
+ sandbox,
919
+ targetPath,
920
+ abortSignal
921
+ });
922
+ await removeMetadataFile({
923
+ sandbox,
924
+ metadataPath,
925
+ abortSignal
926
+ });
927
+ return {
928
+ changed: true,
929
+ filePath: targetPath
930
+ };
931
+ }
932
+ function deriveOriginalContent({ currentDiskContent, existingMetadata }) {
933
+ if (existingMetadata == null) return currentDiskContent;
934
+ if (currentDiskContent == null) return existingMetadata.originalContent;
935
+ if (currentDiskContent === existingMetadata.appliedContent) return existingMetadata.originalContent;
936
+ const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
937
+ const expectedSuffix = `\n\n${existingMetadata.instructions}`;
938
+ if (trimmedDisk.endsWith(expectedSuffix)) {
939
+ const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
940
+ return userBase.length > 0 ? userBase : null;
941
+ }
942
+ if (trimmedDisk === existingMetadata.instructions) return null;
943
+ return existingMetadata.originalContent ?? currentDiskContent;
944
+ }
945
+ function deriveRestoredContent({ currentDiskContent, existingMetadata }) {
946
+ if (currentDiskContent == null) return null;
947
+ if (currentDiskContent === existingMetadata.appliedContent) return existingMetadata.originalContent != null && existingMetadata.originalContent.trim().length > 0 ? existingMetadata.originalContent : null;
948
+ const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
949
+ const expectedSuffix = `\n\n${existingMetadata.instructions}`;
950
+ if (trimmedDisk.endsWith(expectedSuffix)) {
951
+ const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
952
+ return userBase.length > 0 ? userBase : null;
953
+ }
954
+ if (trimmedDisk === existingMetadata.instructions) return null;
955
+ return currentDiskContent;
956
+ }
957
+ function resolveInstructionsFilePath({ homePath, instructionsFile }) {
958
+ if (typeof homePath !== "string" || homePath.trim().length === 0) throw new Error("Invalid homePath: expected a non-empty string.");
959
+ if (!path.posix.isAbsolute(homePath)) throw new Error(`Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`);
960
+ if (typeof instructionsFile !== "string" || instructionsFile.trim().length === 0) throw new Error(`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`);
961
+ const containsTraversal = instructionsFile.split(/[\\/]/).some((segment) => segment === "..");
962
+ const normalizedInstructionsFile = path.posix.normalize(instructionsFile.trim());
963
+ const normalizedNoTrailingSlash = normalizedInstructionsFile.replace(/\/+$/, "");
964
+ if (instructionsFile.includes("\\") || path.posix.isAbsolute(instructionsFile) || path.win32.isAbsolute(instructionsFile) || containsTraversal || instructionsFile.endsWith("/") || instructionsFile.endsWith("\\") || instructionsFile.endsWith("/.") || instructionsFile.endsWith("/..") || normalizedNoTrailingSlash === "" || normalizedNoTrailingSlash === "." || normalizedInstructionsFile.startsWith("../") || normalizedInstructionsFile.includes("/../") || normalizedInstructionsFile.endsWith("/..")) throw new Error(`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`);
965
+ const targetPath = path.posix.join(homePath, normalizedNoTrailingSlash);
966
+ const relative = path.posix.relative(homePath, targetPath);
967
+ if (relative === "" || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`);
968
+ const dir = path.posix.dirname(targetPath);
969
+ const base = path.posix.basename(targetPath);
970
+ return {
971
+ targetPath,
972
+ metadataPath: path.posix.join(dir, `.${base}.ai-sdk-harness-instructions.json`)
973
+ };
974
+ }
975
+ async function readInstructionsMetadata({ sandbox, metadataPath, abortSignal }) {
976
+ const content = await sandbox.readTextFile({
977
+ path: metadataPath,
978
+ abortSignal
979
+ });
980
+ if (content == null) return void 0;
981
+ const parsed = await safeParseJSON({ text: content });
982
+ if (!parsed.success || !isInstructionsMetadata(parsed.value)) throw new Error(`Invalid AI SDK harness instructions metadata: ${metadataPath}`);
983
+ return parsed.value;
1268
984
  }
1269
985
  function isInstructionsMetadata(value) {
1270
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
1271
- return false;
1272
- }
1273
- const candidate = value;
1274
- return candidate.version === INSTRUCTIONS_METADATA_VERSION && (candidate.originalContent === null || typeof candidate.originalContent === "string") && typeof candidate.instructions === "string" && typeof candidate.appliedContent === "string";
1275
- }
1276
- async function writeInstructionsMetadata({
1277
- sandbox,
1278
- metadataPath,
1279
- metadata,
1280
- abortSignal
1281
- }) {
1282
- const temporaryPath = `${metadataPath}.tmp`;
1283
- await sandbox.writeTextFile({
1284
- path: temporaryPath,
1285
- content: `${JSON.stringify(metadata, null, 2)}
1286
- `,
1287
- abortSignal
1288
- });
1289
- await runSandboxCommand({
1290
- sandbox,
1291
- command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(metadataPath)}`,
1292
- abortSignal,
1293
- errorMessage: `Failed to update instructions metadata: ${metadataPath}`
1294
- });
1295
- }
1296
- async function removeMetadataFile({
1297
- sandbox,
1298
- metadataPath,
1299
- abortSignal
1300
- }) {
1301
- await runSandboxCommand({
1302
- sandbox,
1303
- command: `rm -f -- ${shellQuote(metadataPath)}`,
1304
- abortSignal,
1305
- errorMessage: `Failed to remove instructions metadata: ${metadataPath}`
1306
- });
1307
- }
1308
- async function removeTargetFile({
1309
- sandbox,
1310
- targetPath,
1311
- abortSignal
1312
- }) {
1313
- await runSandboxCommand({
1314
- sandbox,
1315
- command: `rm -f -- ${shellQuote(targetPath)}`,
1316
- abortSignal,
1317
- errorMessage: `Failed to remove instructions file: ${targetPath}`
1318
- });
1319
- }
1320
- async function runSandboxCommand({
1321
- sandbox,
1322
- command,
1323
- abortSignal,
1324
- errorMessage
1325
- }) {
1326
- const result = await sandbox.run({ command, abortSignal });
1327
- if (result.exitCode !== 0) {
1328
- throw new Error(
1329
- `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`
1330
- );
1331
- }
1332
- }
1333
-
1334
- // src/utils/write-skills.ts
1335
- import { createHash } from "crypto";
1336
- import path3 from "path";
1337
- import {
1338
- safeParseJSON as safeParseJSON6
1339
- } from "@ai-sdk/provider-utils";
1340
- var SKILLS_MANIFEST_FILENAME = ".ai-sdk-harness-skills.json";
1341
- var SKILLS_MANIFEST_VERSION = 1;
1342
- var SAFE_MANIFEST_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
1343
- async function writeSkills({
1344
- sandbox,
1345
- homePath,
1346
- skillsDir,
1347
- skills,
1348
- abortSignal,
1349
- skillNamePattern = /^[A-Za-z0-9._-]+$/,
1350
- invalidSkillNameMessage = ({ name }) => `Invalid skill name: ${name}`,
1351
- filePathMode = "relative",
1352
- invalidSkillFilePathMessage = ({ skillName, filePath }) => `Invalid skill file path for ${skillName}: ${filePath}`,
1353
- trailingNewline = false
1354
- }) {
1355
- const rootDir = resolveSkillsRootDir({ homePath, skillsDir });
1356
- const projectedSkills = skills.map(
1357
- (skill) => projectSkill({
1358
- skill,
1359
- skillNamePattern,
1360
- invalidSkillNameMessage,
1361
- filePathMode,
1362
- invalidSkillFilePathMessage,
1363
- trailingNewline
1364
- })
1365
- ).sort((a, b) => a.name.localeCompare(b.name));
1366
- assertUniqueSkillNames(projectedSkills);
1367
- const manifestPath = path3.posix.join(rootDir, SKILLS_MANIFEST_FILENAME);
1368
- const existingManifest = await readSkillsManifest({
1369
- sandbox,
1370
- manifestPath,
1371
- abortSignal
1372
- });
1373
- const nextEntries = projectedSkills.map(({ name, hash }) => ({ name, hash }));
1374
- const nextByName = new Map(nextEntries.map((entry) => [entry.name, entry]));
1375
- if (existingManifest?.state === "pending") {
1376
- await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
1377
- const recoveryNames = existingManifest.skills.map((skill) => skill.name);
1378
- await removeSkillDirectories({
1379
- sandbox,
1380
- rootDir,
1381
- skillNames: recoveryNames,
1382
- abortSignal
1383
- });
1384
- const removed2 = recoveryNames.filter((name) => !nextByName.has(name)).sort((a, b) => a.localeCompare(b));
1385
- await writeProjectedSkills({
1386
- sandbox,
1387
- rootDir,
1388
- skills: projectedSkills,
1389
- abortSignal
1390
- });
1391
- await writeSkillsManifest({
1392
- sandbox,
1393
- manifestPath,
1394
- manifest: {
1395
- version: SKILLS_MANIFEST_VERSION,
1396
- state: "complete",
1397
- skills: nextEntries
1398
- },
1399
- abortSignal
1400
- });
1401
- return {
1402
- changed: recoveryNames.length > 0 || projectedSkills.length > 0,
1403
- written: projectedSkills.map((skill) => skill.name),
1404
- removed: removed2,
1405
- unchanged: []
1406
- };
1407
- }
1408
- const previousEntries = existingManifest?.skills ?? [];
1409
- const previousByName = new Map(
1410
- previousEntries.map((entry) => [entry.name, entry])
1411
- );
1412
- const removed = previousEntries.filter((entry) => !nextByName.has(entry.name)).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1413
- const written = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash !== skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1414
- const unchanged = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash === skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1415
- const changed = removed.length > 0 || written.length > 0;
1416
- if (!changed && existingManifest != null) {
1417
- return { changed: false, written, removed, unchanged };
1418
- }
1419
- const previouslyOwned = new Set(previousEntries.map((entry) => entry.name));
1420
- for (const skillName of written) {
1421
- if (previouslyOwned.has(skillName)) continue;
1422
- await assertSkillDirectoryAvailable({
1423
- sandbox,
1424
- rootDir,
1425
- skillName,
1426
- abortSignal
1427
- });
1428
- }
1429
- await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
1430
- const pendingNames = Array.from(
1431
- /* @__PURE__ */ new Set([
1432
- ...previousEntries.map((entry) => entry.name),
1433
- ...nextEntries.map((entry) => entry.name)
1434
- ])
1435
- ).sort((a, b) => a.localeCompare(b));
1436
- await writeSkillsManifest({
1437
- sandbox,
1438
- manifestPath,
1439
- manifest: {
1440
- version: SKILLS_MANIFEST_VERSION,
1441
- state: "pending",
1442
- skills: pendingNames.map((name) => ({
1443
- name,
1444
- hash: nextByName.get(name)?.hash ?? previousByName.get(name).hash
1445
- }))
1446
- },
1447
- abortSignal
1448
- });
1449
- await removeSkillDirectories({
1450
- sandbox,
1451
- rootDir,
1452
- skillNames: [
1453
- ...removed,
1454
- ...written.filter((name) => previouslyOwned.has(name))
1455
- ],
1456
- abortSignal
1457
- });
1458
- const writtenSet = new Set(written);
1459
- await writeProjectedSkills({
1460
- sandbox,
1461
- rootDir,
1462
- skills: projectedSkills.filter((skill) => writtenSet.has(skill.name)),
1463
- abortSignal
1464
- });
1465
- await writeSkillsManifest({
1466
- sandbox,
1467
- manifestPath,
1468
- manifest: {
1469
- version: SKILLS_MANIFEST_VERSION,
1470
- state: "complete",
1471
- skills: nextEntries
1472
- },
1473
- abortSignal
1474
- });
1475
- return { changed, written, removed, unchanged };
1476
- }
1477
- async function ensureSkillsDirectory({
1478
- sandbox,
1479
- rootDir,
1480
- abortSignal
1481
- }) {
1482
- await runSandboxCommand2({
1483
- sandbox,
1484
- command: `mkdir -p ${shellQuote(rootDir)}`,
1485
- abortSignal,
1486
- errorMessage: `Failed to create skills directory: ${rootDir}`
1487
- });
1488
- }
1489
- function projectSkill({
1490
- skill,
1491
- skillNamePattern,
1492
- invalidSkillNameMessage,
1493
- filePathMode,
1494
- invalidSkillFilePathMessage,
1495
- trailingNewline
1496
- }) {
1497
- const name = validateSkillName({
1498
- name: skill.name,
1499
- pattern: skillNamePattern,
1500
- message: invalidSkillNameMessage
1501
- });
1502
- const files = /* @__PURE__ */ new Map();
1503
- files.set("SKILL.md", renderSkillFile({ skill, trailingNewline }));
1504
- for (const file of skill.files ?? []) {
1505
- files.set(
1506
- normalizeSkillFilePath({
1507
- skillName: skill.name,
1508
- filePath: file.path,
1509
- mode: filePathMode,
1510
- message: invalidSkillFilePathMessage
1511
- }),
1512
- file.content
1513
- );
1514
- }
1515
- const projectedFiles = Array.from(files, ([filePath, content]) => ({
1516
- path: filePath,
1517
- content
1518
- })).sort((a, b) => a.path.localeCompare(b.path));
1519
- const hash = createHash("sha256");
1520
- for (const file of projectedFiles) {
1521
- hash.update(String(Buffer.byteLength(file.path)));
1522
- hash.update(":");
1523
- hash.update(file.path);
1524
- hash.update(String(Buffer.byteLength(file.content)));
1525
- hash.update(":");
1526
- hash.update(file.content);
1527
- }
1528
- return { name, hash: hash.digest("hex"), files: projectedFiles };
1529
- }
1530
- async function readSkillsManifest({
1531
- sandbox,
1532
- manifestPath,
1533
- abortSignal
1534
- }) {
1535
- const content = await sandbox.readTextFile({
1536
- path: manifestPath,
1537
- abortSignal
1538
- });
1539
- if (content == null) return void 0;
1540
- const parsed = await safeParseJSON6({ text: content });
1541
- if (!parsed.success || !isSkillsManifest(parsed.value)) {
1542
- throw new Error(`Invalid AI SDK harness skills manifest: ${manifestPath}`);
1543
- }
1544
- return parsed.value;
986
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
987
+ const candidate = value;
988
+ return candidate.version === INSTRUCTIONS_METADATA_VERSION && (candidate.originalContent === null || typeof candidate.originalContent === "string") && typeof candidate.instructions === "string" && typeof candidate.appliedContent === "string";
989
+ }
990
+ async function writeInstructionsMetadata({ sandbox, metadataPath, metadata, abortSignal }) {
991
+ const temporaryPath = `${metadataPath}.tmp`;
992
+ await sandbox.writeTextFile({
993
+ path: temporaryPath,
994
+ content: `${JSON.stringify(metadata, null, 2)}\n`,
995
+ abortSignal
996
+ });
997
+ await runSandboxCommand$1({
998
+ sandbox,
999
+ command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(metadataPath)}`,
1000
+ abortSignal,
1001
+ errorMessage: `Failed to update instructions metadata: ${metadataPath}`
1002
+ });
1003
+ }
1004
+ async function removeMetadataFile({ sandbox, metadataPath, abortSignal }) {
1005
+ await runSandboxCommand$1({
1006
+ sandbox,
1007
+ command: `rm -f -- ${shellQuote(metadataPath)}`,
1008
+ abortSignal,
1009
+ errorMessage: `Failed to remove instructions metadata: ${metadataPath}`
1010
+ });
1011
+ }
1012
+ async function removeTargetFile({ sandbox, targetPath, abortSignal }) {
1013
+ await runSandboxCommand$1({
1014
+ sandbox,
1015
+ command: `rm -f -- ${shellQuote(targetPath)}`,
1016
+ abortSignal,
1017
+ errorMessage: `Failed to remove instructions file: ${targetPath}`
1018
+ });
1019
+ }
1020
+ async function runSandboxCommand$1({ sandbox, command, abortSignal, errorMessage }) {
1021
+ const result = await sandbox.run({
1022
+ command,
1023
+ abortSignal
1024
+ });
1025
+ if (result.exitCode !== 0) throw new Error(`${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`);
1026
+ }
1027
+ //#endregion
1028
+ //#region src/utils/write-skills.ts
1029
+ const SKILLS_MANIFEST_FILENAME = ".ai-sdk-harness-skills.json";
1030
+ const SKILLS_MANIFEST_VERSION = 1;
1031
+ const SAFE_MANIFEST_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
1032
+ async function writeSkills({ sandbox, homePath, skillsDir, skills, abortSignal, skillNamePattern = /^[A-Za-z0-9._-]+$/, invalidSkillNameMessage = ({ name }) => `Invalid skill name: ${name}`, filePathMode = "relative", invalidSkillFilePathMessage = ({ skillName, filePath }) => `Invalid skill file path for ${skillName}: ${filePath}`, trailingNewline = false }) {
1033
+ const rootDir = resolveSkillsRootDir({
1034
+ homePath,
1035
+ skillsDir
1036
+ });
1037
+ const projectedSkills = skills.map((skill) => projectSkill({
1038
+ skill,
1039
+ skillNamePattern,
1040
+ invalidSkillNameMessage,
1041
+ filePathMode,
1042
+ invalidSkillFilePathMessage,
1043
+ trailingNewline
1044
+ })).sort((a, b) => a.name.localeCompare(b.name));
1045
+ assertUniqueSkillNames(projectedSkills);
1046
+ const manifestPath = path.posix.join(rootDir, SKILLS_MANIFEST_FILENAME);
1047
+ const existingManifest = await readSkillsManifest({
1048
+ sandbox,
1049
+ manifestPath,
1050
+ abortSignal
1051
+ });
1052
+ const nextEntries = projectedSkills.map(({ name, hash }) => ({
1053
+ name,
1054
+ hash
1055
+ }));
1056
+ const nextByName = new Map(nextEntries.map((entry) => [entry.name, entry]));
1057
+ if (existingManifest?.state === "pending") {
1058
+ await ensureSkillsDirectory({
1059
+ sandbox,
1060
+ rootDir,
1061
+ abortSignal
1062
+ });
1063
+ const recoveryNames = existingManifest.skills.map((skill) => skill.name);
1064
+ await removeSkillDirectories({
1065
+ sandbox,
1066
+ rootDir,
1067
+ skillNames: recoveryNames,
1068
+ abortSignal
1069
+ });
1070
+ const removed = recoveryNames.filter((name) => !nextByName.has(name)).sort((a, b) => a.localeCompare(b));
1071
+ await writeProjectedSkills({
1072
+ sandbox,
1073
+ rootDir,
1074
+ skills: projectedSkills,
1075
+ abortSignal
1076
+ });
1077
+ await writeSkillsManifest({
1078
+ sandbox,
1079
+ manifestPath,
1080
+ manifest: {
1081
+ version: SKILLS_MANIFEST_VERSION,
1082
+ state: "complete",
1083
+ skills: nextEntries
1084
+ },
1085
+ abortSignal
1086
+ });
1087
+ return {
1088
+ changed: recoveryNames.length > 0 || projectedSkills.length > 0,
1089
+ written: projectedSkills.map((skill) => skill.name),
1090
+ removed,
1091
+ unchanged: []
1092
+ };
1093
+ }
1094
+ const previousEntries = existingManifest?.skills ?? [];
1095
+ const previousByName = new Map(previousEntries.map((entry) => [entry.name, entry]));
1096
+ const removed = previousEntries.filter((entry) => !nextByName.has(entry.name)).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1097
+ const written = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash !== skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1098
+ const unchanged = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash === skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1099
+ const changed = removed.length > 0 || written.length > 0;
1100
+ if (!changed && existingManifest != null) return {
1101
+ changed: false,
1102
+ written,
1103
+ removed,
1104
+ unchanged
1105
+ };
1106
+ const previouslyOwned = new Set(previousEntries.map((entry) => entry.name));
1107
+ for (const skillName of written) {
1108
+ if (previouslyOwned.has(skillName)) continue;
1109
+ await assertSkillDirectoryAvailable({
1110
+ sandbox,
1111
+ rootDir,
1112
+ skillName,
1113
+ abortSignal
1114
+ });
1115
+ }
1116
+ await ensureSkillsDirectory({
1117
+ sandbox,
1118
+ rootDir,
1119
+ abortSignal
1120
+ });
1121
+ const pendingNames = Array.from(/* @__PURE__ */ new Set([...previousEntries.map((entry) => entry.name), ...nextEntries.map((entry) => entry.name)])).sort((a, b) => a.localeCompare(b));
1122
+ await writeSkillsManifest({
1123
+ sandbox,
1124
+ manifestPath,
1125
+ manifest: {
1126
+ version: SKILLS_MANIFEST_VERSION,
1127
+ state: "pending",
1128
+ skills: pendingNames.map((name) => ({
1129
+ name,
1130
+ hash: nextByName.get(name)?.hash ?? previousByName.get(name).hash
1131
+ }))
1132
+ },
1133
+ abortSignal
1134
+ });
1135
+ await removeSkillDirectories({
1136
+ sandbox,
1137
+ rootDir,
1138
+ skillNames: [...removed, ...written.filter((name) => previouslyOwned.has(name))],
1139
+ abortSignal
1140
+ });
1141
+ const writtenSet = new Set(written);
1142
+ await writeProjectedSkills({
1143
+ sandbox,
1144
+ rootDir,
1145
+ skills: projectedSkills.filter((skill) => writtenSet.has(skill.name)),
1146
+ abortSignal
1147
+ });
1148
+ await writeSkillsManifest({
1149
+ sandbox,
1150
+ manifestPath,
1151
+ manifest: {
1152
+ version: SKILLS_MANIFEST_VERSION,
1153
+ state: "complete",
1154
+ skills: nextEntries
1155
+ },
1156
+ abortSignal
1157
+ });
1158
+ return {
1159
+ changed,
1160
+ written,
1161
+ removed,
1162
+ unchanged
1163
+ };
1164
+ }
1165
+ async function ensureSkillsDirectory({ sandbox, rootDir, abortSignal }) {
1166
+ await runSandboxCommand({
1167
+ sandbox,
1168
+ command: `mkdir -p ${shellQuote(rootDir)}`,
1169
+ abortSignal,
1170
+ errorMessage: `Failed to create skills directory: ${rootDir}`
1171
+ });
1172
+ }
1173
+ function projectSkill({ skill, skillNamePattern, invalidSkillNameMessage, filePathMode, invalidSkillFilePathMessage, trailingNewline }) {
1174
+ const name = validateSkillName({
1175
+ name: skill.name,
1176
+ pattern: skillNamePattern,
1177
+ message: invalidSkillNameMessage
1178
+ });
1179
+ const files = /* @__PURE__ */ new Map();
1180
+ files.set("SKILL.md", renderSkillFile({
1181
+ skill,
1182
+ trailingNewline
1183
+ }));
1184
+ for (const file of skill.files ?? []) files.set(normalizeSkillFilePath({
1185
+ skillName: skill.name,
1186
+ filePath: file.path,
1187
+ mode: filePathMode,
1188
+ message: invalidSkillFilePathMessage
1189
+ }), file.content);
1190
+ const projectedFiles = Array.from(files, ([filePath, content]) => ({
1191
+ path: filePath,
1192
+ content
1193
+ })).sort((a, b) => a.path.localeCompare(b.path));
1194
+ const hash = createHash("sha256");
1195
+ for (const file of projectedFiles) {
1196
+ hash.update(String(Buffer.byteLength(file.path)));
1197
+ hash.update(":");
1198
+ hash.update(file.path);
1199
+ hash.update(String(Buffer.byteLength(file.content)));
1200
+ hash.update(":");
1201
+ hash.update(file.content);
1202
+ }
1203
+ return {
1204
+ name,
1205
+ hash: hash.digest("hex"),
1206
+ files: projectedFiles
1207
+ };
1208
+ }
1209
+ async function readSkillsManifest({ sandbox, manifestPath, abortSignal }) {
1210
+ const content = await sandbox.readTextFile({
1211
+ path: manifestPath,
1212
+ abortSignal
1213
+ });
1214
+ if (content == null) return void 0;
1215
+ const parsed = await safeParseJSON({ text: content });
1216
+ if (!parsed.success || !isSkillsManifest(parsed.value)) throw new Error(`Invalid AI SDK harness skills manifest: ${manifestPath}`);
1217
+ return parsed.value;
1545
1218
  }
1546
1219
  function isSkillsManifest(value) {
1547
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
1548
- return false;
1549
- }
1550
- const manifest = value;
1551
- if (manifest.version !== SKILLS_MANIFEST_VERSION || manifest.state !== "complete" && manifest.state !== "pending" || !Array.isArray(manifest.skills)) {
1552
- return false;
1553
- }
1554
- const names = /* @__PURE__ */ new Set();
1555
- for (const entry of manifest.skills) {
1556
- if (entry == null || typeof entry !== "object" || Array.isArray(entry)) {
1557
- return false;
1558
- }
1559
- const skill = entry;
1560
- if (typeof skill.name !== "string" || !isSafeManifestSkillName(skill.name) || typeof skill.hash !== "string" || !/^[a-f0-9]{64}$/.test(skill.hash) || names.has(skill.name)) {
1561
- return false;
1562
- }
1563
- names.add(skill.name);
1564
- }
1565
- return true;
1566
- }
1567
- async function writeSkillsManifest({
1568
- sandbox,
1569
- manifestPath,
1570
- manifest,
1571
- abortSignal
1572
- }) {
1573
- const temporaryPath = `${manifestPath}.tmp`;
1574
- await sandbox.writeTextFile({
1575
- path: temporaryPath,
1576
- content: `${JSON.stringify(manifest, null, 2)}
1577
- `,
1578
- abortSignal
1579
- });
1580
- await runSandboxCommand2({
1581
- sandbox,
1582
- command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(manifestPath)}`,
1583
- abortSignal,
1584
- errorMessage: `Failed to update skills manifest: ${manifestPath}`
1585
- });
1586
- }
1587
- async function assertSkillDirectoryAvailable({
1588
- sandbox,
1589
- rootDir,
1590
- skillName,
1591
- abortSignal
1592
- }) {
1593
- const skillDir = path3.posix.join(rootDir, skillName);
1594
- const result = await sandbox.run({
1595
- command: `test ! -e ${shellQuote(skillDir)}`,
1596
- abortSignal
1597
- });
1598
- if (result.exitCode !== 0) {
1599
- throw new Error(
1600
- `Cannot write harness skill '${skillName}': ${skillDir} already exists and is not owned by the AI SDK harness.`
1601
- );
1602
- }
1603
- }
1604
- async function removeSkillDirectories({
1605
- sandbox,
1606
- rootDir,
1607
- skillNames,
1608
- abortSignal
1609
- }) {
1610
- if (skillNames.length === 0) return;
1611
- const directories = Array.from(new Set(skillNames)).sort((a, b) => a.localeCompare(b)).map((name) => shellQuote(path3.posix.join(rootDir, name))).join(" ");
1612
- await runSandboxCommand2({
1613
- sandbox,
1614
- command: `rm -rf -- ${directories}`,
1615
- abortSignal,
1616
- errorMessage: `Failed to replace harness skills in: ${rootDir}`
1617
- });
1618
- }
1619
- async function writeProjectedSkills({
1620
- sandbox,
1621
- rootDir,
1622
- skills,
1623
- abortSignal
1624
- }) {
1625
- for (const skill of skills) {
1626
- const skillDir = path3.posix.join(rootDir, skill.name);
1627
- for (const file of skill.files) {
1628
- await sandbox.writeTextFile({
1629
- path: path3.posix.join(skillDir, file.path),
1630
- content: file.content,
1631
- abortSignal
1632
- });
1633
- }
1634
- }
1635
- }
1636
- async function runSandboxCommand2({
1637
- sandbox,
1638
- command,
1639
- abortSignal,
1640
- errorMessage
1641
- }) {
1642
- const result = await sandbox.run({ command, abortSignal });
1643
- if (result.exitCode !== 0) {
1644
- throw new Error(
1645
- `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`
1646
- );
1647
- }
1220
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
1221
+ const manifest = value;
1222
+ if (manifest.version !== SKILLS_MANIFEST_VERSION || manifest.state !== "complete" && manifest.state !== "pending" || !Array.isArray(manifest.skills)) return false;
1223
+ const names = /* @__PURE__ */ new Set();
1224
+ for (const entry of manifest.skills) {
1225
+ if (entry == null || typeof entry !== "object" || Array.isArray(entry)) return false;
1226
+ const skill = entry;
1227
+ if (typeof skill.name !== "string" || !isSafeManifestSkillName(skill.name) || typeof skill.hash !== "string" || !/^[a-f0-9]{64}$/.test(skill.hash) || names.has(skill.name)) return false;
1228
+ names.add(skill.name);
1229
+ }
1230
+ return true;
1231
+ }
1232
+ async function writeSkillsManifest({ sandbox, manifestPath, manifest, abortSignal }) {
1233
+ const temporaryPath = `${manifestPath}.tmp`;
1234
+ await sandbox.writeTextFile({
1235
+ path: temporaryPath,
1236
+ content: `${JSON.stringify(manifest, null, 2)}\n`,
1237
+ abortSignal
1238
+ });
1239
+ await runSandboxCommand({
1240
+ sandbox,
1241
+ command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(manifestPath)}`,
1242
+ abortSignal,
1243
+ errorMessage: `Failed to update skills manifest: ${manifestPath}`
1244
+ });
1245
+ }
1246
+ async function assertSkillDirectoryAvailable({ sandbox, rootDir, skillName, abortSignal }) {
1247
+ const skillDir = path.posix.join(rootDir, skillName);
1248
+ if ((await sandbox.run({
1249
+ command: `test ! -e ${shellQuote(skillDir)}`,
1250
+ abortSignal
1251
+ })).exitCode !== 0) throw new Error(`Cannot write harness skill '${skillName}': ${skillDir} already exists and is not owned by the AI SDK harness.`);
1252
+ }
1253
+ async function removeSkillDirectories({ sandbox, rootDir, skillNames, abortSignal }) {
1254
+ if (skillNames.length === 0) return;
1255
+ await runSandboxCommand({
1256
+ sandbox,
1257
+ command: `rm -rf -- ${Array.from(new Set(skillNames)).sort((a, b) => a.localeCompare(b)).map((name) => shellQuote(path.posix.join(rootDir, name))).join(" ")}`,
1258
+ abortSignal,
1259
+ errorMessage: `Failed to replace harness skills in: ${rootDir}`
1260
+ });
1261
+ }
1262
+ async function writeProjectedSkills({ sandbox, rootDir, skills, abortSignal }) {
1263
+ for (const skill of skills) {
1264
+ const skillDir = path.posix.join(rootDir, skill.name);
1265
+ for (const file of skill.files) await sandbox.writeTextFile({
1266
+ path: path.posix.join(skillDir, file.path),
1267
+ content: file.content,
1268
+ abortSignal
1269
+ });
1270
+ }
1271
+ }
1272
+ async function runSandboxCommand({ sandbox, command, abortSignal, errorMessage }) {
1273
+ const result = await sandbox.run({
1274
+ command,
1275
+ abortSignal
1276
+ });
1277
+ if (result.exitCode !== 0) throw new Error(`${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`);
1648
1278
  }
1649
1279
  function assertUniqueSkillNames(skills) {
1650
- for (let index = 1; index < skills.length; index++) {
1651
- if (skills[index - 1].name === skills[index].name) {
1652
- throw new Error(`Duplicate skill name: ${skills[index].name}`);
1653
- }
1654
- }
1280
+ for (let index = 1; index < skills.length; index++) if (skills[index - 1].name === skills[index].name) throw new Error(`Duplicate skill name: ${skills[index].name}`);
1655
1281
  }
1656
1282
  function isSafeManifestSkillName(name) {
1657
- SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1658
- const matches = SAFE_MANIFEST_SKILL_NAME.test(name);
1659
- SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1660
- return matches && name !== "." && name !== ".." && !name.includes("/");
1661
- }
1662
- function validateSkillName({
1663
- name,
1664
- pattern,
1665
- message
1666
- }) {
1667
- pattern.lastIndex = 0;
1668
- const matches = pattern.test(name);
1669
- pattern.lastIndex = 0;
1670
- if (!matches || name === "." || name === "..") {
1671
- throw new Error(message?.({ name }) ?? `Invalid skill name: ${name}`);
1672
- }
1673
- return name;
1674
- }
1675
- function normalizeSkillFilePath({
1676
- skillName,
1677
- filePath,
1678
- mode,
1679
- message
1680
- }) {
1681
- const normalized = mode === "strip-leading-slashes" ? filePath.replace(/^\/+/, "") : path3.posix.normalize(filePath);
1682
- const invalid = normalized === "" || mode === "relative" && normalized === "." || normalized.startsWith("../") || normalized.includes("/../") || normalized.endsWith("/..") || mode === "relative" && path3.posix.isAbsolute(normalized);
1683
- if (invalid) {
1684
- throw new Error(
1685
- message?.({ skillName: skillName ?? "", filePath }) ?? `Invalid skill file path for ${skillName}: ${filePath}`
1686
- );
1687
- }
1688
- return normalized;
1689
- }
1690
- function renderSkillFile({
1691
- skill,
1692
- trailingNewline
1693
- }) {
1694
- const content = `---
1695
- name: ${skill.name}
1696
- description: ${skill.description}
1697
- ---
1698
-
1699
- ${skill.content}`;
1700
- return trailingNewline ? `${content}
1701
- ` : content;
1702
- }
1703
- function resolveSkillsRootDir({
1704
- homePath,
1705
- skillsDir
1706
- }) {
1707
- if (typeof homePath !== "string" || homePath.trim().length === 0) {
1708
- throw new Error("Invalid homePath: expected a non-empty string.");
1709
- }
1710
- if (!path3.posix.isAbsolute(homePath)) {
1711
- throw new Error(
1712
- `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`
1713
- );
1714
- }
1715
- if (typeof skillsDir !== "string" || skillsDir.trim().length === 0) {
1716
- throw new Error(
1717
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`
1718
- );
1719
- }
1720
- const containsTraversal = skillsDir.split(/[\\/]/).some((segment) => segment === "..");
1721
- const normalizedSkillsDir = path3.posix.normalize(skillsDir.trim());
1722
- const normalizedNoTrailingSlash = normalizedSkillsDir.replace(/\/+$/, "");
1723
- if (skillsDir.includes("\\") || path3.posix.isAbsolute(skillsDir) || path3.win32.isAbsolute(skillsDir) || containsTraversal || normalizedNoTrailingSlash === "" || normalizedNoTrailingSlash === "." || normalizedSkillsDir.startsWith("../") || normalizedSkillsDir.includes("/../") || normalizedSkillsDir.endsWith("/..")) {
1724
- throw new Error(
1725
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`
1726
- );
1727
- }
1728
- const rootDir = path3.posix.join(homePath, normalizedNoTrailingSlash);
1729
- const relative = path3.posix.relative(homePath, rootDir);
1730
- if (relative === "" || relative.startsWith("..") || path3.posix.isAbsolute(relative)) {
1731
- throw new Error(
1732
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`
1733
- );
1734
- }
1735
- return rootDir;
1736
- }
1737
-
1738
- // src/utils/bridge-ready.ts
1739
- import {
1740
- safeParseJSON as safeParseJSON7
1741
- } from "@ai-sdk/provider-utils";
1742
- import { z as z4 } from "zod/v4";
1743
-
1744
- // src/v1/harness-v1-bridge-protocol.ts
1745
- import { z as z3 } from "zod/v4";
1746
-
1747
- // src/v1/harness-v1-diagnostic.ts
1748
- import { z } from "zod/v4";
1749
- var harnessV1DebugLevelSchema = z.enum([
1750
- "error",
1751
- "warn",
1752
- "info",
1753
- "debug",
1754
- "trace"
1283
+ SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1284
+ const matches = SAFE_MANIFEST_SKILL_NAME.test(name);
1285
+ SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1286
+ return matches && name !== "." && name !== ".." && !name.includes("/");
1287
+ }
1288
+ function validateSkillName({ name, pattern, message }) {
1289
+ pattern.lastIndex = 0;
1290
+ const matches = pattern.test(name);
1291
+ pattern.lastIndex = 0;
1292
+ if (!matches || name === "." || name === "..") throw new Error(message?.({ name }) ?? `Invalid skill name: ${name}`);
1293
+ return name;
1294
+ }
1295
+ function normalizeSkillFilePath({ skillName, filePath, mode, message }) {
1296
+ const normalized = mode === "strip-leading-slashes" ? filePath.replace(/^\/+/, "") : path.posix.normalize(filePath);
1297
+ if (normalized === "" || mode === "relative" && normalized === "." || normalized.startsWith("../") || normalized.includes("/../") || normalized.endsWith("/..") || mode === "relative" && path.posix.isAbsolute(normalized)) throw new Error(message?.({
1298
+ skillName: skillName ?? "",
1299
+ filePath
1300
+ }) ?? `Invalid skill file path for ${skillName}: ${filePath}`);
1301
+ return normalized;
1302
+ }
1303
+ function renderSkillFile({ skill, trailingNewline }) {
1304
+ const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
1305
+ return trailingNewline ? `${content}\n` : content;
1306
+ }
1307
+ function resolveSkillsRootDir({ homePath, skillsDir }) {
1308
+ if (typeof homePath !== "string" || homePath.trim().length === 0) throw new Error("Invalid homePath: expected a non-empty string.");
1309
+ if (!path.posix.isAbsolute(homePath)) throw new Error(`Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`);
1310
+ if (typeof skillsDir !== "string" || skillsDir.trim().length === 0) throw new Error(`Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`);
1311
+ const containsTraversal = skillsDir.split(/[\\/]/).some((segment) => segment === "..");
1312
+ const normalizedSkillsDir = path.posix.normalize(skillsDir.trim());
1313
+ const normalizedNoTrailingSlash = normalizedSkillsDir.replace(/\/+$/, "");
1314
+ if (skillsDir.includes("\\") || path.posix.isAbsolute(skillsDir) || path.win32.isAbsolute(skillsDir) || containsTraversal || normalizedNoTrailingSlash === "" || normalizedNoTrailingSlash === "." || normalizedSkillsDir.startsWith("../") || normalizedSkillsDir.includes("/../") || normalizedSkillsDir.endsWith("/..")) throw new Error(`Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`);
1315
+ const rootDir = path.posix.join(homePath, normalizedNoTrailingSlash);
1316
+ const relative = path.posix.relative(homePath, rootDir);
1317
+ if (relative === "" || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`Invalid skillsDir ${JSON.stringify(skillsDir)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`);
1318
+ return rootDir;
1319
+ }
1320
+ //#endregion
1321
+ //#region src/v1/harness-v1-diagnostic.ts
1322
+ /** Severity of a diagnostic, ordered most → least severe. */
1323
+ const harnessV1DebugLevelSchema = z.enum([
1324
+ "error",
1325
+ "warn",
1326
+ "info",
1327
+ "debug",
1328
+ "trace"
1755
1329
  ]);
1756
- var harnessV1DebugConfigSchema = z.object({
1757
- enabled: z.boolean().optional(),
1758
- level: harnessV1DebugLevelSchema.optional(),
1759
- subsystems: z.array(z.string()).optional()
1330
+ /**
1331
+ * Per-session diagnostics configuration the framework hands an adapter (and the
1332
+ * host sends on `start.debug`). When absent or `enabled` is false the adapter
1333
+ * captures and emits nothing. `subsystems` filters structured events by dotted
1334
+ * prefix; console capture is independent of the subsystem filter.
1335
+ */
1336
+ const harnessV1DebugConfigSchema = z.object({
1337
+ enabled: z.boolean().optional(),
1338
+ level: harnessV1DebugLevelSchema.optional(),
1339
+ subsystems: z.array(z.string()).optional()
1760
1340
  });
1761
-
1762
- // src/v1/harness-v1-stream-part.ts
1763
- import { z as z2 } from "zod/v4";
1764
- var harnessV1JsonValueSchema = z2.lazy(
1765
- () => z2.union([
1766
- z2.string(),
1767
- z2.number(),
1768
- z2.boolean(),
1769
- z2.null(),
1770
- z2.array(harnessV1JsonValueSchema),
1771
- z2.record(z2.string(), harnessV1JsonValueSchema)
1772
- ])
1773
- );
1774
- var harnessV1ToolResultValueSchema = harnessV1JsonValueSchema;
1775
- var harnessV1JsonObjectSchema = z2.record(
1776
- z2.string(),
1777
- harnessV1JsonValueSchema
1778
- );
1779
- var harnessV1MetadataSchema = z2.record(
1780
- z2.string(),
1781
- z2.record(z2.string(), harnessV1JsonValueSchema)
1782
- );
1783
- var harnessV1ProviderMetadataSchema = z2.record(
1784
- z2.string(),
1785
- z2.record(z2.string(), harnessV1JsonValueSchema)
1786
- );
1787
- var harnessV1CallWarningSchema = z2.union([
1788
- z2.object({
1789
- type: z2.literal("unsupported-setting"),
1790
- setting: z2.string(),
1791
- details: z2.string().optional()
1792
- }),
1793
- z2.object({
1794
- type: z2.literal("unsupported-tool"),
1795
- tool: z2.string(),
1796
- details: z2.string().optional()
1797
- }),
1798
- z2.object({ type: z2.literal("other"), message: z2.string() })
1341
+ //#endregion
1342
+ //#region src/v1/harness-v1-stream-part.ts
1343
+ const harnessV1JsonValueSchema = z.lazy(() => z.union([
1344
+ z.string(),
1345
+ z.number(),
1346
+ z.boolean(),
1347
+ z.null(),
1348
+ z.array(harnessV1JsonValueSchema),
1349
+ z.record(z.string(), harnessV1JsonValueSchema)
1350
+ ]));
1351
+ const harnessV1ToolResultValueSchema = harnessV1JsonValueSchema;
1352
+ const harnessV1JsonObjectSchema = z.record(z.string(), harnessV1JsonValueSchema);
1353
+ const harnessV1MetadataSchema = z.record(z.string(), z.record(z.string(), harnessV1JsonValueSchema));
1354
+ const harnessV1ProviderMetadataSchema = z.record(z.string(), z.record(z.string(), harnessV1JsonValueSchema));
1355
+ const harnessV1CallWarningSchema = z.union([
1356
+ z.object({
1357
+ type: z.literal("unsupported-setting"),
1358
+ setting: z.string(),
1359
+ details: z.string().optional()
1360
+ }),
1361
+ z.object({
1362
+ type: z.literal("unsupported-tool"),
1363
+ tool: z.string(),
1364
+ details: z.string().optional()
1365
+ }),
1366
+ z.object({
1367
+ type: z.literal("other"),
1368
+ message: z.string()
1369
+ })
1799
1370
  ]);
1800
- var harnessV1UsageSchema = z2.object({
1801
- inputTokens: z2.object({
1802
- total: z2.number().optional(),
1803
- noCache: z2.number().optional(),
1804
- cacheRead: z2.number().optional(),
1805
- cacheWrite: z2.number().optional()
1806
- }),
1807
- outputTokens: z2.object({
1808
- total: z2.number().optional(),
1809
- text: z2.number().optional(),
1810
- reasoning: z2.number().optional()
1811
- }),
1812
- raw: harnessV1JsonObjectSchema.optional()
1371
+ const harnessV1UsageSchema = z.object({
1372
+ inputTokens: z.object({
1373
+ total: z.number().optional(),
1374
+ noCache: z.number().optional(),
1375
+ cacheRead: z.number().optional(),
1376
+ cacheWrite: z.number().optional()
1377
+ }),
1378
+ outputTokens: z.object({
1379
+ total: z.number().optional(),
1380
+ text: z.number().optional(),
1381
+ reasoning: z.number().optional()
1382
+ }),
1383
+ raw: harnessV1JsonObjectSchema.optional()
1813
1384
  });
1814
- var harnessV1FinishReasonSchema = z2.object({
1815
- unified: z2.enum([
1816
- "stop",
1817
- "length",
1818
- "content-filter",
1819
- "tool-calls",
1820
- "error",
1821
- "other"
1822
- ]),
1823
- raw: z2.string().optional()
1385
+ const harnessV1FinishReasonSchema = z.object({
1386
+ unified: z.enum([
1387
+ "stop",
1388
+ "length",
1389
+ "content-filter",
1390
+ "tool-calls",
1391
+ "error",
1392
+ "other"
1393
+ ]),
1394
+ raw: z.string().optional()
1824
1395
  });
1825
- var harnessV1StreamStartPartSchema = z2.object({
1826
- type: z2.literal("stream-start"),
1827
- warnings: z2.array(harnessV1CallWarningSchema).readonly().optional(),
1828
- modelId: z2.string().optional()
1396
+ const harnessV1StreamStartPartSchema = z.object({
1397
+ type: z.literal("stream-start"),
1398
+ warnings: z.array(harnessV1CallWarningSchema).readonly().optional(),
1399
+ modelId: z.string().optional()
1829
1400
  });
1830
- var harnessV1TextStartPartSchema = z2.object({
1831
- type: z2.literal("text-start"),
1832
- id: z2.string(),
1833
- harnessMetadata: harnessV1MetadataSchema.optional()
1401
+ const harnessV1TextStartPartSchema = z.object({
1402
+ type: z.literal("text-start"),
1403
+ id: z.string(),
1404
+ harnessMetadata: harnessV1MetadataSchema.optional()
1834
1405
  });
1835
- var harnessV1TextDeltaPartSchema = z2.object({
1836
- type: z2.literal("text-delta"),
1837
- id: z2.string(),
1838
- delta: z2.string(),
1839
- harnessMetadata: harnessV1MetadataSchema.optional()
1406
+ const harnessV1TextDeltaPartSchema = z.object({
1407
+ type: z.literal("text-delta"),
1408
+ id: z.string(),
1409
+ delta: z.string(),
1410
+ harnessMetadata: harnessV1MetadataSchema.optional()
1840
1411
  });
1841
- var harnessV1TextEndPartSchema = z2.object({
1842
- type: z2.literal("text-end"),
1843
- id: z2.string(),
1844
- harnessMetadata: harnessV1MetadataSchema.optional()
1412
+ const harnessV1TextEndPartSchema = z.object({
1413
+ type: z.literal("text-end"),
1414
+ id: z.string(),
1415
+ harnessMetadata: harnessV1MetadataSchema.optional()
1845
1416
  });
1846
- var harnessV1ReasoningStartPartSchema = z2.object({
1847
- type: z2.literal("reasoning-start"),
1848
- id: z2.string(),
1849
- harnessMetadata: harnessV1MetadataSchema.optional()
1417
+ const harnessV1ReasoningStartPartSchema = z.object({
1418
+ type: z.literal("reasoning-start"),
1419
+ id: z.string(),
1420
+ harnessMetadata: harnessV1MetadataSchema.optional()
1850
1421
  });
1851
- var harnessV1ReasoningDeltaPartSchema = z2.object({
1852
- type: z2.literal("reasoning-delta"),
1853
- id: z2.string(),
1854
- delta: z2.string(),
1855
- harnessMetadata: harnessV1MetadataSchema.optional()
1422
+ const harnessV1ReasoningDeltaPartSchema = z.object({
1423
+ type: z.literal("reasoning-delta"),
1424
+ id: z.string(),
1425
+ delta: z.string(),
1426
+ harnessMetadata: harnessV1MetadataSchema.optional()
1856
1427
  });
1857
- var harnessV1ReasoningEndPartSchema = z2.object({
1858
- type: z2.literal("reasoning-end"),
1859
- id: z2.string(),
1860
- harnessMetadata: harnessV1MetadataSchema.optional()
1428
+ const harnessV1ReasoningEndPartSchema = z.object({
1429
+ type: z.literal("reasoning-end"),
1430
+ id: z.string(),
1431
+ harnessMetadata: harnessV1MetadataSchema.optional()
1861
1432
  });
1862
- var harnessV1ToolInputStartPartSchema = z2.object({
1863
- type: z2.literal("tool-input-start"),
1864
- id: z2.string(),
1865
- toolName: z2.string(),
1866
- providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1867
- providerExecuted: z2.boolean().optional(),
1868
- dynamic: z2.boolean().optional(),
1869
- title: z2.string().optional()
1433
+ const harnessV1ToolInputStartPartSchema = z.object({
1434
+ type: z.literal("tool-input-start"),
1435
+ id: z.string(),
1436
+ toolName: z.string(),
1437
+ providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1438
+ providerExecuted: z.boolean().optional(),
1439
+ dynamic: z.boolean().optional(),
1440
+ title: z.string().optional()
1870
1441
  });
1871
- var harnessV1ToolInputDeltaPartSchema = z2.object({
1872
- type: z2.literal("tool-input-delta"),
1873
- id: z2.string(),
1874
- delta: z2.string(),
1875
- providerMetadata: harnessV1ProviderMetadataSchema.optional()
1442
+ const harnessV1ToolInputDeltaPartSchema = z.object({
1443
+ type: z.literal("tool-input-delta"),
1444
+ id: z.string(),
1445
+ delta: z.string(),
1446
+ providerMetadata: harnessV1ProviderMetadataSchema.optional()
1876
1447
  });
1877
- var harnessV1ToolInputEndPartSchema = z2.object({
1878
- type: z2.literal("tool-input-end"),
1879
- id: z2.string(),
1880
- providerMetadata: harnessV1ProviderMetadataSchema.optional()
1448
+ const harnessV1ToolInputEndPartSchema = z.object({
1449
+ type: z.literal("tool-input-end"),
1450
+ id: z.string(),
1451
+ providerMetadata: harnessV1ProviderMetadataSchema.optional()
1881
1452
  });
1882
- var harnessV1ToolCallPartSchema = z2.object({
1883
- type: z2.literal("tool-call"),
1884
- toolCallId: z2.string(),
1885
- toolName: z2.string(),
1886
- input: z2.string(),
1887
- providerExecuted: z2.boolean().optional(),
1888
- dynamic: z2.boolean().optional(),
1889
- providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1890
- nativeName: z2.string().optional(),
1891
- stepToolCallCount: z2.number().int().positive().optional()
1453
+ const harnessV1ToolCallPartSchema = z.object({
1454
+ type: z.literal("tool-call"),
1455
+ toolCallId: z.string(),
1456
+ toolName: z.string(),
1457
+ input: z.string(),
1458
+ providerExecuted: z.boolean().optional(),
1459
+ dynamic: z.boolean().optional(),
1460
+ providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1461
+ nativeName: z.string().optional(),
1462
+ stepToolCallCount: z.number().int().positive().optional()
1892
1463
  });
1893
- var harnessV1ToolApprovalRequestPartSchema = z2.object({
1894
- type: z2.literal("tool-approval-request"),
1895
- approvalId: z2.string(),
1896
- toolCallId: z2.string(),
1897
- providerMetadata: harnessV1ProviderMetadataSchema.optional()
1464
+ const harnessV1ToolApprovalRequestPartSchema = z.object({
1465
+ type: z.literal("tool-approval-request"),
1466
+ approvalId: z.string(),
1467
+ toolCallId: z.string(),
1468
+ providerMetadata: harnessV1ProviderMetadataSchema.optional()
1898
1469
  });
1899
- var harnessV1ToolResultPartSchema = z2.object({
1900
- type: z2.literal("tool-result"),
1901
- toolCallId: z2.string(),
1902
- toolName: z2.string(),
1903
- result: harnessV1ToolResultValueSchema,
1904
- isError: z2.boolean().optional(),
1905
- preliminary: z2.boolean().optional(),
1906
- dynamic: z2.boolean().optional(),
1907
- providerMetadata: harnessV1ProviderMetadataSchema.optional()
1470
+ const harnessV1ToolResultPartSchema = z.object({
1471
+ type: z.literal("tool-result"),
1472
+ toolCallId: z.string(),
1473
+ toolName: z.string(),
1474
+ result: harnessV1ToolResultValueSchema,
1475
+ isError: z.boolean().optional(),
1476
+ preliminary: z.boolean().optional(),
1477
+ dynamic: z.boolean().optional(),
1478
+ providerMetadata: harnessV1ProviderMetadataSchema.optional()
1908
1479
  });
1909
- var harnessV1FinishStepPartSchema = z2.object({
1910
- type: z2.literal("finish-step"),
1911
- finishReason: harnessV1FinishReasonSchema,
1912
- usage: harnessV1UsageSchema,
1913
- harnessMetadata: harnessV1MetadataSchema.optional()
1480
+ const harnessV1FinishStepPartSchema = z.object({
1481
+ type: z.literal("finish-step"),
1482
+ finishReason: harnessV1FinishReasonSchema,
1483
+ usage: harnessV1UsageSchema,
1484
+ harnessMetadata: harnessV1MetadataSchema.optional()
1914
1485
  });
1915
- var harnessV1FinishPartSchema = z2.object({
1916
- type: z2.literal("finish"),
1917
- finishReason: harnessV1FinishReasonSchema,
1918
- totalUsage: harnessV1UsageSchema,
1919
- harnessMetadata: harnessV1MetadataSchema.optional()
1486
+ const harnessV1FinishPartSchema = z.object({
1487
+ type: z.literal("finish"),
1488
+ finishReason: harnessV1FinishReasonSchema,
1489
+ totalUsage: harnessV1UsageSchema,
1490
+ harnessMetadata: harnessV1MetadataSchema.optional()
1920
1491
  });
1921
- var harnessV1FileChangePartSchema = z2.object({
1922
- type: z2.literal("file-change"),
1923
- event: z2.enum(["create", "modify", "delete"]),
1924
- path: z2.string(),
1925
- harnessMetadata: harnessV1MetadataSchema.optional()
1492
+ const harnessV1FileChangePartSchema = z.object({
1493
+ type: z.literal("file-change"),
1494
+ event: z.enum([
1495
+ "create",
1496
+ "modify",
1497
+ "delete"
1498
+ ]),
1499
+ path: z.string(),
1500
+ harnessMetadata: harnessV1MetadataSchema.optional()
1926
1501
  });
1927
- var harnessV1CompactionPartSchema = z2.object({
1928
- type: z2.literal("compaction"),
1929
- trigger: z2.enum(["manual", "auto"]),
1930
- summary: z2.string(),
1931
- tokensBefore: z2.number().optional(),
1932
- tokensAfter: z2.number().optional(),
1933
- harnessMetadata: harnessV1MetadataSchema.optional()
1502
+ const harnessV1CompactionPartSchema = z.object({
1503
+ type: z.literal("compaction"),
1504
+ trigger: z.enum(["manual", "auto"]),
1505
+ summary: z.string(),
1506
+ tokensBefore: z.number().optional(),
1507
+ tokensAfter: z.number().optional(),
1508
+ harnessMetadata: harnessV1MetadataSchema.optional()
1934
1509
  });
1935
- var harnessV1ErrorPartSchema = z2.object({
1936
- type: z2.literal("error"),
1937
- error: z2.unknown()
1510
+ const harnessV1ErrorPartSchema = z.object({
1511
+ type: z.literal("error"),
1512
+ error: z.unknown()
1938
1513
  });
1939
- var harnessV1RawPartSchema = z2.object({
1940
- type: z2.literal("raw"),
1941
- rawValue: z2.unknown()
1514
+ const harnessV1RawPartSchema = z.object({
1515
+ type: z.literal("raw"),
1516
+ rawValue: z.unknown()
1942
1517
  });
1943
- var harnessV1StreamPartSchema = z2.discriminatedUnion("type", [
1944
- harnessV1StreamStartPartSchema,
1945
- harnessV1TextStartPartSchema,
1946
- harnessV1TextDeltaPartSchema,
1947
- harnessV1TextEndPartSchema,
1948
- harnessV1ReasoningStartPartSchema,
1949
- harnessV1ReasoningDeltaPartSchema,
1950
- harnessV1ReasoningEndPartSchema,
1951
- harnessV1ToolInputStartPartSchema,
1952
- harnessV1ToolInputDeltaPartSchema,
1953
- harnessV1ToolInputEndPartSchema,
1954
- harnessV1ToolCallPartSchema,
1955
- harnessV1ToolApprovalRequestPartSchema,
1956
- harnessV1ToolResultPartSchema,
1957
- harnessV1FinishStepPartSchema,
1958
- harnessV1FinishPartSchema,
1959
- harnessV1FileChangePartSchema,
1960
- harnessV1CompactionPartSchema,
1961
- harnessV1ErrorPartSchema,
1962
- harnessV1RawPartSchema
1518
+ z.discriminatedUnion("type", [
1519
+ harnessV1StreamStartPartSchema,
1520
+ harnessV1TextStartPartSchema,
1521
+ harnessV1TextDeltaPartSchema,
1522
+ harnessV1TextEndPartSchema,
1523
+ harnessV1ReasoningStartPartSchema,
1524
+ harnessV1ReasoningDeltaPartSchema,
1525
+ harnessV1ReasoningEndPartSchema,
1526
+ harnessV1ToolInputStartPartSchema,
1527
+ harnessV1ToolInputDeltaPartSchema,
1528
+ harnessV1ToolInputEndPartSchema,
1529
+ harnessV1ToolCallPartSchema,
1530
+ harnessV1ToolApprovalRequestPartSchema,
1531
+ harnessV1ToolResultPartSchema,
1532
+ harnessV1FinishStepPartSchema,
1533
+ harnessV1FinishPartSchema,
1534
+ harnessV1FileChangePartSchema,
1535
+ harnessV1CompactionPartSchema,
1536
+ harnessV1ErrorPartSchema,
1537
+ harnessV1RawPartSchema
1963
1538
  ]);
1964
-
1965
- // src/v1/harness-v1-bridge-protocol.ts
1966
- var harnessV1BridgeToolWireSchema = z3.object({
1967
- name: z3.string(),
1968
- description: z3.string().optional(),
1969
- inputSchema: z3.unknown().optional()
1539
+ //#endregion
1540
+ //#region src/v1/harness-v1-bridge-protocol.ts
1541
+ /**
1542
+ * The subset of a host-defined tool that travels on the `start` message. The
1543
+ * runtime only needs the name, description, and JSON-Schema input to surface
1544
+ * the tool; `execute` stays on the host.
1545
+ */
1546
+ const harnessV1BridgeToolWireSchema = z.object({
1547
+ name: z.string(),
1548
+ description: z.string().optional(),
1549
+ inputSchema: z.unknown().optional()
1970
1550
  });
1971
- var harnessV1BridgePermissionModeSchema = z3.enum([
1972
- "allow-reads",
1973
- "allow-edits",
1974
- "allow-all"
1975
- ]);
1976
- var harnessV1BridgeBuiltinToolFilteringSchema = z3.discriminatedUnion(
1977
- "mode",
1978
- [
1979
- z3.object({
1980
- mode: z3.literal("allow"),
1981
- toolNames: z3.array(z3.string())
1982
- }),
1983
- z3.object({
1984
- mode: z3.literal("deny"),
1985
- toolNames: z3.array(z3.string())
1986
- })
1987
- ]
1988
- );
1989
- var harnessV1BridgeResponseFormatSchema = z3.discriminatedUnion("type", [
1990
- z3.object({ type: z3.literal("text") }),
1991
- z3.object({
1992
- type: z3.literal("json"),
1993
- schema: z3.record(z3.string(), z3.json()).optional(),
1994
- name: z3.string().optional(),
1995
- description: z3.string().optional()
1996
- })
1551
+ const harnessV1BridgePermissionModeSchema = z.enum([
1552
+ "allow-reads",
1553
+ "allow-edits",
1554
+ "allow-all"
1997
1555
  ]);
1998
- var harnessV1BridgeStartBaseSchema = z3.object({
1999
- type: z3.literal("start"),
2000
- prompt: z3.string(),
2001
- tools: z3.array(harnessV1BridgeToolWireSchema).optional(),
2002
- model: z3.string().optional(),
2003
- debug: harnessV1DebugConfigSchema.optional(),
2004
- permissionMode: harnessV1BridgePermissionModeSchema.optional(),
2005
- builtinToolFiltering: harnessV1BridgeBuiltinToolFilteringSchema.optional(),
2006
- responseFormat: harnessV1BridgeResponseFormatSchema.optional()
2007
- });
2008
- var harnessV1BridgeHelloSchema = z3.object({
2009
- type: z3.literal("bridge-hello"),
2010
- state: z3.string().optional(),
2011
- lastSeq: z3.number().optional(),
2012
- capabilities: z3.object({
2013
- experimental_userMessageResponses: z3.boolean().optional()
2014
- }).optional()
2015
- });
2016
- var experimental_harnessV1BridgeUserMessageResponseSchema = z3.object({
2017
- type: z3.literal("user-message-response"),
2018
- messageId: z3.string(),
2019
- accepted: z3.boolean(),
2020
- error: z3.object({ message: z3.string() }).optional()
2021
- });
2022
- var harnessV1BridgeStopSchema = z3.object({
2023
- type: z3.literal("bridge-stop"),
2024
- data: z3.unknown()
2025
- });
2026
- var harnessV1BridgeThreadSchema = z3.object({
2027
- type: z3.literal("bridge-thread"),
2028
- threadId: z3.string()
1556
+ const harnessV1BridgeBuiltinToolFilteringSchema = z.discriminatedUnion("mode", [z.object({
1557
+ mode: z.literal("allow"),
1558
+ toolNames: z.array(z.string())
1559
+ }), z.object({
1560
+ mode: z.literal("deny"),
1561
+ toolNames: z.array(z.string())
1562
+ })]);
1563
+ const harnessV1BridgeResponseFormatSchema = z.discriminatedUnion("type", [z.object({ type: z.literal("text") }), z.object({
1564
+ type: z.literal("json"),
1565
+ schema: z.record(z.string(), z.json()).optional(),
1566
+ name: z.string().optional(),
1567
+ description: z.string().optional()
1568
+ })]);
1569
+ z.object({
1570
+ type: z.literal("start"),
1571
+ prompt: z.string(),
1572
+ tools: z.array(harnessV1BridgeToolWireSchema).optional(),
1573
+ model: z.string().optional(),
1574
+ debug: harnessV1DebugConfigSchema.optional(),
1575
+ permissionMode: harnessV1BridgePermissionModeSchema.optional(),
1576
+ builtinToolFiltering: harnessV1BridgeBuiltinToolFilteringSchema.optional(),
1577
+ responseFormat: harnessV1BridgeResponseFormatSchema.optional()
2029
1578
  });
2030
- var harnessV1BridgeSandboxLogSchema = z3.object({
2031
- type: z3.literal("sandbox-log"),
2032
- source: z3.string(),
2033
- stream: z3.enum(["stdout", "stderr"]),
2034
- line: z3.string()
1579
+ /**
1580
+ * Sent the instant the bridge accepts an authenticated WS connection. The host
1581
+ * waits for it before sending `start`/`resume`, because some sandbox runtimes
1582
+ * complete the upstream WS handshake before the connection is wired through to
1583
+ * the bridge process — anything sent in that gap is dropped. Carries the
1584
+ * bridge's lifecycle `state` and highest emitted `seq` for reconnect.
1585
+ */
1586
+ const harnessV1BridgeHelloSchema = z.object({
1587
+ type: z.literal("bridge-hello"),
1588
+ state: z.string().optional(),
1589
+ lastSeq: z.number().optional(),
1590
+ capabilities: z.object({ experimental_userMessageResponses: z.boolean().optional() }).optional()
2035
1591
  });
2036
- var harnessV1BridgeDebugEventSchema = z3.object({
2037
- type: z3.literal("debug-event"),
2038
- level: harnessV1DebugLevelSchema,
2039
- subsystem: z3.string(),
2040
- message: z3.string(),
2041
- attrs: z3.record(z3.string(), z3.unknown()).optional(),
2042
- error: z3.object({
2043
- name: z3.string().optional(),
2044
- message: z3.string(),
2045
- stack: z3.string().optional()
2046
- }).optional()
1592
+ const experimental_harnessV1BridgeUserMessageResponseSchema = z.object({
1593
+ type: z.literal("user-message-response"),
1594
+ messageId: z.string(),
1595
+ accepted: z.boolean(),
1596
+ error: z.object({ message: z.string() }).optional()
2047
1597
  });
2048
- var harnessV1BridgeOutboundMessageSchema = z3.discriminatedUnion(
2049
- "type",
2050
- [
2051
- harnessV1StreamStartPartSchema,
2052
- harnessV1TextStartPartSchema,
2053
- harnessV1TextDeltaPartSchema,
2054
- harnessV1TextEndPartSchema,
2055
- harnessV1ReasoningStartPartSchema,
2056
- harnessV1ReasoningDeltaPartSchema,
2057
- harnessV1ReasoningEndPartSchema,
2058
- harnessV1ToolInputStartPartSchema,
2059
- harnessV1ToolInputDeltaPartSchema,
2060
- harnessV1ToolInputEndPartSchema,
2061
- harnessV1ToolCallPartSchema,
2062
- harnessV1ToolApprovalRequestPartSchema,
2063
- harnessV1ToolResultPartSchema,
2064
- harnessV1FinishStepPartSchema,
2065
- harnessV1FinishPartSchema,
2066
- harnessV1FileChangePartSchema,
2067
- harnessV1CompactionPartSchema,
2068
- harnessV1ErrorPartSchema,
2069
- harnessV1RawPartSchema,
2070
- harnessV1BridgeHelloSchema,
2071
- experimental_harnessV1BridgeUserMessageResponseSchema,
2072
- harnessV1BridgeStopSchema,
2073
- harnessV1BridgeThreadSchema,
2074
- harnessV1BridgeSandboxLogSchema,
2075
- harnessV1BridgeDebugEventSchema
2076
- ]
2077
- );
2078
- var harnessV1BridgeToolResultInboundSchema = z3.object({
2079
- type: z3.literal("tool-result"),
2080
- toolCallId: z3.string(),
2081
- output: z3.unknown(),
2082
- isError: z3.boolean().optional(),
2083
- toolResult: z3.unknown().optional()
1598
+ /**
1599
+ * The bridge's reply to an inbound `stop`. Carries the adapter-specific
1600
+ * payload the host serializes into lifecycle state `data`.
1601
+ */
1602
+ const harnessV1BridgeStopSchema = z.object({
1603
+ type: z.literal("bridge-stop"),
1604
+ data: z.unknown()
2084
1605
  });
2085
- var harnessV1BridgeToolApprovalResponseInboundSchema = z3.object({
2086
- type: z3.literal("tool-approval-response"),
2087
- approvalId: z3.string(),
2088
- approved: z3.boolean(),
2089
- reason: z3.string().optional()
1606
+ /**
1607
+ * A resume coordinate the bridge proactively announces (e.g. Codex's thread id)
1608
+ * so the host can cache it for a later resume without waiting for `stop`.
1609
+ */
1610
+ const harnessV1BridgeThreadSchema = z.object({
1611
+ type: z.literal("bridge-thread"),
1612
+ threadId: z.string()
2090
1613
  });
2091
- var harnessV1BridgeUserMessageInboundSchema = z3.object({
2092
- type: z3.literal("user-message"),
2093
- messageId: z3.string().optional(),
2094
- text: z3.string()
1614
+ /**
1615
+ * One captured console line from inside the sandbox. The bridge line-buffers
1616
+ * `process.stdout`/`process.stderr` and emits one of these per complete line.
1617
+ * Routed host-side to the diagnostics sink, never to the consumer stream.
1618
+ */
1619
+ const harnessV1BridgeSandboxLogSchema = z.object({
1620
+ type: z.literal("sandbox-log"),
1621
+ source: z.string(),
1622
+ stream: z.enum(["stdout", "stderr"]),
1623
+ line: z.string()
2095
1624
  });
2096
- var experimental_harnessV1BridgeUserMessageInboundSchema = harnessV1BridgeUserMessageInboundSchema.extend({
2097
- messageId: z3.string()
1625
+ /**
1626
+ * A structured diagnostic an adapter emits from inside the bridge via
1627
+ * `turn.bridgeLog(...)`. Gated by the session's debug level + subsystem filter.
1628
+ */
1629
+ const harnessV1BridgeDebugEventSchema = z.object({
1630
+ type: z.literal("debug-event"),
1631
+ level: harnessV1DebugLevelSchema,
1632
+ subsystem: z.string(),
1633
+ message: z.string(),
1634
+ attrs: z.record(z.string(), z.unknown()).optional(),
1635
+ error: z.object({
1636
+ name: z.string().optional(),
1637
+ message: z.string(),
1638
+ stack: z.string().optional()
1639
+ }).optional()
2098
1640
  });
2099
- var harnessV1BridgeAbortInboundSchema = z3.object({
2100
- type: z3.literal("abort")
2101
- });
2102
- var harnessV1BridgeDestroyInboundSchema = z3.object({
2103
- type: z3.literal("destroy")
1641
+ z.discriminatedUnion("type", [
1642
+ harnessV1StreamStartPartSchema,
1643
+ harnessV1TextStartPartSchema,
1644
+ harnessV1TextDeltaPartSchema,
1645
+ harnessV1TextEndPartSchema,
1646
+ harnessV1ReasoningStartPartSchema,
1647
+ harnessV1ReasoningDeltaPartSchema,
1648
+ harnessV1ReasoningEndPartSchema,
1649
+ harnessV1ToolInputStartPartSchema,
1650
+ harnessV1ToolInputDeltaPartSchema,
1651
+ harnessV1ToolInputEndPartSchema,
1652
+ harnessV1ToolCallPartSchema,
1653
+ harnessV1ToolApprovalRequestPartSchema,
1654
+ harnessV1ToolResultPartSchema,
1655
+ harnessV1FinishStepPartSchema,
1656
+ harnessV1FinishPartSchema,
1657
+ harnessV1FileChangePartSchema,
1658
+ harnessV1CompactionPartSchema,
1659
+ harnessV1ErrorPartSchema,
1660
+ harnessV1RawPartSchema,
1661
+ harnessV1BridgeHelloSchema,
1662
+ experimental_harnessV1BridgeUserMessageResponseSchema,
1663
+ harnessV1BridgeStopSchema,
1664
+ harnessV1BridgeThreadSchema,
1665
+ harnessV1BridgeSandboxLogSchema,
1666
+ harnessV1BridgeDebugEventSchema
1667
+ ]);
1668
+ z.object({
1669
+ type: z.literal("tool-result"),
1670
+ toolCallId: z.string(),
1671
+ output: z.unknown(),
1672
+ isError: z.boolean().optional(),
1673
+ toolResult: z.unknown().optional()
2104
1674
  });
2105
- var harnessV1BridgeResumeInboundSchema = z3.object({
2106
- type: z3.literal("resume"),
2107
- lastSeenEventId: z3.number()
1675
+ z.object({
1676
+ type: z.literal("tool-approval-response"),
1677
+ approvalId: z.string(),
1678
+ approved: z.boolean(),
1679
+ reason: z.string().optional()
2108
1680
  });
2109
- var harnessV1BridgeStopInboundSchema = z3.object({
2110
- type: z3.literal("stop")
1681
+ z.object({
1682
+ type: z.literal("user-message"),
1683
+ messageId: z.string().optional(),
1684
+ text: z.string()
1685
+ }).extend({ messageId: z.string() });
1686
+ z.object({ type: z.literal("abort") });
1687
+ z.object({ type: z.literal("destroy") });
1688
+ z.object({
1689
+ type: z.literal("resume"),
1690
+ lastSeenEventId: z.number()
2111
1691
  });
2112
- var harnessV1BridgeReadySchema = z3.object({
2113
- type: z3.literal("bridge-ready"),
2114
- port: z3.number()
1692
+ z.object({ type: z.literal("stop") });
1693
+ /**
1694
+ * The JSON line the bridge writes to stdout once its WebSocket server is bound,
1695
+ * announcing the port the host should connect to.
1696
+ */
1697
+ const harnessV1BridgeReadySchema = z.object({
1698
+ type: z.literal("bridge-ready"),
1699
+ port: z.number()
2115
1700
  });
2116
-
2117
- // src/utils/bridge-ready.ts
2118
- var bridgeMetaSchema = z4.object({
2119
- type: z4.string().optional(),
2120
- port: z4.number().optional(),
2121
- state: z4.string().optional(),
2122
- pid: z4.number().optional()
1701
+ //#endregion
1702
+ //#region src/utils/bridge-ready.ts
1703
+ const bridgeMetaSchema = z.object({
1704
+ type: z.string().optional(),
1705
+ port: z.number().optional(),
1706
+ state: z.string().optional(),
1707
+ pid: z.number().optional()
2123
1708
  });
2124
- async function markBridgeStarting({
2125
- sandbox,
2126
- bridgeStateDir,
2127
- bridgeType,
2128
- abortSignal
2129
- }) {
2130
- try {
2131
- await sandbox.writeTextFile({
2132
- path: bridgeMetaPath(bridgeStateDir),
2133
- content: JSON.stringify({ type: bridgeType, state: "starting" }),
2134
- abortSignal
2135
- });
2136
- } catch {
2137
- }
2138
- }
2139
- async function waitForBridgeReady({
2140
- proc,
2141
- sandbox,
2142
- bridgeStateDir,
2143
- bridgeType,
2144
- timeoutMs,
2145
- abortSignal,
2146
- pollIntervalMs = 100,
2147
- createTimeoutError,
2148
- createExitError
2149
- }) {
2150
- const reader = proc.stdout.pipeThrough(new TextDecoderStream()).getReader();
2151
- const decoder = lineDecoder();
2152
- const stdoutTail = [];
2153
- const deadline = Date.now() + timeoutMs;
2154
- let pendingStdoutRead;
2155
- let pendingMetaRead;
2156
- let nextMetaReadAt = 0;
2157
- let cancelReader = false;
2158
- try {
2159
- while (true) {
2160
- if (abortSignal?.aborted) {
2161
- cancelReader = true;
2162
- await proc.kill();
2163
- throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
2164
- }
2165
- const remaining = deadline - Date.now();
2166
- if (remaining <= 0) {
2167
- cancelReader = true;
2168
- await proc.kill();
2169
- throw await makeBridgeReadyError({
2170
- createError: createTimeoutError,
2171
- fallbackMessage: "bridge did not become ready in time.",
2172
- proc,
2173
- stdoutTail
2174
- });
2175
- }
2176
- pendingStdoutRead ??= reader.read();
2177
- const metaReadDelayMs = Math.max(0, nextMetaReadAt - Date.now());
2178
- if (pendingMetaRead === void 0 && metaReadDelayMs === 0) {
2179
- pendingMetaRead = readBridgeMetaReady({
2180
- sandbox,
2181
- bridgeStateDir,
2182
- bridgeType,
2183
- abortSignal
2184
- });
2185
- }
2186
- const result = await Promise.race([
2187
- pendingStdoutRead.then((read) => ({ source: "stdout", read })),
2188
- ...pendingMetaRead === void 0 ? [] : [
2189
- pendingMetaRead.then((port) => ({
2190
- source: "metadata",
2191
- port
2192
- }))
2193
- ],
2194
- sleep2(
2195
- Math.min(
2196
- remaining,
2197
- pendingMetaRead === void 0 ? metaReadDelayMs : pollIntervalMs
2198
- )
2199
- ).then(() => void 0)
2200
- ]);
2201
- if (result === void 0) continue;
2202
- if (result.source === "metadata") {
2203
- pendingMetaRead = void 0;
2204
- if (result.port !== void 0) {
2205
- cancelReader = true;
2206
- return {
2207
- port: result.port,
2208
- source: "metadata",
2209
- stdoutTail: [...stdoutTail]
2210
- };
2211
- }
2212
- nextMetaReadAt = Date.now() + pollIntervalMs;
2213
- continue;
2214
- }
2215
- pendingStdoutRead = void 0;
2216
- const { value, done } = result.read;
2217
- if (done) {
2218
- for (const line of decoder.flush()) {
2219
- pushTail({ lines: stdoutTail, line });
2220
- }
2221
- throw await makeBridgeReadyError({
2222
- createError: createExitError,
2223
- fallbackMessage: "bridge exited before becoming ready.",
2224
- proc,
2225
- stdoutTail
2226
- });
2227
- }
2228
- if (value === void 0) continue;
2229
- for (const line of decoder.push(value)) {
2230
- pushTail({ lines: stdoutTail, line });
2231
- const parsed = await safeParseJSON7({
2232
- text: line,
2233
- schema: harnessV1BridgeReadySchema
2234
- });
2235
- if (parsed.success) {
2236
- return {
2237
- port: parsed.value.port,
2238
- source: "stdout",
2239
- stdoutTail: [...stdoutTail]
2240
- };
2241
- }
2242
- }
2243
- }
2244
- } finally {
2245
- if (cancelReader) {
2246
- await Promise.race([reader.cancel().catch(() => {
2247
- }), sleep2(100)]);
2248
- }
2249
- try {
2250
- reader.releaseLock();
2251
- } catch {
2252
- }
2253
- }
2254
- }
2255
- async function readBridgeMetaReady({
2256
- sandbox,
2257
- bridgeStateDir,
2258
- bridgeType,
2259
- abortSignal
2260
- }) {
2261
- const raw = await Promise.resolve(
2262
- sandbox.readTextFile({
2263
- path: bridgeMetaPath(bridgeStateDir),
2264
- abortSignal
2265
- })
2266
- ).catch(() => null);
2267
- if (raw == null) return void 0;
2268
- const parsed = await safeParseJSON7({ text: raw, schema: bridgeMetaSchema });
2269
- if (!parsed.success) return void 0;
2270
- if (parsed.value.type !== bridgeType) return void 0;
2271
- if (parsed.value.state !== "waiting") return void 0;
2272
- if (parsed.value.port === void 0 || parsed.value.port <= 0) {
2273
- return void 0;
2274
- }
2275
- return parsed.value.port;
2276
- }
2277
- async function makeBridgeReadyError({
2278
- createError,
2279
- fallbackMessage,
2280
- proc,
2281
- stdoutTail
2282
- }) {
2283
- if (createError) {
2284
- return createError({ proc, stdoutTail: [...stdoutTail] });
2285
- }
2286
- return new Error(fallbackMessage);
1709
+ async function markBridgeStarting({ sandbox, bridgeStateDir, bridgeType, abortSignal }) {
1710
+ try {
1711
+ await sandbox.writeTextFile({
1712
+ path: bridgeMetaPath(bridgeStateDir),
1713
+ content: JSON.stringify({
1714
+ type: bridgeType,
1715
+ state: "starting"
1716
+ }),
1717
+ abortSignal
1718
+ });
1719
+ } catch {}
1720
+ }
1721
+ async function waitForBridgeReady({ proc, sandbox, bridgeStateDir, bridgeType, timeoutMs, abortSignal, pollIntervalMs = 100, createTimeoutError, createExitError }) {
1722
+ const reader = proc.stdout.pipeThrough(new TextDecoderStream()).getReader();
1723
+ const decoder = lineDecoder$1();
1724
+ const stdoutTail = [];
1725
+ const deadline = Date.now() + timeoutMs;
1726
+ let pendingStdoutRead;
1727
+ let pendingMetaRead;
1728
+ let nextMetaReadAt = 0;
1729
+ let cancelReader = false;
1730
+ try {
1731
+ while (true) {
1732
+ if (abortSignal?.aborted) {
1733
+ cancelReader = true;
1734
+ await proc.kill();
1735
+ throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
1736
+ }
1737
+ const remaining = deadline - Date.now();
1738
+ if (remaining <= 0) {
1739
+ cancelReader = true;
1740
+ await proc.kill();
1741
+ throw await makeBridgeReadyError({
1742
+ createError: createTimeoutError,
1743
+ fallbackMessage: "bridge did not become ready in time.",
1744
+ proc,
1745
+ stdoutTail
1746
+ });
1747
+ }
1748
+ pendingStdoutRead ??= reader.read();
1749
+ const metaReadDelayMs = Math.max(0, nextMetaReadAt - Date.now());
1750
+ if (pendingMetaRead === void 0 && metaReadDelayMs === 0) pendingMetaRead = readBridgeMetaReady({
1751
+ sandbox,
1752
+ bridgeStateDir,
1753
+ bridgeType,
1754
+ abortSignal
1755
+ });
1756
+ const result = await Promise.race([
1757
+ pendingStdoutRead.then((read) => ({
1758
+ source: "stdout",
1759
+ read
1760
+ })),
1761
+ ...pendingMetaRead === void 0 ? [] : [pendingMetaRead.then((port) => ({
1762
+ source: "metadata",
1763
+ port
1764
+ }))],
1765
+ sleep$2(Math.min(remaining, pendingMetaRead === void 0 ? metaReadDelayMs : pollIntervalMs)).then(() => void 0)
1766
+ ]);
1767
+ if (result === void 0) continue;
1768
+ if (result.source === "metadata") {
1769
+ pendingMetaRead = void 0;
1770
+ if (result.port !== void 0) {
1771
+ cancelReader = true;
1772
+ return {
1773
+ port: result.port,
1774
+ source: "metadata",
1775
+ stdoutTail: [...stdoutTail]
1776
+ };
1777
+ }
1778
+ nextMetaReadAt = Date.now() + pollIntervalMs;
1779
+ continue;
1780
+ }
1781
+ pendingStdoutRead = void 0;
1782
+ const { value, done } = result.read;
1783
+ if (done) {
1784
+ for (const line of decoder.flush()) pushTail({
1785
+ lines: stdoutTail,
1786
+ line
1787
+ });
1788
+ throw await makeBridgeReadyError({
1789
+ createError: createExitError,
1790
+ fallbackMessage: "bridge exited before becoming ready.",
1791
+ proc,
1792
+ stdoutTail
1793
+ });
1794
+ }
1795
+ if (value === void 0) continue;
1796
+ for (const line of decoder.push(value)) {
1797
+ pushTail({
1798
+ lines: stdoutTail,
1799
+ line
1800
+ });
1801
+ const parsed = await safeParseJSON({
1802
+ text: line,
1803
+ schema: harnessV1BridgeReadySchema
1804
+ });
1805
+ if (parsed.success) return {
1806
+ port: parsed.value.port,
1807
+ source: "stdout",
1808
+ stdoutTail: [...stdoutTail]
1809
+ };
1810
+ }
1811
+ }
1812
+ } finally {
1813
+ if (cancelReader) await Promise.race([reader.cancel().catch(() => {}), sleep$2(100)]);
1814
+ try {
1815
+ reader.releaseLock();
1816
+ } catch {}
1817
+ }
1818
+ }
1819
+ async function readBridgeMetaReady({ sandbox, bridgeStateDir, bridgeType, abortSignal }) {
1820
+ const raw = await Promise.resolve(sandbox.readTextFile({
1821
+ path: bridgeMetaPath(bridgeStateDir),
1822
+ abortSignal
1823
+ })).catch(() => null);
1824
+ if (raw == null) return void 0;
1825
+ const parsed = await safeParseJSON({
1826
+ text: raw,
1827
+ schema: bridgeMetaSchema
1828
+ });
1829
+ if (!parsed.success) return void 0;
1830
+ if (parsed.value.type !== bridgeType) return void 0;
1831
+ if (parsed.value.state !== "waiting") return void 0;
1832
+ if (parsed.value.port === void 0 || parsed.value.port <= 0) return;
1833
+ return parsed.value.port;
1834
+ }
1835
+ async function makeBridgeReadyError({ createError, fallbackMessage, proc, stdoutTail }) {
1836
+ if (createError) return createError({
1837
+ proc,
1838
+ stdoutTail: [...stdoutTail]
1839
+ });
1840
+ return new Error(fallbackMessage);
2287
1841
  }
2288
1842
  function bridgeMetaPath(bridgeStateDir) {
2289
- return `${bridgeStateDir}/bridge-meta.json`;
1843
+ return `${bridgeStateDir}/bridge-meta.json`;
2290
1844
  }
2291
1845
  function pushTail({ lines, line }) {
2292
- lines.push(line);
2293
- if (lines.length > 20) lines.shift();
2294
- }
2295
- function sleep2(ms) {
2296
- return new Promise((resolve) => setTimeout(resolve, ms));
2297
- }
2298
- function lineDecoder() {
2299
- let buffer = "";
2300
- return {
2301
- push(chunk) {
2302
- buffer += chunk;
2303
- const lines = [];
2304
- let nl;
2305
- while ((nl = buffer.indexOf("\n")) !== -1) {
2306
- const raw = buffer.slice(0, nl);
2307
- buffer = buffer.slice(nl + 1);
2308
- const line = raw.replace(/\r$/, "").trim();
2309
- if (line.length > 0) lines.push(line);
2310
- }
2311
- return lines;
2312
- },
2313
- flush() {
2314
- const line = buffer.replace(/\r$/, "").trim();
2315
- buffer = "";
2316
- return line.length > 0 ? [line] : [];
2317
- }
2318
- };
2319
- }
2320
-
2321
- // src/utils/bridge-token.ts
2322
- import { randomBytes as randomBytes2 } from "crypto";
1846
+ lines.push(line);
1847
+ if (lines.length > 20) lines.shift();
1848
+ }
1849
+ function sleep$2(ms) {
1850
+ return new Promise((resolve) => setTimeout(resolve, ms));
1851
+ }
1852
+ function lineDecoder$1() {
1853
+ let buffer = "";
1854
+ return {
1855
+ push(chunk) {
1856
+ buffer += chunk;
1857
+ const lines = [];
1858
+ let nl;
1859
+ while ((nl = buffer.indexOf("\n")) !== -1) {
1860
+ const raw = buffer.slice(0, nl);
1861
+ buffer = buffer.slice(nl + 1);
1862
+ const line = raw.replace(/\r$/, "").trim();
1863
+ if (line.length > 0) lines.push(line);
1864
+ }
1865
+ return lines;
1866
+ },
1867
+ flush() {
1868
+ const line = buffer.replace(/\r$/, "").trim();
1869
+ buffer = "";
1870
+ return line.length > 0 ? [line] : [];
1871
+ }
1872
+ };
1873
+ }
1874
+ //#endregion
1875
+ //#region src/utils/bridge-token.ts
2323
1876
  function createBridgeToken() {
2324
- return randomBytes2(32).toString("hex");
2325
- }
2326
- function withBridgeToken({
2327
- endpoint,
2328
- token
2329
- }) {
2330
- const bridgeUrl = new URL(endpoint.url);
2331
- bridgeUrl.searchParams.set("agent_bridge_token", token);
2332
- return { ...endpoint, url: bridgeUrl.toString() };
2333
- }
2334
-
2335
- // src/utils/bridge-asset.ts
2336
- import { readFile } from "fs/promises";
1877
+ return randomBytes(32).toString("hex");
1878
+ }
1879
+ function withBridgeToken({ endpoint, token }) {
1880
+ const bridgeUrl = new URL(endpoint.url);
1881
+ bridgeUrl.searchParams.set("agent_bridge_token", token);
1882
+ return {
1883
+ ...endpoint,
1884
+ url: bridgeUrl.toString()
1885
+ };
1886
+ }
1887
+ //#endregion
1888
+ //#region src/utils/bridge-asset.ts
2337
1889
  function createReadBridgeAsset(assetUrls) {
2338
- return async (name) => readFile(assetUrls[name], "utf8");
1890
+ return async (name) => readFile(assetUrls[name], "utf8");
2339
1891
  }
2340
-
2341
- // src/utils/bridge-diagnostics.ts
2342
- var DEFAULT_TAIL_LIMIT = 20;
1892
+ //#endregion
1893
+ //#region src/utils/bridge-diagnostics.ts
1894
+ const DEFAULT_TAIL_LIMIT = 20;
2343
1895
  function isSerializedError(error) {
2344
- return typeof error === "object" && error != null && typeof error.message === "string";
1896
+ return typeof error === "object" && error != null && typeof error.message === "string";
2345
1897
  }
2346
1898
  function stringifyUnknown(value) {
2347
- if (typeof value === "string") return value;
2348
- try {
2349
- return JSON.stringify(value);
2350
- } catch {
2351
- return String(value);
2352
- }
1899
+ if (typeof value === "string") return value;
1900
+ try {
1901
+ return JSON.stringify(value);
1902
+ } catch {
1903
+ return String(value);
1904
+ }
2353
1905
  }
2354
1906
  function formatBridgeError(error) {
2355
- if (error instanceof Error) {
2356
- return error.stack ?? `${error.name}: ${error.message}`;
2357
- }
2358
- if (isSerializedError(error)) {
2359
- return error.stack ?? `${error.name ? `${error.name}: ` : ""}${error.message}`;
2360
- }
2361
- return stringifyUnknown(error);
1907
+ if (error instanceof Error) return error.stack ?? `${error.name}: ${error.message}`;
1908
+ if (isSerializedError(error)) return error.stack ?? `${error.name ? `${error.name}: ` : ""}${error.message}`;
1909
+ return stringifyUnknown(error);
2362
1910
  }
2363
1911
  function writeToStderr(line) {
2364
- try {
2365
- process.stderr.write(line);
2366
- } catch {
2367
- }
2368
- }
2369
- function logBridgeError({
2370
- harnessId,
2371
- sessionId,
2372
- context,
2373
- error,
2374
- write = writeToStderr
2375
- }) {
2376
- const prefix = `[harness:${harnessId}:error${sessionId ? ` session=${sessionId}` : ""}]`;
2377
- const message = context ? `${context}: ${formatBridgeError(error)}` : formatBridgeError(error);
2378
- for (const line of message.split("\n")) {
2379
- if (line.trim().length > 0) {
2380
- write(`${prefix} ${line}
2381
- `);
2382
- }
2383
- }
2384
- }
2385
- function createBridgeErrorHandler({
2386
- harnessId,
2387
- sessionId
2388
- }) {
2389
- return (event) => {
2390
- logBridgeError({
2391
- harnessId,
2392
- sessionId,
2393
- context: "bridge emitted an error frame",
2394
- error: event.error
2395
- });
2396
- };
2397
- }
2398
- async function createBridgeStartupError({
2399
- message,
2400
- proc,
2401
- stdoutTail,
2402
- stderrTail,
2403
- stderrDone
2404
- }) {
2405
- if (stderrDone) {
2406
- await Promise.race([stderrDone, sleep3(250)]).catch(() => {
2407
- });
2408
- }
2409
- let exitStatus = "";
2410
- try {
2411
- const result = await Promise.race([
2412
- proc.wait(),
2413
- sleep3(250).then(() => void 0)
2414
- ]);
2415
- if (result?.exitCode !== void 0) {
2416
- exitStatus = ` Exit code: ${result.exitCode}.`;
2417
- }
2418
- } catch {
2419
- }
2420
- const details = [];
2421
- if (stdoutTail.length > 0) {
2422
- details.push(`stdout:
2423
- ${stdoutTail.join("\n")}`);
2424
- }
2425
- if (stderrTail.length > 0) {
2426
- details.push(`stderr:
2427
- ${stderrTail.join("\n")}`);
2428
- }
2429
- return new Error(
2430
- `${message}${exitStatus}${details.length > 0 ? `
2431
-
2432
- ${details.join("\n\n")}` : ""}`
2433
- );
2434
- }
2435
- async function forwardBridgeProcessStream({
2436
- stream,
2437
- streamName,
2438
- source = "bridge",
2439
- collectTail,
2440
- tailLimit = DEFAULT_TAIL_LIMIT
2441
- }) {
2442
- try {
2443
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
2444
- const decoder = lineDecoder2();
2445
- while (true) {
2446
- const { value, done } = await reader.read();
2447
- const lines = done ? decoder.flush() : value ? decoder.push(value) : [];
2448
- for (const line of lines) {
2449
- recordTail({ lines: collectTail, line, limit: tailLimit });
2450
- writeToStderr(`[harness:${source}:${streamName}] ${line}
2451
- `);
2452
- }
2453
- if (done) return;
2454
- }
2455
- } catch {
2456
- }
1912
+ try {
1913
+ process.stderr.write(line);
1914
+ } catch {}
1915
+ }
1916
+ function logBridgeError({ harnessId, sessionId, context, error, write = writeToStderr }) {
1917
+ const prefix = `[harness:${harnessId}:error${sessionId ? ` session=${sessionId}` : ""}]`;
1918
+ const message = context ? `${context}: ${formatBridgeError(error)}` : formatBridgeError(error);
1919
+ for (const line of message.split("\n")) if (line.trim().length > 0) write(`${prefix} ${line}\n`);
1920
+ }
1921
+ function createBridgeErrorHandler({ harnessId, sessionId }) {
1922
+ return (event) => {
1923
+ logBridgeError({
1924
+ harnessId,
1925
+ sessionId,
1926
+ context: "bridge emitted an error frame",
1927
+ error: event.error
1928
+ });
1929
+ };
1930
+ }
1931
+ async function createBridgeStartupError({ message, proc, stdoutTail, stderrTail, stderrDone }) {
1932
+ if (stderrDone) await Promise.race([stderrDone, sleep$1(250)]).catch(() => {});
1933
+ let exitStatus = "";
1934
+ try {
1935
+ const result = await Promise.race([proc.wait(), sleep$1(250).then(() => void 0)]);
1936
+ if (result?.exitCode !== void 0) exitStatus = ` Exit code: ${result.exitCode}.`;
1937
+ } catch {}
1938
+ const details = [];
1939
+ if (stdoutTail.length > 0) details.push(`stdout:\n${stdoutTail.join("\n")}`);
1940
+ if (stderrTail.length > 0) details.push(`stderr:\n${stderrTail.join("\n")}`);
1941
+ return /* @__PURE__ */ new Error(`${message}${exitStatus}${details.length > 0 ? `\n\n${details.join("\n\n")}` : ""}`);
1942
+ }
1943
+ async function forwardBridgeProcessStream({ stream, streamName, source = "bridge", collectTail, tailLimit = DEFAULT_TAIL_LIMIT }) {
1944
+ try {
1945
+ const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
1946
+ const decoder = lineDecoder();
1947
+ while (true) {
1948
+ const { value, done } = await reader.read();
1949
+ const lines = done ? decoder.flush() : value ? decoder.push(value) : [];
1950
+ for (const line of lines) {
1951
+ recordTail({
1952
+ lines: collectTail,
1953
+ line,
1954
+ limit: tailLimit
1955
+ });
1956
+ writeToStderr(`[harness:${source}:${streamName}] ${line}\n`);
1957
+ }
1958
+ if (done) return;
1959
+ }
1960
+ } catch {}
2457
1961
  }
2458
1962
  async function drainBridgeProcessStream(stream) {
2459
- try {
2460
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
2461
- while (true) {
2462
- const { done } = await reader.read();
2463
- if (done) return;
2464
- }
2465
- } catch {
2466
- }
1963
+ try {
1964
+ const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
1965
+ while (true) {
1966
+ const { done } = await reader.read();
1967
+ if (done) return;
1968
+ }
1969
+ } catch {}
1970
+ }
1971
+ function recordTail({ lines, line, limit }) {
1972
+ if (!lines) return;
1973
+ lines.push(line);
1974
+ while (lines.length > limit) lines.shift();
2467
1975
  }
2468
- function recordTail({
2469
- lines,
2470
- line,
2471
- limit
2472
- }) {
2473
- if (!lines) return;
2474
- lines.push(line);
2475
- while (lines.length > limit) lines.shift();
2476
- }
2477
- function lineDecoder2() {
2478
- let buffer = "";
2479
- return {
2480
- push(chunk) {
2481
- buffer += chunk;
2482
- const lines = [];
2483
- let nl;
2484
- while ((nl = buffer.indexOf("\n")) !== -1) {
2485
- const raw = buffer.slice(0, nl);
2486
- buffer = buffer.slice(nl + 1);
2487
- const line = raw.replace(/\r$/, "").trim();
2488
- if (line.length > 0) lines.push(line);
2489
- }
2490
- return lines;
2491
- },
2492
- flush() {
2493
- const line = buffer.replace(/\r$/, "").trim();
2494
- buffer = "";
2495
- return line.length > 0 ? [line] : [];
2496
- }
2497
- };
2498
- }
2499
- function sleep3(ms) {
2500
- return new Promise((resolve) => setTimeout(resolve, ms));
2501
- }
2502
-
2503
- // src/utils/resolve-sandbox-default-working-directory.ts
2504
- import { posix } from "path";
2505
- async function resolveSandboxDefaultWorkingDirectory({
2506
- sandboxSession,
2507
- abortSignal
2508
- }) {
2509
- if ("defaultWorkingDirectory" in sandboxSession) {
2510
- return sandboxSession.defaultWorkingDirectory;
2511
- }
2512
- const result = await sandboxSession.run({
2513
- command: "pwd",
2514
- abortSignal
2515
- });
2516
- if (result.exitCode !== 0) {
2517
- throw new Error(
2518
- `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
2519
- );
2520
- }
2521
- const cwd = result.stdout.trim();
2522
- if (!posix.isAbsolute(cwd)) {
2523
- throw new Error(
2524
- `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
2525
- );
2526
- }
2527
- return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
1976
+ function lineDecoder() {
1977
+ let buffer = "";
1978
+ return {
1979
+ push(chunk) {
1980
+ buffer += chunk;
1981
+ const lines = [];
1982
+ let nl;
1983
+ while ((nl = buffer.indexOf("\n")) !== -1) {
1984
+ const raw = buffer.slice(0, nl);
1985
+ buffer = buffer.slice(nl + 1);
1986
+ const line = raw.replace(/\r$/, "").trim();
1987
+ if (line.length > 0) lines.push(line);
1988
+ }
1989
+ return lines;
1990
+ },
1991
+ flush() {
1992
+ const line = buffer.replace(/\r$/, "").trim();
1993
+ buffer = "";
1994
+ return line.length > 0 ? [line] : [];
1995
+ }
1996
+ };
1997
+ }
1998
+ function sleep$1(ms) {
1999
+ return new Promise((resolve) => setTimeout(resolve, ms));
2000
+ }
2001
+ //#endregion
2002
+ //#region src/utils/resolve-sandbox-default-working-directory.ts
2003
+ async function resolveSandboxDefaultWorkingDirectory({ sandboxSession, abortSignal }) {
2004
+ if ("defaultWorkingDirectory" in sandboxSession) return sandboxSession.defaultWorkingDirectory;
2005
+ const result = await sandboxSession.run({
2006
+ command: "pwd",
2007
+ abortSignal
2008
+ });
2009
+ if (result.exitCode !== 0) throw new Error(`Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
2010
+ const cwd = result.stdout.trim();
2011
+ if (!posix.isAbsolute(cwd)) throw new Error(`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`);
2012
+ return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
2013
+ }
2014
+ //#endregion
2015
+ //#region src/utils/get-restricted-sandbox-session.ts
2016
+ function getRestrictedSandboxSession(sandboxSession) {
2017
+ return "restricted" in sandboxSession ? sandboxSession.restricted() : sandboxSession;
2528
2018
  }
2019
+ //#endregion
2020
+ export { SandboxChannel, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, formatBridgeError, forwardBridgeProcessStream, generateSandboxCredentialPlaceholder, getAiGatewayAuthFromEnv, getJwtExpiresAt, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isLinux, isMacOS, isSandboxCredentialPlaceholder, isWindows, logBridgeError, markBridgeStarting, maskSandboxCredentials, parseJwtPayload, readLinuxSecretServicePassword, readMacOSKeychainPassword, readWindowsCredentialManagerPassword, refreshOAuthAccessToken, resolveSandboxCredentialEnvironment, resolveSandboxDefaultWorkingDirectory, resolveSandboxHomeDir, shellQuote, shouldResolveNativeSubscription, sleep, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeInstructions, writeSkills };
2529
2021
 
2530
- // src/utils/get-restricted-sandbox-session.ts
2531
- function getRestrictedSandboxSession(sandboxSession) {
2532
- return "restricted" in sandboxSession ? sandboxSession.restricted() : sandboxSession;
2533
- }
2534
- export {
2535
- SandboxChannel,
2536
- applyCredentialForwarding,
2537
- classifyDiskLog,
2538
- createBridgeErrorHandler,
2539
- createBridgeStartupError,
2540
- createBridgeToken,
2541
- createCredentialRequestTransformation,
2542
- createReadBridgeAsset,
2543
- drainBridgeProcessStream,
2544
- experimental_createBridgeUserMessageSubmitter,
2545
- formatBridgeError,
2546
- forwardBridgeProcessStream,
2547
- generateSandboxCredentialPlaceholder,
2548
- getAiGatewayAuthFromEnv,
2549
- getJwtExpiresAt,
2550
- getRestrictedSandboxSession,
2551
- isAccessTokenExpiringSoon,
2552
- isHarnessAuthenticationEnvironment,
2553
- isLinux,
2554
- isMacOS,
2555
- isSandboxCredentialPlaceholder,
2556
- isWindows,
2557
- logBridgeError,
2558
- markBridgeStarting,
2559
- maskSandboxCredentials,
2560
- parseJwtPayload,
2561
- readLinuxSecretServicePassword,
2562
- readMacOSKeychainPassword,
2563
- readWindowsCredentialManagerPassword,
2564
- refreshOAuthAccessToken,
2565
- resolveSandboxCredentialEnvironment,
2566
- resolveSandboxDefaultWorkingDirectory,
2567
- resolveSandboxHomeDir,
2568
- shellQuote,
2569
- shouldResolveNativeSubscription,
2570
- sleep,
2571
- waitForBridgeReady,
2572
- warnCredentialBrokeringUnavailable,
2573
- withBridgeToken,
2574
- writeInstructions,
2575
- writeSkills
2576
- };
2577
2022
  //# sourceMappingURL=index.js.map