@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.
Files changed (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
@@ -1,219 +1,346 @@
1
- import { defineChatMiddleware } from "@tanstack/ai";
2
- import { getSandboxRuntime } from "@tanstack/ai/adapter-internals";
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 { DEFAULT_WORKSPACE_ROOT } from "./bootstrap.js";
10
- const runState = /* @__PURE__ */ new WeakMap();
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
- try {
13
- await state.watcher?.stop();
14
- } catch (error) {
15
- state.logger?.warn("sandbox watcher stop failed", { phase, error });
16
- }
17
- await Promise.allSettled(state.pendingDiffs);
18
- if (state.watcher) state.logger?.sandbox("sandbox watcher stopped", { phase });
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
- if (context === null || typeof context !== "object") return void 0;
22
- const c = context;
23
- const userId = typeof c.userId === "string" ? c.userId : void 0;
24
- const orgId = typeof c.orgId === "string" ? c.orgId : void 0;
25
- if (userId === void 0 && orgId === void 0) return void 0;
26
- return { userId, orgId };
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
- function buildEnsureCtx(ctx) {
29
- return {
30
- threadId: ctx.threadId,
31
- runId: ctx.runId,
32
- store: ctx.getOptional(SandboxStoreCapability),
33
- locks: ctx.getOptional(LocksCapability),
34
- tenant: tenantFrom(ctx.context),
35
- signal: ctx.signal
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
- if (!hooks) return;
40
- const typed = {
41
- create: "onFileCreate",
42
- change: "onFileChange",
43
- delete: "onFileDelete"
44
- }[event.type];
45
- for (const fn of [hooks.onFile, hooks[typed]]) {
46
- if (!fn) continue;
47
- try {
48
- await fn(event);
49
- } catch (error) {
50
- logger?.errors("sandbox file hook failed", {
51
- path: event.path,
52
- type: event.type,
53
- error
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
- return defineChatMiddleware({
60
- name: "sandbox",
61
- provides: [SandboxCapability, ProjectionCapability],
62
- // SandboxPolicyCapability is provided conditionally (only when the
63
- // definition has a policy), so it is intentionally NOT declared here —
64
- // consumers read it via `getOptional`.
65
- optionalRequires: [SandboxStoreCapability, LocksCapability],
66
- async setup(ctx) {
67
- const ensureCtx = buildEnsureCtx(ctx);
68
- const handle = await definition.ensure(ensureCtx);
69
- provideSandbox(ctx, handle);
70
- if (definition.policy) provideSandboxPolicy(ctx, definition.policy);
71
- const runtime = getSandboxRuntime(ctx, { optional: true });
72
- const logger = runtime?.logger;
73
- const watchRoot = definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT;
74
- let baseSha = "";
75
- try {
76
- const shaRes = await handle.process.exec("git rev-parse HEAD", {
77
- cwd: watchRoot
78
- });
79
- if (shaRes.exitCode === 0) {
80
- baseSha = shaRes.stdout.trim();
81
- logger?.sandbox("sandbox git baseline captured", {
82
- root: watchRoot,
83
- baseSha
84
- });
85
- } else {
86
- logger?.sandbox("sandbox git baseline unavailable (non-zero exit)", {
87
- root: watchRoot,
88
- exitCode: shaRes.exitCode,
89
- stderr: shaRes.stderr
90
- });
91
- }
92
- } catch (error) {
93
- logger?.warn("sandbox git baseline capture failed", {
94
- root: watchRoot,
95
- error
96
- });
97
- }
98
- const workspace = definition.workspace;
99
- if (workspace !== void 0) {
100
- const root = workspace.root ?? DEFAULT_WORKSPACE_ROOT;
101
- const workspaceHash = computeWorkspaceHash(workspace);
102
- const secrets = workspace.secrets;
103
- provideWorkspaceProjection(ctx, {
104
- skills: workspace.skills ?? [],
105
- plugins: workspace.plugins ?? [],
106
- resolveSecret: (ref) => {
107
- if (secrets === void 0) {
108
- throw new Error(
109
- `resolveSecret: no secrets defined on this workspace (ref: "${ref.__secretName}")`
110
- );
111
- }
112
- return resolveSecret(secrets, ref);
113
- },
114
- markerPath: `${root}/.tanstack-projected-${workspaceHash}`,
115
- root,
116
- ...workspace.scripts !== void 0 ? { scripts: workspace.scripts } : {}
117
- });
118
- }
119
- const hooks = definition.hooks;
120
- await hooks?.onReady?.(handle);
121
- const fe = resolveFileEvents(definition.fileEvents);
122
- const pendingDiffs = [];
123
- let watcher;
124
- if (fe.enabled) {
125
- watcher = await watchWorkspace(handle, {
126
- onEvent: (event) => {
127
- const enriched = buildFileHookEvent(
128
- handle,
129
- watchRoot,
130
- baseSha,
131
- event,
132
- logger
133
- );
134
- void dispatchDefinitionHooks(hooks, enriched, logger);
135
- runtime?.emit(enriched);
136
- if (fe.diff) {
137
- pendingDiffs.push(
138
- enriched.diff().then((diff) => {
139
- runtime?.emitFileDiff({ path: event.path, diff });
140
- }).catch((error) => {
141
- logger?.warn("sandbox file diff emit failed", {
142
- path: event.path,
143
- error
144
- });
145
- })
146
- );
147
- }
148
- },
149
- // Watch the SAME root the enrichment layer relativizes against
150
- // (`buildFileHookEvent(handle, watchRoot, …)` and the `baseSha`
151
- // capture). Without this the watcher defaults to `/workspace` while
152
- // enrichment uses `watchRoot`, so a custom `workspace.root` makes the
153
- // two look at different directories and git pathspecs break.
154
- root: watchRoot,
155
- ...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
156
- ...logger !== void 0 ? { logger } : {}
157
- });
158
- logger?.sandbox("sandbox watcher started", {
159
- root: watchRoot,
160
- diff: fe.diff
161
- });
162
- }
163
- runState.set(ctx, {
164
- handle,
165
- ensureCtx,
166
- pendingDiffs,
167
- ...watcher ? { watcher } : {},
168
- ...logger !== void 0 ? { logger } : {}
169
- });
170
- },
171
- async onFinish(ctx) {
172
- const state = runState.get(ctx);
173
- if (!state) return;
174
- const { handle, ensureCtx } = state;
175
- await drainWatcher(state, "finish");
176
- const lifecycle = definition.lifecycle;
177
- if (lifecycle?.snapshot === "after-run" && handle.capabilities.snapshots && handle.snapshot) {
178
- const snapshot = await handle.snapshot(`after-run-${ctx.runId}`);
179
- const store = ensureCtx.store;
180
- if (store) {
181
- const key = definition.key(ensureCtx);
182
- const existing = await store.get(key);
183
- if (existing) {
184
- await store.upsert({
185
- ...existing,
186
- latestSnapshotId: snapshot.id,
187
- updatedAt: Date.now()
188
- });
189
- }
190
- }
191
- }
192
- if (lifecycle?.destroyOnComplete) {
193
- await definition.destroy(ensureCtx);
194
- await definition.hooks?.onDestroy?.();
195
- }
196
- },
197
- async onAbort(ctx, _info) {
198
- const state = runState.get(ctx);
199
- if (!state) return;
200
- await drainWatcher(state, "abort");
201
- await definition.destroy(state.ensureCtx);
202
- await definition.hooks?.onDestroy?.();
203
- },
204
- async onError(ctx, info) {
205
- const state = runState.get(ctx);
206
- if (!state) return;
207
- await drainWatcher(state, "error");
208
- await definition.hooks?.onError?.(info.error);
209
- if (definition.lifecycle?.destroyOnComplete) {
210
- await definition.destroy(state.ensureCtx);
211
- await definition.hooks?.onDestroy?.();
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
- export {
217
- withSandbox
218
- };
219
- //# sourceMappingURL=middleware.js.map
343
+ //#endregion
344
+ export { withSandbox };
345
+
346
+ //# sourceMappingURL=middleware.js.map