@finalai/final-agent-provider 0.10.1 → 0.11.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.
Files changed (159) hide show
  1. package/HOSTING.md +334 -245
  2. package/README.md +126 -40
  3. package/WORKSPACE-EGRESS.md +11 -11
  4. package/dist/modules/catalog.d.mts +9 -0
  5. package/dist/modules/catalog.mjs +21 -0
  6. package/dist/modules/catalog.mjs.map +7 -0
  7. package/dist/modules/harnesses/acp-agent.d.mts +4 -0
  8. package/dist/modules/harnesses/acp-agent.mjs +35 -0
  9. package/dist/modules/harnesses/acp-agent.mjs.map +7 -0
  10. package/dist/modules/harnesses/adapters.d.mts +7 -0
  11. package/dist/modules/harnesses/adapters.mjs +74 -0
  12. package/dist/modules/harnesses/adapters.mjs.map +7 -0
  13. package/dist/modules/harnesses/claude-code.d.mts +14 -0
  14. package/dist/modules/harnesses/claude-code.mjs +79 -0
  15. package/dist/modules/harnesses/claude-code.mjs.map +7 -0
  16. package/dist/modules/harnesses/codex.d.mts +12 -0
  17. package/dist/modules/harnesses/codex.mjs +90 -0
  18. package/dist/modules/harnesses/codex.mjs.map +7 -0
  19. package/dist/modules/harnesses/config-fields.d.mts +98 -57
  20. package/dist/modules/harnesses/config-fields.mjs +58 -35
  21. package/dist/modules/harnesses/config-fields.mjs.map +3 -3
  22. package/dist/modules/harnesses/definitions.d.mts +3 -0
  23. package/dist/modules/harnesses/definitions.mjs +21 -0
  24. package/dist/modules/harnesses/definitions.mjs.map +7 -0
  25. package/dist/modules/harnesses/fidelity.d.mts +1 -1
  26. package/dist/modules/harnesses/fidelity.mjs +17 -14
  27. package/dist/modules/harnesses/fidelity.mjs.map +2 -2
  28. package/dist/modules/harnesses/final.d.mts +12 -0
  29. package/dist/modules/harnesses/final.mjs +70 -0
  30. package/dist/modules/harnesses/final.mjs.map +7 -0
  31. package/dist/modules/harnesses/index.d.mts +13 -12
  32. package/dist/modules/harnesses/index.mjs +86 -45
  33. package/dist/modules/harnesses/index.mjs.map +3 -3
  34. package/dist/modules/harnesses/names.d.mts +43 -0
  35. package/dist/modules/harnesses/names.mjs +59 -0
  36. package/dist/modules/harnesses/names.mjs.map +7 -0
  37. package/dist/modules/harnesses/nanocode-claude.d.mts +18 -0
  38. package/dist/modules/harnesses/nanocode-claude.mjs +101 -0
  39. package/dist/modules/harnesses/nanocode-claude.mjs.map +7 -0
  40. package/dist/modules/harnesses/nanocode-codex.d.mts +13 -0
  41. package/dist/modules/harnesses/nanocode-codex.mjs +64 -0
  42. package/dist/modules/harnesses/nanocode-codex.mjs.map +7 -0
  43. package/dist/modules/harnesses/pi.d.mts +12 -0
  44. package/dist/modules/harnesses/pi.mjs +71 -0
  45. package/dist/modules/harnesses/pi.mjs.map +7 -0
  46. package/dist/modules/integrations.d.mts +9 -0
  47. package/dist/modules/integrations.mjs +29 -0
  48. package/dist/modules/integrations.mjs.map +7 -0
  49. package/dist/modules/provider/general.bundle.d.mts +1 -0
  50. package/dist/modules/provider/general.bundle.mjs +5 -0
  51. package/dist/modules/provider/general.bundle.mjs.map +7 -0
  52. package/dist/modules/provider/general.d.mts +11 -0
  53. package/dist/modules/provider/general.mjs +130 -0
  54. package/dist/modules/provider/general.mjs.map +7 -0
  55. package/dist/modules/provider/index.d.mts +3 -0
  56. package/dist/modules/provider/index.mjs +13 -0
  57. package/dist/modules/provider/index.mjs.map +7 -0
  58. package/dist/modules/sandboxes/access.d.mts +34 -0
  59. package/dist/modules/sandboxes/access.mjs +15 -0
  60. package/dist/modules/sandboxes/access.mjs.map +7 -0
  61. package/dist/modules/sandboxes/computer.d.mts +24 -51
  62. package/dist/modules/sandboxes/computer.mjs +523 -888
  63. package/dist/modules/sandboxes/computer.mjs.map +3 -3
  64. package/dist/modules/sandboxes/computers-api.d.mts +3 -2
  65. package/dist/modules/sandboxes/computers-api.mjs +2 -4
  66. package/dist/modules/sandboxes/computers-api.mjs.map +2 -2
  67. package/dist/modules/sandboxes/device-api.d.mts +4 -3
  68. package/dist/modules/sandboxes/device-api.mjs +2 -4
  69. package/dist/modules/sandboxes/device-api.mjs.map +2 -2
  70. package/dist/modules/sandboxes/device.d.mts +6 -6
  71. package/dist/modules/sandboxes/device.mjs +126 -121
  72. package/dist/modules/sandboxes/device.mjs.map +3 -3
  73. package/dist/modules/sandboxes/egress.d.mts +10 -9
  74. package/dist/modules/sandboxes/egress.mjs +73 -56
  75. package/dist/modules/sandboxes/egress.mjs.map +2 -2
  76. package/dist/modules/sandboxes/environment.d.mts +35 -269
  77. package/dist/modules/sandboxes/environment.mjs +27 -104
  78. package/dist/modules/sandboxes/environment.mjs.map +3 -3
  79. package/dist/modules/sandboxes/index.d.mts +9 -69
  80. package/dist/modules/sandboxes/index.mjs +35 -95
  81. package/dist/modules/sandboxes/index.mjs.map +3 -3
  82. package/dist/modules/sandboxes/local.d.mts +28 -0
  83. package/dist/modules/sandboxes/local.mjs +499 -0
  84. package/dist/modules/sandboxes/local.mjs.map +7 -0
  85. package/dist/modules/sandboxes/session.d.mts +0 -3
  86. package/dist/modules/sandboxes/session.mjs +2 -22
  87. package/dist/modules/sandboxes/session.mjs.map +2 -2
  88. package/dist/modules/sandboxes/testing/fake-machine.mjs +0 -28
  89. package/dist/modules/sandboxes/testing/fake-machine.mjs.map +2 -2
  90. package/package.json +76 -104
  91. package/dist/modules/connections.d.mts +0 -11
  92. package/dist/modules/connections.mjs +0 -16
  93. package/dist/modules/connections.mjs.map +0 -7
  94. package/dist/modules/harnesses/acp-describe.d.mts +0 -20
  95. package/dist/modules/harnesses/acp-describe.mjs +0 -110
  96. package/dist/modules/harnesses/acp-describe.mjs.map +0 -7
  97. package/dist/modules/harnesses/local-stdio.d.mts +0 -47
  98. package/dist/modules/harnesses/local-stdio.mjs +0 -41
  99. package/dist/modules/harnesses/local-stdio.mjs.map +0 -7
  100. package/dist/modules/harnesses/remote-acp.d.mts +0 -76
  101. package/dist/modules/harnesses/remote-acp.mjs +0 -83
  102. package/dist/modules/harnesses/remote-acp.mjs.map +0 -7
  103. package/dist/modules/harnesses/remote-claude-code.d.mts +0 -88
  104. package/dist/modules/harnesses/remote-claude-code.mjs +0 -113
  105. package/dist/modules/harnesses/remote-claude-code.mjs.map +0 -7
  106. package/dist/modules/harnesses/remote-client-tls.d.mts +0 -35
  107. package/dist/modules/harnesses/remote-client-tls.mjs +0 -37
  108. package/dist/modules/harnesses/remote-client-tls.mjs.map +0 -7
  109. package/dist/modules/harnesses/remote-codex.d.mts +0 -98
  110. package/dist/modules/harnesses/remote-codex.mjs +0 -128
  111. package/dist/modules/harnesses/remote-codex.mjs.map +0 -7
  112. package/dist/modules/harnesses/remote-pi.d.mts +0 -78
  113. package/dist/modules/harnesses/remote-pi.mjs +0 -104
  114. package/dist/modules/harnesses/remote-pi.mjs.map +0 -7
  115. package/dist/modules/harnesses/remote-url.d.mts +0 -5
  116. package/dist/modules/harnesses/remote-url.mjs +0 -19
  117. package/dist/modules/harnesses/remote-url.mjs.map +0 -7
  118. package/dist/modules/harnesses/serverless-final.d.mts +0 -63
  119. package/dist/modules/harnesses/serverless-final.mjs +0 -88
  120. package/dist/modules/harnesses/serverless-final.mjs.map +0 -7
  121. package/dist/modules/harnesses/serverless-nanocode-claude.d.mts +0 -64
  122. package/dist/modules/harnesses/serverless-nanocode-claude.mjs +0 -105
  123. package/dist/modules/harnesses/serverless-nanocode-claude.mjs.map +0 -7
  124. package/dist/modules/harnesses/serverless-nanocode-codex.d.mts +0 -66
  125. package/dist/modules/harnesses/serverless-nanocode-codex.mjs +0 -113
  126. package/dist/modules/harnesses/serverless-nanocode-codex.mjs.map +0 -7
  127. package/dist/modules/harnesses/serverless-pi.d.mts +0 -64
  128. package/dist/modules/harnesses/serverless-pi.mjs +0 -98
  129. package/dist/modules/harnesses/serverless-pi.mjs.map +0 -7
  130. package/dist/modules/runtime/index.d.mts +0 -7
  131. package/dist/modules/runtime/index.mjs +0 -18
  132. package/dist/modules/runtime/index.mjs.map +0 -7
  133. package/dist/modules/runtime/local-stdio.d.mts +0 -10
  134. package/dist/modules/runtime/local-stdio.mjs +0 -106
  135. package/dist/modules/runtime/local-stdio.mjs.map +0 -7
  136. package/dist/modules/runtime/remote-acp.d.mts +0 -16
  137. package/dist/modules/runtime/remote-acp.mjs +0 -152
  138. package/dist/modules/runtime/remote-acp.mjs.map +0 -7
  139. package/dist/modules/runtime/remote-claude-code.d.mts +0 -5
  140. package/dist/modules/runtime/remote-claude-code.mjs +0 -47
  141. package/dist/modules/runtime/remote-claude-code.mjs.map +0 -7
  142. package/dist/modules/runtime/remote-codex.d.mts +0 -5
  143. package/dist/modules/runtime/remote-codex.mjs +0 -47
  144. package/dist/modules/runtime/remote-codex.mjs.map +0 -7
  145. package/dist/modules/runtime/remote-host.d.mts +0 -127
  146. package/dist/modules/runtime/remote-host.mjs +0 -387
  147. package/dist/modules/runtime/remote-host.mjs.map +0 -7
  148. package/dist/modules/runtime/remote-pi.d.mts +0 -5
  149. package/dist/modules/runtime/remote-pi.mjs +0 -44
  150. package/dist/modules/runtime/remote-pi.mjs.map +0 -7
  151. package/dist/modules/sandboxes/guide.d.mts +0 -35
  152. package/dist/modules/sandboxes/guide.mjs +0 -119
  153. package/dist/modules/sandboxes/guide.mjs.map +0 -7
  154. package/dist/modules/sandboxes/workspace-setup.d.mts +0 -42
  155. package/dist/modules/sandboxes/workspace-setup.mjs +0 -242
  156. package/dist/modules/sandboxes/workspace-setup.mjs.map +0 -7
  157. package/dist/modules/testing/harness.d.mts +0 -9
  158. package/dist/modules/testing/harness.mjs +0 -123
  159. package/dist/modules/testing/harness.mjs.map +0 -7
