@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,151 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
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/drivers/cloudflare.ts
|
|
26
|
+
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
27
|
+
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
28
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
29
|
+
function cleanUrl(input) {
|
|
30
|
+
return input.replace(/\/+$/, "");
|
|
31
|
+
}
|
|
32
|
+
function sandboxIdFor(workspaceId) {
|
|
33
|
+
return `claxedo-${workspaceId}`;
|
|
34
|
+
}
|
|
35
|
+
function createCloudflareSandboxDriver(options) {
|
|
36
|
+
const doFetch = options.fetch ?? fetch;
|
|
37
|
+
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
38
|
+
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
39
|
+
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
40
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
41
|
+
const base = cleanUrl(options.workerUrl);
|
|
42
|
+
const headers = {
|
|
43
|
+
Authorization: `Bearer ${options.apiToken}`,
|
|
44
|
+
"Content-Type": "application/json"
|
|
45
|
+
};
|
|
46
|
+
async function call(sandboxId, action, body, method = "POST") {
|
|
47
|
+
const url = `${base}/sandbox/${encodeURIComponent(sandboxId)}${action ? `/${action}` : ""}`;
|
|
48
|
+
const res = await doFetch(url, {
|
|
49
|
+
method,
|
|
50
|
+
headers,
|
|
51
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json().catch(() => ({}));
|
|
55
|
+
return { status: res.status, data };
|
|
56
|
+
}
|
|
57
|
+
function workspaceDirectory(input) {
|
|
58
|
+
return input.workspaceRoot ?? workspaceDir;
|
|
59
|
+
}
|
|
60
|
+
async function bootEnv(input, hostId) {
|
|
61
|
+
const env = {
|
|
62
|
+
...workspaceRuntimeTargetEnv({
|
|
63
|
+
workspaceId: input.workspaceId,
|
|
64
|
+
hostId,
|
|
65
|
+
directory: workspaceDirectory(input),
|
|
66
|
+
port: runtimePort,
|
|
67
|
+
host: "0.0.0.0"
|
|
68
|
+
}),
|
|
69
|
+
...workspaceRuntimeSourceEnv({ source: input.source }),
|
|
70
|
+
...input.env,
|
|
71
|
+
...await options.env?.(input, { id: hostId })
|
|
72
|
+
};
|
|
73
|
+
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
74
|
+
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
75
|
+
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
76
|
+
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
79
|
+
function egressRegistrations(input) {
|
|
80
|
+
if (!input.secrets?.length) return void 0;
|
|
81
|
+
return input.secrets.map((secret) => {
|
|
82
|
+
if (!secret.header) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`cloudflare brokered secret "${secret.name}" requires a header \u2014 the egress proxy injects the value as an HTTP header`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (secret.hosts.length === 0) {
|
|
88
|
+
throw new Error(`cloudflare brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
89
|
+
}
|
|
90
|
+
return { hosts: secret.hosts, header: secret.header, value: secret.value };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function ensureHost(input) {
|
|
94
|
+
const sandboxId = sandboxIdFor(input.workspaceId);
|
|
95
|
+
const hostId = sandboxId;
|
|
96
|
+
const egress = egressRegistrations(input);
|
|
97
|
+
const { status, data } = await call(
|
|
98
|
+
sandboxId,
|
|
99
|
+
"ensure-runtime",
|
|
100
|
+
{
|
|
101
|
+
env: await bootEnv(input, hostId),
|
|
102
|
+
port: runtimePort,
|
|
103
|
+
command: runtimeCommand,
|
|
104
|
+
...egress ? { egress } : {}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
if (status >= 500) return { provisioning: true, retryAfterMs: 2e3 };
|
|
108
|
+
if (status >= 400 || !data?.url) {
|
|
109
|
+
throw new Error(`Cloudflare ensure-runtime failed (${status}): ${data?.error ?? "no runtime url"}`);
|
|
110
|
+
}
|
|
111
|
+
const targetOut = {
|
|
112
|
+
workspaceId: input.workspaceId,
|
|
113
|
+
sandboxId,
|
|
114
|
+
url: data.url,
|
|
115
|
+
hostId,
|
|
116
|
+
driverResourceId: sandboxId,
|
|
117
|
+
driver: {
|
|
118
|
+
id: "cloudflare",
|
|
119
|
+
resourceId: sandboxId
|
|
120
|
+
},
|
|
121
|
+
labels: input.labels
|
|
122
|
+
};
|
|
123
|
+
return targetOut;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id: "cloudflare",
|
|
127
|
+
metadata: {
|
|
128
|
+
driverRunsIn: ["worker"],
|
|
129
|
+
hostStopBehavior: "not-supported",
|
|
130
|
+
hostResumeBehavior: "same-host",
|
|
131
|
+
targetAccess: "relay",
|
|
132
|
+
secretBrokering: "proxy"
|
|
133
|
+
},
|
|
134
|
+
ensureHost,
|
|
135
|
+
async touch(target) {
|
|
136
|
+
await call(target.sandboxId, "touch-runtime", { port: runtimePort }).catch(() => void 0);
|
|
137
|
+
},
|
|
138
|
+
async stop(target) {
|
|
139
|
+
void target;
|
|
140
|
+
},
|
|
141
|
+
async destroy(target) {
|
|
142
|
+
const { status } = await call(target.sandboxId, "", {}, "DELETE");
|
|
143
|
+
if (status >= 400 && status !== 404) {
|
|
144
|
+
throw new Error(`Cloudflare destroy failed (${status}) for ${target.sandboxId}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
createCloudflareSandboxDriver
|
|
151
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
2
|
+
export type DaytonaSandboxLike = {
|
|
3
|
+
id: string;
|
|
4
|
+
state?: string;
|
|
5
|
+
labels?: Record<string, string>;
|
|
6
|
+
process: {
|
|
7
|
+
executeCommand: (command: string, cwd?: string, env?: Record<string, string>, timeout?: number) => Promise<unknown>;
|
|
8
|
+
};
|
|
9
|
+
getPreviewLink: (port: number) => Promise<{
|
|
10
|
+
url?: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
}>;
|
|
13
|
+
getSignedPreviewUrl: (port: number, expiresInSeconds?: number) => Promise<{
|
|
14
|
+
url?: string;
|
|
15
|
+
token?: string;
|
|
16
|
+
}>;
|
|
17
|
+
refreshActivity: () => Promise<void>;
|
|
18
|
+
start: (timeout?: number) => Promise<void>;
|
|
19
|
+
stop: (timeout?: number) => Promise<void>;
|
|
20
|
+
delete: (timeout?: number) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export type DaytonaClientLike = {
|
|
23
|
+
findByLabels?: (labels: Record<string, string>) => Promise<DaytonaSandboxLike | undefined>;
|
|
24
|
+
list?: (labels?: Record<string, string>, page?: number, limit?: number) => Promise<{
|
|
25
|
+
items?: DaytonaSandboxLike[];
|
|
26
|
+
}>;
|
|
27
|
+
create: (params: {
|
|
28
|
+
name?: string;
|
|
29
|
+
image?: string;
|
|
30
|
+
snapshot?: string;
|
|
31
|
+
envVars?: Record<string, string>;
|
|
32
|
+
/** Env-var-name → Daytona secret-name references (brokered secrets). */
|
|
33
|
+
secrets?: Record<string, string>;
|
|
34
|
+
labels?: Record<string, string>;
|
|
35
|
+
public?: boolean;
|
|
36
|
+
autoStopInterval?: number;
|
|
37
|
+
autoDeleteInterval?: number;
|
|
38
|
+
networkBlockAll?: boolean;
|
|
39
|
+
networkAllowList?: string;
|
|
40
|
+
}, options?: {
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}) => Promise<DaytonaSandboxLike>;
|
|
43
|
+
get: (sandboxIdOrName: string) => Promise<DaytonaSandboxLike>;
|
|
44
|
+
/**
|
|
45
|
+
* Create-or-update an org secret whose value is only substituted by
|
|
46
|
+
* Daytona's egress proxy for requests to `hosts`
|
|
47
|
+
* (https://www.daytona.io/docs/en/secrets/). Referenced from `create`'s
|
|
48
|
+
* `secrets` map; inside the sandbox the env var holds only an opaque
|
|
49
|
+
* `dtn_secret_…` placeholder, never the value.
|
|
50
|
+
*/
|
|
51
|
+
upsertSecret?: (params: {
|
|
52
|
+
name: string;
|
|
53
|
+
value: string;
|
|
54
|
+
hosts: string[];
|
|
55
|
+
}) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
export type DaytonaSandboxDriverOptions = {
|
|
58
|
+
/** Daytona API base URL. Defaults to the SDK default. */
|
|
59
|
+
apiUrl?: string;
|
|
60
|
+
/** Daytona API key. */
|
|
61
|
+
apiKey: string;
|
|
62
|
+
/** Optional Daytona organization id. */
|
|
63
|
+
organizationId?: string;
|
|
64
|
+
/** Optional Daytona target/region. */
|
|
65
|
+
target?: string;
|
|
66
|
+
/** Snapshot the sandbox boots from. */
|
|
67
|
+
baseSnapshot: string;
|
|
68
|
+
/** Port the workspace-runtime listens on inside the sandbox. */
|
|
69
|
+
runtimePort?: number;
|
|
70
|
+
/** Path to the workspace-runtime binary inside the image. */
|
|
71
|
+
runtimeCommand?: string;
|
|
72
|
+
/** Workspace directory inside the sandbox. */
|
|
73
|
+
workspaceDir?: string;
|
|
74
|
+
/** Default runner injected as WORKSPACE_RUNTIME_RUNNER. */
|
|
75
|
+
runner?: string;
|
|
76
|
+
/** Static control-plane config injected so the runtime verifies relay-proxied requests. */
|
|
77
|
+
controlEnv?: {
|
|
78
|
+
relayJwksUrl?: string;
|
|
79
|
+
relayVerifyPem?: string;
|
|
80
|
+
managementJwksUrl?: string;
|
|
81
|
+
};
|
|
82
|
+
/** Dynamic runtime env that needs the acquired sandbox id or current lease. */
|
|
83
|
+
env?: (input: SandboxDriverEnsureInput, sandbox: DaytonaSandboxLike) => Record<string, string> | Promise<Record<string, string>>;
|
|
84
|
+
/** Auto-stop / auto-delete policy, in minutes. */
|
|
85
|
+
autoStopMinutes?: number;
|
|
86
|
+
autoDeleteMinutes?: number;
|
|
87
|
+
/** Signed preview URL expiry, in seconds. */
|
|
88
|
+
previewExpirySeconds?: number;
|
|
89
|
+
/** Daytona SDK operation timeout, in seconds. */
|
|
90
|
+
operationTimeoutSeconds?: number;
|
|
91
|
+
/** Injected for tests. */
|
|
92
|
+
client?: DaytonaClientLike;
|
|
93
|
+
};
|
|
94
|
+
export declare function createDaytonaSandboxDriver(options: DaytonaSandboxDriverOptions): SandboxDriver;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/drivers/daytona.ts
|
|
4
|
+
import { Daytona } from "@daytona/sdk";
|
|
5
|
+
|
|
6
|
+
// src/daytona-allow-list.ts
|
|
7
|
+
function formatDaytonaAllowList(cidrs) {
|
|
8
|
+
return cidrs.slice(0, 10).join(",");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/runtime-env.ts
|
|
12
|
+
function workspaceRuntimeTargetEnv(input) {
|
|
13
|
+
return {
|
|
14
|
+
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
15
|
+
...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
|
|
16
|
+
WORKSPACE_RUNTIME_DIRECTORY: input.directory,
|
|
17
|
+
WORKSPACE_RUNTIME_PORT: String(input.port),
|
|
18
|
+
...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function workspaceRuntimeSourceEnv(input) {
|
|
22
|
+
if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
|
|
23
|
+
return {
|
|
24
|
+
WORKSPACE_RUNTIME_SOURCE_KIND: "git",
|
|
25
|
+
WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
|
|
26
|
+
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/command.ts
|
|
31
|
+
function shell(input) {
|
|
32
|
+
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/constants.ts
|
|
36
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
37
|
+
|
|
38
|
+
// src/drivers/daytona.ts
|
|
39
|
+
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
40
|
+
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
41
|
+
var DEFAULT_PREVIEW_EXPIRY_S = 3600;
|
|
42
|
+
var DEFAULT_OPERATION_TIMEOUT_S = 60;
|
|
43
|
+
function labelName(workspaceId) {
|
|
44
|
+
return `claxedo-${workspaceId}`;
|
|
45
|
+
}
|
|
46
|
+
function daytonaSecretName(workspaceId, secretName) {
|
|
47
|
+
const safe = (value) => value.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
48
|
+
return `claxedo-${safe(workspaceId)}-${safe(secretName)}`;
|
|
49
|
+
}
|
|
50
|
+
function transientDriverError(err) {
|
|
51
|
+
const shaped = err;
|
|
52
|
+
const status = shaped.response?.status ?? shaped.status;
|
|
53
|
+
if (typeof status === "number" && status >= 500) return true;
|
|
54
|
+
const text = `${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
55
|
+
return text.includes("timeout") || text.includes("pending") || text.includes("starting");
|
|
56
|
+
}
|
|
57
|
+
function createDefaultClient(options) {
|
|
58
|
+
const sdk = new Daytona({
|
|
59
|
+
apiKey: options.apiKey,
|
|
60
|
+
...options.apiUrl ? { apiUrl: options.apiUrl } : {},
|
|
61
|
+
...options.organizationId ? { organizationId: options.organizationId } : {},
|
|
62
|
+
...options.target ? { target: options.target } : {},
|
|
63
|
+
_experimental: { otelEnabled: false }
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
async findByLabels(labels) {
|
|
67
|
+
for await (const sandbox of sdk.list({ labels })) return sandbox;
|
|
68
|
+
return void 0;
|
|
69
|
+
},
|
|
70
|
+
create(params, operation) {
|
|
71
|
+
if (params.image) return sdk.create({ ...params, image: params.image }, operation);
|
|
72
|
+
return sdk.create(params, operation);
|
|
73
|
+
},
|
|
74
|
+
get: (sandboxIdOrName) => sdk.get(sandboxIdOrName),
|
|
75
|
+
async upsertSecret({ name, value, hosts }) {
|
|
76
|
+
await sdk.secret.create({ name, value, hosts });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createDaytonaSandboxDriver(options) {
|
|
81
|
+
const client = options.client ?? createDefaultClient(options);
|
|
82
|
+
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
83
|
+
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
84
|
+
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
85
|
+
const previewExpiry = options.previewExpirySeconds ?? DEFAULT_PREVIEW_EXPIRY_S;
|
|
86
|
+
const operationTimeout = options.operationTimeoutSeconds ?? DEFAULT_OPERATION_TIMEOUT_S;
|
|
87
|
+
function workspaceDirectory(input) {
|
|
88
|
+
return input.workspaceRoot ?? workspaceDir;
|
|
89
|
+
}
|
|
90
|
+
function staticBootEnv(input, hostId, directory) {
|
|
91
|
+
const env = {
|
|
92
|
+
...workspaceRuntimeTargetEnv({
|
|
93
|
+
workspaceId: input.workspaceId,
|
|
94
|
+
hostId,
|
|
95
|
+
directory,
|
|
96
|
+
port: runtimePort
|
|
97
|
+
}),
|
|
98
|
+
...workspaceRuntimeSourceEnv({ source: input.source }),
|
|
99
|
+
...input.env
|
|
100
|
+
};
|
|
101
|
+
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
102
|
+
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
103
|
+
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
104
|
+
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
105
|
+
return env;
|
|
106
|
+
}
|
|
107
|
+
async function bootEnv(input, sandbox, hostId) {
|
|
108
|
+
return {
|
|
109
|
+
...staticBootEnv(input, hostId, workspaceDirectory(input)),
|
|
110
|
+
...await options.env?.(input, sandbox)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function findExisting(workspaceId) {
|
|
114
|
+
const labels = { "claxedo.workspaceId": workspaceId };
|
|
115
|
+
if (client.findByLabels) return client.findByLabels(labels).catch(() => void 0);
|
|
116
|
+
const result = await client.list?.(labels, 1, 1).catch(() => void 0);
|
|
117
|
+
return result?.items?.[0];
|
|
118
|
+
}
|
|
119
|
+
async function sandboxById(sandboxId) {
|
|
120
|
+
return client.get(sandboxId);
|
|
121
|
+
}
|
|
122
|
+
async function previewUrl(sandbox) {
|
|
123
|
+
const signed = await sandbox.getSignedPreviewUrl(runtimePort, previewExpiry).catch(() => void 0);
|
|
124
|
+
if (signed?.url) return signed;
|
|
125
|
+
return sandbox.getPreviewLink(runtimePort).catch(() => void 0);
|
|
126
|
+
}
|
|
127
|
+
async function startRuntime(sandbox, input, env) {
|
|
128
|
+
const directory = workspaceDirectory(input);
|
|
129
|
+
const script = `if (command -v ss >/dev/null && ss -ltn 2>/dev/null | grep -q :${runtimePort}); then exit 0; fi; mkdir -p ${shell(directory)}; cd ${shell(directory)}; nohup ${runtimeCommand} > /tmp/claxedo-wr.log 2>&1 & sleep 1`;
|
|
130
|
+
await sandbox.process.executeCommand(`sh -lc ${shell(script)}`, directory, env, operationTimeout).catch(() => void 0);
|
|
131
|
+
}
|
|
132
|
+
async function ensureStarted(sandbox) {
|
|
133
|
+
if (!sandbox.state || sandbox.state === "started") return true;
|
|
134
|
+
if (sandbox.state === "starting") return false;
|
|
135
|
+
await sandbox.start(operationTimeout).catch(() => void 0);
|
|
136
|
+
return sandbox.state === "started";
|
|
137
|
+
}
|
|
138
|
+
async function readyTarget(input, sandbox, hostId) {
|
|
139
|
+
await startRuntime(sandbox, input, await bootEnv(input, sandbox, hostId));
|
|
140
|
+
const preview = await previewUrl(sandbox);
|
|
141
|
+
if (!preview?.url) return { provisioning: true, retryAfterMs: 2e3 };
|
|
142
|
+
const target = {
|
|
143
|
+
workspaceId: input.workspaceId,
|
|
144
|
+
sandboxId: sandbox.id,
|
|
145
|
+
url: preview.url,
|
|
146
|
+
hostId,
|
|
147
|
+
driverResourceId: sandbox.id,
|
|
148
|
+
driver: {
|
|
149
|
+
id: "daytona",
|
|
150
|
+
resourceId: sandbox.id
|
|
151
|
+
},
|
|
152
|
+
labels: { ...input.labels, ...preview.token ? { "daytona.previewToken": preview.token } : {} }
|
|
153
|
+
};
|
|
154
|
+
return target;
|
|
155
|
+
}
|
|
156
|
+
async function brokeredSecretReferences(input) {
|
|
157
|
+
if (!input.secrets?.length) return {};
|
|
158
|
+
if (!client.upsertSecret) {
|
|
159
|
+
throw new Error("daytona sandbox client does not support secret brokering");
|
|
160
|
+
}
|
|
161
|
+
const references = {};
|
|
162
|
+
for (const secret of input.secrets) {
|
|
163
|
+
if (secret.hosts.length === 0) {
|
|
164
|
+
throw new Error(`daytona brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
165
|
+
}
|
|
166
|
+
const secretName = daytonaSecretName(input.workspaceId, secret.name);
|
|
167
|
+
await client.upsertSecret({ name: secretName, value: secret.value, hosts: secret.hosts });
|
|
168
|
+
references[secret.name] = secretName;
|
|
169
|
+
}
|
|
170
|
+
return references;
|
|
171
|
+
}
|
|
172
|
+
async function ensureHost(input) {
|
|
173
|
+
const hostId = labelName(input.workspaceId);
|
|
174
|
+
const bootSource = input.bootSource?.kind === "image" ? { image: input.bootSource.image } : { snapshot: input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? options.baseSnapshot };
|
|
175
|
+
const net = network(input);
|
|
176
|
+
const secrets = await brokeredSecretReferences(input);
|
|
177
|
+
const sandbox = await findExisting(input.workspaceId) ?? await client.create({
|
|
178
|
+
name: labelName(input.workspaceId),
|
|
179
|
+
...bootSource,
|
|
180
|
+
envVars: staticBootEnv(input, hostId, workspaceDirectory(input)),
|
|
181
|
+
...Object.keys(secrets).length ? { secrets } : {},
|
|
182
|
+
labels: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
|
|
183
|
+
public: false,
|
|
184
|
+
...net,
|
|
185
|
+
...options.autoStopMinutes !== void 0 ? { autoStopInterval: options.autoStopMinutes } : {},
|
|
186
|
+
...options.autoDeleteMinutes !== void 0 ? { autoDeleteInterval: options.autoDeleteMinutes } : {}
|
|
187
|
+
}, { timeout: operationTimeout }).catch((err) => {
|
|
188
|
+
if (transientDriverError(err)) return void 0;
|
|
189
|
+
throw err;
|
|
190
|
+
});
|
|
191
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
192
|
+
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
193
|
+
return readyTarget(input, sandbox, hostId);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
id: "daytona",
|
|
197
|
+
metadata: {
|
|
198
|
+
driverRunsIn: ["worker", "node"],
|
|
199
|
+
hostStopBehavior: "suspends-host",
|
|
200
|
+
hostResumeBehavior: "same-host",
|
|
201
|
+
targetAccess: "relay",
|
|
202
|
+
secretBrokering: "native"
|
|
203
|
+
},
|
|
204
|
+
ensureHost,
|
|
205
|
+
async resumeHost(input) {
|
|
206
|
+
const sandbox = await sandboxById(input.lease.sandboxId);
|
|
207
|
+
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
208
|
+
return readyTarget(input.ensure, sandbox, input.ensure.hostId ?? labelName(input.ensure.workspaceId));
|
|
209
|
+
},
|
|
210
|
+
async touch(target) {
|
|
211
|
+
await sandboxById(target.sandboxId).then((sandbox) => sandbox.refreshActivity()).catch(() => void 0);
|
|
212
|
+
},
|
|
213
|
+
async suspend(target) {
|
|
214
|
+
await sandboxById(target.sandboxId).then((sandbox) => sandbox.stop(operationTimeout)).catch(() => void 0);
|
|
215
|
+
},
|
|
216
|
+
async stop(target) {
|
|
217
|
+
await sandboxById(target.sandboxId).then((sandbox) => sandbox.stop(operationTimeout)).catch(() => void 0);
|
|
218
|
+
},
|
|
219
|
+
async destroy(target) {
|
|
220
|
+
await sandboxById(target.sandboxId).then((sandbox) => sandbox.delete(operationTimeout)).catch((err) => {
|
|
221
|
+
const status = err.response?.status ?? err.status;
|
|
222
|
+
if (status !== 404) throw err;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function network(input) {
|
|
228
|
+
if (!input.net || input.net.mode === "allow-all") return {};
|
|
229
|
+
const cidrs = input.net.cidrs ?? [];
|
|
230
|
+
if (cidrs.length === 0) return { networkBlockAll: true };
|
|
231
|
+
return { networkAllowList: formatDaytonaAllowList(cidrs) };
|
|
232
|
+
}
|
|
233
|
+
export {
|
|
234
|
+
createDaytonaSandboxDriver
|
|
235
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
2
|
+
type SandboxEnv = Record<string, string | undefined>;
|
|
3
|
+
export type DockerCommandResult = {
|
|
4
|
+
stdout?: string | Buffer;
|
|
5
|
+
stderr?: string | Buffer;
|
|
6
|
+
};
|
|
7
|
+
export type DockerCommandRunner = (args: string[], timeoutMs?: number) => Promise<DockerCommandResult>;
|
|
8
|
+
export type DockerSandboxDriverOptions = {
|
|
9
|
+
image: string;
|
|
10
|
+
runtimePort?: number;
|
|
11
|
+
runtimeCommand?: string;
|
|
12
|
+
workspaceDir?: string;
|
|
13
|
+
runner?: string;
|
|
14
|
+
env?: (input: SandboxDriverEnsureInput, host: {
|
|
15
|
+
id: string;
|
|
16
|
+
}) => Record<string, string> | Promise<Record<string, string>>;
|
|
17
|
+
syncLocalAuth?: boolean;
|
|
18
|
+
authHome?: string;
|
|
19
|
+
operationTimeoutMs?: number;
|
|
20
|
+
docker?: DockerCommandRunner;
|
|
21
|
+
};
|
|
22
|
+
export declare function dockerSandboxSyncLocalAuth(env?: SandboxEnv): boolean;
|
|
23
|
+
export declare function dockerSandboxAuthHome(env?: SandboxEnv): string;
|
|
24
|
+
export declare function dockerSandboxImageFromEnv(env?: SandboxEnv): string;
|
|
25
|
+
export declare function parseDockerPort(input: string): string | undefined;
|
|
26
|
+
export declare function dockerLocalAuthCandidates(home: string): ({
|
|
27
|
+
kind: "file";
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "dir";
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
})[];
|
|
35
|
+
export declare function createDockerSandboxDriver(options: DockerSandboxDriverOptions): SandboxDriver;
|
|
36
|
+
export {};
|