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