@@ -1,29 +1,21 @@
1
- import { ProcessInspectionOperation } from "@finalai/agent-protocol/process-inspection";
2
- import {
3
- formatProviderRef,
4
- NetworkPolicy,
5
- packageRef,
6
- SandboxAccess
7
- } from "@finalai/agent-provider";
1
+ import { Capability, FilesAccess, ScreenAccess, ShellAccess } from "@finalai/agent-protocol";
2
+ import { NetworkPolicy } from "@finalai/agent-provider";
8
3
  import {
9
4
  defineSandboxProvider,
10
- withSandboxNetworking,
11
- mapSandboxHandle,
12
- SandboxExecutionOperation,
5
+ FULL_SANDBOX_GRANT,
13
6
  materializeSandboxSeeds,
14
- PLATFORM_ENVIRONMENT_NAMES,
15
7
  platformProcessEnvironment,
16
8
  PortProtocol,
17
9
  reportPlatformNetworkPolicy,
18
10
  SandboxCapability,
11
+ SandboxExecutionOperation,
19
12
  sandboxSeedArtifact,
20
13
  sandboxSeedArtifactSchema,
21
14
  sandboxSeedFiles,
22
- sandboxSeedFilesForStart,
15
+ sandboxSeedFilesForAttach,
23
16
  sandboxSeedIdentity,
24
- sandboxWorkspaceId,
25
- SandboxWorkspace,
26
17
  withPlatformEgressHosts,
18
+ withSandboxNetworking,
27
19
  writePlatformFiles
28
20
  } from "@finalai/agent-provider/sandbox";
