@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/package.json +6 -6
- 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 +417 -26
- package/src/index.d.ts +96 -1
- 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,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
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
656
|
+
patchCount: totalPatchCount,
|
|
272
657
|
});
|
|
273
|
-
await emitSandboxEvent(
|
|
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:
|
|
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 &&
|
|
284
|
-
await emitSandboxEvent(
|
|
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:
|
|
673
|
+
patchCount: totalPatchCount,
|
|
289
674
|
totalDiffLines: 0,
|
|
290
675
|
timestampMs: nowMs(),
|
|
291
676
|
});
|
|
292
677
|
if (!options.autoAcceptDiffs) {
|
|
293
|
-
await emitSandboxEvent(
|
|
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:
|
|
687
|
+
patchCount: totalPatchCount,
|
|
303
688
|
});
|
|
304
689
|
}
|
|
305
|
-
await emitSandboxEvent(
|
|
690
|
+
await emitSandboxEvent(runtimeDb, {
|
|
306
691
|
type: "SandboxDiffAccepted",
|
|
307
692
|
runId: runtime.runId,
|
|
308
693
|
sandboxId: options.sandboxId,
|
|
309
|
-
patchCount:
|
|
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(
|
|
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(
|
|
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
|
}
|