@tangle-network/agent-app 0.22.0 → 0.24.0
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/dist/{DesignCanvasEditor-YPVETLZG.js → DesignCanvasEditor-C5SOZHF5.js} +2 -2
- package/dist/{chunk-NSKJFV4Y.js → chunk-GCIQVSNW.js} +4 -17
- package/dist/chunk-GCIQVSNW.js.map +1 -0
- package/dist/chunk-XDCHKFBX.js +897 -0
- package/dist/chunk-XDCHKFBX.js.map +1 -0
- package/dist/design-canvas-react/index.d.ts +90 -4
- package/dist/design-canvas-react/index.js +354 -28
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -0
- package/dist/sandbox/index.d.ts +183 -1
- package/dist/sandbox/index.js +54 -532
- package/dist/sandbox/index.js.map +1 -1
- package/dist/web-react/index.d.ts +62 -1
- package/dist/web-react/index.js +435 -142
- package/dist/web-react/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-NSKJFV4Y.js.map +0 -1
- /package/dist/{DesignCanvasEditor-YPVETLZG.js.map → DesignCanvasEditor-C5SOZHF5.js.map} +0 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertHarnessModelCompatible
|
|
3
|
+
} from "./chunk-5VXPDXZJ.js";
|
|
4
|
+
import {
|
|
5
|
+
buildAppToolMcpServer
|
|
6
|
+
} from "./chunk-A76ZHWNF.js";
|
|
7
|
+
|
|
8
|
+
// src/sandbox/index.ts
|
|
9
|
+
import {
|
|
10
|
+
Sandbox
|
|
11
|
+
} from "@tangle-network/sandbox";
|
|
12
|
+
|
|
13
|
+
// src/sandbox/workspace-terminal.ts
|
|
14
|
+
function createWorkspaceSandboxManager(opts) {
|
|
15
|
+
return {
|
|
16
|
+
async ensureWorkspaceSandbox(workspaceId, userId, options) {
|
|
17
|
+
if (!workspaceId) throw new Error("workspaceId is required");
|
|
18
|
+
if (!userId) throw new Error("userId is required");
|
|
19
|
+
const ctx = { workspaceId, userId };
|
|
20
|
+
const client = await opts.getClient(ctx);
|
|
21
|
+
const name = opts.nameForWorkspace(workspaceId, ctx);
|
|
22
|
+
let listError;
|
|
23
|
+
let existing = [];
|
|
24
|
+
try {
|
|
25
|
+
existing = await opts.listSandboxes(client, ctx);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
listError = err;
|
|
28
|
+
opts.onListError?.(err, ctx);
|
|
29
|
+
}
|
|
30
|
+
const found = existing.find((box) => box.name === name);
|
|
31
|
+
if (found) {
|
|
32
|
+
return await opts.prepareExisting?.(found, ctx, options) ?? found;
|
|
33
|
+
}
|
|
34
|
+
const created = await opts.createSandbox({
|
|
35
|
+
client,
|
|
36
|
+
ctx,
|
|
37
|
+
name,
|
|
38
|
+
options,
|
|
39
|
+
listError
|
|
40
|
+
});
|
|
41
|
+
await opts.waitForRunning?.(created, ctx);
|
|
42
|
+
return await opts.prepareCreated?.(created, ctx, options) ?? created;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var DEFAULT_TERMINAL_TOKEN_PREFIX = "sbxt_";
|
|
47
|
+
var DEFAULT_TERMINAL_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
48
|
+
var BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
49
|
+
async function createSandboxTerminalToken(subject, opts) {
|
|
50
|
+
validateTerminalSubject(subject);
|
|
51
|
+
const secret = opts.secret?.trim();
|
|
52
|
+
if (!secret) throw new Error("terminal token secret is required");
|
|
53
|
+
const now = opts.now ?? Date.now;
|
|
54
|
+
const expiresInMs = opts.expiresInMs ?? DEFAULT_TERMINAL_TOKEN_TTL_MS;
|
|
55
|
+
if (!Number.isFinite(expiresInMs) || expiresInMs <= 0) throw new Error("expiresInMs must be a positive number");
|
|
56
|
+
const expiresAt = new Date(now() + expiresInMs);
|
|
57
|
+
const payload = {
|
|
58
|
+
...subject,
|
|
59
|
+
exp: Math.floor(expiresAt.getTime() / 1e3),
|
|
60
|
+
n: crypto.randomUUID()
|
|
61
|
+
};
|
|
62
|
+
const encodedPayload = base64urlText(JSON.stringify(payload));
|
|
63
|
+
const signature = await signText(encodedPayload, secret);
|
|
64
|
+
return {
|
|
65
|
+
token: `${opts.prefix ?? DEFAULT_TERMINAL_TOKEN_PREFIX}${encodedPayload}.${signature}`,
|
|
66
|
+
expiresAt
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function verifySandboxTerminalToken(token, expected, opts) {
|
|
70
|
+
validateTerminalSubject(expected);
|
|
71
|
+
const secret = opts.secret?.trim();
|
|
72
|
+
const prefix = opts.prefix ?? DEFAULT_TERMINAL_TOKEN_PREFIX;
|
|
73
|
+
if (!secret || !token.startsWith(prefix)) return false;
|
|
74
|
+
const body = token.slice(prefix.length);
|
|
75
|
+
const dot = body.lastIndexOf(".");
|
|
76
|
+
if (dot <= 0 || dot === body.length - 1) return false;
|
|
77
|
+
const encodedPayload = body.slice(0, dot);
|
|
78
|
+
const signature = body.slice(dot + 1);
|
|
79
|
+
if (!timingSafeEqual(signature, await signText(encodedPayload, secret))) return false;
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = JSON.parse(textFromBase64url(encodedPayload));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const now = opts.now ?? Date.now;
|
|
87
|
+
return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(now() / 1e3);
|
|
88
|
+
}
|
|
89
|
+
function createWorkspaceSandboxConnectionHandler(opts) {
|
|
90
|
+
return async function handleWorkspaceSandboxConnection({ request, params }) {
|
|
91
|
+
const user = await opts.requireUser(request);
|
|
92
|
+
const workspaceId = params.workspaceId;
|
|
93
|
+
if (!workspaceId) return Response.json({ error: "workspaceId is required" }, { status: 400 });
|
|
94
|
+
await opts.requireWorkspaceAccess({ request, userId: user.id, workspaceId });
|
|
95
|
+
let box;
|
|
96
|
+
try {
|
|
97
|
+
box = await opts.ensureWorkspaceSandbox(workspaceId, user.id);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return Response.json(
|
|
100
|
+
{ error: err instanceof Error ? err.message : "Failed to provision workspace sandbox" },
|
|
101
|
+
{ status: 500 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const directSidecarUrl = box.connection?.sidecarUrl ?? (box.connection?.authToken ? box.connection?.runtimeUrl : void 0);
|
|
105
|
+
const directSidecarToken = box.connection?.authToken ?? box.connection?.sidecarToken;
|
|
106
|
+
const directSidecarExpiresAt = box.connection?.authTokenExpiresAt;
|
|
107
|
+
if (opts.exposeDirectSidecar && directSidecarUrl && directSidecarToken && directSidecarExpiresAt) {
|
|
108
|
+
return Response.json({
|
|
109
|
+
runtimeUrl: directSidecarUrl,
|
|
110
|
+
sidecarUrl: directSidecarUrl,
|
|
111
|
+
token: directSidecarToken,
|
|
112
|
+
expiresAt: directSidecarExpiresAt,
|
|
113
|
+
status: box.status,
|
|
114
|
+
sandboxId: box.id
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (!box.connection?.runtimeUrl) {
|
|
118
|
+
return Response.json(
|
|
119
|
+
{
|
|
120
|
+
error: "Workspace sandbox runtime not ready. The sandbox is still initializing -- retry in a few seconds.",
|
|
121
|
+
status: box.status
|
|
122
|
+
},
|
|
123
|
+
{ status: 503 }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const secret = typeof opts.tokenSecret === "function" ? opts.tokenSecret() : opts.tokenSecret;
|
|
127
|
+
let scoped;
|
|
128
|
+
try {
|
|
129
|
+
scoped = await createSandboxTerminalToken(
|
|
130
|
+
{ userId: user.id, workspaceId, sandboxId: box.id },
|
|
131
|
+
{ secret, expiresInMs: opts.tokenExpiresInMs, prefix: opts.tokenPrefix }
|
|
132
|
+
);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return Response.json(
|
|
135
|
+
{ error: err instanceof Error ? err.message : "Failed to mint sandbox token" },
|
|
136
|
+
{ status: 503 }
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const runtimeUrl = opts.proxyRuntimeUrl ? opts.proxyRuntimeUrl({ request, workspaceId, sandboxId: box.id, box }) : `/api/workspaces/${encodeURIComponent(workspaceId)}/sandbox/runtime/${encodeURIComponent(box.id)}`;
|
|
140
|
+
return Response.json({
|
|
141
|
+
runtimeUrl,
|
|
142
|
+
sidecarUrl: runtimeUrl,
|
|
143
|
+
token: scoped.token,
|
|
144
|
+
expiresAt: scoped.expiresAt.toISOString(),
|
|
145
|
+
status: box.status,
|
|
146
|
+
sandboxId: box.id
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function createWorkspaceSandboxRuntimeProxyHandler(opts) {
|
|
151
|
+
return async function handleWorkspaceSandboxRuntimeProxy({ request, params }) {
|
|
152
|
+
const user = await opts.requireUser(request);
|
|
153
|
+
const workspaceId = params.workspaceId;
|
|
154
|
+
const sandboxId = params.sandboxId;
|
|
155
|
+
const runtimePath = params["*"];
|
|
156
|
+
if (!workspaceId || !sandboxId || !runtimePath) {
|
|
157
|
+
return Response.json({ error: "workspaceId, sandboxId, and runtime path are required" }, { status: 400 });
|
|
158
|
+
}
|
|
159
|
+
const encodedRuntimePath = encodeSandboxRuntimePath(runtimePath);
|
|
160
|
+
if (!encodedRuntimePath) return Response.json({ error: "Invalid sandbox runtime path" }, { status: 400 });
|
|
161
|
+
await opts.requireWorkspaceAccess({ request, userId: user.id, workspaceId, sandboxId });
|
|
162
|
+
const token = terminalTokenFromRequest(request.headers);
|
|
163
|
+
const secret = typeof opts.tokenSecret === "function" ? opts.tokenSecret() : opts.tokenSecret;
|
|
164
|
+
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret, prefix: opts.tokenPrefix })) {
|
|
165
|
+
return Response.json({ error: "Invalid terminal token" }, { status: 403 });
|
|
166
|
+
}
|
|
167
|
+
const requestUrl = new URL(request.url);
|
|
168
|
+
const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
|
|
169
|
+
const credentials = runtimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
|
|
170
|
+
const upstreamUrl = runtimeConnection ? new URL(encodedRuntimePath, `${runtimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(
|
|
171
|
+
`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${encodedRuntimePath}`,
|
|
172
|
+
credentials.baseUrl
|
|
173
|
+
);
|
|
174
|
+
upstreamUrl.search = requestUrl.search;
|
|
175
|
+
const headers = buildSandboxRuntimeProxyHeaders(
|
|
176
|
+
request.headers,
|
|
177
|
+
runtimeConnection?.authToken ?? credentials.apiKey,
|
|
178
|
+
opts.forwardHeaders
|
|
179
|
+
);
|
|
180
|
+
const init = {
|
|
181
|
+
method: request.method,
|
|
182
|
+
headers,
|
|
183
|
+
redirect: "manual"
|
|
184
|
+
};
|
|
185
|
+
if (request.method !== "GET" && request.method !== "HEAD" && request.body) {
|
|
186
|
+
init.body = request.body;
|
|
187
|
+
init.duplex = "half";
|
|
188
|
+
}
|
|
189
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
190
|
+
const response = await fetchImpl(upstreamUrl, init);
|
|
191
|
+
const responseHeaders = new Headers(response.headers);
|
|
192
|
+
responseHeaders.delete("set-cookie");
|
|
193
|
+
return new Response(response.body, {
|
|
194
|
+
status: response.status,
|
|
195
|
+
statusText: response.statusText,
|
|
196
|
+
headers: responseHeaders
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
var SANDBOX_TERMINAL_WS_PATHNAME = /^\/api\/workspaces\/([^/]+)\/sandbox\/runtime\/([^/]+)\/(terminals\/[^/]+\/ws)$/;
|
|
201
|
+
function matchSandboxTerminalWsPath(pathname) {
|
|
202
|
+
const m = SANDBOX_TERMINAL_WS_PATHNAME.exec(pathname);
|
|
203
|
+
if (!m) return null;
|
|
204
|
+
const [, workspaceId, sandboxId, subPath] = m;
|
|
205
|
+
if (!workspaceId || !sandboxId || !subPath) return null;
|
|
206
|
+
return { workspaceId: decodeURIComponent(workspaceId), sandboxId: decodeURIComponent(sandboxId), subPath };
|
|
207
|
+
}
|
|
208
|
+
function isSandboxTerminalWsUpgrade(request) {
|
|
209
|
+
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return false;
|
|
210
|
+
try {
|
|
211
|
+
return matchSandboxTerminalWsPath(new URL(request.url).pathname) !== null;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function createWorkspaceSandboxTerminalUpgradeHandler(opts) {
|
|
217
|
+
return async function handleWorkspaceSandboxTerminalUpgrade(request) {
|
|
218
|
+
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return null;
|
|
219
|
+
let url;
|
|
220
|
+
try {
|
|
221
|
+
url = new URL(request.url);
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const match = matchSandboxTerminalWsPath(url.pathname);
|
|
226
|
+
if (!match) return null;
|
|
227
|
+
const { workspaceId, sandboxId, subPath } = match;
|
|
228
|
+
let user;
|
|
229
|
+
try {
|
|
230
|
+
user = await opts.requireUser(request);
|
|
231
|
+
} catch {
|
|
232
|
+
return new Response("Unauthorized", { status: 401 });
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
await opts.requireWorkspaceAccess({ request, userId: user.id, workspaceId, sandboxId });
|
|
236
|
+
} catch {
|
|
237
|
+
return new Response("Forbidden", { status: 403 });
|
|
238
|
+
}
|
|
239
|
+
const token = terminalTokenFromRequest(request.headers);
|
|
240
|
+
const secret = typeof opts.tokenSecret === "function" ? opts.tokenSecret() : opts.tokenSecret;
|
|
241
|
+
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret, prefix: opts.tokenPrefix })) {
|
|
242
|
+
return new Response("Invalid terminal token", { status: 403 });
|
|
243
|
+
}
|
|
244
|
+
const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
|
|
245
|
+
const credentials = runtimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
|
|
246
|
+
const upstreamUrl = runtimeConnection ? new URL(subPath, `${runtimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${subPath}`, credentials.baseUrl);
|
|
247
|
+
upstreamUrl.search = url.search;
|
|
248
|
+
const upstreamHeaders = new Headers(request.headers);
|
|
249
|
+
upstreamHeaders.set("Authorization", `Bearer ${runtimeConnection?.authToken ?? credentials.apiKey}`);
|
|
250
|
+
upstreamHeaders.delete("host");
|
|
251
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
252
|
+
return fetchImpl(upstreamUrl.toString(), { method: request.method, headers: upstreamHeaders });
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
var DEFAULT_RUNTIME_PROXY_HEADERS = ["accept", "content-type", "last-event-id", "x-session-id"];
|
|
256
|
+
function buildSandboxRuntimeProxyHeaders(source, sandboxApiKey, forwardHeaders = DEFAULT_RUNTIME_PROXY_HEADERS) {
|
|
257
|
+
const headers = new Headers();
|
|
258
|
+
headers.set("Authorization", `Bearer ${sandboxApiKey}`);
|
|
259
|
+
for (const name of forwardHeaders) {
|
|
260
|
+
const value = source.get(name);
|
|
261
|
+
if (value) headers.set(name, value);
|
|
262
|
+
}
|
|
263
|
+
return headers;
|
|
264
|
+
}
|
|
265
|
+
function encodeSandboxRuntimePath(runtimePath) {
|
|
266
|
+
const segments = runtimePath.split("/");
|
|
267
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) return null;
|
|
268
|
+
return segments.map((segment) => encodeURIComponent(segment)).join("/");
|
|
269
|
+
}
|
|
270
|
+
function bearerToken(value) {
|
|
271
|
+
if (!value) return null;
|
|
272
|
+
const trimmed = value.trim();
|
|
273
|
+
if (!trimmed) return null;
|
|
274
|
+
if (trimmed.toLowerCase() === "bearer") return null;
|
|
275
|
+
if (trimmed.toLowerCase().startsWith("bearer ")) {
|
|
276
|
+
const token = trimmed.slice("bearer ".length).trim();
|
|
277
|
+
return token || null;
|
|
278
|
+
}
|
|
279
|
+
return trimmed;
|
|
280
|
+
}
|
|
281
|
+
function bearerSubprotocolToken(value) {
|
|
282
|
+
if (!value) return null;
|
|
283
|
+
for (const part of value.split(",")) {
|
|
284
|
+
const protocol = part.trim();
|
|
285
|
+
if (!protocol.toLowerCase().startsWith(BEARER_SUBPROTOCOL_PREFIX)) continue;
|
|
286
|
+
const encoded = protocol.slice(BEARER_SUBPROTOCOL_PREFIX.length);
|
|
287
|
+
if (!encoded) return null;
|
|
288
|
+
try {
|
|
289
|
+
const token = textFromBase64url(encoded).trim();
|
|
290
|
+
return token || null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function terminalTokenFromRequest(headers) {
|
|
298
|
+
return bearerToken(headers.get("Authorization")) ?? bearerSubprotocolToken(headers.get("Sec-WebSocket-Protocol"));
|
|
299
|
+
}
|
|
300
|
+
function validateTerminalSubject(subject) {
|
|
301
|
+
if (!subject.userId) throw new Error("userId is required");
|
|
302
|
+
if (!subject.workspaceId) throw new Error("workspaceId is required");
|
|
303
|
+
if (!subject.sandboxId) throw new Error("sandboxId is required");
|
|
304
|
+
}
|
|
305
|
+
async function signText(message, secret) {
|
|
306
|
+
const enc = new TextEncoder();
|
|
307
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
308
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
309
|
+
return base64url(new Uint8Array(sig));
|
|
310
|
+
}
|
|
311
|
+
function base64urlText(text) {
|
|
312
|
+
return base64url(new TextEncoder().encode(text));
|
|
313
|
+
}
|
|
314
|
+
function textFromBase64url(value) {
|
|
315
|
+
const b64 = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
316
|
+
const bin = atob(b64);
|
|
317
|
+
const bytes = new Uint8Array(bin.length);
|
|
318
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
319
|
+
return new TextDecoder().decode(bytes);
|
|
320
|
+
}
|
|
321
|
+
function base64url(bytes) {
|
|
322
|
+
let s = "";
|
|
323
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
324
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
325
|
+
}
|
|
326
|
+
function timingSafeEqual(a, b) {
|
|
327
|
+
if (a.length !== b.length) return false;
|
|
328
|
+
let diff = 0;
|
|
329
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
330
|
+
return diff === 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/sandbox/index.ts
|
|
334
|
+
var ok = (value) => ({ succeeded: true, value });
|
|
335
|
+
var fail = (error) => ({
|
|
336
|
+
succeeded: false,
|
|
337
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
338
|
+
});
|
|
339
|
+
var DEFAULT_SANDBOX_RESOURCES = {
|
|
340
|
+
image: "universal",
|
|
341
|
+
cpuCores: 2,
|
|
342
|
+
memoryMB: 4096,
|
|
343
|
+
diskGB: 10,
|
|
344
|
+
maxLifetimeSeconds: 86400,
|
|
345
|
+
idleTimeoutSeconds: 3600
|
|
346
|
+
};
|
|
347
|
+
var _cached = null;
|
|
348
|
+
function getClientFromCreds(creds) {
|
|
349
|
+
const fingerprint = `${creds.apiKey} ${creds.baseUrl}`;
|
|
350
|
+
if (_cached && _cached.fingerprint === fingerprint) return _cached.client;
|
|
351
|
+
const client = new Sandbox({ apiKey: creds.apiKey, baseUrl: creds.baseUrl });
|
|
352
|
+
_cached = { client, fingerprint };
|
|
353
|
+
return client;
|
|
354
|
+
}
|
|
355
|
+
function getClient(shell) {
|
|
356
|
+
const creds = shell.credentials();
|
|
357
|
+
if (creds && typeof creds.then === "function") {
|
|
358
|
+
throw new Error("getClient: scoped (async) credentials require the async sandbox path");
|
|
359
|
+
}
|
|
360
|
+
if (!creds) throw new Error("sandbox credentials are required (apiKey/baseUrl)");
|
|
361
|
+
return getClientFromCreds(creds);
|
|
362
|
+
}
|
|
363
|
+
function resetClientCache() {
|
|
364
|
+
_cached = null;
|
|
365
|
+
}
|
|
366
|
+
function buildAppToolMcpServers(options) {
|
|
367
|
+
const entries = {};
|
|
368
|
+
for (const { tool, key, description } of options.tools) {
|
|
369
|
+
entries[key] = buildAppToolMcpServer({
|
|
370
|
+
tool,
|
|
371
|
+
baseUrl: options.baseUrl,
|
|
372
|
+
token: options.token,
|
|
373
|
+
ctx: options.ctx,
|
|
374
|
+
description,
|
|
375
|
+
headerNames: options.headerNames
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
function shellSingleQuote(value) {
|
|
381
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
382
|
+
}
|
|
383
|
+
async function listRunning(client, name) {
|
|
384
|
+
try {
|
|
385
|
+
const running = await client.list({ status: "running" });
|
|
386
|
+
return ok(running.find((s) => s.name === name) ?? null);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return fail(err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function deleteBox(box) {
|
|
392
|
+
try {
|
|
393
|
+
await box.delete();
|
|
394
|
+
return ok(void 0);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
return fail(err);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function isBoxAlive(box, harness, probe) {
|
|
400
|
+
if (!probe) return true;
|
|
401
|
+
const execTimeout = probe.execTimeoutMs ?? 5e3;
|
|
402
|
+
const psTimeout = probe.psTimeoutMs ?? 3e3;
|
|
403
|
+
const race = (p, ms, label) => Promise.race([
|
|
404
|
+
p,
|
|
405
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(label)), ms))
|
|
406
|
+
]);
|
|
407
|
+
try {
|
|
408
|
+
const alive = await race(box.exec("echo alive"), execTimeout, "alive check timeout");
|
|
409
|
+
if (!alive.stdout.includes("alive")) return false;
|
|
410
|
+
const pattern = probe.sidecarProcessPattern(harness);
|
|
411
|
+
try {
|
|
412
|
+
const ps = await race(
|
|
413
|
+
box.exec(`pgrep -f ${shellSingleQuote(pattern)} || echo no-sidecar`),
|
|
414
|
+
psTimeout,
|
|
415
|
+
"ps check timeout"
|
|
416
|
+
);
|
|
417
|
+
if (ps.stdout.includes("no-sidecar")) return false;
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function resumeStoppedBox(client, name, timeoutMs) {
|
|
426
|
+
try {
|
|
427
|
+
const stopped = await client.list({ status: "stopped" });
|
|
428
|
+
const match = stopped.find((s) => s.name === name) ?? null;
|
|
429
|
+
if (!match) return ok(null);
|
|
430
|
+
await match.resume();
|
|
431
|
+
await match.waitFor("running", { timeoutMs });
|
|
432
|
+
return ok(match);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
return fail(err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function ensureWorkspaceSandbox(shell, options) {
|
|
438
|
+
const { workspaceId, userId, harness, forceNew } = options;
|
|
439
|
+
const scope = { workspaceId, ...userId ? { userId } : {} };
|
|
440
|
+
const creds = await shell.credentials(scope);
|
|
441
|
+
if (!creds) throw new Error("sandbox credentials are required (apiKey/baseUrl)");
|
|
442
|
+
const client = getClientFromCreds(creds);
|
|
443
|
+
const name = shell.boxKey ? shell.boxKey(scope) : shell.name(workspaceId);
|
|
444
|
+
const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
|
|
445
|
+
const resumeTimeout = 12e4;
|
|
446
|
+
const existing = await listRunning(client, name);
|
|
447
|
+
if (existing.succeeded && existing.value) {
|
|
448
|
+
const found = existing.value;
|
|
449
|
+
if (forceNew) {
|
|
450
|
+
const dropped = await deleteBox(found);
|
|
451
|
+
if (!dropped.succeeded) {
|
|
452
|
+
throw new Error(`forceNew: sandbox ${name} could not be deleted`, { cause: dropped.error });
|
|
453
|
+
}
|
|
454
|
+
} else if (found.metadata?.harness === harness && await isBoxAlive(found, harness, shell.livenessProbe)) {
|
|
455
|
+
if (shell.bootstrap) {
|
|
456
|
+
const boot = await shell.bootstrap(found, scope);
|
|
457
|
+
if (!boot.succeeded) {
|
|
458
|
+
throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return found;
|
|
462
|
+
} else {
|
|
463
|
+
const dropped = await deleteBox(found);
|
|
464
|
+
if (!dropped.succeeded) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
|
|
467
|
+
{ cause: dropped.error }
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!forceNew && shell.resumeStopped !== false) {
|
|
473
|
+
const resumed = await resumeStoppedBox(client, name, resumeTimeout);
|
|
474
|
+
if (resumed.succeeded && resumed.value && await isBoxAlive(resumed.value, harness, shell.livenessProbe)) {
|
|
475
|
+
const box2 = resumed.value;
|
|
476
|
+
if (shell.bootstrap) {
|
|
477
|
+
const boot = await shell.bootstrap(box2, scope);
|
|
478
|
+
if (!boot.succeeded) {
|
|
479
|
+
throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return box2;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId);
|
|
486
|
+
const buildCtx = {
|
|
487
|
+
workspaceId,
|
|
488
|
+
connectedIntegrationIds,
|
|
489
|
+
...userId ? { userId } : {}
|
|
490
|
+
};
|
|
491
|
+
const [secrets, env, files] = await Promise.all([
|
|
492
|
+
shell.secrets(workspaceId),
|
|
493
|
+
shell.env(buildCtx),
|
|
494
|
+
shell.files(buildCtx)
|
|
495
|
+
]);
|
|
496
|
+
const profile = shell.profile({ extraFiles: files });
|
|
497
|
+
const role = userId && shell.permissionRole ? shell.permissionRole("developer") : void 0;
|
|
498
|
+
let model = shell.backendModelAtCreate ? resolveModel(shell.provider) : void 0;
|
|
499
|
+
if (model && shell.childKeyMint && model.provider === "openai-compat") {
|
|
500
|
+
const minted = await shell.childKeyMint(scope);
|
|
501
|
+
if (minted.succeeded) model = { ...model, apiKey: minted.value };
|
|
502
|
+
else {
|
|
503
|
+
console.error(
|
|
504
|
+
`[sandbox] childKeyMint failed for ${workspaceId}; using parent key:`,
|
|
505
|
+
minted.error.message
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const storage = shell.storage?.(buildCtx);
|
|
510
|
+
const restore = shell.restore?.(buildCtx);
|
|
511
|
+
const payload = {
|
|
512
|
+
name,
|
|
513
|
+
image: resources.image,
|
|
514
|
+
metadata: shell.metadata(harness),
|
|
515
|
+
...userId ? { permissions: { initialUsers: [{ userId, role }] } } : {},
|
|
516
|
+
env,
|
|
517
|
+
secrets,
|
|
518
|
+
backend: { type: harness, profile, ...model ? { model } : {} },
|
|
519
|
+
...storage ? { storage } : {},
|
|
520
|
+
...restore ? restore : {},
|
|
521
|
+
maxLifetimeSeconds: resources.maxLifetimeSeconds,
|
|
522
|
+
idleTimeoutSeconds: resources.idleTimeoutSeconds,
|
|
523
|
+
resources: {
|
|
524
|
+
cpuCores: resources.cpuCores,
|
|
525
|
+
memoryMB: resources.memoryMB,
|
|
526
|
+
diskGB: resources.diskGB
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
const box = await client.create(payload);
|
|
530
|
+
await box.waitFor("running", { timeoutMs: 12e4 });
|
|
531
|
+
if (!box.connection?.runtimeUrl) await box.refresh();
|
|
532
|
+
if (shell.bootstrap) {
|
|
533
|
+
const boot = await shell.bootstrap(box, scope);
|
|
534
|
+
if (!boot.succeeded) {
|
|
535
|
+
throw new Error(`bootstrap failed on new box ${name}`, { cause: boot.error });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return box;
|
|
539
|
+
}
|
|
540
|
+
function resolveModel(config, override) {
|
|
541
|
+
const c = config ?? {};
|
|
542
|
+
const explicitBaseUrl = c.routerBaseUrl;
|
|
543
|
+
const explicitApiKey = override?.modelApiKey ?? c.apiKey;
|
|
544
|
+
const provider = c.providerName ?? (explicitApiKey ? "openai-compat" : c.openaiApiKey ? "openai" : void 0);
|
|
545
|
+
const modelName = override?.model ?? c.modelName ?? (provider === "openai" || provider === "openai-compat" ? c.defaultModel : void 0);
|
|
546
|
+
const apiKey = explicitApiKey ?? (provider === "openai" ? c.openaiApiKey : void 0);
|
|
547
|
+
if (!provider || !modelName || !apiKey) return void 0;
|
|
548
|
+
return {
|
|
549
|
+
model: modelName,
|
|
550
|
+
provider,
|
|
551
|
+
apiKey,
|
|
552
|
+
...explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function flattenHistory(message, history) {
|
|
556
|
+
if (!history?.length) return message;
|
|
557
|
+
const transcript = history.map((entry) => `${entry.role === "assistant" ? "Assistant" : "User"}: ${entry.content}`).join("\n\n");
|
|
558
|
+
return `${transcript}
|
|
559
|
+
|
|
560
|
+
User: ${message}`;
|
|
561
|
+
}
|
|
562
|
+
function mergeExtraMcp(appToolMcp, baseProfileMcp, extra) {
|
|
563
|
+
for (const key of Object.keys(extra ?? {})) {
|
|
564
|
+
if (key in appToolMcp || key in baseProfileMcp) {
|
|
565
|
+
throw new Error(`extraMcp key '${key}' collides with an existing profile MCP server`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { ...appToolMcp, ...extra ?? {} };
|
|
569
|
+
}
|
|
570
|
+
function attachReasoningEffort(profile, harness, effort) {
|
|
571
|
+
if (!effort || effort === "auto") return profile;
|
|
572
|
+
return {
|
|
573
|
+
...profile,
|
|
574
|
+
extensions: {
|
|
575
|
+
...profile.extensions ?? {},
|
|
576
|
+
[harness]: {
|
|
577
|
+
...profile.extensions?.[harness] ?? {},
|
|
578
|
+
reasoningEffort: effort
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function* streamSandboxPrompt(shell, box, message, options) {
|
|
584
|
+
const harness = options?.harness ?? "opencode";
|
|
585
|
+
const model = resolveModel(shell.provider, {
|
|
586
|
+
model: options?.model,
|
|
587
|
+
modelApiKey: options?.modelApiKey
|
|
588
|
+
});
|
|
589
|
+
if (model?.model) assertHarnessModelCompatible(harness, model.model);
|
|
590
|
+
const prompt = flattenHistory(message, options?.history);
|
|
591
|
+
const appToolMcp = options?.appToolMcp ?? {};
|
|
592
|
+
const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp);
|
|
593
|
+
const profile = shell.profile({ systemPrompt: options?.systemPrompt, extraMcp });
|
|
594
|
+
const profileWithEffort = attachReasoningEffort(profile, harness, options?.effort);
|
|
595
|
+
const stream = box.streamPrompt(prompt, {
|
|
596
|
+
sessionId: options?.sessionId,
|
|
597
|
+
executionId: options?.executionId,
|
|
598
|
+
lastEventId: options?.lastEventId,
|
|
599
|
+
...options?.signal ? { signal: options.signal } : {},
|
|
600
|
+
...options?.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
601
|
+
backend: {
|
|
602
|
+
type: harness,
|
|
603
|
+
profile: profileWithEffort,
|
|
604
|
+
...model ? { model } : {}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
let severedFinishReason = null;
|
|
608
|
+
for await (const event of stream) {
|
|
609
|
+
const step = classifySeveredStream(event);
|
|
610
|
+
if (step) severedFinishReason = step.kind === "step-finish" && step.severed ? step.reason : null;
|
|
611
|
+
if (severedFinishReason && isTerminalPromptEvent(event)) {
|
|
612
|
+
throw new Error(`sandbox model stream severed mid-turn (reason="${severedFinishReason}")`);
|
|
613
|
+
}
|
|
614
|
+
if (options?.disallowQuestions) {
|
|
615
|
+
const q = detectInteractiveQuestion(event);
|
|
616
|
+
if (q) {
|
|
617
|
+
throw new Error(`sandbox agent asked an interactive question during an autonomous run: ${q}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
yield event;
|
|
621
|
+
}
|
|
622
|
+
if (severedFinishReason) {
|
|
623
|
+
throw new Error(`sandbox model stream severed mid-turn (reason="${severedFinishReason}")`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async function runSandboxPrompt(shell, box, message, options) {
|
|
627
|
+
let fullText = "";
|
|
628
|
+
let firstTextSeen = false;
|
|
629
|
+
for await (const rawEvent of streamSandboxPrompt(shell, box, message, options)) {
|
|
630
|
+
const event = rawEvent;
|
|
631
|
+
if (!event.type) continue;
|
|
632
|
+
if (event.type === "message.part.updated") {
|
|
633
|
+
const part = event.data?.part;
|
|
634
|
+
const delta = typeof event.data?.delta === "string" ? event.data.delta : null;
|
|
635
|
+
if (String(part?.type ?? "") === "text") {
|
|
636
|
+
if (!firstTextSeen) {
|
|
637
|
+
firstTextSeen = true;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (delta) fullText += delta;
|
|
641
|
+
else if (typeof part?.text === "string") fullText = part.text;
|
|
642
|
+
}
|
|
643
|
+
} else if (event.type === "result") {
|
|
644
|
+
const finalText = typeof event.data?.finalText === "string" ? event.data.finalText : null;
|
|
645
|
+
if (finalText) fullText = finalText;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return fullText;
|
|
649
|
+
}
|
|
650
|
+
async function syncSandboxMemberAdd(box, seam, userId, role) {
|
|
651
|
+
try {
|
|
652
|
+
await box.permissions.add({ userId, role: seam.roleToSandboxRole(role) });
|
|
653
|
+
return ok(void 0);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
return fail(err);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function syncSandboxMemberRemove(box, userId) {
|
|
659
|
+
try {
|
|
660
|
+
await box.permissions.remove(userId, { preserveHomeDir: true });
|
|
661
|
+
return ok(void 0);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
return fail(err);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function syncSandboxMemberRole(box, seam, userId, role) {
|
|
667
|
+
try {
|
|
668
|
+
await box.permissions.update(userId, { role: seam.roleToSandboxRole(role) });
|
|
669
|
+
return ok(void 0);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
return fail(err);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function secretStoreFromClient(shell) {
|
|
675
|
+
const client = getClient(shell);
|
|
676
|
+
return {
|
|
677
|
+
create: async (name, value) => {
|
|
678
|
+
await client.secrets.create(name, value);
|
|
679
|
+
},
|
|
680
|
+
update: async (name, value) => {
|
|
681
|
+
await client.secrets.update(name, value);
|
|
682
|
+
},
|
|
683
|
+
get: (name) => client.secrets.get(name),
|
|
684
|
+
delete: async (name) => {
|
|
685
|
+
await client.secrets.delete(name);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async function storeSecret(store, name, value) {
|
|
690
|
+
try {
|
|
691
|
+
await store.create(name, value);
|
|
692
|
+
return ok(void 0);
|
|
693
|
+
} catch {
|
|
694
|
+
try {
|
|
695
|
+
await store.update(name, value);
|
|
696
|
+
return ok(void 0);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
return fail(new Error(`Failed to store sandbox secret ${name}`, { cause: err }));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function readSecret(store, name) {
|
|
703
|
+
try {
|
|
704
|
+
return ok(await store.get(name));
|
|
705
|
+
} catch (err) {
|
|
706
|
+
return fail(err);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function deleteSecret(store, name) {
|
|
710
|
+
try {
|
|
711
|
+
await store.delete(name);
|
|
712
|
+
return ok(void 0);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return fail(err);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function mintSandboxScopedToken(box, options) {
|
|
718
|
+
try {
|
|
719
|
+
const token = await box.mintScopedToken({
|
|
720
|
+
scope: options.scope,
|
|
721
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
722
|
+
...options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}
|
|
723
|
+
});
|
|
724
|
+
return ok({ token: token.token, expiresAt: token.expiresAt, scope: token.scope });
|
|
725
|
+
} catch (err) {
|
|
726
|
+
return fail(err);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function driveSandboxTurn(shell, box, message, options) {
|
|
730
|
+
const harness = options.harness ?? "opencode";
|
|
731
|
+
const model = resolveModel(shell.provider, {
|
|
732
|
+
model: options.model,
|
|
733
|
+
modelApiKey: options.modelApiKey
|
|
734
|
+
});
|
|
735
|
+
if (model?.model) assertHarnessModelCompatible(harness, model.model);
|
|
736
|
+
const prompt = flattenHistory(message, options.history);
|
|
737
|
+
const appToolMcp = options.appToolMcp ?? {};
|
|
738
|
+
const extraMcp = mergeExtraMcp(appToolMcp, options.baseProfileMcp ?? {}, options.extraMcp);
|
|
739
|
+
const profile = attachReasoningEffort(
|
|
740
|
+
shell.profile({ systemPrompt: options.systemPrompt, extraMcp }),
|
|
741
|
+
harness,
|
|
742
|
+
options.effort
|
|
743
|
+
);
|
|
744
|
+
try {
|
|
745
|
+
const result = await box.prompt(prompt, {
|
|
746
|
+
sessionId: options.sessionId,
|
|
747
|
+
...options.executionId ? { executionId: options.executionId } : {},
|
|
748
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
749
|
+
...options.signal ? { signal: options.signal } : {},
|
|
750
|
+
backend: { type: harness, profile, ...model ? { model } : {} }
|
|
751
|
+
});
|
|
752
|
+
if (!result.success) return fail(new Error(result.error ?? "sandbox turn failed"));
|
|
753
|
+
return ok(result);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
return fail(err);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
var TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
759
|
+
async function signTerminalProxyToken(secret, encodedPayload) {
|
|
760
|
+
const key = await crypto.subtle.importKey(
|
|
761
|
+
"raw",
|
|
762
|
+
new TextEncoder().encode(secret),
|
|
763
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
764
|
+
false,
|
|
765
|
+
["sign"]
|
|
766
|
+
);
|
|
767
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(encodedPayload));
|
|
768
|
+
return base64UrlEncodeBytes(new Uint8Array(sig));
|
|
769
|
+
}
|
|
770
|
+
async function mintTerminalProxyToken(secret, identity, ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS) {
|
|
771
|
+
if (!secret) return fail(new Error("mintTerminalProxyToken: secret is required"));
|
|
772
|
+
if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {
|
|
773
|
+
return fail(new Error("mintTerminalProxyToken: userId/workspaceId/sandboxId are required"));
|
|
774
|
+
}
|
|
775
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
776
|
+
const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1e3) };
|
|
777
|
+
const encoded = base64UrlEncodeUtf8(JSON.stringify(payload));
|
|
778
|
+
const sig = await signTerminalProxyToken(secret, encoded);
|
|
779
|
+
return ok({ token: `${encoded}.${sig}`, expiresAt });
|
|
780
|
+
}
|
|
781
|
+
async function verifyTerminalProxyToken(secret, token, expected) {
|
|
782
|
+
if (!secret) return false;
|
|
783
|
+
const [encoded, sig, extra] = token.split(".");
|
|
784
|
+
if (!encoded || !sig || extra !== void 0) return false;
|
|
785
|
+
const expectedSig = await signTerminalProxyToken(secret, encoded);
|
|
786
|
+
if (!constantTimeEqual(sig, expectedSig)) return false;
|
|
787
|
+
let payload;
|
|
788
|
+
try {
|
|
789
|
+
payload = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
790
|
+
} catch {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(Date.now() / 1e3);
|
|
794
|
+
}
|
|
795
|
+
function base64UrlEncodeBytes(bytes) {
|
|
796
|
+
let bin = "";
|
|
797
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
798
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
799
|
+
}
|
|
800
|
+
function base64UrlEncodeUtf8(v) {
|
|
801
|
+
return base64UrlEncodeBytes(new TextEncoder().encode(v));
|
|
802
|
+
}
|
|
803
|
+
function base64UrlDecodeUtf8(v) {
|
|
804
|
+
const padded = v.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(v.length / 4) * 4, "=");
|
|
805
|
+
const bin = atob(padded);
|
|
806
|
+
const bytes = new Uint8Array(bin.length);
|
|
807
|
+
for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
|
|
808
|
+
return new TextDecoder().decode(bytes);
|
|
809
|
+
}
|
|
810
|
+
function constantTimeEqual(a, b) {
|
|
811
|
+
if (a.length !== b.length) return false;
|
|
812
|
+
let r = 0;
|
|
813
|
+
for (let i = 0; i < a.length; i += 1) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
814
|
+
return r === 0;
|
|
815
|
+
}
|
|
816
|
+
var SEVERED_FINISH_REASONS = /* @__PURE__ */ new Set(["error", "other", "unknown"]);
|
|
817
|
+
function asPlainRecord(v) {
|
|
818
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
819
|
+
}
|
|
820
|
+
function classifySeveredStream(event) {
|
|
821
|
+
const root = asPlainRecord(event);
|
|
822
|
+
if (!root || root.type !== "message.part.updated") return null;
|
|
823
|
+
const body = asPlainRecord(root.properties) ?? asPlainRecord(root.data) ?? root;
|
|
824
|
+
const part = asPlainRecord(body.part);
|
|
825
|
+
if (!part) return null;
|
|
826
|
+
if (part.type === "step-start") return { kind: "step-start" };
|
|
827
|
+
if (part.type !== "step-finish") return null;
|
|
828
|
+
const reason = typeof part.reason === "string" && part.reason ? part.reason : "unknown";
|
|
829
|
+
return { kind: "step-finish", reason, severed: SEVERED_FINISH_REASONS.has(reason) };
|
|
830
|
+
}
|
|
831
|
+
function isTerminalPromptEvent(event) {
|
|
832
|
+
const t = asPlainRecord(event)?.type;
|
|
833
|
+
return t === "result" || t === "done";
|
|
834
|
+
}
|
|
835
|
+
function detectInteractiveQuestion(event) {
|
|
836
|
+
const root = asPlainRecord(event);
|
|
837
|
+
if (!root) return null;
|
|
838
|
+
const type = typeof root.type === "string" ? root.type : void 0;
|
|
839
|
+
const data = asPlainRecord(root.data);
|
|
840
|
+
const props = asPlainRecord(root.properties);
|
|
841
|
+
const body = props ?? data ?? root;
|
|
842
|
+
if (type === "question.asked" || type === "question") return firstQuestionText(body);
|
|
843
|
+
const part = asPlainRecord(data?.part) ?? asPlainRecord(body.part);
|
|
844
|
+
const tool = typeof part?.tool === "string" && part.tool || typeof part?.name === "string" && part.name || typeof body.tool === "string" && body.tool || void 0;
|
|
845
|
+
const isQ = type === "message.part.updated" && (tool === "question" || asPlainRecord(part)?.type === "question");
|
|
846
|
+
if (!isQ) return null;
|
|
847
|
+
const state = asPlainRecord(asPlainRecord(part)?.state);
|
|
848
|
+
return firstQuestionText(asPlainRecord(state?.input) ?? state ?? part ?? body);
|
|
849
|
+
}
|
|
850
|
+
function firstQuestionText(value) {
|
|
851
|
+
const arr = Array.isArray(value?.questions) ? value.questions : Array.isArray(asPlainRecord(value?.input)?.questions) ? asPlainRecord(value.input).questions : [];
|
|
852
|
+
const first = asPlainRecord(arr[0]);
|
|
853
|
+
const q = typeof first?.question === "string" && first.question || typeof first?.prompt === "string" && first.prompt || void 0;
|
|
854
|
+
return q ?? "interactive question";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export {
|
|
858
|
+
createWorkspaceSandboxManager,
|
|
859
|
+
createSandboxTerminalToken,
|
|
860
|
+
verifySandboxTerminalToken,
|
|
861
|
+
createWorkspaceSandboxConnectionHandler,
|
|
862
|
+
createWorkspaceSandboxRuntimeProxyHandler,
|
|
863
|
+
matchSandboxTerminalWsPath,
|
|
864
|
+
isSandboxTerminalWsUpgrade,
|
|
865
|
+
createWorkspaceSandboxTerminalUpgradeHandler,
|
|
866
|
+
buildSandboxRuntimeProxyHeaders,
|
|
867
|
+
encodeSandboxRuntimePath,
|
|
868
|
+
bearerToken,
|
|
869
|
+
bearerSubprotocolToken,
|
|
870
|
+
terminalTokenFromRequest,
|
|
871
|
+
DEFAULT_SANDBOX_RESOURCES,
|
|
872
|
+
getClient,
|
|
873
|
+
resetClientCache,
|
|
874
|
+
buildAppToolMcpServers,
|
|
875
|
+
ensureWorkspaceSandbox,
|
|
876
|
+
resolveModel,
|
|
877
|
+
flattenHistory,
|
|
878
|
+
mergeExtraMcp,
|
|
879
|
+
attachReasoningEffort,
|
|
880
|
+
streamSandboxPrompt,
|
|
881
|
+
runSandboxPrompt,
|
|
882
|
+
syncSandboxMemberAdd,
|
|
883
|
+
syncSandboxMemberRemove,
|
|
884
|
+
syncSandboxMemberRole,
|
|
885
|
+
secretStoreFromClient,
|
|
886
|
+
storeSecret,
|
|
887
|
+
readSecret,
|
|
888
|
+
deleteSecret,
|
|
889
|
+
mintSandboxScopedToken,
|
|
890
|
+
driveSandboxTurn,
|
|
891
|
+
mintTerminalProxyToken,
|
|
892
|
+
verifyTerminalProxyToken,
|
|
893
|
+
classifySeveredStream,
|
|
894
|
+
isTerminalPromptEvent,
|
|
895
|
+
detectInteractiveQuestion
|
|
896
|
+
};
|
|
897
|
+
//# sourceMappingURL=chunk-XDCHKFBX.js.map
|