@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/LICENSE +201 -0
- package/README.md +200 -2
- package/cordis.patch.yml +19 -0
- package/lib/index.js +2545 -0
- package/lib/types/client.d.ts +137 -0
- package/lib/types/credentials.d.ts +47 -0
- package/lib/types/errors.d.ts +47 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/prompt.d.ts +62 -0
- package/lib/types/sandbox-key.d.ts +178 -0
- package/lib/types/sandbox-registry.d.ts +114 -0
- package/lib/types/tools.d.ts +46 -0
- package/package.json +53 -3
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
|