@ai-sdk/harness 1.0.132 → 1.0.134

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,1750 +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 createSandboxCredentialEnvironment({
1019
- environment,
1020
- credentialEnvironmentVariables,
1021
- credentialForwarding
1022
- }) {
1023
- const sandboxCredentialEnvironment = {};
1024
- for (const environmentVariableName of new Set(
1025
- credentialEnvironmentVariables
1026
- )) {
1027
- if (environment[environmentVariableName] == null) continue;
1028
- const placeholder = generateSandboxCredentialPlaceholder();
1029
- sandboxCredentialEnvironment[environmentVariableName] = credentialForwarding == null ? placeholder : await credentialForwarding({
1030
- credential: placeholder,
1031
- environmentVariableName
1032
- });
1033
- }
1034
- return sandboxCredentialEnvironment;
1035
- }
1036
-
1037
- // src/utils/sandbox-home-dir.ts
1038
- import path from "path";
1039
- async function resolveSandboxHomeDir({
1040
- sandbox,
1041
- abortSignal
1042
- }) {
1043
- const result = await sandbox.run({
1044
- command: 'printf "%s" "$HOME"',
1045
- abortSignal
1046
- });
1047
- const homeDir = result.stdout.trim();
1048
- if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
1049
- throw new Error(
1050
- `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
1051
- );
1052
- }
1053
- return homeDir;
1054
- }
1055
-
1056
- // 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
1057
849
  function shellQuote(value) {
1058
- return `'${value.replace(/'/g, `'\\''`)}'`;
1059
- }
1060
-
1061
- // src/utils/write-instructions.ts
1062
- import path2 from "path";
1063
- import {
1064
- safeParseJSON as safeParseJSON5
1065
- } from "@ai-sdk/provider-utils";
1066
- var INSTRUCTIONS_METADATA_VERSION = 1;
1067
- async function writeInstructions({
1068
- sandbox,
1069
- homePath,
1070
- instructionsFile,
1071
- instructions,
1072
- abortSignal
1073
- }) {
1074
- const { targetPath, metadataPath } = resolveInstructionsFilePath({
1075
- homePath,
1076
- instructionsFile
1077
- });
1078
- const hasInstructions = typeof instructions === "string" && instructions.trim().length > 0;
1079
- const trimmedInstructions = hasInstructions ? instructions.trim() : "";
1080
- const currentDiskContent = await sandbox.readTextFile({
1081
- path: targetPath,
1082
- abortSignal
1083
- });
1084
- const existingMetadata = await readInstructionsMetadata({
1085
- sandbox,
1086
- metadataPath,
1087
- abortSignal
1088
- });
1089
- if (hasInstructions) {
1090
- const originalContent = deriveOriginalContent({
1091
- currentDiskContent,
1092
- existingMetadata
1093
- });
1094
- const targetContent = originalContent != null && originalContent.trim().length > 0 ? `${originalContent.replace(/\n+$/, "")}
1095
-
1096
- ${trimmedInstructions}
1097
- ` : `${trimmedInstructions}
1098
- `;
1099
- if (currentDiskContent === targetContent && existingMetadata != null && existingMetadata.instructions === trimmedInstructions && existingMetadata.originalContent === originalContent) {
1100
- return { changed: false, filePath: targetPath };
1101
- }
1102
- await sandbox.writeTextFile({
1103
- path: targetPath,
1104
- content: targetContent,
1105
- abortSignal
1106
- });
1107
- await writeInstructionsMetadata({
1108
- sandbox,
1109
- metadataPath,
1110
- metadata: {
1111
- version: INSTRUCTIONS_METADATA_VERSION,
1112
- originalContent,
1113
- instructions: trimmedInstructions,
1114
- appliedContent: targetContent
1115
- },
1116
- abortSignal
1117
- });
1118
- return { changed: true, filePath: targetPath };
1119
- }
1120
- if (existingMetadata == null) {
1121
- return { changed: false, filePath: targetPath };
1122
- }
1123
- const restoredContent = deriveRestoredContent({
1124
- currentDiskContent,
1125
- existingMetadata
1126
- });
1127
- if (restoredContent != null) {
1128
- const contentToWrite = `${restoredContent.replace(/\n+$/, "")}
1129
- `;
1130
- await sandbox.writeTextFile({
1131
- path: targetPath,
1132
- content: contentToWrite,
1133
- abortSignal
1134
- });
1135
- } else {
1136
- await removeTargetFile({
1137
- sandbox,
1138
- targetPath,
1139
- abortSignal
1140
- });
1141
- }
1142
- await removeMetadataFile({
1143
- sandbox,
1144
- metadataPath,
1145
- abortSignal
1146
- });
1147
- return { changed: true, filePath: targetPath };
1148
- }
1149
- function deriveOriginalContent({
1150
- currentDiskContent,
1151
- existingMetadata
1152
- }) {
1153
- if (existingMetadata == null) {
1154
- return currentDiskContent;
1155
- }
1156
- if (currentDiskContent == null) {
1157
- return existingMetadata.originalContent;
1158
- }
1159
- if (currentDiskContent === existingMetadata.appliedContent) {
1160
- return existingMetadata.originalContent;
1161
- }
1162
- const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
1163
- const expectedSuffix = `
1164
-
1165
- ${existingMetadata.instructions}`;
1166
- if (trimmedDisk.endsWith(expectedSuffix)) {
1167
- const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
1168
- return userBase.length > 0 ? userBase : null;
1169
- }
1170
- if (trimmedDisk === existingMetadata.instructions) {
1171
- return null;
1172
- }
1173
- return existingMetadata.originalContent ?? currentDiskContent;
1174
- }
1175
- function deriveRestoredContent({
1176
- currentDiskContent,
1177
- existingMetadata
1178
- }) {
1179
- if (currentDiskContent == null) {
1180
- return null;
1181
- }
1182
- if (currentDiskContent === existingMetadata.appliedContent) {
1183
- return existingMetadata.originalContent != null && existingMetadata.originalContent.trim().length > 0 ? existingMetadata.originalContent : null;
1184
- }
1185
- const trimmedDisk = currentDiskContent.replace(/\n+$/, "");
1186
- const expectedSuffix = `
1187
-
1188
- ${existingMetadata.instructions}`;
1189
- if (trimmedDisk.endsWith(expectedSuffix)) {
1190
- const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
1191
- return userBase.length > 0 ? userBase : null;
1192
- }
1193
- if (trimmedDisk === existingMetadata.instructions) {
1194
- return null;
1195
- }
1196
- return currentDiskContent;
1197
- }
1198
- function resolveInstructionsFilePath({
1199
- homePath,
1200
- instructionsFile
1201
- }) {
1202
- if (typeof homePath !== "string" || homePath.trim().length === 0) {
1203
- throw new Error("Invalid homePath: expected a non-empty string.");
1204
- }
1205
- if (!path2.posix.isAbsolute(homePath)) {
1206
- throw new Error(
1207
- `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`
1208
- );
1209
- }
1210
- if (typeof instructionsFile !== "string" || instructionsFile.trim().length === 0) {
1211
- throw new Error(
1212
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`
1213
- );
1214
- }
1215
- const containsTraversal = instructionsFile.split(/[\\/]/).some((segment) => segment === "..");
1216
- const normalizedInstructionsFile = path2.posix.normalize(
1217
- instructionsFile.trim()
1218
- );
1219
- const normalizedNoTrailingSlash = normalizedInstructionsFile.replace(
1220
- /\/+$/,
1221
- ""
1222
- );
1223
- 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("/..")) {
1224
- throw new Error(
1225
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`
1226
- );
1227
- }
1228
- const targetPath = path2.posix.join(homePath, normalizedNoTrailingSlash);
1229
- const relative = path2.posix.relative(homePath, targetPath);
1230
- if (relative === "" || relative.startsWith("..") || path2.posix.isAbsolute(relative)) {
1231
- throw new Error(
1232
- `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`
1233
- );
1234
- }
1235
- const dir = path2.posix.dirname(targetPath);
1236
- const base = path2.posix.basename(targetPath);
1237
- const metadataPath = path2.posix.join(
1238
- dir,
1239
- `.${base}.ai-sdk-harness-instructions.json`
1240
- );
1241
- return { targetPath, metadataPath };
1242
- }
1243
- async function readInstructionsMetadata({
1244
- sandbox,
1245
- metadataPath,
1246
- abortSignal
1247
- }) {
1248
- const content = await sandbox.readTextFile({
1249
- path: metadataPath,
1250
- abortSignal
1251
- });
1252
- if (content == null) return void 0;
1253
- const parsed = await safeParseJSON5({ text: content });
1254
- if (!parsed.success || !isInstructionsMetadata(parsed.value)) {
1255
- throw new Error(
1256
- `Invalid AI SDK harness instructions metadata: ${metadataPath}`
1257
- );
1258
- }
1259
- 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;
1260
984
  }
