@tangle-network/agent-app 0.60.213 → 0.60.215
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/chat-routes/index.js +2 -2
- package/dist/chat-routes/turn-routes.d.ts +34 -1
- package/dist/{chunk-N7FGEWZU.js → chunk-QA2CBATZ.js} +238 -88
- package/dist/chunk-QA2CBATZ.js.map +1 -0
- package/dist/{chunk-IHLO2DN5.js → chunk-WLH4DEB2.js} +47 -1
- package/dist/chunk-WLH4DEB2.js.map +1 -0
- package/dist/platform/delegated-keys.d.ts +3 -1
- package/dist/platform/index.js +10 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/public-consultation/index.js +1 -1
- package/dist/sandbox/foreground-single-flight.d.ts +15 -0
- package/dist/sandbox/index.d.ts +46 -9
- package/dist/sandbox/index.js +5 -1
- package/package.json +1 -1
- package/dist/chunk-IHLO2DN5.js.map +0 -1
- package/dist/chunk-N7FGEWZU.js.map +0 -1
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
flattenHistory,
|
|
41
41
|
readSandboxBinaryBytes,
|
|
42
42
|
statSandboxFileSize
|
|
43
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-QA2CBATZ.js";
|
|
44
44
|
import "../chunk-4LBCTFBP.js";
|
|
45
45
|
import {
|
|
46
46
|
ModelFailoverExhaustedError,
|
|
@@ -80,7 +80,7 @@ import {
|
|
|
80
80
|
rowIdOf,
|
|
81
81
|
storeSupportsDraftPersistence,
|
|
82
82
|
streamChatRouteAsSandboxEvents
|
|
83
|
-
} from "../chunk-
|
|
83
|
+
} from "../chunk-WLH4DEB2.js";
|
|
84
84
|
import "../chunk-Q7WEEN7P.js";
|
|
85
85
|
import "../chunk-UTAIRVOK.js";
|
|
86
86
|
import "../chunk-RN4WPTBH.js";
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* instead of hand-rolling a generator — each omittable to the exact behavior
|
|
25
25
|
* above: `turnLock` (single-flight acquire/release around the turn),
|
|
26
26
|
* `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`
|
|
27
|
-
* (observe + augment the producer input), `
|
|
27
|
+
* (observe + augment the producer input), `afterAuthorize` (start the box, keys
|
|
28
|
+
* and session the turn will need the moment the caller is admitted, so they
|
|
29
|
+
* overlap the lock and the user-row insert), `lifecycle` (deterministic
|
|
28
30
|
* start/complete/error telemetry), `heartbeat` (keepalive during silent
|
|
29
31
|
* producer waits), and `onRawEvent` (the raw producer events, for telemetry) —
|
|
30
32
|
* plus the `authorize` result's `insertUserMessage` flag (suppress the user row
|
|
@@ -230,6 +232,21 @@ export type ChatTurnPrepareResult = {
|
|
|
230
232
|
ok: false;
|
|
231
233
|
response: Response;
|
|
232
234
|
};
|
|
235
|
+
/** What `afterAuthorize` receives: the admitted turn, before the conversation loads and before any lock, read or write. */
|
|
236
|
+
export interface ChatTurnAfterAuthorizeArgs<TContext> {
|
|
237
|
+
request: Request;
|
|
238
|
+
/** Parsed, validated POST body. */
|
|
239
|
+
body: ChatTurnRequestPayload;
|
|
240
|
+
tenantId: string;
|
|
241
|
+
userId: string;
|
|
242
|
+
threadId: string;
|
|
243
|
+
/** The context `authorize` returned. Keep what the seam starts on it: `prepareTurn`, every later seam and `produce` receive the same object. */
|
|
244
|
+
context: TContext;
|
|
245
|
+
/** The turn's trace, for the spans of the work the seam starts. */
|
|
246
|
+
trace: TurnTrace;
|
|
247
|
+
}
|
|
248
|
+
/** Frees what `afterAuthorize` started, for a turn that ends before `produce` takes it. */
|
|
249
|
+
export type ChatTurnAfterAuthorizeRelease = () => void | Promise<void>;
|
|
233
250
|
/** Trusted per-turn limits supplied by a server adapter, never by the body. */
|
|
234
251
|
export interface ChatTurnExecutionLimits {
|
|
235
252
|
maxInputTokens?: number;
|
|
@@ -510,6 +527,22 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
|
|
|
510
527
|
* a plain response without the progress header, a `turn.response` event
|
|
511
528
|
* with it. Omit → nothing to prepare. */
|
|
512
529
|
prepareTurn?(args: ChatTurnPrepareArgs<TContext>): ChatTurnPrepareResult | Promise<ChatTurnPrepareResult>;
|
|
530
|
+
/** Start the work this turn will need, the moment `authorize` and `modelPolicy` have admitted it, so that work overlaps
|
|
531
|
+
* `prepareTurn`, the history read, the lock, the history recheck and the user-row insert instead of following them: the
|
|
532
|
+
* workspace box, the keys a producer mints, a session ensure. Keep what it starts on `args.context`; `produce` receives
|
|
533
|
+
* that object and awaits it.
|
|
534
|
+
*
|
|
535
|
+
* Admission has not finished, so the seam must start only work that the turn's refusal leaves harmless: a held lock
|
|
536
|
+
* (409), a changed history, a gate's answer or a throw all end the turn without `produce`, and the work already
|
|
537
|
+
* started runs on. Start nothing a second request for the same thread could not safely repeat, and nothing that
|
|
538
|
+
* writes product state.
|
|
539
|
+
*
|
|
540
|
+
* Not awaited, and it must return synchronously: start the work and return, never wait for it. It may return a release
|
|
541
|
+
* function; the route calls it once, only if the turn ends without `produce` (the chain above, or a throw before the
|
|
542
|
+
* engine took the turn), so a product frees credentials or leases the unused work holds. Once `produce` runs, the
|
|
543
|
+
* work belongs to `produce` and the route never calls the release. A throw is logged and the turn goes on without
|
|
544
|
+
* the early work. Omit → nothing starts early. */
|
|
545
|
+
afterAuthorize?(args: ChatTurnAfterAuthorizeArgs<TContext>): void | ChatTurnAfterAuthorizeRelease;
|
|
513
546
|
/** Product admission policy over the model a turn NAMES. When the request
|
|
514
547
|
* body carries a `model`, it is checked here — after `authorize`, before
|
|
515
548
|
* the conversation loads, the lock is taken, or any row is written — and a
|
|
@@ -971,12 +971,13 @@ async function runForegroundSandboxSingleFlight(options) {
|
|
|
971
971
|
} catch {
|
|
972
972
|
}
|
|
973
973
|
};
|
|
974
|
+
const timed = (name, work) => options.trace ? options.trace.measure(name, "sandbox", () => work(), void 0, options.traceParent) : work();
|
|
974
975
|
const provisionAsOwner = async (lease) => {
|
|
975
976
|
try {
|
|
976
977
|
return await options.provision();
|
|
977
978
|
} finally {
|
|
978
979
|
try {
|
|
979
|
-
await options.claim.releaseLease(lease);
|
|
980
|
+
await timed("sandbox.lease.release", () => options.claim.releaseLease(lease));
|
|
980
981
|
} catch (error) {
|
|
981
982
|
emit({
|
|
982
983
|
type: "release-failed",
|
|
@@ -987,7 +988,17 @@ async function runForegroundSandboxSingleFlight(options) {
|
|
|
987
988
|
}
|
|
988
989
|
}
|
|
989
990
|
};
|
|
990
|
-
|
|
991
|
+
if (options.reuse) {
|
|
992
|
+
const [state, reused] = await Promise.allSettled([
|
|
993
|
+
timed("sandbox.lease.inspect", () => options.claim.inspect(key)),
|
|
994
|
+
options.reuse()
|
|
995
|
+
]);
|
|
996
|
+
if (reused.status === "rejected") throw reused.reason;
|
|
997
|
+
if (reused.value !== void 0 && state.status === "fulfilled" && state.value.status !== "held") {
|
|
998
|
+
return reused.value;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const initialLease = await timed("sandbox.lease.acquire", () => options.claim.acquireLease(key, ttlSeconds));
|
|
991
1002
|
if (initialLease) return provisionAsOwner(initialLease);
|
|
992
1003
|
emit({ type: "waiting", key, workspaceId: options.workspaceId });
|
|
993
1004
|
while (true) {
|
|
@@ -1531,8 +1542,46 @@ var DEFAULT_SANDBOX_RESOURCES = {
|
|
|
1531
1542
|
idleTimeoutSeconds: 3600
|
|
1532
1543
|
};
|
|
1533
1544
|
var _cached = null;
|
|
1534
|
-
var
|
|
1535
|
-
var
|
|
1545
|
+
var TRUSTED_REUSE_TTL_MS = 10 * 6e4;
|
|
1546
|
+
var REUSE_CHECKS_MAX = 1e3;
|
|
1547
|
+
var reuseChecks = /* @__PURE__ */ new Map();
|
|
1548
|
+
var runningBoxIds = /* @__PURE__ */ new Map();
|
|
1549
|
+
var trustedBoxes = /* @__PURE__ */ new WeakSet();
|
|
1550
|
+
function boxStartedAt(box) {
|
|
1551
|
+
const startedAt = box.startedAt instanceof Date ? box.startedAt.getTime() : Number.NaN;
|
|
1552
|
+
return Number.isFinite(startedAt) ? startedAt : void 0;
|
|
1553
|
+
}
|
|
1554
|
+
function checksFor(box) {
|
|
1555
|
+
const checks = reuseChecks.get(box.id);
|
|
1556
|
+
if (!checks) return void 0;
|
|
1557
|
+
if (checks.startedAt !== boxStartedAt(box)) {
|
|
1558
|
+
reuseChecks.delete(box.id);
|
|
1559
|
+
return void 0;
|
|
1560
|
+
}
|
|
1561
|
+
return checks;
|
|
1562
|
+
}
|
|
1563
|
+
function recordCheck(box, patch) {
|
|
1564
|
+
const startedAt = boxStartedAt(box);
|
|
1565
|
+
if (startedAt === void 0 || !box.id) return;
|
|
1566
|
+
const current = checksFor(box) ?? { startedAt };
|
|
1567
|
+
reuseChecks.delete(box.id);
|
|
1568
|
+
reuseChecks.set(box.id, { ...current, ...patch });
|
|
1569
|
+
while (reuseChecks.size > REUSE_CHECKS_MAX) {
|
|
1570
|
+
const oldest = reuseChecks.keys().next().value;
|
|
1571
|
+
if (oldest === void 0) break;
|
|
1572
|
+
reuseChecks.delete(oldest);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function forgetBox(boxId) {
|
|
1576
|
+
reuseChecks.delete(boxId);
|
|
1577
|
+
}
|
|
1578
|
+
function isRecent(at, ttlMs, now = Date.now()) {
|
|
1579
|
+
return at !== void 0 && Number.isFinite(ttlMs) && ttlMs > 0 && now - at < ttlMs;
|
|
1580
|
+
}
|
|
1581
|
+
function dropTrustedBox(box) {
|
|
1582
|
+
forgetBox(box.id);
|
|
1583
|
+
return trustedBoxes.delete(box);
|
|
1584
|
+
}
|
|
1536
1585
|
function getClientFromCreds(creds) {
|
|
1537
1586
|
const fingerprint = `${creds.apiKey} ${creds.baseUrl}`;
|
|
1538
1587
|
if (_cached && _cached.fingerprint === fingerprint) return _cached.client;
|
|
@@ -1550,8 +1599,8 @@ function getClient(shell) {
|
|
|
1550
1599
|
}
|
|
1551
1600
|
function resetClientCache() {
|
|
1552
1601
|
_cached = null;
|
|
1553
|
-
|
|
1554
|
-
|
|
1602
|
+
reuseChecks.clear();
|
|
1603
|
+
runningBoxIds.clear();
|
|
1555
1604
|
}
|
|
1556
1605
|
function buildAppToolMcpServers(options) {
|
|
1557
1606
|
const entries = {};
|
|
@@ -1985,9 +2034,17 @@ async function materializeDeferredFilesForExistingBox(shell, client, box, stage,
|
|
|
1985
2034
|
const corpusHash = deferredCorpusHash(deferredFiles);
|
|
1986
2035
|
const stampedHash = box.metadata?.[DEFERRED_CORPUS_HASH_KEY];
|
|
1987
2036
|
if (typeof stampedHash === "string" && stampedHash === corpusHash) return ok(box);
|
|
1988
|
-
|
|
2037
|
+
const found = checksFor(box)?.corpus;
|
|
2038
|
+
if (isRecent(found?.at, TRUSTED_REUSE_TTL_MS) && found?.hash === corpusHash) return ok(box);
|
|
2039
|
+
if (await (corpusMarker ?? readDeferredCorpusMarker(box)) === corpusHash) {
|
|
2040
|
+
recordCheck(box, { corpus: { hash: corpusHash, at: Date.now() } });
|
|
2041
|
+
return ok(box);
|
|
2042
|
+
}
|
|
1989
2043
|
const written = await writeDeferredFilesWithRuntimeAuthRefresh(client, box, deferredFiles, stage, name);
|
|
1990
|
-
if (written.succeeded)
|
|
2044
|
+
if (written.succeeded) {
|
|
2045
|
+
await writeDeferredCorpusMarker(written.value, corpusHash);
|
|
2046
|
+
recordCheck(written.value, { corpus: { hash: corpusHash, at: Date.now() } });
|
|
2047
|
+
}
|
|
1991
2048
|
return written;
|
|
1992
2049
|
}
|
|
1993
2050
|
var DEFERRED_CORPUS_MARKER_PATH = ".agent-app/deferred-corpus-hash";
|
|
@@ -2016,6 +2073,26 @@ async function listRunning(client, name) {
|
|
|
2016
2073
|
return fail(err);
|
|
2017
2074
|
}
|
|
2018
2075
|
}
|
|
2076
|
+
function rememberRunningBox(name, box) {
|
|
2077
|
+
runningBoxIds.delete(name);
|
|
2078
|
+
runningBoxIds.set(name, box.id);
|
|
2079
|
+
while (runningBoxIds.size > REUSE_CHECKS_MAX) {
|
|
2080
|
+
const oldest = runningBoxIds.keys().next().value;
|
|
2081
|
+
if (oldest === void 0) break;
|
|
2082
|
+
runningBoxIds.delete(oldest);
|
|
2083
|
+
}
|
|
2084
|
+
return box;
|
|
2085
|
+
}
|
|
2086
|
+
async function getRunningBox(client, id, name) {
|
|
2087
|
+
try {
|
|
2088
|
+
const box = await client.get(id);
|
|
2089
|
+
if (!box || box.name !== name || box.status !== "running") return null;
|
|
2090
|
+
if (typeof box.metadata?.harness !== "string") return null;
|
|
2091
|
+
return box;
|
|
2092
|
+
} catch {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2019
2096
|
async function listStopped(client, name) {
|
|
2020
2097
|
try {
|
|
2021
2098
|
const stopped = await client.list({ status: "stopped" });
|
|
@@ -2027,7 +2104,7 @@ async function listStopped(client, name) {
|
|
|
2027
2104
|
async function deleteBox(box) {
|
|
2028
2105
|
try {
|
|
2029
2106
|
const acknowledgement = await box.delete({ until: "removed" });
|
|
2030
|
-
|
|
2107
|
+
forgetBox(box.id);
|
|
2031
2108
|
if (acknowledgement.removal === "pending") {
|
|
2032
2109
|
return fail(new Error(`sandbox ${box.id} removal is still pending; retry replacement after removal completes`));
|
|
2033
2110
|
}
|
|
@@ -2086,14 +2163,8 @@ async function isBoxAlive(box, probe) {
|
|
|
2086
2163
|
return fail(new Error("alive check failed", { cause }));
|
|
2087
2164
|
}
|
|
2088
2165
|
}
|
|
2089
|
-
var DEFAULT_LIVENESS_CACHE_TTL_MS = 5e3;
|
|
2090
2166
|
function hasRecentLivenessVerification(box, probe, now = Date.now()) {
|
|
2091
|
-
|
|
2092
|
-
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return false;
|
|
2093
|
-
const verifiedAt = livenessVerifiedAt.get(box.id);
|
|
2094
|
-
if (verifiedAt !== void 0 && now - verifiedAt < ttlMs) return true;
|
|
2095
|
-
livenessVerifiedAt.delete(box.id);
|
|
2096
|
-
return false;
|
|
2167
|
+
return isRecent(checksFor(box)?.alive, probe.cacheTtlMs ?? TRUSTED_REUSE_TTL_MS, now);
|
|
2097
2168
|
}
|
|
2098
2169
|
var RUNTIME_CONNECTION_WAIT_MS = 3e4;
|
|
2099
2170
|
var RUNTIME_CONNECTION_POLL_MS = 1e3;
|
|
@@ -2218,14 +2289,17 @@ async function writeDeferredFilesWithRuntimeAuthRefresh(client, box, files, stag
|
|
|
2218
2289
|
}
|
|
2219
2290
|
async function isReusableBox(box, probe) {
|
|
2220
2291
|
if (sandboxEdgeFailed(box) || !sandboxRuntimeUrl(box)) {
|
|
2221
|
-
|
|
2292
|
+
forgetBox(box.id);
|
|
2222
2293
|
return fail(new Error(sandboxEdgeFailed(box) ? "runtime edge readiness failed" : "runtime URL is missing"));
|
|
2223
2294
|
}
|
|
2224
2295
|
if (!probe) return ok(void 0);
|
|
2225
|
-
if (hasRecentLivenessVerification(box, probe))
|
|
2296
|
+
if (hasRecentLivenessVerification(box, probe)) {
|
|
2297
|
+
trustedBoxes.add(box);
|
|
2298
|
+
return ok(void 0);
|
|
2299
|
+
}
|
|
2226
2300
|
const alive = await isBoxAlive(box, probe);
|
|
2227
|
-
if (alive.succeeded)
|
|
2228
|
-
else
|
|
2301
|
+
if (alive.succeeded) recordCheck(box, { alive: Date.now() });
|
|
2302
|
+
else forgetBox(box.id);
|
|
2229
2303
|
return alive;
|
|
2230
2304
|
}
|
|
2231
2305
|
function stoppedBoxResumeError(box, cause) {
|
|
@@ -2253,7 +2327,7 @@ async function resumeStoppedBox(box, timeoutMs, runtimeEnv, onProgress) {
|
|
|
2253
2327
|
}
|
|
2254
2328
|
};
|
|
2255
2329
|
try {
|
|
2256
|
-
|
|
2330
|
+
forgetBox(box.id);
|
|
2257
2331
|
report("started", "Resuming the stopped sandbox");
|
|
2258
2332
|
await resumeWithRequiredRuntimeEnv(box, timeoutMs, runtimeEnv);
|
|
2259
2333
|
await box.waitFor("running", { timeoutMs, ...onProgress ? { onProgress } : {} });
|
|
@@ -2368,57 +2442,59 @@ async function resolveWorkspaceRuntimeEnv(shell, scope) {
|
|
|
2368
2442
|
assertEnvWithinLimits(env);
|
|
2369
2443
|
return env;
|
|
2370
2444
|
}
|
|
2371
|
-
function
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
);
|
|
2445
|
+
function runtimeEnvDigest(env) {
|
|
2446
|
+
return createHash("sha256").update(JSON.stringify(canonicalizeJson(env)), "utf8").digest("hex");
|
|
2447
|
+
}
|
|
2448
|
+
function hasDeliveredEnv(box, env) {
|
|
2449
|
+
const delivered = checksFor(box)?.env;
|
|
2450
|
+
return isRecent(delivered?.at, TRUSTED_REUSE_TTL_MS) && delivered?.digest === runtimeEnvDigest(env);
|
|
2451
|
+
}
|
|
2452
|
+
function hasRecentEgress(box, desired) {
|
|
2453
|
+
const verified = checksFor(box)?.egress;
|
|
2454
|
+
return isRecent(verified?.at, TRUSTED_REUSE_TTL_MS) && verified?.key === normalizedEgressPolicy(desired);
|
|
2455
|
+
}
|
|
2456
|
+
async function pushRuntimeEnv(box, env, measure) {
|
|
2457
|
+
try {
|
|
2458
|
+
await (measure ? measure("runtime_env_push", () => box.setRuntimeEnv(env)) : box.setRuntimeEnv(env));
|
|
2459
|
+
recordCheck(box, { env: { digest: runtimeEnvDigest(env), at: Date.now() } });
|
|
2460
|
+
return ok(void 0);
|
|
2461
|
+
} catch (cause) {
|
|
2462
|
+
return fail(cause);
|
|
2390
2463
|
}
|
|
2464
|
+
}
|
|
2465
|
+
function prefetchExistingBoxReads(shell, box, runtimeEnv, measure) {
|
|
2466
|
+
const desired = shell.egressPolicy;
|
|
2467
|
+
const readEgress = () => measure("egress", async () => {
|
|
2468
|
+
try {
|
|
2469
|
+
return ok(await box.egress.get());
|
|
2470
|
+
} catch (cause) {
|
|
2471
|
+
return fail(cause);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
const egressRecent = desired !== void 0 && hasRecentEgress(box, desired);
|
|
2475
|
+
const prefetch = { decided: Promise.resolve() };
|
|
2476
|
+
if (desired && !egressRecent) prefetch.egress = readEgress();
|
|
2477
|
+
if (shell.deferProfileFiles && !isRecent(checksFor(box)?.corpus?.at, TRUSTED_REUSE_TTL_MS)) {
|
|
2478
|
+
prefetch.corpusMarker = readDeferredCorpusMarker(box);
|
|
2479
|
+
}
|
|
2480
|
+
prefetch.decided = runtimeEnv.then((env) => {
|
|
2481
|
+
const delivered = Object.keys(env).length === 0 || hasDeliveredEnv(box, env);
|
|
2482
|
+
prefetch.envDelivered = delivered;
|
|
2483
|
+
if (desired && egressRecent) {
|
|
2484
|
+
if (desired.mode === "open" || delivered && !shell.bootstrap) prefetch.egressVerified = true;
|
|
2485
|
+
else prefetch.egress = readEgress();
|
|
2486
|
+
}
|
|
2487
|
+
if (!delivered && !egressGatesCredentials(desired)) {
|
|
2488
|
+
prefetch.runtimeEnv = pushRuntimeEnv(box, env, measure);
|
|
2489
|
+
}
|
|
2490
|
+
}, () => void 0);
|
|
2391
2491
|
return prefetch;
|
|
2392
2492
|
}
|
|
2393
2493
|
function egressGatesCredentials(desired) {
|
|
2394
2494
|
return desired !== void 0 && desired.mode !== "open";
|
|
2395
2495
|
}
|
|
2396
|
-
var VERIFIED_EGRESS_TTL_MS = 10 * 6e4;
|
|
2397
|
-
var VERIFIED_EGRESS_MAX = 1e3;
|
|
2398
|
-
function verifiedEgressKey(box, desired) {
|
|
2399
|
-
if (desired.mode !== "open" || !box.id) return null;
|
|
2400
|
-
const startedAt = box.startedAt instanceof Date ? box.startedAt.getTime() : Number.NaN;
|
|
2401
|
-
if (!Number.isFinite(startedAt)) return null;
|
|
2402
|
-
return `${box.id}|${startedAt}|${normalizedEgressPolicy(desired)}`;
|
|
2403
|
-
}
|
|
2404
|
-
function hasVerifiedEgress(box, desired, now = Date.now()) {
|
|
2405
|
-
const key = verifiedEgressKey(box, desired);
|
|
2406
|
-
if (!key) return false;
|
|
2407
|
-
const verifiedAt = verifiedEgress.get(key);
|
|
2408
|
-
if (verifiedAt !== void 0 && now - verifiedAt < VERIFIED_EGRESS_TTL_MS) return true;
|
|
2409
|
-
verifiedEgress.delete(key);
|
|
2410
|
-
return false;
|
|
2411
|
-
}
|
|
2412
2496
|
function recordVerifiedEgress(box, desired) {
|
|
2413
|
-
|
|
2414
|
-
if (!key) return;
|
|
2415
|
-
verifiedEgress.delete(key);
|
|
2416
|
-
verifiedEgress.set(key, Date.now());
|
|
2417
|
-
while (verifiedEgress.size > VERIFIED_EGRESS_MAX) {
|
|
2418
|
-
const oldest = verifiedEgress.keys().next().value;
|
|
2419
|
-
if (oldest === void 0) break;
|
|
2420
|
-
verifiedEgress.delete(oldest);
|
|
2421
|
-
}
|
|
2497
|
+
recordCheck(box, { egress: { key: normalizedEgressPolicy(desired), at: Date.now() } });
|
|
2422
2498
|
}
|
|
2423
2499
|
async function finalizeExistingBox(shell, client, box, stage, name, workspaceId, userId, harness, scope, runtimeEnv, prefetch = {}) {
|
|
2424
2500
|
if (!prefetch.egressVerified) {
|
|
@@ -2442,8 +2518,9 @@ async function finalizeExistingBox(shell, client, box, stage, name, workspaceId,
|
|
|
2442
2518
|
if (prefetch.runtimeEnv) {
|
|
2443
2519
|
const renewed = await prefetch.runtimeEnv;
|
|
2444
2520
|
if (!renewed.succeeded) throw renewed.error;
|
|
2445
|
-
} else if (Object.keys(runtimeEnv).length > 0) {
|
|
2446
|
-
await finalBox
|
|
2521
|
+
} else if (!prefetch.envDelivered && Object.keys(runtimeEnv).length > 0) {
|
|
2522
|
+
const renewed = await pushRuntimeEnv(finalBox, runtimeEnv);
|
|
2523
|
+
if (!renewed.succeeded) throw renewed.error;
|
|
2447
2524
|
}
|
|
2448
2525
|
if (shell.bootstrap) {
|
|
2449
2526
|
const boot = await shell.bootstrap(finalBox, scope);
|
|
@@ -2531,11 +2608,37 @@ async function assertExistingBoxEgress(box, desired, migrate, stage, name, prefe
|
|
|
2531
2608
|
);
|
|
2532
2609
|
}
|
|
2533
2610
|
async function ensureWorkspaceSandbox(shell, options) {
|
|
2534
|
-
await options
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2611
|
+
const box = await claimWorkspaceSandbox(shell, options, false);
|
|
2612
|
+
if (!box) throw new Error("ensureWorkspaceSandbox: a claim that may provision returned no box");
|
|
2613
|
+
return box;
|
|
2614
|
+
}
|
|
2615
|
+
async function reuseWorkspaceSandbox(shell, options) {
|
|
2616
|
+
if (options.forceNew || !hasRecentProbe(shell, options)) return void 0;
|
|
2617
|
+
return claimWorkspaceSandbox(shell, options, true);
|
|
2618
|
+
}
|
|
2619
|
+
function hasRecentProbe(shell, options) {
|
|
2620
|
+
const probe = shell.livenessProbe;
|
|
2621
|
+
if (!probe || options.forceNew) return false;
|
|
2622
|
+
const scope = { workspaceId: options.workspaceId, ...options.userId ? { userId: options.userId } : {} };
|
|
2623
|
+
const id = runningBoxIds.get(shell.boxKey ? shell.boxKey(scope) : shell.name(options.workspaceId));
|
|
2624
|
+
return isRecent(id === void 0 ? void 0 : reuseChecks.get(id)?.alive, probe.cacheTtlMs ?? TRUSTED_REUSE_TTL_MS);
|
|
2625
|
+
}
|
|
2626
|
+
function spendGate(options) {
|
|
2627
|
+
let gated;
|
|
2628
|
+
return () => {
|
|
2629
|
+
const before = options.spend?.beforeProvision;
|
|
2630
|
+
if (!before) return Promise.resolve();
|
|
2631
|
+
const run = () => before({
|
|
2632
|
+
workspaceId: options.workspaceId,
|
|
2633
|
+
...options.userId ? { userId: options.userId } : {}
|
|
2634
|
+
});
|
|
2635
|
+
return gated ??= Promise.resolve(options.trace ? options.trace.measure("sandbox.spend.before_provision", "sandbox", run, void 0, options.traceParent) : run());
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
async function claimWorkspaceSandbox(shell, options, reuseOnly) {
|
|
2538
2639
|
const { trace } = options;
|
|
2640
|
+
const gate = spendGate(options);
|
|
2641
|
+
if (!hasRecentProbe(shell, options)) await gate();
|
|
2539
2642
|
const claim = trace?.start("sandbox.claim", "sandbox", { "workspace.harness": options.harness }, options.traceParent);
|
|
2540
2643
|
const traced = trace && claim ? {
|
|
2541
2644
|
...options,
|
|
@@ -2545,9 +2648,13 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
2545
2648
|
}
|
|
2546
2649
|
} : options;
|
|
2547
2650
|
try {
|
|
2548
|
-
const box = await bringUpWorkspaceSandbox(shell, traced);
|
|
2549
|
-
|
|
2550
|
-
|
|
2651
|
+
const box = await bringUpWorkspaceSandbox(shell, traced, gate, reuseOnly);
|
|
2652
|
+
if (!box) {
|
|
2653
|
+
claim?.end({ "sandbox.reuse_declined": true });
|
|
2654
|
+
return void 0;
|
|
2655
|
+
}
|
|
2656
|
+
claim?.end({ "sandbox.id": box.id, ...trustedBoxes.has(box) ? { "sandbox.probe_trusted": true } : {} });
|
|
2657
|
+
if (!trustedBoxes.has(box)) await observeProvisionedBox(shell, options, box);
|
|
2551
2658
|
return box;
|
|
2552
2659
|
} catch (error) {
|
|
2553
2660
|
claim?.end(void 0, error);
|
|
@@ -2555,10 +2662,11 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
2555
2662
|
}
|
|
2556
2663
|
}
|
|
2557
2664
|
async function observeProvisionedBox(shell, options, box) {
|
|
2558
|
-
|
|
2665
|
+
const onProvisioned = options.spend?.onProvisioned;
|
|
2666
|
+
if (!onProvisioned) return;
|
|
2559
2667
|
const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
|
|
2560
|
-
|
|
2561
|
-
await
|
|
2668
|
+
const observe = async () => {
|
|
2669
|
+
await onProvisioned({
|
|
2562
2670
|
workspaceId: options.workspaceId,
|
|
2563
2671
|
...options.userId ? { userId: options.userId } : {},
|
|
2564
2672
|
sandboxId: box.id,
|
|
@@ -2567,17 +2675,20 @@ async function observeProvisionedBox(shell, options, box) {
|
|
|
2567
2675
|
maxLifetimeSeconds: resources.maxLifetimeSeconds,
|
|
2568
2676
|
at: Date.now()
|
|
2569
2677
|
});
|
|
2678
|
+
};
|
|
2679
|
+
try {
|
|
2680
|
+
await (options.trace ? options.trace.measure("sandbox.spend.observe", "sandbox", observe, void 0, options.traceParent) : observe());
|
|
2570
2681
|
} catch {
|
|
2571
2682
|
}
|
|
2572
2683
|
}
|
|
2573
|
-
async function bringUpWorkspaceSandbox(shell, options) {
|
|
2684
|
+
async function bringUpWorkspaceSandbox(shell, options, gate, reuseOnly) {
|
|
2574
2685
|
try {
|
|
2575
|
-
return await provisionWorkspaceSandbox(shell, options);
|
|
2686
|
+
return await provisionWorkspaceSandbox(shell, options, gate, reuseOnly);
|
|
2576
2687
|
} catch (err) {
|
|
2577
2688
|
if (!shell.replaceUnbringableBox) throw err;
|
|
2578
2689
|
if (options.forceNew) throw err;
|
|
2579
2690
|
if (!isUnbringableBoxError(err)) throw err;
|
|
2580
|
-
return await provisionWorkspaceSandbox(shell, { ...options, forceNew: true });
|
|
2691
|
+
return await provisionWorkspaceSandbox(shell, { ...options, forceNew: true }, gate, reuseOnly);
|
|
2581
2692
|
}
|
|
2582
2693
|
}
|
|
2583
2694
|
function isUnbringableBoxError(error) {
|
|
@@ -2611,7 +2722,7 @@ async function requestMissingSandboxReplacement(shell, failure) {
|
|
|
2611
2722
|
"missing sandbox recovery"
|
|
2612
2723
|
);
|
|
2613
2724
|
}
|
|
2614
|
-
async function provisionWorkspaceSandbox(shell, options) {
|
|
2725
|
+
async function provisionWorkspaceSandbox(shell, options, gate, reuseOnly) {
|
|
2615
2726
|
const execTimeoutMs = shell.livenessProbe?.execTimeoutMs;
|
|
2616
2727
|
if (execTimeoutMs !== void 0 && (!Number.isInteger(execTimeoutMs) || execTimeoutMs < 100 || execTimeoutMs > 6e5)) {
|
|
2617
2728
|
throw new Error("livenessProbe.execTimeoutMs must be an integer between 100 and 600000");
|
|
@@ -2625,7 +2736,17 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2625
2736
|
let replacementRequested = false;
|
|
2626
2737
|
const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
|
|
2627
2738
|
const resumeTimeout = shell.provisionTimeoutMs ?? DEFAULT_PROVISION_TIMEOUT_MS;
|
|
2628
|
-
let existing =
|
|
2739
|
+
let existing = ok(null);
|
|
2740
|
+
const knownId = forceNew ? void 0 : runningBoxIds.get(name);
|
|
2741
|
+
if (knownId) {
|
|
2742
|
+
const known = await measure("get_running", () => getRunningBox(client, knownId, name));
|
|
2743
|
+
if (known) existing = ok(known);
|
|
2744
|
+
else runningBoxIds.delete(name);
|
|
2745
|
+
}
|
|
2746
|
+
if (reuseOnly && !(existing.succeeded && existing.value)) return void 0;
|
|
2747
|
+
if (!(existing.succeeded && existing.value)) {
|
|
2748
|
+
existing = await measure("list_running", () => listRunning(client, name));
|
|
2749
|
+
}
|
|
2629
2750
|
if (forceNew && existing.succeeded && !existing.value) {
|
|
2630
2751
|
existing = await listStopped(client, name);
|
|
2631
2752
|
}
|
|
@@ -2645,13 +2766,29 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2645
2766
|
measure("refresh", () => refreshRuntimeConnection(client, found))
|
|
2646
2767
|
]);
|
|
2647
2768
|
if (envResult.status === "rejected") throw envResult.reason;
|
|
2769
|
+
await prefetch.decided;
|
|
2648
2770
|
const runtimeEnv2 = envResult.value;
|
|
2771
|
+
const refusal = { refused: false };
|
|
2772
|
+
const gateBeforeUse = async () => {
|
|
2773
|
+
try {
|
|
2774
|
+
await gate();
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
refusal.refused = true;
|
|
2777
|
+
refusal.error = error;
|
|
2778
|
+
throw error;
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2649
2781
|
try {
|
|
2650
2782
|
if (readyResult.status === "rejected") throw readyResult.reason;
|
|
2651
2783
|
const ready = readyResult.value;
|
|
2652
2784
|
if ((await measure("liveness", () => isReusableBox(ready, shell.livenessProbe))).succeeded) {
|
|
2653
|
-
|
|
2785
|
+
if (!trustedBoxes.has(ready)) await gateBeforeUse();
|
|
2786
|
+
const finalBox = await measure("finalize", () => finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2, prefetch));
|
|
2787
|
+
if (trustedBoxes.has(ready)) trustedBoxes.add(finalBox);
|
|
2788
|
+
return rememberRunningBox(name, finalBox);
|
|
2654
2789
|
}
|
|
2790
|
+
if (reuseOnly) return void 0;
|
|
2791
|
+
await gateBeforeUse();
|
|
2655
2792
|
const recovered = await recoverUnresponsiveBox(
|
|
2656
2793
|
client,
|
|
2657
2794
|
ready,
|
|
@@ -2662,8 +2799,10 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2662
2799
|
runtimeEnv2,
|
|
2663
2800
|
onProgress
|
|
2664
2801
|
);
|
|
2665
|
-
return await finalizeExistingBox(shell, client, recovered, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2);
|
|
2802
|
+
return rememberRunningBox(name, await finalizeExistingBox(shell, client, recovered, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2));
|
|
2666
2803
|
} catch (cause) {
|
|
2804
|
+
if (refusal.refused) throw refusal.error;
|
|
2805
|
+
if (reuseOnly) return void 0;
|
|
2667
2806
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2668
2807
|
const recovery = await requestMissingSandboxReplacement(shell, {
|
|
2669
2808
|
box: found,
|
|
@@ -2677,6 +2816,8 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2677
2816
|
replacementRequested = true;
|
|
2678
2817
|
}
|
|
2679
2818
|
} else {
|
|
2819
|
+
if (reuseOnly) return void 0;
|
|
2820
|
+
await gate();
|
|
2680
2821
|
const dropped = await deleteBox(found);
|
|
2681
2822
|
if (!dropped.succeeded) {
|
|
2682
2823
|
throw new Error(
|
|
@@ -2686,6 +2827,8 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2686
2827
|
}
|
|
2687
2828
|
}
|
|
2688
2829
|
}
|
|
2830
|
+
if (reuseOnly) return void 0;
|
|
2831
|
+
await gate();
|
|
2689
2832
|
if (!replacementRequested && !forceNew && shell.resumeStopped !== false) {
|
|
2690
2833
|
const stopped = await measure("list_stopped", () => listStopped(client, name));
|
|
2691
2834
|
if (!stopped.succeeded) throw stopped.error;
|
|
@@ -2705,7 +2848,7 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2705
2848
|
try {
|
|
2706
2849
|
const box2 = await refreshRuntimeConnection(client, resumed.value);
|
|
2707
2850
|
if ((await isReusableBox(box2, shell.livenessProbe)).succeeded) {
|
|
2708
|
-
return await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2);
|
|
2851
|
+
return rememberRunningBox(name, await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2));
|
|
2709
2852
|
}
|
|
2710
2853
|
const recovered = await recoverUnresponsiveBox(
|
|
2711
2854
|
client,
|
|
@@ -2717,7 +2860,7 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2717
2860
|
runtimeEnv2,
|
|
2718
2861
|
onProgress
|
|
2719
2862
|
);
|
|
2720
|
-
return await finalizeExistingBox(shell, client, recovered, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2);
|
|
2863
|
+
return rememberRunningBox(name, await finalizeExistingBox(shell, client, recovered, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2));
|
|
2721
2864
|
} catch (cause) {
|
|
2722
2865
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2723
2866
|
const recovery = await requestMissingSandboxReplacement(shell, {
|
|
@@ -2821,7 +2964,7 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2821
2964
|
throw new Error(`bootstrap failed on new box ${name}`, { cause: boot.error });
|
|
2822
2965
|
}
|
|
2823
2966
|
}
|
|
2824
|
-
return box;
|
|
2967
|
+
return rememberRunningBox(name, box);
|
|
2825
2968
|
}
|
|
2826
2969
|
function historyTranscript(history) {
|
|
2827
2970
|
return history.map((entry) => `${entry.role === "assistant" ? "Assistant" : "User"}: ${entry.content}`).join("\n\n");
|
|
@@ -3217,8 +3360,10 @@ async function* streamSandboxPrompt(shell, box, message, options) {
|
|
|
3217
3360
|
...options.detach ? { "sandbox.detached": true } : {}
|
|
3218
3361
|
}
|
|
3219
3362
|
}) : stream;
|
|
3363
|
+
let delivered = false;
|
|
3220
3364
|
try {
|
|
3221
3365
|
for await (const event of observed) {
|
|
3366
|
+
delivered = true;
|
|
3222
3367
|
const step = classifySeveredStream(event);
|
|
3223
3368
|
if (step) severedFinishReason = step.kind === "step-finish" && step.severed ? step.reason : null;
|
|
3224
3369
|
if (severedFinishReason && isTerminalPromptEvent(event)) {
|
|
@@ -3232,6 +3377,9 @@ async function* streamSandboxPrompt(shell, box, message, options) {
|
|
|
3232
3377
|
}
|
|
3233
3378
|
yield event;
|
|
3234
3379
|
}
|
|
3380
|
+
} catch (error) {
|
|
3381
|
+
if (!delivered && !options?.signal?.aborted) dropTrustedBox(box);
|
|
3382
|
+
throw error;
|
|
3235
3383
|
} finally {
|
|
3236
3384
|
emitSandboxActivity(options?.spend, box);
|
|
3237
3385
|
}
|
|
@@ -3477,6 +3625,7 @@ export {
|
|
|
3477
3625
|
PYPI_EGRESS_DOMAINS,
|
|
3478
3626
|
buildProductEgressPolicy,
|
|
3479
3627
|
DEFAULT_SANDBOX_RESOURCES,
|
|
3628
|
+
dropTrustedBox,
|
|
3480
3629
|
getClient,
|
|
3481
3630
|
resetClientCache,
|
|
3482
3631
|
buildAppToolMcpServers,
|
|
@@ -3504,6 +3653,7 @@ export {
|
|
|
3504
3653
|
assertEnvWithinLimits,
|
|
3505
3654
|
peekWorkspaceSandbox,
|
|
3506
3655
|
ensureWorkspaceSandbox,
|
|
3656
|
+
reuseWorkspaceSandbox,
|
|
3507
3657
|
flattenHistory,
|
|
3508
3658
|
mergeHistoryIntoParts,
|
|
3509
3659
|
mergeExtraMcp,
|
|
@@ -3527,4 +3677,4 @@ export {
|
|
|
3527
3677
|
isTerminalPromptEvent,
|
|
3528
3678
|
detectInteractiveQuestion
|
|
3529
3679
|
};
|
|
3530
|
-
//# sourceMappingURL=chunk-
|
|
3680
|
+
//# sourceMappingURL=chunk-QA2CBATZ.js.map
|