@claxedo/sandbox-manager 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/command.d.ts +3 -0
- package/dist/command.mjs +18 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.mjs +7 -0
- package/dist/daytona-allow-list.d.ts +5 -0
- package/dist/daytona-allow-list.mjs +9 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.mjs +14 -0
- package/dist/driver-catalog.d.ts +62 -0
- package/dist/driver-catalog.mjs +206 -0
- package/dist/drivers/cloudflare-egress.d.ts +40 -0
- package/dist/drivers/cloudflare-egress.mjs +115 -0
- package/dist/drivers/cloudflare.d.ts +37 -0
- package/dist/drivers/cloudflare.mjs +151 -0
- package/dist/drivers/daytona.d.ts +94 -0
- package/dist/drivers/daytona.mjs +235 -0
- package/dist/drivers/docker.d.ts +36 -0
- package/dist/drivers/docker.mjs +329 -0
- package/dist/drivers/fetch-bridge.d.ts +10 -0
- package/dist/drivers/fetch-bridge.mjs +104 -0
- package/dist/drivers/modal.d.ts +68 -0
- package/dist/drivers/modal.mjs +227 -0
- package/dist/drivers/vercel.d.ts +109 -0
- package/dist/drivers/vercel.mjs +297 -0
- package/dist/image-name.d.ts +7 -0
- package/dist/image.d.ts +8 -0
- package/dist/image.mjs +124 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.mjs +330 -0
- package/dist/lease-policy.d.ts +49 -0
- package/dist/lease-policy.mjs +126 -0
- package/dist/lease-types.d.ts +89 -0
- package/dist/lease-types.mjs +1 -0
- package/dist/runtime-env.d.ts +24 -0
- package/dist/runtime-env.mjs +35 -0
- package/dist/runtime-version.d.ts +1 -0
- package/dist/runtime-version.mjs +24 -0
- package/dist/service-url-exposure.d.ts +10 -0
- package/dist/service-url-exposure.mjs +46 -0
- package/dist/stores/memory.d.ts +6 -0
- package/dist/stores/memory.mjs +117 -0
- package/package.json +178 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/drivers/modal.ts
|
|
4
|
+
import { ModalClient, Probe } from "modal";
|
|
5
|
+
|
|
6
|
+
// src/runtime-env.ts
|
|
7
|
+
function workspaceRuntimeTargetEnv(input) {
|
|
8
|
+
return {
|
|
9
|
+
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
10
|
+
...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
|
|
11
|
+
WORKSPACE_RUNTIME_DIRECTORY: input.directory,
|
|
12
|
+
WORKSPACE_RUNTIME_PORT: String(input.port),
|
|
13
|
+
...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function workspaceRuntimeSourceEnv(input) {
|
|
17
|
+
if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
|
|
18
|
+
return {
|
|
19
|
+
WORKSPACE_RUNTIME_SOURCE_KIND: "git",
|
|
20
|
+
WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
|
|
21
|
+
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/command.ts
|
|
26
|
+
function shell(input) {
|
|
27
|
+
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/constants.ts
|
|
31
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
32
|
+
|
|
33
|
+
// src/image.ts
|
|
34
|
+
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
35
|
+
|
|
36
|
+
// src/runtime-version.ts
|
|
37
|
+
import { createRequire } from "module";
|
|
38
|
+
var require2 = createRequire(import.meta.url);
|
|
39
|
+
var versionCache;
|
|
40
|
+
function workspaceRuntimeVersion() {
|
|
41
|
+
if (versionCache) return versionCache;
|
|
42
|
+
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
43
|
+
if (envVersion) {
|
|
44
|
+
versionCache = envVersion;
|
|
45
|
+
return versionCache;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
49
|
+
return versionCache;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
versionCache = "0.0.0";
|
|
53
|
+
return versionCache;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/image-name.ts
|
|
57
|
+
var SNAPSHOT_SCHEMA_VERSION = 8;
|
|
58
|
+
var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
|
|
59
|
+
function snapshotVersion(value) {
|
|
60
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
61
|
+
}
|
|
62
|
+
function sandboxImageRepository(env = process.env) {
|
|
63
|
+
const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
|
|
64
|
+
return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
|
|
65
|
+
}
|
|
66
|
+
function buildIdSuffix(buildId, env) {
|
|
67
|
+
const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
|
|
68
|
+
return id ? `-${snapshotVersion(id)}` : "";
|
|
69
|
+
}
|
|
70
|
+
function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
71
|
+
return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
72
|
+
}
|
|
73
|
+
function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
74
|
+
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/image.ts
|
|
78
|
+
var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
79
|
+
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
80
|
+
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
81
|
+
|
|
82
|
+
// src/drivers/modal.ts
|
|
83
|
+
var DEFAULT_APP_NAME = "claxedo";
|
|
84
|
+
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
85
|
+
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
86
|
+
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
87
|
+
var DEFAULT_TUNNEL_TIMEOUT_MS = 3e4;
|
|
88
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
89
|
+
function nameFor(workspaceId) {
|
|
90
|
+
return `claxedo-${workspaceId}`;
|
|
91
|
+
}
|
|
92
|
+
function transientDriverError(err) {
|
|
93
|
+
const shaped = err;
|
|
94
|
+
const status = shaped.response?.status ?? shaped.status;
|
|
95
|
+
if (typeof status === "number" && status >= 500) return true;
|
|
96
|
+
const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
97
|
+
return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline");
|
|
98
|
+
}
|
|
99
|
+
function createModalSandboxDriver(options) {
|
|
100
|
+
const client = options.client ?? new ModalClient({
|
|
101
|
+
tokenId: options.tokenId,
|
|
102
|
+
tokenSecret: options.tokenSecret
|
|
103
|
+
});
|
|
104
|
+
const appName = options.appName ?? DEFAULT_APP_NAME;
|
|
105
|
+
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
106
|
+
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
107
|
+
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
108
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
109
|
+
const tunnelTimeoutMs = options.tunnelTimeoutMs ?? DEFAULT_TUNNEL_TIMEOUT_MS;
|
|
110
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
111
|
+
const readinessProbe = options.readinessProbe ?? ((port) => Probe.withTcp(port, { intervalMs: 1e3 }));
|
|
112
|
+
function workspaceDirectory(input) {
|
|
113
|
+
return input.workspaceRoot ?? workspaceDir;
|
|
114
|
+
}
|
|
115
|
+
function bootEnv(input, hostId) {
|
|
116
|
+
const env = {
|
|
117
|
+
...workspaceRuntimeTargetEnv({
|
|
118
|
+
workspaceId: input.workspaceId,
|
|
119
|
+
hostId,
|
|
120
|
+
directory: workspaceDirectory(input),
|
|
121
|
+
port: runtimePort,
|
|
122
|
+
host: "0.0.0.0"
|
|
123
|
+
}),
|
|
124
|
+
...workspaceRuntimeSourceEnv({ source: input.source }),
|
|
125
|
+
...input.env
|
|
126
|
+
};
|
|
127
|
+
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
128
|
+
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
129
|
+
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
130
|
+
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
131
|
+
return env;
|
|
132
|
+
}
|
|
133
|
+
async function image(input) {
|
|
134
|
+
if (input.bootSource?.kind === "driver-snapshot") return client.images.fromId(input.bootSource.snapshotId);
|
|
135
|
+
if (input.bootSource?.kind === "image") return client.images.fromId(input.bootSource.image);
|
|
136
|
+
if (input.snapshot) return client.images.fromId(input.snapshot);
|
|
137
|
+
return client.images.fromRegistry(options.baseImage ?? SANDBOX_IMAGE);
|
|
138
|
+
}
|
|
139
|
+
function network(input) {
|
|
140
|
+
if (!input.net || input.net.mode === "allow-all") return {};
|
|
141
|
+
if ((input.net.hosts ?? []).length > 0) {
|
|
142
|
+
throw new Error("Modal SandboxDriver cannot enforce host-based network policy");
|
|
143
|
+
}
|
|
144
|
+
return { blockNetwork: true };
|
|
145
|
+
}
|
|
146
|
+
async function readyTarget(input, sandbox, hostId) {
|
|
147
|
+
await sandbox.setTags(input.labels).catch(() => void 0);
|
|
148
|
+
const tunnels = await sandbox.tunnels(tunnelTimeoutMs).catch(() => void 0);
|
|
149
|
+
const tunnel = tunnels?.[runtimePort];
|
|
150
|
+
if (!tunnel?.url) return { provisioning: true, retryAfterMs: 2e3 };
|
|
151
|
+
return {
|
|
152
|
+
workspaceId: input.workspaceId,
|
|
153
|
+
sandboxId: sandbox.sandboxId,
|
|
154
|
+
url: tunnel.url,
|
|
155
|
+
hostId,
|
|
156
|
+
driverResourceId: sandbox.sandboxId,
|
|
157
|
+
driver: {
|
|
158
|
+
id: "modal",
|
|
159
|
+
resourceId: sandbox.sandboxId
|
|
160
|
+
},
|
|
161
|
+
labels: input.labels
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function ensureHost(input) {
|
|
165
|
+
const hostId = input.hostId ?? nameFor(input.workspaceId);
|
|
166
|
+
const directory = workspaceDirectory(input);
|
|
167
|
+
const env = {
|
|
168
|
+
...bootEnv(input, hostId),
|
|
169
|
+
...await options.env?.(input, { id: hostId })
|
|
170
|
+
};
|
|
171
|
+
const sandbox = await client.sandboxes.create(
|
|
172
|
+
await client.apps.fromName(appName, { createIfMissing: true }),
|
|
173
|
+
await image(input),
|
|
174
|
+
{
|
|
175
|
+
name: nameFor(input.workspaceId),
|
|
176
|
+
command: ["bash", "-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
|
|
177
|
+
workdir: "/",
|
|
178
|
+
env,
|
|
179
|
+
encryptedPorts: [runtimePort],
|
|
180
|
+
timeoutMs,
|
|
181
|
+
...options.idleTimeoutMs !== void 0 ? { idleTimeoutMs: options.idleTimeoutMs } : {},
|
|
182
|
+
tags: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
|
|
183
|
+
readinessProbe: readinessProbe(runtimePort),
|
|
184
|
+
...network(input)
|
|
185
|
+
}
|
|
186
|
+
).catch((err) => {
|
|
187
|
+
if (transientDriverError(err)) return void 0;
|
|
188
|
+
throw err;
|
|
189
|
+
});
|
|
190
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
191
|
+
return readyTarget(input, sandbox, hostId);
|
|
192
|
+
}
|
|
193
|
+
async function sandboxById(target) {
|
|
194
|
+
return client.sandboxes.fromId(target.sandboxId);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
id: "modal",
|
|
198
|
+
metadata: {
|
|
199
|
+
driverRunsIn: ["node"],
|
|
200
|
+
hostStopBehavior: "terminates-host",
|
|
201
|
+
hostResumeBehavior: "replacement-host",
|
|
202
|
+
targetAccess: "relay",
|
|
203
|
+
secretBrokering: "none"
|
|
204
|
+
},
|
|
205
|
+
ensureHost,
|
|
206
|
+
resumeHost: (input) => ensureHost(input.ensure),
|
|
207
|
+
async touch() {
|
|
208
|
+
},
|
|
209
|
+
async suspend(target) {
|
|
210
|
+
await (await sandboxById(target)).terminate();
|
|
211
|
+
},
|
|
212
|
+
async stop(target) {
|
|
213
|
+
await (await sandboxById(target)).terminate();
|
|
214
|
+
},
|
|
215
|
+
async destroy(target) {
|
|
216
|
+
await (await sandboxById(target)).terminate();
|
|
217
|
+
},
|
|
218
|
+
async snapshot(target) {
|
|
219
|
+
const snapshot = await (await sandboxById(target)).snapshotFilesystem(operationTimeoutMs);
|
|
220
|
+
if (!snapshot.imageId) throw new Error(`Modal snapshot did not return an image id for ${target.sandboxId}`);
|
|
221
|
+
return { snapshotId: snapshot.imageId };
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
createModalSandboxDriver
|
|
227
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type NetworkPolicy } from "@vercel/sandbox";
|
|
2
|
+
import type { SandboxBrokeredSecret, SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
3
|
+
type VercelSnapshotLike = {
|
|
4
|
+
snapshotId: string;
|
|
5
|
+
};
|
|
6
|
+
type VercelCommandLike = {
|
|
7
|
+
wait?: (params?: {
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}) => Promise<unknown>;
|
|
10
|
+
kill?: (signal?: string) => Promise<unknown>;
|
|
11
|
+
stdout?: (params?: {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}) => Promise<string>;
|
|
14
|
+
};
|
|
15
|
+
export type VercelSandboxLike = {
|
|
16
|
+
sandboxId: string;
|
|
17
|
+
domain: (port: number) => string;
|
|
18
|
+
runCommand: (command: string | {
|
|
19
|
+
cmd: string;
|
|
20
|
+
args?: string[];
|
|
21
|
+
cwd?: string;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
detached?: boolean;
|
|
24
|
+
sudo?: boolean;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}, args?: string[], opts?: {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}) => Promise<VercelCommandLike>;
|
|
29
|
+
extendTimeout: (duration: number, opts?: {
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
stop: (opts?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
blocking?: boolean;
|
|
35
|
+
}) => Promise<unknown>;
|
|
36
|
+
snapshot: (opts?: {
|
|
37
|
+
expiration?: number;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}) => Promise<VercelSnapshotLike>;
|
|
40
|
+
updateNetworkPolicy: (policy: NetworkPolicy, opts?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}) => Promise<NetworkPolicy>;
|
|
43
|
+
};
|
|
44
|
+
export type VercelSandboxFactoryLike = {
|
|
45
|
+
create: (params?: {
|
|
46
|
+
token?: string;
|
|
47
|
+
teamId?: string;
|
|
48
|
+
projectId?: string;
|
|
49
|
+
source?: {
|
|
50
|
+
type: "snapshot";
|
|
51
|
+
snapshotId: string;
|
|
52
|
+
} | {
|
|
53
|
+
type: "git";
|
|
54
|
+
url: string;
|
|
55
|
+
revision?: string;
|
|
56
|
+
depth?: number;
|
|
57
|
+
};
|
|
58
|
+
ports?: number[];
|
|
59
|
+
timeout?: number;
|
|
60
|
+
runtime?: string;
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
networkPolicy?: NetworkPolicy;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}) => Promise<VercelSandboxLike>;
|
|
65
|
+
get: (params: {
|
|
66
|
+
token?: string;
|
|
67
|
+
teamId?: string;
|
|
68
|
+
projectId?: string;
|
|
69
|
+
sandboxId: string;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
}) => Promise<VercelSandboxLike>;
|
|
72
|
+
};
|
|
73
|
+
export type VercelSandboxDriverOptions = {
|
|
74
|
+
token: string;
|
|
75
|
+
teamId: string;
|
|
76
|
+
projectId: string;
|
|
77
|
+
baseSnapshotId?: string;
|
|
78
|
+
runtime?: string;
|
|
79
|
+
runtimePort?: number;
|
|
80
|
+
runtimeCommand?: string;
|
|
81
|
+
workspaceDir?: string;
|
|
82
|
+
runner?: string;
|
|
83
|
+
controlEnv?: {
|
|
84
|
+
relayJwksUrl?: string;
|
|
85
|
+
relayVerifyPem?: string;
|
|
86
|
+
managementJwksUrl?: string;
|
|
87
|
+
};
|
|
88
|
+
env?: (input: SandboxDriverEnsureInput, host: {
|
|
89
|
+
id: string;
|
|
90
|
+
}) => Record<string, string> | Promise<Record<string, string>>;
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
snapshotBuildTimeoutMs?: number;
|
|
93
|
+
operationTimeoutMs?: number;
|
|
94
|
+
keepAliveMs?: number;
|
|
95
|
+
sandbox?: VercelSandboxFactoryLike;
|
|
96
|
+
snapshotCache?: Map<string, string>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Brokered-secret egress policy: Vercel's firewall injects the credential as
|
|
100
|
+
* a header on outbound requests to the allowlisted hosts, so the raw value
|
|
101
|
+
* never enters the sandbox (see @claxedo/sandbox-manager SandboxBrokeredSecret
|
|
102
|
+
* and https://vercel.com/docs/sandbox/concepts/firewall — credentials
|
|
103
|
+
* brokering). The resulting policy is deny-all-except-brokered-hosts, which is
|
|
104
|
+
* the secure default: a sandbox holding a brokered credential should only be
|
|
105
|
+
* able to reach the host that credential is for.
|
|
106
|
+
*/
|
|
107
|
+
export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[]): NetworkPolicy;
|
|
108
|
+
export declare function createVercelSandboxDriver(options: VercelSandboxDriverOptions): SandboxDriver;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/drivers/vercel.ts
|
|
4
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
5
|
+
|
|
6
|
+
// src/runtime-env.ts
|
|
7
|
+
function workspaceRuntimeTargetEnv(input) {
|
|
8
|
+
return {
|
|
9
|
+
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
10
|
+
...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
|
|
11
|
+
WORKSPACE_RUNTIME_DIRECTORY: input.directory,
|
|
12
|
+
WORKSPACE_RUNTIME_PORT: String(input.port),
|
|
13
|
+
...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function workspaceRuntimeSourceEnv(input) {
|
|
17
|
+
if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
|
|
18
|
+
return {
|
|
19
|
+
WORKSPACE_RUNTIME_SOURCE_KIND: "git",
|
|
20
|
+
WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
|
|
21
|
+
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/runtime-version.ts
|
|
26
|
+
import { createRequire } from "module";
|
|
27
|
+
var require2 = createRequire(import.meta.url);
|
|
28
|
+
var versionCache;
|
|
29
|
+
function workspaceRuntimeVersion() {
|
|
30
|
+
if (versionCache) return versionCache;
|
|
31
|
+
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
32
|
+
if (envVersion) {
|
|
33
|
+
versionCache = envVersion;
|
|
34
|
+
return versionCache;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
38
|
+
return versionCache;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
versionCache = "0.0.0";
|
|
42
|
+
return versionCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/command.ts
|
|
46
|
+
function shell(input) {
|
|
47
|
+
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/constants.ts
|
|
51
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
52
|
+
|
|
53
|
+
// src/defaults.ts
|
|
54
|
+
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
55
|
+
|
|
56
|
+
// src/drivers/vercel.ts
|
|
57
|
+
var DEFAULT_RUNTIME = "node22";
|
|
58
|
+
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
59
|
+
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
60
|
+
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
61
|
+
var DEFAULT_SNAPSHOT_BUILD_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
62
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
63
|
+
var DEFAULT_KEEP_ALIVE_MS = 5 * 60 * 1e3;
|
|
64
|
+
var EXPECTED_RUNTIME_VERSION = workspaceRuntimeVersion();
|
|
65
|
+
var RUNTIME_PKG = `@claxedo/workspace-runtime@${EXPECTED_RUNTIME_VERSION}`;
|
|
66
|
+
var snapshotCache = /* @__PURE__ */ new Map();
|
|
67
|
+
function nameFor(workspaceId) {
|
|
68
|
+
return `claxedo-${workspaceId}`;
|
|
69
|
+
}
|
|
70
|
+
function transientDriverError(err) {
|
|
71
|
+
const shaped = err;
|
|
72
|
+
const status = shaped.response?.status ?? shaped.status;
|
|
73
|
+
if (typeof status === "number" && status >= 500) return true;
|
|
74
|
+
const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
75
|
+
return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
|
|
76
|
+
}
|
|
77
|
+
function network(input) {
|
|
78
|
+
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
79
|
+
const hosts = input.net.hosts ?? [];
|
|
80
|
+
if (hosts.length === 0) return "deny-all";
|
|
81
|
+
return { allow: hosts };
|
|
82
|
+
}
|
|
83
|
+
function vercelBrokeredNetworkPolicy(secrets) {
|
|
84
|
+
const allow = {};
|
|
85
|
+
for (const secret of secrets) {
|
|
86
|
+
if (!secret.header) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (const host of secret.hosts) {
|
|
92
|
+
;
|
|
93
|
+
(allow[host] ??= []).push({ transform: [{ headers: { [secret.header]: secret.value } }] });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { allow };
|
|
97
|
+
}
|
|
98
|
+
function createVercelSandboxDriver(options) {
|
|
99
|
+
const sandboxFactory = options.sandbox ?? Sandbox;
|
|
100
|
+
const runtime = options.runtime ?? DEFAULT_RUNTIME;
|
|
101
|
+
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
102
|
+
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
103
|
+
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
104
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
105
|
+
const snapshotBuildTimeoutMs = options.snapshotBuildTimeoutMs ?? DEFAULT_SNAPSHOT_BUILD_TIMEOUT_MS;
|
|
106
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
107
|
+
const keepAliveMs = options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS;
|
|
108
|
+
const cache = options.snapshotCache ?? snapshotCache;
|
|
109
|
+
function workspaceDirectory(input) {
|
|
110
|
+
return input.workspaceRoot ?? workspaceDir;
|
|
111
|
+
}
|
|
112
|
+
function credentials() {
|
|
113
|
+
return {
|
|
114
|
+
token: options.token,
|
|
115
|
+
teamId: options.teamId,
|
|
116
|
+
projectId: options.projectId
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function bootEnv(input, hostId) {
|
|
120
|
+
const env = {
|
|
121
|
+
...workspaceRuntimeTargetEnv({
|
|
122
|
+
workspaceId: input.workspaceId,
|
|
123
|
+
hostId,
|
|
124
|
+
directory: workspaceDirectory(input),
|
|
125
|
+
port: runtimePort,
|
|
126
|
+
host: "0.0.0.0"
|
|
127
|
+
}),
|
|
128
|
+
...workspaceRuntimeSourceEnv({ source: input.source }),
|
|
129
|
+
...input.env
|
|
130
|
+
};
|
|
131
|
+
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
132
|
+
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
133
|
+
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
134
|
+
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
135
|
+
return env;
|
|
136
|
+
}
|
|
137
|
+
async function runSetup(sandbox, command) {
|
|
138
|
+
await sandbox.runCommand("bash", ["-c", command], {
|
|
139
|
+
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function ensureSnapshot() {
|
|
143
|
+
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
144
|
+
const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${EXPECTED_RUNTIME_VERSION}`;
|
|
145
|
+
const hit = cache.get(cacheKey);
|
|
146
|
+
if (hit) return hit;
|
|
147
|
+
const builder = await sandboxFactory.create({
|
|
148
|
+
...credentials(),
|
|
149
|
+
runtime,
|
|
150
|
+
ports: [runtimePort],
|
|
151
|
+
timeout: snapshotBuildTimeoutMs
|
|
152
|
+
});
|
|
153
|
+
await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
|
|
154
|
+
await runSetup(builder, [
|
|
155
|
+
"npm i -g --min-release-age=2 tsx@4.22.3 opencode-ai@1.15.10 @anthropic-ai/claude-code@2.1.150 @openai/codex@0.133.0 @google/gemini-cli@0.43.0 @earendil-works/pi-coding-agent@0.75.5",
|
|
156
|
+
"curl https://cursor.com/install -fsS | bash",
|
|
157
|
+
"curl -fsSL https://ampcode.com/install.sh | bash",
|
|
158
|
+
"curl -fsSL https://app.factory.ai/cli | sh",
|
|
159
|
+
"sudo ln -sf $HOME/.local/bin/agent /usr/local/bin/agent",
|
|
160
|
+
"sudo ln -sf $HOME/.local/bin/cursor-agent /usr/local/bin/cursor-agent",
|
|
161
|
+
"sudo ln -sf $HOME/.local/bin/amp /usr/local/bin/amp",
|
|
162
|
+
"sudo ln -sf $HOME/.local/bin/droid /usr/local/bin/droid",
|
|
163
|
+
"command -v opencode",
|
|
164
|
+
"command -v claude",
|
|
165
|
+
"command -v codex",
|
|
166
|
+
"command -v gemini",
|
|
167
|
+
"command -v agent",
|
|
168
|
+
"command -v cursor-agent",
|
|
169
|
+
"command -v amp",
|
|
170
|
+
"command -v droid",
|
|
171
|
+
"command -v pi"
|
|
172
|
+
].join(" && "));
|
|
173
|
+
await runSetup(builder, [
|
|
174
|
+
`sudo mkdir -p ${shell(RUNTIME_DIR)}`,
|
|
175
|
+
`sudo chown $(whoami) ${shell(RUNTIME_DIR)}`,
|
|
176
|
+
`cd ${shell(RUNTIME_DIR)}`,
|
|
177
|
+
"npm init -y",
|
|
178
|
+
`npm install ${RUNTIME_PKG} --min-release-age=2`,
|
|
179
|
+
`node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${EXPECTED_RUNTIME_VERSION}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${EXPECTED_RUNTIME_VERSION}, got '+actual)}"`,
|
|
180
|
+
"test -x node_modules/.bin/workspace-runtime",
|
|
181
|
+
"test -x node_modules/.bin/claude-agent-acp",
|
|
182
|
+
"test -x node_modules/.bin/codex-acp",
|
|
183
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
|
|
184
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/claude-agent-acp /usr/local/bin/claude-agent-acp`,
|
|
185
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
|
|
186
|
+
"command -v workspace-runtime",
|
|
187
|
+
"command -v claude-agent-acp",
|
|
188
|
+
"command -v codex-acp"
|
|
189
|
+
].join(" && "));
|
|
190
|
+
const snapshot = await builder.snapshot({
|
|
191
|
+
expiration: 0,
|
|
192
|
+
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
193
|
+
});
|
|
194
|
+
cache.set(cacheKey, snapshot.snapshotId);
|
|
195
|
+
return snapshot.snapshotId;
|
|
196
|
+
}
|
|
197
|
+
async function source(input) {
|
|
198
|
+
if (input.bootSource?.kind === "image") {
|
|
199
|
+
throw new Error("Vercel SandboxDriver does not support image boot; use driver snapshots");
|
|
200
|
+
}
|
|
201
|
+
const snapshotId = input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? await ensureSnapshot();
|
|
202
|
+
return { type: "snapshot", snapshotId };
|
|
203
|
+
}
|
|
204
|
+
async function startRuntime(sandbox, input, env) {
|
|
205
|
+
const directory = workspaceDirectory(input);
|
|
206
|
+
await sandbox.runCommand({
|
|
207
|
+
cmd: "bash",
|
|
208
|
+
args: ["-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
|
|
209
|
+
cwd: "/vercel/sandbox",
|
|
210
|
+
env,
|
|
211
|
+
detached: true,
|
|
212
|
+
signal: AbortSignal.timeout(operationTimeoutMs)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function readyTarget(input, sandbox, hostId) {
|
|
216
|
+
const url = sandbox.domain(runtimePort);
|
|
217
|
+
return {
|
|
218
|
+
workspaceId: input.workspaceId,
|
|
219
|
+
sandboxId: sandbox.sandboxId,
|
|
220
|
+
url,
|
|
221
|
+
hostId,
|
|
222
|
+
driverResourceId: sandbox.sandboxId,
|
|
223
|
+
driver: {
|
|
224
|
+
id: "vercel",
|
|
225
|
+
resourceId: sandbox.sandboxId
|
|
226
|
+
},
|
|
227
|
+
labels: input.labels
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function ensureHost(input) {
|
|
231
|
+
const hostId = input.hostId ?? nameFor(input.workspaceId);
|
|
232
|
+
const env = {
|
|
233
|
+
...bootEnv(input, hostId),
|
|
234
|
+
...await options.env?.(input, { id: hostId })
|
|
235
|
+
};
|
|
236
|
+
const sandbox = await sandboxFactory.create({
|
|
237
|
+
...credentials(),
|
|
238
|
+
source: await source(input),
|
|
239
|
+
ports: [runtimePort],
|
|
240
|
+
timeout: timeoutMs,
|
|
241
|
+
env,
|
|
242
|
+
...network(input) ? { networkPolicy: network(input) } : {}
|
|
243
|
+
}).catch((err) => {
|
|
244
|
+
if (transientDriverError(err)) return void 0;
|
|
245
|
+
throw err;
|
|
246
|
+
});
|
|
247
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
248
|
+
if (input.secrets?.length) {
|
|
249
|
+
await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets), {
|
|
250
|
+
signal: AbortSignal.timeout(operationTimeoutMs)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
await startRuntime(sandbox, input, env);
|
|
254
|
+
return readyTarget(input, sandbox, hostId);
|
|
255
|
+
}
|
|
256
|
+
async function sandboxById(target) {
|
|
257
|
+
return sandboxFactory.get({
|
|
258
|
+
...credentials(),
|
|
259
|
+
sandboxId: target.sandboxId
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
id: "vercel",
|
|
264
|
+
metadata: {
|
|
265
|
+
driverRunsIn: ["node"],
|
|
266
|
+
hostStopBehavior: "terminates-host",
|
|
267
|
+
hostResumeBehavior: "replacement-host",
|
|
268
|
+
targetAccess: "relay",
|
|
269
|
+
secretBrokering: "native"
|
|
270
|
+
},
|
|
271
|
+
ensureHost,
|
|
272
|
+
resumeHost: (input) => ensureHost(input.ensure),
|
|
273
|
+
async touch(target) {
|
|
274
|
+
await (await sandboxById(target)).extendTimeout(keepAliveMs).catch(() => void 0);
|
|
275
|
+
},
|
|
276
|
+
async suspend(target) {
|
|
277
|
+
await (await sandboxById(target)).stop({ blocking: false });
|
|
278
|
+
},
|
|
279
|
+
async stop(target) {
|
|
280
|
+
await (await sandboxById(target)).stop({ blocking: false });
|
|
281
|
+
},
|
|
282
|
+
async destroy(target) {
|
|
283
|
+
await (await sandboxById(target)).stop({ blocking: true });
|
|
284
|
+
},
|
|
285
|
+
async snapshot(target) {
|
|
286
|
+
const snapshot = await (await sandboxById(target)).snapshot({
|
|
287
|
+
expiration: 0,
|
|
288
|
+
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
289
|
+
});
|
|
290
|
+
return { snapshotId: snapshot.snapshotId };
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export {
|
|
295
|
+
createVercelSandboxDriver,
|
|
296
|
+
vercelBrokeredNetworkPolicy
|
|
297
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SNAPSHOT_SCHEMA_VERSION = 8;
|
|
2
|
+
export declare const DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
|
|
3
|
+
export type SandboxImageEnv = Record<string, string | undefined>;
|
|
4
|
+
export declare function snapshotVersion(value: string): string;
|
|
5
|
+
export declare function sandboxImageRepository(env?: SandboxImageEnv): string;
|
|
6
|
+
export declare function defaultSandboxImage(version?: string, buildId?: string, env?: SandboxImageEnv): string;
|
|
7
|
+
export declare function defaultSnapshotName(version?: string, buildId?: string, env?: SandboxImageEnv): string;
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Daytona, Image } from "@daytona/sdk";
|
|
2
|
+
export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
|
|
3
|
+
export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, defaultSandboxImage, defaultSnapshotName, } from "./image-name";
|
|
4
|
+
export declare const SANDBOX_IMAGE_REPOSITORY: string;
|
|
5
|
+
export declare const SANDBOX_IMAGE: string;
|
|
6
|
+
export declare const SNAPSHOT_NAME: string;
|
|
7
|
+
export declare function buildSandboxImage(): Image;
|
|
8
|
+
export declare function ensureSnapshot(daytona: Daytona): Promise<string>;
|