@smithers-orchestrator/sandbox 0.20.4 → 0.21.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/src/execute.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { mkdir, stat, writeFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { Effect, Metric } from "effect";
@@ -13,16 +14,82 @@ import { SandboxTransport, layerForSandboxRuntime, resolveSandboxRuntime, } from
13
14
  /** @typedef {import("./SandboxRuntime.ts").SandboxRuntime} SandboxRuntime */
14
15
  /** @typedef {import("./SandboxHandle.ts").SandboxHandle} SandboxHandle */
15
16
  /** @typedef {import("./SandboxTransportService.ts").SandboxTransportService} SandboxTransportService */
17
+ /** @typedef {import("./SandboxProvider.ts").SandboxProvider} SandboxProvider */
18
+ /** @typedef {import("./SandboxProvider.ts").SandboxProviderRequest} SandboxProviderRequest */
19
+ /** @typedef {import("./SandboxProvider.ts").SandboxProviderResult} SandboxProviderResult */
16
20
  /** @typedef {import("@smithers-orchestrator/observability/SmithersEvent").SmithersEvent} SmithersEvent */
17
21
 
18
22
  const DEFAULT_MAX_CONCURRENT_SANDBOXES = 10;
23
+ const sandboxProviderRegistry = new Map();
24
+ const sandboxExecutionContext = new AsyncLocalStorage();
25
+
26
+ /**
27
+ * @param {SandboxProvider} provider
28
+ * @returns {() => void}
29
+ */
30
+ export function registerSandboxProvider(provider) {
31
+ if (!provider || typeof provider !== "object" || typeof provider.run !== "function") {
32
+ throw new SmithersError("INVALID_INPUT", "Sandbox provider must be an object with a run(request) function.");
33
+ }
34
+ if (typeof provider.id !== "string" || provider.id.trim().length === 0) {
35
+ throw new SmithersError("INVALID_INPUT", "Sandbox provider must include a non-empty id.");
36
+ }
37
+ const id = provider.id.trim();
38
+ sandboxProviderRegistry.set(id, { ...provider, id });
39
+ return () => {
40
+ if (sandboxProviderRegistry.get(id)?.run === provider.run) {
41
+ sandboxProviderRegistry.delete(id);
42
+ }
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @param {unknown} value
48
+ * @returns {SandboxProvider | undefined}
49
+ */
50
+ export function resolveSandboxProvider(value) {
51
+ if (value === undefined || value === null) {
52
+ return undefined;
53
+ }
54
+ if (typeof value === "string") {
55
+ const provider = sandboxProviderRegistry.get(value);
56
+ if (!provider) {
57
+ throw new SmithersError("INVALID_INPUT", `Sandbox provider "${value}" is not registered.`, { provider: value });
58
+ }
59
+ return provider;
60
+ }
61
+ if (typeof value === "object" && typeof value.run === "function") {
62
+ const id = typeof value.id === "string" && value.id.trim().length > 0
63
+ ? value.id.trim()
64
+ : "custom";
65
+ return { ...value, id };
66
+ }
67
+ throw new SmithersError("INVALID_INPUT", "Sandbox provider must be a registered provider id or a provider object.", { providerType: typeof value });
68
+ }
69
+ /**
70
+ * @param {unknown} value
71
+ * @returns {value is SmithersDb}
72
+ */
73
+ function isSmithersDbAdapter(value) {
74
+ return Boolean(value &&
75
+ typeof value === "object" &&
76
+ typeof value.insertEventWithNextSeq === "function" &&
77
+ typeof value.listSandboxes === "function");
78
+ }
79
+ /**
80
+ * @param {ConstructorParameters<typeof SmithersDb>[0] | SmithersDb} db
81
+ * @returns {SmithersDb}
82
+ */
83
+ function resolveRuntimeDbAdapter(db) {
84
+ return isSmithersDbAdapter(db) ? db : new SmithersDb(db);
85
+ }
19
86
  /**
20
87
  * @param {ConstructorParameters<typeof SmithersDb>[0]} db
21
88
  * @param {SmithersEvent} event
22
89
  * @returns {Promise<void>}
23
90
  */
24
91
  async function emitSandboxEvent(db, event) {
25
- const adapter = new SmithersDb(db);
92
+ const adapter = resolveRuntimeDbAdapter(db);
26
93
  await adapter.insertEventWithNextSeq({
27
94
  runId: event.runId,
28
95
  timestampMs: event.timestampMs,
@@ -82,9 +149,156 @@ function requireSandboxHandle(handle, sandboxId) {
82
149
  return handle;
83
150
  throw new SmithersError("SANDBOX_EXECUTION_FAILED", `Sandbox ${sandboxId} did not initialize correctly.`, { sandboxId });
84
151
  }
152
+ /**
153
+ * @param {unknown} command
154
+ * @returns {string}
155
+ */
156
+ function resolveSandboxCommand(command) {
157
+ return typeof command === "string" && command.trim().length > 0
158
+ ? command
159
+ : "smithers up bundle.tsx";
160
+ }
161
+ /**
162
+ * @param {unknown} value
163
+ * @returns {Record<string, unknown> | null}
164
+ */
165
+ function asPlainObject(value) {
166
+ return value && typeof value === "object" && !Array.isArray(value)
167
+ ? /** @type {Record<string, unknown>} */ (value)
168
+ : null;
169
+ }
170
+ /**
171
+ * @param {unknown} value
172
+ * @returns {number}
173
+ */
174
+ function diffBundlePatchCount(value) {
175
+ const bundle = asPlainObject(value);
176
+ const patches = Array.isArray(bundle?.patches) ? bundle.patches : [];
177
+ return patches.length;
178
+ }
179
+ /**
180
+ * @param {unknown} status
181
+ * @returns {status is "finished" | "failed" | "cancelled"}
182
+ */
183
+ function isSandboxBundleStatus(status) {
184
+ return status === "finished" || status === "failed" || status === "cancelled";
185
+ }
186
+ /**
187
+ * @param {SandboxProviderResult} result
188
+ * @param {string} defaultBundlePath
189
+ * @returns {Promise<{ bundlePath: string; remoteRunId: string | null; workspaceId: string | null; containerId: string | null; }>}
190
+ */
191
+ async function materializeProviderResult(result, defaultBundlePath) {
192
+ const source = asPlainObject(result);
193
+ if (!source) {
194
+ throw new SmithersError("SANDBOX_EXECUTION_FAILED", "Sandbox provider returned an invalid result.");
195
+ }
196
+ if (typeof source.bundlePath === "string" && source.bundlePath.length > 0) {
197
+ return {
198
+ bundlePath: source.bundlePath,
199
+ remoteRunId: typeof source.remoteRunId === "string" ? source.remoteRunId : null,
200
+ workspaceId: typeof source.workspaceId === "string" ? source.workspaceId : null,
201
+ containerId: typeof source.containerId === "string" ? source.containerId : null,
202
+ };
203
+ }
204
+ if (!isSandboxBundleStatus(source.status)) {
205
+ throw new SmithersError("SANDBOX_EXECUTION_FAILED", "Sandbox provider result must include either bundlePath or status.", {
206
+ status: source.status,
207
+ });
208
+ }
209
+ const remoteRunId = typeof source.remoteRunId === "string"
210
+ ? source.remoteRunId
211
+ : typeof source.runId === "string"
212
+ ? source.runId
213
+ : null;
214
+ await writeSandboxBundle({
215
+ bundlePath: defaultBundlePath,
216
+ output: source.outputs ?? source.output,
217
+ status: source.status,
218
+ runId: remoteRunId ?? undefined,
219
+ streamLogPath: typeof source.streamLogPath === "string" ? source.streamLogPath : null,
220
+ patches: Array.isArray(source.patches) ? source.patches : undefined,
221
+ artifacts: Array.isArray(source.artifacts) ? source.artifacts : undefined,
222
+ diffBundle: source.diffBundle,
223
+ });
224
+ return {
225
+ bundlePath: defaultBundlePath,
226
+ remoteRunId,
227
+ workspaceId: typeof source.workspaceId === "string" ? source.workspaceId : null,
228
+ containerId: typeof source.containerId === "string" ? source.containerId : null,
229
+ };
230
+ }
231
+ /**
232
+ * @param {SandboxProvider} provider
233
+ * @param {SandboxProviderRequest} request
234
+ * @returns {Promise<SandboxProviderResult>}
235
+ */
236
+ async function runSandboxProvider(provider, request) {
237
+ return sandboxExecutionContext.run({
238
+ depth: (sandboxExecutionContext.getStore()?.depth ?? 0) + 1,
239
+ sandboxId: request.sandboxId,
240
+ runId: request.runId,
241
+ providerId: provider.id,
242
+ }, async () => provider.run(request));
243
+ }
244
+ /**
245
+ * @param {unknown} bundle
246
+ * @returns {bundle is import("./SandboxProvider.ts").SandboxDiffBundleLike}
247
+ */
248
+ function isDiffBundleLike(bundle) {
249
+ const source = asPlainObject(bundle);
250
+ return Boolean(source &&
251
+ typeof source.seq === "number" &&
252
+ typeof source.baseRef === "string" &&
253
+ Array.isArray(source.patches));
254
+ }
255
+ /**
256
+ * @param {import("./ValidatedSandboxBundle.ts").ValidatedSandboxBundle} validated
257
+ * @param {ExecuteSandboxOptions} options
258
+ */
259
+ async function applyAcceptedSandboxChanges(validated, options) {
260
+ const diffBundle = validated.manifest.diffBundle;
261
+ if (diffBundle === undefined) {
262
+ return;
263
+ }
264
+ if (!isDiffBundleLike(diffBundle)) {
265
+ throw new SmithersError("INVALID_INPUT", "Sandbox bundle diffBundle is malformed.", {
266
+ sandboxId: options.sandboxId,
267
+ });
268
+ }
269
+ if (typeof options.applyDiffBundle !== "function") {
270
+ throw new SmithersError("INVALID_INPUT", "Sandbox bundle contains a diffBundle but no diff applier was provided.", {
271
+ sandboxId: options.sandboxId,
272
+ });
273
+ }
274
+ await options.applyDiffBundle(diffBundle, options.rootDir);
275
+ }
276
+ /**
277
+ * @param {unknown} config
278
+ */
279
+ function redactSandboxConfig(config) {
280
+ const source = asPlainObject(config);
281
+ if (!source) {
282
+ return config;
283
+ }
284
+ const redacted = { ...source };
285
+ const env = asPlainObject(source.env);
286
+ if (env) {
287
+ redacted.env = Object.fromEntries(Object.keys(env).sort().map((key) => [key, "[redacted]"]));
288
+ }
289
+ return redacted;
290
+ }
85
291
  export const __executeSandboxInternals = {
86
292
  directorySize,
293
+ diffBundlePatchCount,
294
+ isDiffBundleLike,
295
+ materializeProviderResult,
87
296
  requireSandboxHandle,
297
+ redactSandboxConfig,
298
+ resolveRuntimeDbAdapter,
299
+ resolveSandboxProvider,
300
+ resolveSandboxCommand,
301
+ sandboxExecutionContext,
88
302
  };
89
303
  /**
90
304
  * @returns {number}
@@ -112,24 +326,42 @@ function isSandboxActive(status) {
112
326
  */
113
327
  export async function executeSandbox(options) {
114
328
  const runtime = requireTaskRuntime();
329
+ const parentSandbox = sandboxExecutionContext.getStore();
330
+ if (parentSandbox && !options.allowNested) {
331
+ throw new SmithersError("INVALID_INPUT", "Nested <Sandbox> execution is disabled by default. Set allowNested on the nested sandbox only if the provider and diff policy are explicitly designed for nesting.", {
332
+ sandboxId: options.sandboxId,
333
+ parentSandboxId: parentSandbox.sandboxId,
334
+ parentRunId: parentSandbox.runId,
335
+ parentProviderId: parentSandbox.providerId,
336
+ });
337
+ }
115
338
  runtime.heartbeat({
116
339
  sandboxId: options.sandboxId,
117
340
  stage: "initializing",
118
341
  progress: 0,
119
342
  });
120
- const adapter = new SmithersDb(runtime.db);
121
- const requestedRuntime = options.runtime ?? "bubblewrap";
122
- const selectedRuntime = resolveSandboxRuntime(requestedRuntime);
343
+ const runtimeDb = runtime.db ?? options.parentWorkflow?.db;
344
+ if (!runtimeDb) {
345
+ throw new SmithersError("TASK_RUNTIME_UNAVAILABLE", "Sandbox execution requires a task runtime database.", {
346
+ sandboxId: options.sandboxId,
347
+ });
348
+ }
349
+ const adapter = resolveRuntimeDbAdapter(runtimeDb);
350
+ const provider = resolveSandboxProvider(options.provider);
351
+ const requestedRuntime = options.runtime;
352
+ const selectedRuntime = provider ? provider.id : resolveSandboxRuntime(requestedRuntime ?? "bubblewrap");
123
353
  const createdAtMs = nowMs();
354
+ const rawConfig = asPlainObject(options.config) ?? {};
124
355
  const configJson = JSON.stringify({
125
- runtime: requestedRuntime,
356
+ provider: provider?.id,
357
+ runtime: requestedRuntime ?? (provider ? undefined : selectedRuntime),
126
358
  selectedRuntime,
127
359
  allowNetwork: options.allowNetwork,
128
360
  maxOutputBytes: options.maxOutputBytes,
129
361
  toolTimeoutMs: options.toolTimeoutMs,
130
362
  reviewDiffs: options.reviewDiffs ?? true,
131
363
  autoAcceptDiffs: Boolean(options.autoAcceptDiffs),
132
- ...options.config,
364
+ ...redactSandboxConfig(rawConfig),
133
365
  });
134
366
  const sandboxRoot = join(options.rootDir, ".smithers", "sandboxes", runtime.runId, options.sandboxId);
135
367
  const requestBundlePath = join(sandboxRoot, "request-bundle");
@@ -138,6 +370,7 @@ export async function executeSandbox(options) {
138
370
  */
139
371
  const childLogPath = (childRunId) => join(options.rootDir, ".smithers", "executions", childRunId, "logs", "stream.ndjson");
140
372
  let handle = null;
373
+ let providerRequest = null;
141
374
  try {
142
375
  const existingSandboxes = await adapter.listSandboxes(runtime.runId);
143
376
  const activeSandboxCount = existingSandboxes.filter((row) => isSandboxActive(row?.status)).length;
@@ -162,7 +395,7 @@ export async function executeSandbox(options) {
162
395
  completedAtMs: null,
163
396
  bundlePath: null,
164
397
  });
165
- await emitSandboxEvent(runtime.db, {
398
+ await emitSandboxEvent(runtimeDb, {
166
399
  type: "SandboxCreated",
167
400
  runId: runtime.runId,
168
401
  sandboxId: options.sandboxId,
@@ -179,21 +412,165 @@ export async function executeSandbox(options) {
179
412
  await writeFile(join(requestBundlePath, "README.md"), JSON.stringify({
180
413
  status: "pending",
181
414
  sandboxId: options.sandboxId,
182
- runtime: selectedRuntime,
415
+ provider: selectedRuntime,
416
+ runtime: provider ? options.runtime : selectedRuntime,
183
417
  input: options.input ?? {},
184
418
  }, null, 2), "utf8");
419
+ if (provider) {
420
+ const bundleSizeBytes = await directorySize(join(requestBundlePath, "README.md"));
421
+ await emitSandboxEvent(runtimeDb, {
422
+ type: "SandboxShipped",
423
+ runId: runtime.runId,
424
+ sandboxId: options.sandboxId,
425
+ runtime: selectedRuntime,
426
+ bundleSizeBytes,
427
+ timestampMs: nowMs(),
428
+ });
429
+ runtime.heartbeat({
430
+ sandboxId: options.sandboxId,
431
+ stage: "shipped",
432
+ progress: 25,
433
+ });
434
+ await adapter.upsertSandbox({
435
+ runId: runtime.runId,
436
+ sandboxId: options.sandboxId,
437
+ runtime: selectedRuntime,
438
+ remoteRunId: null,
439
+ workspaceId: null,
440
+ containerId: null,
441
+ configJson,
442
+ status: "shipped",
443
+ shippedAtMs: nowMs(),
444
+ completedAtMs: null,
445
+ bundlePath: null,
446
+ });
447
+ runtime.heartbeat({
448
+ sandboxId: options.sandboxId,
449
+ stage: "executing",
450
+ progress: 40,
451
+ });
452
+ const childStartedMs = performance.now();
453
+ providerRequest = {
454
+ runId: runtime.runId,
455
+ sandboxId: options.sandboxId,
456
+ input: options.input,
457
+ rootDir: options.rootDir,
458
+ requestBundlePath,
459
+ resultBundlePath: join(sandboxRoot, "result"),
460
+ workflow: options.workflow,
461
+ parentWorkflow: options.parentWorkflow,
462
+ executeChildWorkflow: options.executeChildWorkflow,
463
+ allowNetwork: options.allowNetwork,
464
+ maxOutputBytes: options.maxOutputBytes,
465
+ toolTimeoutMs: options.toolTimeoutMs,
466
+ config: rawConfig,
467
+ signal: runtime.signal,
468
+ heartbeat: runtime.heartbeat,
469
+ };
470
+ const providerResult = await runSandboxProvider(provider, providerRequest);
471
+ const materialized = await materializeProviderResult(providerResult, providerRequest.resultBundlePath);
472
+ const validated = await validateSandboxBundle(materialized.bundlePath);
473
+ const totalPatchCount = validated.patchFiles.length + diffBundlePatchCount(validated.manifest.diffBundle);
474
+ runtime.heartbeat({
475
+ sandboxId: options.sandboxId,
476
+ stage: "bundle-collected",
477
+ progress: 85,
478
+ bundlePath: validated.bundlePath,
479
+ patchCount: totalPatchCount,
480
+ });
481
+ await emitSandboxEvent(runtimeDb, {
482
+ type: "SandboxBundleReceived",
483
+ runId: runtime.runId,
484
+ sandboxId: options.sandboxId,
485
+ bundleSizeBytes: validated.bundleSizeBytes,
486
+ patchCount: totalPatchCount,
487
+ hasOutputs: validated.manifest.outputs !== undefined,
488
+ timestampMs: nowMs(),
489
+ });
490
+ const reviewDiffs = options.reviewDiffs ?? true;
491
+ if (reviewDiffs && totalPatchCount > 0) {
492
+ await emitSandboxEvent(runtimeDb, {
493
+ type: "SandboxDiffReviewRequested",
494
+ runId: runtime.runId,
495
+ sandboxId: options.sandboxId,
496
+ patchCount: totalPatchCount,
497
+ totalDiffLines: 0,
498
+ timestampMs: nowMs(),
499
+ });
500
+ if (!options.autoAcceptDiffs) {
501
+ await emitSandboxEvent(runtimeDb, {
502
+ type: "SandboxDiffRejected",
503
+ runId: runtime.runId,
504
+ sandboxId: options.sandboxId,
505
+ reason: "Diff review approval is required before applying sandbox patches.",
506
+ timestampMs: nowMs(),
507
+ });
508
+ throw new SmithersError("INVALID_INPUT", "Sandbox produced changes that require review approval.", {
509
+ sandboxId: options.sandboxId,
510
+ patchCount: totalPatchCount,
511
+ });
512
+ }
513
+ await emitSandboxEvent(runtimeDb, {
514
+ type: "SandboxDiffAccepted",
515
+ runId: runtime.runId,
516
+ sandboxId: options.sandboxId,
517
+ patchCount: totalPatchCount,
518
+ timestampMs: nowMs(),
519
+ });
520
+ }
521
+ if (!reviewDiffs || totalPatchCount === 0 || options.autoAcceptDiffs) {
522
+ await applyAcceptedSandboxChanges(validated, options);
523
+ }
524
+ await adapter.upsertSandbox({
525
+ runId: runtime.runId,
526
+ sandboxId: options.sandboxId,
527
+ runtime: selectedRuntime,
528
+ remoteRunId: materialized.remoteRunId ?? validated.manifest.runId ?? null,
529
+ workspaceId: materialized.workspaceId,
530
+ containerId: materialized.containerId,
531
+ configJson,
532
+ status: validated.manifest.status,
533
+ shippedAtMs: createdAtMs,
534
+ completedAtMs: nowMs(),
535
+ bundlePath: validated.bundlePath,
536
+ });
537
+ await emitSandboxEvent(runtimeDb, {
538
+ type: "SandboxCompleted",
539
+ runId: runtime.runId,
540
+ sandboxId: options.sandboxId,
541
+ remoteRunId: materialized.remoteRunId ?? validated.manifest.runId,
542
+ runtime: selectedRuntime,
543
+ status: validated.manifest.status,
544
+ durationMs: performance.now() - childStartedMs,
545
+ timestampMs: nowMs(),
546
+ });
547
+ runtime.heartbeat({
548
+ sandboxId: options.sandboxId,
549
+ stage: "completed",
550
+ progress: 100,
551
+ status: validated.manifest.status,
552
+ });
553
+ return validated.manifest.outputs;
554
+ }
185
555
  const transportConfig = {
186
556
  runId: runtime.runId,
187
557
  sandboxId: options.sandboxId,
188
558
  runtime: selectedRuntime,
189
559
  rootDir: options.rootDir,
190
- image: options.config?.image ?? undefined,
560
+ image: typeof rawConfig.image === "string" ? rawConfig.image : undefined,
561
+ allowNetwork: options.allowNetwork,
562
+ env: rawConfig.env,
563
+ ports: rawConfig.ports,
564
+ volumes: rawConfig.volumes,
565
+ memoryLimit: rawConfig.memoryLimit,
566
+ cpuLimit: rawConfig.cpuLimit,
567
+ workspace: rawConfig.workspace,
191
568
  };
192
569
  handle = await transportCall(selectedRuntime, sandboxTransport((svc) => svc.create(transportConfig)));
193
570
  const sandboxHandle = requireSandboxHandle(handle, options.sandboxId);
194
571
  await transportCall(selectedRuntime, sandboxTransport((svc) => svc.ship(requestBundlePath, sandboxHandle)));
195
572
  const bundleSizeBytes = await directorySize(join(requestBundlePath, "README.md"));
196
- await emitSandboxEvent(runtime.db, {
573
+ await emitSandboxEvent(runtimeDb, {
197
574
  type: "SandboxShipped",
198
575
  runId: runtime.runId,
199
576
  sandboxId: options.sandboxId,
@@ -219,7 +596,9 @@ export async function executeSandbox(options) {
219
596
  completedAtMs: null,
220
597
  bundlePath: null,
221
598
  });
222
- await transportCall(selectedRuntime, sandboxTransport((svc) => svc.execute("smithers up bundle.tsx", sandboxHandle)));
599
+ if (options.config?.command) {
600
+ await transportCall(selectedRuntime, sandboxTransport((svc) => svc.execute(resolveSandboxCommand(options.config?.command), sandboxHandle)));
601
+ }
223
602
  runtime.heartbeat({
224
603
  sandboxId: options.sandboxId,
225
604
  stage: "executing",
@@ -229,7 +608,12 @@ export async function executeSandbox(options) {
229
608
  throw new SmithersError("INVALID_INPUT", `Sandbox ${options.sandboxId} is missing a child workflow executor.`, { sandboxId: options.sandboxId });
230
609
  }
231
610
  const childStartedMs = performance.now();
232
- const child = await options.executeChildWorkflow(options.parentWorkflow, {
611
+ const child = await sandboxExecutionContext.run({
612
+ depth: (sandboxExecutionContext.getStore()?.depth ?? 0) + 1,
613
+ sandboxId: options.sandboxId,
614
+ runId: runtime.runId,
615
+ providerId: selectedRuntime,
616
+ }, async () => options.executeChildWorkflow(options.parentWorkflow, {
233
617
  workflow: options.workflow,
234
618
  input: options.input,
235
619
  parentRunId: runtime.runId,
@@ -238,7 +622,7 @@ export async function executeSandbox(options) {
238
622
  maxOutputBytes: options.maxOutputBytes,
239
623
  toolTimeoutMs: options.toolTimeoutMs,
240
624
  signal: runtime.signal,
241
- });
625
+ }));
242
626
  runtime.heartbeat({
243
627
  sandboxId: options.sandboxId,
244
628
  stage: "child-finished",
@@ -246,7 +630,7 @@ export async function executeSandbox(options) {
246
630
  childRunId: child.runId,
247
631
  childStatus: child.status,
248
632
  });
249
- await emitSandboxEvent(runtime.db, {
633
+ await emitSandboxEvent(runtimeDb, {
250
634
  type: "SandboxHeartbeat",
251
635
  runId: runtime.runId,
252
636
  sandboxId: options.sandboxId,
@@ -263,34 +647,35 @@ export async function executeSandbox(options) {
263
647
  });
264
648
  const collected = await transportCall(selectedRuntime, sandboxTransport((svc) => svc.collect(sandboxHandle)));
265
649
  const validated = await validateSandboxBundle(collected.bundlePath);
650
+ const totalPatchCount = validated.patchFiles.length + diffBundlePatchCount(validated.manifest.diffBundle);
266
651
  runtime.heartbeat({
267
652
  sandboxId: options.sandboxId,
268
653
  stage: "bundle-collected",
269
654
  progress: 85,
270
655
  bundlePath: validated.bundlePath,
271
- patchCount: validated.patchFiles.length,
656
+ patchCount: totalPatchCount,
272
657
  });
273
- await emitSandboxEvent(runtime.db, {
658
+ await emitSandboxEvent(runtimeDb, {
274
659
  type: "SandboxBundleReceived",
275
660
  runId: runtime.runId,
276
661
  sandboxId: options.sandboxId,
277
662
  bundleSizeBytes: validated.bundleSizeBytes,
278
- patchCount: validated.patchFiles.length,
663
+ patchCount: totalPatchCount,
279
664
  hasOutputs: validated.manifest.outputs !== undefined,
280
665
  timestampMs: nowMs(),
281
666
  });
282
667
  const reviewDiffs = options.reviewDiffs ?? true;
283
- if (reviewDiffs && validated.patchFiles.length > 0) {
284
- await emitSandboxEvent(runtime.db, {
668
+ if (reviewDiffs && totalPatchCount > 0) {
669
+ await emitSandboxEvent(runtimeDb, {
285
670
  type: "SandboxDiffReviewRequested",
286
671
  runId: runtime.runId,
287
672
  sandboxId: options.sandboxId,
288
- patchCount: validated.patchFiles.length,
673
+ patchCount: totalPatchCount,
289
674
  totalDiffLines: 0,
290
675
  timestampMs: nowMs(),
291
676
  });
292
677
  if (!options.autoAcceptDiffs) {
293
- await emitSandboxEvent(runtime.db, {
678
+ await emitSandboxEvent(runtimeDb, {
294
679
  type: "SandboxDiffRejected",
295
680
  runId: runtime.runId,
296
681
  sandboxId: options.sandboxId,
@@ -299,17 +684,20 @@ export async function executeSandbox(options) {
299
684
  });
300
685
  throw new SmithersError("INVALID_INPUT", "Sandbox produced patches that require review approval.", {
301
686
  sandboxId: options.sandboxId,
302
- patchCount: validated.patchFiles.length,
687
+ patchCount: totalPatchCount,
303
688
  });
304
689
  }
305
- await emitSandboxEvent(runtime.db, {
690
+ await emitSandboxEvent(runtimeDb, {
306
691
  type: "SandboxDiffAccepted",
307
692
  runId: runtime.runId,
308
693
  sandboxId: options.sandboxId,
309
- patchCount: validated.patchFiles.length,
694
+ patchCount: totalPatchCount,
310
695
  timestampMs: nowMs(),
311
696
  });
312
697
  }
698
+ if (!reviewDiffs || totalPatchCount === 0 || options.autoAcceptDiffs) {
699
+ await applyAcceptedSandboxChanges(validated, options);
700
+ }
313
701
  await adapter.upsertSandbox({
314
702
  runId: runtime.runId,
315
703
  sandboxId: options.sandboxId,
@@ -323,7 +711,7 @@ export async function executeSandbox(options) {
323
711
  completedAtMs: nowMs(),
324
712
  bundlePath: validated.bundlePath,
325
713
  });
326
- await emitSandboxEvent(runtime.db, {
714
+ await emitSandboxEvent(runtimeDb, {
327
715
  type: "SandboxCompleted",
328
716
  runId: runtime.runId,
329
717
  sandboxId: options.sandboxId,
@@ -355,7 +743,7 @@ export async function executeSandbox(options) {
355
743
  completedAtMs: nowMs(),
356
744
  bundlePath: handle?.resultPath ?? null,
357
745
  });
358
- await emitSandboxEvent(runtime.db, {
746
+ await emitSandboxEvent(runtimeDb, {
359
747
  type: "SandboxFailed",
360
748
  runId: runtime.runId,
361
749
  sandboxId: options.sandboxId,
@@ -375,5 +763,8 @@ export async function executeSandbox(options) {
375
763
  if (handle) {
376
764
  await transportCall(selectedRuntime, sandboxTransport((svc) => svc.cleanup(handle))).catch(() => undefined);
377
765
  }
766
+ if (provider && providerRequest && typeof provider.cleanup === "function") {
767
+ await Promise.resolve(provider.cleanup(providerRequest)).catch(() => undefined);
768
+ }
378
769
  }
379
770
  }