29
21
  import {
@@ -41,7 +33,7 @@ import {
41
33
  homeBaselineScript,
42
34
  homeContentScript
43
35
  } from "./build-scripts.mjs";
44
- import { sleep as defaultSleep, sha256Hex } from "./bytes.mjs";
36
+ import { canonicalJson, sleep as defaultSleep, sha256Hex } from "./bytes.mjs";
45
37
  import { computerEditor } from "./computer-editor.mjs";
46
38
  import { readComputerSnapshot } from "./computer-snapshots.mjs";
47
39
  import {
@@ -52,30 +44,15 @@ import {
52
44
  import { createComputerUseEndpoint } from "./computers-api-mcp.mjs";
53
45
  import { createComputersApiTransport } from "./computers-api-transport.mjs";
54
46
  import {
55
- COMPUTER_SANDBOX_SPECIFIER,
47
+ COMPUTER_PROVIDER,
56
48
  computerEnvironmentSchema,
57
- computerOpenOptionsSchema,
58
- ComputerWorkspacePolicy
49
+ DEFAULT_COMPUTER_IMAGE
59
50
  } from "./environment.mjs";
60
- import {
61
- computeComputerArtifactKey,
62
- computerNeedsBuild,
63
- DEFAULT_COMPUTER_IMAGE,
64
- resolveComputerSpec
65
- } from "./guide.mjs";
66
- import { createTransportSession, sessionForAccess } from "./session.mjs";
67
- import {
68
- guideCommands,
69
- installGuideCommands,
70
- mountRepositories,
71
- repositoryPath,
72
- runWorkspaceSetup,
73
- startServices,
74
- WORKSPACE_ROOT
75
- } from "./workspace-setup.mjs";
76
- const computerWorkspaceName = async (options) => options.conversationWorkspace && options.conversationId != null ? `ac-${(await sha256Hex(`${options.conversationId}:${options.target}`)).slice(0, 24)}` : `ab-${compactId(options.bindingId)}`;
77
- import { COMPUTER_SANDBOX_SPECIFIER as COMPUTER_SANDBOX_SPECIFIER2 } from "./environment.mjs";
78
- import { computeComputerArtifactKey as computeComputerArtifactKey2 } from "./guide.mjs";
51
+ import { createTransportSession, readOnlySession } from "./session.mjs";
52
+ import { COMPUTER_PROVIDER as COMPUTER_PROVIDER2 } from "./environment.mjs";
53
+ const WORKSPACE_ROOT = "/workspaces";
54
+ const DEFAULT_IDLE_TIMEOUT_SECONDS = 15 * 60;
55
+ const IDLE_TIMEOUT_MARGIN_SECONDS = 5 * 60;
79
56
  const computerArtifactSchema = sandboxSeedArtifactSchema.extend({
80
57
  version: z.literal(1),
81
58
  key: z.string(),
@@ -90,37 +67,26 @@ const computerArtifactSchema = sandboxSeedArtifactSchema.extend({
90
67
  z.object({ kind: z.literal("captured"), baseId: z.string() })
91
68
  ])
92
69
  });
70
+ const networkPolicySchema = z.object({
71
+ mode: z.nativeEnum(NetworkPolicy),
72
+ allow: z.array(z.string()).optional()
73
+ });
93
74
  const computerStateSchema = z.object({
94
- version: z.literal(1),
95
- computerId: z.string(),
75
+ version: z.literal(2),
96
76
  computerName: z.string(),
97
- workspace: z.nativeEnum(ComputerWorkspacePolicy),
98
- access: z.nativeEnum(SandboxAccess),
99
- guideVersionId: z.string().nullable(),
100
- setupHash: z.string().nullable(),
101
- setupPending: z.boolean().default(false),
102
- workspaceId: z.string().optional(),
103
- networkPolicySaved: z.boolean().optional()
77
+ // Null until the first attach creates the computer.
78
+ computerId: z.string().nullable(),
79
+ // The sandbox setup's network policy, enforced from the computer's first boot.
80
+ network: networkPolicySchema,
81
+ // Ports the setup's services listen on.
82
+ ports: z.array(z.number().int()).default([]),
83
+ // Workspace Device egress manages this computer's network.
84
+ networkPolicySaved: z.boolean().default(false)
104
85
  });
105
86
  const computerSandboxError = (code, message) => Object.assign(new Error(message), { name: "ComputerSandboxError", code });
106
- const RESERVED_ENV = /* @__PURE__ */ new Set([
107
- ...PLATFORM_ENVIRONMENT_NAMES,
108
- "DISPLAY",
109
- "XDG_RUNTIME_DIR",
110
- "PATH",
111
- "HOME",
112
- "USER",
113
- "SHELL",
114
- "HOSTNAME"
115
- ]);
116
- const isReservedEnvName = (name) => name.startsWith("FINAL_NODE_") || RESERVED_ENV.has(name);
117
87
  const HTTP_TOKEN_LIFETIME_MS = 60 * 6e4;
118
88
  const HTTP_TOKEN_RENEW_MS = 5 * 6e4;
119
89
  const BUILD_ROOT = "/tmp/final-build";
120
- const seedsFor = (ctx) => sandboxSeedFiles({
121
- files: ctx.resources.guide == null ? ctx.files : {},
122
- resources: ctx.resources
123
- });
124
90
  const CAPTURE_SCRUBBED_PATHS = [
125
91
  "/home/user/.bash_history",
126
92
  "/home/user/.zsh_history",
@@ -139,6 +105,8 @@ const CAPTURE_SCRUBBED_PATHS = [
139
105
  "/home/user/.ssh"
140
106
  ];
141
107
  const BUILDER_IDLE_TIMEOUT_SECONDS = 2 * 60 * 60;
108
+ const ARTIFACT_KEY_VERSION = 1;
109
+ const PROJECT_ARTIFACT_CAPTURE_PROTOCOL_VERSION = 2;
142
110
  const networkPolicyFor = (network) => ({
143
111
  mode: network?.egress === ComputerEgressPolicy.Internet ? NetworkPolicy.AllowAll : network?.egress === ComputerEgressPolicy.Allowlist ? NetworkPolicy.Allowlist : NetworkPolicy.DenyAll,
144
112
  allow: network?.allow ?? []
@@ -149,34 +117,66 @@ const computerEgressOf = (policy) => {
149
117
  return { egress: ComputerEgressPolicy.Allowlist, allow: policy.allow ?? [] };
150
118
  };
151
119
  const egressFor = (ctx, requested) => computerEgressOf(withPlatformEgressHosts(ctx, requested));
152
- const LINEAGE_PREFIX = "lineage:";
153
- const portOf = (url) => {
154
- try {
155
- const parsed = new URL(url);
156
- if (parsed.port !== "") return Number(parsed.port);
157
- return parsed.protocol === "https:" ? 443 : 80;
158
- } catch {
159
- return null;
160
- }
120
+ const computerNetworkPolicyOf = (setup) => {
121
+ const network = setup?.network;
122
+ if (network == null) return { mode: NetworkPolicy.AllowAll };
123
+ if ((network.deny ?? []).length > 0)
124
+ throw computerSandboxError(
125
+ "invalid_environment",
126
+ "A Final computer cannot deny individual hosts; list the hosts to allow instead"
127
+ );
128
+ if (network.allow == null) return { mode: NetworkPolicy.AllowAll };
129
+ return network.allow.length === 0 ? { mode: NetworkPolicy.DenyAll } : { mode: NetworkPolicy.Allowlist, allow: [...network.allow] };
161
130
  };
131
+ const LINEAGE_PREFIX = "lineage:";
162
132
  const compactId = (id) => id.replaceAll("-", "");
163
- const sandboxWorkspaceOf = {
164
- [ComputerWorkspacePolicy.Session]: SandboxWorkspace.Session,
165
- [ComputerWorkspacePolicy.Conversation]: SandboxWorkspace.Conversation,
166
- [ComputerWorkspacePolicy.Ephemeral]: SandboxWorkspace.Ephemeral
167
- };
133
+ const computerNameFor = async (sandboxId) => `as-${(await sha256Hex(sandboxId)).slice(0, 24)}`;
168
134
  const isRunningState = (state) => state === ComputerState.Running || state === ComputerState.Paused || state === ComputerState.Resuming;
169
135
  const hasNothingToPause = (state) => state === ComputerState.Stopped || state === ComputerState.Shelved || state === ComputerState.Error || state === ComputerState.Deleted;
170
136
  const pauseMayHaveSettled = (error) => isComputersApiError(error) && error.statusCode != null && (error.statusCode === 404 || error.statusCode === 409 || error.statusCode >= 500);
171
137
  const RUNNING_POLL_MAX_MS = 5e3;
138
+ const computerNeedsBuild = (build) => build != null && (build.steps.length > 0 || build.verify.length > 0);
139
+ const computeComputerArtifactKey = async (inputs) => {
140
+ if (!computerNeedsBuild(inputs.build)) {
141
+ const hex2 = await sha256Hex(canonicalJson({ v: ARTIFACT_KEY_VERSION, image: inputs.image }));
142
+ return `img-${hex2.slice(0, 16)}`;
143
+ }
144
+ const hex = await sha256Hex(
145
+ canonicalJson({
146
+ v: ARTIFACT_KEY_VERSION,
147
+ image: inputs.image,
148
+ build: { steps: inputs.build?.steps ?? [], verify: inputs.build?.verify ?? [] },
149
+ diskGb: inputs.diskGb
150
+ })
151
+ );
152
+ return `pa-g${hex.slice(0, 16)}-c${PROJECT_ARTIFACT_CAPTURE_PROTOCOL_VERSION}`;
153
+ };
154
+ const offerContextOf = (ctx) => ({
155
+ organizationId: ctx.organizationId,
156
+ projectId: ctx.projectId,
157
+ sandboxId: ctx.sandboxId,
158
+ credentials: ctx.credentials,
159
+ actingUser: ctx.actingUser,
160
+ identity: ctx.identity,
161
+ platformEgressHosts: ctx.platformEgressHosts,
162
+ platformNetworkPolicy: ctx.platformNetworkPolicy
163
+ });
172
164
  const createComputerSandboxProvider = (config) => {
173
165
  const log = config.log ?? (() => void 0);
174
166
  const randomUuid = config.randomUuid ?? (() => crypto.randomUUID());
175
167
  const now = config.now ?? (() => Date.now());
176
168
  const sleep = config.sleep ?? defaultSleep;
177
- const defaultImage = config.defaultImage ?? DEFAULT_COMPUTER_IMAGE;
178
169
  const runningWaitMs = config.runningWaitMs ?? 5 * 6e4;
179
- const clientFor = (credentials) => createComputersApiClient({ config: config.computersApi ?? {}, credentials });
170
+ const credentialsFor = (ctx) => ({
171
+ computersAccess: async (args) => ({
172
+ baseUrl: config.computersApiUrl,
173
+ token: (await ctx.identity({ audience: config.computersApiUrl, refresh: args?.refresh === true })).token
174
+ })
175
+ });
176
+ const clientFor = (ctx) => createComputersApiClient({
177
+ config: config.computersApi ?? {},
178
+ credentials: credentialsFor(ctx)
179
+ });
180
180
  const registerImage = (client, reference) => client.registerImage(
181
181
  config.registryCredentialId == null ? { reference } : { reference, registryCredentialId: config.registryCredentialId }
182
182
  );
@@ -217,23 +217,6 @@ const createComputerSandboxProvider = (config) => {
217
217
  }
218
218
  return version.id;
219
219
  };
220
- const loadVariables = async (ctx, spec) => {
221
- const env = {};
222
- for (const [name, value] of Object.entries(spec.parameters.env)) {
223
- if (isReservedEnvName(name))
224
- throw computerSandboxError("invalid_environment", `${name} is reserved`);
225
- env[name] = value;
226
- }
227
- for (const name of spec.parameters.variables) {
228
- if (isReservedEnvName(name))
229
- throw computerSandboxError("invalid_environment", `${name} is reserved`);
230
- const value = await ctx.variables.get(name);
231
- if (value == null)
232
- throw computerSandboxError("invalid_environment", `variable ${name} is not set`);
233
- env[name] = value;
234
- }
235
- return env;
236
- };
237
220
  const mcpClosers = /* @__PURE__ */ new Map();
238
221
  const closeComputerMcp = async (computerId) => {
239
222
  const results = await Promise.allSettled(
@@ -254,44 +237,186 @@ const createComputerSandboxProvider = (config) => {
254
237
  );
255
238
  }
256
239
  };
240
+ const createNetworkingController = (options) => {
241
+ try {
242
+ if (config.networking == null) throw new Error("Workspace networking extension is required");
243
+ return config.networking.controller(options);
244
+ } catch (cause) {
245
+ options.onClose?.();
246
+ throw cause;
247
+ }
248
+ };
249
+ const managedWorkspaces = /* @__PURE__ */ new Set();
250
+ const savedPolicyWorkspaces = /* @__PURE__ */ new Set();
251
+ const workspaceKey = (ctx) => `${ctx.organizationId}:${ctx.sandboxId}`;
252
+ const needsWorkspaceNetworking = async (ctx, query = {}) => {
253
+ const needed = await config.networking?.enabled({
254
+ ctx,
255
+ workspaceId: ctx.sandboxId,
256
+ grant: FULL_SANDBOX_GRANT,
257
+ ...query
258
+ }) ?? false;
259
+ if (needed) managedWorkspaces.add(workspaceKey(ctx));
260
+ return needed || managedWorkspaces.has(workspaceKey(ctx));
261
+ };
262
+ const preservePolicy = async (args) => {
263
+ if (config.networking == null) throw new Error("Workspace networking extension is required");
264
+ const retained = await config.networking.prepare({
265
+ ctx: args.ctx,
266
+ workspaceId: args.ctx.sandboxId,
267
+ policy: args.policy
268
+ });
269
+ await args.ctx.platformNetworkPolicy?.(retained ?? args.policy);
270
+ savedPolicyWorkspaces.add(workspaceKey(args.ctx));
271
+ };
272
+ const teardownComputer = async (args) => {
273
+ const failures = [];
274
+ let managed = args.policySaved || managedWorkspaces.has(workspaceKey(args.ctx));
275
+ let queryFailed = false;
276
+ try {
277
+ managed = await needsWorkspaceNetworking(args.ctx, { teardown: true }) || args.policySaved;
278
+ } catch (cause) {
279
+ failures.push(cause);
280
+ queryFailed = true;
281
+ }
282
+ const results = await Promise.allSettled([
283
+ (async () => {
284
+ if (managed && args.fence) {
285
+ const client = clientFor(args.ctx);
286
+ let mayFence = args.policySaved || savedPolicyWorkspaces.has(workspaceKey(args.ctx));
287
+ try {
288
+ if (config.networking != null) {
289
+ await preservePolicy({
290
+ ctx: args.ctx,
291
+ policy: networkPolicyFor(await client.getNetworkPolicy(args.computerId))
292
+ });
293
+ mayFence = true;
294
+ }
295
+ } catch (cause) {
296
+ if (isComputersApiStatus(cause, 404)) mayFence = false;
297
+ else failures.push(cause);
298
+ }
299
+ if (mayFence) {
300
+ try {
301
+ await client.setNetworkPolicy(
302
+ args.computerId,
303
+ { egress: ComputerEgressPolicy.None },
304
+ { autoResume: false }
305
+ );
306
+ } catch (cause) {
307
+ let settled = isComputersApiStatus(cause, 404);
308
+ if (!settled && isComputersApiStatus(cause, 409)) {
309
+ try {
310
+ const latest = await client.getComputer(args.computerId);
311
+ settled = latest == null || latest.state === ComputerState.Deleted;
312
+ } catch (lookup) {
313
+ failures.push(lookup);
314
+ }
315
+ }
316
+ if (!settled) failures.push(cause);
317
+ }
318
+ }
319
+ }
320
+ await args.finish();
321
+ })(),
322
+ (async () => {
323
+ if (managed && config.networking == null)
324
+ throw new Error(
325
+ "Managed Computer teardown requires the workspace egress integration to revoke Device access"
326
+ );
327
+ if (config.networking != null && (managed || queryFailed))
328
+ await config.networking.suspend({ ctx: args.ctx, workspaceId: args.ctx.sandboxId });
329
+ })()
330
+ ]);
331
+ for (const result of results) if (result.status === "rejected") failures.push(result.reason);
332
+ if (failures.length === 1) throw failures[0];
333
+ if (failures.length > 1)
334
+ throw new AggregateError(failures, "Computer teardown and egress revocation failed");
335
+ };
336
+ const setupSession = (args) => {
337
+ const transport = createComputersApiTransport({
338
+ client: args.client,
339
+ computerId: args.info.id
340
+ });
341
+ const sandbox = createTransportSession({
342
+ id: args.info.id,
343
+ description: `setup of ${args.info.name}`,
344
+ workspaceRoot: WORKSPACE_ROOT,
345
+ transport,
346
+ baseEnv: args.env,
347
+ randomUuid,
348
+ network: {
349
+ ports: [],
350
+ getPortEndpoint: async () => {
351
+ throw new Error("setup sessions expose no ports");
352
+ },
353
+ stop: async () => void 0,
354
+ destroy: async () => void 0
355
+ }
356
+ });
357
+ return { sandbox, transport };
358
+ };
359
+ const workspaceNetworking = (args) => {
360
+ if (config.networking == null || !args.enabled) return void 0;
361
+ return createNetworkingController({
362
+ ctx: args.ctx,
363
+ sandbox: args.sandbox,
364
+ workspaceId: args.ctx.sandboxId,
365
+ grant: FULL_SANDBOX_GRANT,
366
+ onClose: args.onClose,
367
+ initialNetworkPolicy: args.initialNetworkPolicy,
368
+ namespaceExists: async () => {
369
+ const current = await args.client.getComputer(args.info.id);
370
+ return current?.state === ComputerState.Running;
371
+ },
372
+ log,
373
+ afterBlock: () => args.client.setNetworkPolicy(
374
+ args.info.id,
375
+ { egress: ComputerEgressPolicy.None },
376
+ { autoResume: false }
377
+ ),
378
+ afterGuard: (policy) => args.client.setNetworkPolicy(
379
+ args.info.id,
380
+ egressFor(args.ctx, policy ?? { mode: NetworkPolicy.DenyAll }),
381
+ { autoResume: false }
382
+ )
383
+ });
384
+ };
257
385
  const buildHandle = async (args) => {
258
- const { client, info, spec } = args;
259
- const commands = await guideCommands(spec.template);
260
- const commandHelp = commands.map(
261
- (command) => `Guide command ${command.name}: bash ${command.path}${command.description == null ? "" : ` \u2014 ${command.description}`}`
262
- ).join("\n");
386
+ const { client, info } = args;
263
387
  const transport = createComputersApiTransport({ client, computerId: info.id });
264
- const ports = [
265
- ...new Set(
266
- spec.template.services.flatMap((service) => {
267
- const port = service.ready == null ? null : portOf(service.ready.url);
268
- return port == null ? [] : [port];
269
- })
270
- )
271
- ];
272
388
  let httpToken = null;
389
+ const fenceOnStop = async () => {
390
+ if (args.networking != null)
391
+ await client.setNetworkPolicy(
392
+ info.id,
393
+ { egress: ComputerEgressPolicy.None },
394
+ { autoResume: false }
395
+ );
396
+ else if (!args.passive)
397
+ await teardownComputer({
398
+ ctx: args.ctx,
399
+ computerId: info.id,
400
+ policySaved: args.networkingManaged,
401
+ fence: true,
402
+ finish: async () => {
403
+ }
404
+ });
405
+ };
273
406
  const session = createTransportSession({
274
407
  id: info.id,
275
- description: `Final computer ${info.name}${spec.guide == null ? "" : ` (${spec.guide.title} v${spec.guide.number})`}${commands.length === 0 ? "" : `
276
- ${commandHelp}`}`,
408
+ description: `Final computer ${info.name}`,
277
409
  workspaceRoot: WORKSPACE_ROOT,
278
410
  transport,
279
411
  baseEnv: args.env,
280
412
  randomUuid,
281
413
  network: {
282
- ports,
414
+ ports: [...args.ports],
283
415
  // The computer's hostname with a preview token, which opens its HTTP ports only.
284
416
  getPortEndpoint: async ({ port, protocol }) => {
285
- const managed = args.networkingManaged === true || args.access === SandboxAccess.Read && await needsWorkspaceNetworking(
286
- args.ctx,
287
- sandboxWorkspaceId({
288
- session: args.ctx.session,
289
- workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
290
- }),
291
- { access: args.access }
292
- );
417
+ const managed = args.networkingManaged || args.passive && await needsWorkspaceNetworking(args.ctx);
293
418
  if (managed && args.networking == null)
294
- throw new Error("Managed workspace previews require an active write binding");
419
+ throw new Error("Managed workspace previews require an attached sandbox");
295
420
  const deadline = (await args.networking?.authorizeExternalAccess())?.expiresAtMs;
296
421
  if (deadline != null && deadline <= now())
297
422
  throw new Error("Refresh workspace networking before preview access");
@@ -301,7 +426,7 @@ ${commandHelp}`}`,
301
426
  const expiresAtMs = Math.min(now() + HTTP_TOKEN_LIFETIME_MS, deadline ?? Infinity);
302
427
  const token = await client.createPreviewToken({
303
428
  computerId: info.id,
304
- name: `agent sandbox ${args.ctx.session.bindingId}`,
429
+ name: `agent sandbox ${args.ctx.sandboxId}`,
305
430
  expiresAt: new Date(expiresAtMs)
306
431
  });
307
432
  httpToken = { token, expiresAtMs };
@@ -315,24 +440,7 @@ ${commandHelp}`}`,
315
440
  stop: async () => {
316
441
  const failures = [];
317
442
  try {
318
- if (args.networking != null)
319
- await client.setNetworkPolicy(
320
- info.id,
321
- { egress: ComputerEgressPolicy.None },
322
- { autoResume: false }
323
- );
324
- else if (args.access !== SandboxAccess.Read)
325
- await teardownComputer({
326
- ctx: args.ctx,
327
- computerId: info.id,
328
- computerName: info.name,
329
- workspace: spec.parameters.workspace,
330
- policySaved: args.networkingManaged === true,
331
- workspaceId: args.ctx.session.workspaceId,
332
- fence: true,
333
- finish: async () => {
334
- }
335
- });
443
+ await fenceOnStop();
336
444
  } catch (cause) {
337
445
  try {
338
446
  if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
@@ -375,14 +483,11 @@ ${commandHelp}`}`,
375
483
  if (failures.length > 1)
376
484
  throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
377
485
  };
378
- if (args.networking == null && args.access !== SandboxAccess.Read)
486
+ if (args.networking == null && !args.passive)
379
487
  await teardownComputer({
380
488
  ctx: args.ctx,
381
489
  computerId: info.id,
382
- computerName: info.name,
383
- workspace: spec.parameters.workspace,
384
- policySaved: args.networkingManaged === true,
385
- workspaceId: args.ctx.session.workspaceId,
490
+ policySaved: args.networkingManaged,
386
491
  fence: true,
387
492
  finish
388
493
  });
@@ -391,13 +496,13 @@ ${commandHelp}`}`,
391
496
  }
392
497
  });
393
498
  let computerUseInstalled = null;
394
- if (args.access !== SandboxAccess.Read) {
499
+ if (!args.passive) {
395
500
  try {
396
501
  computerUseInstalled = (await client.computerUseStatus(info.id, AbortSignal.timeout(1e3))).installed;
397
502
  } catch {
398
503
  }
399
504
  }
400
- const computerUse = args.access === SandboxAccess.Read || computerUseInstalled === false ? null : createComputerUseEndpoint({ client, computerId: info.id, cwd: WORKSPACE_ROOT });
505
+ const computerUse = args.passive || computerUseInstalled === false ? null : createComputerUseEndpoint({ client, computerId: info.id, cwd: WORKSPACE_ROOT });
401
506
  const closeMcp = async () => {
402
507
  try {
403
508
  await computerUse?.close();
@@ -424,71 +529,30 @@ ${commandHelp}`}`,
424
529
  }
425
530
  if (screenAvailable) capabilities.push(SandboxCapability.Screen);
426
531
  if (computerUse != null) capabilities.push(SandboxCapability.ComputerUse);
427
- if (args.access !== SandboxAccess.Read)
532
+ if (!args.passive)
428
533
  capabilities.push(
429
534
  SandboxCapability.Process,
430
535
  SandboxCapability.DurableProcesses,
431
536
  SandboxCapability.NetworkPolicy
432
537
  );
433
- if (args.access === SandboxAccess.Write && args.networking != null)
538
+ if (!args.passive && args.networking != null)
434
539
  capabilities.push(...config.networking?.capabilities ?? []);
435
- const description = {
436
- provider: "computer",
437
- computerId: info.id,
438
- computerName: info.name,
439
- workspaceRoot: WORKSPACE_ROOT,
440
- transport: transport.kind,
441
- guide: spec.guide == null ? null : { slug: spec.guide.slug, number: spec.guide.number },
442
- repositories: spec.parameters.repositories.map((repository) => ({
443
- alias: repository.alias,
444
- path: repositoryPath(repository.alias),
445
- repository: repository.repository
446
- })),
447
- services: spec.template.services.map((service) => ({
448
- name: service.name,
449
- readyUrl: service.ready?.url ?? null
450
- })),
451
- commands,
452
- capabilities
453
- };
454
540
  const full = {
455
541
  ...session,
456
542
  setNetworkPolicy: async (policy) => {
457
- if (args.access === SandboxAccess.Read)
458
- throw new Error("this sandbox binding is read-only");
543
+ if (args.passive) throw new Error("this sandbox handle is read-only");
459
544
  await client.setNetworkPolicy(info.id, egressFor(args.ctx, policy));
460
545
  },
461
- describe: () => description,
462
546
  capabilities
463
547
  };
464
548
  if (computerUse != null) full.computerUse = async () => computerUse.endpoint;
465
- if (screenAvailable) {
466
- full.screen = (options) => client.openScreen(info.id, options);
467
- }
468
- const sandbox = full;
549
+ if (screenAvailable) full.screen = (options) => client.openScreen(info.id, options);
469
550
  let handle = {
470
- sandbox,
551
+ sandbox: args.passive ? readOnlySession(full) : full,
471
552
  onSandboxStop: async () => {
472
553
  const failures = [];
473
554
  try {
474
- if (args.networking != null)
475
- await client.setNetworkPolicy(
476
- info.id,
477
- { egress: ComputerEgressPolicy.None },
478
- { autoResume: false }
479
- );
480
- else
481
- await teardownComputer({
482
- ctx: args.ctx,
483
- computerId: info.id,
484
- computerName: info.name,
485
- workspace: spec.parameters.workspace,
486
- policySaved: args.networkingManaged === true,
487
- workspaceId: args.ctx.session.workspaceId,
488
- fence: true,
489
- finish: async () => {
490
- }
491
- });
555
+ await fenceOnStop();
492
556
  } catch (cause) {
493
557
  try {
494
558
  if (!await deletedAfterFenceError({ client, computerId: info.id, cause }))
@@ -531,10 +595,7 @@ ${commandHelp}`}`,
531
595
  await teardownComputer({
532
596
  ctx: args.ctx,
533
597
  computerId: info.id,
534
- computerName: info.name,
535
- workspace: spec.parameters.workspace,
536
- policySaved: args.networkingManaged === true,
537
- workspaceId: args.ctx.session.workspaceId,
598
+ policySaved: args.networkingManaged,
538
599
  fence: true,
539
600
  finish
540
601
  });
@@ -549,11 +610,12 @@ ${commandHelp}`}`,
549
610
  }
550
611
  }
551
612
  };
613
+ if (args.passive) return handle;
552
614
  if (args.networking != null)
553
615
  handle = await withSandboxNetworking({
554
616
  handle,
555
617
  controller: args.networking,
556
- access: args.access,
618
+ grant: FULL_SANDBOX_GRANT,
557
619
  initialized: true,
558
620
  fetch: config.networking?.fetch,
559
621
  executionPolicy: args.ctx.executionPolicy,
@@ -562,220 +624,17 @@ ${commandHelp}`}`,
562
624
  else if (config.networking != null)
563
625
  handle = await withSandboxNetworking({
564
626
  handle,
565
- access: args.access,
566
- fetch: config.networking?.fetch,
627
+ grant: FULL_SANDBOX_GRANT,
628
+ fetch: config.networking.fetch,
567
629
  executionPolicy: args.ctx.executionPolicy,
568
630
  admission: async () => {
569
- if (await needsWorkspaceNetworking(
570
- args.ctx,
571
- sandboxWorkspaceId({
572
- session: args.ctx.session,
573
- workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
574
- }),
575
- { access: args.access }
576
- ))
631
+ if (await needsWorkspaceNetworking(args.ctx))
577
632
  throw new Error(
578
633
  "Workspace networking changed; reconnect this sandbox before running tools"
579
634
  );
580
635
  }
581
636
  });
582
- return {
583
- handle: mapSandboxHandle({
584
- handle,
585
- mapSandbox: (sandbox2) => sessionForAccess(sandbox2, args.access)
586
- })
587
- };
588
- };
589
- const setupSession = (args) => {
590
- const transport = createComputersApiTransport({
591
- client: args.client,
592
- computerId: args.info.id
593
- });
594
- const sandbox = createTransportSession({
595
- id: args.info.id,
596
- description: `setup of ${args.info.name}`,
597
- workspaceRoot: WORKSPACE_ROOT,
598
- transport,
599
- baseEnv: args.env,
600
- randomUuid,
601
- network: {
602
- ports: [],
603
- getPortEndpoint: async () => {
604
- throw new Error("setup sessions expose no ports");
605
- },
606
- stop: async () => void 0,
607
- destroy: async () => void 0
608
- }
609
- });
610
- return { sandbox, transport };
611
- };
612
- const createNetworkingController = (options) => {
613
- try {
614
- if (config.networking == null) throw new Error("Workspace networking extension is required");
615
- return config.networking.controller(options);
616
- } catch (cause) {
617
- options.onClose?.();
618
- throw cause;
619
- }
620
- };
621
- const workspaceNetworking = (args) => {
622
- if (config.networking == null || !args.enabled) return void 0;
623
- return createNetworkingController({
624
- ctx: {
625
- platformEgressHosts: args.ctx.platformEgressHosts,
626
- platformNetworkPolicy: args.ctx.platformNetworkPolicy,
627
- organizationId: args.ctx.session.organizationId,
628
- credentials: args.ctx.credentials,
629
- actingUser: args.ctx.actingUser,
630
- bindings: args.ctx.bindings
631
- },
632
- sandbox: args.sandbox,
633
- workspaceId: sandboxWorkspaceId({
634
- session: args.ctx.session,
635
- workspace: sandboxWorkspaceOf[args.spec.parameters.workspace]
636
- }),
637
- access: args.access,
638
- onClose: args.onClose,
639
- initialNetworkPolicy: args.initialNetworkPolicy,
640
- namespaceExists: async () => {
641
- const current = await args.client.getComputer(args.info.id);
642
- return current?.state === ComputerState.Running;
643
- },
644
- log,
645
- afterBlock: () => args.client.setNetworkPolicy(
646
- args.info.id,
647
- { egress: ComputerEgressPolicy.None },
648
- { autoResume: false }
649
- ),
650
- afterGuard: (policy) => args.client.setNetworkPolicy(
651
- args.info.id,
652
- egressFor(args.ctx, policy ?? { mode: NetworkPolicy.DenyAll }),
653
- { autoResume: false }
654
- )
655
- });
656
- };
657
- const managedWorkspaces = /* @__PURE__ */ new Set();
658
- const savedPolicyWorkspaces = /* @__PURE__ */ new Set();
659
- const preservePolicy = async (args) => {
660
- if (config.networking == null) throw new Error("Workspace networking extension is required");
661
- const retained = await config.networking.prepare({
662
- ctx: {
663
- platformEgressHosts: args.ctx.platformEgressHosts,
664
- platformNetworkPolicy: args.ctx.platformNetworkPolicy,
665
- organizationId: args.ctx.session.organizationId,
666
- credentials: args.ctx.credentials,
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
- credentials: ctx.credentials,
682
- actingUser: ctx.actingUser,
683
- bindings: ctx.bindings
684
- },
685
- workspaceId,
686
- access: SandboxAccess.Write,
687
- ...query
688
- }) ?? false;
689
- if (needed) managedWorkspaces.add(ctx.session.organizationId + ":" + workspaceId);
690
- return needed || managedWorkspaces.has(ctx.session.organizationId + ":" + workspaceId);
691
- };
692
- const teardownComputer = async (args) => {
693
- let workspaceId;
694
- try {
695
- workspaceId = args.workspaceId ?? sandboxWorkspaceId({
696
- session: args.ctx.session,
697
- workspace: sandboxWorkspaceOf[args.workspace]
698
- });
699
- } catch (cause) {
700
- try {
701
- await args.finish();
702
- } catch (cleanup) {
703
- throw new AggregateError([cause, cleanup], "Computer identity and teardown failed");
704
- }
705
- throw cause;
706
- }
707
- const failures = [];
708
- let managed = args.policySaved || managedWorkspaces.has(args.ctx.session.organizationId + ":" + workspaceId);
709
- let queryFailed = false;
710
- try {
711
- managed = await needsWorkspaceNetworking(args.ctx, workspaceId, { teardown: true }) || args.policySaved;
712
- } catch (cause) {
713
- failures.push(cause);
714
- queryFailed = true;
715
- }
716
- const results = await Promise.allSettled([
717
- (async () => {
718
- if (managed && args.fence) {
719
- const client = clientFor(args.ctx.credentials);
720
- let mayFence = args.policySaved || savedPolicyWorkspaces.has(args.ctx.session.organizationId + ":" + workspaceId);
721
- try {
722
- if (config.networking != null) {
723
- await preservePolicy({
724
- ctx: args.ctx,
725
- workspaceId,
726
- policy: networkPolicyFor(await client.getNetworkPolicy(args.computerId))
727
- });
728
- mayFence = true;
729
- }
730
- } catch (cause) {
731
- if (isComputersApiStatus(cause, 404)) mayFence = false;
732
- else failures.push(cause);
733
- }
734
- if (mayFence) {
735
- try {
736
- await client.setNetworkPolicy(
737
- args.computerId,
738
- { egress: ComputerEgressPolicy.None },
739
- { autoResume: false }
740
- );
741
- } catch (cause) {
742
- let settled = isComputersApiStatus(cause, 404);
743
- if (!settled && isComputersApiStatus(cause, 409)) {
744
- try {
745
- const latest = await client.getComputer(args.computerId);
746
- settled = latest == null || latest.state === ComputerState.Deleted;
747
- } catch (lookup) {
748
- failures.push(lookup);
749
- }
750
- }
751
- if (!settled) failures.push(cause);
752
- }
753
- }
754
- }
755
- await args.finish();
756
- })(),
757
- (async () => {
758
- if (managed && config.networking == null)
759
- throw new Error(
760
- "Managed Computer teardown requires the workspace egress integration to revoke Device access"
761
- );
762
- if (config.networking != null && (managed || queryFailed))
763
- await config.networking.suspend({
764
- ctx: {
765
- organizationId: args.ctx.session.organizationId,
766
- credentials: args.ctx.credentials,
767
- actingUser: args.ctx.actingUser,
768
- bindings: args.ctx.bindings
769
- },
770
- workspaceId
771
- });
772
- })()
773
- ]);
774
- for (const result of results) if (result.status === "rejected") failures.push(result.reason);
775
- if (failures.length === 1) throw failures[0];
776
- if (failures.length > 1)
777
- throw new AggregateError(failures, "Computer teardown and egress revocation failed");
778
- return workspaceId;
637
+ return handle;
779
638
  };
780
639
  const awaitBase = async (client, baseId, key) => {
781
640
  const deadline = now() + (config.buildWaitMs ?? 30 * 6e4);
@@ -821,29 +680,18 @@ ${commandHelp}`}`,
821
680
  pollMs = Math.min(pollMs * 2, RUNNING_POLL_MAX_MS);
822
681
  }
823
682
  };
824
- const awaitShelved = async (client, computerId) => {
825
- const deadline = now() + (config.shelveWaitMs ?? 5 * 6e4);
826
- for (; ; ) {
827
- const computer = await client.getComputer(computerId);
828
- if (computer == null || computer.state === ComputerState.Shelved || computer.state === ComputerState.Deleted)
829
- return;
830
- if (computer.state === ComputerState.Error)
831
- throw computerSandboxError("computer_gone", `computer ${computerId} failed to shelve`);
832
- if (now() >= deadline) throw new Error(`computer ${computerId} is still shelving`);
833
- await sleep(config.shelvePollMs ?? 2e3);
834
- }
835
- };
836
- const build = async (ctx, spec, key) => {
837
- const client = clientFor(ctx.credentials);
838
- const diskGb = config.machine?.diskGb ?? null;
683
+ const build = async (args) => {
684
+ const { ctx, key } = args;
685
+ const client = clientFor(ctx);
686
+ const diskGb = args.machine?.diskGb ?? null;
839
687
  const imageVersionId = await readyImageVersion(client, {
840
- image: spec.image,
688
+ image: args.image,
841
689
  imageVersionId: null
842
690
  });
843
691
  const body = {
844
692
  name: `ag-${key.slice(4, 20)}-${compactId(randomUuid()).slice(0, 8)}`,
845
693
  imageVersionId,
846
- ...config.machine,
694
+ ...args.machine,
847
695
  // The base is the root disk; a user volume would keep /home and /workspaces off it.
848
696
  userVolumeGb: null,
849
697
  // Nothing secret at create: the create env lands in the guest.
@@ -854,7 +702,7 @@ ${commandHelp}`}`,
854
702
  idleTimeoutSeconds: BUILDER_IDLE_TIMEOUT_SECONDS,
855
703
  tags: ["final-agent-sandbox-build"]
856
704
  };
857
- if (config.regionSlug != null) body.regionSlug = config.regionSlug;
705
+ if (args.region != null) body.regionSlug = args.region;
858
706
  const builder = await client.createComputer(
859
707
  body,
860
708
  `agent-guide-build-${(await sha256Hex(`${key}:${randomUuid()}`)).slice(0, 32)}`
@@ -873,8 +721,8 @@ ${homeBaselineScript()}`],
873
721
  throw computerSandboxError("build_failed", "failed to record the pre-build home baseline");
874
722
  }
875
723
  const steps = [
876
- ...spec.template.build.steps.map((step) => ({ step, label: "build step" })),
877
- ...spec.template.build.verify.map((step) => ({ step, label: "verify step" }))
724
+ ...args.build.steps.map((step) => ({ step, label: "build step" })),
725
+ ...args.build.verify.map((step) => ({ step, label: "verify step" }))
878
726
  ];
879
727
  for (const [index, { step, label }] of steps.entries()) {
880
728
  ctx.log?.(`${label} ${index + 1}/${steps.length}`);
@@ -921,7 +769,7 @@ ${step.run}`,
921
769
  return {
922
770
  version: 1,
923
771
  key,
924
- image: spec.image,
772
+ image: args.image,
925
773
  diskGb: diskGb ?? base.diskGb,
926
774
  seedFiles: {},
927
775
  base: { kind: "captured", baseId: base.id }
@@ -946,72 +794,87 @@ ${step.run}`,
946
794
  );
947
795
  }
948
796
  const options = parsed.data;
949
- const resolve = (guides) => resolveComputerSpec({ options, guides, defaultImage });
950
- const keyFor = async (spec, files) => {
797
+ const image = options.image ?? config.defaultImage ?? DEFAULT_COMPUTER_IMAGE;
798
+ const machine = options.machine ?? config.machine;
799
+ const region = options.region ?? config.regionSlug ?? null;
800
+ const idleTimeoutFor = (ctx) => options.idleTimeoutSeconds ?? (ctx.lifecycle.parkAfterIdleMs == null ? DEFAULT_IDLE_TIMEOUT_SECONDS : Math.ceil(ctx.lifecycle.parkAfterIdleMs / 1e3) + IDLE_TIMEOUT_MARGIN_SECONDS);
801
+ const seedsFor = (ctx) => sandboxSeedFiles({ files: ctx.files, resources: ctx.resources });
802
+ const keyFor = async (files) => {
951
803
  const baseKey = await computeComputerArtifactKey({
952
- image: spec.image,
953
- template: spec.template,
954
- diskGb: config.machine?.diskGb ?? null
804
+ image,
805
+ build: options.build,
806
+ diskGb: machine?.diskGb ?? null
955
807
  });
956
808
  return Object.keys(files).length === 0 ? baseKey : `${baseKey}-s${await sha256Hex(sandboxSeedIdentity(files))}`;
957
809
  };
958
- const startServicesFor = async (args) => {
959
- await startServices({
960
- sandbox: args.setup.sandbox,
961
- transport: args.setup.transport,
962
- template: args.spec.template,
963
- // Services run for the computer's life, with the platform's values as they are now.
964
- env: await args.env(),
965
- randomUuid,
966
- log
967
- });
810
+ const retainedBase = async (ctx, baseKey) => {
811
+ const cached = computerArtifactSchema.safeParse(await ctx.artifacts?.get(baseKey));
812
+ if (!cached.success || cached.data.key !== baseKey || cached.data.base.kind !== "captured")
813
+ return null;
814
+ const client = clientFor(ctx);
815
+ const base = await client.getBase(cached.data.base.baseId);
816
+ if (base?.state !== ComputerBaseState.Ready) return null;
817
+ if (!base.retained && !(await client.setBaseRetention(base.id, true)).retained)
818
+ throw computerSandboxError(
819
+ "build_failed",
820
+ "the Computers API did not retain the prepared base"
821
+ );
822
+ return cached.data;
823
+ };
824
+ const stateOf = (raw) => {
825
+ const state = computerStateSchema.safeParse(raw);
826
+ if (!state.success)
827
+ throw computerSandboxError(
828
+ "computer_gone",
829
+ "this sandbox was recorded by an older computer provider; create it again"
830
+ );
831
+ return state.data;
832
+ };
833
+ const findComputer = async (client, state) => {
834
+ const found = await client.getComputer(state.computerId ?? state.computerName);
835
+ return found == null || found.state === ComputerState.Deleted ? null : found;
968
836
  };
969
- const awaitRunning = (ctx, rawState, options2) => awaitComputerRunning(
970
- clientFor(ctx.credentials),
971
- computerStateSchema.pick({ computerId: true }).parse(rawState).computerId,
972
- options2.timeoutMs
973
- );
974
837
  return {
838
+ capabilities: async () => ({
839
+ [Capability.Files]: FilesAccess.Write,
840
+ [Capability.Shell]: ShellAccess.Execute,
841
+ [Capability.ComputerUse]: ScreenAccess.Control
842
+ }),
843
+ // Screen and computer-use availability is known only after attaching the actual Computer.
844
+ additionalFeatures: async () => config.networking?.capabilities.slice() ?? [],
975
845
  processes: async (ctx, rawState, request) => {
976
- const state = computerStateSchema.parse(rawState);
977
- if (request.operation === ProcessInspectionOperation.Stop && state.access === SandboxAccess.Read)
978
- throw new Error("stopping processes requires execute access");
979
- const result = await clientFor(ctx.credentials).inspectProcesses(state.computerId, request);
980
- return request.operation === ProcessInspectionOperation.Available && "available" in result ? { ...result, canStop: state.access !== SandboxAccess.Read } : result;
846
+ const state = stateOf(rawState);
847
+ if (state.computerId == null) throw new Error("this computer has not started yet");
848
+ return clientFor(ctx).inspectProcesses(state.computerId, request);
981
849
  },
982
850
  editor: async (ctx, rawState, request) => {
983
- const state = computerStateSchema.parse(rawState);
984
- if (state.access !== SandboxAccess.Write) {
851
+ const state = stateOf(rawState);
852
+ const computerId = state.computerId;
853
+ if (computerId == null) {
985
854
  if (request.operation === "available") return { available: false };
986
- throw new Error("editor requires write access");
855
+ throw new Error("this computer has not started yet");
987
856
  }
988
- const client = clientFor(ctx.credentials);
857
+ const client = clientFor(ctx);
989
858
  const open = (expiresAtMs) => computerEditor({
990
859
  client,
991
- computerId: state.computerId,
860
+ computerId,
992
861
  request,
993
862
  randomUuid,
994
863
  expiresAtMs
995
864
  });
996
865
  if (request.operation === "available" || request.operation === "close") return open();
997
- const workspaceId = ctx.workspaceId ?? state.workspaceId;
998
- if (state.networkPolicySaved === true && ctx.workspaceId != null && state.workspaceId != null && ctx.workspaceId !== state.workspaceId)
999
- throw new Error("Editor workspace identity does not match its retained provider state");
1000
866
  if (config.networking == null) {
1001
- if (state.networkPolicySaved === true)
867
+ if (state.networkPolicySaved)
1002
868
  throw new Error("Device networking control is required before opening this editor");
1003
869
  return open();
1004
870
  }
1005
- if (workspaceId == null)
1006
- throw new Error(
1007
- "The host must supply logical workspace identity before opening an egress-aware editor"
1008
- );
1009
- const managedKey = ctx.organizationId + ":" + workspaceId;
1010
- const managed = state.networkPolicySaved === true || managedWorkspaces.has(managedKey) || await config.networking.enabled({ ctx, workspaceId, access: SandboxAccess.Write });
871
+ if (ctx.sandboxId == null)
872
+ throw new Error("The host must name the sandbox before opening an egress-aware editor");
873
+ const networkingCtx = { ...ctx, sandboxId: ctx.sandboxId };
874
+ const managed = state.networkPolicySaved || await needsWorkspaceNetworking(networkingCtx);
1011
875
  if (!managed) return open();
1012
- managedWorkspaces.add(managedKey);
1013
- config.networking.validateWorkspaceId?.(workspaceId);
1014
- const info = await client.getComputer(state.computerId);
876
+ config.networking.validateWorkspaceId?.(ctx.sandboxId);
877
+ const info = await client.getComputer(computerId);
1015
878
  if (info == null || info.state !== ComputerState.Running)
1016
879
  throw new Error("Computer must be running before editor access");
1017
880
  const initialNetworkPolicy = networkPolicyFor(await client.getNetworkPolicy(info.id));
@@ -1019,9 +882,9 @@ ${step.run}`,
1019
882
  const manager = createNetworkingController({
1020
883
  sandbox: setup.sandbox,
1021
884
  onClose: () => setup.transport.close(),
1022
- workspaceId,
885
+ workspaceId: ctx.sandboxId,
1023
886
  ctx,
1024
- access: SandboxAccess.Write,
887
+ grant: FULL_SANDBOX_GRANT,
1025
888
  initialNetworkPolicy,
1026
889
  afterBlock: () => client.setNetworkPolicy(
1027
890
  info.id,
@@ -1047,138 +910,116 @@ ${step.run}`,
1047
910
  return open(deadline ?? void 0);
1048
911
  },
1049
912
  readSnapshot: async (ctx, rawState, request) => {
1050
- const state = computerStateSchema.parse(rawState);
913
+ const state = stateOf(rawState);
914
+ if (state.computerId == null) return null;
1051
915
  return readComputerSnapshot({
1052
- client: clientFor(ctx.credentials),
916
+ client: clientFor(ctx),
1053
917
  computerId: state.computerId,
1054
918
  request
1055
919
  });
1056
920
  },
1057
- // The merged parameters' workspace: the binding's, else the saved version's default.
1058
- workspace: async (ctx) => sandboxWorkspaceOf[(await resolve(ctx.guides)).parameters.workspace],
1059
- // Screen and computer-use availability is known only after attaching the actual Computer.
1060
- additionalCapabilities: async () => config.networking?.capabilities.slice() ?? [],
1061
- artifactKey: async (ctx) => keyFor(await resolve(ctx.guides), seedsFor(ctx)),
1062
- // Registers an image reference so its version builds ahead of the first start;
1063
- // a lineage needs nothing until then.
921
+ artifactKey: async (ctx) => keyFor(seedsFor(ctx)),
922
+ // Builds the base an environment with build steps needs, or registers an
923
+ // image reference so its version builds ahead of the first attach (a
924
+ // lineage needs nothing until then).
1064
925
  prepare: async (ctx) => {
1065
- const spec = await resolve(ctx.guides);
1066
926
  const seedFiles = seedsFor(ctx);
1067
- const key = await keyFor(spec, seedFiles);
927
+ const key = await keyFor(seedFiles);
1068
928
  const seedArtifact = await sandboxSeedArtifact(ctx, seedFiles);
1069
- if (computerNeedsBuild(spec)) {
1070
- const baseKey = await keyFor(spec, {});
1071
- const prepared = spec.guide?.preparedArtifact;
1072
- if (prepared?.key === baseKey && prepared.providerRef === formatProviderRef(packageRef(COMPUTER_SANDBOX_SPECIFIER))) {
1073
- const cached2 = computerArtifactSchema.safeParse(prepared.artifact);
1074
- if (cached2.success && cached2.data.key === baseKey && cached2.data.base.kind === "captured") {
1075
- const base = await clientFor(ctx.credentials).getBase(cached2.data.base.baseId);
1076
- if (base?.state === ComputerBaseState.Ready) {
1077
- if (!base.retained) {
1078
- const retained = await clientFor(ctx.credentials).setBaseRetention(base.id, true);
1079
- if (!retained.retained)
1080
- throw computerSandboxError(
1081
- "build_failed",
1082
- "the Computers API did not retain the prepared base"
1083
- );
1084
- }
1085
- return { ...cached2.data, key, ...seedArtifact };
1086
- }
1087
- }
1088
- }
1089
- const cached = computerArtifactSchema.safeParse(await ctx.artifacts?.get(baseKey));
1090
- if (cached.success && cached.data.key === baseKey && cached.data.base.kind === "captured") {
1091
- const base = await clientFor(ctx.credentials).getBase(cached.data.base.baseId);
1092
- if (base?.state === ComputerBaseState.Ready) {
1093
- if (!base.retained) {
1094
- const retained = await clientFor(ctx.credentials).setBaseRetention(base.id, true);
1095
- if (!retained.retained)
1096
- throw computerSandboxError(
1097
- "build_failed",
1098
- "the Computers API did not retain the prepared base"
1099
- );
1100
- }
1101
- return { ...cached.data, key, ...seedArtifact };
1102
- }
1103
- }
1104
- const built = await build(ctx, spec, baseKey);
929
+ if (options.build != null && computerNeedsBuild(options.build)) {
930
+ const baseKey = await keyFor({});
931
+ const retained = await retainedBase(ctx, baseKey);
932
+ if (retained != null) return { ...retained, key, ...seedArtifact };
933
+ const built = await build({
934
+ ctx,
935
+ image,
936
+ build: options.build,
937
+ machine,
938
+ region,
939
+ key: baseKey
940
+ });
1105
941
  await ctx.artifacts?.put(baseKey, built);
1106
942
  return { ...built, key, ...seedArtifact };
1107
943
  }
1108
- const imageVersionId = spec.image.startsWith(LINEAGE_PREFIX) ? null : (await registerImage(clientFor(ctx.credentials), spec.image)).id;
1109
- ctx.log?.(`computer base ${key} is image ${spec.image}`);
944
+ const imageVersionId = image.startsWith(LINEAGE_PREFIX) ? null : (await registerImage(clientFor(ctx), image)).id;
945
+ ctx.log?.(`computer base ${key} is image ${image}`);
1110
946
  return {
1111
947
  version: 1,
1112
948
  key,
1113
- image: spec.image,
1114
- diskGb: config.machine?.diskGb ?? null,
949
+ image,
950
+ diskGb: machine?.diskGb ?? null,
1115
951
  ...seedArtifact,
1116
- base: { kind: "image", image: spec.image, imageVersionId }
952
+ base: { kind: "image", image, imageVersionId }
1117
953
  };
1118
954
  },
1119
- awaitRunning: async (ctx, state, options2) => {
1120
- await awaitRunning(ctx, state, options2);
955
+ // Records the computer; the first attach creates it. The setup's network
956
+ // policy is checked now so a policy a computer cannot enforce never boots.
957
+ create: async (ctx, { setup }) => {
958
+ const network = computerNetworkPolicyOf(setup);
959
+ const ports = [
960
+ ...new Set(
961
+ (setup?.services ?? []).flatMap(
962
+ (service) => service.port == null ? [] : [service.port]
963
+ )
964
+ )
965
+ ];
966
+ return {
967
+ state: {
968
+ version: 2,
969
+ computerName: await computerNameFor(ctx.sandboxId),
970
+ computerId: null,
971
+ network,
972
+ ports,
973
+ networkPolicySaved: false
974
+ },
975
+ region
976
+ };
1121
977
  },
1122
- start: async (ctx, openOptions, rawArtifact) => {
1123
- const access = computerOpenOptionsSchema.parse(openOptions ?? {}).access;
1124
- const spec = await resolve(ctx.guides);
1125
- const policy = withPlatformEgressHosts(ctx, spec.parameters.networkPolicy);
1126
- const egress = computerEgressOf(policy);
978
+ awaitRunning: async (ctx, rawState, options2) => {
979
+ const state = stateOf(rawState);
980
+ await awaitComputerRunning(
981
+ clientFor(ctx),
982
+ state.computerId ?? state.computerName,
983
+ options2.timeoutMs
984
+ );
985
+ },
986
+ attach: async (ctx, { artifact: rawArtifact, state: rawState }) => {
987
+ const state = stateOf(rawState);
1127
988
  const artifact = computerArtifactSchema.safeParse(rawArtifact);
1128
- if (!artifact.success) {
989
+ if (!artifact.success)
1129
990
  throw computerSandboxError(
1130
991
  "artifact_unavailable",
1131
- "this computer base predates the public Computers API provider; prepare it again"
992
+ "this computer base predates the current computer provider; prepare it again"
1132
993
  );
1133
- }
1134
994
  const base = artifact.data.base;
1135
- if (computerNeedsBuild(spec) && base.kind !== "captured") {
995
+ if (computerNeedsBuild(options.build) && base.kind !== "captured")
1136
996
  throw computerSandboxError(
1137
997
  "artifact_unavailable",
1138
998
  `computer base ${artifact.data.key} was prepared without its build steps; prepare it again`
1139
999
  );
1140
- }
1141
- const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
1142
- const client = clientFor(ctx.credentials);
1143
- const workspace = spec.parameters.workspace;
1144
- const name = await computerWorkspaceName({
1145
- ...ctx.session,
1146
- conversationWorkspace: workspace === ComputerWorkspacePolicy.Conversation
1147
- });
1148
- const workspaceId = sandboxWorkspaceId({
1149
- session: ctx.session,
1150
- workspace: sandboxWorkspaceOf[workspace]
1151
- });
1152
- const manageNetworking = await needsWorkspaceNetworking(ctx, workspaceId) || access !== SandboxAccess.Read && await needsWorkspaceNetworking(ctx, workspaceId, { access });
1153
- if (manageNetworking)
1154
- config.networking?.validateWorkspaceId?.(
1155
- sandboxWorkspaceId({ session: ctx.session, workspace: sandboxWorkspaceOf[workspace] })
1156
- );
1157
- const existing = await client.getComputer(name);
1000
+ const networkingCtx = offerContextOf(ctx);
1001
+ const client = clientFor(ctx);
1002
+ const env = platformProcessEnvironment(ctx, {});
1003
+ const policy = withPlatformEgressHosts(ctx, state.network);
1004
+ const egress = computerEgressOf(policy);
1005
+ const manageNetworking = state.networkPolicySaved || await needsWorkspaceNetworking(networkingCtx);
1006
+ if (manageNetworking) config.networking?.validateWorkspaceId?.(ctx.sandboxId);
1007
+ const existing = await findComputer(client, state);
1158
1008
  if (existing?.state === ComputerState.Error)
1159
1009
  throw computerSandboxError(
1160
1010
  "computer_gone",
1161
- `Computer ${existing.id} (${name}) is in the Error state. Recover it through the Computers API before retrying; its workspace is preserved.`
1011
+ `Computer ${existing.id} (${existing.name}) is in the Error state. Recover it through the Computers API before retrying; its disk is preserved.`
1162
1012
  );
1163
- if (manageNetworking && access === SandboxAccess.Read && existing != null && existing.state !== ComputerState.Deleted && existing.state !== ComputerState.Running)
1164
- throw new Error("A managed Computer requires a write binding to resume networking");
1165
- const initialNetworkPolicy = manageNetworking && existing != null && existing.state !== ComputerState.Deleted ? networkPolicyFor(await client.getNetworkPolicy(existing.id)) : networkPolicyFor(egress);
1166
- if (manageNetworking && access !== SandboxAccess.Read && existing?.state === ComputerState.Running)
1013
+ const initialNetworkPolicy = manageNetworking && existing != null ? networkPolicyFor(await client.getNetworkPolicy(existing.id)) : networkPolicyFor(egress);
1014
+ if (manageNetworking && existing?.state === ComputerState.Running)
1167
1015
  await client.disableAutoResume(existing.id);
1168
1016
  if (manageNetworking && config.networking != null)
1169
- await preservePolicy({
1170
- ctx,
1171
- workspaceId: sandboxWorkspaceId({
1172
- session: ctx.session,
1173
- workspace: sandboxWorkspaceOf[workspace]
1174
- }),
1175
- policy: initialNetworkPolicy
1176
- });
1017
+ await preservePolicy({ ctx: networkingCtx, policy: initialNetworkPolicy });
1177
1018
  let info;
1178
- let fresh = true;
1179
- if (existing != null && existing.state !== ComputerState.Deleted) {
1019
+ let booted;
1020
+ if (existing != null) {
1180
1021
  info = existing;
1181
- fresh = !isRunningState(existing.state);
1022
+ booted = !isRunningState(existing.state);
1182
1023
  if (manageNetworking && existing.state !== ComputerState.Running)
1183
1024
  await client.setNetworkPolicy(
1184
1025
  existing.id,
@@ -1186,101 +1027,76 @@ ${step.run}`,
1186
1027
  { autoResume: false }
1187
1028
  );
1188
1029
  if (existing.state === ComputerState.Paused) await client.action(existing.id, "resume");
1189
- else if (fresh) await client.action(existing.id, "start");
1030
+ else if (booted) await client.action(existing.id, "start");
1190
1031
  } else {
1032
+ booted = true;
1191
1033
  const source = base.kind === "captured" ? { baseId: (await awaitBase(client, base.baseId, artifact.data.key)).id } : { imageVersionId: await readyImageVersion(client, base) };
1192
1034
  const body = {
1193
- name,
1035
+ name: state.computerName,
1194
1036
  ...source,
1195
- ...config.machine,
1037
+ ...machine,
1196
1038
  env: {},
1197
1039
  autoResume: !manageNetworking,
1198
- idleTimeoutSeconds: config.idleTimeoutSeconds ?? 15 * 60,
1199
- // Provider teardown owns deletion; VM-level ephemeral disks cannot be paused.
1040
+ idleTimeoutSeconds: idleTimeoutFor(ctx),
1041
+ // The sandbox owns deletion; VM-level ephemeral disks cannot be paused.
1200
1042
  ephemeral: false,
1201
1043
  network: manageNetworking ? { egress: ComputerEgressPolicy.None } : egress,
1202
1044
  tags: [
1203
1045
  "final-agent-sandbox",
1204
- `agent-binding:${ctx.session.bindingId}`,
1046
+ `agent-sandbox:${ctx.sandboxId}`,
1205
1047
  "agent-runtime-attachment:v1",
1206
1048
  "agent-runtime-role:sandbox"
1207
1049
  ]
1208
1050
  };
1209
1051
  body.idlePolicy = ComputerIdlePolicy.Pause;
1210
- if (config.regionSlug != null) body.regionSlug = config.regionSlug;
1211
- const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.session.bindingId}:${name}:${source.baseId ?? source.imageVersionId}:${manageNetworking}`)).slice(0, 32)}`;
1052
+ if (region != null) body.regionSlug = region;
1053
+ const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.sandboxId}:${state.computerName}:${state.computerId ?? "first"}:${source.baseId ?? source.imageVersionId}:${manageNetworking}`)).slice(0, 32)}`;
1212
1054
  info = await client.createComputer(body, idempotencyKey);
1213
1055
  }
1214
- const reused = existing != null && existing.id === info.id;
1215
- log("computer_sandbox.started", { computerId: info.id, reused });
1216
- let cleanupSetup = null;
1056
+ const fresh = existing == null;
1057
+ log("computer_sandbox.attached", { computerId: info.id, fresh, booted });
1058
+ let cleanup = null;
1217
1059
  try {
1218
- info = await awaitRunning(ctx, { computerId: info.id }, { timeoutMs: runningWaitMs });
1060
+ info = await awaitComputerRunning(client, info.id, runningWaitMs);
1219
1061
  if (!manageNetworking) {
1220
- if (reused) await client.setNetworkPolicy(info.id, egress);
1062
+ if (!fresh) await client.setNetworkPolicy(info.id, egress);
1221
1063
  await reportPlatformNetworkPolicy(ctx, policy);
1222
1064
  }
1223
1065
  const setup = setupSession({ client, info, env });
1224
- cleanupSetup = async () => setup.transport.close();
1225
- if (!manageNetworking || access !== SandboxAccess.Read)
1226
- await writePlatformFiles(ctx, setup.sandbox);
1066
+ cleanup = async () => setup.transport.close();
1067
+ await writePlatformFiles(ctx, setup.sandbox);
1227
1068
  const manager = workspaceNetworking({
1228
- ctx,
1229
- enabled: manageNetworking && access !== SandboxAccess.Read,
1230
- access,
1069
+ ctx: networkingCtx,
1070
+ enabled: manageNetworking,
1231
1071
  sandbox: setup.sandbox,
1232
1072
  client,
1233
1073
  info,
1234
- spec,
1235
1074
  onClose: () => setup.transport.close(),
1236
1075
  initialNetworkPolicy
1237
1076
  });
1238
- cleanupSetup = async () => {
1077
+ cleanup = async () => {
1239
1078
  try {
1240
1079
  await manager?.closeOnFailure();
1241
1080
  } finally {
1242
1081
  setup.transport.close();
1243
1082
  }
1244
1083
  };
1245
- const servicesMarker = WORKSPACE_ROOT + "/.final-workspace-services";
1246
- if (fresh && manageNetworking)
1247
- await setup.sandbox.writeTextFile({ path: servicesMarker, content: "pending" });
1248
1084
  await manager?.initialize();
1249
- const preparationMarker = WORKSPACE_ROOT + "/.final/workspace-prepared/" + await sha256Hex(artifact.data.key);
1250
- let preparation = null;
1251
- const prepareWorkspace = () => preparation ??= (async () => {
1252
- await mountRepositories({
1253
- sandbox: setup.sandbox,
1254
- repositories: spec.parameters.repositories,
1255
- resolver: ctx.repositories,
1256
- log
1257
- });
1085
+ const seedMarker = `${WORKSPACE_ROOT}/.final/workspace-prepared/${await sha256Hex(artifact.data.key)}`;
1086
+ let seeding = null;
1087
+ const seedWorkspace = () => seeding ??= (async () => {
1088
+ if (await setup.sandbox.stat({ path: seedMarker }) != null) return;
1258
1089
  await materializeSandboxSeeds({
1259
1090
  sandbox: setup.sandbox,
1260
1091
  root: WORKSPACE_ROOT,
1261
- files: await sandboxSeedFilesForStart(ctx, artifact.data)
1262
- });
1263
- await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
1264
- const result = await runWorkspaceSetup({
1265
- sandbox: setup.sandbox,
1266
- template: spec.template,
1267
- repositories: spec.parameters.repositories,
1268
- log
1092
+ files: await sandboxSeedFilesForAttach(ctx, artifact.data)
1269
1093
  });
1270
- const servicesPending = manager != null && await setup.sandbox.stat({ path: servicesMarker }) != null && await setup.sandbox.readTextFile({ path: servicesMarker }) === "pending";
1271
- if (fresh || servicesPending) {
1272
- await startServicesFor({ setup, spec, env });
1273
- if (manageNetworking)
1274
- await setup.sandbox.writeTextFile({ path: servicesMarker, content: "complete" });
1275
- }
1276
- if (manager != null)
1277
- await setup.sandbox.writeTextFile({ path: preparationMarker, content: "complete" });
1278
- return result;
1094
+ await setup.sandbox.writeTextFile({ path: seedMarker, content: "complete" });
1279
1095
  })().catch((cause) => {
1280
- preparation = null;
1096
+ seeding = null;
1281
1097
  throw cause;
1282
1098
  });
1283
- let ready = !manageNetworking || access !== SandboxAccess.Read;
1099
+ let ready = true;
1284
1100
  if (manager != null) {
1285
1101
  try {
1286
1102
  await ctx.executionPolicy?.beforeExecution({
@@ -1290,310 +1106,120 @@ ${step.run}`,
1290
1106
  await manager.guard();
1291
1107
  } catch {
1292
1108
  log("workspace_networking.setup_deferred", {
1293
- provider: "computer",
1109
+ provider: COMPUTER_PROVIDER,
1294
1110
  computerId: info.id
1295
1111
  });
1296
1112
  ready = false;
1297
1113
  }
1298
1114
  }
1299
- const setupHash = ready ? await prepareWorkspace() : null;
1300
- const { handle } = await buildHandle({
1301
- ctx,
1115
+ if (fresh && ready) await seedWorkspace();
1116
+ const handle = await buildHandle({
1117
+ ctx: { ...networkingCtx, executionPolicy: ctx.executionPolicy },
1302
1118
  client,
1303
1119
  info,
1304
- spec,
1305
- access,
1306
1120
  env,
1121
+ ports: state.ports,
1122
+ passive: false,
1307
1123
  networking: manager,
1308
1124
  networkingManaged: manageNetworking,
1309
1125
  closeSetup: () => setup.transport.close(),
1310
- beforeUse: async () => {
1311
- await prepareWorkspace();
1312
- }
1126
+ beforeUse: fresh ? seedWorkspace : void 0
1313
1127
  });
1314
1128
  return {
1315
1129
  handle,
1316
- region: info.regionSlug ?? config.regionSlug ?? null,
1130
+ region: info.regionSlug ?? region,
1317
1131
  state: {
1318
- version: 1,
1132
+ ...state,
1319
1133
  computerId: info.id,
1320
- computerName: info.name,
1321
- workspace,
1322
- access,
1323
- guideVersionId: spec.guide?.versionId ?? null,
1324
- setupHash,
1325
- setupPending: !ready,
1326
- workspaceId: sandboxWorkspaceId({
1327
- session: ctx.session,
1328
- workspace: sandboxWorkspaceOf[workspace]
1329
- }),
1330
1134
  networkPolicySaved: manageNetworking
1331
- }
1135
+ },
1136
+ fresh,
1137
+ booted
1332
1138
  };
1333
1139
  } catch (error) {
1334
1140
  let cause = error;
1335
1141
  try {
1336
- await cleanupSetup?.();
1142
+ await cleanup?.();
1337
1143
  } catch (cleanupError) {
1338
1144
  cause = new AggregateError(
1339
1145
  [error, cleanupError],
1340
- "Computer setup and egress cleanup failed"
1146
+ "Computer attach and egress cleanup failed"
1341
1147
  );
1342
1148
  }
1343
- if (!reused) {
1149
+ if (fresh) {
1344
1150
  try {
1345
1151
  await client.action(info.id, "delete");
1346
1152
  } catch (cleanupError) {
1347
1153
  throw new AggregateError(
1348
1154
  [cause, cleanupError],
1349
- `computer ${info.name} setup and cleanup failed`
1155
+ `computer ${info.name} attach and cleanup failed`
1350
1156
  );
1351
1157
  }
1352
1158
  }
1353
1159
  throw cause;
1354
1160
  }
1355
1161
  },
1356
- resumePassive: async (ctx, _artifact, rawState) => {
1357
- const state = computerStateSchema.parse(rawState);
1358
- if (ctx.session.workspaceId == null && state.workspaceId != null)
1359
- ctx = { ...ctx, session: { ...ctx.session, workspaceId: state.workspaceId } };
1360
- if (state.networkPolicySaved === true && state.workspaceId != null && ctx.session.workspaceId !== state.workspaceId)
1361
- throw new Error("Passive workspace identity does not match retained provider state");
1362
- const client = clientFor(ctx.credentials);
1363
- const info = await client.getComputer(state.computerId);
1162
+ attachPassive: async (ctx, { state: rawState }) => {
1163
+ const state = stateOf(rawState);
1164
+ const client = clientFor(ctx);
1165
+ const info = await findComputer(client, state);
1364
1166
  if (info == null || info.state !== ComputerState.Running)
1365
1167
  throw computerSandboxError(
1366
1168
  "computer_not_running",
1367
1169
  "computer is not running for passive access"
1368
1170
  );
1369
- await awaitRunning(ctx, state, { timeoutMs: runningWaitMs });
1370
- const spec = await resolve(ctx.guides);
1371
- const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
1372
- return (await buildHandle({
1373
- ctx,
1171
+ return buildHandle({
1172
+ ctx: { ...offerContextOf(ctx), executionPolicy: ctx.executionPolicy },
1374
1173
  client,
1375
- info,
1376
- spec,
1377
- access: SandboxAccess.Read,
1378
- env,
1379
- networkingManaged: state.networkPolicySaved === true
1380
- })).handle;
1381
- },
1382
- resume: async (ctx, _artifact, rawState) => {
1383
- const state = computerStateSchema.parse(rawState);
1384
- if (state.networkPolicySaved === true) {
1385
- if (config.networking == null)
1386
- throw new Error("Device networking control is required before resume");
1387
- }
1388
- if (state.workspaceId != null) {
1389
- if (state.networkPolicySaved === true && ctx.session.workspaceId != null && ctx.session.workspaceId !== state.workspaceId)
1390
- throw new Error("Resume workspace identity does not match retained provider state");
1391
- if (ctx.session.workspaceId == null)
1392
- ctx = { ...ctx, session: { ...ctx.session, workspaceId: state.workspaceId } };
1393
- }
1394
- if (ctx.session.workspaceId == null)
1395
- ctx = {
1396
- ...ctx,
1397
- session: {
1398
- ...ctx.session,
1399
- workspaceId: sandboxWorkspaceId({
1400
- session: ctx.session,
1401
- workspace: sandboxWorkspaceOf[state.workspace]
1402
- })
1403
- }
1404
- };
1405
- const client = clientFor(ctx.credentials);
1406
- const workspaceId = sandboxWorkspaceId({
1407
- session: ctx.session,
1408
- workspace: sandboxWorkspaceOf[state.workspace]
1174
+ info: await awaitComputerRunning(client, info.id, runningWaitMs),
1175
+ env: platformProcessEnvironment(ctx, {}),
1176
+ ports: state.ports,
1177
+ passive: true,
1178
+ networkingManaged: state.networkPolicySaved
1409
1179
  });
1410
- const manageNetworking = state.networkPolicySaved === true || await needsWorkspaceNetworking(ctx, workspaceId) || state.access !== SandboxAccess.Read && await needsWorkspaceNetworking(ctx, workspaceId, { access: state.access });
1411
- if (manageNetworking)
1412
- config.networking?.validateWorkspaceId?.(
1413
- sandboxWorkspaceId({
1414
- session: ctx.session,
1415
- workspace: sandboxWorkspaceOf[state.workspace]
1416
- })
1417
- );
1418
- const info = await client.getComputer(state.computerId);
1419
- if (info == null || info.state === ComputerState.Deleted || info.state === ComputerState.Error) {
1420
- throw computerSandboxError("computer_gone", `computer ${state.computerName} is gone`);
1421
- }
1422
- if (manageNetworking && state.access === SandboxAccess.Read && info.state !== ComputerState.Running)
1423
- throw new Error("A managed Computer requires a write binding to resume networking");
1424
- const initialNetworkPolicy = manageNetworking ? networkPolicyFor(await client.getNetworkPolicy(info.id)) : null;
1425
- if (initialNetworkPolicy != null && state.access !== SandboxAccess.Read && info.state === ComputerState.Running)
1426
- await client.disableAutoResume(info.id);
1427
- const rebooted = !isRunningState(info.state);
1428
- if (manageNetworking && config.networking != null && initialNetworkPolicy != null)
1429
- await preservePolicy({
1430
- ctx,
1431
- workspaceId: sandboxWorkspaceId({
1432
- session: ctx.session,
1433
- workspace: sandboxWorkspaceOf[state.workspace]
1434
- }),
1435
- policy: initialNetworkPolicy
1436
- });
1437
- if (manageNetworking && info.state !== ComputerState.Running)
1438
- await client.setNetworkPolicy(
1439
- info.id,
1440
- { egress: ComputerEgressPolicy.None },
1441
- { autoResume: false }
1442
- );
1443
- if (info.state === ComputerState.Paused) await client.action(info.id, "resume");
1444
- else if (rebooted) await client.action(info.id, "start");
1445
- const running = await awaitRunning(ctx, state, { timeoutMs: runningWaitMs });
1446
- const spec = await resolve(ctx.guides);
1447
- const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
1448
- if (!manageNetworking) {
1449
- const policy = withPlatformEgressHosts(ctx, spec.parameters.networkPolicy);
1450
- await client.setNetworkPolicy(running.id, computerEgressOf(policy));
1451
- await reportPlatformNetworkPolicy(ctx, policy);
1452
- }
1453
- const setup = setupSession({ client, info: running, env });
1454
- const manager = workspaceNetworking({
1455
- ctx,
1456
- enabled: manageNetworking && state.access !== SandboxAccess.Read,
1457
- access: state.access,
1458
- sandbox: setup.sandbox,
1459
- client,
1460
- info: running,
1461
- spec,
1462
- onClose: () => setup.transport.close(),
1463
- initialNetworkPolicy: initialNetworkPolicy ?? networkPolicyFor(egressFor(ctx, spec.parameters.networkPolicy))
1464
- });
1465
- try {
1466
- if (!manageNetworking || state.access !== SandboxAccess.Read)
1467
- await writePlatformFiles(ctx, setup.sandbox);
1468
- if (manageNetworking && rebooted)
1469
- await setup.sandbox.writeTextFile({
1470
- path: WORKSPACE_ROOT + "/.final-workspace-services",
1471
- content: "pending"
1472
- });
1473
- await manager?.initialize();
1474
- let preparation = null;
1475
- const prepareWorkspace = () => preparation ??= (async () => {
1476
- const artifact = state.setupPending ? computerArtifactSchema.parse(_artifact) : null;
1477
- const marker = artifact == null ? null : WORKSPACE_ROOT + "/.final/workspace-prepared/" + await sha256Hex(artifact.key);
1478
- const pending = marker != null && await setup.sandbox.stat({ path: marker }) == null;
1479
- if (pending && artifact != null) {
1480
- await mountRepositories({
1481
- sandbox: setup.sandbox,
1482
- repositories: spec.parameters.repositories,
1483
- resolver: ctx.repositories,
1484
- log
1485
- });
1486
- await materializeSandboxSeeds({
1487
- sandbox: setup.sandbox,
1488
- root: WORKSPACE_ROOT,
1489
- files: await sandboxSeedFilesForStart(ctx, artifact)
1490
- });
1491
- }
1492
- await installGuideCommands({ sandbox: setup.sandbox, template: spec.template });
1493
- if (pending)
1494
- await runWorkspaceSetup({
1495
- sandbox: setup.sandbox,
1496
- template: spec.template,
1497
- repositories: spec.parameters.repositories,
1498
- log
1499
- });
1500
- const servicesMarker = WORKSPACE_ROOT + "/.final-workspace-services";
1501
- const servicesPending = manageNetworking && await setup.sandbox.stat({ path: servicesMarker }) != null && await setup.sandbox.readTextFile({ path: servicesMarker }) === "pending";
1502
- if (rebooted || servicesPending) {
1503
- await startServicesFor({ setup, spec, env });
1504
- if (manageNetworking)
1505
- await setup.sandbox.writeTextFile({ path: servicesMarker, content: "complete" });
1506
- }
1507
- if (pending && marker != null)
1508
- await setup.sandbox.writeTextFile({ path: marker, content: "complete" });
1509
- })().catch((cause) => {
1510
- preparation = null;
1511
- throw cause;
1512
- });
1513
- let ready = !manageNetworking || state.access !== SandboxAccess.Read;
1514
- if (manager != null) {
1515
- try {
1516
- await ctx.executionPolicy?.beforeExecution({
1517
- operation: SandboxExecutionOperation.Run,
1518
- background: false
1519
- });
1520
- await manager.guard();
1521
- } catch {
1522
- log("workspace_networking.setup_deferred", {
1523
- provider: "computer",
1524
- computerId: info.id
1525
- });
1526
- ready = false;
1527
- }
1528
- }
1529
- if (ready) await prepareWorkspace();
1530
- const { handle } = await buildHandle({
1531
- ctx,
1532
- client,
1533
- info: running,
1534
- spec,
1535
- access: state.access,
1536
- env,
1537
- networking: manager,
1538
- networkingManaged: manageNetworking,
1539
- closeSetup: () => setup.transport.close(),
1540
- beforeUse: prepareWorkspace
1541
- });
1542
- return handle;
1543
- } catch (error) {
1544
- try {
1545
- await manager?.closeOnFailure();
1546
- } catch (cleanupError) {
1547
- throw new AggregateError(
1548
- [error, cleanupError],
1549
- "Computer resume and egress cleanup failed"
1550
- );
1551
- } finally {
1552
- setup.transport.close();
1553
- }
1554
- throw error;
1555
- }
1556
1180
  },
1557
- park: async (ctx, _artifact, rawState) => {
1558
- const state = computerStateSchema.parse(rawState);
1559
- const workspaceId = await teardownComputer({
1560
- ctx,
1561
- computerId: state.computerId,
1562
- computerName: state.computerName,
1563
- workspace: state.workspace,
1564
- policySaved: state.networkPolicySaved === true,
1565
- workspaceId: state.networkPolicySaved === true ? state.workspaceId : ctx.session.workspaceId ?? state.workspaceId,
1181
+ // The computer pauses on its own idle policy; nothing to do.
1182
+ detach: async (_ctx, { state }) => stateOf(state),
1183
+ park: async (ctx, { state: rawState }) => {
1184
+ const state = stateOf(rawState);
1185
+ if (state.computerId == null) return state;
1186
+ const computerId = state.computerId;
1187
+ const networkingCtx = offerContextOf(ctx);
1188
+ await teardownComputer({
1189
+ ctx: networkingCtx,
1190
+ computerId,
1191
+ policySaved: state.networkPolicySaved,
1566
1192
  fence: true,
1567
1193
  finish: async () => {
1568
1194
  const failures = [];
1569
1195
  try {
1570
- await closeComputerMcp(state.computerId);
1196
+ await closeComputerMcp(computerId);
1571
1197
  } catch (cause) {
1572
1198
  failures.push(cause);
1573
1199
  }
1574
1200
  try {
1575
- const client = clientFor(ctx.credentials);
1201
+ const client = clientFor(ctx);
1576
1202
  let current;
1577
1203
  try {
1578
- current = await client.getComputer(state.computerId);
1204
+ current = await client.getComputer(computerId);
1579
1205
  } catch (cause) {
1580
1206
  failures.push(cause);
1581
1207
  }
1582
1208
  if (current !== void 0 && (current == null || hasNothingToPause(current.state))) {
1583
1209
  log("computer_sandbox.park_at_rest", {
1584
- computerId: state.computerId,
1210
+ computerId,
1585
1211
  state: current?.state ?? null
1586
1212
  });
1587
1213
  } else {
1588
1214
  try {
1589
- await client.action(state.computerId, "pause");
1215
+ await client.action(computerId, "pause");
1590
1216
  } catch (error) {
1591
1217
  if (!pauseMayHaveSettled(error)) throw error;
1592
- const settled = await client.getComputer(state.computerId);
1218
+ const settled = await client.getComputer(computerId);
1593
1219
  if (settled != null && settled.state !== ComputerState.Paused && !hasNothingToPause(settled.state))
1594
1220
  throw error;
1595
1221
  log("computer_sandbox.park_at_rest", {
1596
- computerId: state.computerId,
1222
+ computerId,
1597
1223
  state: settled?.state ?? null
1598
1224
  });
1599
1225
  }
@@ -1605,54 +1231,63 @@ ${step.run}`,
1605
1231
  if (failures.length > 1) throw new AggregateError(failures, "Computer pause failed");
1606
1232
  }
1607
1233
  });
1608
- const key = ctx.session.organizationId + ":" + workspaceId;
1609
1234
  return {
1610
1235
  ...state,
1611
- workspaceId,
1612
- networkPolicySaved: state.networkPolicySaved === true || savedPolicyWorkspaces.has(key)
1236
+ networkPolicySaved: state.networkPolicySaved || savedPolicyWorkspaces.has(workspaceKey(networkingCtx))
1613
1237
  };
1614
1238
  },
1615
- onSessionEnd: async (ctx, _artifact, rawState, { reason }) => {
1616
- const state = computerStateSchema.parse(rawState);
1617
- const client = clientFor(ctx.credentials);
1618
- const finish = async () => {
1619
- const action = state.workspace === ComputerWorkspacePolicy.Conversation ? "stop" : "delete";
1620
- log("computer_sandbox.session_end", { computerId: state.computerId, reason, action });
1621
- try {
1622
- await client.action(state.computerId, action);
1623
- } catch (error) {
1624
- if (isComputersApiStatus(error, 404)) return;
1625
- if (!isComputersApiStatus(error, 409)) throw error;
1626
- const current = await client.getComputer(state.computerId);
1627
- const settled = current == null || current.state === ComputerState.Deleted || action === "stop" && current.state === ComputerState.Shelved;
1628
- if (!settled) throw error;
1629
- return;
1630
- }
1631
- if (action === "stop") await awaitShelved(client, state.computerId);
1632
- };
1239
+ delete: async (ctx, { state: rawState }) => {
1240
+ const state = stateOf(rawState);
1241
+ const client = clientFor(ctx);
1242
+ const computer = await findComputer(client, state);
1243
+ if (computer == null) {
1244
+ if (state.networkPolicySaved && config.networking != null)
1245
+ await config.networking.suspend({
1246
+ ctx: offerContextOf(ctx),
1247
+ workspaceId: ctx.sandboxId
1248
+ });
1249
+ return;
1250
+ }
1633
1251
  await teardownComputer({
1634
- ctx,
1635
- computerId: state.computerId,
1636
- computerName: state.computerName,
1637
- workspace: state.workspace,
1638
- policySaved: state.networkPolicySaved === true,
1639
- workspaceId: state.networkPolicySaved === true ? state.workspaceId : ctx.session.workspaceId ?? state.workspaceId,
1640
- fence: state.workspace === ComputerWorkspacePolicy.Conversation,
1641
- finish
1252
+ ctx: offerContextOf(ctx),
1253
+ computerId: computer.id,
1254
+ policySaved: state.networkPolicySaved,
1255
+ fence: false,
1256
+ finish: async () => {
1257
+ const failures = [];
1258
+ try {
1259
+ await closeComputerMcp(computer.id);
1260
+ } catch (cause) {
1261
+ failures.push(cause);
1262
+ }
1263
+ log("computer_sandbox.delete", { computerId: computer.id });
1264
+ try {
1265
+ await client.action(computer.id, "delete");
1266
+ } catch (error) {
1267
+ if (!await deletedAfterFenceError({ client, computerId: computer.id, cause: error }))
1268
+ failures.push(error);
1269
+ }
1270
+ if (failures.length === 1) throw failures[0];
1271
+ if (failures.length > 1)
1272
+ throw new AggregateError(failures, "Computer delete or MCP cleanup failed");
1273
+ }
1642
1274
  });
1643
1275
  }
1644
1276
  };
1645
1277
  };
1646
1278
  return defineSandboxProvider({
1647
- name: "computer",
1648
- stateProtocolVersion: 1,
1279
+ name: COMPUTER_PROVIDER,
1280
+ stateProtocolVersion: 2,
1649
1281
  environment
1650
1282
  });
1651
1283
  };
1652
1284
  export {
1653
- COMPUTER_SANDBOX_SPECIFIER2 as COMPUTER_SANDBOX_SPECIFIER,
1654
- computeComputerArtifactKey2 as computeComputerArtifactKey,
1655
- computerWorkspaceName,
1285
+ COMPUTER_PROVIDER2 as COMPUTER_PROVIDER,
1286
+ WORKSPACE_ROOT,
1287
+ computeComputerArtifactKey,
1288
+ computerNameFor,
1289
+ computerNeedsBuild,
1290
+ computerNetworkPolicyOf,
1656
1291
  createComputerSandboxProvider
1657
1292
  };
1658
1293
  //# sourceMappingURL=computer.mjs.map