@finalai/final-agent-provider 0.3.4 → 0.4.1
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 +11 -0
- package/WORKSPACE-EGRESS.md +172 -0
- package/dist/modules/sandboxes/computer-editor.d.mts +1 -0
- package/dist/modules/sandboxes/computer-editor.mjs +11 -1
- package/dist/modules/sandboxes/computer-editor.mjs.map +2 -2
- package/dist/modules/sandboxes/computer.d.mts +11 -1
- package/dist/modules/sandboxes/computer.mjs +842 -110
- package/dist/modules/sandboxes/computer.mjs.map +3 -3
- package/dist/modules/sandboxes/computers-api.d.mts +8 -1
- package/dist/modules/sandboxes/computers-api.mjs +31 -3
- package/dist/modules/sandboxes/computers-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device-api.d.mts +49 -2
- package/dist/modules/sandboxes/device-api.mjs +106 -12
- package/dist/modules/sandboxes/device-api.mjs.map +2 -2
- package/dist/modules/sandboxes/egress-bootstrap.d.mts +3 -0
- package/dist/modules/sandboxes/egress-bootstrap.mjs +928 -0
- package/dist/modules/sandboxes/egress-bootstrap.mjs.map +7 -0
- package/dist/modules/sandboxes/egress.d.mts +90 -0
- package/dist/modules/sandboxes/egress.mjs +1519 -0
- package/dist/modules/sandboxes/egress.mjs.map +7 -0
- package/dist/modules/sandboxes/session.mjs +14 -9
- package/dist/modules/sandboxes/session.mjs.map +2 -2
- package/dist/modules/sandboxes/testing/fake-backends.mjs +3 -1
- package/dist/modules/sandboxes/testing/fake-backends.mjs.map +2 -2
- package/package.json +17 -6
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
} from "@finalai/agent-provider";
|
|
8
8
|
import {
|
|
9
9
|
defineSandboxProvider,
|
|
10
|
+
withSandboxNetworking,
|
|
11
|
+
mapSandboxHandle,
|
|
12
|
+
SandboxExecutionOperation,
|
|
10
13
|
materializeSandboxSeeds,
|
|
11
14
|
PLATFORM_ENVIRONMENT_NAMES,
|
|
12
15
|
platformProcessEnvironment,
|
|
@@ -18,6 +21,7 @@ import {
|
|
|
18
21
|
sandboxSeedFiles,
|
|
19
22
|
sandboxSeedFilesForStart,
|
|
20
23
|
sandboxSeedIdentity,
|
|
24
|
+
sandboxWorkspaceId,
|
|
21
25
|
SandboxWorkspace,
|
|
22
26
|
withPlatformEgressHosts,
|
|
23
27
|
writePlatformFiles
|
|
@@ -70,6 +74,7 @@ import {
|
|
|
70
74
|
startServices,
|
|
71
75
|
WORKSPACE_ROOT
|
|
72
76
|
} from "./workspace-setup.mjs";
|
|
77
|
+
const computerWorkspaceName = async (options) => options.conversationWorkspace && options.conversationId != null ? `ac-${(await sha256Hex(`${options.conversationId}:${options.target}`)).slice(0, 24)}` : `ab-${compactId(options.bindingId)}`;
|
|
73
78
|
import {
|
|
74
79
|
COMPUTER_SANDBOX_SPECIFIER as COMPUTER_SANDBOX_SPECIFIER2,
|
|
75
80
|
COMPUTERS_API_KEY_SECRET,
|
|
@@ -97,7 +102,10 @@ const computerStateSchema = z.object({
|
|
|
97
102
|
workspace: z.nativeEnum(ComputerWorkspacePolicy),
|
|
98
103
|
access: z.nativeEnum(SandboxAccess),
|
|
99
104
|
guideVersionId: z.string().nullable(),
|
|
100
|
-
setupHash: z.string().nullable()
|
|
105
|
+
setupHash: z.string().nullable(),
|
|
106
|
+
setupPending: z.boolean().default(false),
|
|
107
|
+
workspaceId: z.string().optional(),
|
|
108
|
+
networkPolicySaved: z.boolean().optional()
|
|
101
109
|
});
|
|
102
110
|
const computerSandboxError = (code, message) => Object.assign(new Error(message), { name: "ComputerSandboxError", code });
|
|
103
111
|
const RESERVED_ENV = /* @__PURE__ */ new Set([
|
|
@@ -136,6 +144,10 @@ const CAPTURE_SCRUBBED_PATHS = [
|
|
|
136
144
|
"/home/user/.ssh"
|
|
137
145
|
];
|
|
138
146
|
const BUILDER_IDLE_TIMEOUT_SECONDS = 2 * 60 * 60;
|
|
147
|
+
const networkPolicyFor = (network) => ({
|
|
148
|
+
mode: network?.egress === ComputerEgressPolicy.Internet ? NetworkPolicy.AllowAll : network?.egress === ComputerEgressPolicy.Allowlist ? NetworkPolicy.Allowlist : NetworkPolicy.DenyAll,
|
|
149
|
+
allow: network?.allow ?? []
|
|
150
|
+
});
|
|
139
151
|
const computerEgressOf = (policy) => {
|
|
140
152
|
if (policy.mode === NetworkPolicy.AllowAll) return { egress: ComputerEgressPolicy.Internet };
|
|
141
153
|
if (policy.mode === NetworkPolicy.DenyAll) return { egress: ComputerEgressPolicy.None };
|
|
@@ -235,6 +247,19 @@ const createComputerSandboxProvider = (config) => {
|
|
|
235
247
|
);
|
|
236
248
|
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
237
249
|
};
|
|
250
|
+
const deletedAfterFenceError = async (options) => {
|
|
251
|
+
if (isComputersApiStatus(options.cause, 404)) return true;
|
|
252
|
+
if (!isComputersApiStatus(options.cause, 409)) return false;
|
|
253
|
+
try {
|
|
254
|
+
const current = await options.client.getComputer(options.computerId);
|
|
255
|
+
return current == null || current.state === ComputerState.Deleted;
|
|
256
|
+
} catch (lookup) {
|
|
257
|
+
throw new AggregateError(
|
|
258
|
+
[options.cause, lookup],
|
|
259
|
+
"Computer fence and deletion lookup failed"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
238
263
|
const buildHandle = async (args) => {
|
|
239
264
|
const { client, info, spec } = args;
|
|
240
265
|
const commands = await guideCommands(spec.template);
|
|
@@ -263,10 +288,23 @@ ${commandHelp}`}`,
|
|
|
263
288
|
ports,
|
|
264
289
|
// The computer's hostname with a preview token, which opens its HTTP ports only.
|
|
265
290
|
getPortEndpoint: async ({ port, protocol }) => {
|
|
291
|
+
const managed = args.networkingManaged === true || args.access === SandboxAccess.Read && await needsWorkspaceNetworking(
|
|
292
|
+
args.ctx,
|
|
293
|
+
sandboxWorkspaceId({
|
|
294
|
+
session: args.ctx.session,
|
|
295
|
+
workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
|
|
296
|
+
}),
|
|
297
|
+
{ access: args.access }
|
|
298
|
+
);
|
|
299
|
+
if (managed && args.networking == null)
|
|
300
|
+
throw new Error("Managed workspace previews require an active write binding");
|
|
301
|
+
const deadline = (await args.networking?.authorizeExternalAccess())?.expiresAtMs;
|
|
302
|
+
if (deadline != null && deadline <= now())
|
|
303
|
+
throw new Error("Refresh workspace networking before preview access");
|
|
266
304
|
const hostname = info.hostname ?? (await client.getComputer(info.id))?.hostname ?? null;
|
|
267
305
|
if (hostname == null) throw new Error(`computer ${info.name} has no public hostname`);
|
|
268
|
-
if (httpToken == null || httpToken.expiresAtMs - now() < HTTP_TOKEN_RENEW_MS) {
|
|
269
|
-
const expiresAtMs = now() + HTTP_TOKEN_LIFETIME_MS;
|
|
306
|
+
if (httpToken == null || httpToken.expiresAtMs - now() < HTTP_TOKEN_RENEW_MS || deadline != null && httpToken.expiresAtMs > deadline) {
|
|
307
|
+
const expiresAtMs = Math.min(now() + HTTP_TOKEN_LIFETIME_MS, deadline ?? Infinity);
|
|
270
308
|
const token = await client.createPreviewToken({
|
|
271
309
|
computerId: info.id,
|
|
272
310
|
name: `agent sandbox ${args.ctx.session.bindingId}`,
|
|
@@ -281,17 +319,80 @@ ${commandHelp}`}`,
|
|
|
281
319
|
};
|
|
282
320
|
},
|
|
283
321
|
stop: async () => {
|
|
284
|
-
|
|
285
|
-
await client.action(info.id, "stop");
|
|
286
|
-
},
|
|
287
|
-
destroy: async () => {
|
|
288
|
-
await closeComputerMcp(info.id);
|
|
289
|
-
transport.close();
|
|
322
|
+
const failures = [];
|
|
290
323
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
324
|
+
if (args.networking != null)
|
|
325
|
+
await client.setNetworkPolicy(
|
|
326
|
+
info.id,
|
|
327
|
+
{ egress: ComputerEgressPolicy.None },
|
|
328
|
+
{ autoResume: false }
|
|
329
|
+
);
|
|
330
|
+
else if (args.access !== SandboxAccess.Read)
|
|
331
|
+
await teardownComputer({
|
|
332
|
+
ctx: args.ctx,
|
|
333
|
+
computerId: info.id,
|
|
334
|
+
computerName: info.name,
|
|
335
|
+
workspace: spec.parameters.workspace,
|
|
336
|
+
policySaved: args.networkingManaged === true,
|
|
337
|
+
workspaceId: args.ctx.session.workspaceId,
|
|
338
|
+
fence: true,
|
|
339
|
+
finish: async () => {
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} catch (cause) {
|
|
343
|
+
try {
|
|
344
|
+
if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
|
|
345
|
+
failures.push(cause);
|
|
346
|
+
} catch (lookup) {
|
|
347
|
+
failures.push(lookup);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await closeComputerMcp(info.id);
|
|
352
|
+
} catch (cause) {
|
|
353
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await client.action(info.id, "stop");
|
|
357
|
+
} catch (cause) {
|
|
358
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
294
359
|
}
|
|
360
|
+
transport.close();
|
|
361
|
+
args.closeSetup?.();
|
|
362
|
+
if (failures.length === 1) throw failures[0];
|
|
363
|
+
if (failures.length > 1)
|
|
364
|
+
throw new AggregateError(failures, "Computer stop or networking fence failed");
|
|
365
|
+
},
|
|
366
|
+
destroy: async () => {
|
|
367
|
+
const finish = async () => {
|
|
368
|
+
const failures = [];
|
|
369
|
+
try {
|
|
370
|
+
await closeComputerMcp(info.id);
|
|
371
|
+
} catch (cause) {
|
|
372
|
+
failures.push(cause);
|
|
373
|
+
}
|
|
374
|
+
transport.close();
|
|
375
|
+
try {
|
|
376
|
+
await client.action(info.id, "delete");
|
|
377
|
+
} catch (cause) {
|
|
378
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
379
|
+
}
|
|
380
|
+
if (failures.length === 1) throw failures[0];
|
|
381
|
+
if (failures.length > 1)
|
|
382
|
+
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
383
|
+
};
|
|
384
|
+
if (args.networking == null && args.access !== SandboxAccess.Read)
|
|
385
|
+
await teardownComputer({
|
|
386
|
+
ctx: args.ctx,
|
|
387
|
+
computerId: info.id,
|
|
388
|
+
computerName: info.name,
|
|
389
|
+
workspace: spec.parameters.workspace,
|
|
390
|
+
policySaved: args.networkingManaged === true,
|
|
391
|
+
workspaceId: args.ctx.session.workspaceId,
|
|
392
|
+
fence: true,
|
|
393
|
+
finish
|
|
394
|
+
});
|
|
395
|
+
else await finish();
|
|
295
396
|
}
|
|
296
397
|
}
|
|
297
398
|
});
|
|
@@ -331,6 +432,8 @@ ${commandHelp}`}`,
|
|
|
331
432
|
if (computerUse != null) capabilities.push(SandboxCapability.ComputerUse);
|
|
332
433
|
if (args.access !== SandboxAccess.Read)
|
|
333
434
|
capabilities.push(SandboxCapability.Process, SandboxCapability.NetworkPolicy);
|
|
435
|
+
if (args.access === SandboxAccess.Write && args.networking != null)
|
|
436
|
+
capabilities.push(...config.networking?.capabilities ?? []);
|
|
334
437
|
const description = {
|
|
335
438
|
provider: "computer",
|
|
336
439
|
computerId: info.id,
|
|
@@ -364,38 +467,126 @@ ${commandHelp}`}`,
|
|
|
364
467
|
if (screenAvailable) {
|
|
365
468
|
full.screen = (options) => client.openScreen(info.id, options);
|
|
366
469
|
}
|
|
367
|
-
const sandbox =
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
470
|
+
const sandbox = full;
|
|
471
|
+
let handle = {
|
|
472
|
+
sandbox,
|
|
473
|
+
onSandboxStop: async () => {
|
|
474
|
+
const failures = [];
|
|
475
|
+
try {
|
|
476
|
+
if (args.networking != null)
|
|
477
|
+
await client.setNetworkPolicy(
|
|
478
|
+
info.id,
|
|
479
|
+
{ egress: ComputerEgressPolicy.None },
|
|
480
|
+
{ autoResume: false }
|
|
481
|
+
);
|
|
482
|
+
else
|
|
483
|
+
await teardownComputer({
|
|
484
|
+
ctx: args.ctx,
|
|
485
|
+
computerId: info.id,
|
|
486
|
+
computerName: info.name,
|
|
487
|
+
workspace: spec.parameters.workspace,
|
|
488
|
+
policySaved: args.networkingManaged === true,
|
|
489
|
+
workspaceId: args.ctx.session.workspaceId,
|
|
490
|
+
fence: true,
|
|
491
|
+
finish: async () => {
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
} catch (cause) {
|
|
372
495
|
try {
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
|
|
496
|
+
if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
|
|
497
|
+
failures.push(cause);
|
|
498
|
+
} catch (lookup) {
|
|
499
|
+
failures.push(lookup);
|
|
376
500
|
}
|
|
377
|
-
}
|
|
378
|
-
|
|
501
|
+
}
|
|
502
|
+
for (const finish of [closeMcp, () => transport.close(), () => args.closeSetup?.()]) {
|
|
379
503
|
try {
|
|
380
|
-
await
|
|
381
|
-
}
|
|
382
|
-
|
|
504
|
+
await finish();
|
|
505
|
+
} catch (cause) {
|
|
506
|
+
failures.push(cause);
|
|
383
507
|
}
|
|
508
|
+
}
|
|
509
|
+
if (failures.length === 1) throw failures[0];
|
|
510
|
+
if (failures.length > 1)
|
|
511
|
+
throw new AggregateError(failures, "Computer fence and MCP cleanup failed");
|
|
512
|
+
},
|
|
513
|
+
onSandboxDelete: async () => {
|
|
514
|
+
const finish = async () => {
|
|
515
|
+
const failures = [];
|
|
384
516
|
try {
|
|
385
|
-
await
|
|
386
|
-
} catch (
|
|
387
|
-
|
|
517
|
+
await closeMcp();
|
|
518
|
+
} catch (cause) {
|
|
519
|
+
failures.push(cause);
|
|
388
520
|
}
|
|
389
|
-
|
|
390
|
-
|
|
521
|
+
transport.close();
|
|
522
|
+
args.closeSetup?.();
|
|
391
523
|
try {
|
|
392
|
-
await
|
|
393
|
-
}
|
|
394
|
-
|
|
524
|
+
await client.action(info.id, "delete");
|
|
525
|
+
} catch (cause) {
|
|
526
|
+
if (!isComputersApiStatus(cause, 404)) failures.push(cause);
|
|
395
527
|
}
|
|
528
|
+
if (failures.length === 1) throw failures[0];
|
|
529
|
+
if (failures.length > 1)
|
|
530
|
+
throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
|
|
531
|
+
};
|
|
532
|
+
if (args.networking == null)
|
|
533
|
+
await teardownComputer({
|
|
534
|
+
ctx: args.ctx,
|
|
535
|
+
computerId: info.id,
|
|
536
|
+
computerName: info.name,
|
|
537
|
+
workspace: spec.parameters.workspace,
|
|
538
|
+
policySaved: args.networkingManaged === true,
|
|
539
|
+
workspaceId: args.ctx.session.workspaceId,
|
|
540
|
+
fence: true,
|
|
541
|
+
finish
|
|
542
|
+
});
|
|
543
|
+
else await finish();
|
|
544
|
+
},
|
|
545
|
+
onRuntimeShutdown: async () => {
|
|
546
|
+
try {
|
|
547
|
+
await closeMcp();
|
|
548
|
+
} finally {
|
|
549
|
+
transport.close();
|
|
550
|
+
args.closeSetup?.();
|
|
396
551
|
}
|
|
397
552
|
}
|
|
398
553
|
};
|
|
554
|
+
if (args.networking != null)
|
|
555
|
+
handle = await withSandboxNetworking({
|
|
556
|
+
handle,
|
|
557
|
+
controller: args.networking,
|
|
558
|
+
access: args.access,
|
|
559
|
+
initialized: true,
|
|
560
|
+
fetch: config.networking?.fetch,
|
|
561
|
+
executionPolicy: args.ctx.executionPolicy,
|
|
562
|
+
beforeUse: args.beforeUse
|
|
563
|
+
});
|
|
564
|
+
else if (config.networking != null)
|
|
565
|
+
handle = await withSandboxNetworking({
|
|
566
|
+
handle,
|
|
567
|
+
access: args.access,
|
|
568
|
+
fetch: config.networking?.fetch,
|
|
569
|
+
executionPolicy: args.ctx.executionPolicy,
|
|
570
|
+
admission: async () => {
|
|
571
|
+
if (await needsWorkspaceNetworking(
|
|
572
|
+
args.ctx,
|
|
573
|
+
sandboxWorkspaceId({
|
|
574
|
+
session: args.ctx.session,
|
|
575
|
+
workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
|
|
576
|
+
}),
|
|
577
|
+
{ access: args.access }
|
|
578
|
+
))
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Workspace networking changed; reconnect this sandbox before running tools"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
handle: mapSandboxHandle({
|
|
586
|
+
handle,
|
|
587
|
+
mapSandbox: (sandbox2) => sessionForAccess(sandbox2, args.access)
|
|
588
|
+
})
|
|
589
|
+
};
|
|
399
590
|
};
|
|
400
591
|
const setupSession = (args) => {
|
|
401
592
|
const transport = createComputersApiTransport({
|
|
@@ -420,6 +611,170 @@ ${commandHelp}`}`,
|
|
|
420
611
|
});
|
|
421
612
|
return { sandbox, transport };
|
|
422
613
|
};
|
|
614
|
+
const createNetworkingController = (options) => {
|
|
615
|
+
try {
|
|
616
|
+
if (config.networking == null) throw new Error("Workspace networking extension is required");
|
|
617
|
+
return config.networking.controller(options);
|
|
618
|
+
} catch (cause) {
|
|
619
|
+
options.onClose?.();
|
|
620
|
+
throw cause;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const workspaceNetworking = (args) => {
|
|
624
|
+
if (config.networking == null || !args.enabled) return void 0;
|
|
625
|
+
return createNetworkingController({
|
|
626
|
+
ctx: {
|
|
627
|
+
platformEgressHosts: args.ctx.platformEgressHosts,
|
|
628
|
+
platformNetworkPolicy: args.ctx.platformNetworkPolicy,
|
|
629
|
+
organizationId: args.ctx.session.organizationId,
|
|
630
|
+
actingUser: args.ctx.actingUser,
|
|
631
|
+
bindings: args.ctx.bindings
|
|
632
|
+
},
|
|
633
|
+
sandbox: args.sandbox,
|
|
634
|
+
workspaceId: sandboxWorkspaceId({
|
|
635
|
+
session: args.ctx.session,
|
|
636
|
+
workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
|
|
637
|
+
}),
|
|
638
|
+
access: args.access,
|
|
639
|
+
onClose: args.onClose,
|
|
640
|
+
initialNetworkPolicy: args.initialNetworkPolicy,
|
|
641
|
+
namespaceExists: async () => {
|
|
642
|
+
const current = await args.client.getComputer(args.info.id);
|
|
643
|
+
return current?.state === ComputerState.Running;
|
|
644
|
+
},
|
|
645
|
+
log,
|
|
646
|
+
afterBlock: () => args.client.setNetworkPolicy(
|
|
647
|
+
args.info.id,
|
|
648
|
+
{ egress: ComputerEgressPolicy.None },
|
|
649
|
+
{ autoResume: false }
|
|
650
|
+
),
|
|
651
|
+
afterGuard: (policy) => args.client.setNetworkPolicy(
|
|
652
|
+
args.info.id,
|
|
653
|
+
egressFor(args.ctx, policy ?? { mode: NetworkPolicy.DenyAll }),
|
|
654
|
+
{ autoResume: false }
|
|
655
|
+
)
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
const managedWorkspaces = /* @__PURE__ */ new Set();
|
|
659
|
+
const savedPolicyWorkspaces = /* @__PURE__ */ new Set();
|
|
660
|
+
const preservePolicy = async (args) => {
|
|
661
|
+
if (config.networking == null) throw new Error("Workspace networking extension is required");
|
|
662
|
+
const retained = await config.networking.prepare({
|
|
663
|
+
ctx: {
|
|
664
|
+
platformEgressHosts: args.ctx.platformEgressHosts,
|
|
665
|
+
platformNetworkPolicy: args.ctx.platformNetworkPolicy,
|
|
666
|
+
organizationId: args.ctx.session.organizationId,
|
|
667
|
+
actingUser: args.ctx.actingUser,
|
|
668
|
+
bindings: args.ctx.bindings
|
|
669
|
+
},
|
|
670
|
+
workspaceId: args.workspaceId,
|
|
671
|
+
policy: args.policy
|
|
672
|
+
});
|
|
673
|
+
await reportPlatformNetworkPolicy(args.ctx, retained ?? args.policy);
|
|
674
|
+
savedPolicyWorkspaces.add(args.ctx.session.organizationId + ":" + args.workspaceId);
|
|
675
|
+
};
|
|
676
|
+
const needsWorkspaceNetworking = async (ctx, workspaceId, query = {}) => {
|
|
677
|
+
const needed = await config.networking?.enabled({
|
|
678
|
+
ctx: {
|
|
679
|
+
platformEgressHosts: ctx.platformEgressHosts,
|
|
680
|
+
organizationId: ctx.session.organizationId,
|
|
681
|
+
actingUser: ctx.actingUser,
|
|
682
|
+
bindings: ctx.bindings
|
|
683
|
+
},
|
|
684
|
+
workspaceId,
|
|
685
|
+
access: SandboxAccess.Write,
|
|
686
|
+
...query
|
|
687
|
+
}) ?? false;
|
|
688
|
+
if (needed) managedWorkspaces.add(ctx.session.organizationId + ":" + workspaceId);
|
|
689
|
+
return needed || managedWorkspaces.has(ctx.session.organizationId + ":" + workspaceId);
|
|
690
|
+
};
|
|
691
|
+
const teardownComputer = async (args) => {
|
|
692
|
+
let workspaceId;
|
|
693
|
+
try {
|
|
694
|
+
workspaceId = args.workspaceId ?? sandboxWorkspaceId({
|
|
695
|
+
session: args.ctx.session,
|
|
696
|
+
workspace: sandboxWorkspaceOf[args.workspace]
|
|
697
|
+
});
|
|
698
|
+
} catch (cause) {
|
|
699
|
+
try {
|
|
700
|
+
await args.finish();
|
|
701
|
+
} catch (cleanup) {
|
|
702
|
+
throw new AggregateError([cause, cleanup], "Computer identity and teardown failed");
|
|
703
|
+
}
|
|
704
|
+
throw cause;
|
|
705
|
+
}
|
|
706
|
+
const failures = [];
|
|
707
|
+
let managed = args.policySaved || managedWorkspaces.has(args.ctx.session.organizationId + ":" + workspaceId);
|
|
708
|
+
let queryFailed = false;
|
|
709
|
+
try {
|
|
710
|
+
managed = await needsWorkspaceNetworking(args.ctx, workspaceId, { teardown: true }) || args.policySaved;
|
|
711
|
+
} catch (cause) {
|
|
712
|
+
failures.push(cause);
|
|
713
|
+
queryFailed = true;
|
|
714
|
+
}
|
|
715
|
+
const results = await Promise.allSettled([
|
|
716
|
+
(async () => {
|
|
717
|
+
if (managed && args.fence) {
|
|
718
|
+
const client = clientFor(args.ctx.session.organizationId);
|
|
719
|
+
let mayFence = args.policySaved || savedPolicyWorkspaces.has(args.ctx.session.organizationId + ":" + workspaceId);
|
|
720
|
+
try {
|
|
721
|
+
if (config.networking != null) {
|
|
722
|
+
await preservePolicy({
|
|
723
|
+
ctx: args.ctx,
|
|
724
|
+
workspaceId,
|
|
725
|
+
policy: networkPolicyFor(await client.getNetworkPolicy(args.computerId))
|
|
726
|
+
});
|
|
727
|
+
mayFence = true;
|
|
728
|
+
}
|
|
729
|
+
} catch (cause) {
|
|
730
|
+
if (isComputersApiStatus(cause, 404)) mayFence = false;
|
|
731
|
+
else failures.push(cause);
|
|
732
|
+
}
|
|
733
|
+
if (mayFence) {
|
|
734
|
+
try {
|
|
735
|
+
await client.setNetworkPolicy(
|
|
736
|
+
args.computerId,
|
|
737
|
+
{ egress: ComputerEgressPolicy.None },
|
|
738
|
+
{ autoResume: false }
|
|
739
|
+
);
|
|
740
|
+
} catch (cause) {
|
|
741
|
+
let settled = isComputersApiStatus(cause, 404);
|
|
742
|
+
if (!settled && isComputersApiStatus(cause, 409)) {
|
|
743
|
+
try {
|
|
744
|
+
const latest = await client.getComputer(args.computerId);
|
|
745
|
+
settled = latest == null || latest.state === ComputerState.Deleted;
|
|
746
|
+
} catch (lookup) {
|
|
747
|
+
failures.push(lookup);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (!settled) failures.push(cause);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
await args.finish();
|
|
755
|
+
})(),
|
|
756
|
+
(async () => {
|
|
757
|
+
if (managed && config.networking == null)
|
|
758
|
+
throw new Error(
|
|
759
|
+
"Managed Computer teardown requires the workspace egress integration to revoke Device access"
|
|
760
|
+
);
|
|
761
|
+
if (config.networking != null && (managed || queryFailed))
|
|
762
|
+
await config.networking.suspend({
|
|
763
|
+
ctx: {
|
|
764
|
+
organizationId: args.ctx.session.organizationId,
|
|
765
|
+
actingUser: args.ctx.actingUser,
|
|
766
|
+
bindings: args.ctx.bindings
|
|
767
|
+
},
|
|
768
|
+
workspaceId
|
|
769
|
+
});
|
|
770
|
+
})()
|
|
771
|
+
]);
|
|
772
|
+
for (const result of results) if (result.status === "rejected") failures.push(result.reason);
|
|
773
|
+
if (failures.length === 1) throw failures[0];
|
|
774
|
+
if (failures.length > 1)
|
|
775
|
+
throw new AggregateError(failures, "Computer teardown and egress revocation failed");
|
|
776
|
+
return workspaceId;
|
|
777
|
+
};
|
|
423
778
|
const awaitBase = async (client, baseId, key) => {
|
|
424
779
|
const deadline = now() + (config.buildWaitMs ?? 30 * 6e4);
|
|
425
780
|
for (; ; ) {
|
|
@@ -625,18 +980,72 @@ ${step.run}`,
|
|
|
625
980
|
);
|
|
626
981
|
return request.operation === ProcessInspectionOperation.Available && "available" in result ? { ...result, canStop: state.access !== SandboxAccess.Read } : result;
|
|
627
982
|
},
|
|
628
|
-
editor: async (
|
|
983
|
+
editor: async (ctx, rawState, request) => {
|
|
629
984
|
const state = computerStateSchema.parse(rawState);
|
|
630
985
|
if (state.access !== SandboxAccess.Write) {
|
|
631
986
|
if (request.operation === "available") return { available: false };
|
|
632
987
|
throw new Error("editor requires write access");
|
|
633
988
|
}
|
|
634
|
-
|
|
635
|
-
|
|
989
|
+
const client = clientFor(ctx.organizationId);
|
|
990
|
+
const open = (expiresAtMs) => computerEditor({
|
|
991
|
+
client,
|
|
636
992
|
computerId: state.computerId,
|
|
637
993
|
request,
|
|
638
|
-
randomUuid
|
|
994
|
+
randomUuid,
|
|
995
|
+
expiresAtMs
|
|
639
996
|
});
|
|
997
|
+
if (request.operation === "available" || request.operation === "close") return open();
|
|
998
|
+
const workspaceId = ctx.workspaceId ?? state.workspaceId;
|
|
999
|
+
if (state.networkPolicySaved === true && ctx.workspaceId != null && state.workspaceId != null && ctx.workspaceId !== state.workspaceId)
|
|
1000
|
+
throw new Error("Editor workspace identity does not match its retained provider state");
|
|
1001
|
+
if (config.networking == null) {
|
|
1002
|
+
if (state.networkPolicySaved === true)
|
|
1003
|
+
throw new Error("Device networking control is required before opening this editor");
|
|
1004
|
+
return open();
|
|
1005
|
+
}
|
|
1006
|
+
if (workspaceId == null)
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
"The host must supply logical workspace identity before opening an egress-aware editor"
|
|
1009
|
+
);
|
|
1010
|
+
const managedKey = ctx.organizationId + ":" + workspaceId;
|
|
1011
|
+
const managed = state.networkPolicySaved === true || managedWorkspaces.has(managedKey) || await config.networking.enabled({ ctx, workspaceId, access: SandboxAccess.Write });
|
|
1012
|
+
if (!managed) return open();
|
|
1013
|
+
managedWorkspaces.add(managedKey);
|
|
1014
|
+
config.networking.validateWorkspaceId?.(workspaceId);
|
|
1015
|
+
const info = await client.getComputer(state.computerId);
|
|
1016
|
+
if (info == null || info.state !== ComputerState.Running)
|
|
1017
|
+
throw new Error("Computer must be running before editor access");
|
|
1018
|
+
const initialNetworkPolicy = networkPolicyFor(await client.getNetworkPolicy(info.id));
|
|
1019
|
+
const setup = setupSession({ client, info, env: async () => ({}) });
|
|
1020
|
+
const manager = createNetworkingController({
|
|
1021
|
+
sandbox: setup.sandbox,
|
|
1022
|
+
onClose: () => setup.transport.close(),
|
|
1023
|
+
workspaceId,
|
|
1024
|
+
ctx,
|
|
1025
|
+
access: SandboxAccess.Write,
|
|
1026
|
+
initialNetworkPolicy,
|
|
1027
|
+
afterBlock: () => client.setNetworkPolicy(
|
|
1028
|
+
info.id,
|
|
1029
|
+
{ egress: ComputerEgressPolicy.None },
|
|
1030
|
+
{ autoResume: false }
|
|
1031
|
+
),
|
|
1032
|
+
afterGuard: async (policy) => client.setNetworkPolicy(
|
|
1033
|
+
info.id,
|
|
1034
|
+
policy == null ? { egress: ComputerEgressPolicy.None } : egressFor(ctx, policy),
|
|
1035
|
+
{ autoResume: false }
|
|
1036
|
+
),
|
|
1037
|
+
allowOwnerChange: false
|
|
1038
|
+
});
|
|
1039
|
+
const { expiresAtMs: deadline } = await manager.authorizeExternalAccess().finally(async () => {
|
|
1040
|
+
try {
|
|
1041
|
+
await manager.close();
|
|
1042
|
+
} finally {
|
|
1043
|
+
setup.transport.close();
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
if (deadline != null && deadline <= now())
|
|
1047
|
+
throw new Error("Refresh workspace egress before opening the editor");
|
|
1048
|
+
return open(deadline ?? void 0);
|
|
640
1049
|
},
|
|
641
1050
|
readSnapshot: async (ctx, rawState, request) => {
|
|
642
1051
|
const state = computerStateSchema.parse(rawState);
|
|
@@ -648,6 +1057,8 @@ ${step.run}`,
|
|
|
648
1057
|
},
|
|
649
1058
|
// The merged parameters' workspace: the binding's, else the saved version's default.
|
|
650
1059
|
workspace: async (ctx) => sandboxWorkspaceOf[(await resolve(ctx.guides)).parameters.workspace],
|
|
1060
|
+
// Screen and computer-use availability is known only after attaching the actual Computer.
|
|
1061
|
+
additionalCapabilities: async () => config.networking?.capabilities.slice() ?? [],
|
|
651
1062
|
artifactKey: async (ctx) => keyFor(await resolve(ctx.guides), seedsFor(ctx)),
|
|
652
1063
|
// Registers an image reference so its version builds ahead of the first start;
|
|
653
1064
|
// a lineage needs nothing until then.
|
|
@@ -737,18 +1148,50 @@ ${step.run}`,
|
|
|
737
1148
|
const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
|
|
738
1149
|
const client = clientFor(ctx.session.organizationId);
|
|
739
1150
|
const workspace = spec.parameters.workspace;
|
|
740
|
-
const name =
|
|
1151
|
+
const name = await computerWorkspaceName({
|
|
1152
|
+
...ctx.session,
|
|
1153
|
+
conversationWorkspace: workspace === ComputerWorkspacePolicy.Conversation
|
|
1154
|
+
});
|
|
1155
|
+
const workspaceId = sandboxWorkspaceId({
|
|
1156
|
+
session: ctx.session,
|
|
1157
|
+
workspace: sandboxWorkspaceOf[workspace]
|
|
1158
|
+
});
|
|
1159
|
+
const manageNetworking = await needsWorkspaceNetworking(ctx, workspaceId) || access !== SandboxAccess.Read && await needsWorkspaceNetworking(ctx, workspaceId, { access });
|
|
1160
|
+
if (manageNetworking)
|
|
1161
|
+
config.networking?.validateWorkspaceId?.(
|
|
1162
|
+
sandboxWorkspaceId({ session: ctx.session, workspace: sandboxWorkspaceOf[workspace] })
|
|
1163
|
+
);
|
|
741
1164
|
const existing = await client.getComputer(name);
|
|
742
1165
|
if (existing?.state === ComputerState.Error)
|
|
743
1166
|
throw computerSandboxError(
|
|
744
1167
|
"computer_gone",
|
|
745
1168
|
`Computer ${existing.id} (${name}) is in the Error state. Recover it through the Computers API before retrying; its workspace is preserved.`
|
|
746
1169
|
);
|
|
1170
|
+
if (manageNetworking && access === SandboxAccess.Read && existing != null && existing.state !== ComputerState.Deleted && existing.state !== ComputerState.Running)
|
|
1171
|
+
throw new Error("A managed Computer requires a write binding to resume networking");
|
|
1172
|
+
const initialNetworkPolicy = manageNetworking && existing != null && existing.state !== ComputerState.Deleted ? networkPolicyFor(await client.getNetworkPolicy(existing.id)) : networkPolicyFor(egress);
|
|
1173
|
+
if (manageNetworking && access !== SandboxAccess.Read && existing?.state === ComputerState.Running)
|
|
1174
|
+
await client.disableAutoResume(existing.id);
|
|
1175
|
+
if (manageNetworking && config.networking != null)
|
|
1176
|
+
await preservePolicy({
|
|
1177
|
+
ctx,
|
|
1178
|
+
workspaceId: sandboxWorkspaceId({
|
|
1179
|
+
session: ctx.session,
|
|
1180
|
+
workspace: sandboxWorkspaceOf[workspace]
|
|
1181
|
+
}),
|
|
1182
|
+
policy: initialNetworkPolicy
|
|
1183
|
+
});
|
|
747
1184
|
let info;
|
|
748
1185
|
let fresh = true;
|
|
749
1186
|
if (existing != null && existing.state !== ComputerState.Deleted) {
|
|
750
1187
|
info = existing;
|
|
751
1188
|
fresh = !isRunningState(existing.state);
|
|
1189
|
+
if (manageNetworking && existing.state !== ComputerState.Running)
|
|
1190
|
+
await client.setNetworkPolicy(
|
|
1191
|
+
existing.id,
|
|
1192
|
+
{ egress: ComputerEgressPolicy.None },
|
|
1193
|
+
{ autoResume: false }
|
|
1194
|
+
);
|
|
752
1195
|
if (existing.state === ComputerState.Paused) await client.action(existing.id, "resume");
|
|
753
1196
|
else if (fresh) await client.action(existing.id, "start");
|
|
754
1197
|
} else {
|
|
@@ -758,11 +1201,11 @@ ${step.run}`,
|
|
|
758
1201
|
...source,
|
|
759
1202
|
...config.machine,
|
|
760
1203
|
env: {},
|
|
761
|
-
autoResume:
|
|
1204
|
+
autoResume: !manageNetworking,
|
|
762
1205
|
idleTimeoutSeconds: config.idleTimeoutSeconds ?? 15 * 60,
|
|
763
1206
|
// Provider teardown owns deletion; VM-level ephemeral disks cannot be paused.
|
|
764
1207
|
ephemeral: false,
|
|
765
|
-
network: egress,
|
|
1208
|
+
network: manageNetworking ? { egress: ComputerEgressPolicy.None } : egress,
|
|
766
1209
|
tags: [
|
|
767
1210
|
"final-agent-sandbox",
|
|
768
1211
|
`agent-binding:${ctx.session.bindingId}`,
|
|
@@ -772,37 +1215,109 @@ ${step.run}`,
|
|
|
772
1215
|
};
|
|
773
1216
|
body.idlePolicy = ComputerIdlePolicy.Pause;
|
|
774
1217
|
if (config.regionSlug != null) body.regionSlug = config.regionSlug;
|
|
775
|
-
const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.session.bindingId}:${name}:${source.baseId ?? source.imageVersionId}`)).slice(0, 32)}`;
|
|
1218
|
+
const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.session.bindingId}:${name}:${source.baseId ?? source.imageVersionId}:${manageNetworking}`)).slice(0, 32)}`;
|
|
776
1219
|
info = await client.createComputer(body, idempotencyKey);
|
|
777
1220
|
}
|
|
778
1221
|
const reused = existing != null && existing.id === info.id;
|
|
779
1222
|
log("computer_sandbox.started", { computerId: info.id, reused });
|
|
1223
|
+
let cleanupSetup = null;
|
|
780
1224
|
try {
|
|
781
1225
|
info = await awaitRunning(ctx, { computerId: info.id }, { timeoutMs: runningWaitMs });
|
|
782
|
-
if (
|
|
783
|
-
|
|
1226
|
+
if (!manageNetworking) {
|
|
1227
|
+
if (reused) await client.setNetworkPolicy(info.id, egress);
|
|
1228
|
+
await reportPlatformNetworkPolicy(ctx, policy);
|
|
1229
|
+
}
|
|
784
1230
|
const setup = setupSession({ client, info, env });
|
|
785
|
-
|
|
786
|
-
|
|
1231
|
+
cleanupSetup = async () => setup.transport.close();
|
|
1232
|
+
if (!manageNetworking || access !== SandboxAccess.Read)
|
|
1233
|
+
await writePlatformFiles(ctx, setup.sandbox);
|
|
1234
|
+
const manager = workspaceNetworking({
|
|
1235
|
+
ctx,
|
|
1236
|
+
enabled: manageNetworking && access !== SandboxAccess.Read,
|
|
1237
|
+
access,
|
|
787
1238
|
sandbox: setup.sandbox,
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1239
|
+
client,
|
|
1240
|
+
info,
|
|
1241
|
+
spec,
|
|
1242
|
+
onClose: () => setup.transport.close(),
|
|
1243
|
+
initialNetworkPolicy
|
|
791
1244
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1245
|
+
cleanupSetup = async () => {
|
|
1246
|
+
try {
|
|
1247
|
+
await manager?.closeOnFailure();
|
|
1248
|
+
} finally {
|
|
1249
|
+
setup.transport.close();
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const servicesMarker = WORKSPACE_ROOT + "/.final-workspace-services";
|
|
1253
|
+
if (fresh && manageNetworking)
|
|
1254
|
+
await setup.sandbox.writeTextFile({ path: servicesMarker, content: "pending" });
|
|
1255
|
+
await manager?.initialize();
|
|
1256
|
+
const preparationMarker = WORKSPACE_ROOT + "/.final/workspace-prepared/" + await sha256Hex(artifact.data.key);
|
|
1257
|
+
let preparation = null;
|
|
1258
|
+
const prepareWorkspace = () => preparation ??= (async () => {
|
|
1259
|
+
await mountRepositories({
|
|
1260
|
+
sandbox: setup.sandbox,
|
|
1261
|
+
repositories: spec.parameters.repositories,
|
|
1262
|
+
resolver: ctx.repositories,
|
|
1263
|
+
log
|
|
1264
|
+
});
|
|
1265
|
+
await materializeSandboxSeeds({
|
|
1266
|
+
sandbox: setup.sandbox,
|
|
1267
|
+
root: WORKSPACE_ROOT,
|
|
1268
|
+
files: await sandboxSeedFilesForStart(ctx, artifact.data)
|
|
1269
|
+
});
|
|
1270
|
+
await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
|
|
1271
|
+
const result = await runWorkspaceSetup({
|
|
1272
|
+
sandbox: setup.sandbox,
|
|
1273
|
+
template: spec.template,
|
|
1274
|
+
repositories: spec.parameters.repositories,
|
|
1275
|
+
log
|
|
1276
|
+
});
|
|
1277
|
+
const servicesPending = manager != null && await setup.sandbox.stat({ path: servicesMarker }) != null && await setup.sandbox.readTextFile({ path: servicesMarker }) === "pending";
|
|
1278
|
+
if (fresh || servicesPending) {
|
|
1279
|
+
await startServicesFor({ setup, spec, env });
|
|
1280
|
+
if (manageNetworking)
|
|
1281
|
+
await setup.sandbox.writeTextFile({ path: servicesMarker, content: "complete" });
|
|
1282
|
+
}
|
|
1283
|
+
if (manager != null)
|
|
1284
|
+
await setup.sandbox.writeTextFile({ path: preparationMarker, content: "complete" });
|
|
1285
|
+
return result;
|
|
1286
|
+
})().catch((cause) => {
|
|
1287
|
+
preparation = null;
|
|
1288
|
+
throw cause;
|
|
796
1289
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1290
|
+
let ready = !manageNetworking || access !== SandboxAccess.Read;
|
|
1291
|
+
if (manager != null) {
|
|
1292
|
+
try {
|
|
1293
|
+
await ctx.executionPolicy?.beforeExecution({
|
|
1294
|
+
operation: SandboxExecutionOperation.Run,
|
|
1295
|
+
background: false
|
|
1296
|
+
});
|
|
1297
|
+
await manager.guard();
|
|
1298
|
+
} catch {
|
|
1299
|
+
log("workspace_networking.setup_deferred", {
|
|
1300
|
+
provider: "computer",
|
|
1301
|
+
computerId: info.id
|
|
1302
|
+
});
|
|
1303
|
+
ready = false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const setupHash = ready ? await prepareWorkspace() : null;
|
|
1307
|
+
const { handle } = await buildHandle({
|
|
1308
|
+
ctx,
|
|
1309
|
+
client,
|
|
1310
|
+
info,
|
|
1311
|
+
spec,
|
|
1312
|
+
access,
|
|
1313
|
+
env,
|
|
1314
|
+
networking: manager,
|
|
1315
|
+
networkingManaged: manageNetworking,
|
|
1316
|
+
closeSetup: () => setup.transport.close(),
|
|
1317
|
+
beforeUse: async () => {
|
|
1318
|
+
await prepareWorkspace();
|
|
1319
|
+
}
|
|
803
1320
|
});
|
|
804
|
-
if (fresh) await startServicesFor({ setup, spec, env });
|
|
805
|
-
const { handle } = await buildHandle({ ctx, client, info, spec, access, env });
|
|
806
1321
|
return {
|
|
807
1322
|
handle,
|
|
808
1323
|
region: info.regionSlug ?? config.regionSlug ?? null,
|
|
@@ -813,25 +1328,44 @@ ${step.run}`,
|
|
|
813
1328
|
workspace,
|
|
814
1329
|
access,
|
|
815
1330
|
guideVersionId: spec.guide?.versionId ?? null,
|
|
816
|
-
setupHash
|
|
1331
|
+
setupHash,
|
|
1332
|
+
setupPending: !ready,
|
|
1333
|
+
workspaceId: sandboxWorkspaceId({
|
|
1334
|
+
session: ctx.session,
|
|
1335
|
+
workspace: sandboxWorkspaceOf[workspace]
|
|
1336
|
+
}),
|
|
1337
|
+
networkPolicySaved: manageNetworking
|
|
817
1338
|
}
|
|
818
1339
|
};
|
|
819
1340
|
} catch (error) {
|
|
1341
|
+
let cause = error;
|
|
1342
|
+
try {
|
|
1343
|
+
await cleanupSetup?.();
|
|
1344
|
+
} catch (cleanupError) {
|
|
1345
|
+
cause = new AggregateError(
|
|
1346
|
+
[error, cleanupError],
|
|
1347
|
+
"Computer setup and egress cleanup failed"
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
820
1350
|
if (!reused) {
|
|
821
1351
|
try {
|
|
822
1352
|
await client.action(info.id, "delete");
|
|
823
1353
|
} catch (cleanupError) {
|
|
824
1354
|
throw new AggregateError(
|
|
825
|
-
[
|
|
1355
|
+
[cause, cleanupError],
|
|
826
1356
|
`computer ${info.name} setup and cleanup failed`
|
|
827
1357
|
);
|
|
828
1358
|
}
|
|
829
1359
|
}
|
|
830
|
-
throw
|
|
1360
|
+
throw cause;
|
|
831
1361
|
}
|
|
832
1362
|
},
|
|
833
1363
|
resumePassive: async (ctx, _artifact, rawState) => {
|
|
834
1364
|
const state = computerStateSchema.parse(rawState);
|
|
1365
|
+
if (ctx.session.workspaceId == null && state.workspaceId != null)
|
|
1366
|
+
ctx = { ...ctx, session: { ...ctx.session, workspaceId: state.workspaceId } };
|
|
1367
|
+
if (state.networkPolicySaved === true && state.workspaceId != null && ctx.session.workspaceId !== state.workspaceId)
|
|
1368
|
+
throw new Error("Passive workspace identity does not match retained provider state");
|
|
835
1369
|
const client = clientFor(ctx.session.organizationId);
|
|
836
1370
|
const info = await client.getComputer(state.computerId);
|
|
837
1371
|
if (info == null || info.state !== ComputerState.Running)
|
|
@@ -842,80 +1376,277 @@ ${step.run}`,
|
|
|
842
1376
|
await awaitRunning(ctx, state, { timeoutMs: runningWaitMs });
|
|
843
1377
|
const spec = await resolve(ctx.guides);
|
|
844
1378
|
const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
|
|
845
|
-
return (await buildHandle({
|
|
1379
|
+
return (await buildHandle({
|
|
1380
|
+
ctx,
|
|
1381
|
+
client,
|
|
1382
|
+
info,
|
|
1383
|
+
spec,
|
|
1384
|
+
access: SandboxAccess.Read,
|
|
1385
|
+
env,
|
|
1386
|
+
networkingManaged: state.networkPolicySaved === true
|
|
1387
|
+
})).handle;
|
|
846
1388
|
},
|
|
847
1389
|
resume: async (ctx, _artifact, rawState) => {
|
|
848
1390
|
const state = computerStateSchema.parse(rawState);
|
|
1391
|
+
if (state.networkPolicySaved === true) {
|
|
1392
|
+
if (config.networking == null)
|
|
1393
|
+
throw new Error("Device networking control is required before resume");
|
|
1394
|
+
}
|
|
1395
|
+
if (state.workspaceId != null) {
|
|
1396
|
+
if (state.networkPolicySaved === true && ctx.session.workspaceId != null && ctx.session.workspaceId !== state.workspaceId)
|
|
1397
|
+
throw new Error("Resume workspace identity does not match retained provider state");
|
|
1398
|
+
if (ctx.session.workspaceId == null)
|
|
1399
|
+
ctx = { ...ctx, session: { ...ctx.session, workspaceId: state.workspaceId } };
|
|
1400
|
+
}
|
|
1401
|
+
if (ctx.session.workspaceId == null)
|
|
1402
|
+
ctx = {
|
|
1403
|
+
...ctx,
|
|
1404
|
+
session: {
|
|
1405
|
+
...ctx.session,
|
|
1406
|
+
workspaceId: sandboxWorkspaceId({
|
|
1407
|
+
session: ctx.session,
|
|
1408
|
+
workspace: sandboxWorkspaceOf[state.workspace]
|
|
1409
|
+
})
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
849
1412
|
const client = clientFor(ctx.session.organizationId);
|
|
1413
|
+
const workspaceId = sandboxWorkspaceId({
|
|
1414
|
+
session: ctx.session,
|
|
1415
|
+
workspace: sandboxWorkspaceOf[state.workspace]
|
|
1416
|
+
});
|
|
1417
|
+
const manageNetworking = state.networkPolicySaved === true || await needsWorkspaceNetworking(ctx, workspaceId) || state.access !== SandboxAccess.Read && await needsWorkspaceNetworking(ctx, workspaceId, { access: state.access });
|
|
1418
|
+
if (manageNetworking)
|
|
1419
|
+
config.networking?.validateWorkspaceId?.(
|
|
1420
|
+
sandboxWorkspaceId({
|
|
1421
|
+
session: ctx.session,
|
|
1422
|
+
workspace: sandboxWorkspaceOf[state.workspace]
|
|
1423
|
+
})
|
|
1424
|
+
);
|
|
850
1425
|
const info = await client.getComputer(state.computerId);
|
|
851
1426
|
if (info == null || info.state === ComputerState.Deleted || info.state === ComputerState.Error) {
|
|
852
1427
|
throw computerSandboxError("computer_gone", `computer ${state.computerName} is gone`);
|
|
853
1428
|
}
|
|
1429
|
+
if (manageNetworking && state.access === SandboxAccess.Read && info.state !== ComputerState.Running)
|
|
1430
|
+
throw new Error("A managed Computer requires a write binding to resume networking");
|
|
1431
|
+
const initialNetworkPolicy = manageNetworking ? networkPolicyFor(await client.getNetworkPolicy(info.id)) : null;
|
|
1432
|
+
if (initialNetworkPolicy != null && state.access !== SandboxAccess.Read && info.state === ComputerState.Running)
|
|
1433
|
+
await client.disableAutoResume(info.id);
|
|
854
1434
|
const rebooted = !isRunningState(info.state);
|
|
1435
|
+
if (manageNetworking && config.networking != null && initialNetworkPolicy != null)
|
|
1436
|
+
await preservePolicy({
|
|
1437
|
+
ctx,
|
|
1438
|
+
workspaceId: sandboxWorkspaceId({
|
|
1439
|
+
session: ctx.session,
|
|
1440
|
+
workspace: sandboxWorkspaceOf[state.workspace]
|
|
1441
|
+
}),
|
|
1442
|
+
policy: initialNetworkPolicy
|
|
1443
|
+
});
|
|
1444
|
+
if (manageNetworking && info.state !== ComputerState.Running)
|
|
1445
|
+
await client.setNetworkPolicy(
|
|
1446
|
+
info.id,
|
|
1447
|
+
{ egress: ComputerEgressPolicy.None },
|
|
1448
|
+
{ autoResume: false }
|
|
1449
|
+
);
|
|
855
1450
|
if (info.state === ComputerState.Paused) await client.action(info.id, "resume");
|
|
856
1451
|
else if (rebooted) await client.action(info.id, "start");
|
|
857
1452
|
const running = await awaitRunning(ctx, state, { timeoutMs: runningWaitMs });
|
|
858
1453
|
const spec = await resolve(ctx.guides);
|
|
859
1454
|
const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1455
|
+
if (!manageNetworking) {
|
|
1456
|
+
const policy = withPlatformEgressHosts(ctx, spec.parameters.networkPolicy);
|
|
1457
|
+
await client.setNetworkPolicy(running.id, computerEgressOf(policy));
|
|
1458
|
+
await reportPlatformNetworkPolicy(ctx, policy);
|
|
1459
|
+
}
|
|
863
1460
|
const setup = setupSession({ client, info: running, env });
|
|
864
|
-
|
|
865
|
-
await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
|
|
866
|
-
if (rebooted) await startServicesFor({ setup, spec, env });
|
|
867
|
-
const { handle } = await buildHandle({
|
|
1461
|
+
const manager = workspaceNetworking({
|
|
868
1462
|
ctx,
|
|
1463
|
+
enabled: manageNetworking && state.access !== SandboxAccess.Read,
|
|
1464
|
+
access: state.access,
|
|
1465
|
+
sandbox: setup.sandbox,
|
|
869
1466
|
client,
|
|
870
1467
|
info: running,
|
|
871
1468
|
spec,
|
|
872
|
-
|
|
873
|
-
|
|
1469
|
+
onClose: () => setup.transport.close(),
|
|
1470
|
+
initialNetworkPolicy: initialNetworkPolicy ?? networkPolicyFor(egressFor(ctx, spec.parameters.networkPolicy))
|
|
874
1471
|
});
|
|
875
|
-
return handle;
|
|
876
|
-
},
|
|
877
|
-
park: async (ctx, _artifact, rawState) => {
|
|
878
|
-
const state = computerStateSchema.parse(rawState);
|
|
879
|
-
await closeComputerMcp(state.computerId);
|
|
880
|
-
const client = clientFor(ctx.session.organizationId);
|
|
881
|
-
const current = await client.getComputer(state.computerId);
|
|
882
|
-
if (current == null || hasNothingToPause(current.state)) {
|
|
883
|
-
log("computer_sandbox.park_at_rest", {
|
|
884
|
-
computerId: state.computerId,
|
|
885
|
-
state: current?.state ?? null
|
|
886
|
-
});
|
|
887
|
-
return state;
|
|
888
|
-
}
|
|
889
1472
|
try {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1473
|
+
if (!manageNetworking || state.access !== SandboxAccess.Read)
|
|
1474
|
+
await writePlatformFiles(ctx, setup.sandbox);
|
|
1475
|
+
if (manageNetworking && rebooted)
|
|
1476
|
+
await setup.sandbox.writeTextFile({
|
|
1477
|
+
path: WORKSPACE_ROOT + "/.final-workspace-services",
|
|
1478
|
+
content: "pending"
|
|
1479
|
+
});
|
|
1480
|
+
await manager?.initialize();
|
|
1481
|
+
let preparation = null;
|
|
1482
|
+
const prepareWorkspace = () => preparation ??= (async () => {
|
|
1483
|
+
const artifact = state.setupPending ? computerArtifactSchema.parse(_artifact) : null;
|
|
1484
|
+
const marker = artifact == null ? null : WORKSPACE_ROOT + "/.final/workspace-prepared/" + await sha256Hex(artifact.key);
|
|
1485
|
+
const pending = marker != null && await setup.sandbox.stat({ path: marker }) == null;
|
|
1486
|
+
if (pending && artifact != null) {
|
|
1487
|
+
await mountRepositories({
|
|
1488
|
+
sandbox: setup.sandbox,
|
|
1489
|
+
repositories: spec.parameters.repositories,
|
|
1490
|
+
resolver: ctx.repositories,
|
|
1491
|
+
log
|
|
1492
|
+
});
|
|
1493
|
+
await materializeSandboxSeeds({
|
|
1494
|
+
sandbox: setup.sandbox,
|
|
1495
|
+
root: WORKSPACE_ROOT,
|
|
1496
|
+
files: await sandboxSeedFilesForStart(ctx, artifact)
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
|
|
1500
|
+
if (pending)
|
|
1501
|
+
await runWorkspaceSetup({
|
|
1502
|
+
sandbox: setup.sandbox,
|
|
1503
|
+
template: spec.template,
|
|
1504
|
+
repositories: spec.parameters.repositories,
|
|
1505
|
+
log
|
|
1506
|
+
});
|
|
1507
|
+
const servicesMarker = WORKSPACE_ROOT + "/.final-workspace-services";
|
|
1508
|
+
const servicesPending = manageNetworking && await setup.sandbox.stat({ path: servicesMarker }) != null && await setup.sandbox.readTextFile({ path: servicesMarker }) === "pending";
|
|
1509
|
+
if (rebooted || servicesPending) {
|
|
1510
|
+
await startServicesFor({ setup, spec, env });
|
|
1511
|
+
if (manageNetworking)
|
|
1512
|
+
await setup.sandbox.writeTextFile({ path: servicesMarker, content: "complete" });
|
|
1513
|
+
}
|
|
1514
|
+
if (pending && marker != null)
|
|
1515
|
+
await setup.sandbox.writeTextFile({ path: marker, content: "complete" });
|
|
1516
|
+
})().catch((cause) => {
|
|
1517
|
+
preparation = null;
|
|
1518
|
+
throw cause;
|
|
1519
|
+
});
|
|
1520
|
+
let ready = !manageNetworking || state.access !== SandboxAccess.Read;
|
|
1521
|
+
if (manager != null) {
|
|
1522
|
+
try {
|
|
1523
|
+
await ctx.executionPolicy?.beforeExecution({
|
|
1524
|
+
operation: SandboxExecutionOperation.Run,
|
|
1525
|
+
background: false
|
|
1526
|
+
});
|
|
1527
|
+
await manager.guard();
|
|
1528
|
+
} catch {
|
|
1529
|
+
log("workspace_networking.setup_deferred", {
|
|
1530
|
+
provider: "computer",
|
|
1531
|
+
computerId: info.id
|
|
1532
|
+
});
|
|
1533
|
+
ready = false;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (ready) await prepareWorkspace();
|
|
1537
|
+
const { handle } = await buildHandle({
|
|
1538
|
+
ctx,
|
|
1539
|
+
client,
|
|
1540
|
+
info: running,
|
|
1541
|
+
spec,
|
|
1542
|
+
access: state.access,
|
|
1543
|
+
env,
|
|
1544
|
+
networking: manager,
|
|
1545
|
+
networkingManaged: manageNetworking,
|
|
1546
|
+
closeSetup: () => setup.transport.close(),
|
|
1547
|
+
beforeUse: prepareWorkspace
|
|
899
1548
|
});
|
|
1549
|
+
return handle;
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
try {
|
|
1552
|
+
await manager?.closeOnFailure();
|
|
1553
|
+
} catch (cleanupError) {
|
|
1554
|
+
throw new AggregateError(
|
|
1555
|
+
[error, cleanupError],
|
|
1556
|
+
"Computer resume and egress cleanup failed"
|
|
1557
|
+
);
|
|
1558
|
+
} finally {
|
|
1559
|
+
setup.transport.close();
|
|
1560
|
+
}
|
|
1561
|
+
throw error;
|
|
900
1562
|
}
|
|
901
|
-
|
|
1563
|
+
},
|
|
1564
|
+
park: async (ctx, _artifact, rawState) => {
|
|
1565
|
+
const state = computerStateSchema.parse(rawState);
|
|
1566
|
+
const workspaceId = await teardownComputer({
|
|
1567
|
+
ctx,
|
|
1568
|
+
computerId: state.computerId,
|
|
1569
|
+
computerName: state.computerName,
|
|
1570
|
+
workspace: state.workspace,
|
|
1571
|
+
policySaved: state.networkPolicySaved === true,
|
|
1572
|
+
workspaceId: state.networkPolicySaved === true ? state.workspaceId : ctx.session.workspaceId ?? state.workspaceId,
|
|
1573
|
+
fence: true,
|
|
1574
|
+
finish: async () => {
|
|
1575
|
+
const failures = [];
|
|
1576
|
+
try {
|
|
1577
|
+
await closeComputerMcp(state.computerId);
|
|
1578
|
+
} catch (cause) {
|
|
1579
|
+
failures.push(cause);
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
const client = clientFor(ctx.session.organizationId);
|
|
1583
|
+
let current;
|
|
1584
|
+
try {
|
|
1585
|
+
current = await client.getComputer(state.computerId);
|
|
1586
|
+
} catch (cause) {
|
|
1587
|
+
failures.push(cause);
|
|
1588
|
+
}
|
|
1589
|
+
if (current !== void 0 && (current == null || hasNothingToPause(current.state))) {
|
|
1590
|
+
log("computer_sandbox.park_at_rest", {
|
|
1591
|
+
computerId: state.computerId,
|
|
1592
|
+
state: current?.state ?? null
|
|
1593
|
+
});
|
|
1594
|
+
} else {
|
|
1595
|
+
try {
|
|
1596
|
+
await client.action(state.computerId, "pause");
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (!pauseMayHaveSettled(error)) throw error;
|
|
1599
|
+
const settled = await client.getComputer(state.computerId);
|
|
1600
|
+
if (settled != null && settled.state !== ComputerState.Paused && !hasNothingToPause(settled.state))
|
|
1601
|
+
throw error;
|
|
1602
|
+
log("computer_sandbox.park_at_rest", {
|
|
1603
|
+
computerId: state.computerId,
|
|
1604
|
+
state: settled?.state ?? null
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
} catch (cause) {
|
|
1609
|
+
failures.push(cause);
|
|
1610
|
+
}
|
|
1611
|
+
if (failures.length === 1) throw failures[0];
|
|
1612
|
+
if (failures.length > 1) throw new AggregateError(failures, "Computer pause failed");
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
const key = ctx.session.organizationId + ":" + workspaceId;
|
|
1616
|
+
return {
|
|
1617
|
+
...state,
|
|
1618
|
+
workspaceId,
|
|
1619
|
+
networkPolicySaved: state.networkPolicySaved === true || savedPolicyWorkspaces.has(key)
|
|
1620
|
+
};
|
|
902
1621
|
},
|
|
903
1622
|
onSessionEnd: async (ctx, _artifact, rawState, { reason }) => {
|
|
904
1623
|
const state = computerStateSchema.parse(rawState);
|
|
905
1624
|
const client = clientFor(ctx.session.organizationId);
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1625
|
+
const finish = async () => {
|
|
1626
|
+
const action = state.workspace === ComputerWorkspacePolicy.Conversation ? "stop" : "delete";
|
|
1627
|
+
log("computer_sandbox.session_end", { computerId: state.computerId, reason, action });
|
|
1628
|
+
try {
|
|
1629
|
+
await client.action(state.computerId, action);
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
if (isComputersApiStatus(error, 404)) return;
|
|
1632
|
+
if (!isComputersApiStatus(error, 409)) throw error;
|
|
1633
|
+
const current = await client.getComputer(state.computerId);
|
|
1634
|
+
const settled = current == null || current.state === ComputerState.Deleted || action === "stop" && current.state === ComputerState.Shelved;
|
|
1635
|
+
if (!settled) throw error;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
if (action === "stop") await awaitShelved(client, state.computerId);
|
|
1639
|
+
};
|
|
1640
|
+
await teardownComputer({
|
|
1641
|
+
ctx,
|
|
1642
|
+
computerId: state.computerId,
|
|
1643
|
+
computerName: state.computerName,
|
|
1644
|
+
workspace: state.workspace,
|
|
1645
|
+
policySaved: state.networkPolicySaved === true,
|
|
1646
|
+
workspaceId: state.networkPolicySaved === true ? state.workspaceId : ctx.session.workspaceId ?? state.workspaceId,
|
|
1647
|
+
fence: state.workspace === ComputerWorkspacePolicy.Conversation,
|
|
1648
|
+
finish
|
|
1649
|
+
});
|
|
919
1650
|
}
|
|
920
1651
|
};
|
|
921
1652
|
};
|
|
@@ -930,6 +1661,7 @@ export {
|
|
|
930
1661
|
COMPUTERS_API_TEAM_SECRET,
|
|
931
1662
|
COMPUTER_SANDBOX_SPECIFIER2 as COMPUTER_SANDBOX_SPECIFIER,
|
|
932
1663
|
computeComputerArtifactKey2 as computeComputerArtifactKey,
|
|
1664
|
+
computerWorkspaceName,
|
|
933
1665
|
createComputerSandboxProvider
|
|
934
1666
|
};
|
|
935
1667
|
//# sourceMappingURL=computer.mjs.map
|