@smithers-orchestrator/sandbox 0.20.3 → 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,6 +149,157 @@ 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
+ }
291
+ export const __executeSandboxInternals = {
292
+ directorySize,
293
+ diffBundlePatchCount,
294
+ isDiffBundleLike,
295
+ materializeProviderResult,
296
+ requireSandboxHandle,
297
+ redactSandboxConfig,
298
+ resolveRuntimeDbAdapter,
299
+ resolveSandboxProvider,
300
+ resolveSandboxCommand,
301
+ sandboxExecutionContext,
302
+ };
85
303
  /**
86
304
  * @returns {number}
87
305
  */
@@ -108,24 +326,42 @@ function isSandboxActive(status) {
108
326
  */
109
327
  export async function executeSandbox(options) {
110
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
+ }
111
338
  runtime.heartbeat({
112
339
  sandboxId: options.sandboxId,
113
340
  stage: "initializing",
114
341
  progress: 0,
115
342
  });
116
- const adapter = new SmithersDb(runtime.db);
117
- const requestedRuntime = options.runtime ?? "bubblewrap";
118
- 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");
119
353
  const createdAtMs = nowMs();
354
+ const rawConfig = asPlainObject(options.config) ?? {};
120
355
  const configJson = JSON.stringify({
121
- runtime: requestedRuntime,
356
+ provider: provider?.id,
357
+ runtime: requestedRuntime ?? (provider ? undefined : selectedRuntime),
122
358
  selectedRuntime,
123
359
  allowNetwork: options.allowNetwork,
124
360
  maxOutputBytes: options.maxOutputBytes,
125
361
  toolTimeoutMs: options.toolTimeoutMs,
126
362
  reviewDiffs: options.reviewDiffs ?? true,
127
363
  autoAcceptDiffs: Boolean(options.autoAcceptDiffs),
128
- ...options.config,
364
+ ...redactSandboxConfig(rawConfig),
129
365
  });
130
366
  const sandboxRoot = join(options.rootDir, ".smithers", "sandboxes", runtime.runId, options.sandboxId);
131
367
  const requestBundlePath = join(sandboxRoot, "request-bundle");
@@ -134,6 +370,7 @@ export async function executeSandbox(options) {
134
370
  */
135
371
  const childLogPath = (childRunId) => join(options.rootDir, ".smithers", "executions", childRunId, "logs", "stream.ndjson");
136
372
  let handle = null;
373
+ let providerRequest = null;
137
374
  try {
138
375
  const existingSandboxes = await adapter.listSandboxes(runtime.runId);
139
376
  const activeSandboxCount = existingSandboxes.filter((row) => isSandboxActive(row?.status)).length;
@@ -158,7 +395,7 @@ export async function executeSandbox(options) {
158
395
  completedAtMs: null,
159
396
  bundlePath: null,
160
397
  });
161
- await emitSandboxEvent(runtime.db, {
398
+ await emitSandboxEvent(runtimeDb, {
162
399
  type: "SandboxCreated",
163
400
  runId: runtime.runId,
164
401
  sandboxId: options.sandboxId,
@@ -175,21 +412,165 @@ export async function executeSandbox(options) {
175
412
  await writeFile(join(requestBundlePath, "README.md"), JSON.stringify({
176
413
  status: "pending",
177
414
  sandboxId: options.sandboxId,
178
- runtime: selectedRuntime,
415
+ provider: selectedRuntime,
416
+ runtime: provider ? options.runtime : selectedRuntime,
179
417
  input: options.input ?? {},
180
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
+ }
181
555
  const transportConfig = {
182
556
  runId: runtime.runId,
183
557
  sandboxId: options.sandboxId,
184
558
  runtime: selectedRuntime,
185
559
  rootDir: options.rootDir,
186
- 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,
187
568
  };
188
569
  handle = await transportCall(selectedRuntime, sandboxTransport((svc) => svc.create(transportConfig)));
189
570
  const sandboxHandle = requireSandboxHandle(handle, options.sandboxId);
190
571
  await transportCall(selectedRuntime, sandboxTransport((svc) => svc.ship(requestBundlePath, sandboxHandle)));
191
572
  const bundleSizeBytes = await directorySize(join(requestBundlePath, "README.md"));
192
- await emitSandboxEvent(runtime.db, {
573
+ await emitSandboxEvent(runtimeDb, {
193
574
  type: "SandboxShipped",
194
575
  runId: runtime.runId,
195
576
  sandboxId: options.sandboxId,
@@ -215,7 +596,9 @@ export async function executeSandbox(options) {
215
596
  completedAtMs: null,
216
597
  bundlePath: null,
217
598
  });
218
- 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
+ }
219
602
  runtime.heartbeat({
220
603
  sandboxId: options.sandboxId,
221
604
  stage: "executing",
@@ -225,7 +608,12 @@ export async function executeSandbox(options) {
225
608
  throw new SmithersError("INVALID_INPUT", `Sandbox ${options.sandboxId} is missing a child workflow executor.`, { sandboxId: options.sandboxId });
226
609
  }
227
610
  const childStartedMs = performance.now();
228
- 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, {
229
617
  workflow: options.workflow,
230
618
  input: options.input,
231
619
  parentRunId: runtime.runId,
@@ -234,7 +622,7 @@ export async function executeSandbox(options) {
234
622
  maxOutputBytes: options.maxOutputBytes,
235
623
  toolTimeoutMs: options.toolTimeoutMs,
236
624
  signal: runtime.signal,
237
- });
625
+ }));
238
626
  runtime.heartbeat({
239
627
  sandboxId: options.sandboxId,
240
628
  stage: "child-finished",
@@ -242,7 +630,7 @@ export async function executeSandbox(options) {
242
630
  childRunId: child.runId,
243
631
  childStatus: child.status,
244
632
  });
245
- await emitSandboxEvent(runtime.db, {
633
+ await emitSandboxEvent(runtimeDb, {
246
634
  type: "SandboxHeartbeat",
247
635
  runId: runtime.runId,
248
636
  sandboxId: options.sandboxId,
@@ -259,34 +647,35 @@ export async function executeSandbox(options) {
259
647
  });
260
648
  const collected = await transportCall(selectedRuntime, sandboxTransport((svc) => svc.collect(sandboxHandle)));
261
649
  const validated = await validateSandboxBundle(collected.bundlePath);
650
+ const totalPatchCount = validated.patchFiles.length + diffBundlePatchCount(validated.manifest.diffBundle);
262
651
  runtime.heartbeat({
263
652
  sandboxId: options.sandboxId,
264
653
  stage: "bundle-collected",
265
654
  progress: 85,
266
655
  bundlePath: validated.bundlePath,
267
- patchCount: validated.patchFiles.length,
656
+ patchCount: totalPatchCount,
268
657
  });
269
- await emitSandboxEvent(runtime.db, {
658
+ await emitSandboxEvent(runtimeDb, {
270
659
  type: "SandboxBundleReceived",
271
660
  runId: runtime.runId,
272
661
  sandboxId: options.sandboxId,
273
662
  bundleSizeBytes: validated.bundleSizeBytes,
274
- patchCount: validated.patchFiles.length,
663
+ patchCount: totalPatchCount,
275
664
  hasOutputs: validated.manifest.outputs !== undefined,
276
665
  timestampMs: nowMs(),
277
666
  });
278
667
  const reviewDiffs = options.reviewDiffs ?? true;
279
- if (reviewDiffs && validated.patchFiles.length > 0) {
280
- await emitSandboxEvent(runtime.db, {
668
+ if (reviewDiffs && totalPatchCount > 0) {
669
+ await emitSandboxEvent(runtimeDb, {
281
670
  type: "SandboxDiffReviewRequested",
282
671
  runId: runtime.runId,
283
672
  sandboxId: options.sandboxId,
284
- patchCount: validated.patchFiles.length,
673
+ patchCount: totalPatchCount,
285
674
  totalDiffLines: 0,
286
675
  timestampMs: nowMs(),
287
676
  });
288
677
  if (!options.autoAcceptDiffs) {
289
- await emitSandboxEvent(runtime.db, {
678
+ await emitSandboxEvent(runtimeDb, {
290
679
  type: "SandboxDiffRejected",
291
680
  runId: runtime.runId,
292
681
  sandboxId: options.sandboxId,
@@ -295,17 +684,20 @@ export async function executeSandbox(options) {
295
684
  });
296
685
  throw new SmithersError("INVALID_INPUT", "Sandbox produced patches that require review approval.", {
297
686
  sandboxId: options.sandboxId,
298
- patchCount: validated.patchFiles.length,
687
+ patchCount: totalPatchCount,
299
688
  });
300
689
  }
301
- await emitSandboxEvent(runtime.db, {
690
+ await emitSandboxEvent(runtimeDb, {
302
691
  type: "SandboxDiffAccepted",
303
692
  runId: runtime.runId,
304
693
  sandboxId: options.sandboxId,
305
- patchCount: validated.patchFiles.length,
694
+ patchCount: totalPatchCount,
306
695
  timestampMs: nowMs(),
307
696
  });
308
697
  }
698
+ if (!reviewDiffs || totalPatchCount === 0 || options.autoAcceptDiffs) {
699
+ await applyAcceptedSandboxChanges(validated, options);
700
+ }
309
701
  await adapter.upsertSandbox({
310
702
  runId: runtime.runId,
311
703
  sandboxId: options.sandboxId,
@@ -319,7 +711,7 @@ export async function executeSandbox(options) {
319
711
  completedAtMs: nowMs(),
320
712
  bundlePath: validated.bundlePath,
321
713
  });
322
- await emitSandboxEvent(runtime.db, {
714
+ await emitSandboxEvent(runtimeDb, {
323
715
  type: "SandboxCompleted",
324
716
  runId: runtime.runId,
325
717
  sandboxId: options.sandboxId,
@@ -351,7 +743,7 @@ export async function executeSandbox(options) {
351
743
  completedAtMs: nowMs(),
352
744
  bundlePath: handle?.resultPath ?? null,
353
745
  });
354
- await emitSandboxEvent(runtime.db, {
746
+ await emitSandboxEvent(runtimeDb, {
355
747
  type: "SandboxFailed",
356
748
  runId: runtime.runId,
357
749
  sandboxId: options.sandboxId,
@@ -371,5 +763,8 @@ export async function executeSandbox(options) {
371
763
  if (handle) {
372
764
  await transportCall(selectedRuntime, sandboxTransport((svc) => svc.cleanup(handle))).catch(() => undefined);
373
765
  }
766
+ if (provider && providerRequest && typeof provider.cleanup === "function") {
767
+ await Promise.resolve(provider.cleanup(providerRequest)).catch(() => undefined);
768
+ }
374
769
  }
375
770
  }