@claxedo/sandbox-manager 0.5.1 → 0.5.2
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/README.md +49 -0
- package/dist/command.mjs +0 -2
- package/dist/constants.mjs +0 -2
- package/dist/daytona-allow-list.mjs +0 -2
- package/dist/defaults.mjs +0 -2
- package/dist/driver-catalog.d.ts +8 -5
- package/dist/driver-catalog.mjs +20 -11
- package/dist/drivers/box.d.ts +52 -0
- package/dist/drivers/box.mjs +304 -0
- package/dist/drivers/cloudflare-egress.mjs +0 -2
- package/dist/drivers/cloudflare.mjs +8 -5
- package/dist/drivers/daytona.mjs +0 -2
- package/dist/drivers/docker.d.ts +5 -0
- package/dist/drivers/docker.mjs +28 -10
- package/dist/drivers/fetch-bridge.mjs +0 -2
- package/dist/drivers/modal.mjs +2 -10
- package/dist/drivers/vercel.d.ts +1 -0
- package/dist/drivers/vercel.mjs +76 -60
- package/dist/image.mjs +2 -10
- package/dist/index.mjs +0 -2
- package/dist/lease-policy.mjs +7 -2
- package/dist/lease-types.mjs +0 -1
- package/dist/runtime-env.mjs +0 -2
- package/dist/runtime-version.mjs +2 -10
- package/dist/service-url-exposure.mjs +0 -2
- package/dist/stores/memory.mjs +0 -2
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -13,6 +13,55 @@ The package owns the generic pieces:
|
|
|
13
13
|
|
|
14
14
|
It deliberately does not own Claxedo product auth, billing, Convex schema, SQLite app storage, routes, or relay tokens. Applications provide those through adapters and call `createSandboxManager`.
|
|
15
15
|
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @claxedo/sandbox-manager
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
A `SandboxManager` is three parts wired together: a `SandboxLeaseStore` (where
|
|
25
|
+
lease state lives), a `SandboxDriver` (how a sandbox is actually placed), and
|
|
26
|
+
`createSandboxManager` (the epoch/retry orchestration on top of both). This
|
|
27
|
+
example uses the in-memory lease store and the Docker driver, so it runs
|
|
28
|
+
end-to-end with only a local Docker daemon — no provider account needed:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createSandboxManager } from "@claxedo/sandbox-manager"
|
|
32
|
+
import { createMemoryLeaseStore } from "@claxedo/sandbox-manager/stores/memory"
|
|
33
|
+
import { createDockerSandboxDriver } from "@claxedo/sandbox-manager/drivers/docker"
|
|
34
|
+
import { SANDBOX_IMAGE } from "@claxedo/sandbox-manager/image"
|
|
35
|
+
|
|
36
|
+
const manager = createSandboxManager({
|
|
37
|
+
leaseStore: createMemoryLeaseStore(),
|
|
38
|
+
driver: createDockerSandboxDriver({ image: SANDBOX_IMAGE }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const result = await manager.ensure("workspace-1", { homeRegion: "local" })
|
|
42
|
+
|
|
43
|
+
if (result.status === "ready") {
|
|
44
|
+
console.log(`sandbox ready at ${result.url} (sandboxId: ${result.sandboxId})`)
|
|
45
|
+
} else if (result.status === "provisioning") {
|
|
46
|
+
console.log(`still provisioning, retry in ${result.retryAfterMs}ms`)
|
|
47
|
+
} else {
|
|
48
|
+
console.error(`unavailable: ${result.error}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Later, on the same workspace: `ensure` re-resolves the existing lease
|
|
52
|
+
// instead of placing a new sandbox.
|
|
53
|
+
await manager.stop("workspace-1")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Swap `createDockerSandboxDriver` for `createDaytonaSandboxDriver`,
|
|
57
|
+
`createModalSandboxDriver`, `createVercelSandboxDriver`,
|
|
58
|
+
`createCloudflareSandboxDriver`, or `createBoxSandboxDriver` (all under
|
|
59
|
+
`@claxedo/sandbox-manager/drivers/*`) to place on a hosted provider instead —
|
|
60
|
+
see [`docs/architecture.md`](docs/architecture.md) for the full driver
|
|
61
|
+
comparison and each provider's required options. Swap `createMemoryLeaseStore`
|
|
62
|
+
for a persisted `SandboxLeaseStore` implementation (e.g. backed by SQLite or
|
|
63
|
+
Convex) to survive process restarts.
|
|
64
|
+
|
|
16
65
|
## Credentials & secrets
|
|
17
66
|
|
|
18
67
|
There are two distinct channels for getting values into a sandbox, chosen by
|
package/dist/command.mjs
CHANGED
package/dist/constants.mjs
CHANGED
package/dist/defaults.mjs
CHANGED
package/dist/driver-catalog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SandboxDriverMetadata } from "./index";
|
|
2
|
-
export declare const sandboxDriverIds: readonly ["daytona", "modal", "vercel", "cloudflare", "docker"];
|
|
2
|
+
export declare const sandboxDriverIds: readonly ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
3
3
|
export type SandboxDriverID = (typeof sandboxDriverIds)[number];
|
|
4
4
|
export type SandboxDriverAuth = {
|
|
5
5
|
daytona?: {
|
|
@@ -18,6 +18,9 @@ export type SandboxDriverAuth = {
|
|
|
18
18
|
api_token?: string;
|
|
19
19
|
worker_url?: string;
|
|
20
20
|
};
|
|
21
|
+
box?: {
|
|
22
|
+
api_key?: string;
|
|
23
|
+
};
|
|
21
24
|
docker?: {
|
|
22
25
|
image?: string;
|
|
23
26
|
};
|
|
@@ -38,15 +41,15 @@ export type SandboxDriverCatalogEntry = {
|
|
|
38
41
|
};
|
|
39
42
|
export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
|
|
40
43
|
export declare function isSandboxDriverID(input: string | undefined): input is SandboxDriverID;
|
|
41
|
-
export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "
|
|
44
|
+
export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel" | undefined;
|
|
42
45
|
export declare function defaultSandboxDriverID(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): SandboxDriverID;
|
|
43
46
|
export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
|
|
44
47
|
export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
|
|
45
48
|
export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
|
|
46
49
|
export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv, managedDriverIds?: ReadonlySet<string>): {
|
|
47
|
-
default_driver: "
|
|
50
|
+
default_driver: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
|
|
48
51
|
drivers: {
|
|
49
|
-
id: "
|
|
52
|
+
id: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
|
|
50
53
|
label: string;
|
|
51
54
|
fields: {
|
|
52
55
|
key: string;
|
|
@@ -54,7 +57,7 @@ export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: Sand
|
|
|
54
57
|
secret?: boolean;
|
|
55
58
|
}[];
|
|
56
59
|
configured: boolean;
|
|
57
|
-
source: "
|
|
60
|
+
source: "config" | "managed" | "none";
|
|
58
61
|
default: boolean;
|
|
59
62
|
}[];
|
|
60
63
|
};
|
package/dist/driver-catalog.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/runtime-version.ts
|
|
4
|
-
|
|
5
|
-
var require2 = createRequire(import.meta.url);
|
|
2
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
6
3
|
var versionCache;
|
|
7
4
|
function workspaceRuntimeVersion() {
|
|
8
5
|
if (versionCache) return versionCache;
|
|
@@ -11,12 +8,7 @@ function workspaceRuntimeVersion() {
|
|
|
11
8
|
versionCache = envVersion;
|
|
12
9
|
return versionCache;
|
|
13
10
|
}
|
|
14
|
-
|
|
15
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
16
|
-
return versionCache;
|
|
17
|
-
} catch {
|
|
18
|
-
}
|
|
19
|
-
versionCache = "0.0.0";
|
|
11
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
20
12
|
return versionCache;
|
|
21
13
|
}
|
|
22
14
|
|
|
@@ -39,7 +31,7 @@ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env =
|
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
// src/driver-catalog.ts
|
|
42
|
-
var sandboxDriverIds = ["daytona", "modal", "vercel", "cloudflare", "docker"];
|
|
34
|
+
var sandboxDriverIds = ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
43
35
|
var sandboxDriverCatalog = {
|
|
44
36
|
daytona: {
|
|
45
37
|
id: "daytona",
|
|
@@ -99,6 +91,18 @@ var sandboxDriverCatalog = {
|
|
|
99
91
|
secretBrokering: "proxy"
|
|
100
92
|
}
|
|
101
93
|
},
|
|
94
|
+
box: {
|
|
95
|
+
id: "box",
|
|
96
|
+
label: "Box",
|
|
97
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
98
|
+
metadata: {
|
|
99
|
+
driverRunsIn: ["node"],
|
|
100
|
+
hostStopBehavior: "suspends-host",
|
|
101
|
+
hostResumeBehavior: "same-host",
|
|
102
|
+
targetAccess: "relay",
|
|
103
|
+
secretBrokering: "none"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
102
106
|
docker: {
|
|
103
107
|
id: "docker",
|
|
104
108
|
label: "Docker",
|
|
@@ -128,6 +132,7 @@ function sandboxDriverAuth(cfg, id, env = process.env) {
|
|
|
128
132
|
if (id === "modal") return authModal(cfg, env);
|
|
129
133
|
if (id === "vercel") return authVercel(cfg, env);
|
|
130
134
|
if (id === "cloudflare") return authCloudflare(cfg, env);
|
|
135
|
+
if (id === "box") return authBox(cfg, env);
|
|
131
136
|
return authDocker(cfg, env);
|
|
132
137
|
}
|
|
133
138
|
function hasSandboxDriverAuth(cfg, id, env = process.env) {
|
|
@@ -187,6 +192,10 @@ function authCloudflare(cfg, env) {
|
|
|
187
192
|
const worker_url = clean(cfg?.auth?.cloudflare?.worker_url) ?? clean(env.CLOUDFLARE_SANDBOX_WORKER_URL);
|
|
188
193
|
return api_token && worker_url ? { api_token, worker_url } : void 0;
|
|
189
194
|
}
|
|
195
|
+
function authBox(cfg, env) {
|
|
196
|
+
const api_key = clean(cfg?.auth?.box?.api_key) ?? clean(env.BOX_API_KEY);
|
|
197
|
+
return api_key ? { api_key } : void 0;
|
|
198
|
+
}
|
|
190
199
|
function authDocker(cfg, env) {
|
|
191
200
|
if (!dockerSandboxEnabled(env)) return;
|
|
192
201
|
return {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
2
|
+
export type BoxApiBox = {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
state: string;
|
|
6
|
+
url?: string | null;
|
|
7
|
+
ip?: string | null;
|
|
8
|
+
subdomain?: string | null;
|
|
9
|
+
createdAt?: string | null;
|
|
10
|
+
};
|
|
11
|
+
export type BoxCommandResult = {
|
|
12
|
+
stdout?: string;
|
|
13
|
+
stderr?: string;
|
|
14
|
+
exitCode?: number;
|
|
15
|
+
};
|
|
16
|
+
export type BoxFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
17
|
+
export type BoxSandboxDriverOptions = {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
image?: string;
|
|
21
|
+
runtimePort?: number;
|
|
22
|
+
runtimeCommand?: string;
|
|
23
|
+
workspaceDir?: string;
|
|
24
|
+
runner?: string;
|
|
25
|
+
/** Container name used for the runtime container inside the box. */
|
|
26
|
+
containerName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Box auto-archives after this many seconds of inactivity. `null` (default)
|
|
29
|
+
* disables auto-archival so the SandboxManager owns the lifecycle; a number
|
|
30
|
+
* lets Box reclaim idle boxes to save cost.
|
|
31
|
+
*/
|
|
32
|
+
ttlSeconds?: number | null;
|
|
33
|
+
/**
|
|
34
|
+
* Optional `docker login` before the pull, for a private runtime registry
|
|
35
|
+
* (the default image lives on ghcr.io). Omit for public images.
|
|
36
|
+
*/
|
|
37
|
+
registryAuth?: {
|
|
38
|
+
server: string;
|
|
39
|
+
username: string;
|
|
40
|
+
password: string;
|
|
41
|
+
};
|
|
42
|
+
env?: (input: SandboxDriverEnsureInput, host: {
|
|
43
|
+
id: string;
|
|
44
|
+
}) => Record<string, string> | Promise<Record<string, string>>;
|
|
45
|
+
provisionTimeoutMs?: number;
|
|
46
|
+
provisionIntervalMs?: number;
|
|
47
|
+
healthTimeoutMs?: number;
|
|
48
|
+
healthIntervalMs?: number;
|
|
49
|
+
operationTimeoutMs?: number;
|
|
50
|
+
fetchImpl?: BoxFetch;
|
|
51
|
+
};
|
|
52
|
+
export declare function createBoxSandboxDriver(options: BoxSandboxDriverOptions): SandboxDriver;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/runtime-env.ts
|
|
2
|
+
function workspaceRuntimeTargetEnv(input) {
|
|
3
|
+
return {
|
|
4
|
+
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
5
|
+
...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
|
|
6
|
+
WORKSPACE_RUNTIME_DIRECTORY: input.directory,
|
|
7
|
+
WORKSPACE_RUNTIME_PORT: String(input.port),
|
|
8
|
+
...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function workspaceRuntimeSourceEnv(input) {
|
|
12
|
+
if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
|
|
13
|
+
return {
|
|
14
|
+
WORKSPACE_RUNTIME_SOURCE_KIND: "git",
|
|
15
|
+
WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
|
|
16
|
+
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/command.ts
|
|
21
|
+
function shell(input) {
|
|
22
|
+
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/constants.ts
|
|
26
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
27
|
+
|
|
28
|
+
// src/image.ts
|
|
29
|
+
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
30
|
+
|
|
31
|
+
// src/runtime-version.ts
|
|
32
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
33
|
+
var versionCache;
|
|
34
|
+
function workspaceRuntimeVersion() {
|
|
35
|
+
if (versionCache) return versionCache;
|
|
36
|
+
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
37
|
+
if (envVersion) {
|
|
38
|
+
versionCache = envVersion;
|
|
39
|
+
return versionCache;
|
|
40
|
+
}
|
|
41
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
42
|
+
return versionCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/image-name.ts
|
|
46
|
+
var SNAPSHOT_SCHEMA_VERSION = 8;
|
|
47
|
+
var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
|
|
48
|
+
function snapshotVersion(value) {
|
|
49
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
50
|
+
}
|
|
51
|
+
function sandboxImageRepository(env = process.env) {
|
|
52
|
+
const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
|
|
53
|
+
return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
|
|
54
|
+
}
|
|
55
|
+
function buildIdSuffix(buildId, env) {
|
|
56
|
+
const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
|
|
57
|
+
return id ? `-${snapshotVersion(id)}` : "";
|
|
58
|
+
}
|
|
59
|
+
function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
60
|
+
return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
61
|
+
}
|
|
62
|
+
function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
63
|
+
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/image.ts
|
|
67
|
+
var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
68
|
+
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
69
|
+
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
70
|
+
|
|
71
|
+
// src/drivers/box.ts
|
|
72
|
+
var DEFAULT_BASE_URL = "https://ascii.dev/api/box/v1";
|
|
73
|
+
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
74
|
+
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
75
|
+
var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
|
|
76
|
+
var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
|
|
77
|
+
var DEFAULT_PROVISION_INTERVAL_MS = 2e3;
|
|
78
|
+
var DEFAULT_HEALTH_TIMEOUT_MS = 6e4;
|
|
79
|
+
var DEFAULT_HEALTH_INTERVAL_MS = 1e3;
|
|
80
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 12e4;
|
|
81
|
+
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
82
|
+
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
83
|
+
function sleep(ms) {
|
|
84
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
85
|
+
}
|
|
86
|
+
function clean(input) {
|
|
87
|
+
const txt = input?.trim();
|
|
88
|
+
return txt ? txt : void 0;
|
|
89
|
+
}
|
|
90
|
+
function transientDriverError(err) {
|
|
91
|
+
const shaped = err;
|
|
92
|
+
if (typeof shaped.status === "number" && shaped.status >= 500) return true;
|
|
93
|
+
const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
94
|
+
return text.includes("timeout") || text.includes("unavailable") || text.includes("econnreset") || text.includes("network");
|
|
95
|
+
}
|
|
96
|
+
var BoxApiError = class extends Error {
|
|
97
|
+
status;
|
|
98
|
+
constructor(message, status) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "BoxApiError";
|
|
101
|
+
this.status = status;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
function createBoxSandboxDriver(options) {
|
|
105
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
106
|
+
const image = options.image ?? SANDBOX_IMAGE;
|
|
107
|
+
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
108
|
+
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
109
|
+
const containerName = options.containerName ?? DEFAULT_CONTAINER_NAME;
|
|
110
|
+
const ttlSeconds = options.ttlSeconds === void 0 ? null : options.ttlSeconds;
|
|
111
|
+
const provisionTimeoutMs = options.provisionTimeoutMs ?? DEFAULT_PROVISION_TIMEOUT_MS;
|
|
112
|
+
const provisionIntervalMs = options.provisionIntervalMs ?? DEFAULT_PROVISION_INTERVAL_MS;
|
|
113
|
+
const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
114
|
+
const healthIntervalMs = options.healthIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
|
|
115
|
+
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
116
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
117
|
+
async function api(path, init) {
|
|
118
|
+
let res;
|
|
119
|
+
try {
|
|
120
|
+
res = await fetchImpl(`${baseUrl}${path}`, {
|
|
121
|
+
method: init?.method ?? "GET",
|
|
122
|
+
headers: {
|
|
123
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
124
|
+
...init?.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
125
|
+
},
|
|
126
|
+
...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
127
|
+
signal: AbortSignal.timeout(operationTimeoutMs)
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new BoxApiError(err instanceof Error ? err.message : String(err));
|
|
131
|
+
}
|
|
132
|
+
const text = await res.text();
|
|
133
|
+
const body = text ? JSON.parse(text) : {};
|
|
134
|
+
if (!res.ok || body.ok === false) {
|
|
135
|
+
const detail = clean(body.error) ?? clean(body.message) ?? res.statusText;
|
|
136
|
+
throw new BoxApiError(`Box API ${init?.method ?? "GET"} ${path} failed: ${detail}`, res.status);
|
|
137
|
+
}
|
|
138
|
+
return body;
|
|
139
|
+
}
|
|
140
|
+
function runtimePort(input) {
|
|
141
|
+
return input.workspaceRuntimePort ?? options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
142
|
+
}
|
|
143
|
+
function workspaceDirectory(input) {
|
|
144
|
+
return input.workspaceRoot ?? workspaceDir;
|
|
145
|
+
}
|
|
146
|
+
function resolveImage(input) {
|
|
147
|
+
if (input.bootSource?.kind === "image") return input.bootSource.image;
|
|
148
|
+
return input.snapshot ?? image;
|
|
149
|
+
}
|
|
150
|
+
function bootEnv(input, hostId) {
|
|
151
|
+
const env = {
|
|
152
|
+
...workspaceRuntimeTargetEnv({
|
|
153
|
+
workspaceId: input.workspaceId,
|
|
154
|
+
hostId,
|
|
155
|
+
directory: workspaceDirectory(input),
|
|
156
|
+
port: runtimePort(input),
|
|
157
|
+
host: "0.0.0.0"
|
|
158
|
+
}),
|
|
159
|
+
...workspaceRuntimeSourceEnv({ source: input.source }),
|
|
160
|
+
...input.env
|
|
161
|
+
};
|
|
162
|
+
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
163
|
+
return env;
|
|
164
|
+
}
|
|
165
|
+
async function exec(boxId, command) {
|
|
166
|
+
const body = await api(`/boxes/${boxId}/commands`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: { command }
|
|
169
|
+
});
|
|
170
|
+
const result = body.result ?? body;
|
|
171
|
+
return {
|
|
172
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
173
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
174
|
+
exitCode: typeof result.exitCode === "number" ? result.exitCode : 0
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function execOrThrow(boxId, command, label) {
|
|
178
|
+
const result = await exec(boxId, command);
|
|
179
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
180
|
+
throw new BoxApiError(`Box ${boxId} ${label} failed (exit ${result.exitCode}): ${clean(result.stderr) ?? clean(result.stdout) ?? ""}`);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
async function waitUntilReady(boxId) {
|
|
185
|
+
const until = Date.now() + provisionTimeoutMs;
|
|
186
|
+
for (; ; ) {
|
|
187
|
+
const { box } = await api(`/boxes/${boxId}`);
|
|
188
|
+
if (READY_STATES.has(box.state)) return true;
|
|
189
|
+
if (!PENDING_STATES.has(box.state)) {
|
|
190
|
+
throw new BoxApiError(`Box ${boxId} entered non-ready state: ${box.state}`);
|
|
191
|
+
}
|
|
192
|
+
if (Date.now() >= until) return false;
|
|
193
|
+
await sleep(provisionIntervalMs);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function dockerRunScript(input, env) {
|
|
197
|
+
const port = runtimePort(input);
|
|
198
|
+
const directory = workspaceDirectory(input);
|
|
199
|
+
const bootScript = [`mkdir -p ${shell(directory)}`, `cd ${shell(directory)}`, `exec ${runtimeCommand}`].join(" && ");
|
|
200
|
+
const envArgs = Object.entries(env).map(([key, value]) => `--env ${shell(`${key}=${value}`)}`).join(" ");
|
|
201
|
+
const login = options.registryAuth ? `echo ${shell(options.registryAuth.password)} | docker login ${shell(options.registryAuth.server)} --username ${shell(options.registryAuth.username)} --password-stdin && ` : "";
|
|
202
|
+
return `${login}docker rm -f ${containerName} >/dev/null 2>&1 || true; docker run -d --name ${containerName} -p ${port}:${port} ${envArgs} --entrypoint sh ${shell(resolveImage(input))} -lc ${shell(bootScript)}`;
|
|
203
|
+
}
|
|
204
|
+
async function waitForHealth(boxId, input) {
|
|
205
|
+
const port = runtimePort(input);
|
|
206
|
+
const until = Date.now() + healthTimeoutMs;
|
|
207
|
+
let last = "workspace runtime not ready";
|
|
208
|
+
while (Date.now() < until) {
|
|
209
|
+
const probe = await exec(
|
|
210
|
+
boxId,
|
|
211
|
+
`curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:${port}/global/health || true`
|
|
212
|
+
);
|
|
213
|
+
const code = clean(probe.stdout);
|
|
214
|
+
if (code === "200") return;
|
|
215
|
+
last = `health probe returned ${code ?? "no response"}`;
|
|
216
|
+
await sleep(healthIntervalMs);
|
|
217
|
+
}
|
|
218
|
+
throw new BoxApiError(`Box ${boxId} runtime did not become healthy: ${last}`);
|
|
219
|
+
}
|
|
220
|
+
async function boot(boxId, input, hostId) {
|
|
221
|
+
const env = {
|
|
222
|
+
...bootEnv(input, hostId),
|
|
223
|
+
...await options.env?.(input, { id: hostId })
|
|
224
|
+
};
|
|
225
|
+
const port = runtimePort(input);
|
|
226
|
+
await execOrThrow(boxId, dockerRunScript(input, env), "docker run");
|
|
227
|
+
await execOrThrow(boxId, `host ${port}`, "host publish");
|
|
228
|
+
await waitForHealth(boxId, input);
|
|
229
|
+
const urlResult = await execOrThrow(boxId, `host url ${port}`, "host url");
|
|
230
|
+
const url = clean(urlResult.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop());
|
|
231
|
+
if (!url) throw new BoxApiError(`Box ${boxId} did not return a public URL for port ${port}`);
|
|
232
|
+
return {
|
|
233
|
+
workspaceId: input.workspaceId,
|
|
234
|
+
sandboxId: boxId,
|
|
235
|
+
url,
|
|
236
|
+
hostId,
|
|
237
|
+
driverResourceId: boxId,
|
|
238
|
+
driver: { id: "box", resourceId: boxId },
|
|
239
|
+
labels: input.labels
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function assertNetwork(input) {
|
|
243
|
+
if (input.net && input.net.mode !== "allow-all") {
|
|
244
|
+
throw new BoxApiError("Box SandboxDriver cannot enforce host-based network policy");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function ensureHost(input) {
|
|
248
|
+
assertNetwork(input);
|
|
249
|
+
const hostId = input.hostId ?? `box-${input.workspaceId}`;
|
|
250
|
+
const created = await api("/boxes", {
|
|
251
|
+
method: "POST",
|
|
252
|
+
body: { ttlSeconds }
|
|
253
|
+
}).catch((err) => {
|
|
254
|
+
if (transientDriverError(err)) return void 0;
|
|
255
|
+
throw err;
|
|
256
|
+
});
|
|
257
|
+
if (!created) return { provisioning: true, retryAfterMs: provisionIntervalMs };
|
|
258
|
+
const boxId = created.box.id;
|
|
259
|
+
const ready = await waitUntilReady(boxId);
|
|
260
|
+
if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
|
|
261
|
+
return boot(boxId, input, hostId);
|
|
262
|
+
}
|
|
263
|
+
async function resumeHost(input) {
|
|
264
|
+
const boxId = input.lease.sandboxId;
|
|
265
|
+
if (!boxId) return ensureHost(input.ensure);
|
|
266
|
+
assertNetwork(input.ensure);
|
|
267
|
+
const hostId = input.ensure.hostId ?? input.lease.hostId ?? `box-${input.ensure.workspaceId}`;
|
|
268
|
+
try {
|
|
269
|
+
await api(`/boxes/${boxId}/resume`, { method: "POST" });
|
|
270
|
+
const ready = await waitUntilReady(boxId);
|
|
271
|
+
if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
|
|
272
|
+
return await boot(boxId, input.ensure, hostId);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (transientDriverError(err)) return { provisioning: true, retryAfterMs: provisionIntervalMs };
|
|
275
|
+
return ensureHost(input.ensure);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function stop(target) {
|
|
279
|
+
await api(`/boxes/${target.sandboxId}/stop`, { method: "POST" });
|
|
280
|
+
}
|
|
281
|
+
async function destroy(target) {
|
|
282
|
+
await api(`/boxes/${target.sandboxId}`, { method: "DELETE" });
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
id: "box",
|
|
286
|
+
metadata: {
|
|
287
|
+
driverRunsIn: ["node"],
|
|
288
|
+
hostStopBehavior: "suspends-host",
|
|
289
|
+
hostResumeBehavior: "same-host",
|
|
290
|
+
targetAccess: "relay",
|
|
291
|
+
secretBrokering: "none"
|
|
292
|
+
},
|
|
293
|
+
ensureHost,
|
|
294
|
+
resumeHost,
|
|
295
|
+
async touch() {
|
|
296
|
+
},
|
|
297
|
+
suspend: stop,
|
|
298
|
+
stop,
|
|
299
|
+
destroy
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
createBoxSandboxDriver
|
|
304
|
+
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/constants.ts
|
|
4
2
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
5
3
|
|
|
@@ -25,7 +23,7 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
25
23
|
// src/drivers/cloudflare.ts
|
|
26
24
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
27
25
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
28
|
-
var DEFAULT_TIMEOUT_MS =
|
|
26
|
+
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
29
27
|
function cleanUrl(input) {
|
|
30
28
|
return input.replace(/\/+$/, "");
|
|
31
29
|
}
|
|
@@ -94,7 +92,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
94
92
|
const sandboxId = sandboxIdFor(input.workspaceId);
|
|
95
93
|
const hostId = sandboxId;
|
|
96
94
|
const egress = egressRegistrations(input);
|
|
97
|
-
const
|
|
95
|
+
const response = await call(
|
|
98
96
|
sandboxId,
|
|
99
97
|
"ensure-runtime",
|
|
100
98
|
{
|
|
@@ -103,7 +101,12 @@ function createCloudflareSandboxDriver(options) {
|
|
|
103
101
|
command: runtimeCommand,
|
|
104
102
|
...egress ? { egress } : {}
|
|
105
103
|
}
|
|
106
|
-
)
|
|
104
|
+
).catch((error) => {
|
|
105
|
+
if (error instanceof Error && error.name === "TimeoutError") return void 0;
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
if (!response) return { provisioning: true, retryAfterMs: 2e3 };
|
|
109
|
+
const { status, data } = response;
|
|
107
110
|
if (status >= 500) return { provisioning: true, retryAfterMs: 2e3 };
|
|
108
111
|
if (status >= 400 || !data?.url) {
|
|
109
112
|
throw new Error(`Cloudflare ensure-runtime failed (${status}): ${data?.error ?? "no runtime url"}`);
|
package/dist/drivers/daytona.mjs
CHANGED
package/dist/drivers/docker.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type DockerCommandResult = {
|
|
|
5
5
|
stderr?: string | Buffer;
|
|
6
6
|
};
|
|
7
7
|
export type DockerCommandRunner = (args: string[], timeoutMs?: number) => Promise<DockerCommandResult>;
|
|
8
|
+
export type DockerHealthFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
8
9
|
export type DockerSandboxDriverOptions = {
|
|
9
10
|
image: string;
|
|
10
11
|
runtimePort?: number;
|
|
@@ -17,6 +18,10 @@ export type DockerSandboxDriverOptions = {
|
|
|
17
18
|
syncLocalAuth?: boolean;
|
|
18
19
|
authHome?: string;
|
|
19
20
|
operationTimeoutMs?: number;
|
|
21
|
+
waitForHealth?: boolean;
|
|
22
|
+
healthTimeoutMs?: number;
|
|
23
|
+
healthIntervalMs?: number;
|
|
24
|
+
healthFetch?: DockerHealthFetch;
|
|
20
25
|
docker?: DockerCommandRunner;
|
|
21
26
|
};
|
|
22
27
|
export declare function dockerSandboxSyncLocalAuth(env?: SandboxEnv): boolean;
|
package/dist/drivers/docker.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/drivers/docker.ts
|
|
4
2
|
import { execFile } from "node:child_process";
|
|
5
3
|
import fs from "node:fs/promises";
|
|
@@ -38,8 +36,7 @@ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
|
38
36
|
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
39
37
|
|
|
40
38
|
// src/runtime-version.ts
|
|
41
|
-
|
|
42
|
-
var require2 = createRequire(import.meta.url);
|
|
39
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
43
40
|
var versionCache;
|
|
44
41
|
function workspaceRuntimeVersion() {
|
|
45
42
|
if (versionCache) return versionCache;
|
|
@@ -48,12 +45,7 @@ function workspaceRuntimeVersion() {
|
|
|
48
45
|
versionCache = envVersion;
|
|
49
46
|
return versionCache;
|
|
50
47
|
}
|
|
51
|
-
|
|
52
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
53
|
-
return versionCache;
|
|
54
|
-
} catch {
|
|
55
|
-
}
|
|
56
|
-
versionCache = "0.0.0";
|
|
48
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
57
49
|
return versionCache;
|
|
58
50
|
}
|
|
59
51
|
|
|
@@ -86,6 +78,8 @@ var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
|
86
78
|
// src/drivers/docker.ts
|
|
87
79
|
var execFileAsync = promisify(execFile);
|
|
88
80
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
81
|
+
var DEFAULT_HEALTH_TIMEOUT_MS = 2e4;
|
|
82
|
+
var DEFAULT_HEALTH_INTERVAL_MS = 250;
|
|
89
83
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
90
84
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
91
85
|
var DOCKER_LABEL = "claxedo";
|
|
@@ -145,6 +139,9 @@ async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS) {
|
|
|
145
139
|
timeout: timeoutMs
|
|
146
140
|
});
|
|
147
141
|
}
|
|
142
|
+
function sleep(ms) {
|
|
143
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
148
145
|
async function stageLocalAgentAuth(containerId, input) {
|
|
149
146
|
if (!input.syncLocalAuth) return;
|
|
150
147
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "claxedo-docker-auth-"));
|
|
@@ -203,6 +200,9 @@ function createDockerSandboxDriver(options) {
|
|
|
203
200
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
204
201
|
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
205
202
|
const timeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
203
|
+
const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
204
|
+
const healthIntervalMs = options.healthIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
|
|
205
|
+
const healthFetch = options.healthFetch ?? fetch;
|
|
206
206
|
const syncLocalAuth = options.syncLocalAuth ?? dockerSandboxSyncLocalAuth();
|
|
207
207
|
const authHome = options.authHome ?? dockerSandboxAuthHome();
|
|
208
208
|
function runtimePort(input) {
|
|
@@ -236,6 +236,7 @@ function createDockerSandboxDriver(options) {
|
|
|
236
236
|
const out = await dockerCommand(["port", sandboxId, `${runtimePort(input)}/tcp`], timeoutMs);
|
|
237
237
|
const url = parseDockerPort(String(out.stdout ?? ""));
|
|
238
238
|
if (!url) throw new Error(`Docker container ${sandboxId} does not expose workspace runtime port ${runtimePort(input)}`);
|
|
239
|
+
if (options.waitForHealth !== false) await waitForHealth(url);
|
|
239
240
|
return {
|
|
240
241
|
workspaceId: input.workspaceId,
|
|
241
242
|
sandboxId,
|
|
@@ -249,6 +250,23 @@ function createDockerSandboxDriver(options) {
|
|
|
249
250
|
labels: input.labels
|
|
250
251
|
};
|
|
251
252
|
}
|
|
253
|
+
async function waitForHealth(url) {
|
|
254
|
+
const until = Date.now() + healthTimeoutMs;
|
|
255
|
+
let error = "workspace runtime not ready";
|
|
256
|
+
while (Date.now() < until) {
|
|
257
|
+
try {
|
|
258
|
+
const res = await healthFetch(`${url}/global/health`, {
|
|
259
|
+
signal: AbortSignal.timeout(Math.min(2e3, Math.max(1, healthTimeoutMs)))
|
|
260
|
+
});
|
|
261
|
+
if (res.ok) return;
|
|
262
|
+
error = `${res.status} ${res.statusText}`;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
error = err instanceof Error ? err.message : String(err);
|
|
265
|
+
}
|
|
266
|
+
await sleep(healthIntervalMs);
|
|
267
|
+
}
|
|
268
|
+
throw new Error(error);
|
|
269
|
+
}
|
|
252
270
|
async function ensureHost(input) {
|
|
253
271
|
if (input.net && input.net.mode !== "allow-all") {
|
|
254
272
|
throw new Error("Docker SandboxDriver cannot enforce restricted network policies");
|
package/dist/drivers/modal.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/drivers/modal.ts
|
|
4
2
|
import { ModalClient, Probe } from "modal";
|
|
5
3
|
|
|
@@ -34,8 +32,7 @@ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
|
34
32
|
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
35
33
|
|
|
36
34
|
// src/runtime-version.ts
|
|
37
|
-
|
|
38
|
-
var require2 = createRequire(import.meta.url);
|
|
35
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
39
36
|
var versionCache;
|
|
40
37
|
function workspaceRuntimeVersion() {
|
|
41
38
|
if (versionCache) return versionCache;
|
|
@@ -44,12 +41,7 @@ function workspaceRuntimeVersion() {
|
|
|
44
41
|
versionCache = envVersion;
|
|
45
42
|
return versionCache;
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
49
|
-
return versionCache;
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
versionCache = "0.0.0";
|
|
44
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
53
45
|
return versionCache;
|
|
54
46
|
}
|
|
55
47
|
|
package/dist/drivers/vercel.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export type VercelSandboxDriverOptions = {
|
|
|
94
94
|
keepAliveMs?: number;
|
|
95
95
|
sandbox?: VercelSandboxFactoryLike;
|
|
96
96
|
snapshotCache?: Map<string, string>;
|
|
97
|
+
now?: () => number;
|
|
97
98
|
};
|
|
98
99
|
/**
|
|
99
100
|
* Brokered-secret egress policy: Vercel's firewall injects the credential as
|
package/dist/drivers/vercel.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/drivers/vercel.ts
|
|
4
2
|
import { Sandbox } from "@vercel/sandbox";
|
|
5
3
|
|
|
@@ -23,8 +21,7 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
// src/runtime-version.ts
|
|
26
|
-
|
|
27
|
-
var require2 = createRequire(import.meta.url);
|
|
24
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
28
25
|
var versionCache;
|
|
29
26
|
function workspaceRuntimeVersion() {
|
|
30
27
|
if (versionCache) return versionCache;
|
|
@@ -33,12 +30,7 @@ function workspaceRuntimeVersion() {
|
|
|
33
30
|
versionCache = envVersion;
|
|
34
31
|
return versionCache;
|
|
35
32
|
}
|
|
36
|
-
|
|
37
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
38
|
-
return versionCache;
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
versionCache = "0.0.0";
|
|
33
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
42
34
|
return versionCache;
|
|
43
35
|
}
|
|
44
36
|
|
|
@@ -63,7 +55,9 @@ var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
|
63
55
|
var DEFAULT_KEEP_ALIVE_MS = 5 * 60 * 1e3;
|
|
64
56
|
var EXPECTED_RUNTIME_VERSION = workspaceRuntimeVersion();
|
|
65
57
|
var RUNTIME_PKG = `@claxedo/workspace-runtime@${EXPECTED_RUNTIME_VERSION}`;
|
|
58
|
+
var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
66
59
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
60
|
+
var snapshotCacheState = /* @__PURE__ */ new WeakMap();
|
|
67
61
|
function nameFor(workspaceId) {
|
|
68
62
|
return `claxedo-${workspaceId}`;
|
|
69
63
|
}
|
|
@@ -106,6 +100,12 @@ function createVercelSandboxDriver(options) {
|
|
|
106
100
|
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
107
101
|
const keepAliveMs = options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS;
|
|
108
102
|
const cache = options.snapshotCache ?? snapshotCache;
|
|
103
|
+
const now = options.now ?? Date.now;
|
|
104
|
+
const cacheState = snapshotCacheState.get(cache) ?? {
|
|
105
|
+
expires: /* @__PURE__ */ new Map(),
|
|
106
|
+
builds: /* @__PURE__ */ new Map()
|
|
107
|
+
};
|
|
108
|
+
snapshotCacheState.set(cache, cacheState);
|
|
109
109
|
function workspaceDirectory(input) {
|
|
110
110
|
return input.workspaceRoot ?? workspaceDir;
|
|
111
111
|
}
|
|
@@ -143,56 +143,72 @@ function createVercelSandboxDriver(options) {
|
|
|
143
143
|
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
144
144
|
const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${EXPECTED_RUNTIME_VERSION}`;
|
|
145
145
|
const hit = cache.get(cacheKey);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"sudo
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
146
|
+
const expiresAt = cacheState.expires.get(cacheKey);
|
|
147
|
+
if (hit && expiresAt === void 0) {
|
|
148
|
+
cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
|
|
149
|
+
return hit;
|
|
150
|
+
}
|
|
151
|
+
if (hit && expiresAt !== void 0 && expiresAt > now()) return hit;
|
|
152
|
+
const running = cacheState.builds.get(cacheKey);
|
|
153
|
+
if (running) return running;
|
|
154
|
+
const build = (async () => {
|
|
155
|
+
const builder = await sandboxFactory.create({
|
|
156
|
+
...credentials(),
|
|
157
|
+
runtime,
|
|
158
|
+
ports: [runtimePort],
|
|
159
|
+
timeout: snapshotBuildTimeoutMs
|
|
160
|
+
});
|
|
161
|
+
await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
|
|
162
|
+
await runSetup(builder, [
|
|
163
|
+
"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",
|
|
164
|
+
"curl https://cursor.com/install -fsS | bash",
|
|
165
|
+
"curl -fsSL https://ampcode.com/install.sh | bash",
|
|
166
|
+
"curl -fsSL https://app.factory.ai/cli | sh",
|
|
167
|
+
"sudo ln -sf $HOME/.local/bin/agent /usr/local/bin/agent",
|
|
168
|
+
"sudo ln -sf $HOME/.local/bin/cursor-agent /usr/local/bin/cursor-agent",
|
|
169
|
+
"sudo ln -sf $HOME/.local/bin/amp /usr/local/bin/amp",
|
|
170
|
+
"sudo ln -sf $HOME/.local/bin/droid /usr/local/bin/droid",
|
|
171
|
+
"command -v opencode",
|
|
172
|
+
"command -v claude",
|
|
173
|
+
"command -v codex",
|
|
174
|
+
"command -v gemini",
|
|
175
|
+
"command -v agent",
|
|
176
|
+
"command -v cursor-agent",
|
|
177
|
+
"command -v amp",
|
|
178
|
+
"command -v droid",
|
|
179
|
+
"command -v pi"
|
|
180
|
+
].join(" && "));
|
|
181
|
+
await runSetup(builder, [
|
|
182
|
+
`sudo mkdir -p ${shell(RUNTIME_DIR)}`,
|
|
183
|
+
`sudo chown $(whoami) ${shell(RUNTIME_DIR)}`,
|
|
184
|
+
`cd ${shell(RUNTIME_DIR)}`,
|
|
185
|
+
"npm init -y",
|
|
186
|
+
`npm install ${RUNTIME_PKG} --min-release-age=2`,
|
|
187
|
+
`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)}"`,
|
|
188
|
+
"test -x node_modules/.bin/workspace-runtime",
|
|
189
|
+
"test -x node_modules/.bin/claude-agent-acp",
|
|
190
|
+
"test -x node_modules/.bin/codex-acp",
|
|
191
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
|
|
192
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/claude-agent-acp /usr/local/bin/claude-agent-acp`,
|
|
193
|
+
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
|
|
194
|
+
"command -v workspace-runtime",
|
|
195
|
+
"command -v claude-agent-acp",
|
|
196
|
+
"command -v codex-acp"
|
|
197
|
+
].join(" && "));
|
|
198
|
+
const snapshot = await builder.snapshot({
|
|
199
|
+
expiration: 0,
|
|
200
|
+
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
201
|
+
});
|
|
202
|
+
cache.set(cacheKey, snapshot.snapshotId);
|
|
203
|
+
cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
|
|
204
|
+
return snapshot.snapshotId;
|
|
205
|
+
})();
|
|
206
|
+
cacheState.builds.set(cacheKey, build);
|
|
207
|
+
try {
|
|
208
|
+
return await build;
|
|
209
|
+
} finally {
|
|
210
|
+
if (cacheState.builds.get(cacheKey) === build) cacheState.builds.delete(cacheKey);
|
|
211
|
+
}
|
|
196
212
|
}
|
|
197
213
|
async function source(input) {
|
|
198
214
|
if (input.bootSource?.kind === "image") {
|
package/dist/image.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/image.ts
|
|
4
2
|
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
5
3
|
|
|
@@ -12,8 +10,7 @@ var WORKSPACE_DIR = "/workspace";
|
|
|
12
10
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
13
11
|
|
|
14
12
|
// src/runtime-version.ts
|
|
15
|
-
|
|
16
|
-
var require2 = createRequire(import.meta.url);
|
|
13
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
17
14
|
var versionCache;
|
|
18
15
|
function workspaceRuntimeVersion() {
|
|
19
16
|
if (versionCache) return versionCache;
|
|
@@ -22,12 +19,7 @@ function workspaceRuntimeVersion() {
|
|
|
22
19
|
versionCache = envVersion;
|
|
23
20
|
return versionCache;
|
|
24
21
|
}
|
|
25
|
-
|
|
26
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
27
|
-
return versionCache;
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
versionCache = "0.0.0";
|
|
22
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
31
23
|
return versionCache;
|
|
32
24
|
}
|
|
33
25
|
|
package/dist/index.mjs
CHANGED
package/dist/lease-policy.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/lease-policy.ts
|
|
4
2
|
var DEFAULT_WORKSPACE_HOST_DECISION_CONFIG = {
|
|
5
3
|
maxRetries: 8,
|
|
@@ -36,6 +34,13 @@ var DRIVER_PLACEMENT = {
|
|
|
36
34
|
canStopExplicitly: false,
|
|
37
35
|
hasHealthProbe: true
|
|
38
36
|
},
|
|
37
|
+
box: {
|
|
38
|
+
canPauseAndRestartSameResource: true,
|
|
39
|
+
canCreateFilesystemSnapshot: false,
|
|
40
|
+
canStartFromPreparedImage: true,
|
|
41
|
+
canStopExplicitly: true,
|
|
42
|
+
hasHealthProbe: true
|
|
43
|
+
},
|
|
39
44
|
docker: {
|
|
40
45
|
canPauseAndRestartSameResource: true,
|
|
41
46
|
canCreateFilesystemSnapshot: false,
|
package/dist/lease-types.mjs
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
package/dist/runtime-env.mjs
CHANGED
package/dist/runtime-version.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from 'module';var require = __createRequire(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/runtime-version.ts
|
|
4
|
-
|
|
5
|
-
var require2 = createRequire(import.meta.url);
|
|
2
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
6
3
|
var versionCache;
|
|
7
4
|
function workspaceRuntimeVersion() {
|
|
8
5
|
if (versionCache) return versionCache;
|
|
@@ -11,12 +8,7 @@ function workspaceRuntimeVersion() {
|
|
|
11
8
|
versionCache = envVersion;
|
|
12
9
|
return versionCache;
|
|
13
10
|
}
|
|
14
|
-
|
|
15
|
-
versionCache = require2("@claxedo/workspace-runtime/package.json").version;
|
|
16
|
-
return versionCache;
|
|
17
|
-
} catch {
|
|
18
|
-
}
|
|
19
|
-
versionCache = "0.0.0";
|
|
11
|
+
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
20
12
|
return versionCache;
|
|
21
13
|
}
|
|
22
14
|
export {
|
package/dist/stores/memory.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claxedo/sandbox-manager",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Sandbox lifecycle manager with epoch-based leases and pluggable drivers for Daytona, Modal, Vercel Sandbox, Cloudflare, and Docker",
|
|
5
|
+
"description": "Sandbox lifecycle manager with epoch-based leases and pluggable drivers for Daytona, Modal, Vercel Sandbox, Cloudflare, Box, and Docker",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"sandbox",
|
|
15
15
|
"daytona",
|
|
16
16
|
"modal",
|
|
17
|
+
"vercel",
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"docker",
|
|
20
|
+
"box",
|
|
17
21
|
"agents"
|
|
18
22
|
],
|
|
19
23
|
"main": "./dist/index.mjs",
|
|
@@ -54,6 +58,13 @@
|
|
|
54
58
|
"import": "./dist/driver-catalog.mjs",
|
|
55
59
|
"default": "./dist/driver-catalog.mjs"
|
|
56
60
|
},
|
|
61
|
+
"./drivers/box": {
|
|
62
|
+
"types": "./dist/drivers/box.d.ts",
|
|
63
|
+
"bun": "./dist/drivers/box.mjs",
|
|
64
|
+
"development": "./dist/drivers/box.mjs",
|
|
65
|
+
"import": "./dist/drivers/box.mjs",
|
|
66
|
+
"default": "./dist/drivers/box.mjs"
|
|
67
|
+
},
|
|
57
68
|
"./drivers/cloudflare": {
|
|
58
69
|
"types": "./dist/drivers/cloudflare.d.ts",
|
|
59
70
|
"bun": "./dist/drivers/cloudflare.mjs",
|
|
@@ -155,7 +166,6 @@
|
|
|
155
166
|
"access": "public"
|
|
156
167
|
},
|
|
157
168
|
"dependencies": {
|
|
158
|
-
"@claxedo/workspace-runtime": "0.5.1",
|
|
159
169
|
"@daytona/sdk": "0.192.0",
|
|
160
170
|
"@vercel/sandbox": "1.10.2",
|
|
161
171
|
"modal": "0.7.5"
|
|
@@ -172,7 +182,7 @@
|
|
|
172
182
|
"@types/node": "24.12.2",
|
|
173
183
|
"esbuild": "0.25.12",
|
|
174
184
|
"tsx": "4.22.3",
|
|
175
|
-
"typescript": "
|
|
185
|
+
"typescript": "catalog:",
|
|
176
186
|
"vitest": "2.1.9"
|
|
177
187
|
}
|
|
178
188
|
}
|