@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/package.json +6 -9
- package/src/ExecuteSandboxOptions.ts +17 -13
- package/src/SandboxBundleManifest.ts +7 -4
- package/src/SandboxHandle.ts +26 -0
- package/src/SandboxProvider.ts +59 -0
- package/src/SandboxTransportConfig.ts +8 -0
- package/src/bundle.js +25 -9
- package/src/effect/http-runner.js +23 -5
- package/src/effect/process-runner.js +428 -0
- package/src/effect/sandbox-entity.js +34 -0
- package/src/effect/socket-runner.js +33 -2
- package/src/execute.js +421 -26
- package/src/index.d.ts +96 -1
- package/src/sandboxPath.js +1 -5
- package/src/transport.js +11 -6
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 =
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
656
|
+
patchCount: totalPatchCount,
|
|
268
657
|
});
|
|
269
|
-
await emitSandboxEvent(
|
|
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:
|
|
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 &&
|
|
280
|
-
await emitSandboxEvent(
|
|
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:
|
|
673
|
+
patchCount: totalPatchCount,
|
|
285
674
|
totalDiffLines: 0,
|
|
286
675
|
timestampMs: nowMs(),
|
|
287
676
|
});
|
|
288
677
|
if (!options.autoAcceptDiffs) {
|
|
289
|
-
await emitSandboxEvent(
|
|
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:
|
|
687
|
+
patchCount: totalPatchCount,
|
|
299
688
|
});
|
|
300
689
|
}
|
|
301
|
-
await emitSandboxEvent(
|
|
690
|
+
await emitSandboxEvent(runtimeDb, {
|
|
302
691
|
type: "SandboxDiffAccepted",
|
|
303
692
|
runId: runtime.runId,
|
|
304
693
|
sandboxId: options.sandboxId,
|
|
305
|
-
patchCount:
|
|
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(
|
|
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(
|
|
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
|
}
|