@finalai/final-agent-provider 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HOSTING.md +334 -245
- package/README.md +125 -46
- package/WORKSPACE-EGRESS.md +11 -11
- package/dist/modules/catalog.d.mts +9 -0
- package/dist/modules/catalog.mjs +21 -0
- package/dist/modules/catalog.mjs.map +7 -0
- package/dist/modules/harnesses/acp-agent.d.mts +4 -0
- package/dist/modules/harnesses/acp-agent.mjs +35 -0
- package/dist/modules/harnesses/acp-agent.mjs.map +7 -0
- package/dist/modules/harnesses/adapters.d.mts +7 -0
- package/dist/modules/harnesses/adapters.mjs +74 -0
- package/dist/modules/harnesses/adapters.mjs.map +7 -0
- package/dist/modules/harnesses/claude-code.d.mts +14 -0
- package/dist/modules/harnesses/claude-code.mjs +79 -0
- package/dist/modules/harnesses/claude-code.mjs.map +7 -0
- package/dist/modules/harnesses/codex.d.mts +12 -0
- package/dist/modules/harnesses/codex.mjs +90 -0
- package/dist/modules/harnesses/codex.mjs.map +7 -0
- package/dist/modules/harnesses/config-fields.d.mts +98 -57
- package/dist/modules/harnesses/config-fields.mjs +58 -35
- package/dist/modules/harnesses/config-fields.mjs.map +3 -3
- package/dist/modules/harnesses/definitions.d.mts +3 -0
- package/dist/modules/harnesses/definitions.mjs +21 -0
- package/dist/modules/harnesses/definitions.mjs.map +7 -0
- package/dist/modules/harnesses/fidelity.d.mts +1 -1
- package/dist/modules/harnesses/fidelity.mjs +17 -14
- package/dist/modules/harnesses/fidelity.mjs.map +2 -2
- package/dist/modules/harnesses/final.d.mts +12 -0
- package/dist/modules/harnesses/final.mjs +70 -0
- package/dist/modules/harnesses/final.mjs.map +7 -0
- package/dist/modules/harnesses/index.d.mts +13 -12
- package/dist/modules/harnesses/index.mjs +86 -45
- package/dist/modules/harnesses/index.mjs.map +3 -3
- package/dist/modules/harnesses/names.d.mts +43 -0
- package/dist/modules/harnesses/names.mjs +59 -0
- package/dist/modules/harnesses/names.mjs.map +7 -0
- package/dist/modules/harnesses/nanocode-claude.d.mts +18 -0
- package/dist/modules/harnesses/nanocode-claude.mjs +101 -0
- package/dist/modules/harnesses/nanocode-claude.mjs.map +7 -0
- package/dist/modules/harnesses/nanocode-codex.d.mts +13 -0
- package/dist/modules/harnesses/nanocode-codex.mjs +64 -0
- package/dist/modules/harnesses/nanocode-codex.mjs.map +7 -0
- package/dist/modules/harnesses/pi.d.mts +12 -0
- package/dist/modules/harnesses/pi.mjs +71 -0
- package/dist/modules/harnesses/pi.mjs.map +7 -0
- package/dist/modules/integrations.d.mts +9 -0
- package/dist/modules/integrations.mjs +29 -0
- package/dist/modules/integrations.mjs.map +7 -0
- package/dist/modules/provider/general.bundle.d.mts +1 -0
- package/dist/modules/provider/general.bundle.mjs +5 -0
- package/dist/modules/provider/general.bundle.mjs.map +7 -0
- package/dist/modules/provider/general.d.mts +11 -0
- package/dist/modules/provider/general.mjs +130 -0
- package/dist/modules/provider/general.mjs.map +7 -0
- package/dist/modules/provider/index.d.mts +3 -0
- package/dist/modules/provider/index.mjs +13 -0
- package/dist/modules/provider/index.mjs.map +7 -0
- package/dist/modules/sandboxes/access.d.mts +34 -0
- package/dist/modules/sandboxes/access.mjs +15 -0
- package/dist/modules/sandboxes/access.mjs.map +7 -0
- package/dist/modules/sandboxes/computer.d.mts +24 -51
- package/dist/modules/sandboxes/computer.mjs +523 -888
- package/dist/modules/sandboxes/computer.mjs.map +3 -3
- package/dist/modules/sandboxes/computers-api.d.mts +3 -2
- package/dist/modules/sandboxes/computers-api.mjs +2 -4
- package/dist/modules/sandboxes/computers-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device-api.d.mts +4 -3
- package/dist/modules/sandboxes/device-api.mjs +2 -4
- package/dist/modules/sandboxes/device-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device.d.mts +6 -6
- package/dist/modules/sandboxes/device.mjs +126 -121
- package/dist/modules/sandboxes/device.mjs.map +3 -3
- package/dist/modules/sandboxes/egress.d.mts +10 -9
- package/dist/modules/sandboxes/egress.mjs +73 -56
- package/dist/modules/sandboxes/egress.mjs.map +2 -2
- package/dist/modules/sandboxes/environment.d.mts +35 -269
- package/dist/modules/sandboxes/environment.mjs +27 -104
- package/dist/modules/sandboxes/environment.mjs.map +3 -3
- package/dist/modules/sandboxes/index.d.mts +9 -69
- package/dist/modules/sandboxes/index.mjs +35 -95
- package/dist/modules/sandboxes/index.mjs.map +3 -3
- package/dist/modules/sandboxes/local.d.mts +29 -0
- package/dist/modules/sandboxes/local.mjs +555 -0
- package/dist/modules/sandboxes/local.mjs.map +7 -0
- package/dist/modules/sandboxes/session.d.mts +0 -3
- package/dist/modules/sandboxes/session.mjs +2 -22
- package/dist/modules/sandboxes/session.mjs.map +2 -2
- package/dist/modules/sandboxes/testing/fake-machine.mjs +0 -28
- package/dist/modules/sandboxes/testing/fake-machine.mjs.map +2 -2
- package/package.json +73 -137
- package/dist/modules/authoring-final-channel.d.mts +0 -3
- package/dist/modules/authoring-final-channel.mjs +0 -10
- package/dist/modules/authoring-final-channel.mjs.map +0 -7
- package/dist/modules/channel-factories.d.mts +0 -40
- package/dist/modules/channel-factories.mjs +0 -212
- package/dist/modules/channel-factories.mjs.map +0 -7
- package/dist/modules/channels/content.d.mts +0 -6
- package/dist/modules/channels/content.mjs +0 -31
- package/dist/modules/channels/content.mjs.map +0 -7
- package/dist/modules/channels/delivery.d.mts +0 -54
- package/dist/modules/channels/delivery.mjs +0 -243
- package/dist/modules/channels/delivery.mjs.map +0 -7
- package/dist/modules/channels/direct-messaging.d.mts +0 -5
- package/dist/modules/channels/direct-messaging.mjs +0 -8
- package/dist/modules/channels/direct-messaging.mjs.map +0 -7
- package/dist/modules/channels/fake-platform.d.mts +0 -36
- package/dist/modules/channels/fake-platform.mjs +0 -168
- package/dist/modules/channels/fake-platform.mjs.map +0 -7
- package/dist/modules/channels/group-messaging.d.mts +0 -5
- package/dist/modules/channels/group-messaging.mjs +0 -8
- package/dist/modules/channels/group-messaging.mjs.map +0 -7
- package/dist/modules/channels/inbound.d.mts +0 -52
- package/dist/modules/channels/inbound.mjs +0 -42
- package/dist/modules/channels/inbound.mjs.map +0 -7
- package/dist/modules/channels/index.d.mts +0 -9
- package/dist/modules/channels/index.mjs +0 -10
- package/dist/modules/channels/index.mjs.map +0 -7
- package/dist/modules/channels/messaging-channel.d.mts +0 -7
- package/dist/modules/channels/messaging-channel.mjs +0 -99
- package/dist/modules/channels/messaging-channel.mjs.map +0 -7
- package/dist/modules/channels/projection.d.mts +0 -36
- package/dist/modules/channels/projection.mjs +0 -233
- package/dist/modules/channels/projection.mjs.map +0 -7
- package/dist/modules/channels/prompt.d.mts +0 -72
- package/dist/modules/channels/prompt.mjs +0 -208
- package/dist/modules/channels/prompt.mjs.map +0 -7
- package/dist/modules/channels/routing.d.mts +0 -5
- package/dist/modules/channels/routing.mjs +0 -53
- package/dist/modules/channels/routing.mjs.map +0 -7
- package/dist/modules/connections.d.mts +0 -11
- package/dist/modules/connections.mjs +0 -16
- package/dist/modules/connections.mjs.map +0 -7
- package/dist/modules/harnesses/acp-describe.d.mts +0 -20
- package/dist/modules/harnesses/acp-describe.mjs +0 -110
- package/dist/modules/harnesses/acp-describe.mjs.map +0 -7
- package/dist/modules/harnesses/local-stdio.d.mts +0 -47
- package/dist/modules/harnesses/local-stdio.mjs +0 -41
- package/dist/modules/harnesses/local-stdio.mjs.map +0 -7
- package/dist/modules/harnesses/remote-acp.d.mts +0 -76
- package/dist/modules/harnesses/remote-acp.mjs +0 -83
- package/dist/modules/harnesses/remote-acp.mjs.map +0 -7
- package/dist/modules/harnesses/remote-claude-code.d.mts +0 -88
- package/dist/modules/harnesses/remote-claude-code.mjs +0 -113
- package/dist/modules/harnesses/remote-claude-code.mjs.map +0 -7
- package/dist/modules/harnesses/remote-client-tls.d.mts +0 -35
- package/dist/modules/harnesses/remote-client-tls.mjs +0 -37
- package/dist/modules/harnesses/remote-client-tls.mjs.map +0 -7
- package/dist/modules/harnesses/remote-codex.d.mts +0 -98
- package/dist/modules/harnesses/remote-codex.mjs +0 -128
- package/dist/modules/harnesses/remote-codex.mjs.map +0 -7
- package/dist/modules/harnesses/remote-pi.d.mts +0 -78
- package/dist/modules/harnesses/remote-pi.mjs +0 -104
- package/dist/modules/harnesses/remote-pi.mjs.map +0 -7
- package/dist/modules/harnesses/remote-url.d.mts +0 -5
- package/dist/modules/harnesses/remote-url.mjs +0 -19
- package/dist/modules/harnesses/remote-url.mjs.map +0 -7
- package/dist/modules/harnesses/serverless-final.d.mts +0 -63
- package/dist/modules/harnesses/serverless-final.mjs +0 -88
- package/dist/modules/harnesses/serverless-final.mjs.map +0 -7
- package/dist/modules/harnesses/serverless-nanocode-claude.d.mts +0 -64
- package/dist/modules/harnesses/serverless-nanocode-claude.mjs +0 -105
- package/dist/modules/harnesses/serverless-nanocode-claude.mjs.map +0 -7
- package/dist/modules/harnesses/serverless-nanocode-codex.d.mts +0 -66
- package/dist/modules/harnesses/serverless-nanocode-codex.mjs +0 -113
- package/dist/modules/harnesses/serverless-nanocode-codex.mjs.map +0 -7
- package/dist/modules/harnesses/serverless-pi.d.mts +0 -64
- package/dist/modules/harnesses/serverless-pi.mjs +0 -98
- package/dist/modules/harnesses/serverless-pi.mjs.map +0 -7
- package/dist/modules/runtime/index.d.mts +0 -7
- package/dist/modules/runtime/index.mjs +0 -18
- package/dist/modules/runtime/index.mjs.map +0 -7
- package/dist/modules/runtime/local-stdio.d.mts +0 -10
- package/dist/modules/runtime/local-stdio.mjs +0 -106
- package/dist/modules/runtime/local-stdio.mjs.map +0 -7
- package/dist/modules/runtime/remote-acp.d.mts +0 -16
- package/dist/modules/runtime/remote-acp.mjs +0 -152
- package/dist/modules/runtime/remote-acp.mjs.map +0 -7
- package/dist/modules/runtime/remote-claude-code.d.mts +0 -5
- package/dist/modules/runtime/remote-claude-code.mjs +0 -47
- package/dist/modules/runtime/remote-claude-code.mjs.map +0 -7
- package/dist/modules/runtime/remote-codex.d.mts +0 -5
- package/dist/modules/runtime/remote-codex.mjs +0 -47
- package/dist/modules/runtime/remote-codex.mjs.map +0 -7
- package/dist/modules/runtime/remote-host.d.mts +0 -127
- package/dist/modules/runtime/remote-host.mjs +0 -387
- package/dist/modules/runtime/remote-host.mjs.map +0 -7
- package/dist/modules/runtime/remote-pi.d.mts +0 -5
- package/dist/modules/runtime/remote-pi.mjs +0 -44
- package/dist/modules/runtime/remote-pi.mjs.map +0 -7
- package/dist/modules/sandboxes/guide.d.mts +0 -35
- package/dist/modules/sandboxes/guide.mjs +0 -119
- package/dist/modules/sandboxes/guide.mjs.map +0 -7
- package/dist/modules/sandboxes/workspace-setup.d.mts +0 -42
- package/dist/modules/sandboxes/workspace-setup.mjs +0 -242
- package/dist/modules/sandboxes/workspace-setup.mjs.map +0 -7
- package/dist/modules/testing/harness.d.mts +0 -9
- package/dist/modules/testing/harness.mjs +0 -123
- package/dist/modules/testing/harness.mjs.map +0 -7
|
@@ -1,29 +1,21 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
COMPUTER_PROVIDER,
|
|
56
48
|
computerEnvironmentSchema,
|
|
57
|
-
|
|
58
|
-
ComputerWorkspacePolicy
|
|
49
|
+
DEFAULT_COMPUTER_IMAGE
|
|
59
50
|
} from "./environment.mjs";
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
95
|
-
computerId: z.string(),
|
|
75
|
+
version: z.literal(2),
|
|
96
76
|
computerName: z.string(),
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
486
|
+
if (args.networking == null && !args.passive)
|
|
379
487
|
await teardownComputer({
|
|
380
488
|
ctx: args.ctx,
|
|
381
489
|
computerId: info.id,
|
|
382
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
fetch: config.networking
|
|
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
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
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:
|
|
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
|
-
...
|
|
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 (
|
|
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
|
-
...
|
|
877
|
-
...
|
|
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:
|
|
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
|
|
950
|
-
const
|
|
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
|
|
953
|
-
|
|
954
|
-
diskGb:
|
|
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
|
|
959
|
-
await
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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 =
|
|
977
|
-
if (
|
|
978
|
-
|
|
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 =
|
|
984
|
-
|
|
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("
|
|
855
|
+
throw new Error("this computer has not started yet");
|
|
987
856
|
}
|
|
988
|
-
const client = clientFor(ctx
|
|
857
|
+
const client = clientFor(ctx);
|
|
989
858
|
const open = (expiresAtMs) => computerEditor({
|
|
990
859
|
client,
|
|
991
|
-
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
|
|
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 (
|
|
1006
|
-
throw new Error(
|
|
1007
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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 =
|
|
913
|
+
const state = stateOf(rawState);
|
|
914
|
+
if (state.computerId == null) return null;
|
|
1051
915
|
return readComputerSnapshot({
|
|
1052
|
-
client: clientFor(ctx
|
|
916
|
+
client: clientFor(ctx),
|
|
1053
917
|
computerId: state.computerId,
|
|
1054
918
|
request
|
|
1055
919
|
});
|
|
1056
920
|
},
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
//
|
|
1060
|
-
|
|
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(
|
|
927
|
+
const key = await keyFor(seedFiles);
|
|
1068
928
|
const seedArtifact = await sandboxSeedArtifact(ctx, seedFiles);
|
|
1069
|
-
if (computerNeedsBuild(
|
|
1070
|
-
const baseKey = await keyFor(
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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 =
|
|
1109
|
-
ctx.log?.(`computer base ${key} is 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
|
|
1114
|
-
diskGb:
|
|
949
|
+
image,
|
|
950
|
+
diskGb: machine?.diskGb ?? null,
|
|
1115
951
|
...seedArtifact,
|
|
1116
|
-
base: { kind: "image", image
|
|
952
|
+
base: { kind: "image", image, imageVersionId }
|
|
1117
953
|
};
|
|
1118
954
|
},
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
|
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(
|
|
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
|
|
1142
|
-
const
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
|
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
|
-
|
|
1164
|
-
|
|
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
|
|
1179
|
-
if (existing != null
|
|
1019
|
+
let booted;
|
|
1020
|
+
if (existing != null) {
|
|
1180
1021
|
info = existing;
|
|
1181
|
-
|
|
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 (
|
|
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
|
-
...
|
|
1037
|
+
...machine,
|
|
1196
1038
|
env: {},
|
|
1197
1039
|
autoResume: !manageNetworking,
|
|
1198
|
-
idleTimeoutSeconds:
|
|
1199
|
-
//
|
|
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-
|
|
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 (
|
|
1211
|
-
const idempotencyKey = `agent-sandbox-${(await sha256Hex(`${ctx.
|
|
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
|
|
1215
|
-
log("computer_sandbox.
|
|
1216
|
-
let
|
|
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
|
|
1060
|
+
info = await awaitComputerRunning(client, info.id, runningWaitMs);
|
|
1219
1061
|
if (!manageNetworking) {
|
|
1220
|
-
if (
|
|
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
|
-
|
|
1225
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1250
|
-
let
|
|
1251
|
-
const
|
|
1252
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1096
|
+
seeding = null;
|
|
1281
1097
|
throw cause;
|
|
1282
1098
|
});
|
|
1283
|
-
let ready =
|
|
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:
|
|
1109
|
+
provider: COMPUTER_PROVIDER,
|
|
1294
1110
|
computerId: info.id
|
|
1295
1111
|
});
|
|
1296
1112
|
ready = false;
|
|
1297
1113
|
}
|
|
1298
1114
|
}
|
|
1299
|
-
|
|
1300
|
-
const
|
|
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:
|
|
1311
|
-
await prepareWorkspace();
|
|
1312
|
-
}
|
|
1126
|
+
beforeUse: fresh ? seedWorkspace : void 0
|
|
1313
1127
|
});
|
|
1314
1128
|
return {
|
|
1315
1129
|
handle,
|
|
1316
|
-
region: info.regionSlug ??
|
|
1130
|
+
region: info.regionSlug ?? region,
|
|
1317
1131
|
state: {
|
|
1318
|
-
|
|
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
|
|
1142
|
+
await cleanup?.();
|
|
1337
1143
|
} catch (cleanupError) {
|
|
1338
1144
|
cause = new AggregateError(
|
|
1339
1145
|
[error, cleanupError],
|
|
1340
|
-
"Computer
|
|
1146
|
+
"Computer attach and egress cleanup failed"
|
|
1341
1147
|
);
|
|
1342
1148
|
}
|
|
1343
|
-
if (
|
|
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}
|
|
1155
|
+
`computer ${info.name} attach and cleanup failed`
|
|
1350
1156
|
);
|
|
1351
1157
|
}
|
|
1352
1158
|
}
|
|
1353
1159
|
throw cause;
|
|
1354
1160
|
}
|
|
1355
1161
|
},
|
|
1356
|
-
|
|
1357
|
-
const state =
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
networkingManaged: state.networkPolicySaved
|
|
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
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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(
|
|
1196
|
+
await closeComputerMcp(computerId);
|
|
1571
1197
|
} catch (cause) {
|
|
1572
1198
|
failures.push(cause);
|
|
1573
1199
|
}
|
|
1574
1200
|
try {
|
|
1575
|
-
const client = clientFor(ctx
|
|
1201
|
+
const client = clientFor(ctx);
|
|
1576
1202
|
let current;
|
|
1577
1203
|
try {
|
|
1578
|
-
current = await client.getComputer(
|
|
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
|
|
1210
|
+
computerId,
|
|
1585
1211
|
state: current?.state ?? null
|
|
1586
1212
|
});
|
|
1587
1213
|
} else {
|
|
1588
1214
|
try {
|
|
1589
|
-
await client.action(
|
|
1215
|
+
await client.action(computerId, "pause");
|
|
1590
1216
|
} catch (error) {
|
|
1591
1217
|
if (!pauseMayHaveSettled(error)) throw error;
|
|
1592
|
-
const settled = await client.getComputer(
|
|
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
|
|
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
|
-
|
|
1612
|
-
networkPolicySaved: state.networkPolicySaved === true || savedPolicyWorkspaces.has(key)
|
|
1236
|
+
networkPolicySaved: state.networkPolicySaved || savedPolicyWorkspaces.has(workspaceKey(networkingCtx))
|
|
1613
1237
|
};
|
|
1614
1238
|
},
|
|
1615
|
-
|
|
1616
|
-
const state =
|
|
1617
|
-
const client = clientFor(ctx
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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:
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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:
|
|
1648
|
-
stateProtocolVersion:
|
|
1279
|
+
name: COMPUTER_PROVIDER,
|
|
1280
|
+
stateProtocolVersion: 2,
|
|
1649
1281
|
environment
|
|
1650
1282
|
});
|
|
1651
1283
|
};
|
|
1652
1284
|
export {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|