@finalai/final-agent-provider 0.11.0 → 0.13.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/README.md +1 -14
- package/dist/modules/integrations.d.mts +0 -1
- package/dist/modules/integrations.mjs +0 -2
- package/dist/modules/integrations.mjs.map +2 -2
- package/dist/modules/sandboxes/computer.d.mts +2 -4
- package/dist/modules/sandboxes/computer.mjs +110 -480
- package/dist/modules/sandboxes/computer.mjs.map +2 -2
- package/dist/modules/sandboxes/computers-api.d.mts +6 -1
- package/dist/modules/sandboxes/computers-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device-api.d.mts +8 -48
- package/dist/modules/sandboxes/device-api.mjs +2 -92
- package/dist/modules/sandboxes/device-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device.d.mts +1 -1
- package/dist/modules/sandboxes/device.mjs.map +2 -2
- package/dist/modules/sandboxes/environment.d.mts +4 -0
- package/dist/modules/sandboxes/environment.mjs.map +2 -2
- package/dist/modules/sandboxes/local.d.mts +2 -1
- package/dist/modules/sandboxes/local.mjs +64 -8
- package/dist/modules/sandboxes/local.mjs.map +2 -2
- package/dist/modules/sandboxes/session.mjs +0 -1
- package/dist/modules/sandboxes/session.mjs.map +2 -2
- package/package.json +5 -57
- package/WORKSPACE-EGRESS.md +0 -184
- package/dist/modules/authoring-final-channel.d.mts +0 -3
- package/dist/modules/authoring-final-channel.mjs +0 -10
- package/dist/modules/authoring-final-channel.mjs.map +0 -7
- package/dist/modules/channel-factories.d.mts +0 -40
- package/dist/modules/channel-factories.mjs +0 -212
- package/dist/modules/channel-factories.mjs.map +0 -7
- package/dist/modules/channels/content.d.mts +0 -6
- package/dist/modules/channels/content.mjs +0 -31
- package/dist/modules/channels/content.mjs.map +0 -7
- package/dist/modules/channels/delivery.d.mts +0 -54
- package/dist/modules/channels/delivery.mjs +0 -243
- package/dist/modules/channels/delivery.mjs.map +0 -7
- package/dist/modules/channels/direct-messaging.d.mts +0 -5
- package/dist/modules/channels/direct-messaging.mjs +0 -8
- package/dist/modules/channels/direct-messaging.mjs.map +0 -7
- package/dist/modules/channels/fake-platform.d.mts +0 -36
- package/dist/modules/channels/fake-platform.mjs +0 -168
- package/dist/modules/channels/fake-platform.mjs.map +0 -7
- package/dist/modules/channels/group-messaging.d.mts +0 -5
- package/dist/modules/channels/group-messaging.mjs +0 -8
- package/dist/modules/channels/group-messaging.mjs.map +0 -7
- package/dist/modules/channels/inbound.d.mts +0 -52
- package/dist/modules/channels/inbound.mjs +0 -42
- package/dist/modules/channels/inbound.mjs.map +0 -7
- package/dist/modules/channels/index.d.mts +0 -9
- package/dist/modules/channels/index.mjs +0 -10
- package/dist/modules/channels/index.mjs.map +0 -7
- package/dist/modules/channels/messaging-channel.d.mts +0 -7
- package/dist/modules/channels/messaging-channel.mjs +0 -99
- package/dist/modules/channels/messaging-channel.mjs.map +0 -7
- package/dist/modules/channels/projection.d.mts +0 -36
- package/dist/modules/channels/projection.mjs +0 -233
- package/dist/modules/channels/projection.mjs.map +0 -7
- package/dist/modules/channels/prompt.d.mts +0 -72
- package/dist/modules/channels/prompt.mjs +0 -208
- package/dist/modules/channels/prompt.mjs.map +0 -7
- package/dist/modules/channels/routing.d.mts +0 -5
- package/dist/modules/channels/routing.mjs +0 -53
- package/dist/modules/channels/routing.mjs.map +0 -7
- package/dist/modules/sandboxes/access.d.mts +0 -34
- package/dist/modules/sandboxes/access.mjs +0 -15
- package/dist/modules/sandboxes/access.mjs.map +0 -7
- package/dist/modules/sandboxes/egress-bootstrap.d.mts +0 -3
- package/dist/modules/sandboxes/egress-bootstrap.mjs +0 -929
- package/dist/modules/sandboxes/egress-bootstrap.mjs.map +0 -7
- package/dist/modules/sandboxes/egress.d.mts +0 -94
- package/dist/modules/sandboxes/egress.mjs +0 -1561
- package/dist/modules/sandboxes/egress.mjs.map +0 -7
|
@@ -2,20 +2,17 @@ import { Capability, FilesAccess, ScreenAccess, ShellAccess } from "@finalai/age
|
|
|
2
2
|
import { NetworkPolicy } from "@finalai/agent-provider";
|
|
3
3
|
import {
|
|
4
4
|
defineSandboxProvider,
|
|
5
|
-
FULL_SANDBOX_GRANT,
|
|
6
5
|
materializeSandboxSeeds,
|
|
7
6
|
platformProcessEnvironment,
|
|
8
7
|
PortProtocol,
|
|
9
8
|
reportPlatformNetworkPolicy,
|
|
10
9
|
SandboxCapability,
|
|
11
|
-
SandboxExecutionOperation,
|
|
12
10
|
sandboxSeedArtifact,
|
|
13
11
|
sandboxSeedArtifactSchema,
|
|
14
12
|
sandboxSeedFiles,
|
|
15
13
|
sandboxSeedFilesForAttach,
|
|
16
14
|
sandboxSeedIdentity,
|
|
17
15
|
withPlatformEgressHosts,
|
|
18
|
-
withSandboxNetworking,
|
|
19
16
|
writePlatformFiles
|
|
20
17
|
} from "@finalai/agent-provider/sandbox";
|
|
21
18
|
import {
|
|
@@ -79,9 +76,7 @@ const computerStateSchema = z.object({
|
|
|
79
76
|
// The sandbox setup's network policy, enforced from the computer's first boot.
|
|
80
77
|
network: networkPolicySchema,
|
|
81
78
|
// Ports the setup's services listen on.
|
|
82
|
-
ports: z.array(z.number().int()).default([])
|
|
83
|
-
// Workspace Device egress manages this computer's network.
|
|
84
|
-
networkPolicySaved: z.boolean().default(false)
|
|
79
|
+
ports: z.array(z.number().int()).default([])
|
|
85
80
|
});
|
|
86
81
|
const computerSandboxError = (code, message) => Object.assign(new Error(message), { name: "ComputerSandboxError", code });
|
|
87
82
|
const HTTP_TOKEN_LIFETIME_MS = 60 * 6e4;
|
|
@@ -107,10 +102,6 @@ const CAPTURE_SCRUBBED_PATHS = [
|
|
|
107
102
|
const BUILDER_IDLE_TIMEOUT_SECONDS = 2 * 60 * 60;
|
|
108
103
|
const ARTIFACT_KEY_VERSION = 1;
|
|
109
104
|
const PROJECT_ARTIFACT_CAPTURE_PROTOCOL_VERSION = 2;
|
|
110
|
-
const networkPolicyFor = (network) => ({
|
|
111
|
-
mode: network?.egress === ComputerEgressPolicy.Internet ? NetworkPolicy.AllowAll : network?.egress === ComputerEgressPolicy.Allowlist ? NetworkPolicy.Allowlist : NetworkPolicy.DenyAll,
|
|
112
|
-
allow: network?.allow ?? []
|
|
113
|
-
});
|
|
114
105
|
const computerEgressOf = (policy) => {
|
|
115
106
|
if (policy.mode === NetworkPolicy.AllowAll) return { egress: ComputerEgressPolicy.Internet };
|
|
116
107
|
if (policy.mode === NetworkPolicy.DenyAll) return { egress: ComputerEgressPolicy.None };
|
|
@@ -151,16 +142,6 @@ const computeComputerArtifactKey = async (inputs) => {
|
|
|
151
142
|
);
|
|
152
143
|
return `pa-g${hex.slice(0, 16)}-c${PROJECT_ARTIFACT_CAPTURE_PROTOCOL_VERSION}`;
|
|
153
144
|
};
|
|
154
|
-
const offerContextOf = (ctx) => ({
|
|
155
|
-
organizationId: ctx.organizationId,
|
|
156
|
-
projectId: ctx.projectId,
|
|
157
|
-
sandboxId: ctx.sandboxId,
|
|
158
|
-
credentials: ctx.credentials,
|
|
159
|
-
actingUser: ctx.actingUser,
|
|
160
|
-
identity: ctx.identity,
|
|
161
|
-
platformEgressHosts: ctx.platformEgressHosts,
|
|
162
|
-
platformNetworkPolicy: ctx.platformNetworkPolicy
|
|
163
|
-
});
|
|
164
145
|
const createComputerSandboxProvider = (config) => {
|
|
165
146
|
const log = config.log ?? (() => void 0);
|
|
166
147
|
const randomUuid = config.randomUuid ?? (() => crypto.randomUUID());
|
|
@@ -237,102 +218,6 @@ const createComputerSandboxProvider = (config) => {
|
|
|
237
218
|
);
|
|
238
219
|
}
|
|
239
220
|
};
|
|
240
|
-
const createNetworkingController = (options) => {
|
|
241
|
-
try {
|
|
242
|
-
if (config.networking == null) throw new Error("Workspace networking extension is required");
|
|
243
|
-
return config.networking.controller(options);
|
|
244
|
-
} catch (cause) {
|
|
245
|
-
options.onClose?.();
|
|
246
|
-
throw cause;
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
const managedWorkspaces = /* @__PURE__ */ new Set();
|
|
250
|
-
const savedPolicyWorkspaces = /* @__PURE__ */ new Set();
|
|
251
|
-
const workspaceKey = (ctx) => `${ctx.organizationId}:${ctx.sandboxId}`;
|
|
252
|
-
const needsWorkspaceNetworking = async (ctx, query = {}) => {
|
|
253
|
-
const needed = await config.networking?.enabled({
|
|
254
|
-
ctx,
|
|
255
|
-
workspaceId: ctx.sandboxId,
|
|
256
|
-
grant: FULL_SANDBOX_GRANT,
|
|
257
|
-
...query
|
|
258
|
-
}) ?? false;
|
|
259
|
-
if (needed) managedWorkspaces.add(workspaceKey(ctx));
|
|
260
|
-
return needed || managedWorkspaces.has(workspaceKey(ctx));
|
|
261
|
-
};
|
|
262
|
-
const preservePolicy = async (args) => {
|
|
263
|
-
if (config.networking == null) throw new Error("Workspace networking extension is required");
|
|
264
|
-
const retained = await config.networking.prepare({
|
|
265
|
-
ctx: args.ctx,
|
|
266
|
-
workspaceId: args.ctx.sandboxId,
|
|
267
|
-
policy: args.policy
|
|
268
|
-
});
|
|
269
|
-
await args.ctx.platformNetworkPolicy?.(retained ?? args.policy);
|
|
270
|
-
savedPolicyWorkspaces.add(workspaceKey(args.ctx));
|
|
271
|
-
};
|
|
272
|
-
const teardownComputer = async (args) => {
|
|
273
|
-
const failures = [];
|
|
274
|
-
let managed = args.policySaved || managedWorkspaces.has(workspaceKey(args.ctx));
|
|
275
|
-
let queryFailed = false;
|
|
276
|
-
try {
|
|
277
|
-
managed = await needsWorkspaceNetworking(args.ctx, { teardown: true }) || args.policySaved;
|
|
278
|
-
} catch (cause) {
|
|
279
|
-
failures.push(cause);
|
|
280
|
-
queryFailed = true;
|
|
281
|
-
}
|
|
282
|
-
const results = await Promise.allSettled([
|
|
283
|
-
(async () => {
|
|
284
|
-
if (managed && args.fence) {
|
|
285
|
-
const client = clientFor(args.ctx);
|
|
286
|
-
let mayFence = args.policySaved || savedPolicyWorkspaces.has(workspaceKey(args.ctx));
|
|
287
|
-
try {
|
|
288
|
-
if (config.networking != null) {
|
|
289
|
-
await preservePolicy({
|
|
290
|
-
ctx: args.ctx,
|
|
291
|
-
policy: networkPolicyFor(await client.getNetworkPolicy(args.computerId))
|
|
292
|
-
});
|
|
293
|
-
mayFence = true;
|
|
294
|
-
}
|
|
295
|
-
} catch (cause) {
|
|
296
|
-
if (isComputersApiStatus(cause, 404)) mayFence = false;
|
|
297
|
-
else failures.push(cause);
|
|
298
|
-
}
|
|
299
|
-
if (mayFence) {
|
|
300
|
-
try {
|
|
301
|
-
await client.setNetworkPolicy(
|
|
302
|
-
args.computerId,
|
|
303
|
-
{ egress: ComputerEgressPolicy.None },
|
|
304
|
-
{ autoResume: false }
|
|
305
|
-
);
|
|
306
|
-
} catch (cause) {
|
|
307
|
-
let settled = isComputersApiStatus(cause, 404);
|
|
308
|
-
if (!settled && isComputersApiStatus(cause, 409)) {
|
|
309
|
-
try {
|
|
310
|
-
const latest = await client.getComputer(args.computerId);
|
|
311
|
-
settled = latest == null || latest.state === ComputerState.Deleted;
|
|
312
|
-
} catch (lookup) {
|
|
313
|
-
failures.push(lookup);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (!settled) failures.push(cause);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
await args.finish();
|
|
321
|
-
})(),
|
|
322
|
-
(async () => {
|
|
323
|
-
if (managed && config.networking == null)
|
|
324
|
-
throw new Error(
|
|
325
|
-
"Managed Computer teardown requires the workspace egress integration to revoke Device access"
|
|
326
|
-
);
|
|
327
|
-
if (config.networking != null && (managed || queryFailed))
|
|
328
|
-
await config.networking.suspend({ ctx: args.ctx, workspaceId: args.ctx.sandboxId });
|
|
329
|
-
})()
|
|
330
|
-
]);
|
|
331
|
-
for (const result of results) if (result.status === "rejected") failures.push(result.reason);
|
|
332
|
-
if (failures.length === 1) throw failures[0];
|
|
333
|
-
if (failures.length > 1)
|
|
334
|
-
throw new AggregateError(failures, "Computer teardown and egress revocation failed");
|
|
335
|
-
};
|
|
336
221
|
const setupSession = (args) => {
|
|
337
222
|
const transport = createComputersApiTransport({
|
|
338
223
|
client: args.client,
|
|
@@ -356,53 +241,10 @@ const createComputerSandboxProvider = (config) => {
|
|
|
356
241
|
});
|
|
357
242
|
return { sandbox, transport };
|
|
358
243
|
};
|
|
359
|
-
const workspaceNetworking = (args) => {
|
|
360
|
-
if (config.networking == null || !args.enabled) return void 0;
|
|
361
|
-
return createNetworkingController({
|
|
362
|
-
ctx: args.ctx,
|
|
363
|
-
sandbox: args.sandbox,
|
|
364
|
-
workspaceId: args.ctx.sandboxId,
|
|
365
|
-
grant: FULL_SANDBOX_GRANT,
|
|
366
|
-
onClose: args.onClose,
|
|
367
|
-
initialNetworkPolicy: args.initialNetworkPolicy,
|
|
368
|
-
namespaceExists: async () => {
|
|
369
|
-
const current = await args.client.getComputer(args.info.id);
|
|
370
|
-
return current?.state === ComputerState.Running;
|
|
371
|
-
},
|
|
372
|
-
log,
|
|
373
|
-
afterBlock: () => args.client.setNetworkPolicy(
|
|
374
|
-
args.info.id,
|
|
375
|
-
{ egress: ComputerEgressPolicy.None },
|
|
376
|
-
{ autoResume: false }
|
|
377
|
-
),
|
|
378
|
-
afterGuard: (policy) => args.client.setNetworkPolicy(
|
|
379
|
-
args.info.id,
|
|
380
|
-
egressFor(args.ctx, policy ?? { mode: NetworkPolicy.DenyAll }),
|
|
381
|
-
{ autoResume: false }
|
|
382
|
-
)
|
|
383
|
-
});
|
|
384
|
-
};
|
|
385
244
|
const buildHandle = async (args) => {
|
|
386
245
|
const { client, info } = args;
|
|
387
246
|
const transport = createComputersApiTransport({ client, computerId: info.id });
|
|
388
247
|
let httpToken = null;
|
|
389
|
-
const fenceOnStop = async () => {
|
|
390
|
-
if (args.networking != null)
|
|
391
|
-
await client.setNetworkPolicy(
|
|
392
|
-
info.id,
|
|
393
|
-
{ egress: ComputerEgressPolicy.None },
|
|
394
|
-
{ autoResume: false }
|
|
395
|
-
);
|
|
396
|
-
else if (!args.passive)
|
|
397
|
-
await teardownComputer({
|
|
398
|
-
ctx: args.ctx,
|
|
399
|
-
computerId: info.id,
|
|
400
|
-
policySaved: args.networkingManaged,
|
|
401
|
-
fence: true,
|
|
402
|
-
finish: async () => {
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
};
|
|
406
248
|
const session = createTransportSession({
|
|
407
249
|
id: info.id,
|
|
408
250
|
description: `Final computer ${info.name}`,
|
|
@@ -414,16 +256,10 @@ const createComputerSandboxProvider = (config) => {
|
|
|
414
256
|
ports: [...args.ports],
|
|
415
257
|
// The computer's hostname with a preview token, which opens its HTTP ports only.
|
|
416
258
|
getPortEndpoint: async ({ port, protocol }) => {
|
|
417
|
-
const managed = args.networkingManaged || args.passive && await needsWorkspaceNetworking(args.ctx);
|
|
418
|
-
if (managed && args.networking == null)
|
|
419
|
-
throw new Error("Managed workspace previews require an attached sandbox");
|
|
420
|
-
const deadline = (await args.networking?.authorizeExternalAccess())?.expiresAtMs;
|
|
421
|
-
if (deadline != null && deadline <= now())
|
|
422
|
-
throw new Error("Refresh workspace networking before preview access");
|
|
423
259
|
const hostname = info.hostname ?? (await client.getComputer(info.id))?.hostname ?? null;
|
|
424
260
|
if (hostname == null) throw new Error(`computer ${info.name} has no public hostname`);
|
|
425
|
-
if (httpToken == null || httpToken.expiresAtMs - now() < HTTP_TOKEN_RENEW_MS
|
|
426
|
-
const expiresAtMs =
|
|
261
|
+
if (httpToken == null || httpToken.expiresAtMs - now() < HTTP_TOKEN_RENEW_MS) {
|
|
262
|
+
const expiresAtMs = now() + HTTP_TOKEN_LIFETIME_MS;
|
|
427
263
|
const token = await client.createPreviewToken({
|
|
428
264
|
computerId: info.id,
|
|
429
265
|
name: `agent sandbox ${args.ctx.sandboxId}`,
|
|
@@ -439,16 +275,6 @@ const createComputerSandboxProvider = (config) => {
|
|
|
439
275
|
},
|
|
440
276
|
stop: async () => {
|
|
441
277
|
const failures = [];
|
|
442
|
-
try {
|
|
443
|
-
await fenceOnStop();
|
|
444
|
-
} catch (cause) {
|
|
445
|
-
try {
|
|
446
|
-
if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
|
|
447
|
-
failures.push(cause);
|
|
448
|
-
} catch (lookup) {
|
|
449
|
-
failures.push(lookup);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
278
|
try {
|
|
453
279
|
await closeComputerMcp(info.id);
|
|
454
280
|
} catch (cause) {
|
|
@@ -463,35 +289,24 @@ const createComputerSandboxProvider = (config) => {
|
|
|
463
289
|
args.closeSetup?.();
|
|
464
290
|
if (failures.length === 1) throw failures[0];
|
|
465
291
|
if (failures.length > 1)
|
|
466
|
-
throw new AggregateError(failures, "Computer stop or
|
|
292
|
+
throw new AggregateError(failures, "Computer stop or MCP cleanup failed");
|
|
467
293
|
},
|
|
468
294
|
destroy: async () => {
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
485
|
-
};
|
|
486
|
-
if (args.networking == null && !args.passive)
|
|
487
|
-
await teardownComputer({
|
|
488
|
-
ctx: args.ctx,
|
|
489
|
-
computerId: info.id,
|
|
490
|
-
policySaved: args.networkingManaged,
|
|
491
|
-
fence: true,
|
|
492
|
-
finish
|
|
493
|
-
});
|
|
494
|
-
else await finish();
|
|
295
|
+
const failures = [];
|
|
296
|
+
try {
|
|
297
|
+
await closeComputerMcp(info.id);
|
|
298
|
+
} catch (cause) {
|
|
299
|
+
failures.push(cause);
|
|
300
|
+
}
|
|
301
|
+
transport.close();
|
|
302
|
+
try {
|
|
303
|
+
await client.action(info.id, "delete");
|
|
304
|
+
} catch (cause) {
|
|
305
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
306
|
+
}
|
|
307
|
+
if (failures.length === 1) throw failures[0];
|
|
308
|
+
if (failures.length > 1)
|
|
309
|
+
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
495
310
|
}
|
|
496
311
|
}
|
|
497
312
|
});
|
|
@@ -535,8 +350,6 @@ const createComputerSandboxProvider = (config) => {
|
|
|
535
350
|
SandboxCapability.DurableProcesses,
|
|
536
351
|
SandboxCapability.NetworkPolicy
|
|
537
352
|
);
|
|
538
|
-
if (!args.passive && args.networking != null)
|
|
539
|
-
capabilities.push(...config.networking?.capabilities ?? []);
|
|
540
353
|
const full = {
|
|
541
354
|
...session,
|
|
542
355
|
setNetworkPolicy: async (policy) => {
|
|
@@ -547,20 +360,10 @@ const createComputerSandboxProvider = (config) => {
|
|
|
547
360
|
};
|
|
548
361
|
if (computerUse != null) full.computerUse = async () => computerUse.endpoint;
|
|
549
362
|
if (screenAvailable) full.screen = (options) => client.openScreen(info.id, options);
|
|
550
|
-
|
|
363
|
+
return {
|
|
551
364
|
sandbox: args.passive ? readOnlySession(full) : full,
|
|
552
365
|
onSandboxStop: async () => {
|
|
553
366
|
const failures = [];
|
|
554
|
-
try {
|
|
555
|
-
await fenceOnStop();
|
|
556
|
-
} catch (cause) {
|
|
557
|
-
try {
|
|
558
|
-
if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
|
|
559
|
-
failures.push(cause);
|
|
560
|
-
} catch (lookup) {
|
|
561
|
-
failures.push(lookup);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
367
|
for (const finish of [closeMcp, () => transport.close(), () => args.closeSetup?.()]) {
|
|
565
368
|
try {
|
|
566
369
|
await finish();
|
|
@@ -569,37 +372,25 @@ const createComputerSandboxProvider = (config) => {
|
|
|
569
372
|
}
|
|
570
373
|
}
|
|
571
374
|
if (failures.length === 1) throw failures[0];
|
|
572
|
-
if (failures.length > 1)
|
|
573
|
-
throw new AggregateError(failures, "Computer fence and MCP cleanup failed");
|
|
375
|
+
if (failures.length > 1) throw new AggregateError(failures, "Computer MCP cleanup failed");
|
|
574
376
|
},
|
|
575
377
|
onSandboxDelete: async () => {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
593
|
-
};
|
|
594
|
-
if (args.networking == null)
|
|
595
|
-
await teardownComputer({
|
|
596
|
-
ctx: args.ctx,
|
|
597
|
-
computerId: info.id,
|
|
598
|
-
policySaved: args.networkingManaged,
|
|
599
|
-
fence: true,
|
|
600
|
-
finish
|
|
601
|
-
});
|
|
602
|
-
else await finish();
|
|
378
|
+
const failures = [];
|
|
379
|
+
try {
|
|
380
|
+
await closeMcp();
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
failures.push(cause);
|
|
383
|
+
}
|
|
384
|
+
transport.close();
|
|
385
|
+
args.closeSetup?.();
|
|
386
|
+
try {
|
|
387
|
+
await client.action(info.id, "delete");
|
|
388
|
+
} catch (cause) {
|
|
389
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
390
|
+
}
|
|
391
|
+
if (failures.length === 1) throw failures[0];
|
|
392
|
+
if (failures.length > 1)
|
|
393
|
+
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
603
394
|
},
|
|
604
395
|
onRuntimeShutdown: async () => {
|
|
605
396
|
try {
|
|
@@ -610,31 +401,6 @@ const createComputerSandboxProvider = (config) => {
|
|
|
610
401
|
}
|
|
611
402
|
}
|
|
612
403
|
};
|
|
613
|
-
if (args.passive) return handle;
|
|
614
|
-
if (args.networking != null)
|
|
615
|
-
handle = await withSandboxNetworking({
|
|
616
|
-
handle,
|
|
617
|
-
controller: args.networking,
|
|
618
|
-
grant: FULL_SANDBOX_GRANT,
|
|
619
|
-
initialized: true,
|
|
620
|
-
fetch: config.networking?.fetch,
|
|
621
|
-
executionPolicy: args.ctx.executionPolicy,
|
|
622
|
-
beforeUse: args.beforeUse
|
|
623
|
-
});
|
|
624
|
-
else if (config.networking != null)
|
|
625
|
-
handle = await withSandboxNetworking({
|
|
626
|
-
handle,
|
|
627
|
-
grant: FULL_SANDBOX_GRANT,
|
|
628
|
-
fetch: config.networking.fetch,
|
|
629
|
-
executionPolicy: args.ctx.executionPolicy,
|
|
630
|
-
admission: async () => {
|
|
631
|
-
if (await needsWorkspaceNetworking(args.ctx))
|
|
632
|
-
throw new Error(
|
|
633
|
-
"Workspace networking changed; reconnect this sandbox before running tools"
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
return handle;
|
|
638
404
|
};
|
|
639
405
|
const awaitBase = async (client, baseId, key) => {
|
|
640
406
|
const deadline = now() + (config.buildWaitMs ?? 30 * 6e4);
|
|
@@ -840,8 +606,6 @@ ${step.run}`,
|
|
|
840
606
|
[Capability.Shell]: ShellAccess.Execute,
|
|
841
607
|
[Capability.ComputerUse]: ScreenAccess.Control
|
|
842
608
|
}),
|
|
843
|
-
// Screen and computer-use availability is known only after attaching the actual Computer.
|
|
844
|
-
additionalFeatures: async () => config.networking?.capabilities.slice() ?? [],
|
|
845
609
|
processes: async (ctx, rawState, request) => {
|
|
846
610
|
const state = stateOf(rawState);
|
|
847
611
|
if (state.computerId == null) throw new Error("this computer has not started yet");
|
|
@@ -862,52 +626,7 @@ ${step.run}`,
|
|
|
862
626
|
randomUuid,
|
|
863
627
|
expiresAtMs
|
|
864
628
|
});
|
|
865
|
-
|
|
866
|
-
if (config.networking == null) {
|
|
867
|
-
if (state.networkPolicySaved)
|
|
868
|
-
throw new Error("Device networking control is required before opening this editor");
|
|
869
|
-
return open();
|
|
870
|
-
}
|
|
871
|
-
if (ctx.sandboxId == null)
|
|
872
|
-
throw new Error("The host must name the sandbox before opening an egress-aware editor");
|
|
873
|
-
const networkingCtx = { ...ctx, sandboxId: ctx.sandboxId };
|
|
874
|
-
const managed = state.networkPolicySaved || await needsWorkspaceNetworking(networkingCtx);
|
|
875
|
-
if (!managed) return open();
|
|
876
|
-
config.networking.validateWorkspaceId?.(ctx.sandboxId);
|
|
877
|
-
const info = await client.getComputer(computerId);
|
|
878
|
-
if (info == null || info.state !== ComputerState.Running)
|
|
879
|
-
throw new Error("Computer must be running before editor access");
|
|
880
|
-
const initialNetworkPolicy = networkPolicyFor(await client.getNetworkPolicy(info.id));
|
|
881
|
-
const setup = setupSession({ client, info, env: async () => ({}) });
|
|
882
|
-
const manager = createNetworkingController({
|
|
883
|
-
sandbox: setup.sandbox,
|
|
884
|
-
onClose: () => setup.transport.close(),
|
|
885
|
-
workspaceId: ctx.sandboxId,
|
|
886
|
-
ctx,
|
|
887
|
-
grant: FULL_SANDBOX_GRANT,
|
|
888
|
-
initialNetworkPolicy,
|
|
889
|
-
afterBlock: () => client.setNetworkPolicy(
|
|
890
|
-
info.id,
|
|
891
|
-
{ egress: ComputerEgressPolicy.None },
|
|
892
|
-
{ autoResume: false }
|
|
893
|
-
),
|
|
894
|
-
afterGuard: async (policy) => client.setNetworkPolicy(
|
|
895
|
-
info.id,
|
|
896
|
-
policy == null ? { egress: ComputerEgressPolicy.None } : egressFor(ctx, policy),
|
|
897
|
-
{ autoResume: false }
|
|
898
|
-
),
|
|
899
|
-
allowOwnerChange: false
|
|
900
|
-
});
|
|
901
|
-
const { expiresAtMs: deadline } = await manager.authorizeExternalAccess().finally(async () => {
|
|
902
|
-
try {
|
|
903
|
-
await manager.close();
|
|
904
|
-
} finally {
|
|
905
|
-
setup.transport.close();
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
if (deadline != null && deadline <= now())
|
|
909
|
-
throw new Error("Refresh workspace egress before opening the editor");
|
|
910
|
-
return open(deadline ?? void 0);
|
|
629
|
+
return open();
|
|
911
630
|
},
|
|
912
631
|
readSnapshot: async (ctx, rawState, request) => {
|
|
913
632
|
const state = stateOf(rawState);
|
|
@@ -969,8 +688,7 @@ ${step.run}`,
|
|
|
969
688
|
computerName: await computerNameFor(ctx.sandboxId),
|
|
970
689
|
computerId: null,
|
|
971
690
|
network,
|
|
972
|
-
ports
|
|
973
|
-
networkPolicySaved: false
|
|
691
|
+
ports
|
|
974
692
|
},
|
|
975
693
|
region
|
|
976
694
|
};
|
|
@@ -997,35 +715,21 @@ ${step.run}`,
|
|
|
997
715
|
"artifact_unavailable",
|
|
998
716
|
`computer base ${artifact.data.key} was prepared without its build steps; prepare it again`
|
|
999
717
|
);
|
|
1000
|
-
const networkingCtx = offerContextOf(ctx);
|
|
1001
718
|
const client = clientFor(ctx);
|
|
1002
719
|
const env = platformProcessEnvironment(ctx, {});
|
|
1003
720
|
const policy = withPlatformEgressHosts(ctx, state.network);
|
|
1004
721
|
const egress = computerEgressOf(policy);
|
|
1005
|
-
const manageNetworking = state.networkPolicySaved || await needsWorkspaceNetworking(networkingCtx);
|
|
1006
|
-
if (manageNetworking) config.networking?.validateWorkspaceId?.(ctx.sandboxId);
|
|
1007
722
|
const existing = await findComputer(client, state);
|
|
1008
723
|
if (existing?.state === ComputerState.Error)
|
|
1009
724
|
throw computerSandboxError(
|
|
1010
725
|
"computer_gone",
|
|
1011
726
|
`Computer ${existing.id} (${existing.name}) is in the Error state. Recover it through the Computers API before retrying; its disk is preserved.`
|
|
1012
727
|
);
|
|
1013
|
-
const initialNetworkPolicy = manageNetworking && existing != null ? networkPolicyFor(await client.getNetworkPolicy(existing.id)) : networkPolicyFor(egress);
|
|
1014
|
-
if (manageNetworking && existing?.state === ComputerState.Running)
|
|
1015
|
-
await client.disableAutoResume(existing.id);
|
|
1016
|
-
if (manageNetworking && config.networking != null)
|
|
1017
|
-
await preservePolicy({ ctx: networkingCtx, policy: initialNetworkPolicy });
|
|
1018
728
|
let info;
|
|
1019
729
|
let booted;
|
|
1020
730
|
if (existing != null) {
|
|
1021
731
|
info = existing;
|
|
1022
732
|
booted = !isRunningState(existing.state);
|
|
1023
|
-
if (manageNetworking && existing.state !== ComputerState.Running)
|
|
1024
|
-
await client.setNetworkPolicy(
|
|
1025
|
-
existing.id,
|
|
1026
|
-
{ egress: ComputerEgressPolicy.None },
|
|
1027
|
-
{ autoResume: false }
|
|
1028
|
-
);
|
|
1029
733
|
if (existing.state === ComputerState.Paused) await client.action(existing.id, "resume");
|
|
1030
734
|
else if (booted) await client.action(existing.id, "start");
|
|
1031
735
|
} else {
|
|
@@ -1036,11 +740,11 @@ ${step.run}`,
|
|
|
1036
740
|
...source,
|
|
1037
741
|
...machine,
|
|
1038
742
|
env: {},
|
|
1039
|
-
autoResume:
|
|
743
|
+
autoResume: true,
|
|
1040
744
|
idleTimeoutSeconds: idleTimeoutFor(ctx),
|
|
1041
745
|
// The sandbox owns deletion; VM-level ephemeral disks cannot be paused.
|
|
1042
746
|
ephemeral: false,
|
|
1043
|
-
network:
|
|
747
|
+
network: egress,
|
|
1044
748
|
tags: [
|
|
1045
749
|
"final-agent-sandbox",
|
|
1046
750
|
`agent-sandbox:${ctx.sandboxId}`,
|
|
@@ -1050,7 +754,7 @@ ${step.run}`,
|
|
|
1050
754
|
};
|
|
1051
755
|
body.idlePolicy = ComputerIdlePolicy.Pause;
|
|
1052
756
|
if (region != null) body.regionSlug = region;
|
|
1053
|
-
const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.sandboxId}:${state.computerName}:${state.computerId ?? "first"}:${source.baseId ?? source.imageVersionId}
|
|
757
|
+
const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.sandboxId}:${state.computerName}:${state.computerId ?? "first"}:${source.baseId ?? source.imageVersionId}:false`)).slice(0, 32)}`;
|
|
1054
758
|
info = await client.createComputer(body, idempotencyKey);
|
|
1055
759
|
}
|
|
1056
760
|
const fresh = existing == null;
|
|
@@ -1058,81 +762,35 @@ ${step.run}`,
|
|
|
1058
762
|
let cleanup = null;
|
|
1059
763
|
try {
|
|
1060
764
|
info = await awaitComputerRunning(client, info.id, runningWaitMs);
|
|
1061
|
-
if (!
|
|
1062
|
-
|
|
1063
|
-
await reportPlatformNetworkPolicy(ctx, policy);
|
|
1064
|
-
}
|
|
765
|
+
if (!fresh) await client.setNetworkPolicy(info.id, egress);
|
|
766
|
+
await reportPlatformNetworkPolicy(ctx, policy);
|
|
1065
767
|
const setup = setupSession({ client, info, env });
|
|
1066
768
|
cleanup = async () => setup.transport.close();
|
|
1067
769
|
await writePlatformFiles(ctx, setup.sandbox);
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
initialNetworkPolicy
|
|
1076
|
-
});
|
|
1077
|
-
cleanup = async () => {
|
|
1078
|
-
try {
|
|
1079
|
-
await manager?.closeOnFailure();
|
|
1080
|
-
} finally {
|
|
1081
|
-
setup.transport.close();
|
|
1082
|
-
}
|
|
1083
|
-
};
|
|
1084
|
-
await manager?.initialize();
|
|
1085
|
-
const seedMarker = `${WORKSPACE_ROOT}/.final/workspace-prepared/${await sha256Hex(artifact.data.key)}`;
|
|
1086
|
-
let seeding = null;
|
|
1087
|
-
const seedWorkspace = () => seeding ??= (async () => {
|
|
1088
|
-
if (await setup.sandbox.stat({ path: seedMarker }) != null) return;
|
|
1089
|
-
await materializeSandboxSeeds({
|
|
1090
|
-
sandbox: setup.sandbox,
|
|
1091
|
-
root: WORKSPACE_ROOT,
|
|
1092
|
-
files: await sandboxSeedFilesForAttach(ctx, artifact.data)
|
|
1093
|
-
});
|
|
1094
|
-
await setup.sandbox.writeTextFile({ path: seedMarker, content: "complete" });
|
|
1095
|
-
})().catch((cause) => {
|
|
1096
|
-
seeding = null;
|
|
1097
|
-
throw cause;
|
|
1098
|
-
});
|
|
1099
|
-
let ready = true;
|
|
1100
|
-
if (manager != null) {
|
|
1101
|
-
try {
|
|
1102
|
-
await ctx.executionPolicy?.beforeExecution({
|
|
1103
|
-
operation: SandboxExecutionOperation.Run,
|
|
1104
|
-
background: false
|
|
1105
|
-
});
|
|
1106
|
-
await manager.guard();
|
|
1107
|
-
} catch {
|
|
1108
|
-
log("workspace_networking.setup_deferred", {
|
|
1109
|
-
provider: COMPUTER_PROVIDER,
|
|
1110
|
-
computerId: info.id
|
|
770
|
+
if (fresh) {
|
|
771
|
+
const seedMarker = `${WORKSPACE_ROOT}/.final/workspace-prepared/${await sha256Hex(artifact.data.key)}`;
|
|
772
|
+
if (await setup.sandbox.stat({ path: seedMarker }) == null) {
|
|
773
|
+
await materializeSandboxSeeds({
|
|
774
|
+
sandbox: setup.sandbox,
|
|
775
|
+
root: WORKSPACE_ROOT,
|
|
776
|
+
files: await sandboxSeedFilesForAttach(ctx, artifact.data)
|
|
1111
777
|
});
|
|
1112
|
-
|
|
778
|
+
await setup.sandbox.writeTextFile({ path: seedMarker, content: "complete" });
|
|
1113
779
|
}
|
|
1114
780
|
}
|
|
1115
|
-
if (fresh && ready) await seedWorkspace();
|
|
1116
781
|
const handle = await buildHandle({
|
|
1117
|
-
ctx
|
|
782
|
+
ctx,
|
|
1118
783
|
client,
|
|
1119
784
|
info,
|
|
1120
785
|
env,
|
|
1121
786
|
ports: state.ports,
|
|
1122
787
|
passive: false,
|
|
1123
|
-
|
|
1124
|
-
networkingManaged: manageNetworking,
|
|
1125
|
-
closeSetup: () => setup.transport.close(),
|
|
1126
|
-
beforeUse: fresh ? seedWorkspace : void 0
|
|
788
|
+
closeSetup: () => setup.transport.close()
|
|
1127
789
|
});
|
|
1128
790
|
return {
|
|
1129
791
|
handle,
|
|
1130
792
|
region: info.regionSlug ?? region,
|
|
1131
|
-
state: {
|
|
1132
|
-
...state,
|
|
1133
|
-
computerId: info.id,
|
|
1134
|
-
networkPolicySaved: manageNetworking
|
|
1135
|
-
},
|
|
793
|
+
state: { ...state, computerId: info.id },
|
|
1136
794
|
fresh,
|
|
1137
795
|
booted
|
|
1138
796
|
};
|
|
@@ -1143,7 +801,7 @@ ${step.run}`,
|
|
|
1143
801
|
} catch (cleanupError) {
|
|
1144
802
|
cause = new AggregateError(
|
|
1145
803
|
[error, cleanupError],
|
|
1146
|
-
"Computer attach and
|
|
804
|
+
"Computer attach and setup cleanup failed"
|
|
1147
805
|
);
|
|
1148
806
|
}
|
|
1149
807
|
if (fresh) {
|
|
@@ -1169,13 +827,12 @@ ${step.run}`,
|
|
|
1169
827
|
"computer is not running for passive access"
|
|
1170
828
|
);
|
|
1171
829
|
return buildHandle({
|
|
1172
|
-
ctx
|
|
830
|
+
ctx,
|
|
1173
831
|
client,
|
|
1174
832
|
info: await awaitComputerRunning(client, info.id, runningWaitMs),
|
|
1175
833
|
env: platformProcessEnvironment(ctx, {}),
|
|
1176
834
|
ports: state.ports,
|
|
1177
|
-
passive: true
|
|
1178
|
-
networkingManaged: state.networkPolicySaved
|
|
835
|
+
passive: true
|
|
1179
836
|
});
|
|
1180
837
|
},
|
|
1181
838
|
// The computer pauses on its own idle policy; nothing to do.
|
|
@@ -1184,94 +841,67 @@ ${step.run}`,
|
|
|
1184
841
|
const state = stateOf(rawState);
|
|
1185
842
|
if (state.computerId == null) return state;
|
|
1186
843
|
const computerId = state.computerId;
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
844
|
+
const failures = [];
|
|
845
|
+
try {
|
|
846
|
+
await closeComputerMcp(computerId);
|
|
847
|
+
} catch (cause) {
|
|
848
|
+
failures.push(cause);
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
const client = clientFor(ctx);
|
|
852
|
+
let current;
|
|
853
|
+
try {
|
|
854
|
+
current = await client.getComputer(computerId);
|
|
855
|
+
} catch (cause) {
|
|
856
|
+
failures.push(cause);
|
|
857
|
+
}
|
|
858
|
+
if (current !== void 0 && (current == null || hasNothingToPause(current.state))) {
|
|
859
|
+
log("computer_sandbox.park_at_rest", {
|
|
860
|
+
computerId,
|
|
861
|
+
state: current?.state ?? null
|
|
862
|
+
});
|
|
863
|
+
} else {
|
|
1200
864
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
state: current?.state ?? null
|
|
1212
|
-
});
|
|
1213
|
-
} else {
|
|
1214
|
-
try {
|
|
1215
|
-
await client.action(computerId, "pause");
|
|
1216
|
-
} catch (error) {
|
|
1217
|
-
if (!pauseMayHaveSettled(error)) throw error;
|
|
1218
|
-
const settled = await client.getComputer(computerId);
|
|
1219
|
-
if (settled != null && settled.state !== ComputerState.Paused && !hasNothingToPause(settled.state))
|
|
1220
|
-
throw error;
|
|
1221
|
-
log("computer_sandbox.park_at_rest", {
|
|
1222
|
-
computerId,
|
|
1223
|
-
state: settled?.state ?? null
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
} catch (cause) {
|
|
1228
|
-
failures.push(cause);
|
|
865
|
+
await client.action(computerId, "pause");
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (!pauseMayHaveSettled(error)) throw error;
|
|
868
|
+
const settled = await client.getComputer(computerId);
|
|
869
|
+
if (settled != null && settled.state !== ComputerState.Paused && !hasNothingToPause(settled.state))
|
|
870
|
+
throw error;
|
|
871
|
+
log("computer_sandbox.park_at_rest", {
|
|
872
|
+
computerId,
|
|
873
|
+
state: settled?.state ?? null
|
|
874
|
+
});
|
|
1229
875
|
}
|
|
1230
|
-
if (failures.length === 1) throw failures[0];
|
|
1231
|
-
if (failures.length > 1) throw new AggregateError(failures, "Computer pause failed");
|
|
1232
876
|
}
|
|
1233
|
-
})
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
877
|
+
} catch (cause) {
|
|
878
|
+
failures.push(cause);
|
|
879
|
+
}
|
|
880
|
+
if (failures.length === 1) throw failures[0];
|
|
881
|
+
if (failures.length > 1) throw new AggregateError(failures, "Computer pause failed");
|
|
882
|
+
return state;
|
|
1238
883
|
},
|
|
1239
884
|
delete: async (ctx, { state: rawState }) => {
|
|
1240
885
|
const state = stateOf(rawState);
|
|
1241
886
|
const client = clientFor(ctx);
|
|
1242
887
|
const computer = await findComputer(client, state);
|
|
1243
|
-
if (computer == null)
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
return;
|
|
888
|
+
if (computer == null) return;
|
|
889
|
+
const failures = [];
|
|
890
|
+
try {
|
|
891
|
+
await closeComputerMcp(computer.id);
|
|
892
|
+
} catch (cause) {
|
|
893
|
+
failures.push(cause);
|
|
1250
894
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
failures.push(cause);
|
|
1262
|
-
}
|
|
1263
|
-
log("computer_sandbox.delete", { computerId: computer.id });
|
|
1264
|
-
try {
|
|
1265
|
-
await client.action(computer.id, "delete");
|
|
1266
|
-
} catch (error) {
|
|
1267
|
-
if (!await deletedAfterFenceError({ client, computerId: computer.id, cause: error }))
|
|
1268
|
-
failures.push(error);
|
|
1269
|
-
}
|
|
1270
|
-
if (failures.length === 1) throw failures[0];
|
|
1271
|
-
if (failures.length > 1)
|
|
1272
|
-
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
895
|
+
log("computer_sandbox.delete", { computerId: computer.id });
|
|
896
|
+
try {
|
|
897
|
+
await client.action(computer.id, "delete");
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (!await deletedAfterFenceError({ client, computerId: computer.id, cause: error }))
|
|
900
|
+
failures.push(error);
|
|
901
|
+
}
|
|
902
|
+
if (failures.length === 1) throw failures[0];
|
|
903
|
+
if (failures.length > 1)
|
|
904
|
+
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
1275
905
|
}
|
|
1276
906
|
};
|
|
1277
907
|
};
|