1261
985
  function isInstructionsMetadata(value) {
1262
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
1263
- return false;
1264
- }
1265
- const candidate = value;
1266
- return candidate.version === INSTRUCTIONS_METADATA_VERSION && (candidate.originalContent === null || typeof candidate.originalContent === "string") && typeof candidate.instructions === "string" && typeof candidate.appliedContent === "string";
1267
- }
1268
- async function writeInstructionsMetadata({
1269
- sandbox,
1270
- metadataPath,
1271
- metadata,
1272
- abortSignal
1273
- }) {
1274
- const temporaryPath = `${metadataPath}.tmp`;
1275
- await sandbox.writeTextFile({
1276
- path: temporaryPath,
1277
- content: `${JSON.stringify(metadata, null, 2)}
1278
- `,
1279
- abortSignal
1280
- });
1281
- await runSandboxCommand({
1282
- sandbox,
1283
- command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(metadataPath)}`,
1284
- abortSignal,
1285
- errorMessage: `Failed to update instructions metadata: ${metadataPath}`
1286
- });
1287
- }
1288
- async function removeMetadataFile({
1289
- sandbox,
1290
- metadataPath,
1291
- abortSignal
1292
- }) {
1293
- await runSandboxCommand({
1294
- sandbox,
1295
- command: `rm -f -- ${shellQuote(metadataPath)}`,
1296
- abortSignal,
1297
- errorMessage: `Failed to remove instructions metadata: ${metadataPath}`
1298
- });
1299
- }
1300
- async function removeTargetFile({
1301
- sandbox,
1302
- targetPath,
1303
- abortSignal
1304
- }) {
1305
- await runSandboxCommand({
1306
- sandbox,
1307
- command: `rm -f -- ${shellQuote(targetPath)}`,
1308
- abortSignal,
1309
- errorMessage: `Failed to remove instructions file: ${targetPath}`
1310
- });
1311
- }
1312
- async function runSandboxCommand({
1313
- sandbox,
1314
- command,
1315
- abortSignal,
1316
- errorMessage
1317
- }) {
1318
- const result = await sandbox.run({ command, abortSignal });
1319
- if (result.exitCode !== 0) {
1320
- throw new Error(
1321
- `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`
1322
- );
1323
- }
1324
- }
1325
-
1326
- // src/utils/write-skills.ts
1327
- import { createHash } from "crypto";
1328
- import path3 from "path";
1329
- import {
1330
- safeParseJSON as safeParseJSON6
1331
- } from "@ai-sdk/provider-utils";
1332
- var SKILLS_MANIFEST_FILENAME = ".ai-sdk-harness-skills.json";
1333
- var SKILLS_MANIFEST_VERSION = 1;
1334
- var SAFE_MANIFEST_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
1335
- async function writeSkills({
1336
- sandbox,
1337
- homePath,
1338
- skillsDir,
1339
- skills,
1340
- abortSignal,
1341
- skillNamePattern = /^[A-Za-z0-9._-]+$/,
1342
- invalidSkillNameMessage = ({ name }) => `Invalid skill name: ${name}`,
1343
- filePathMode = "relative",
1344
- invalidSkillFilePathMessage = ({ skillName, filePath }) => `Invalid skill file path for ${skillName}: ${filePath}`,
1345
- trailingNewline = false
1346
- }) {
1347
- const rootDir = resolveSkillsRootDir({ homePath, skillsDir });
1348
- const projectedSkills = skills.map(
1349
- (skill) => projectSkill({
1350
- skill,
1351
- skillNamePattern,
1352
- invalidSkillNameMessage,
1353
- filePathMode,
1354
- invalidSkillFilePathMessage,
1355
- trailingNewline
1356
- })
1357
- ).sort((a, b) => a.name.localeCompare(b.name));
1358
- assertUniqueSkillNames(projectedSkills);
1359
- const manifestPath = path3.posix.join(rootDir, SKILLS_MANIFEST_FILENAME);
1360
- const existingManifest = await readSkillsManifest({
1361
- sandbox,
1362
- manifestPath,
1363
- abortSignal
1364
- });
1365
- const nextEntries = projectedSkills.map(({ name, hash }) => ({ name, hash }));
1366
- const nextByName = new Map(nextEntries.map((entry) => [entry.name, entry]));
1367
- if (existingManifest?.state === "pending") {
1368
- await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
1369
- const recoveryNames = existingManifest.skills.map((skill) => skill.name);
1370
- await removeSkillDirectories({
1371
- sandbox,
1372
- rootDir,
1373
- skillNames: recoveryNames,
1374
- abortSignal
1375
- });
1376
- const removed2 = recoveryNames.filter((name) => !nextByName.has(name)).sort((a, b) => a.localeCompare(b));
1377
- await writeProjectedSkills({
1378
- sandbox,
1379
- rootDir,
1380
- skills: projectedSkills,
1381
- abortSignal
1382
- });
1383
- await writeSkillsManifest({
1384
- sandbox,
1385
- manifestPath,
1386
- manifest: {
1387
- version: SKILLS_MANIFEST_VERSION,
1388
- state: "complete",
1389
- skills: nextEntries
1390
- },
1391
- abortSignal
1392
- });
1393
- return {
1394
- changed: recoveryNames.length > 0 || projectedSkills.length > 0,
1395
- written: projectedSkills.map((skill) => skill.name),
1396
- removed: removed2,
1397
- unchanged: []
1398
- };
1399
- }
1400
- const previousEntries = existingManifest?.skills ?? [];
1401
- const previousByName = new Map(
1402
- previousEntries.map((entry) => [entry.name, entry])
1403
- );
1404
- const removed = previousEntries.filter((entry) => !nextByName.has(entry.name)).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1405
- const written = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash !== skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1406
- const unchanged = projectedSkills.filter((skill) => previousByName.get(skill.name)?.hash === skill.hash).map((skill) => skill.name).sort((a, b) => a.localeCompare(b));
1407
- const changed = removed.length > 0 || written.length > 0;
1408
- if (!changed && existingManifest != null) {
1409
- return { changed: false, written, removed, unchanged };
1410
- }
1411
- const previouslyOwned = new Set(previousEntries.map((entry) => entry.name));
1412
- for (const skillName of written) {
1413
- if (previouslyOwned.has(skillName)) continue;
1414
- await assertSkillDirectoryAvailable({
1415
- sandbox,
1416
- rootDir,
1417
- skillName,
1418
- abortSignal
1419
- });
1420
- }
1421
- await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
1422
- const pendingNames = Array.from(
1423
- /* @__PURE__ */ new Set([
1424
- ...previousEntries.map((entry) => entry.name),
1425
- ...nextEntries.map((entry) => entry.name)
1426
- ])
1427
- ).sort((a, b) => a.localeCompare(b));
1428
- await writeSkillsManifest({
1429
- sandbox,
1430
- manifestPath,
1431
- manifest: {
1432
- version: SKILLS_MANIFEST_VERSION,
1433
- state: "pending",
1434
- skills: pendingNames.map((name) => ({
1435
- name,
1436
- hash: nextByName.get(name)?.hash ?? previousByName.get(name).hash
1437
- }))
1438
- },
1439
- abortSignal
1440
- });
1441
- await removeSkillDirectories({
1442
- sandbox,
1443
- rootDir,
1444
- skillNames: [
1445
- ...removed,
1446
- ...written.filter((name) => previouslyOwned.has(name))
1447
- ],
1448
- abortSignal
1449
- });
1450
- const writtenSet = new Set(written);
1451
- await writeProjectedSkills({
1452
- sandbox,
1453
- rootDir,
1454
- skills: projectedSkills.filter((skill) => writtenSet.has(skill.name)),
1455
- abortSignal
1456
- });
1457
- await writeSkillsManifest({
1458
- sandbox,
1459
- manifestPath,
1460
- manifest: {
1461
- version: SKILLS_MANIFEST_VERSION,
1462
- state: "complete",
1463
- skills: nextEntries
1464
- },
1465
- abortSignal
1466
- });
1467
- return { changed, written, removed, unchanged };
1468
- }
1469
- async function ensureSkillsDirectory({
1470
- sandbox,
1471
- rootDir,
1472
- abortSignal
1473
- }) {
1474
- await runSandboxCommand2({
1475
- sandbox,
1476
- command: `mkdir -p ${shellQuote(rootDir)}`,
1477
- abortSignal,
1478
- errorMessage: `Failed to create skills directory: ${rootDir}`
1479
- });
1480
- }
1481
- function projectSkill({
1482
- skill,
1483
- skillNamePattern,
1484
- invalidSkillNameMessage,
1485
- filePathMode,
1486
- invalidSkillFilePathMessage,
1487
- trailingNewline
1488
- }) {
1489
- const name = validateSkillName({
1490
- name: skill.name,
1491
- pattern: skillNamePattern,
1492
- message: invalidSkillNameMessage
1493
- });
1494
- const files = /* @__PURE__ */ new Map();
1495
- files.set("SKILL.md", renderSkillFile({ skill, trailingNewline }));
1496
- for (const file of skill.files ?? []) {
1497
- files.set(
1498
- normalizeSkillFilePath({
1499
- skillName: skill.name,
1500
- filePath: file.path,
1501
- mode: filePathMode,
1502
- message: invalidSkillFilePathMessage
1503
- }),
1504
- file.content
1505
- );
1506
- }
1507
- const projectedFiles = Array.from(files, ([filePath, content]) => ({
1508
- path: filePath,
1509
- content
1510
- })).sort((a, b) => a.path.localeCompare(b.path));
1511
- const hash = createHash("sha256");
1512
- for (const file of projectedFiles) {
1513
- hash.update(String(Buffer.byteLength(file.path)));
1514
- hash.update(":");
1515
- hash.update(file.path);
1516
- hash.update(String(Buffer.byteLength(file.content)));
1517
- hash.update(":");
1518
- hash.update(file.content);
1519
- }
1520
- return { name, hash: hash.digest("hex"), files: projectedFiles };
1521
- }
1522
- async function readSkillsManifest({
1523
- sandbox,
1524
- manifestPath,
1525
- abortSignal
1526
- }) {
1527
- const content = await sandbox.readTextFile({
1528
- path: manifestPath,
1529
- abortSignal
1530
- });
1531
- if (content == null) return void 0;
1532
- const parsed = await safeParseJSON6({ text: content });
1533
- if (!parsed.success || !isSkillsManifest(parsed.value)) {
1534
- throw new Error(`Invalid AI SDK harness skills manifest: ${manifestPath}`);
1535
- }
1536
- 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;
1537
1218
  }
1538
1219
  function isSkillsManifest(value) {
1539
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
1540
- return false;
1541
- }
1542
- const manifest = value;
1543
- if (manifest.version !== SKILLS_MANIFEST_VERSION || manifest.state !== "complete" && manifest.state !== "pending" || !Array.isArray(manifest.skills)) {
1544
- return false;
1545
- }
1546
- const names = /* @__PURE__ */ new Set();
1547
- for (const entry of manifest.skills) {
1548
- if (entry == null || typeof entry !== "object" || Array.isArray(entry)) {
1549
- return false;
1550
- }
1551
- const skill = entry;
1552
- if (typeof skill.name !== "string" || !isSafeManifestSkillName(skill.name) || typeof skill.hash !== "string" || !/^[a-f0-9]{64}$/.test(skill.hash) || names.has(skill.name)) {
1553
- return false;
1554
- }
1555
- names.add(skill.name);
1556
- }
1557
- return true;
1558
- }
1559
- async function writeSkillsManifest({
1560
- sandbox,
1561
- manifestPath,
1562
- manifest,
1563
- abortSignal
1564
- }) {
1565
- const temporaryPath = `${manifestPath}.tmp`;
1566
- await sandbox.writeTextFile({
1567
- path: temporaryPath,
1568
- content: `${JSON.stringify(manifest, null, 2)}
1569
- `,
1570
- abortSignal
1571
- });
1572
- await runSandboxCommand2({
1573
- sandbox,
1574
- command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(manifestPath)}`,
1575
- abortSignal,
1576
- errorMessage: `Failed to update skills manifest: ${manifestPath}`
1577
- });
1578
- }
1579
- async function assertSkillDirectoryAvailable({
1580
- sandbox,
1581
- rootDir,
1582
- skillName,
1583
- abortSignal
1584
- }) {
1585
- const skillDir = path3.posix.join(rootDir, skillName);
1586
- const result = await sandbox.run({
1587
- command: `test ! -e ${shellQuote(skillDir)}`,
1588
- abortSignal
1589
- });
1590
- if (result.exitCode !== 0) {
1591
- throw new Error(
1592
- `Cannot write harness skill '${skillName}': ${skillDir} already exists and is not owned by the AI SDK harness.`
1593
- );
1594
- }
1595
- }
1596
- async function removeSkillDirectories({
1597
- sandbox,
1598
- rootDir,
1599
- skillNames,
1600
- abortSignal
1601
- }) {
1602
- if (skillNames.length === 0) return;
1603
- const directories = Array.from(new Set(skillNames)).sort((a, b) => a.localeCompare(b)).map((name) => shellQuote(path3.posix.join(rootDir, name))).join(" ");
1604
- await runSandboxCommand2({
1605
- sandbox,
1606
- command: `rm -rf -- ${directories}`,
1607
- abortSignal,
1608
- errorMessage: `Failed to replace harness skills in: ${rootDir}`
1609
- });
1610
- }
1611
- async function writeProjectedSkills({
1612
- sandbox,
1613
- rootDir,
1614
- skills,
1615
- abortSignal
1616
- }) {
1617
- for (const skill of skills) {
1618
- const skillDir = path3.posix.join(rootDir, skill.name);
1619
- for (const file of skill.files) {
1620
- await sandbox.writeTextFile({
1621
- path: path3.posix.join(skillDir, file.path),
1622
- content: file.content,
1623
- abortSignal
1624
- });
1625
- }
1626
- }
1627
- }
1628
- async function runSandboxCommand2({
1629
- sandbox,
1630
- command,
1631
- abortSignal,
1632
- errorMessage
1633
- }) {
1634
- const result = await sandbox.run({ command, abortSignal });
1635
- if (result.exitCode !== 0) {
1636
- throw new Error(
1637
- `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`
1638
- );
1639
- }
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}` : ""}`);
1640
1278
  }
