@tanstack/ai-sandbox 0.2.3 → 0.3.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/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
package/dist/esm/middleware.js
CHANGED
|
@@ -1,219 +1,346 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { SandboxStoreCapability, LocksCapability, SandboxCapability, provideSandbox, provideSandboxPolicy } from "./capabilities.js";
|
|
4
|
-
import { computeWorkspaceHash } from "./key.js";
|
|
5
|
-
import { resolveFileEvents, buildFileHookEvent } from "./file-diff.js";
|
|
1
|
+
import { SandboxCapability, provideSandbox, provideSandboxPolicy } from "./capabilities.js";
|
|
2
|
+
import { SandboxInstanceStoreCapability } from "./instance-store.js";
|
|
6
3
|
import { ProjectionCapability, provideWorkspaceProjection } from "./projection.js";
|
|
4
|
+
import { provideSandboxDurability, resolveSandboxDurability } from "./durability.js";
|
|
5
|
+
import { computeWorkspaceHash } from "./key.js";
|
|
6
|
+
import { buildFileHookEvent, resolveFileEvents } from "./file-diff.js";
|
|
7
7
|
import { resolveSecret } from "./secrets.js";
|
|
8
|
+
import { createToolHistoryRecorder, stripObservedToolCalls } from "./tool-history.js";
|
|
9
|
+
import "./bootstrap.js";
|
|
8
10
|
import { watchWorkspace } from "./watch.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
+
import { defineChatMiddleware, provideDetachableRun, provideRunDetached, wasCancelRequested } from "@tanstack/ai";
|
|
12
|
+
import { InMemoryLockStore, LocksCapability } from "@tanstack/ai/locks";
|
|
13
|
+
import { getPendingTurn, getRunDisconnect, getSandboxRuntime } from "@tanstack/ai/adapter-internals";
|
|
14
|
+
//#region src/middleware.ts
|
|
15
|
+
/**
|
|
16
|
+
* `withSandbox(definition, options?)` — the middleware that PROVIDES the
|
|
17
|
+
* {@link SandboxCapability} a harness adapter requires.
|
|
18
|
+
*
|
|
19
|
+
* - `setup`: resume-or-create the sandbox (via the definition's ensure
|
|
20
|
+
* algorithm), provide the handle, using the durability seams from
|
|
21
|
+
* {@link SandboxMiddlewareOptions} (or, failing that, a bus-provided
|
|
22
|
+
* SandboxInstanceStoreCapability / LocksCapability, then an in-memory
|
|
23
|
+
* fallback). If `fileEvents` is not false, starts a
|
|
24
|
+
* watcher that dispatches to sandbox-scoped hooks and forwards to the runtime
|
|
25
|
+
* sink.
|
|
26
|
+
* - `onFinish`/`onAbort`/`onError`: stop the watcher, snapshot (`after-run`)
|
|
27
|
+
* and/or destroy per lifecycle.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: streamed sandbox lifecycle events (sandbox.created, workspace.setup.*)
|
|
30
|
+
* are emitted by the harness adapter's chatStream (which can yield CUSTOM
|
|
31
|
+
* chunks), not from here — middleware setup runs before streaming begins.
|
|
32
|
+
*/
|
|
33
|
+
var runState = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
/**
|
|
35
|
+
* Stop the watcher and drain any in-flight `diff()` promises before teardown,
|
|
36
|
+
* so the final file's diff isn't dropped when a run finishes/aborts/errors
|
|
37
|
+
* mid-computation. The `pendingDiffs` await is the load-bearing line — without
|
|
38
|
+
* it a deferred diff resolves after the run is gone and its chunk is lost.
|
|
39
|
+
*/
|
|
11
40
|
async function drainWatcher(state, phase) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
try {
|
|
42
|
+
await state.watcher?.stop();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
state.logger?.warn("sandbox watcher stop failed", {
|
|
45
|
+
phase,
|
|
46
|
+
error
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
await Promise.allSettled(state.pendingDiffs);
|
|
50
|
+
if (state.watcher) state.logger?.sandbox("sandbox watcher stopped", { phase });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Record the two facts a later attach and the reaper both need, then publish the
|
|
54
|
+
* detach verdict core reads.
|
|
55
|
+
*
|
|
56
|
+
* Shared by the DISCONNECT subscriber registered in `setup` (the run is still
|
|
57
|
+
* going — the normal case) and `onAbort`'s detach branch (the run is being torn
|
|
58
|
+
* down while detachable), so the two can never write a different shape of detach.
|
|
59
|
+
*
|
|
60
|
+
* GUARDED, and reports failure rather than throwing. `update` is a documented
|
|
61
|
+
* no-op for an unknown runId, so a vanished record does not turn teardown into a
|
|
62
|
+
* throw; a genuinely rejecting store is the caller's to react to — `onAbort` falls
|
|
63
|
+
* through to destroying the sandbox, because a DESTROYED sandbox beats an
|
|
64
|
+
* unreachable one, while the disconnect subscriber has nothing to fall back to
|
|
65
|
+
* (the run is alive and still using the sandbox) and simply leaves the verdict
|
|
66
|
+
* unpublished.
|
|
67
|
+
*
|
|
68
|
+
* The verdict is published ONLY on success. Publishing it after a failed record
|
|
69
|
+
* write would leave core holding the log open for a takeover that can never be
|
|
70
|
+
* found, since nothing in the store points at the run.
|
|
71
|
+
*/
|
|
72
|
+
async function recordDetach(definition, state, durability, ctx, phase) {
|
|
73
|
+
try {
|
|
74
|
+
await durability.runs.update(ctx.runId, {
|
|
75
|
+
detachedSince: Date.now(),
|
|
76
|
+
sandboxKey: definition.key(state.ensureCtx)
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
state.logger?.warn("sandbox detach record write failed", {
|
|
80
|
+
runId: ctx.runId,
|
|
81
|
+
phase,
|
|
82
|
+
error
|
|
83
|
+
});
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
provideRunDetached(ctx, true);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether an out-of-band cancel has been recorded for this run, in EITHER band.
|
|
91
|
+
* A user pressing Stop and a user closing the tab produce the IDENTICAL
|
|
92
|
+
* connection close, so intent is never inferred from the disconnect itself: it
|
|
93
|
+
* arrives in-process (the abort reason carried the cancel sentinel) or durably
|
|
94
|
+
* (another host recorded it on the run record).
|
|
95
|
+
*/
|
|
96
|
+
async function cancelIntent(durability, runId, inProcess) {
|
|
97
|
+
if (inProcess) return true;
|
|
98
|
+
if (durability === void 0) return false;
|
|
99
|
+
return wasCancelRequested(durability.runs, runId);
|
|
19
100
|
}
|
|
101
|
+
/** Defensively pull tenant scoping out of the runtime context, if present. */
|
|
20
102
|
function tenantFrom(context) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
103
|
+
if (context === null || typeof context !== "object") return void 0;
|
|
104
|
+
const c = context;
|
|
105
|
+
const userId = typeof c.userId === "string" ? c.userId : void 0;
|
|
106
|
+
const orgId = typeof c.orgId === "string" ? c.orgId : void 0;
|
|
107
|
+
if (userId === void 0 && orgId === void 0) return void 0;
|
|
108
|
+
return {
|
|
109
|
+
userId,
|
|
110
|
+
orgId
|
|
111
|
+
};
|
|
27
112
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the ensure seams. Precedence is explicit option → capability bus →
|
|
115
|
+
* (in `ensure`) the in-memory fallback. The option wins because it is visible
|
|
116
|
+
* at the call site; the bus remains for platform/framework injection.
|
|
117
|
+
*/
|
|
118
|
+
function buildEnsureCtx(ctx, options) {
|
|
119
|
+
return {
|
|
120
|
+
threadId: ctx.threadId,
|
|
121
|
+
runId: ctx.runId,
|
|
122
|
+
store: options?.instances ?? ctx.getOptional(SandboxInstanceStoreCapability),
|
|
123
|
+
locks: options?.locks ?? ctx.getOptional(LocksCapability),
|
|
124
|
+
tenant: tenantFrom(ctx.context),
|
|
125
|
+
signal: ctx.signal
|
|
126
|
+
};
|
|
37
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Dispatch a sandbox file event to the per-type hooks declared on the
|
|
130
|
+
* definition. Errors in individual hooks are swallowed so one bad hook
|
|
131
|
+
* cannot break the run — but are logged under the `errors` category first, so
|
|
132
|
+
* a throwing hook is observable (matching the run-scoped path in the engine
|
|
133
|
+
* and the behavior the observability docs promise).
|
|
134
|
+
*/
|
|
38
135
|
async function dispatchDefinitionHooks(hooks, event, logger) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
136
|
+
if (!hooks) return;
|
|
137
|
+
const typed = {
|
|
138
|
+
create: "onFileCreate",
|
|
139
|
+
change: "onFileChange",
|
|
140
|
+
delete: "onFileDelete"
|
|
141
|
+
}[event.type];
|
|
142
|
+
for (const fn of [hooks.onFile, hooks[typed]]) {
|
|
143
|
+
if (!fn) continue;
|
|
144
|
+
try {
|
|
145
|
+
await fn(event);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger?.errors("sandbox file hook failed", {
|
|
148
|
+
path: event.path,
|
|
149
|
+
type: event.type,
|
|
150
|
+
error
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
57
154
|
}
|
|
58
|
-
function withSandbox(definition) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
155
|
+
function withSandbox(definition, options) {
|
|
156
|
+
return defineChatMiddleware({
|
|
157
|
+
name: "sandbox",
|
|
158
|
+
provides: [SandboxCapability, ProjectionCapability],
|
|
159
|
+
optionalRequires: [SandboxInstanceStoreCapability, LocksCapability],
|
|
160
|
+
async setup(ctx) {
|
|
161
|
+
const ensureCtx = buildEnsureCtx(ctx, options);
|
|
162
|
+
const durability = resolveSandboxDurability(options);
|
|
163
|
+
if (durability !== void 0) {
|
|
164
|
+
provideSandboxDurability(ctx, durability);
|
|
165
|
+
provideDetachableRun(ctx, true);
|
|
166
|
+
}
|
|
167
|
+
const runtime = getSandboxRuntime(ctx, { optional: true });
|
|
168
|
+
const logger = runtime?.logger;
|
|
169
|
+
const state = {
|
|
170
|
+
ensureCtx,
|
|
171
|
+
pendingDiffs: [],
|
|
172
|
+
toolHistory: createToolHistoryRecorder(),
|
|
173
|
+
...logger ? { logger } : {},
|
|
174
|
+
...durability ? { durability } : {}
|
|
175
|
+
};
|
|
176
|
+
runState.set(ctx, state);
|
|
177
|
+
if (durability !== void 0) {
|
|
178
|
+
try {
|
|
179
|
+
await durability.runs.createOrResume({
|
|
180
|
+
runId: ctx.runId,
|
|
181
|
+
threadId: ctx.threadId,
|
|
182
|
+
startedAt: Date.now()
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger?.warn("sandbox run record pre-create failed", {
|
|
186
|
+
runId: ctx.runId,
|
|
187
|
+
error
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await getPendingTurn(ctx, { optional: true })?.snapshot();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger?.warn("sandbox pending-turn snapshot failed", {
|
|
194
|
+
runId: ctx.runId,
|
|
195
|
+
error
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (durability !== void 0 && durability.detachOnDisconnect) getRunDisconnect(ctx, { optional: true })?.subscribe(async () => {
|
|
200
|
+
if (await cancelIntent(durability, ctx.runId, false)) return;
|
|
201
|
+
if (await recordDetach(definition, state, durability, ctx, "disconnect")) state.logger?.sandbox("sandbox run detached on disconnect; the run continues", { runId: ctx.runId });
|
|
202
|
+
});
|
|
203
|
+
const handle = await definition.ensure(ensureCtx);
|
|
204
|
+
state.handle = handle;
|
|
205
|
+
provideSandbox(ctx, handle);
|
|
206
|
+
if (definition.policy) provideSandboxPolicy(ctx, definition.policy);
|
|
207
|
+
if (durability !== void 0 && (ensureCtx.locks === void 0 || ensureCtx.locks instanceof InMemoryLockStore)) logger?.warn("sandbox durability is wired over an InMemoryLockStore: run claims are serialized within this process only and the lease never signals loss, so two hosts can drive one run and duplicate its event log. Use a distributed LockStore via withLocks for any multi-replica deploy.", { runId: ctx.runId });
|
|
208
|
+
const watchRoot = definition.workspace?.root ?? "/workspace";
|
|
209
|
+
let baseSha = "";
|
|
210
|
+
try {
|
|
211
|
+
const shaRes = await handle.process.exec("git rev-parse HEAD", { cwd: watchRoot });
|
|
212
|
+
if (shaRes.exitCode === 0) {
|
|
213
|
+
baseSha = shaRes.stdout.trim();
|
|
214
|
+
logger?.sandbox("sandbox git baseline captured", {
|
|
215
|
+
root: watchRoot,
|
|
216
|
+
baseSha
|
|
217
|
+
});
|
|
218
|
+
} else logger?.sandbox("sandbox git baseline unavailable (non-zero exit)", {
|
|
219
|
+
root: watchRoot,
|
|
220
|
+
exitCode: shaRes.exitCode,
|
|
221
|
+
stderr: shaRes.stderr
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger?.warn("sandbox git baseline capture failed", {
|
|
225
|
+
root: watchRoot,
|
|
226
|
+
error
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const workspace = definition.workspace;
|
|
230
|
+
if (workspace !== void 0) {
|
|
231
|
+
const root = workspace.root ?? "/workspace";
|
|
232
|
+
const workspaceHash = computeWorkspaceHash(workspace);
|
|
233
|
+
const secrets = workspace.secrets;
|
|
234
|
+
provideWorkspaceProjection(ctx, {
|
|
235
|
+
skills: workspace.skills ?? [],
|
|
236
|
+
plugins: workspace.plugins ?? [],
|
|
237
|
+
resolveSecret: (ref) => {
|
|
238
|
+
if (secrets === void 0) throw new Error(`resolveSecret: no secrets defined on this workspace (ref: "${ref.__secretName}")`);
|
|
239
|
+
return resolveSecret(secrets, ref);
|
|
240
|
+
},
|
|
241
|
+
markerPath: `${root}/.tanstack-projected-${workspaceHash}`,
|
|
242
|
+
root,
|
|
243
|
+
...workspace.scripts !== void 0 ? { scripts: workspace.scripts } : {}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const hooks = definition.hooks;
|
|
247
|
+
await hooks?.onReady?.(handle);
|
|
248
|
+
const fe = resolveFileEvents(definition.fileEvents);
|
|
249
|
+
const pendingDiffs = state.pendingDiffs;
|
|
250
|
+
let watcher;
|
|
251
|
+
if (fe.enabled) {
|
|
252
|
+
watcher = await watchWorkspace(handle, {
|
|
253
|
+
onEvent: (event) => {
|
|
254
|
+
const enriched = buildFileHookEvent(handle, watchRoot, baseSha, event, logger);
|
|
255
|
+
dispatchDefinitionHooks(hooks, enriched, logger);
|
|
256
|
+
runtime?.emit(enriched);
|
|
257
|
+
if (fe.diff) pendingDiffs.push(enriched.diff().then((diff) => {
|
|
258
|
+
runtime?.emitFileDiff({
|
|
259
|
+
path: event.path,
|
|
260
|
+
diff
|
|
261
|
+
});
|
|
262
|
+
}).catch((error) => {
|
|
263
|
+
logger?.warn("sandbox file diff emit failed", {
|
|
264
|
+
path: event.path,
|
|
265
|
+
error
|
|
266
|
+
});
|
|
267
|
+
}));
|
|
268
|
+
},
|
|
269
|
+
root: watchRoot,
|
|
270
|
+
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
271
|
+
...logger !== void 0 ? { logger } : {}
|
|
272
|
+
});
|
|
273
|
+
logger?.sandbox("sandbox watcher started", {
|
|
274
|
+
root: watchRoot,
|
|
275
|
+
diff: fe.diff
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (watcher) state.watcher = watcher;
|
|
279
|
+
},
|
|
280
|
+
onConfig(_ctx, config) {
|
|
281
|
+
const messages = stripObservedToolCalls(config.messages);
|
|
282
|
+
if (messages.length === config.messages.length) return;
|
|
283
|
+
return { messages };
|
|
284
|
+
},
|
|
285
|
+
onIteration(ctx) {
|
|
286
|
+
runState.get(ctx)?.toolHistory.reconcile(ctx);
|
|
287
|
+
},
|
|
288
|
+
onChunk(ctx, chunk) {
|
|
289
|
+
runState.get(ctx)?.toolHistory.observe(chunk, ctx);
|
|
290
|
+
},
|
|
291
|
+
async onFinish(ctx) {
|
|
292
|
+
const state = runState.get(ctx);
|
|
293
|
+
if (!state) return;
|
|
294
|
+
const { handle, ensureCtx } = state;
|
|
295
|
+
state.toolHistory.reconcile(ctx);
|
|
296
|
+
await drainWatcher(state, "finish");
|
|
297
|
+
const lifecycle = definition.lifecycle;
|
|
298
|
+
if (lifecycle?.snapshot === "after-run" && handle?.capabilities.snapshots && handle.snapshot) {
|
|
299
|
+
const snapshot = await handle.snapshot(`after-run-${ctx.runId}`);
|
|
300
|
+
const store = ensureCtx.store;
|
|
301
|
+
if (store) {
|
|
302
|
+
const key = definition.key(ensureCtx);
|
|
303
|
+
const existing = await store.get(key);
|
|
304
|
+
if (existing) await store.upsert({
|
|
305
|
+
...existing,
|
|
306
|
+
latestSnapshotId: snapshot.id,
|
|
307
|
+
updatedAt: Date.now()
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (lifecycle?.destroyOnComplete) {
|
|
312
|
+
await definition.destroy(ensureCtx);
|
|
313
|
+
await definition.hooks?.onDestroy?.();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
async onAbort(ctx, info) {
|
|
317
|
+
const state = runState.get(ctx);
|
|
318
|
+
if (!state) return;
|
|
319
|
+
await drainWatcher(state, "abort");
|
|
320
|
+
const durability = state.durability;
|
|
321
|
+
const cancelled = await cancelIntent(durability, ctx.runId, info.cancelRequested === true);
|
|
322
|
+
if (durability !== void 0 && !cancelled && durability.detachOnDisconnect) {
|
|
323
|
+
if (await recordDetach(definition, state, durability, ctx, "abort")) return;
|
|
324
|
+
await definition.destroy(state.ensureCtx);
|
|
325
|
+
await definition.hooks?.onDestroy?.();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
await definition.destroy(state.ensureCtx);
|
|
329
|
+
await definition.hooks?.onDestroy?.();
|
|
330
|
+
},
|
|
331
|
+
async onError(ctx, info) {
|
|
332
|
+
const state = runState.get(ctx);
|
|
333
|
+
if (!state) return;
|
|
334
|
+
await drainWatcher(state, "error");
|
|
335
|
+
await definition.hooks?.onError?.(info.error);
|
|
336
|
+
if (definition.lifecycle?.destroyOnComplete) {
|
|
337
|
+
await definition.destroy(state.ensureCtx);
|
|
338
|
+
await definition.hooks?.onDestroy?.();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
215
342
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//# sourceMappingURL=middleware.js.map
|
|
343
|
+
//#endregion
|
|
344
|
+
export { withSandbox };
|
|
345
|
+
|
|
346
|
+
//# sourceMappingURL=middleware.js.map
|