@huaweidevtools/aishell-dsh-e2b 0.0.0-stage → 0.0.1-beta.4

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/lib/index.js ADDED
@@ -0,0 +1,2545 @@
1
+ import { Sandbox } from "e2b";
2
+ import { createCredentialsStore } from "@huaweidevtools/aishell-dsh-credentials-store";
3
+ import { FOREVER_EXPIRY, clearE2BKey, fingerprint, readE2BKey, readStoredPair, writeE2BKey } from "@huaweidevtools/aishell-dsh-credentials-store/accounts";
4
+ import { sign } from "@huaweidevtools/aishell-dsh-credentials-store/signer";
5
+ import { defineTool } from "@deepseek-ai/dsh-tools";
6
+
7
+ //#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.5/node_modules/@deepseek-ai/cosmokit/lib/index.js
8
+ /** Return true when a value is `null` or `undefined`. */
9
+ function isNullable(value) {
10
+ return value === null || value === void 0;
11
+ }
12
+ /** Return true for non-array object values. */
13
+ function isPlainObject(data) {
14
+ return data && typeof data === "object" && !Array.isArray(data);
15
+ }
16
+ /** Filter object entries and return a new object. */
17
+ function filterKeys(object, filter) {
18
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
19
+ }
20
+ /** Map object values while preserving the original key set. */
21
+ function mapValues(object, transform) {
22
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
23
+ }
24
+ /** Pick selected keys from an object, optionally including `undefined` values. */
25
+ function pick(source, keys, forced) {
26
+ if (!keys) return { ...source };
27
+ const result = {};
28
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
29
+ return result;
30
+ }
31
+ /** Shared config references used by schema validators and plugin runtimes. */
32
+ const write = Symbol.for("cosmokit.volatile.write");
33
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
34
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
35
+ if (value === null || typeof value !== "object") return value;
36
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
37
+ ancestors.add(value);
38
+ try {
39
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
40
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
41
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
42
+ } finally {
43
+ ancestors.delete(value);
44
+ }
45
+ }
46
+ /**
47
+ * Create a detached reference containing an immutable copy of the supplied data.
48
+ * @param value - validated config data; class instances and functions are unsupported.
49
+ * @returns a reference whose value is updated only by its owning runtime.
50
+ */
51
+ function createVolatile(value) {
52
+ let current = snapshot(value);
53
+ return Object.freeze({
54
+ get: () => current,
55
+ [write]: (value$1) => {
56
+ current = value$1;
57
+ }
58
+ });
59
+ }
60
+ /**
61
+ * Identify references across ESM/CJS copies of the shared library.
62
+ * @param value - a parsed config value.
63
+ * @returns whether the value implements the shared reference protocol.
64
+ */
65
+ function isVolatile(value) {
66
+ return typeof value === "object" && value !== null && write in value;
67
+ }
68
+ /** Test values using `instanceof` with a `toStringTag` fallback. */
69
+ function is(type, value) {
70
+ if (arguments.length === 1) return (value$1) => is(type, value$1);
71
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
72
+ }
73
+ function isArrayBufferLike(value) {
74
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
75
+ }
76
+ function isArrayBufferSource(value) {
77
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
78
+ }
79
+ /** Binary source detection and base64/hex conversion helpers. */
80
+ var Binary;
81
+ (function(Binary$1) {
82
+ Binary$1.is = isArrayBufferLike;
83
+ Binary$1.isSource = isArrayBufferSource;
84
+ function fromSource(source) {
85
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
86
+ else return source;
87
+ }
88
+ Binary$1.fromSource = fromSource;
89
+ function toBase64(source) {
90
+ source = fromSource(source);
91
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
92
+ let binary = "";
93
+ const bytes = new Uint8Array(source);
94
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
95
+ return btoa(binary);
96
+ }
97
+ Binary$1.toBase64 = toBase64;
98
+ function fromBase64(source) {
99
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
100
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
101
+ }
102
+ Binary$1.fromBase64 = fromBase64;
103
+ function toHex(source) {
104
+ source = fromSource(source);
105
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
106
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
107
+ }
108
+ Binary$1.toHex = toHex;
109
+ function fromHex(source) {
110
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
111
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
112
+ const buffer = [];
113
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
114
+ return Uint8Array.from(buffer).buffer;
115
+ }
116
+ Binary$1.fromHex = fromHex;
117
+ })(Binary || (Binary = {}));
118
+ /** Decode a base64 string into binary data. */
119
+ const base64ToArrayBuffer = Binary.fromBase64;
120
+ /** Encode binary data as base64. */
121
+ const arrayBufferToBase64 = Binary.toBase64;
122
+ /** Decode a hex string into binary data. */
123
+ const hexToArrayBuffer = Binary.fromHex;
124
+ /** Encode binary data as hex. */
125
+ const arrayBufferToHex = Binary.toHex;
126
+ /** Deep-clone common JavaScript values while preserving prototypes and cycles. */
127
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
128
+ if (!source || typeof source !== "object") return source;
129
+ if (is("Date", source)) return new Date(source.valueOf());
130
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
131
+ if (isArrayBufferLike(source)) return source.slice(0);
132
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
133
+ const cached = refs.get(source);
134
+ if (cached) return cached;
135
+ if (Array.isArray(source)) {
136
+ const result$1 = [];
137
+ refs.set(source, result$1);
138
+ source.forEach((value, index) => {
139
+ result$1[index] = Reflect.apply(clone, null, [value, refs]);
140
+ });
141
+ return result$1;
142
+ }
143
+ const result = Object.create(Object.getPrototypeOf(source));
144
+ refs.set(source, result);
145
+ for (const key of Reflect.ownKeys(source)) {
146
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
147
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
148
+ Reflect.defineProperty(result, key, descriptor);
149
+ }
150
+ return result;
151
+ }
152
+ /**
153
+ * Compare values recursively, treating two volatile references as equal regardless of value.
154
+ * Strict comparison distinguishes null/undefined, treats opaque objects by identity,
155
+ * compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
156
+ * @param a - first value.
157
+ * @param b - second value.
158
+ * @param strict - whether to require strict data equality outside volatile references.
159
+ * @returns whether the values compare equal.
160
+ */
161
+ function deepEqual(a, b, strict) {
162
+ const ancestors = /* @__PURE__ */ new Set();
163
+ function compare(a$1, b$1) {
164
+ if (a$1 === b$1) return true;
165
+ if (isVolatile(a$1) || isVolatile(b$1)) return isVolatile(a$1) && isVolatile(b$1);
166
+ if (!strict && isNullable(a$1) && isNullable(b$1)) return true;
167
+ if (typeof a$1 !== typeof b$1 || typeof a$1 !== "object" || !a$1 || !b$1) return false;
168
+ if (ancestors.has(a$1)) return false;
169
+ function check(test, then) {
170
+ return test(a$1) ? test(b$1) ? then(a$1, b$1) : false : test(b$1) ? false : void 0;
171
+ }
172
+ ancestors.add(a$1);
173
+ try {
174
+ return check(Array.isArray, (a$2, b$2) => {
175
+ if (a$2.length !== b$2.length) return false;
176
+ for (let index = 0; index < a$2.length; index++) if (!compare(a$2[index], b$2[index])) return false;
177
+ return true;
178
+ }) ?? check(is("Date"), (a$2, b$2) => a$2.valueOf() === b$2.valueOf()) ?? check(is("URL"), (a$2, b$2) => a$2.href === b$2.href) ?? check(is("RegExp"), (a$2, b$2) => a$2.source === b$2.source && a$2.flags === b$2.flags) ?? check(isArrayBufferLike, (a$2, b$2) => {
179
+ if (a$2.byteLength !== b$2.byteLength) return false;
180
+ const viewA = new Uint8Array(a$2);
181
+ const viewB = new Uint8Array(b$2);
182
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
183
+ return true;
184
+ }) ?? ((!strict || [a$1, b$1].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
185
+ ...a$1,
186
+ ...b$1
187
+ }).every((key) => compare(a$1[key], b$1[key])));
188
+ } finally {
189
+ ancestors.delete(a$1);
190
+ }
191
+ }
192
+ return compare(a, b);
193
+ }
194
+ /** Time constants plus parsing and formatting helpers. */
195
+ var Time;
196
+ (function(Time$1) {
197
+ Time$1.millisecond = 1;
198
+ Time$1.second = 1e3;
199
+ Time$1.minute = Time$1.second * 60;
200
+ Time$1.hour = Time$1.minute * 60;
201
+ Time$1.day = Time$1.hour * 24;
202
+ Time$1.week = Time$1.day * 7;
203
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
204
+ function setTimezoneOffset(offset) {
205
+ timezoneOffset = offset;
206
+ }
207
+ Time$1.setTimezoneOffset = setTimezoneOffset;
208
+ function getTimezoneOffset() {
209
+ return timezoneOffset;
210
+ }
211
+ Time$1.getTimezoneOffset = getTimezoneOffset;
212
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
213
+ if (typeof date === "number") date = new Date(date);
214
+ if (offset === void 0) offset = timezoneOffset;
215
+ return Math.floor((date.valueOf() / Time$1.minute - offset) / 1440);
216
+ }
217
+ Time$1.getDateNumber = getDateNumber;
218
+ function fromDateNumber(value, offset) {
219
+ const date = new Date(value * Time$1.day);
220
+ if (offset === void 0) offset = timezoneOffset;
221
+ return new Date(+date + offset * Time$1.minute);
222
+ }
223
+ Time$1.fromDateNumber = fromDateNumber;
224
+ const numeric = /\d+(?:\.\d+)?/.source;
225
+ const timeRegExp = new RegExp(`^${[
226
+ "w(?:eek(?:s)?)?",
227
+ "d(?:ay(?:s)?)?",
228
+ "h(?:our(?:s)?)?",
229
+ "m(?:in(?:ute)?(?:s)?)?",
230
+ "s(?:ec(?:ond)?(?:s)?)?"
231
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
232
+ function parseTime(source) {
233
+ const capture = timeRegExp.exec(source);
234
+ if (!capture) return 0;
235
+ return (parseFloat(capture[1]) * Time$1.week || 0) + (parseFloat(capture[2]) * Time$1.day || 0) + (parseFloat(capture[3]) * Time$1.hour || 0) + (parseFloat(capture[4]) * Time$1.minute || 0) + (parseFloat(capture[5]) * Time$1.second || 0);
236
+ }
237
+ Time$1.parseTime = parseTime;
238
+ function parseDate(date) {
239
+ const parsed = parseTime(date);
240
+ if (parsed) date = Date.now() + parsed;
241
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
242
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
243
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
244
+ }
245
+ Time$1.parseDate = parseDate;
246
+ function format(ms) {
247
+ const abs = Math.abs(ms);
248
+ if (abs >= Time$1.day - Time$1.hour / 2) return Math.round(ms / Time$1.day) + "d";
249
+ else if (abs >= Time$1.hour - Time$1.minute / 2) return Math.round(ms / Time$1.hour) + "h";
250
+ else if (abs >= Time$1.minute - Time$1.second / 2) return Math.round(ms / Time$1.minute) + "m";
251
+ else if (abs >= Time$1.second) return Math.round(ms / Time$1.second) + "s";
252
+ return ms + "ms";
253
+ }
254
+ Time$1.format = format;
255
+ function toDigits(source, length = 2) {
256
+ return source.toString().padStart(length, "0");
257
+ }
258
+ Time$1.toDigits = toDigits;
259
+ function template(template$1, time = /* @__PURE__ */ new Date()) {
260
+ return template$1.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
261
+ }
262
+ Time$1.template = template;
263
+ })(Time || (Time = {}));
264
+
265
+ //#endregion
266
+ //#region ../../node_modules/.pnpm/@deepseek-ai+schemastery@3.18.4/node_modules/@deepseek-ai/schemastery/lib/index.mjs
267
+ const kSchema = Symbol.for("schemastery");
268
+ const kValidationError = Symbol.for("ValidationError");
269
+ globalThis.__schemastery_index__ ??= 0;
270
+ globalThis.__schemastery_refs__ = void 0;
271
+ var ValidationError = class extends TypeError {
272
+ options;
273
+ name = "ValidationError";
274
+ constructor(message, options) {
275
+ let prefix = "$";
276
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
277
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
278
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
279
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
280
+ super((prefix === "$" ? "" : `${prefix} `) + message);
281
+ this.options = options;
282
+ }
283
+ static is(error) {
284
+ return !!error?.[kValidationError];
285
+ }
286
+ };
287
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
288
+ const Schema = function(options) {
289
+ const schema = function(data, options$1 = {}) {
290
+ return Schema.resolve(data, schema, options$1)[0];
291
+ };
292
+ if (options.refs) {
293
+ const refs = mapValues(options.refs, (options$1) => new Schema(options$1));
294
+ const getRef = (uid) => refs[uid];
295
+ for (const key in refs) {
296
+ const options$1 = refs[key];
297
+ options$1.sKey = getRef(options$1.sKey);
298
+ options$1.inner = getRef(options$1.inner);
299
+ options$1.list = options$1.list && options$1.list.map(getRef);
300
+ options$1.dict = options$1.dict && mapValues(options$1.dict, getRef);
301
+ }
302
+ return refs[options.uid];
303
+ }
304
+ Object.assign(schema, options);
305
+ if (typeof schema.callback === "string") try {
306
+ schema.callback = new Function("return " + schema.callback)();
307
+ } catch {}
308
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
309
+ Object.setPrototypeOf(schema, Schema.prototype);
310
+ schema.meta ||= {};
311
+ schema.toString = schema.toString.bind(schema);
312
+ return schema;
313
+ };
314
+ Schema.prototype = Object.create(Function.prototype);
315
+ Schema.prototype[kSchema] = true;
316
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
317
+ return {
318
+ version: 1,
319
+ vendor: "schemastery",
320
+ validate: (value) => {
321
+ try {
322
+ return { value: Schema.resolve(value, this, {})[0] };
323
+ } catch (error) {
324
+ if (ValidationError.is(error)) return { issues: [{
325
+ message: error.message,
326
+ path: error.options.path
327
+ }] };
328
+ throw error;
329
+ }
330
+ }
331
+ };
332
+ } });
333
+ Schema.ValidationError = ValidationError;
334
+ Schema.prototype.toJSON = function toJSON() {
335
+ if (globalThis.__schemastery_refs__) {
336
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
337
+ return this.uid;
338
+ }
339
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
340
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
341
+ const result = {
342
+ uid: this.uid,
343
+ refs: globalThis.__schemastery_refs__
344
+ };
345
+ globalThis.__schemastery_refs__ = void 0;
346
+ return result;
347
+ };
348
+ Schema.prototype.set = function set(key, value) {
349
+ this.dict[key] = value;
350
+ return this;
351
+ };
352
+ Schema.prototype.push = function push(value) {
353
+ this.list.push(value);
354
+ return this;
355
+ };
356
+ function mergeDesc(original, messages) {
357
+ const result = typeof original === "string" ? { "": original } : { ...original };
358
+ for (const locale in messages) {
359
+ const value = messages[locale];
360
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
361
+ else if (typeof value === "string") result[locale] = value;
362
+ }
363
+ return result;
364
+ }
365
+ function getInner(value) {
366
+ return value?.$value ?? value?.$inner;
367
+ }
368
+ function extractKeys(data) {
369
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
370
+ }
371
+ Schema.prototype.i18n = function i18n(messages) {
372
+ const schema = Schema(this);
373
+ const desc = mergeDesc(schema.meta.description, messages);
374
+ if (Object.keys(desc).length) schema.meta.description = desc;
375
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
376
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
377
+ });
378
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
379
+ return inner.i18n(mapValues(messages, (data = {}) => {
380
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
381
+ if (Array.isArray(data)) return data[index];
382
+ return extractKeys(data);
383
+ }));
384
+ });
385
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
386
+ if (getInner(data)) return getInner(data);
387
+ return extractKeys(data);
388
+ }));
389
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
390
+ return schema;
391
+ };
392
+ Schema.prototype.extra = function extra(key, value) {
393
+ const schema = Schema(this);
394
+ schema.meta = {
395
+ ...schema.meta,
396
+ [key]: value
397
+ };
398
+ return schema;
399
+ };
400
+ for (const key of [
401
+ "required",
402
+ "disabled",
403
+ "collapse",
404
+ "hidden",
405
+ "loose"
406
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
407
+ const schema = Schema(this);
408
+ schema.meta = {
409
+ ...schema.meta,
410
+ [key]: value
411
+ };
412
+ return schema;
413
+ } });
414
+ Schema.prototype.deprecated = function deprecated() {
415
+ const schema = Schema(this);
416
+ schema.meta.badges ||= [];
417
+ schema.meta.badges.push({
418
+ text: "deprecated",
419
+ type: "danger"
420
+ });
421
+ return schema;
422
+ };
423
+ Schema.prototype.experimental = function experimental() {
424
+ const schema = Schema(this);
425
+ schema.meta.badges ||= [];
426
+ schema.meta.badges.push({
427
+ text: "experimental",
428
+ type: "warning"
429
+ });
430
+ return schema;
431
+ };
432
+ Schema.prototype.pattern = function pattern(regexp) {
433
+ const schema = Schema(this);
434
+ const pattern$1 = pick(regexp, ["source", "flags"]);
435
+ schema.meta = {
436
+ ...schema.meta,
437
+ pattern: pattern$1
438
+ };
439
+ return schema;
440
+ };
441
+ Schema.prototype.simplify = function simplify(value) {
442
+ if (isVolatile(value)) value = value.get();
443
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
444
+ if (isNullable(value)) return value;
445
+ if (this.type === "object" || this.type === "dict") {
446
+ const result = {};
447
+ for (const key in value) {
448
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
449
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
450
+ }
451
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
452
+ return result;
453
+ } else if (this.type === "array" || this.type === "tuple") {
454
+ const result = [];
455
+ value.forEach((value$1, index) => {
456
+ const schema = this.type === "array" ? this.inner : this.list[index];
457
+ const item = schema ? schema.simplify(value$1) : value$1;
458
+ result.push(item);
459
+ });
460
+ return result;
461
+ } else if (this.type === "intersect") {
462
+ const result = {};
463
+ for (const item of this.list) Object.assign(result, item.simplify(value));
464
+ return result;
465
+ } else if (this.type === "union") for (const schema of this.list) try {
466
+ Schema.resolve(value, schema, {});
467
+ return schema.simplify(value);
468
+ } catch {}
469
+ return value;
470
+ };
471
+ Schema.prototype.toString = function toString(inline) {
472
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
473
+ };
474
+ Schema.prototype.role = function role(role$1, extra) {
475
+ const schema = Schema(this);
476
+ schema.meta = {
477
+ ...schema.meta,
478
+ role: role$1,
479
+ extra
480
+ };
481
+ return schema;
482
+ };
483
+ for (const key of [
484
+ "default",
485
+ "link",
486
+ "comment",
487
+ "description",
488
+ "max",
489
+ "min",
490
+ "step"
491
+ ]) Object.assign(Schema.prototype, { [key](value) {
492
+ const schema = Schema(this);
493
+ schema.meta = {
494
+ ...schema.meta,
495
+ [key]: value
496
+ };
497
+ return schema;
498
+ } });
499
+ Schema.prototype.volatile = function volatile() {
500
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
501
+ return this.extra("volatile", true);
502
+ };
503
+ const resolvers = {};
504
+ const checkedVolatile = Symbol("checked-volatile-schema");
505
+ function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
506
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
507
+ if (states.has(blocked)) return;
508
+ states.add(blocked);
509
+ seen.set(schema, states);
510
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
511
+ const nested = blocked || !!schema.meta?.volatile;
512
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
513
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
514
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
515
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
516
+ }
517
+ Schema.extend = function extend(type, resolve) {
518
+ resolvers[type] = resolve;
519
+ };
520
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
521
+ if (!schema) return [data];
522
+ if (!options[checkedVolatile]) {
523
+ validateVolatileSchema(schema, options.path);
524
+ options = {
525
+ ...options,
526
+ [checkedVolatile]: true
527
+ };
528
+ }
529
+ if (schema.meta?.volatile) {
530
+ const inner = Schema(schema);
531
+ inner.meta = {
532
+ ...schema.meta,
533
+ volatile: false
534
+ };
535
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
536
+ try {
537
+ return [createVolatile(value), adapted];
538
+ } catch (error) {
539
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
540
+ }
541
+ }
542
+ if (options.ignore?.(data, schema)) return [data];
543
+ if (isNullable(data) && schema.type !== "lazy") {
544
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
545
+ let current = schema;
546
+ let fallback = schema.meta.default;
547
+ while (current?.type === "intersect" && isNullable(fallback)) {
548
+ current = current.list[0];
549
+ fallback = current?.meta.default;
550
+ }
551
+ if (isNullable(fallback)) return [data];
552
+ data = clone(fallback);
553
+ }
554
+ const callback = resolvers[schema.type];
555
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
556
+ try {
557
+ return callback(data, schema, options, strict);
558
+ } catch (error) {
559
+ if (!schema.meta.loose) throw error;
560
+ return [schema.meta.default];
561
+ }
562
+ };
563
+ Schema.from = function from(source) {
564
+ if (isNullable(source)) return Schema.any();
565
+ else if ([
566
+ "string",
567
+ "number",
568
+ "boolean"
569
+ ].includes(typeof source)) return Schema.const(source).required();
570
+ else if (source[kSchema]) return source;
571
+ else if (typeof source === "function") switch (source) {
572
+ case String: return Schema.string().required();
573
+ case Number: return Schema.number().required();
574
+ case Boolean: return Schema.boolean().required();
575
+ case Function: return Schema.function().required();
576
+ default: return Schema.is(source).required();
577
+ }
578
+ else throw new TypeError(`cannot infer schema from ${source}`);
579
+ };
580
+ Schema.lazy = function lazy(builder) {
581
+ const toJSON = () => {
582
+ if (!schema.inner[kSchema]) {
583
+ schema.inner = schema.builder();
584
+ schema.inner.meta = {
585
+ ...schema.meta,
586
+ ...schema.inner.meta
587
+ };
588
+ }
589
+ return schema.inner.toJSON();
590
+ };
591
+ const schema = new Schema({
592
+ type: "lazy",
593
+ builder,
594
+ inner: { toJSON }
595
+ });
596
+ return schema;
597
+ };
598
+ Schema.natural = function natural() {
599
+ return Schema.number().step(1).min(0);
600
+ };
601
+ Schema.percent = function percent() {
602
+ return Schema.number().step(.01).min(0).max(1).role("slider");
603
+ };
604
+ Schema.date = function date() {
605
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
606
+ const date$1 = new Date(value);
607
+ if (isNaN(+date$1)) throw new ValidationError(`invalid date "${value}"`, options);
608
+ return date$1;
609
+ }, true)]);
610
+ };
611
+ Schema.regExp = function regExp(flag = "") {
612
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
613
+ try {
614
+ return new RegExp(value, flag);
615
+ } catch (e) {
616
+ throw new ValidationError(e.message, options);
617
+ }
618
+ }, true)]);
619
+ };
620
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
621
+ return Schema.union([
622
+ Schema.is(ArrayBuffer),
623
+ Schema.is(SharedArrayBuffer),
624
+ Schema.transform(Schema.any(), (value, options) => {
625
+ if (Binary.isSource(value)) return Binary.fromSource(value);
626
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
627
+ }, true),
628
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
629
+ try {
630
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
631
+ } catch (e) {
632
+ throw new ValidationError(e.message, options);
633
+ }
634
+ }, true)] : []
635
+ ]);
636
+ };
637
+ Schema.extend("lazy", (data, schema, options, strict) => {
638
+ if (!schema.inner[kSchema]) {
639
+ schema.inner = schema.builder();
640
+ schema.inner.meta = {
641
+ ...schema.meta,
642
+ ...schema.inner.meta
643
+ };
644
+ validateVolatileSchema(schema.inner, options.path, true);
645
+ }
646
+ return Schema.resolve(data, schema.inner, options, strict);
647
+ });
648
+ Schema.extend("any", (data) => {
649
+ return [data];
650
+ });
651
+ Schema.extend("never", (data, _, options) => {
652
+ throw new ValidationError(`expected nullable but got ${data}`, options);
653
+ });
654
+ Schema.extend("const", (data, { value }, options) => {
655
+ if (deepEqual(data, value)) return [value];
656
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
657
+ });
658
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
659
+ const { max = Infinity, min = -Infinity } = meta;
660
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
661
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
662
+ }
663
+ Schema.extend("string", (data, { meta }, options) => {
664
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
665
+ if (meta.pattern) {
666
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
667
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
668
+ }
669
+ checkWithinRange(data.length, meta, "string length", options);
670
+ return [data];
671
+ });
672
+ function decimalShift(data, digits) {
673
+ const str = data.toString();
674
+ if (str.includes("e")) return data * Math.pow(10, digits);
675
+ const index = str.indexOf(".");
676
+ if (index === -1) return data * Math.pow(10, digits);
677
+ const frac = str.slice(index + 1);
678
+ const integer = str.slice(0, index);
679
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
680
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
681
+ }
682
+ function isMultipleOf(data, min, step) {
683
+ step = Math.abs(step);
684
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
685
+ const index = step.toString().indexOf(".");
686
+ const digits = step.toString().slice(index + 1).length;
687
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
688
+ }
689
+ Schema.extend("number", (data, { meta }, options) => {
690
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
691
+ checkWithinRange(data, meta, "number", options);
692
+ const { step } = meta;
693
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
694
+ return [data];
695
+ });
696
+ Schema.extend("boolean", (data, _, options) => {
697
+ if (typeof data === "boolean") return [data];
698
+ throw new ValidationError(`expected boolean but got ${data}`, options);
699
+ });
700
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
701
+ let value = 0, keys = [];
702
+ if (typeof data === "number") {
703
+ value = data;
704
+ for (const key in bits) if (data & bits[key]) keys.push(key);
705
+ } else if (Array.isArray(data)) {
706
+ keys = data;
707
+ for (const key of keys) {
708
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
709
+ if (key in bits) value |= bits[key];
710
+ }
711
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
712
+ if (value === meta.default) return [value];
713
+ return [value, keys];
714
+ });
715
+ Schema.extend("function", (data, _, options) => {
716
+ if (typeof data === "function") return [data];
717
+ throw new ValidationError(`expected function but got ${data}`, options);
718
+ });
719
+ Schema.extend("is", (data, { constructor }, options) => {
720
+ if (typeof constructor === "function") {
721
+ if (data instanceof constructor) return [data];
722
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
723
+ } else {
724
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
725
+ let prototype = Object.getPrototypeOf(data);
726
+ while (prototype) {
727
+ if (prototype.constructor?.name === constructor) return [data];
728
+ prototype = Object.getPrototypeOf(prototype);
729
+ }
730
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
731
+ }
732
+ });
733
+ function property(data, key, schema, options) {
734
+ try {
735
+ const [value, adapted] = Schema.resolve(data[key], schema, {
736
+ ...options,
737
+ path: [...options.path || [], key]
738
+ });
739
+ if (adapted !== void 0) data[key] = adapted;
740
+ return value;
741
+ } catch (e) {
742
+ if (!options?.autofix) throw e;
743
+ delete data[key];
744
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
745
+ }
746
+ }
747
+ Schema.extend("array", (data, { inner, meta }, options) => {
748
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
749
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
750
+ return [data.map((_, index) => property(data, index, inner, options))];
751
+ });
752
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
753
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
754
+ const result = {};
755
+ for (const key in data) {
756
+ let rKey;
757
+ try {
758
+ rKey = Schema.resolve(key, sKey, options)[0];
759
+ } catch (error) {
760
+ if (strict) continue;
761
+ throw error;
762
+ }
763
+ result[rKey] = property(data, key, inner, options);
764
+ data[rKey] = data[key];
765
+ if (key !== rKey) delete data[key];
766
+ }
767
+ return [result];
768
+ });
769
+ Schema.extend("tuple", (data, { list }, options, strict) => {
770
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
771
+ const result = list.map((inner, index) => property(data, index, inner, options));
772
+ if (strict) return [result];
773
+ result.push(...data.slice(list.length));
774
+ return [result];
775
+ });
776
+ function merge(result, data) {
777
+ for (const key in data) {
778
+ if (key in result) continue;
779
+ result[key] = data[key];
780
+ }
781
+ }
782
+ Schema.extend("object", (data, { dict }, options, strict) => {
783
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
784
+ const result = {};
785
+ for (const key in dict) {
786
+ const value = property(data, key, dict[key], options);
787
+ if (!isNullable(value) || key in data) result[key] = value;
788
+ }
789
+ if (!strict) merge(result, data);
790
+ return [result];
791
+ });
792
+ Schema.extend("union", (data, { list, toString }, options, strict) => {
793
+ const messages = [];
794
+ for (const inner of list) try {
795
+ return Schema.resolve(data, inner, options, strict);
796
+ } catch (error) {
797
+ messages.push(error);
798
+ }
799
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
800
+ });
801
+ Schema.extend("intersect", (data, { list, toString }, options, strict) => {
802
+ if (!list.length) return [data];
803
+ let result;
804
+ for (const inner of list) {
805
+ const value = Schema.resolve(data, inner, options, true)[0];
806
+ if (isNullable(value)) continue;
807
+ if (isNullable(result)) result = value;
808
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
809
+ else if (typeof value === "object") merge(result ??= {}, value);
810
+ else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
811
+ }
812
+ if (!strict && isPlainObject(data)) merge(result, data);
813
+ return [result];
814
+ });
815
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
816
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
817
+ if (preserve) return [callback(result)];
818
+ else return [callback(result), callback(adapted)];
819
+ });
820
+ const formatters = {};
821
+ function defineMethod(name$1, keys, format) {
822
+ formatters[name$1] = format;
823
+ Object.assign(Schema, { [name$1](...args) {
824
+ const schema = new Schema({ type: name$1 });
825
+ keys.forEach((key, index) => {
826
+ switch (key) {
827
+ case "sKey":
828
+ schema.sKey = args[index] ?? Schema.string();
829
+ break;
830
+ case "inner":
831
+ schema.inner = Schema.from(args[index]);
832
+ break;
833
+ case "list":
834
+ schema.list = args[index].map(Schema.from);
835
+ break;
836
+ case "dict":
837
+ schema.dict = mapValues(args[index], Schema.from);
838
+ break;
839
+ case "bits":
840
+ schema.bits = {};
841
+ for (const key$1 in args[index]) {
842
+ if (typeof args[index][key$1] !== "number") continue;
843
+ schema.bits[key$1] = args[index][key$1];
844
+ }
845
+ break;
846
+ case "callback": {
847
+ const callback = schema.callback = args[index];
848
+ callback["toJSON"] ||= () => callback.toString();
849
+ break;
850
+ }
851
+ case "constructor": {
852
+ const constructor = schema.constructor = args[index];
853
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
854
+ break;
855
+ }
856
+ default: schema[key] = args[index];
857
+ }
858
+ });
859
+ if (name$1 === "object" || name$1 === "dict") schema.meta.default = {};
860
+ else if (name$1 === "array" || name$1 === "tuple") schema.meta.default = [];
861
+ else if (name$1 === "bitset") schema.meta.default = 0;
862
+ return schema;
863
+ } });
864
+ }
865
+ defineMethod("is", ["constructor"], ({ constructor }) => {
866
+ if (typeof constructor === "function") return constructor.name;
867
+ else return constructor;
868
+ });
869
+ defineMethod("any", [], () => "any");
870
+ defineMethod("never", [], () => "never");
871
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
872
+ defineMethod("string", [], () => "string");
873
+ defineMethod("number", [], () => "number");
874
+ defineMethod("boolean", [], () => "boolean");
875
+ defineMethod("bitset", ["bits"], () => "bitset");
876
+ defineMethod("function", [], () => "function");
877
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
878
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
879
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
880
+ defineMethod("object", ["dict"], ({ dict }) => {
881
+ if (Object.keys(dict).length === 0) return "{}";
882
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
883
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
884
+ }).join(", ")} }`;
885
+ });
886
+ defineMethod("union", ["list"], ({ list }, inline) => {
887
+ const result = list.map(({ toString: format }) => format()).join(" | ");
888
+ return inline ? `(${result})` : result;
889
+ });
890
+ defineMethod("intersect", ["list"], ({ list }) => {
891
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
892
+ });
893
+ defineMethod("transform", [
894
+ "inner",
895
+ "callback",
896
+ "preserve"
897
+ ], ({ inner }, isInner) => inner.toString(isInner));
898
+
899
+ //#endregion
900
+ //#region src/client.ts
901
+ /**
902
+ * How many pages of a list response to follow before giving up.
903
+ *
904
+ * The gateway pages at 100 and reports more with a cursor, so this is a bound
905
+ * of 1000 sandboxes. It exists to stop a filter that matches nothing from
906
+ * walking an entire account one page at a time; no deployment this plugin is
907
+ * used against should reach it.
908
+ */
909
+ const MAX_LIST_PAGES = 10;
910
+ /**
911
+ * The template to start from when the caller names none.
912
+ *
913
+ * Not the SDK's own default. The SDK resolves an omitted template to `"base"`,
914
+ * a name from the hosted service; this gateway's templates are its own, and it
915
+ * answers `"base"` with `404 sandbox unknown was not found` — a message that
916
+ * reads like a missing sandbox rather than an unknown template, which is what
917
+ * makes it worth pinning here rather than discovering at the first create.
918
+ *
919
+ * `"default"` is what this gateway provisions.
920
+ */
921
+ const DEFAULT_TEMPLATE = "default";
922
+ /** Builds and caches SDK handles, one per sandbox id. */
923
+ var SandboxClient = class {
924
+ constructor(options) {
925
+ this.handles = new Map();
926
+ this.options = options;
927
+ }
928
+ /**
929
+ * The live handle for one sandbox.
930
+ *
931
+ * @param sandboxId - the sandbox to address.
932
+ * @returns the cached handle, or a newly connected one.
933
+ */
934
+ async handle(sandboxId) {
935
+ const cached = this.handles.get(sandboxId);
936
+ if (cached !== void 0) return cached;
937
+ const connected = await this.connect(sandboxId);
938
+ this.handles.set(sandboxId, connected);
939
+ return connected;
940
+ }
941
+ /**
942
+ * Create a sandbox and cache its handle.
943
+ *
944
+ * @param template - the template name or id; {@link DEFAULT_TEMPLATE} when
945
+ * omitted.
946
+ * @param options - create options forwarded to the SDK.
947
+ * @returns the created sandbox handle.
948
+ */
949
+ async create(template, options) {
950
+ const common = {
951
+ domain: this.options.resolveDomain(),
952
+ requestTimeoutMs: this.options.resolveRequestTimeoutMs(),
953
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
954
+ ...options.envs === void 0 ? {} : { envs: options.envs },
955
+ ...options.metadata === void 0 ? {} : { metadata: options.metadata }
956
+ };
957
+ const resolved = template ?? DEFAULT_TEMPLATE;
958
+ const sandbox = await this.runWithKeyRefresh(options.signal, (apiKey, signal) => Sandbox.create(resolved, {
959
+ ...common,
960
+ apiKey,
961
+ ...signal === void 0 ? {} : { signal }
962
+ }));
963
+ this.handles.set(sandbox.sandboxId, sandbox);
964
+ return sandbox;
965
+ }
966
+ /**
967
+ * Find the sandboxes carrying one metadata pair.
968
+ *
969
+ * The SDK encodes the pair into the `metadata` query parameter and the
970
+ * gateway forwards it to the upstream unchanged, so this is the mechanism a
971
+ * restarted process uses to rediscover the sandbox a conversation was
972
+ * working in.
973
+ *
974
+ * The search follows the pagination cursor. A single page would be enough
975
+ * for the common case, but not for a busy account: the gateway pages at 100
976
+ * and reports more with a cursor, and a conversation's own sandbox is not
977
+ * ordered ahead of the others — so on the day an account holds more than a
978
+ * page, the one sandbox the caller is looking for is as likely as any to be
979
+ * on a later page, and a single-page search would report "none".
980
+ *
981
+ * @param metadata - the pair to match; a sandbox must carry all of it.
982
+ * @param signal - caller cancellation.
983
+ * @returns one row per matching sandbox.
984
+ */
985
+ async findByMetadata(metadata, signal) {
986
+ const domain = this.options.resolveDomain();
987
+ const requestTimeoutMs = this.options.resolveRequestTimeoutMs();
988
+ const items = await this.runWithKeyRefresh(signal, async (apiKey) => {
989
+ const paginator = Sandbox.list({
990
+ apiKey,
991
+ domain,
992
+ requestTimeoutMs,
993
+ query: { metadata }
994
+ });
995
+ const found = [];
996
+ for (let page = 0; page < MAX_LIST_PAGES && paginator.hasNext; page += 1) found.push(...await paginator.nextItems());
997
+ return found;
998
+ });
999
+ return items.map((item) => ({
1000
+ sandboxId: item.sandboxId,
1001
+ state: item.state
1002
+ }));
1003
+ }
1004
+ /**
1005
+ * List the sandboxes this account currently has.
1006
+ *
1007
+ * Follows the pagination cursor, for the same reason the metadata search
1008
+ * does: the gateway pages at 100, and a caller asking what it has should not
1009
+ * be told "these are all of them" when they are the first hundred.
1010
+ *
1011
+ * @param signal - caller cancellation.
1012
+ * @returns one row per sandbox, in the shape the tool renders.
1013
+ */
1014
+ async list(signal) {
1015
+ const domain = this.options.resolveDomain();
1016
+ const requestTimeoutMs = this.options.resolveRequestTimeoutMs();
1017
+ const items = await this.runWithKeyRefresh(signal, async (apiKey) => {
1018
+ const paginator = Sandbox.list({
1019
+ apiKey,
1020
+ domain,
1021
+ requestTimeoutMs
1022
+ });
1023
+ const all = [];
1024
+ for (let page = 0; page < MAX_LIST_PAGES && paginator.hasNext; page += 1) all.push(...await paginator.nextItems());
1025
+ return all;
1026
+ });
1027
+ return items.map((item) => ({
1028
+ sandboxId: item.sandboxId,
1029
+ templateId: item.templateId,
1030
+ state: item.state,
1031
+ startedAt: item.startedAt.toISOString(),
1032
+ endAt: item.endAt.toISOString()
1033
+ }));
1034
+ }
1035
+ /**
1036
+ * Drop a handle for a sandbox that no longer exists.
1037
+ *
1038
+ * Called after a kill, so a later call against the same id re-resolves
1039
+ * rather than reusing an endpoint the gateway has already torn down.
1040
+ *
1041
+ * @param sandboxId - the sandbox whose handle to drop.
1042
+ */
1043
+ forget(sandboxId) {
1044
+ this.handles.delete(sandboxId);
1045
+ }
1046
+ /** Connect to an existing sandbox. */
1047
+ async connect(sandboxId) {
1048
+ const domain = this.options.resolveDomain();
1049
+ const requestTimeoutMs = this.options.resolveRequestTimeoutMs();
1050
+ return this.runWithKeyRefresh(void 0, (apiKey, signal) => Sandbox.connect(sandboxId, {
1051
+ apiKey,
1052
+ domain,
1053
+ requestTimeoutMs,
1054
+ ...signal === void 0 ? {} : { signal }
1055
+ }));
1056
+ }
1057
+ /**
1058
+ * Run one SDK call, retrying once against a freshly fetched key when the
1059
+ * gateway rejects the one in hand.
1060
+ *
1061
+ * The key is handed to the callback rather than captured by it. A callback
1062
+ * that closed over the first key would "retry" by presenting that same
1063
+ * rejected key again — a loop that looks like a refresh and is not one.
1064
+ *
1065
+ * The retry is deliberately narrow: only an authentication failure clears
1066
+ * the cache, and only once. A key that fails for any other reason — a
1067
+ * sandbox that does not exist, a rate limit — is not evidence about the key,
1068
+ * and refreshing on it would rotate the tenant for no reason.
1069
+ */
1070
+ async runWithKeyRefresh(signal, run) {
1071
+ const apiKey = await this.options.keys.get(signal);
1072
+ try {
1073
+ return await run(apiKey, signal);
1074
+ } catch (error) {
1075
+ if (!isAuthenticationFailure(error)) throw error;
1076
+ this.options.keys.invalidate();
1077
+ const replacement = await this.options.keys.get(signal);
1078
+ return run(replacement, signal);
1079
+ }
1080
+ }
1081
+ };
1082
+ /**
1083
+ * Whether an error is the gateway refusing the credential.
1084
+ *
1085
+ * The SDK's `AuthenticationError` does not extend `SandboxError`, so a check
1086
+ * for `SandboxError` alone would miss exactly this case — which is the case
1087
+ * that has a remedy.
1088
+ */
1089
+ function isAuthenticationFailure(error) {
1090
+ if (error === null || typeof error !== "object") return false;
1091
+ return error.constructor?.name === "AuthenticationError";
1092
+ }
1093
+
1094
+ //#endregion
1095
+ //#region src/credentials.ts
1096
+ /**
1097
+ * Open a credential store.
1098
+ *
1099
+ * Takes no options: the service name is fixed in the store package, because a
1100
+ * per-plugin choice of namespace is how a writer and a reader end up looking at
1101
+ * different stores.
1102
+ */
1103
+ function openCredentialStore() {
1104
+ return createCredentialsStore();
1105
+ }
1106
+ /**
1107
+ * Read the current credentials.
1108
+ *
1109
+ * AK and SK are read as a pair: half a pair reads as no credentials rather
1110
+ * than an error, so a partially-configured machine gets the "authenticate
1111
+ * first" message instead of a signing failure with no remedy. The
1112
+ * SecurityToken is optional and is omitted from the object entirely when
1113
+ * absent — an empty string would sign as a third header the gateway then
1114
+ * rejects.
1115
+ *
1116
+ * Called per request, never captured: that is what makes a credential saved
1117
+ * from the credential form apply to the next call.
1118
+ *
1119
+ * @param store - the credential store to read.
1120
+ * @returns the credentials, or `undefined` when the pair is incomplete.
1121
+ */
1122
+ function readCredentials(store) {
1123
+ return readStoredPair(store);
1124
+ }
1125
+
1126
+ //#endregion
1127
+ //#region src/sandbox-key.ts
1128
+ /** Path of the key endpoint: POST creates, GET lists. */
1129
+ const KEYS_PATH = "/e2b/keys";
1130
+ /** An error the model can act on, carrying the endpoint's own message. */
1131
+ var SandboxKeyError = class extends Error {
1132
+ constructor(message) {
1133
+ super(message);
1134
+ this.name = "SandboxKeyError";
1135
+ }
1136
+ };
1137
+ /**
1138
+ * Whether a stored key may still be used for the credentials in hand.
1139
+ *
1140
+ * - No credentials → no, whatever the key says: nothing granted under a
1141
+ * credential set that has since been cleared may still be presented.
1142
+ * - No fingerprint on the key (an older build) → yes. Rotating it would orphan
1143
+ * every sandbox made before this build, and that is worse than the gap it
1144
+ * closes; the 401 fallback still catches a dead key.
1145
+ * - Otherwise → the fingerprint must equal the current AK's.
1146
+ *
1147
+ * @param stored - the key read from the store.
1148
+ * @param credentials - the credentials in hand, or `undefined` when cleared.
1149
+ * @returns `true` when the key belongs to the current credentials.
1150
+ */
1151
+ function matchesFingerprint(stored, credentials) {
1152
+ if (credentials === void 0) return false;
1153
+ if (stored.akFingerprint === void 0) return true;
1154
+ return stored.akFingerprint === fingerprint(credentials.ak);
1155
+ }
1156
+ /**
1157
+ * Convert the stored bundle section into this module's shape.
1158
+ *
1159
+ * Returns `undefined` when there is no usable key: a corrupt section should
1160
+ * lead to a new key, not to a half-parsed value presented to the gateway.
1161
+ *
1162
+ * The section is decoded by the shared store module, so this function only
1163
+ * renames fields — it never parses JSON.
1164
+ *
1165
+ * @param section - the e2b section read from the store.
1166
+ * @returns the key, or `undefined` when there is none.
1167
+ */
1168
+ function fromStoredSection(section) {
1169
+ if (section === void 0) return void 0;
1170
+ return {
1171
+ key: section.api_key,
1172
+ expiresAt: section.expired_at,
1173
+ ...section.ak_fingerprint === void 0 ? {} : { akFingerprint: section.ak_fingerprint }
1174
+ };
1175
+ }
1176
+ /**
1177
+ * Whether a stored key is past its expiry.
1178
+ *
1179
+ * @param key - the stored key.
1180
+ * @param now - the instant to compare against.
1181
+ * @returns `true` when the key should be replaced.
1182
+ */
1183
+ function isExpired(key, now) {
1184
+ const at = Date.parse(key.expiresAt);
1185
+ if (Number.isNaN(at)) return false;
1186
+ return at <= now;
1187
+ }
1188
+ /** The stored form of one key, as the bundle keeps it. */
1189
+ function toStoredSection(key) {
1190
+ return {
1191
+ api_key: key.key,
1192
+ expired_at: key.expiresAt,
1193
+ ...key.akFingerprint === void 0 ? {} : { ak_fingerprint: key.akFingerprint }
1194
+ };
1195
+ }
1196
+ /** One key, read from the store and replaced when it expires or is refused. */
1197
+ var SandboxKeySource = class {
1198
+ constructor(options) {
1199
+ this.options = options;
1200
+ this.fetchImpl = options.fetchImpl ?? fetch;
1201
+ }
1202
+ /**
1203
+ * Return a usable API key.
1204
+ *
1205
+ * A key is used only while it is unexpired **and** still belongs to the
1206
+ * credentials in hand — see the module comment on why the second check
1207
+ * exists and why a mismatch does not delete the stored entry.
1208
+ *
1209
+ * @param signal - caller cancellation, forwarded to the fetch.
1210
+ * @returns the key to present as `X-API-KEY`.
1211
+ * @throws {SandboxKeyError} when no credentials are stored, so nothing can
1212
+ * be created to replace a key the current credentials do not own.
1213
+ */
1214
+ async get(signal) {
1215
+ const now = Date.now();
1216
+ const credentials = this.options.readCredentials();
1217
+ if (this.current !== void 0 && !isExpired(this.current, now) && matchesFingerprint(this.current, credentials)) return this.current.key;
1218
+ const stored = fromStoredSection(readE2BKey(this.options.store));
1219
+ if (stored !== void 0 && !isExpired(stored, now) && matchesFingerprint(stored, credentials)) {
1220
+ this.current = stored;
1221
+ return stored.key;
1222
+ }
1223
+ this.current = void 0;
1224
+ this.pending ??= this.createKey(credentials, signal);
1225
+ try {
1226
+ this.current = await this.pending;
1227
+ return this.current.key;
1228
+ } finally {
1229
+ this.pending = void 0;
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Forget the key, so the next {@link get} creates a new one.
1234
+ *
1235
+ * Called when the gateway refuses the key as unauthenticated — the one
1236
+ * signal that it has stopped working earlier than its expiry said. The
1237
+ * replacement is a different key, and therefore a different tenant: sandboxes
1238
+ * created under the old one stop being reachable through this plugin.
1239
+ */
1240
+ invalidate() {
1241
+ this.current = void 0;
1242
+ clearE2BKey(this.options.store);
1243
+ }
1244
+ /**
1245
+ * Create a key, store it, and return it.
1246
+ *
1247
+ * @param credentials - the pair to sign with; already read by {@link get},
1248
+ * so the fingerprint recorded on the key is the one it was minted under.
1249
+ * @param signal - caller cancellation, forwarded to the fetch.
1250
+ */
1251
+ async createKey(credentials, signal) {
1252
+ if (credentials === void 0) throw new SandboxKeyError("未进行华为云 IAM 认证,请认证后重试。 / Not authenticated with Huawei Cloud IAM. Please authenticate, then retry.");
1253
+ const url = new URL(`${this.options.baseUrl}${KEYS_PATH}`);
1254
+ const headers = sign("POST", url.host, url.pathname, {}, "", credentials);
1255
+ const timeout = AbortSignal.timeout(this.options.timeoutMs);
1256
+ const composed = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
1257
+ let response;
1258
+ try {
1259
+ response = await this.fetchImpl(url, {
1260
+ method: "POST",
1261
+ headers: {
1262
+ host: headers.host,
1263
+ "x-sdk-date": headers["x-sdk-date"],
1264
+ Authorization: headers.Authorization,
1265
+ ...headers["x-security-token"] === void 0 ? {} : { "x-security-token": headers["x-security-token"] }
1266
+ },
1267
+ signal: composed
1268
+ });
1269
+ } catch (error) {
1270
+ throw new SandboxKeyError(`aishell-e2b: cannot reach the sandbox key service at ${this.options.baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
1271
+ }
1272
+ if (!response.ok) throw new SandboxKeyError(await describeFailure(response));
1273
+ const created = {
1274
+ ...await readCreatedKey(response),
1275
+ akFingerprint: fingerprint(credentials.ak)
1276
+ };
1277
+ try {
1278
+ writeE2BKey(this.options.store, toStoredSection(created));
1279
+ } catch (error) {}
1280
+ return created;
1281
+ }
1282
+ };
1283
+ /**
1284
+ * Turn a failing response into a message naming what the operator can do.
1285
+ *
1286
+ * Three conditions carry different instructions and must not be flattened:
1287
+ *
1288
+ * - `1010` — the account holds too many keys; delete one, then retry.
1289
+ * - `1008` — too many creates recently; wait, then retry.
1290
+ * - `2001`/401 — the signature was refused; check the stored AK/SK.
1291
+ */
1292
+ async function describeFailure(response) {
1293
+ let code = "";
1294
+ let message = "";
1295
+ try {
1296
+ const parsed = await response.json();
1297
+ if (typeof parsed.error_code === "string") code = parsed.error_code;
1298
+ if (typeof parsed.error_msg === "string") message = parsed.error_msg;
1299
+ } catch {}
1300
+ if (code === "AISHELL.1010") return "aishell-e2b: this account already holds the maximum number of sandbox keys. Delete one that is no longer in use, then retry.";
1301
+ if (code === "AISHELL.1008") return "aishell-e2b: too many sandbox keys were created recently. Retry shortly.";
1302
+ if (code === "AISHELL.2001" || response.status === 401 || response.status === 403) return "aishell-e2b: the sandbox key service rejected the request signature. Check the stored access key and secret key, then retry.";
1303
+ if (code === "AISHELL.3003") return "aishell-e2b: the sandbox key service is not ready right now. Retry shortly.";
1304
+ const detail = message === "" ? "" : `: ${message}`;
1305
+ return `aishell-e2b: the sandbox key service failed with HTTP ${response.status}${code === "" ? "" : ` (${code})`}${detail}`;
1306
+ }
1307
+ /**
1308
+ * Read the created key out of the response body.
1309
+ *
1310
+ * The plaintext appears exactly once, in this response — the service keeps only
1311
+ * a hash — so a body that does not carry one is a failure, not something to
1312
+ * retry against a later read.
1313
+ */
1314
+ async function readCreatedKey(response) {
1315
+ let parsed;
1316
+ try {
1317
+ parsed = await response.json();
1318
+ } catch (error) {
1319
+ throw new SandboxKeyError(`aishell-e2b: the sandbox key service returned a body that is not JSON: ${error instanceof Error ? error.message : String(error)}`);
1320
+ }
1321
+ const body = parsed;
1322
+ const key = body?.api_key;
1323
+ if (typeof key !== "string" || key === "") throw new SandboxKeyError("aishell-e2b: the sandbox key service returned no usable key.");
1324
+ const expiresAt = typeof body?.expires_at === "string" && body.expires_at !== "" ? body.expires_at : FOREVER_EXPIRY;
1325
+ return {
1326
+ key,
1327
+ expiresAt
1328
+ };
1329
+ }
1330
+
1331
+ //#endregion
1332
+ //#region src/sandbox-registry.ts
1333
+ /**
1334
+ * The sandbox a conversation works in.
1335
+ *
1336
+ * One conversation, one sandbox. That is the whole model: there is no
1337
+ * "which sandbox did you mean" question for the tools to ask, because there is
1338
+ * only ever one answer, and no `sandboxId` parameter for the model to get
1339
+ * wrong. The tools act on *the* sandbox, and creating one is the explicit act
1340
+ * that binds it to the conversation.
1341
+ *
1342
+ * ## Why the binding is created explicitly, never lazily
1343
+ *
1344
+ * A tool that quietly created a sandbox on first use would have to choose the
1345
+ * template and the lifetime itself, and those are exactly the two facts the
1346
+ * operator should be deciding — a preview page wants a different lifetime from
1347
+ * a debugging session, and that choice is the reason the create tool takes
1348
+ * parameters at all. A lazy create would move that decision into the plugin
1349
+ * and out of the conversation, where nobody can see it or correct it.
1350
+ *
1351
+ * So a tool with no sandbox bound fails, and names `aishell__sandbox_create` as the way
1352
+ * forward. The failure is cheap and legible; a silently invented sandbox is
1353
+ * neither.
1354
+ *
1355
+ * ## Why the binding survives a restart
1356
+ *
1357
+ * The mapping cannot live in memory alone. A plugin restart — a reload, a
1358
+ * crash, a profile edit — would otherwise orphan a running sandbox: the model
1359
+ * would find no binding, create a second sandbox, and the first would keep
1360
+ * billing until its lifetime expired, with nothing in the conversation
1361
+ * accounting for it.
1362
+ *
1363
+ * So the sandbox carries its conversation in its own `metadata` at creation,
1364
+ * and the registry rebuilds the binding by querying for it. The gateway
1365
+ * forwards `metadata` filters to the upstream unchanged, and
1366
+ * `Sandbox.list({ query: { metadata } })` is ordinary SDK surface, so the
1367
+ * recovery needs nothing from either service.
1368
+ *
1369
+ * A recovered binding is refused if the sandbox is not running. A paused
1370
+ * sandbox is not adopted: connecting to it would resume it and silently
1371
+ * restart its clock, which is a billing decision nobody asked for.
1372
+ *
1373
+ * @module @huaweidevtools/aishell-dsh-e2b/sandbox-registry
1374
+ */
1375
+ /** The metadata key a sandbox carries its owning conversation under. */
1376
+ const SESSION_METADATA_KEY = "aishell-session";
1377
+ /** In-memory bindings plus the lookup used to rebuild them. */
1378
+ var SandboxRegistry = class {
1379
+ constructor(find) {
1380
+ this.bySession = new Map();
1381
+ this.find = find;
1382
+ }
1383
+ /**
1384
+ * Bind a sandbox to its conversation.
1385
+ *
1386
+ * @param sessionId - the owning conversation, or `undefined` for a call that
1387
+ * ran outside one. Such a call is recorded nowhere: without a conversation
1388
+ * to key on, there would be nothing to bind later calls to.
1389
+ * @param sandboxId - the sandbox to bind.
1390
+ */
1391
+ bind(sessionId, sandboxId) {
1392
+ if (sessionId === void 0 || sessionId === "") return;
1393
+ this.bySession.set(sessionId, sandboxId);
1394
+ }
1395
+ /**
1396
+ * The sandbox this conversation is bound to, recovering the binding if this
1397
+ * process has not seen one.
1398
+ *
1399
+ * @param sessionId - the calling conversation.
1400
+ * @param signal - caller cancellation, forwarded to the lookup.
1401
+ * @returns the sandbox id, or `undefined` when the conversation has none.
1402
+ */
1403
+ async resolve(sessionId, signal) {
1404
+ if (sessionId === void 0 || sessionId === "") return void 0;
1405
+ const known = this.bySession.get(sessionId);
1406
+ if (known !== void 0) return known;
1407
+ const found = await this.find({ [SESSION_METADATA_KEY]: sessionId }, signal);
1408
+ const running = found.find((candidate) => candidate.state === "running");
1409
+ if (running === void 0) return void 0;
1410
+ this.bySession.set(sessionId, running.sandboxId);
1411
+ return running.sandboxId;
1412
+ }
1413
+ /**
1414
+ * The sandbox bound right now, without a lookup.
1415
+ *
1416
+ * Used where a call has already resolved the binding and needs it again
1417
+ * without a second round trip.
1418
+ *
1419
+ * @param sessionId - the calling conversation.
1420
+ * @returns the bound sandbox id, or `undefined`.
1421
+ */
1422
+ bound(sessionId) {
1423
+ if (sessionId === void 0 || sessionId === "") return void 0;
1424
+ return this.bySession.get(sessionId);
1425
+ }
1426
+ /**
1427
+ * Drop a binding, because the sandbox it names no longer exists.
1428
+ *
1429
+ * @param sandboxId - the sandbox that is gone.
1430
+ */
1431
+ forget(sandboxId) {
1432
+ for (const [session, bound] of this.bySession) if (bound === sandboxId) this.bySession.delete(session);
1433
+ }
1434
+ };
1435
+ /** The metadata a new sandbox carries so its conversation can be recovered. */
1436
+ function sessionMetadata(sessionId, extra) {
1437
+ const tags = { ...extra };
1438
+ if (sessionId !== void 0 && sessionId !== "") tags[SESSION_METADATA_KEY] = sessionId;
1439
+ return Object.keys(tags).length === 0 ? void 0 : tags;
1440
+ }
1441
+ /**
1442
+ * The message a tool gets when the conversation has no sandbox.
1443
+ *
1444
+ * Names the remedy, and deliberately does not offer to create one: the model
1445
+ * chooses the template and the lifetime, and it cannot do that from inside a
1446
+ * tool that was asked to write a file.
1447
+ */
1448
+ const NO_SANDBOX_MESSAGE = "aishell-e2b: this conversation has no sandbox. Call aishell__sandbox_create first, choosing the lifetime the work needs.";
1449
+ /**
1450
+ * The message a tool gets when a sandbox existed but is now gone.
1451
+ *
1452
+ * Distinct from {@link NO_SANDBOX_MESSAGE} because the remedy differs: the
1453
+ * binding is stale rather than absent, and the model should know the previous
1454
+ * sandbox's contents are not recoverable — creating a new one starts empty.
1455
+ */
1456
+ const SANDBOX_GONE_MESSAGE = "aishell-e2b: the sandbox this conversation was using no longer exists (it was deleted or its lifetime expired). Its contents are gone. Call aishell__sandbox_create to start a new one.";
1457
+
1458
+ //#endregion
1459
+ //#region src/errors.ts
1460
+ /**
1461
+ * Turning SDK failures into messages the model can act on.
1462
+ *
1463
+ * Two properties decide the shape here.
1464
+ *
1465
+ * **A non-zero exit is not an error.** The SDK's foreground `commands.run`
1466
+ * rejects with `CommandExitError` when a command exits non-zero, carrying the
1467
+ * exit code and both streams. Reporting that as a tool failure tells the model
1468
+ * "the tool broke" when the truth is "the command ran and returned 1" — which
1469
+ * for a build, a test run, or a `grep` with no match is the ordinary outcome.
1470
+ * Callers convert it to a normal result instead; see `tools.ts`.
1471
+ *
1472
+ * **The error hierarchy does not share a root.** `AuthenticationError`,
1473
+ * `BuildError` and `VolumeError` do not extend `SandboxError`, so matching on
1474
+ * `SandboxError` alone would let exactly the failures with remedies fall
1475
+ * through to a generic message. Matching is therefore by constructor name,
1476
+ * which is stable across the SDK's own re-bundling and needs no import of a
1477
+ * class the package may rename.
1478
+ *
1479
+ * @module @huaweidevtools/aishell-dsh-e2b/errors
1480
+ */
1481
+ /** A failure whose text is already the message the model should see. */
1482
+ var SandboxToolError = class extends Error {
1483
+ constructor(message) {
1484
+ super(message);
1485
+ this.name = "SandboxToolError";
1486
+ }
1487
+ };
1488
+ /** The remedy each SDK failure carries, keyed by its constructor name. */
1489
+ const REMEDIES = {
1490
+ AuthenticationError: "the sandbox gateway rejected the API key. Re-authenticate with Huawei Cloud, then retry.",
1491
+ SandboxNotFoundError: "no such sandbox. It may have expired. Call aishell__sandbox_list to see the sandboxes this account currently has, or aishell__sandbox_create to make a new one.",
1492
+ FileNotFoundError: "no such file in the sandbox. Call aishell__sandbox_files_list to see what is there.",
1493
+ RateLimitError: "the sandbox service is rate limiting; retry shortly.",
1494
+ NotEnoughSpaceError: "the sandbox is out of disk space.",
1495
+ InvalidArgumentError: "the sandbox service rejected an argument.",
1496
+ TemplateError: "the sandbox template does not support this operation. This usually means the sandbox's envd is older than the feature requires.",
1497
+ TimeoutError: "the request timed out. A sandbox that has expired reports this too.",
1498
+ GitAuthError: "git authentication failed inside the sandbox.",
1499
+ GitUpstreamError: "the git remote rejected the operation inside the sandbox.",
1500
+ BuildError: "a template build failed.",
1501
+ VolumeError: "a volume operation failed.",
1502
+ SandboxError: "the sandbox service reported an error."
1503
+ };
1504
+ /**
1505
+ * Render one thrown value as the message a tool should fail with.
1506
+ *
1507
+ * @param error - whatever was thrown.
1508
+ * @param context - what the call was doing, prefixed for orientation.
1509
+ * @returns the message to surface.
1510
+ */
1511
+ function describeSandboxError(error, context) {
1512
+ if (error instanceof SandboxToolError) return error.message;
1513
+ const name$1 = errorName(error);
1514
+ const remedy = REMEDIES[name$1];
1515
+ const detail = error instanceof Error ? error.message : String(error);
1516
+ const body = detail === "" ? "" : `: ${detail}`;
1517
+ return remedy === void 0 ? `aishell-e2b: ${context} failed${body}` : `aishell-e2b: ${context} failed — ${remedy}${body}`;
1518
+ }
1519
+ /** The constructor name of a thrown value, or `''` for a non-object. */
1520
+ function errorName(error) {
1521
+ if (error === null || typeof error !== "object") return "";
1522
+ return error.constructor?.name ?? "";
1523
+ }
1524
+ /**
1525
+ * Whether a thrown value is a command that ran and exited non-zero.
1526
+ *
1527
+ * Kept separate from {@link describeSandboxError} because it is not a failure:
1528
+ * see the module comment.
1529
+ */
1530
+ function isCommandExitError(error) {
1531
+ return errorName(error) === "CommandExitError";
1532
+ }
1533
+
1534
+ //#endregion
1535
+ //#region src/tools.ts
1536
+ /**
1537
+ * The sandbox this call acts on, or a failure naming the way forward.
1538
+ *
1539
+ * Recovery is attempted on every call, not only in `aishell__sandbox_create`: a plugin
1540
+ * restart between two calls would otherwise strand a running sandbox, and the
1541
+ * model would create a second one while the first kept billing.
1542
+ *
1543
+ * @param deps - the tool dependencies.
1544
+ * @param sessionId - the calling conversation, from the execution context.
1545
+ * @param signal - caller cancellation, forwarded to the recovery lookup.
1546
+ * @returns the bound sandbox id.
1547
+ */
1548
+ async function requireSandboxId(deps, sessionId, signal) {
1549
+ const id = await deps.registry.resolve(sessionId, signal);
1550
+ if (id === void 0) throw new SandboxToolError(NO_SANDBOX_MESSAGE);
1551
+ return id;
1552
+ }
1553
+ /**
1554
+ * Translate a failure about the sandbox itself into the message that fits.
1555
+ *
1556
+ * A sandbox that has expired is not the same situation as a conversation that
1557
+ * never had one, and the distinction is worth the trouble: the first means the
1558
+ * work in it is gone and a replacement starts empty, which the model has to
1559
+ * know before it decides to continue.
1560
+ *
1561
+ * @param error - the thrown value.
1562
+ * @param context - what the call was doing.
1563
+ * @returns the message to fail with.
1564
+ */
1565
+ function describeBoundError(error, context) {
1566
+ return errorName(error) === "SandboxNotFoundError" ? SANDBOX_GONE_MESSAGE : describeSandboxError(error, context);
1567
+ }
1568
+ /** Project one sandbox's identity and lifetime into a JSON-safe object. */
1569
+ function describeInfo(info) {
1570
+ return {
1571
+ sandboxId: info.sandboxId,
1572
+ templateId: info.templateId,
1573
+ state: info.state,
1574
+ startedAt: info.startedAt.toISOString(),
1575
+ endAt: info.endAt.toISOString(),
1576
+ cpuCount: info.cpuCount,
1577
+ memoryMB: info.memoryMB,
1578
+ envdVersion: info.envdVersion,
1579
+ ...info.metadata === void 0 ? {} : { metadata: info.metadata }
1580
+ };
1581
+ }
1582
+ /** How much of a command's output a result carries before it is elided. */
1583
+ const OUTPUT_LIMIT = 3e4;
1584
+ /** Keep a stream useful without letting one command flood the context. */
1585
+ function clip(text) {
1586
+ if (text.length <= OUTPUT_LIMIT) return {
1587
+ text,
1588
+ truncated: false
1589
+ };
1590
+ return {
1591
+ text: text.slice(0, OUTPUT_LIMIT),
1592
+ truncated: true
1593
+ };
1594
+ }
1595
+ /**
1596
+ * Reject a map whose values are not strings.
1597
+ *
1598
+ * The tool's schema can only say "an object" — the harness schema DSL has no
1599
+ * way to constrain the value type of a free-form map — while both the sandbox
1600
+ * service and the SDK require string values. Without this check a number or a
1601
+ * nested object would be stringified somewhere downstream and surface as a
1602
+ * command that behaves oddly, far from the call that caused it.
1603
+ */
1604
+ function stringMap(value, field) {
1605
+ for (const [key, entry] of Object.entries(value)) if (typeof entry !== "string") throw new SandboxToolError(`aishell-e2b: ${field}.${key} must be a string, got ${typeof entry}.`);
1606
+ return value;
1607
+ }
1608
+ /** The session id a tool call runs under, from its execution context. */
1609
+ function sessionOf(exec) {
1610
+ return exec.agent?.session?.id;
1611
+ }
1612
+ /**
1613
+ * Register every sandbox tool.
1614
+ *
1615
+ * @param ctx - the plugin context, providing the tool registry.
1616
+ * @param deps - the shared dependencies.
1617
+ * @returns disposers for the registrations.
1618
+ */
1619
+ function registerSandboxTools(ctx, deps) {
1620
+ const definitions = [
1621
+ createSandboxTool(deps),
1622
+ listSandboxesTool(deps),
1623
+ sandboxInfoTool(deps),
1624
+ killSandboxTool(deps),
1625
+ setSandboxTimeoutTool(deps),
1626
+ runCommandTool(deps),
1627
+ killCommandTool(deps),
1628
+ writeFileTool(deps),
1629
+ readFileTool(deps),
1630
+ downloadFileTool(deps, ctx),
1631
+ listFilesTool(deps),
1632
+ getPortUrlTool(deps)
1633
+ ];
1634
+ return definitions.map((definition) => ctx.tools.register(definition));
1635
+ }
1636
+ /** The output schema every sandbox-describing tool shares. */
1637
+ const SANDBOX_DESCRIPTION_SCHEMA = {
1638
+ type: "object",
1639
+ additionalProperties: false,
1640
+ properties: {
1641
+ sandboxId: {
1642
+ type: "string",
1643
+ required: true
1644
+ },
1645
+ templateId: {
1646
+ type: "string",
1647
+ required: true
1648
+ },
1649
+ state: {
1650
+ type: "string",
1651
+ required: true
1652
+ },
1653
+ startedAt: {
1654
+ type: "string",
1655
+ required: true
1656
+ },
1657
+ endAt: {
1658
+ type: "string",
1659
+ required: true
1660
+ },
1661
+ cpuCount: {
1662
+ type: "integer",
1663
+ required: true
1664
+ },
1665
+ memoryMB: {
1666
+ type: "integer",
1667
+ required: true
1668
+ },
1669
+ envdVersion: {
1670
+ type: "string",
1671
+ required: true
1672
+ },
1673
+ metadata: {
1674
+ type: "object",
1675
+ additionalProperties: true
1676
+ }
1677
+ }
1678
+ };
1679
+ /** The one-line rendering every sandbox-describing tool shares. */
1680
+ function renderSandboxDescription(value) {
1681
+ return [{
1682
+ type: "text",
1683
+ text: `Sandbox ${value.sandboxId} is ${value.state}, expires at ${value.endAt}.`
1684
+ }];
1685
+ }
1686
+ function createSandboxTool(deps) {
1687
+ return defineTool({
1688
+ name: "aishell__sandbox_create",
1689
+ description: "Create a sandbox for this conversation: a disposable Linux environment that runs for a bounded time and is then deleted along with everything in it. This binds the sandbox to the conversation — the other sandbox tools act on it without being told which one. Creating a second one replaces the binding; the first keeps running until its lifetime expires, so delete it with aishell__sandbox_kill when it is no longer needed.",
1690
+ parameters: {
1691
+ template: {
1692
+ type: "string",
1693
+ description: "Template name or id to start from. Defaults to the service's base template."
1694
+ },
1695
+ timeoutMs: {
1696
+ type: "number",
1697
+ description: "Lifetime in MILLISECONDS (the TypeScript SDK counts milliseconds; the Python SDK counts seconds). The sandbox and everything in it are deleted when this expires. Default 300000 (5 minutes). Choose it from what the work needs: a page to look at once needs minutes, a working session longer."
1698
+ },
1699
+ envs: {
1700
+ type: "object",
1701
+ additionalProperties: true,
1702
+ description: "Environment variables every command in the sandbox starts with."
1703
+ },
1704
+ metadata: {
1705
+ type: "object",
1706
+ additionalProperties: true,
1707
+ description: "Extra key/value labels for finding this sandbox later with aishell__sandbox_list. The conversation tag is added automatically."
1708
+ }
1709
+ },
1710
+ output: {
1711
+ schema: SANDBOX_DESCRIPTION_SCHEMA,
1712
+ render: (_args, value) => [{
1713
+ type: "text",
1714
+ text: `Sandbox ${value.sandboxId} is ${value.state} (${String(value.cpuCount)} vCPU, ${String(value.memoryMB)} MB), expires at ${value.endAt}.`
1715
+ }]
1716
+ },
1717
+ timeoutMs: 12e4,
1718
+ async execute(args, exec) {
1719
+ const input = args;
1720
+ const sessionId = sessionOf(exec);
1721
+ try {
1722
+ const metadata = sessionMetadata(sessionId, input.metadata === void 0 ? void 0 : stringMap(input.metadata, "metadata"));
1723
+ const sandbox = await deps.client.create(input.template, {
1724
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
1725
+ ...input.envs === void 0 ? {} : { envs: stringMap(input.envs, "envs") },
1726
+ ...metadata === void 0 ? {} : { metadata },
1727
+ signal: exec.signal
1728
+ });
1729
+ deps.registry.bind(sessionId, sandbox.sandboxId);
1730
+ return describeInfo(await sandbox.getInfo({ signal: exec.signal }));
1731
+ } catch (error) {
1732
+ throw new SandboxToolError(describeSandboxError(error, "aishell__sandbox_create"));
1733
+ }
1734
+ }
1735
+ });
1736
+ }
1737
+ function listSandboxesTool(deps) {
1738
+ return defineTool({
1739
+ name: "aishell__sandbox_list",
1740
+ description: "List the sandboxes this account currently has. Reports sandboxes from every conversation, not just this one.",
1741
+ parameters: {},
1742
+ output: {
1743
+ schema: {
1744
+ type: "object",
1745
+ additionalProperties: false,
1746
+ properties: { sandboxes: {
1747
+ type: "array",
1748
+ required: true,
1749
+ items: {
1750
+ type: "object",
1751
+ additionalProperties: false,
1752
+ properties: {
1753
+ sandboxId: {
1754
+ type: "string",
1755
+ required: true
1756
+ },
1757
+ templateId: {
1758
+ type: "string",
1759
+ required: true
1760
+ },
1761
+ state: {
1762
+ type: "string",
1763
+ required: true
1764
+ },
1765
+ startedAt: {
1766
+ type: "string",
1767
+ required: true
1768
+ },
1769
+ endAt: {
1770
+ type: "string",
1771
+ required: true
1772
+ }
1773
+ }
1774
+ }
1775
+ } }
1776
+ },
1777
+ render: (_args, value) => {
1778
+ const rows = value.sandboxes;
1779
+ if (rows.length === 0) return [{
1780
+ type: "text",
1781
+ text: "No sandboxes."
1782
+ }];
1783
+ const lines = rows.map((row) => `${row.sandboxId} ${row.state} expires ${row.endAt} (${row.templateId})`);
1784
+ return [{
1785
+ type: "text",
1786
+ text: lines.join("\n")
1787
+ }];
1788
+ }
1789
+ },
1790
+ timeoutMs: 6e4,
1791
+ async execute(_args, exec) {
1792
+ try {
1793
+ return { sandboxes: await deps.client.list(exec.signal) };
1794
+ } catch (error) {
1795
+ throw new SandboxToolError(describeSandboxError(error, "aishell__sandbox_list"));
1796
+ }
1797
+ }
1798
+ });
1799
+ }
1800
+ function sandboxInfoTool(deps) {
1801
+ return defineTool({
1802
+ name: "aishell__sandbox_info",
1803
+ description: "Report this conversation's sandbox: its state, resources, and expiry time.",
1804
+ parameters: {},
1805
+ output: {
1806
+ schema: SANDBOX_DESCRIPTION_SCHEMA,
1807
+ render: (_args, value) => renderSandboxDescription(value)
1808
+ },
1809
+ timeoutMs: 6e4,
1810
+ async execute(_args, exec) {
1811
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
1812
+ try {
1813
+ const handle = await deps.client.handle(sandboxId);
1814
+ return describeInfo(await handle.getInfo({ signal: exec.signal }));
1815
+ } catch (error) {
1816
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_info"));
1817
+ }
1818
+ }
1819
+ });
1820
+ }
1821
+ function killSandboxTool(deps) {
1822
+ return defineTool({
1823
+ name: "aishell__sandbox_kill",
1824
+ description: "Delete this conversation's sandbox immediately, discarding everything in it, and release the conversation's binding to it. Do this when the work is done rather than waiting for the lifetime to expire.",
1825
+ parameters: {},
1826
+ output: {
1827
+ schema: {
1828
+ type: "object",
1829
+ additionalProperties: false,
1830
+ properties: {
1831
+ sandboxId: {
1832
+ type: "string",
1833
+ required: true
1834
+ },
1835
+ killed: {
1836
+ type: "boolean",
1837
+ required: true
1838
+ }
1839
+ }
1840
+ },
1841
+ render: (_args, value) => [{
1842
+ type: "text",
1843
+ text: value.killed ? `Sandbox ${value.sandboxId} deleted.` : `Sandbox ${value.sandboxId} was already gone.`
1844
+ }]
1845
+ },
1846
+ timeoutMs: 6e4,
1847
+ async execute(_args, exec) {
1848
+ const sessionId = sessionOf(exec);
1849
+ const sandboxId = await requireSandboxId(deps, sessionId, exec.signal);
1850
+ try {
1851
+ const handle = await deps.client.handle(sandboxId);
1852
+ await handle.kill({ signal: exec.signal });
1853
+ deps.client.forget(sandboxId);
1854
+ deps.registry.forget(sandboxId);
1855
+ return {
1856
+ sandboxId,
1857
+ killed: true
1858
+ };
1859
+ } catch (error) {
1860
+ if (errorName(error) === "SandboxNotFoundError") {
1861
+ deps.client.forget(sandboxId);
1862
+ deps.registry.forget(sandboxId);
1863
+ return {
1864
+ sandboxId,
1865
+ killed: false
1866
+ };
1867
+ }
1868
+ throw new SandboxToolError(describeSandboxError(error, "aishell__sandbox_kill"));
1869
+ }
1870
+ }
1871
+ });
1872
+ }
1873
+ function setSandboxTimeoutTool(deps) {
1874
+ return defineTool({
1875
+ name: "aishell__sandbox_set_timeout",
1876
+ description: "Change how long this conversation's sandbox stays alive, measured from now. Use it to extend a sandbox that is still needed, or to shorten one that is not.",
1877
+ parameters: { timeoutMs: {
1878
+ type: "number",
1879
+ required: true,
1880
+ description: "New lifetime in MILLISECONDS from now. The service caps this at 24 hours for a pro account and 1 hour for a hobby account."
1881
+ } },
1882
+ output: {
1883
+ schema: {
1884
+ type: "object",
1885
+ additionalProperties: false,
1886
+ properties: {
1887
+ sandboxId: {
1888
+ type: "string",
1889
+ required: true
1890
+ },
1891
+ endAt: {
1892
+ type: "string",
1893
+ required: true
1894
+ }
1895
+ }
1896
+ },
1897
+ render: (_args, value) => [{
1898
+ type: "text",
1899
+ text: `Sandbox ${value.sandboxId} now expires at ${value.endAt}.`
1900
+ }]
1901
+ },
1902
+ timeoutMs: 6e4,
1903
+ async execute(args, exec) {
1904
+ const input = args;
1905
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
1906
+ try {
1907
+ const handle = await deps.client.handle(sandboxId);
1908
+ await handle.setTimeout(input.timeoutMs, { signal: exec.signal });
1909
+ const info = await handle.getInfo({ signal: exec.signal });
1910
+ return {
1911
+ sandboxId,
1912
+ endAt: info.endAt.toISOString()
1913
+ };
1914
+ } catch (error) {
1915
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_set_timeout"));
1916
+ }
1917
+ }
1918
+ });
1919
+ }
1920
+ function runCommandTool(deps) {
1921
+ return defineTool({
1922
+ name: "aishell__sandbox_commands_run",
1923
+ description: "Run a shell command in this conversation's sandbox. A command that exits non-zero is reported with its exit code and output rather than failing the tool, so a build error or a test failure reads as a result. Set background to start a long-running process (a server, for example) and get its pid back immediately.",
1924
+ parameters: {
1925
+ command: {
1926
+ type: "string",
1927
+ required: true,
1928
+ description: "Shell command to run, executed by bash inside the sandbox."
1929
+ },
1930
+ cwd: {
1931
+ type: "string",
1932
+ description: "Working directory inside the sandbox."
1933
+ },
1934
+ envs: {
1935
+ type: "object",
1936
+ additionalProperties: true,
1937
+ description: "Environment variables for this command, added to the sandbox's own."
1938
+ },
1939
+ timeoutMs: {
1940
+ type: "number",
1941
+ description: "How long to let the command run, in MILLISECONDS. Default 60000."
1942
+ },
1943
+ background: {
1944
+ type: "boolean",
1945
+ description: "Start the command and return its pid without waiting. Use it for a server that should keep running, and stop it later with aishell__sandbox_commands_kill."
1946
+ }
1947
+ },
1948
+ output: {
1949
+ schema: {
1950
+ type: "object",
1951
+ additionalProperties: false,
1952
+ properties: {
1953
+ exitCode: { type: "integer" },
1954
+ stdout: { type: "string" },
1955
+ stderr: { type: "string" },
1956
+ outputTruncated: { type: "boolean" },
1957
+ pid: { type: "integer" }
1958
+ }
1959
+ },
1960
+ render: (_args, value) => {
1961
+ if (value.pid !== void 0) return [{
1962
+ type: "text",
1963
+ text: `Started in the background with pid ${String(value.pid)}.`
1964
+ }];
1965
+ const parts = [`exit ${String(value.exitCode ?? "?")}`];
1966
+ if (value.stdout) parts.push(`stdout:\n${value.stdout}`);
1967
+ if (value.stderr) parts.push(`stderr:\n${value.stderr}`);
1968
+ if (value.outputTruncated) parts.push("(output truncated)");
1969
+ return [{
1970
+ type: "text",
1971
+ text: parts.join("\n")
1972
+ }];
1973
+ }
1974
+ },
1975
+ timeoutMs: 3e5,
1976
+ async execute(args, exec) {
1977
+ const input = args;
1978
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
1979
+ try {
1980
+ const handle = await deps.client.handle(sandboxId);
1981
+ const options = {
1982
+ ...input.cwd === void 0 ? {} : { cwd: input.cwd },
1983
+ ...input.envs === void 0 ? {} : { envs: stringMap(input.envs, "envs") },
1984
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
1985
+ signal: exec.signal
1986
+ };
1987
+ if (input.background === true) {
1988
+ const process = await handle.commands.run(input.command, {
1989
+ ...options,
1990
+ background: true
1991
+ });
1992
+ return { pid: process.pid };
1993
+ }
1994
+ try {
1995
+ const result = await handle.commands.run(input.command, options);
1996
+ const out = clip(result.stdout);
1997
+ const err = clip(result.stderr);
1998
+ return {
1999
+ exitCode: result.exitCode,
2000
+ stdout: out.text,
2001
+ stderr: err.text,
2002
+ outputTruncated: out.truncated || err.truncated
2003
+ };
2004
+ } catch (error) {
2005
+ if (isCommandExitError(error)) {
2006
+ const out = clip(String(error.stdout));
2007
+ const err = clip(String(error.stderr));
2008
+ return {
2009
+ exitCode: error.exitCode,
2010
+ stdout: out.text,
2011
+ stderr: err.text,
2012
+ outputTruncated: out.truncated || err.truncated
2013
+ };
2014
+ }
2015
+ throw error;
2016
+ }
2017
+ } catch (error) {
2018
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_commands_run"));
2019
+ }
2020
+ }
2021
+ });
2022
+ }
2023
+ function killCommandTool(deps) {
2024
+ return defineTool({
2025
+ name: "aishell__sandbox_commands_kill",
2026
+ description: "Stop a process running in this conversation's sandbox, by the pid aishell__sandbox_commands_run returned for a background command. Use it to stop a server started earlier — for example to free its port or to restart it with different settings — without deleting the whole sandbox and everything in it.",
2027
+ parameters: { pid: {
2028
+ type: "integer",
2029
+ required: true,
2030
+ description: "Process id, as returned by a background aishell__sandbox_commands_run."
2031
+ } },
2032
+ output: {
2033
+ schema: {
2034
+ type: "object",
2035
+ additionalProperties: false,
2036
+ properties: {
2037
+ pid: {
2038
+ type: "integer",
2039
+ required: true
2040
+ },
2041
+ killed: {
2042
+ type: "boolean",
2043
+ required: true
2044
+ }
2045
+ }
2046
+ },
2047
+ render: (_args, value) => [{
2048
+ type: "text",
2049
+ text: value.killed ? `Process ${String(value.pid)} stopped.` : `No process ${String(value.pid)} was running.`
2050
+ }]
2051
+ },
2052
+ timeoutMs: 6e4,
2053
+ async execute(args, exec) {
2054
+ const input = args;
2055
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2056
+ try {
2057
+ const handle = await deps.client.handle(sandboxId);
2058
+ const killed = await handle.commands.kill(input.pid, { signal: exec.signal });
2059
+ return {
2060
+ pid: input.pid,
2061
+ killed
2062
+ };
2063
+ } catch (error) {
2064
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_commands_kill"));
2065
+ }
2066
+ }
2067
+ });
2068
+ }
2069
+ function writeFileTool(deps) {
2070
+ return defineTool({
2071
+ name: "aishell__sandbox_files_write",
2072
+ description: "Write a text file in this conversation's sandbox, creating parent directories as needed. Use it to put source, configuration, or a static page where a command can read it.",
2073
+ parameters: {
2074
+ path: {
2075
+ type: "string",
2076
+ required: true,
2077
+ description: "Absolute path inside the sandbox, e.g. /home/user/site/index.html."
2078
+ },
2079
+ content: {
2080
+ type: "string",
2081
+ required: true,
2082
+ description: "Full file content, written as UTF-8 text."
2083
+ }
2084
+ },
2085
+ output: {
2086
+ schema: {
2087
+ type: "object",
2088
+ additionalProperties: false,
2089
+ properties: {
2090
+ path: {
2091
+ type: "string",
2092
+ required: true
2093
+ },
2094
+ bytes: {
2095
+ type: "integer",
2096
+ required: true
2097
+ }
2098
+ }
2099
+ },
2100
+ render: (_args, value) => [{
2101
+ type: "text",
2102
+ text: `Wrote ${String(value.bytes)} bytes to ${String(value.path)}.`
2103
+ }]
2104
+ },
2105
+ timeoutMs: 12e4,
2106
+ async execute(args, exec) {
2107
+ const input = args;
2108
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2109
+ try {
2110
+ const handle = await deps.client.handle(sandboxId);
2111
+ await handle.files.write(input.path, input.content);
2112
+ return {
2113
+ path: input.path,
2114
+ bytes: Buffer.byteLength(input.content, "utf8")
2115
+ };
2116
+ } catch (error) {
2117
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_files_write"));
2118
+ }
2119
+ }
2120
+ });
2121
+ }
2122
+ function readFileTool(deps) {
2123
+ return defineTool({
2124
+ name: "aishell__sandbox_files_read",
2125
+ description: "Read a file from this conversation's sandbox and return its contents. Text is returned as text; ask for base64 to inspect a binary file.",
2126
+ parameters: {
2127
+ path: {
2128
+ type: "string",
2129
+ required: true,
2130
+ description: "Absolute path inside the sandbox."
2131
+ },
2132
+ format: {
2133
+ type: "string",
2134
+ enum: ["text", "base64"],
2135
+ description: "How to return the bytes. Default text."
2136
+ }
2137
+ },
2138
+ output: {
2139
+ schema: {
2140
+ type: "object",
2141
+ additionalProperties: false,
2142
+ properties: {
2143
+ path: {
2144
+ type: "string",
2145
+ required: true
2146
+ },
2147
+ format: {
2148
+ type: "string",
2149
+ required: true
2150
+ },
2151
+ content: {
2152
+ type: "string",
2153
+ required: true
2154
+ },
2155
+ bytes: {
2156
+ type: "integer",
2157
+ required: true
2158
+ },
2159
+ truncated: {
2160
+ type: "boolean",
2161
+ required: true
2162
+ }
2163
+ }
2164
+ },
2165
+ render: (_args, value) => [{
2166
+ type: "text",
2167
+ text: value.truncated ? `${value.path} (first ${String(value.bytes)} bytes, truncated):\n${value.content}` : `${value.path}:\n${value.content}`
2168
+ }]
2169
+ },
2170
+ timeoutMs: 12e4,
2171
+ async execute(args, exec) {
2172
+ const input = args;
2173
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2174
+ try {
2175
+ const handle = await deps.client.handle(sandboxId);
2176
+ if (input.format === "base64") {
2177
+ const bytes = await handle.files.read(input.path, { format: "bytes" });
2178
+ const clipped$1 = bytes.byteLength > OUTPUT_LIMIT ? bytes.subarray(0, OUTPUT_LIMIT) : bytes;
2179
+ return {
2180
+ path: input.path,
2181
+ format: "base64",
2182
+ content: Buffer.from(clipped$1).toString("base64"),
2183
+ bytes: clipped$1.byteLength,
2184
+ truncated: clipped$1.byteLength !== bytes.byteLength
2185
+ };
2186
+ }
2187
+ const text = await handle.files.read(input.path, { format: "text" });
2188
+ const clipped = clip(text);
2189
+ return {
2190
+ path: input.path,
2191
+ format: "text",
2192
+ content: clipped.text,
2193
+ bytes: Buffer.byteLength(clipped.text, "utf8"),
2194
+ truncated: clipped.truncated
2195
+ };
2196
+ } catch (error) {
2197
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_files_read"));
2198
+ }
2199
+ }
2200
+ });
2201
+ }
2202
+ function downloadFileTool(deps, ctx) {
2203
+ return defineTool({
2204
+ name: "aishell__sandbox_files_download",
2205
+ description: "Copy a file out of this conversation's sandbox into the local workspace. Text is written as text. A binary file cannot be written to the local workspace — the harness filesystem accepts text only — so for one of those this returns base64 instead and reports that it had to; read those with aishell__sandbox_files_read and format base64 if that is what you wanted.",
2206
+ parameters: {
2207
+ path: {
2208
+ type: "string",
2209
+ required: true,
2210
+ description: "Absolute path inside the sandbox."
2211
+ },
2212
+ localPath: {
2213
+ type: "string",
2214
+ required: true,
2215
+ description: "Destination path in the local workspace. Ignored for a binary file, which cannot be written there."
2216
+ }
2217
+ },
2218
+ output: {
2219
+ schema: {
2220
+ type: "object",
2221
+ additionalProperties: false,
2222
+ properties: {
2223
+ path: {
2224
+ type: "string",
2225
+ required: true
2226
+ },
2227
+ localPath: { type: "string" },
2228
+ bytes: {
2229
+ type: "integer",
2230
+ required: true
2231
+ },
2232
+ encoding: {
2233
+ type: "string",
2234
+ required: true
2235
+ },
2236
+ content: { type: "string" }
2237
+ }
2238
+ },
2239
+ render: (_args, value) => [{
2240
+ type: "text",
2241
+ text: value.encoding === "text" ? `Copied ${value.path} (${String(value.bytes)} bytes) to ${String(value.localPath)}.` : `${value.path} is binary (${String(value.bytes)} bytes) and cannot be written to the local workspace. Its contents are base64 above.`
2242
+ }]
2243
+ },
2244
+ timeoutMs: 12e4,
2245
+ async execute(args, exec) {
2246
+ const input = args;
2247
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2248
+ try {
2249
+ const handle = await deps.client.handle(sandboxId);
2250
+ const bytes = await handle.files.read(input.path, { format: "bytes" });
2251
+ if (isProbablyBinary(bytes)) {
2252
+ const clipped = bytes.byteLength > OUTPUT_LIMIT ? bytes.subarray(0, OUTPUT_LIMIT) : bytes;
2253
+ return {
2254
+ path: input.path,
2255
+ bytes: bytes.byteLength,
2256
+ encoding: "base64",
2257
+ content: Buffer.from(clipped).toString("base64")
2258
+ };
2259
+ }
2260
+ const fs = deps.resolveFs() ?? ctx.get("fs");
2261
+ if (fs === void 0) return {
2262
+ path: input.path,
2263
+ bytes: bytes.byteLength,
2264
+ encoding: "base64",
2265
+ content: Buffer.from(bytes).toString("base64")
2266
+ };
2267
+ const text = Buffer.from(bytes).toString("utf8");
2268
+ const target = await fs.resolve(input.localPath, { signal: exec.signal });
2269
+ await fs.writeText(target, text, void 0, exec.signal);
2270
+ return {
2271
+ path: input.path,
2272
+ localPath: fs.processPath(target),
2273
+ bytes: bytes.byteLength,
2274
+ encoding: "text"
2275
+ };
2276
+ } catch (error) {
2277
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_files_download"));
2278
+ }
2279
+ }
2280
+ });
2281
+ }
2282
+ /**
2283
+ * Whether bytes look like something the local workspace cannot hold.
2284
+ *
2285
+ * The harness filesystem writes UTF-8 text, so the question is only whether
2286
+ * these bytes survive a round trip through it. A NUL byte settles it: no text
2287
+ * file contains one, and every common binary format does. A UTF-8 decode
2288
+ * failure settles it the other way. Everything else is treated as text, which
2289
+ * is the permissive direction — a misread text file is recoverable, a binary
2290
+ * file mangled by decoding is not.
2291
+ *
2292
+ * @param bytes - the file's contents.
2293
+ * @returns whether the bytes should be returned encoded instead of written.
2294
+ */
2295
+ function isProbablyBinary(bytes) {
2296
+ if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return false;
2297
+ if (bytes.includes(0)) return true;
2298
+ try {
2299
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2300
+ return false;
2301
+ } catch {
2302
+ return true;
2303
+ }
2304
+ }
2305
+ function listFilesTool(deps) {
2306
+ return defineTool({
2307
+ name: "aishell__sandbox_files_list",
2308
+ description: "List the entries in a directory of this conversation's sandbox.",
2309
+ parameters: {
2310
+ path: {
2311
+ type: "string",
2312
+ required: true,
2313
+ description: "Absolute directory path."
2314
+ },
2315
+ depth: {
2316
+ type: "number",
2317
+ description: "How many levels below the directory to include. Default 1."
2318
+ }
2319
+ },
2320
+ output: {
2321
+ schema: {
2322
+ type: "object",
2323
+ additionalProperties: false,
2324
+ properties: {
2325
+ path: {
2326
+ type: "string",
2327
+ required: true
2328
+ },
2329
+ entries: {
2330
+ type: "array",
2331
+ required: true,
2332
+ items: {
2333
+ type: "object",
2334
+ additionalProperties: false,
2335
+ properties: {
2336
+ name: {
2337
+ type: "string",
2338
+ required: true
2339
+ },
2340
+ path: {
2341
+ type: "string",
2342
+ required: true
2343
+ },
2344
+ type: {
2345
+ type: "string",
2346
+ required: true
2347
+ },
2348
+ size: { type: "integer" }
2349
+ }
2350
+ }
2351
+ }
2352
+ }
2353
+ },
2354
+ render: (_args, value) => {
2355
+ const entries = value.entries;
2356
+ if (entries.length === 0) return [{
2357
+ type: "text",
2358
+ text: `${value.path} is empty.`
2359
+ }];
2360
+ const lines = entries.map((entry) => `${entry.type === "dir" ? "d" : "-"} ${entry.name}${entry.size === void 0 ? "" : ` ${String(entry.size)} bytes`}`);
2361
+ return [{
2362
+ type: "text",
2363
+ text: lines.join("\n")
2364
+ }];
2365
+ }
2366
+ },
2367
+ timeoutMs: 6e4,
2368
+ async execute(args, exec) {
2369
+ const input = args;
2370
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2371
+ try {
2372
+ const handle = await deps.client.handle(sandboxId);
2373
+ const options = input.depth === void 0 ? void 0 : { depth: input.depth };
2374
+ const entries = await handle.files.list(input.path, options);
2375
+ return {
2376
+ path: input.path,
2377
+ entries: entries.map((entry) => ({
2378
+ name: entry.name,
2379
+ path: entry.path,
2380
+ type: entry.type === void 0 ? "unknown" : String(entry.type),
2381
+ ...entry.type === "file" ? { size: entry.size } : {}
2382
+ }))
2383
+ };
2384
+ } catch (error) {
2385
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_files_list"));
2386
+ }
2387
+ }
2388
+ });
2389
+ }
2390
+ function getPortUrlTool(deps) {
2391
+ return defineTool({
2392
+ name: "aishell__sandbox_get_port_url",
2393
+ description: "Get the public URL for a port a process is listening on inside this conversation's sandbox — the address to open a server running there. The URL resolves to the sandbox and stops working when the sandbox is deleted or expires.",
2394
+ parameters: { port: {
2395
+ type: "integer",
2396
+ required: true,
2397
+ description: "Port the process listens on inside the sandbox, e.g. 3000."
2398
+ } },
2399
+ output: {
2400
+ schema: {
2401
+ type: "object",
2402
+ additionalProperties: false,
2403
+ properties: {
2404
+ url: {
2405
+ type: "string",
2406
+ required: true
2407
+ },
2408
+ sandboxId: {
2409
+ type: "string",
2410
+ required: true
2411
+ },
2412
+ port: {
2413
+ type: "integer",
2414
+ required: true
2415
+ }
2416
+ }
2417
+ },
2418
+ render: (_args, value) => [{
2419
+ type: "text",
2420
+ text: `Port ${String(value.port)} is reachable at ${value.url}.`
2421
+ }]
2422
+ },
2423
+ timeoutMs: 6e4,
2424
+ async execute(args, exec) {
2425
+ const input = args;
2426
+ const sandboxId = await requireSandboxId(deps, sessionOf(exec), exec.signal);
2427
+ try {
2428
+ const handle = await deps.client.handle(sandboxId);
2429
+ return {
2430
+ url: `https://${handle.getHost(input.port)}`,
2431
+ sandboxId,
2432
+ port: input.port
2433
+ };
2434
+ } catch (error) {
2435
+ throw new SandboxToolError(describeBoundError(error, "aishell__sandbox_get_port_url"));
2436
+ }
2437
+ }
2438
+ });
2439
+ }
2440
+
2441
+ //#endregion
2442
+ //#region src/prompt.ts
2443
+ /** Position of the tier guidance in the assembled context. */
2444
+ const SANDBOX_PROMPT_ORDER = 110;
2445
+ /** Name this contribution registers under; unique across the assembled prompt. */
2446
+ const SANDBOX_SECTION_NAME = "aishell:e2b";
2447
+ /** What the model reads about which tier to use. */
2448
+ const SANDBOX_GUIDANCE = `A sandbox is a Linux environment running remotely. It exists for a bounded time, then is deleted along with everything in it. What it gives you that this machine does not is a public address.
2449
+
2450
+ That is the clearest signal: when the work has to be deployed somewhere reachable and the user wants a URL they can open — a web app, a demo, a service, a static page — use the sandbox tools (aishell__sandbox_*). Put the files there with aishell__sandbox_files_write, start the server with aishell__sandbox_commands_run and background: true, then call aishell__sandbox_get_port_url and give the user that address.
2451
+
2452
+ At the other end, a production system belongs in real infrastructure, deployed with the Terraform tools (aishell__terraform_* and their neighbours). It will cost money, it should keep running, and it will be operated. The sandbox is the wrong tier for it not because it is small but because it is deliberately short-lived — the address stops working when it expires.
2453
+
2454
+ So: a page or a demo, or anything that should end in a URL to open, is a sandbox. A system to run and keep running is Terraform.
2455
+
2456
+ If the request does not make clear which of these it is, ask the user before choosing. Say what the difference amounts to for them: a sandbox is throwaway and costs nothing to discard, while Terraform provisions real resources that are billed and meant to keep running. Follow what they pick.
2457
+
2458
+ A sandbox is often the right place to work something out before committing real resources to it.
2459
+
2460
+ This conversation works in at most one sandbox at a time. Create it with aishell__sandbox_create before the tools that use it, and choose its lifetime there — the value should match what the work needs, since the sandbox and its contents are deleted when it expires.`;
2461
+ /**
2462
+ * Register the guidance section.
2463
+ *
2464
+ * Deferred behind `ctx.inject` rather than declared in this plugin's own
2465
+ * `inject`: the guidance is an enhancement, and a deployment without a system
2466
+ * prompt should lose it rather than fail to mount.
2467
+ *
2468
+ * @param ctx - host context.
2469
+ */
2470
+ function registerSandboxGuidance(ctx) {
2471
+ ctx.inject(["systemPrompt"], (scope) => {
2472
+ scope.systemPrompt.section({
2473
+ name: SANDBOX_SECTION_NAME,
2474
+ order: SANDBOX_PROMPT_ORDER,
2475
+ interpolate: false,
2476
+ text: ({ scope: renderScope }) => ctx.tools.get("aishell__sandbox_create", renderScope) === void 0 ? "" : SANDBOX_GUIDANCE
2477
+ });
2478
+ });
2479
+ }
2480
+
2481
+ //#endregion
2482
+ //#region src/index.ts
2483
+ /** The provisioning service the API key is fetched from. */
2484
+ const DEFAULT_API_BASE_URL = "https://openapi.aishell.openbuildlab.com";
2485
+ /** The sandbox gateway's domain — the value an E2B client calls `E2B_DOMAIN`. */
2486
+ const DEFAULT_DOMAIN = "aishell.openbuildlab.com";
2487
+ /** Default per-request timeout for both the key fetch and SDK calls. */
2488
+ const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
2489
+ /**
2490
+ * Settings schema for this entry.
2491
+ *
2492
+ * Both URLs are Volatile: they are deployment facts that differ between a
2493
+ * staging and a production profile, and a change should apply to the next call
2494
+ * rather than requiring a restart.
2495
+ */
2496
+ const Config = Schema.object({
2497
+ apiBaseUrl: Schema.string().default(DEFAULT_API_BASE_URL).volatile(),
2498
+ domain: Schema.string().default(DEFAULT_DOMAIN).volatile(),
2499
+ requestTimeoutMs: Schema.number().step(1).min(1).default(DEFAULT_REQUEST_TIMEOUT_MS).volatile()
2500
+ });
2501
+ const name = "aishell-e2b";
2502
+ /**
2503
+ * Required services: the tool registry alone.
2504
+ *
2505
+ * The filesystem and the system prompt are both optional and taken through
2506
+ * `ctx.inject` / `ctx.get` — a deployment without a filesystem loses one
2507
+ * tool's destination, and one without a system prompt loses the guidance
2508
+ * section, neither of which should stop the sandbox tools from registering.
2509
+ */
2510
+ const inject = ["tools"];
2511
+ function apply(ctx, config) {
2512
+ const store = openCredentialStore();
2513
+ const keys = new SandboxKeySource({
2514
+ baseUrl: config.apiBaseUrl.get(),
2515
+ readCredentials: () => readCredentials(store),
2516
+ store,
2517
+ timeoutMs: config.requestTimeoutMs.get()
2518
+ });
2519
+ const client = new SandboxClient({
2520
+ resolveDomain: () => config.domain.get(),
2521
+ keys,
2522
+ resolveRequestTimeoutMs: () => config.requestTimeoutMs.get()
2523
+ });
2524
+ const registry = new SandboxRegistry(
2525
+ // Recovering a binding after a restart is a metadata query, so the
2526
+ // registry asks the gateway through the same client every tool uses.
2527
+ (metadata, signal) => client.findByMetadata(metadata, signal)
2528
+ );
2529
+ const disposers = registerSandboxTools(ctx, {
2530
+ client,
2531
+ registry,
2532
+ resolveFs: () => ctx.get("fs")
2533
+ });
2534
+ ctx.effect(() => () => {
2535
+ for (const dispose of disposers) dispose();
2536
+ }, "aishell-e2b: tools");
2537
+ ctx.effect(() => ctx.on("aishell/credentials-updated", () => {
2538
+ keys.invalidate();
2539
+ }), "aishell-e2b: credential changes");
2540
+ registerSandboxGuidance(ctx);
2541
+ }
2542
+
2543
+ //#endregion
2544
+ export { Config, SANDBOX_GUIDANCE, SANDBOX_PROMPT_ORDER, SANDBOX_SECTION_NAME, apply, inject, name };
2545
+ //# sourceMappingURL=index.js.map