1641
1279
  function assertUniqueSkillNames(skills) {
1642
- for (let index = 1; index < skills.length; index++) {
1643
- if (skills[index - 1].name === skills[index].name) {
1644
- throw new Error(`Duplicate skill name: ${skills[index].name}`);
1645
- }
1646
- }
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}`);
1647
1281
  }
1648
1282
  function isSafeManifestSkillName(name) {
1649
- SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1650
- const matches = SAFE_MANIFEST_SKILL_NAME.test(name);
1651
- SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
1652
- return matches && name !== "." && name !== ".." && !name.includes("/");
1653
- }
1654
- function validateSkillName({
1655
- name,
1656
- pattern,
1657
- message
1658
- }) {
1659
- pattern.lastIndex = 0;
1660
- const matches = pattern.test(name);
1661
- pattern.lastIndex = 0;
1662
- if (!matches || name === "." || name === "..") {
1663
- throw new Error(message?.({ name }) ?? `Invalid skill name: ${name}`);
1664
- }
1665
- return name;
1666
- }
1667
- function normalizeSkillFilePath({
1668
- skillName,
1669
- filePath,
1670
- mode,
1671
- message
1672
- }) {
1673
- const normalized = mode === "strip-leading-slashes" ? filePath.replace(/^\/+/, "") : path3.posix.normalize(filePath);
1674
- const invalid = normalized === "" || mode === "relative" && normalized === "." || normalized.startsWith("../") || normalized.includes("/../") || normalized.endsWith("/..") || mode === "relative" && path3.posix.isAbsolute(normalized);
1675
- if (invalid) {
1676
- throw new Error(
1677
- message?.({ skillName: skillName ?? "", filePath }) ?? `Invalid skill file path for ${skillName}: ${filePath}`
1678
- );
1679
- }
1680
- return normalized;
1681
- }
1682
- function renderSkillFile({
1683
- skill,
1684
- trailingNewline
1685
- }) {
1686
- const content = `---
1687
- name: ${skill.name}
1688
- description: ${skill.description}
1689
- ---
1690
-
1691
- ${skill.content}`;
1692
- return trailingNewline ? `${content}
1693
- ` : content;
1694
- }
1695
- function resolveSkillsRootDir({
1696
- homePath,
1697
- skillsDir
1698
- }) {
1699
- if (typeof homePath !== "string" || homePath.trim().length === 0) {
1700
- throw new Error("Invalid homePath: expected a non-empty string.");
1701
- }
1702
- if (!path3.posix.isAbsolute(homePath)) {
1703
- throw new Error(
1704
- `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`
1705
- );
1706
- }
1707
- if (typeof skillsDir !== "string" || skillsDir.trim().length === 0) {
1708
- throw new Error(
1709
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`
1710
- );
1711
- }
1712
- const containsTraversal = skillsDir.split(/[\\/]/).some((segment) => segment === "..");
1713
- const normalizedSkillsDir = path3.posix.normalize(skillsDir.trim());
1714
- const normalizedNoTrailingSlash = normalizedSkillsDir.replace(/\/+$/, "");
1715
- if (skillsDir.includes("\\") || path3.posix.isAbsolute(skillsDir) || path3.win32.isAbsolute(skillsDir) || containsTraversal || normalizedNoTrailingSlash === "" || normalizedNoTrailingSlash === "." || normalizedSkillsDir.startsWith("../") || normalizedSkillsDir.includes("/../") || normalizedSkillsDir.endsWith("/..")) {
1716
- throw new Error(
1717
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`
1718
- );
1719
- }
1720
- const rootDir = path3.posix.join(homePath, normalizedNoTrailingSlash);
1721
- const relative = path3.posix.relative(homePath, rootDir);
1722
- if (relative === "" || relative.startsWith("..") || path3.posix.isAbsolute(relative)) {
1723
- throw new Error(
1724
- `Invalid skillsDir ${JSON.stringify(skillsDir)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`
1725
- );
1726
- }
1727
- return rootDir;
1728
- }
1729
-
1730
- // src/utils/bridge-ready.ts
1731
- import {
1732
- safeParseJSON as safeParseJSON7
1733
- } from "@ai-sdk/provider-utils";
1734
- import { z as z4 } from "zod/v4";
1735
-
1736
- // src/v1/harness-v1-bridge-protocol.ts
1737
- import { z as z3 } from "zod/v4";
1738
-
1739
- // src/v1/harness-v1-diagnostic.ts
1740
- import { z } from "zod/v4";
1741
- var harnessV1DebugLevelSchema = z.enum([
1742
- "error",
1743
- "warn",
1744
- "info",
1745
- "debug",
1746
- "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"
1747
1329
  ]);
1748
- var harnessV1DebugConfigSchema = z.object({
1749
- enabled: z.boolean().optional(),
1750
- level: harnessV1DebugLevelSchema.optional(),
1751
- 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()
1752
1340
  });
1753
-
1754
- // src/v1/harness-v1-stream-part.ts
1755
- import { z as z2 } from "zod/v4";
1756
- var harnessV1JsonValueSchema = z2.lazy(
1757
- () => z2.union([
1758
- z2.string(),
1759
- z2.number(),
1760
- z2.boolean(),
1761
- z2.null(),
1762
- z2.array(harnessV1JsonValueSchema),
1763
- z2.record(z2.string(), harnessV1JsonValueSchema)
1764
- ])
1765
- );
1766
- var harnessV1ToolResultValueSchema = harnessV1JsonValueSchema;
1767
- var harnessV1JsonObjectSchema = z2.record(
1768
- z2.string(),
1769
- harnessV1JsonValueSchema
1770
- );
1771
- var harnessV1MetadataSchema = z2.record(
1772
- z2.string(),
1773
- z2.record(z2.string(), harnessV1JsonValueSchema)
1774
- );
1775
- var harnessV1ProviderMetadataSchema = z2.record(
1776
- z2.string(),
1777
- z2.record(z2.string(), harnessV1JsonValueSchema)
1778
- );
1779
- var harnessV1CallWarningSchema = z2.union([
1780
- z2.object({
1781
- type: z2.literal("unsupported-setting"),
1782
- setting: z2.string(),
1783
- details: z2.string().optional()
1784
- }),
1785
- z2.object({
1786
- type: z2.literal("unsupported-tool"),
1787
- tool: z2.string(),
1788
- details: z2.string().optional()
1789
- }),
1790
- 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
+ })
1791
1370
  ]);
1792
- var harnessV1UsageSchema = z2.object({
1793
- inputTokens: z2.object({
1794
- total: z2.number().optional(),
1795
- noCache: z2.number().optional(),
1796
- cacheRead: z2.number().optional(),
1797
- cacheWrite: z2.number().optional()
1798
- }),
1799
- outputTokens: z2.object({
1800
- total: z2.number().optional(),
1801
- text: z2.number().optional(),
1802
- reasoning: z2.number().optional()
1803
- }),
1804
- 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()
1805
1384
  });
1806
- var harnessV1FinishReasonSchema = z2.object({
1807
- unified: z2.enum([
1808
- "stop",
1809
- "length",
1810
- "content-filter",
1811
- "tool-calls",
1812
- "error",
1813
- "other"
1814
- ]),
1815
- 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()
1816
1395
  });
1817
- var harnessV1StreamStartPartSchema = z2.object({
1818
- type: z2.literal("stream-start"),
1819
- warnings: z2.array(harnessV1CallWarningSchema).readonly().optional(),
1820
- 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()
1821
1400
  });
1822
- var harnessV1TextStartPartSchema = z2.object({
1823
- type: z2.literal("text-start"),
1824
- id: z2.string(),
1825
- harnessMetadata: harnessV1MetadataSchema.optional()
1401
+ const harnessV1TextStartPartSchema = z.object({
1402
+ type: z.literal("text-start"),
1403
+ id: z.string(),
1404
+ harnessMetadata: harnessV1MetadataSchema.optional()
1826
1405
  });
1827
- var harnessV1TextDeltaPartSchema = z2.object({
1828
- type: z2.literal("text-delta"),
1829
- id: z2.string(),
1830
- delta: z2.string(),
1831
- 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()
1832
1411
  });
1833
- var harnessV1TextEndPartSchema = z2.object({
1834
- type: z2.literal("text-end"),
1835
- id: z2.string(),
1836
- harnessMetadata: harnessV1MetadataSchema.optional()
1412
+ const harnessV1TextEndPartSchema = z.object({
1413
+ type: z.literal("text-end"),
1414
+ id: z.string(),
1415
+ harnessMetadata: harnessV1MetadataSchema.optional()
1837
1416
  });
1838
- var harnessV1ReasoningStartPartSchema = z2.object({
1839
- type: z2.literal("reasoning-start"),
1840
- id: z2.string(),
1841
- harnessMetadata: harnessV1MetadataSchema.optional()
1417
+ const harnessV1ReasoningStartPartSchema = z.object({
1418
+ type: z.literal("reasoning-start"),
1419
+ id: z.string(),
1420
+ harnessMetadata: harnessV1MetadataSchema.optional()
1842
1421
  });
1843
- var harnessV1ReasoningDeltaPartSchema = z2.object({
1844
- type: z2.literal("reasoning-delta"),
1845
- id: z2.string(),
1846
- delta: z2.string(),
1847
- 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()
1848
1427
  });
1849
- var harnessV1ReasoningEndPartSchema = z2.object({
1850
- type: z2.literal("reasoning-end"),
1851
- id: z2.string(),
1852
- harnessMetadata: harnessV1MetadataSchema.optional()
1428
+ const harnessV1ReasoningEndPartSchema = z.object({
1429
+ type: z.literal("reasoning-end"),
1430
+ id: z.string(),
1431
+ harnessMetadata: harnessV1MetadataSchema.optional()
1853
1432
  });
1854
- var harnessV1ToolInputStartPartSchema = z2.object({
1855
- type: z2.literal("tool-input-start"),
1856
- id: z2.string(),
1857
- toolName: z2.string(),
1858
- providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1859
- providerExecuted: z2.boolean().optional(),
1860
- dynamic: z2.boolean().optional(),
1861
- 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()
1862
1441
  });
1863
- var harnessV1ToolInputDeltaPartSchema = z2.object({
1864
- type: z2.literal("tool-input-delta"),
1865
- id: z2.string(),
1866
- delta: z2.string(),
1867
- 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()
1868
1447
  });
1869
- var harnessV1ToolInputEndPartSchema = z2.object({
1870
- type: z2.literal("tool-input-end"),
1871
- id: z2.string(),
1872
- providerMetadata: harnessV1ProviderMetadataSchema.optional()
1448
+ const harnessV1ToolInputEndPartSchema = z.object({
1449
+ type: z.literal("tool-input-end"),
1450
+ id: z.string(),
1451
+ providerMetadata: harnessV1ProviderMetadataSchema.optional()
1873
1452
  });
1874
- var harnessV1ToolCallPartSchema = z2.object({
1875
- type: z2.literal("tool-call"),
1876
- toolCallId: z2.string(),
1877
- toolName: z2.string(),
1878
- input: z2.string(),
1879
- providerExecuted: z2.boolean().optional(),
1880
- dynamic: z2.boolean().optional(),
1881
- providerMetadata: harnessV1ProviderMetadataSchema.optional(),
1882
- nativeName: z2.string().optional(),
1883
- 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()
1884
1463
  });
1885
- var harnessV1ToolApprovalRequestPartSchema = z2.object({
1886
- type: z2.literal("tool-approval-request"),
1887
- approvalId: z2.string(),
1888
- toolCallId: z2.string(),
1889
- 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()
1890
1469
  });
1891
- var harnessV1ToolResultPartSchema = z2.object({
1892
- type: z2.literal("tool-result"),
1893
- toolCallId: z2.string(),
1894
- toolName: z2.string(),
1895
- result: harnessV1ToolResultValueSchema,
1896
- isError: z2.boolean().optional(),
1897
- preliminary: z2.boolean().optional(),
1898
- dynamic: z2.boolean().optional(),
1899
- 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()
1900
1479
  });
1901
- var harnessV1FinishStepPartSchema = z2.object({
1902
- type: z2.literal("finish-step"),
1903
- finishReason: harnessV1FinishReasonSchema,
1904
- usage: harnessV1UsageSchema,
1905
- harnessMetadata: harnessV1MetadataSchema.optional()
1480
+ const harnessV1FinishStepPartSchema = z.object({
1481
+ type: z.literal("finish-step"),
1482
+ finishReason: harnessV1FinishReasonSchema,
1483
+ usage: harnessV1UsageSchema,
1484
+ harnessMetadata: harnessV1MetadataSchema.optional()
1906
1485
  });
1907
- var harnessV1FinishPartSchema = z2.object({
1908
- type: z2.literal("finish"),
1909
- finishReason: harnessV1FinishReasonSchema,
1910
- totalUsage: harnessV1UsageSchema,
1911
- harnessMetadata: harnessV1MetadataSchema.optional()
1486
+ const harnessV1FinishPartSchema = z.object({
1487
+ type: z.literal("finish"),
1488
+ finishReason: harnessV1FinishReasonSchema,
1489
+ totalUsage: harnessV1UsageSchema,
1490
+ harnessMetadata: harnessV1MetadataSchema.optional()
1912
1491
  });
1913
- var harnessV1FileChangePartSchema = z2.object({
1914
- type: z2.literal("file-change"),
1915
- event: z2.enum(["create", "modify", "delete"]),
1916
- path: z2.string(),
1917
- 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()
1918
1501
  });
1919
- var harnessV1CompactionPartSchema = z2.object({
1920
- type: z2.literal("compaction"),
1921
- trigger: z2.enum(["manual", "auto"]),
1922
- summary: z2.string(),
1923
- tokensBefore: z2.number().optional(),
1924
- tokensAfter: z2.number().optional(),
1925
- 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()
1926
1509
  });
1927
- var harnessV1ErrorPartSchema = z2.object({
1928
- type: z2.literal("error"),
1929
- error: z2.unknown()
1510
+ const harnessV1ErrorPartSchema = z.object({
1511
+ type: z.literal("error"),
1512
+ error: z.unknown()
1930
1513
  });
1931
- var harnessV1RawPartSchema = z2.object({
1932
- type: z2.literal("raw"),
1933
- rawValue: z2.unknown()
1514
+ const harnessV1RawPartSchema = z.object({
1515
+ type: z.literal("raw"),
1516
+ rawValue: z.unknown()
1934
1517
  });
1935
- var harnessV1StreamPartSchema = z2.discriminatedUnion("type", [
1936
- harnessV1StreamStartPartSchema,
1937
- harnessV1TextStartPartSchema,
1938
- harnessV1TextDeltaPartSchema,
1939
- harnessV1TextEndPartSchema,
1940
- harnessV1ReasoningStartPartSchema,
1941
- harnessV1ReasoningDeltaPartSchema,
1942
- harnessV1ReasoningEndPartSchema,
1943
- harnessV1ToolInputStartPartSchema,
1944
- harnessV1ToolInputDeltaPartSchema,
1945
- harnessV1ToolInputEndPartSchema,
1946
- harnessV1ToolCallPartSchema,
1947
- harnessV1ToolApprovalRequestPartSchema,
1948
- harnessV1ToolResultPartSchema,
1949
- harnessV1FinishStepPartSchema,
1950
- harnessV1FinishPartSchema,
1951
- harnessV1FileChangePartSchema,
1952
- harnessV1CompactionPartSchema,
1953
- harnessV1ErrorPartSchema,
1954
- 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
1955
1538
  ]);
1956
-
1957
- // src/v1/harness-v1-bridge-protocol.ts
1958
- var harnessV1BridgeToolWireSchema = z3.object({
1959
- name: z3.string(),
1960
- description: z3.string().optional(),
1961
- 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()
1962
1550
  });
1963
- var harnessV1BridgePermissionModeSchema = z3.enum([
1964
- "allow-reads",
1965
- "allow-edits",
1966
- "allow-all"
1967
- ]);
1968
- var harnessV1BridgeBuiltinToolFilteringSchema = z3.discriminatedUnion(
1969
- "mode",
1970
- [
1971
- z3.object({
1972
- mode: z3.literal("allow"),
1973
- toolNames: z3.array(z3.string())
1974
- }),
1975
- z3.object({
1976
- mode: z3.literal("deny"),
1977
- toolNames: z3.array(z3.string())
1978
- })
1979
- ]
1980
- );
1981
- var harnessV1BridgeResponseFormatSchema = z3.discriminatedUnion("type", [
1982
- z3.object({ type: z3.literal("text") }),
1983
- z3.object({
1984
- type: z3.literal("json"),
1985
- schema: z3.record(z3.string(), z3.json()).optional(),
1986
- name: z3.string().optional(),
1987
- description: z3.string().optional()
1988
- })
1551
+ const harnessV1BridgePermissionModeSchema = z.enum([
1552
+ "allow-reads",
1553
+ "allow-edits",
1554
+ "allow-all"
1989
1555
  ]);
1990
- var harnessV1BridgeStartBaseSchema = z3.object({
1991
- type: z3.literal("start"),
1992
- prompt: z3.string(),
1993
- tools: z3.array(harnessV1BridgeToolWireSchema).optional(),
1994
- model: z3.string().optional(),
1995
- debug: harnessV1DebugConfigSchema.optional(),
1996
- permissionMode: harnessV1BridgePermissionModeSchema.optional(),
1997
- builtinToolFiltering: harnessV1BridgeBuiltinToolFilteringSchema.optional(),
1998
- responseFormat: harnessV1BridgeResponseFormatSchema.optional()
1999
- });
2000
- var harnessV1BridgeHelloSchema = z3.object({
2001
- type: z3.literal("bridge-hello"),
2002
- state: z3.string().optional(),
2003
- lastSeq: z3.number().optional(),
2004
- capabilities: z3.object({
2005
- experimental_userMessageResponses: z3.boolean().optional()
2006
- }).optional()
2007
- });
2008
- var experimental_harnessV1BridgeUserMessageResponseSchema = z3.object({
2009
- type: z3.literal("user-message-response"),
2010
- messageId: z3.string(),
2011
- accepted: z3.boolean(),
2012
- error: z3.object({ message: z3.string() }).optional()
2013
- });
2014
- var harnessV1BridgeStopSchema = z3.object({
2015
- type: z3.literal("bridge-stop"),
2016
- data: z3.unknown()
2017
- });
2018
- var harnessV1BridgeThreadSchema = z3.object({
2019
- type: z3.literal("bridge-thread"),
2020
- 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()
2021
1578
  });
2022
- var harnessV1BridgeSandboxLogSchema = z3.object({
2023
- type: z3.literal("sandbox-log"),
2024
- source: z3.string(),
2025
- stream: z3.enum(["stdout", "stderr"]),
2026
- 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()
2027
1591
  });
2028
- var harnessV1BridgeDebugEventSchema = z3.object({
2029
- type: z3.literal("debug-event"),
2030
- level: harnessV1DebugLevelSchema,
2031
- subsystem: z3.string(),
2032
- message: z3.string(),
2033
- attrs: z3.record(z3.string(), z3.unknown()).optional(),
2034
- error: z3.object({
2035
- name: z3.string().optional(),
2036
- message: z3.string(),
2037
- stack: z3.string().optional()
2038
- }).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()
2039
1597
  });
2040
- var harnessV1BridgeOutboundMessageSchema = z3.discriminatedUnion(
2041
- "type",
2042
- [
2043
- harnessV1StreamStartPartSchema,
2044
- harnessV1TextStartPartSchema,
2045
- harnessV1TextDeltaPartSchema,
2046
- harnessV1TextEndPartSchema,
2047
- harnessV1ReasoningStartPartSchema,
2048
- harnessV1ReasoningDeltaPartSchema,
2049
- harnessV1ReasoningEndPartSchema,
2050
- harnessV1ToolInputStartPartSchema,
2051
- harnessV1ToolInputDeltaPartSchema,
2052
- harnessV1ToolInputEndPartSchema,
2053
- harnessV1ToolCallPartSchema,
2054
- harnessV1ToolApprovalRequestPartSchema,
2055
- harnessV1ToolResultPartSchema,
2056
- harnessV1FinishStepPartSchema,
2057
- harnessV1FinishPartSchema,
2058
- harnessV1FileChangePartSchema,
2059
- harnessV1CompactionPartSchema,
2060
- harnessV1ErrorPartSchema,
2061
- harnessV1RawPartSchema,
2062
- harnessV1BridgeHelloSchema,
2063
- experimental_harnessV1BridgeUserMessageResponseSchema,
2064
- harnessV1BridgeStopSchema,
2065
- harnessV1BridgeThreadSchema,
2066
- harnessV1BridgeSandboxLogSchema,
2067
- harnessV1BridgeDebugEventSchema
2068
- ]
2069
- );
2070
- var harnessV1BridgeToolResultInboundSchema = z3.object({
2071
- type: z3.literal("tool-result"),
2072
- toolCallId: z3.string(),
2073
- output: z3.unknown(),
2074
- isError: z3.boolean().optional(),
2075
- 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()
2076
1605
  });
2077
- var harnessV1BridgeToolApprovalResponseInboundSchema = z3.object({
2078
- type: z3.literal("tool-approval-response"),
2079
- approvalId: z3.string(),
2080
- approved: z3.boolean(),
2081
- 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()
2082
1613
  });
2083
- var harnessV1BridgeUserMessageInboundSchema = z3.object({
2084
- type: z3.literal("user-message"),
2085
- messageId: z3.string().optional(),
2086
- 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()
2087
1624
  });
2088
- var experimental_harnessV1BridgeUserMessageInboundSchema = harnessV1BridgeUserMessageInboundSchema.extend({
2089
- 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()
2090
1640
  });
2091
- var harnessV1BridgeAbortInboundSchema = z3.object({
2092
- type: z3.literal("abort")
2093
- });
2094
- var harnessV1BridgeDestroyInboundSchema = z3.object({
2095
- 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()
2096
1674
  });
2097
- var harnessV1BridgeResumeInboundSchema = z3.object({
2098
- type: z3.literal("resume"),
2099
- 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()
2100
1680
  });
2101
- var harnessV1BridgeStopInboundSchema = z3.object({
2102
- 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()
2103
1691
  });
2104
- var harnessV1BridgeReadySchema = z3.object({
2105
- type: z3.literal("bridge-ready"),
2106
- 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()
2107
1700
  });
2108
-
2109
- // src/utils/bridge-ready.ts
2110
- var bridgeMetaSchema = z4.object({
2111
- type: z4.string().optional(),
2112
- port: z4.number().optional(),
2113
- state: z4.string().optional(),
2114
- 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()
2115
1708
  });
2116
- async function markBridgeStarting({
2117
- sandbox,
2118
- bridgeStateDir,
2119
- bridgeType,
2120
- abortSignal
2121
- }) {
2122
- try {
2123
- await sandbox.writeTextFile({
2124
- path: bridgeMetaPath(bridgeStateDir),
2125
- content: JSON.stringify({ type: bridgeType, state: "starting" }),
2126
- abortSignal
2127
- });
2128
- } catch {
2129
- }
2130
- }
2131
- async function waitForBridgeReady({
2132
- proc,
2133
- sandbox,
2134
- bridgeStateDir,
2135
- bridgeType,
2136
- timeoutMs,
2137
- abortSignal,
2138
- pollIntervalMs = 100,
2139
- createTimeoutError,
2140
- createExitError
2141
- }) {
2142
- const reader = proc.stdout.pipeThrough(new TextDecoderStream()).getReader();
2143
- const decoder = lineDecoder();
2144
- const stdoutTail = [];
2145
- const deadline = Date.now() + timeoutMs;
2146
- let pendingStdoutRead;
2147
- let pendingMetaRead;
2148
- let nextMetaReadAt = 0;
2149
- let cancelReader = false;
2150
- try {
2151
- while (true) {
2152
- if (abortSignal?.aborted) {
2153
- cancelReader = true;
2154
- await proc.kill();
2155
- throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
2156
- }
2157
- const remaining = deadline - Date.now();
2158
- if (remaining <= 0) {
2159
- cancelReader = true;
2160
- await proc.kill();
2161
- throw await makeBridgeReadyError({
2162
- createError: createTimeoutError,
2163
- fallbackMessage: "bridge did not become ready in time.",
2164
- proc,
2165
- stdoutTail
2166
- });
2167
- }
2168
- pendingStdoutRead ??= reader.read();
2169
- const metaReadDelayMs = Math.max(0, nextMetaReadAt - Date.now());
2170
- if (pendingMetaRead === void 0 && metaReadDelayMs === 0) {
2171
- pendingMetaRead = readBridgeMetaReady({
2172
- sandbox,
2173
- bridgeStateDir,
2174
- bridgeType,
2175
- abortSignal
2176
- });
2177
- }
2178
- const result = await Promise.race([
2179
- pendingStdoutRead.then((read) => ({ source: "stdout", read })),
2180
- ...pendingMetaRead === void 0 ? [] : [
2181
- pendingMetaRead.then((port) => ({
2182
- source: "metadata",
2183
- port
2184
- }))
2185
- ],
2186
- sleep2(
2187
- Math.min(
2188
- remaining,
2189
- pendingMetaRead === void 0 ? metaReadDelayMs : pollIntervalMs
2190
- )
2191
- ).then(() => void 0)
2192
- ]);
2193
- if (result === void 0) continue;
2194
- if (result.source === "metadata") {
2195
- pendingMetaRead = void 0;
2196
- if (result.port !== void 0) {
2197
- cancelReader = true;
2198
- return {
2199
- port: result.port,
2200
- source: "metadata",
2201
- stdoutTail: [...stdoutTail]
2202
- };
2203
- }
2204
- nextMetaReadAt = Date.now() + pollIntervalMs;
2205
- continue;
2206
- }
2207
- pendingStdoutRead = void 0;
2208
- const { value, done } = result.read;
2209
- if (done) {
2210
- for (const line of decoder.flush()) {
2211
- pushTail({ lines: stdoutTail, line });
2212
- }
2213
- throw await makeBridgeReadyError({
2214
- createError: createExitError,
2215
- fallbackMessage: "bridge exited before becoming ready.",
2216
- proc,
2217
- stdoutTail
2218
- });
2219
- }
2220
- if (value === void 0) continue;
2221
- for (const line of decoder.push(value)) {
2222
- pushTail({ lines: stdoutTail, line });
2223
- const parsed = await safeParseJSON7({
2224
- text: line,
2225
- schema: harnessV1BridgeReadySchema
2226
- });
2227
- if (parsed.success) {
2228
- return {
2229
- port: parsed.value.port,
2230
- source: "stdout",
2231
- stdoutTail: [...stdoutTail]
2232
- };
2233
- }
2234
- }
2235
- }
2236
- } finally {
2237
- if (cancelReader) {
2238
- await Promise.race([reader.cancel().catch(() => {
2239
- }), sleep2(100)]);
2240
- }
2241
- try {
2242
- reader.releaseLock();
2243
- } catch {
2244
- }
2245
- }
2246
- }
2247
- async function readBridgeMetaReady({
2248
- sandbox,
2249
- bridgeStateDir,
2250
- bridgeType,
2251
- abortSignal
2252
- }) {
2253
- const raw = await Promise.resolve(
2254
- sandbox.readTextFile({
2255
- path: bridgeMetaPath(bridgeStateDir),
2256
- abortSignal
2257
- })
2258
- ).catch(() => null);
2259
- if (raw == null) return void 0;
2260
- const parsed = await safeParseJSON7({ text: raw, schema: bridgeMetaSchema });
2261
- if (!parsed.success) return void 0;
2262
- if (parsed.value.type !== bridgeType) return void 0;
2263
- if (parsed.value.state !== "waiting") return void 0;
2264
- if (parsed.value.port === void 0 || parsed.value.port <= 0) {
2265
- return void 0;
2266
- }
2267
- return parsed.value.port;
2268
- }
2269
- async function makeBridgeReadyError({
2270
- createError,
2271
- fallbackMessage,
2272
- proc,
2273
- stdoutTail
2274
- }) {
2275
- if (createError) {
2276
- return createError({ proc, stdoutTail: [...stdoutTail] });
2277
- }
2278
- 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);
2279
1841
  }
2280
1842
  function bridgeMetaPath(bridgeStateDir) {
2281
- return `${bridgeStateDir}/bridge-meta.json`;
1843
+ return `${bridgeStateDir}/bridge-meta.json`;
2282
1844
  }
2283
1845
  function pushTail({ lines, line }) {
2284
- lines.push(line);
2285
- if (lines.length > 20) lines.shift();
2286
- }
2287
- function sleep2(ms) {
2288
- return new Promise((resolve) => setTimeout(resolve, ms));
2289
- }
2290
- function lineDecoder() {
2291
- let buffer = "";
2292
- return {
2293
- push(chunk) {
2294
- buffer += chunk;
2295
- const lines = [];
2296
- let nl;
2297
- while ((nl = buffer.indexOf("\n")) !== -1) {
2298
- const raw = buffer.slice(0, nl);
2299
- buffer = buffer.slice(nl + 1);
2300
- const line = raw.replace(/\r$/, "").trim();
2301
- if (line.length > 0) lines.push(line);
2302
- }
2303
- return lines;
2304
- },
2305
- flush() {
2306
- const line = buffer.replace(/\r$/, "").trim();
2307
- buffer = "";
2308
- return line.length > 0 ? [line] : [];
2309
- }
2310
- };
2311
- }
2312
-
2313
- // src/utils/bridge-token.ts
2314
- 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
2315
1876
  function createBridgeToken() {
2316
- return randomBytes2(32).toString("hex");
2317
- }
2318
- function withBridgeToken({
2319
- endpoint,
2320
- token
2321
- }) {
2322
- const bridgeUrl = new URL(endpoint.url);
2323
- bridgeUrl.searchParams.set("agent_bridge_token", token);
2324
- return { ...endpoint, url: bridgeUrl.toString() };
2325
- }
2326
-
2327
- // src/utils/bridge-asset.ts
2328
- 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
2329
1889
  function createReadBridgeAsset(assetUrls) {
2330
- return async (name) => readFile(assetUrls[name], "utf8");
1890
+ return async (name) => readFile(assetUrls[name], "utf8");
2331
1891
  }
2332
-
2333
- // src/utils/bridge-diagnostics.ts
2334
- var DEFAULT_TAIL_LIMIT = 20;
1892
+ //#endregion
1893
+ //#region src/utils/bridge-diagnostics.ts
1894
+ const DEFAULT_TAIL_LIMIT = 20;
2335
1895
  function isSerializedError(error) {
2336
- return typeof error === "object" && error != null && typeof error.message === "string";
1896
+ return typeof error === "object" && error != null && typeof error.message === "string";
2337
1897
  }
2338
1898
  function stringifyUnknown(value) {
2339
- if (typeof value === "string") return value;
2340
- try {
2341
- return JSON.stringify(value);
2342
- } catch {
2343
- return String(value);
2344
- }
1899
+ if (typeof value === "string") return value;
1900
+ try {
1901
+ return JSON.stringify(value);
1902
+ } catch {
1903
+ return String(value);
1904
+ }
2345
1905
  }
2346
1906
  function formatBridgeError(error) {
2347
- if (error instanceof Error) {
2348
- return error.stack ?? `${error.name}: ${error.message}`;
2349
- }
2350
- if (isSerializedError(error)) {
2351
- return error.stack ?? `${error.name ? `${error.name}: ` : ""}${error.message}`;
2352
- }
2353
- 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);
2354
1910
  }
2355
1911
  function writeToStderr(line) {
2356
- try {
2357
- process.stderr.write(line);
2358
- } catch {
2359
- }
2360
- }
2361
- function logBridgeError({
2362
- harnessId,
2363
- sessionId,
2364
- context,
2365
- error,
2366
- write = writeToStderr
2367
- }) {
2368
- const prefix = `[harness:${harnessId}:error${sessionId ? ` session=${sessionId}` : ""}]`;
2369
- const message = context ? `${context}: ${formatBridgeError(error)}` : formatBridgeError(error);
2370
- for (const line of message.split("\n")) {
2371
- if (line.trim().length > 0) {
2372
- write(`${prefix} ${line}
2373
- `);
2374
- }
2375
- }
2376
- }
2377
- function createBridgeErrorHandler({
2378
- harnessId,
2379
- sessionId
2380
- }) {
2381
- return (event) => {
2382
- logBridgeError({
2383
- harnessId,
2384
- sessionId,
2385
- context: "bridge emitted an error frame",
2386
- error: event.error
2387
- });
2388
- };
2389
- }
2390
- async function createBridgeStartupError({
2391
- message,
2392
- proc,
2393
- stdoutTail,
2394
- stderrTail,
2395
- stderrDone
2396
- }) {
2397
- if (stderrDone) {
2398
- await Promise.race([stderrDone, sleep3(250)]).catch(() => {
2399
- });
2400
- }
2401
- let exitStatus = "";
2402
- try {
2403
- const result = await Promise.race([
2404
- proc.wait(),
2405
- sleep3(250).then(() => void 0)
2406
- ]);
2407
- if (result?.exitCode !== void 0) {
2408
- exitStatus = ` Exit code: ${result.exitCode}.`;
2409
- }
2410
- } catch {
2411
- }
2412
- const details = [];
2413
- if (stdoutTail.length > 0) {
2414
- details.push(`stdout:
2415
- ${stdoutTail.join("\n")}`);
2416
- }
2417
- if (stderrTail.length > 0) {
2418
- details.push(`stderr:
2419
- ${stderrTail.join("\n")}`);
2420
- }
2421
- return new Error(
2422
- `${message}${exitStatus}${details.length > 0 ? `
2423
-
2424
- ${details.join("\n\n")}` : ""}`
2425
- );
2426
- }
2427
- async function forwardBridgeProcessStream({
2428
- stream,
2429
- streamName,
2430
- source = "bridge",
2431
- collectTail,
2432
- tailLimit = DEFAULT_TAIL_LIMIT
2433
- }) {
2434
- try {
2435
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
2436
- const decoder = lineDecoder2();
2437
- while (true) {
2438
- const { value, done } = await reader.read();
2439
- const lines = done ? decoder.flush() : value ? decoder.push(value) : [];
2440
- for (const line of lines) {
2441
- recordTail({ lines: collectTail, line, limit: tailLimit });
2442
- writeToStderr(`[harness:${source}:${streamName}] ${line}
2443
- `);
2444
- }
2445
- if (done) return;
2446
- }
2447
- } catch {
2448
- }
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 {}
2449
1961
  }
2450
1962
  async function drainBridgeProcessStream(stream) {
2451
- try {
2452
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
2453
- while (true) {
2454
- const { done } = await reader.read();
2455
- if (done) return;
2456
- }
2457
- } catch {
2458
- }
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();
2459
1975
  }
2460
- function recordTail({
2461
- lines,
2462
- line,
2463
- limit
2464
- }) {
2465
- if (!lines) return;
2466
- lines.push(line);
2467
- while (lines.length > limit) lines.shift();
2468
- }
2469
- function lineDecoder2() {
2470
- let buffer = "";
2471
- return {
2472
- push(chunk) {
2473
- buffer += chunk;
2474
- const lines = [];
2475
- let nl;
2476
- while ((nl = buffer.indexOf("\n")) !== -1) {
2477
- const raw = buffer.slice(0, nl);
2478
- buffer = buffer.slice(nl + 1);
2479
- const line = raw.replace(/\r$/, "").trim();
2480
- if (line.length > 0) lines.push(line);
2481
- }
2482
- return lines;
2483
- },
2484
- flush() {
2485
- const line = buffer.replace(/\r$/, "").trim();
2486
- buffer = "";
2487
- return line.length > 0 ? [line] : [];
2488
- }
2489
- };
2490
- }
2491
- function sleep3(ms) {
2492
- return new Promise((resolve) => setTimeout(resolve, ms));
2493
- }
2494
-
2495
- // src/utils/resolve-sandbox-default-working-directory.ts
2496
- import { posix } from "path";
2497
- async function resolveSandboxDefaultWorkingDirectory({
2498
- sandboxSession,
2499
- abortSignal
2500
- }) {
2501
- if ("defaultWorkingDirectory" in sandboxSession) {
2502
- return sandboxSession.defaultWorkingDirectory;
2503
- }
2504
- const result = await sandboxSession.run({
2505
- command: "pwd",
2506
- abortSignal
2507
- });
2508
- if (result.exitCode !== 0) {
2509
- throw new Error(
2510
- `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
2511
- );
2512
- }
2513
- const cwd = result.stdout.trim();
2514
- if (!posix.isAbsolute(cwd)) {
2515
- throw new Error(
2516
- `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
2517
- );
2518
- }
2519
- 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;
2520
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 };
2521
2021
 
2522
- // src/utils/get-restricted-sandbox-session.ts
2523
- function getRestrictedSandboxSession(sandboxSession) {
2524
- return "restricted" in sandboxSession ? sandboxSession.restricted() : sandboxSession;
2525
- }
2526
- export {
2527
- SandboxChannel,
2528
- applyCredentialForwarding,
2529
- classifyDiskLog,
2530
- createBridgeErrorHandler,
2531
- createBridgeStartupError,
2532
- createBridgeToken,
2533
- createCredentialRequestTransformation,
2534
- createReadBridgeAsset,
2535
- createSandboxCredentialEnvironment,
2536
- drainBridgeProcessStream,
2537
- experimental_createBridgeUserMessageSubmitter,
2538
- formatBridgeError,
2539
- forwardBridgeProcessStream,
2540
- generateSandboxCredentialPlaceholder,
2541
- getAiGatewayAuthFromEnv,
2542
- getJwtExpiresAt,
2543
- getRestrictedSandboxSession,
2544
- isAccessTokenExpiringSoon,
2545
- isHarnessAuthenticationEnvironment,
2546
- isLinux,
2547
- isMacOS,
2548
- isSandboxCredentialPlaceholder,
2549
- isWindows,
2550
- logBridgeError,
2551
- markBridgeStarting,
2552
- maskSandboxCredentials,
2553
- parseJwtPayload,
2554
- readLinuxSecretServicePassword,
2555
- readMacOSKeychainPassword,
2556
- readWindowsCredentialManagerPassword,
2557
- refreshOAuthAccessToken,
2558
- resolveSandboxDefaultWorkingDirectory,
2559
- resolveSandboxHomeDir,
2560
- shellQuote,
2561
- shouldResolveNativeSubscription,
2562
- sleep,
2563
- waitForBridgeReady,
2564
- warnCredentialBrokeringUnavailable,
2565
- withBridgeToken,
2566
- writeInstructions,
2567
- writeSkills
2568
- };
2569
2022
  //# sourceMappingURL=index.js.map