@finalai/final-agent-provider 0.3.3 → 0.4.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.
@@ -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
- await closeComputerMcp(info.id);
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
- await client.action(info.id, "delete");
292
- } catch (error) {
293
- if (!isComputersApiStatus(error, 404)) throw error;
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 = sessionForAccess(full, args.access);
368
- return {
369
- handle: {
370
- sandbox,
371
- onSandboxStop: async () => {
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 closeMcp();
374
- } finally {
375
- transport.close();
496
+ if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
497
+ failures.push(cause);
498
+ } catch (lookup) {
499
+ failures.push(lookup);
376
500
  }
377
- },
378
- onSandboxDelete: async () => {
501
+ }
502
+ for (const finish of [closeMcp, () => transport.close(), () => args.closeSetup?.()]) {
379
503
  try {
380
- await closeMcp();
381
- } finally {
382
- transport.close();
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 client.action(info.id, "delete");
386
- } catch (error) {
387
- if (!isComputersApiStatus(error, 404)) throw error;
517
+ await closeMcp();
518
+ } catch (cause) {
519
+ failures.push(cause);
388
520
  }
389
- },
390
- onRuntimeShutdown: async () => {
521
+ transport.close();
522
+ args.closeSetup?.();
391
523
  try {
392
- await closeMcp();
393
- } finally {
394
- transport.close();
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 (_ctx, rawState, request) => {
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
- return computerEditor({
635
- client: clientFor(_ctx.organizationId),
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 = workspace === ComputerWorkspacePolicy.Conversation && ctx.session.conversationId != null ? `ac-${(await sha256Hex(`${ctx.session.conversationId}:${ctx.session.target}`)).slice(0, 24)}` : `ab-${compactId(ctx.session.bindingId)}`;
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: true,
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 (reused) await client.setNetworkPolicy(info.id, egress);
783
- await reportPlatformNetworkPolicy(ctx, policy);
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
- await writePlatformFiles(ctx, setup.sandbox);
786
- await mountRepositories({
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
- repositories: spec.parameters.repositories,
789
- resolver: ctx.repositories,
790
- log
1239
+ client,
1240
+ info,
1241
+ spec,
1242
+ onClose: () => setup.transport.close(),
1243
+ initialNetworkPolicy
791
1244
  });
792
- await materializeSandboxSeeds({
793
- sandbox: setup.sandbox,
794
- root: WORKSPACE_ROOT,
795
- files: await sandboxSeedFilesForStart(ctx, artifact.data)
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
- await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
798
- const setupHash = await runWorkspaceSetup({
799
- sandbox: setup.sandbox,
800
- template: spec.template,
801
- repositories: spec.parameters.repositories,
802
- log
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
- [error, cleanupError],
1355
+ [cause, cleanupError],
826
1356
  `computer ${info.name} setup and cleanup failed`
827
1357
  );
828
1358
  }
829
1359
  }
830
- throw error;
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({ ctx, client, info, spec, access: state.access, env })).handle;
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
- const policy = withPlatformEgressHosts(ctx, spec.parameters.networkPolicy);
861
- await client.setNetworkPolicy(running.id, computerEgressOf(policy));
862
- await reportPlatformNetworkPolicy(ctx, policy);
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
- await writePlatformFiles(ctx, setup.sandbox);
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
- access: state.access,
873
- env
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
- await client.action(state.computerId, "pause");
891
- } catch (error) {
892
- if (!pauseMayHaveSettled(error)) throw error;
893
- const settled = await client.getComputer(state.computerId);
894
- if (settled != null && settled.state !== ComputerState.Paused && !hasNothingToPause(settled.state))
895
- throw error;
896
- log("computer_sandbox.park_at_rest", {
897
- computerId: state.computerId,
898
- state: settled?.state ?? null
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
- return state;
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 action = state.workspace === ComputerWorkspacePolicy.Conversation ? "stop" : "delete";
907
- log("computer_sandbox.session_end", { computerId: state.computerId, reason, action });
908
- try {
909
- await client.action(state.computerId, action);
910
- } catch (error) {
911
- if (isComputersApiStatus(error, 404)) return;
912
- if (!isComputersApiStatus(error, 409)) throw error;
913
- const current = await client.getComputer(state.computerId);
914
- const settled = current == null || current.state === ComputerState.Deleted || action === "stop" && current.state === ComputerState.Shelved;
915
- if (!settled) throw error;
916
- return;
917
- }
918
- if (action === "stop") await awaitShelved(client, state.computerId);
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