@cloudflare/sandbox 0.7.4 → 0.7.6
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/Dockerfile +8 -2
- package/dist/{contexts-uY_burk0.d.ts → contexts-BHx40XTT.d.ts} +35 -2
- package/dist/contexts-BHx40XTT.d.ts.map +1 -0
- package/dist/{errors-Bzl0ZNia.js → errors-CYUY62c6.js} +11 -1
- package/dist/errors-CYUY62c6.js.map +1 -0
- package/dist/index.d.ts +42 -42
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -8
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +11 -3
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +23 -9
- package/dist/opencode/index.js.map +1 -1
- package/dist/{sandbox-CgjQQZGw.d.ts → sandbox-DGAjk7r3.d.ts} +244 -5
- package/dist/sandbox-DGAjk7r3.d.ts.map +1 -0
- package/package.json +3 -2
- package/dist/contexts-uY_burk0.d.ts.map +0 -1
- package/dist/errors-Bzl0ZNia.js.map +0 -1
- package/dist/sandbox-CgjQQZGw.d.ts.map +0 -1
package/dist/openai/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as Sandbox } from "../sandbox-
|
|
1
|
+
import { t as Sandbox } from "../sandbox-DGAjk7r3.js";
|
|
2
2
|
import { ApplyPatchOperation, ApplyPatchResult, Editor as Editor$1, Shell as Shell$1, ShellAction, ShellResult } from "@openai/agents";
|
|
3
3
|
|
|
4
4
|
//#region src/openai/index.d.ts
|
package/dist/opencode/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as Sandbox } from "../sandbox-
|
|
2
|
-
import {
|
|
1
|
+
import { t as Sandbox } from "../sandbox-DGAjk7r3.js";
|
|
2
|
+
import { o as OpencodeStartupContext } from "../contexts-BHx40XTT.js";
|
|
3
3
|
import { OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
4
4
|
import { Config } from "@opencode-ai/sdk/v2";
|
|
5
5
|
|
|
@@ -141,6 +141,14 @@ declare function createOpencodeServer(sandbox: Sandbox<unknown>, options?: Openc
|
|
|
141
141
|
* ```
|
|
142
142
|
*/
|
|
143
143
|
declare function createOpencode<TClient = OpencodeClient>(sandbox: Sandbox<unknown>, options?: OpencodeOptions): Promise<OpencodeResult<TClient>>;
|
|
144
|
+
/**
|
|
145
|
+
* Proxy a request directly to the OpenCode server.
|
|
146
|
+
*
|
|
147
|
+
* Unlike `proxyToOpencode()`, this helper does not apply any web UI redirects
|
|
148
|
+
* or query parameter rewrites. Use it for API/CLI traffic where raw request
|
|
149
|
+
* forwarding is preferred.
|
|
150
|
+
*/
|
|
151
|
+
declare function proxyToOpencodeServer(request: Request, sandbox: Sandbox<unknown>, server: OpencodeServer): Promise<Response>;
|
|
144
152
|
/**
|
|
145
153
|
* Proxy a request to the OpenCode web UI.
|
|
146
154
|
*
|
|
@@ -190,5 +198,5 @@ declare function createOpencode<TClient = OpencodeClient>(sandbox: Sandbox<unkno
|
|
|
190
198
|
*/
|
|
191
199
|
declare function proxyToOpencode(request: Request, sandbox: Sandbox<unknown>, server: OpencodeServer): Response | Promise<Response>;
|
|
192
200
|
//#endregion
|
|
193
|
-
export { type OpencodeOptions, type OpencodeResult, type OpencodeServer, OpencodeStartupError, createOpencode, createOpencodeServer, proxyToOpencode };
|
|
201
|
+
export { type OpencodeOptions, type OpencodeResult, type OpencodeServer, OpencodeStartupError, createOpencode, createOpencodeServer, proxyToOpencode, proxyToOpencodeServer };
|
|
194
202
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/opencode/types.ts","../../src/opencode/opencode.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAOiB,UAAA,eAAA,CAMN;EAMM;EAaA,IAAA,CAAA,EAAA,MAAA;EAAyB;EAEhC,SAAA,CAAA,EAAA,MAAA;EAEA;EAAc,MAAA,CAAA,EAvBb,MAuBa;AAMxB;;;;AAA0C,UAvBzB,cAAA,CAuByB;EAAK;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/opencode/types.ts","../../src/opencode/opencode.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAOiB,UAAA,eAAA,CAMN;EAMM;EAaA,IAAA,CAAA,EAAA,MAAA;EAAyB;EAEhC,SAAA,CAAA,EAAA,MAAA;EAEA;EAAc,MAAA,CAAA,EAvBb,MAuBa;AAMxB;;;;AAA0C,UAvBzB,cAAA,CAuByB;EAAK;;;;ECoQzB;EACX,KAAA,EAAA,EDtRA,OCsRA,CAAA,IAAA,CAAA;;;;;AAiEX;AAA+C,UDhV9B,cCgV8B,CAAA,UDhVL,cCgVK,CAAA,CAAA;EACpC;EACC,MAAA,EDhVF,OCgVE;EACc;EAAf,MAAA,ED/UD,cC+UC;;;AA0BX;;AAEW,cDrWE,oBAAA,SAA6B,KAAA,CCqW/B;EACD,SAAA,IAAA,EAAA,yBAAA;EACC,SAAA,OAAA,EDrWgB,sBCqWhB;EAAR,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,OAAA,EDjWU,sBCiWV,EAAA,OAAA,CAAA,EDhWW,YCgWX;;;;;;;AD1YH;AAYA;AAaA;;;;;AAUA;;;;;;;;;ACoQA;;;;;;AAkEA;;;;;;;;AA6BA;;;;;;;AAuDA;;;;;;AAIc,iBA1JQ,oBAAA,CA0JR,OAAA,EAzJH,OAyJG,CAAA,OAAA,CAAA,EAAA,OAAA,CAAA,EAxJF,eAwJE,CAAA,EAvJX,OAuJW,CAvJH,cAuJG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAxFQ,yBAAyB,yBACpC,4BACC,kBACT,QAAQ,eAAe;;;;;;;;iBA0BV,qBAAA,UACL,kBACA,0BACD,iBACP,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmDK,eAAA,UACL,kBACA,0BACD,iBACP,WAAW,QAAQ"}
|
package/dist/opencode/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as createLogger } from "../dist-D9B_6gn_.js";
|
|
2
|
-
import { t as ErrorCode } from "../errors-
|
|
2
|
+
import { t as ErrorCode } from "../errors-CYUY62c6.js";
|
|
3
3
|
|
|
4
4
|
//#region src/opencode/types.ts
|
|
5
5
|
/**
|
|
@@ -24,6 +24,7 @@ function getLogger() {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
const DEFAULT_PORT = 4096;
|
|
27
|
+
const OPENCODE_STARTUP_TIMEOUT_MS = 18e4;
|
|
27
28
|
const OPENCODE_SERVE = (port) => `opencode serve --port ${port} --hostname 0.0.0.0`;
|
|
28
29
|
/**
|
|
29
30
|
* Build the full command, optionally with a directory prefix.
|
|
@@ -72,8 +73,9 @@ async function ensureOpencodeServer(sandbox, port, directory, config) {
|
|
|
72
73
|
try {
|
|
73
74
|
await existingProcess.waitForPort(port, {
|
|
74
75
|
mode: "http",
|
|
75
|
-
path: "/",
|
|
76
|
-
|
|
76
|
+
path: "/global/health",
|
|
77
|
+
status: 200,
|
|
78
|
+
timeout: OPENCODE_STARTUP_TIMEOUT_MS
|
|
77
79
|
});
|
|
78
80
|
} catch (e) {
|
|
79
81
|
const logs = await existingProcess.getLogs();
|
|
@@ -102,8 +104,9 @@ async function ensureOpencodeServer(sandbox, port, directory, config) {
|
|
|
102
104
|
if (retryProcess.status === "starting") try {
|
|
103
105
|
await retryProcess.waitForPort(port, {
|
|
104
106
|
mode: "http",
|
|
105
|
-
path: "/",
|
|
106
|
-
|
|
107
|
+
path: "/global/health",
|
|
108
|
+
status: 200,
|
|
109
|
+
timeout: OPENCODE_STARTUP_TIMEOUT_MS
|
|
107
110
|
});
|
|
108
111
|
} catch (e) {
|
|
109
112
|
const logs = await retryProcess.getLogs();
|
|
@@ -153,8 +156,9 @@ async function startOpencodeServer(sandbox, port, directory, config) {
|
|
|
153
156
|
try {
|
|
154
157
|
await process.waitForPort(port, {
|
|
155
158
|
mode: "http",
|
|
156
|
-
path: "/",
|
|
157
|
-
|
|
159
|
+
path: "/global/health",
|
|
160
|
+
status: 200,
|
|
161
|
+
timeout: OPENCODE_STARTUP_TIMEOUT_MS
|
|
158
162
|
});
|
|
159
163
|
getLogger().info("OpenCode server started successfully", {
|
|
160
164
|
port,
|
|
@@ -291,6 +295,16 @@ async function createOpencode(sandbox, options) {
|
|
|
291
295
|
};
|
|
292
296
|
}
|
|
293
297
|
/**
|
|
298
|
+
* Proxy a request directly to the OpenCode server.
|
|
299
|
+
*
|
|
300
|
+
* Unlike `proxyToOpencode()`, this helper does not apply any web UI redirects
|
|
301
|
+
* or query parameter rewrites. Use it for API/CLI traffic where raw request
|
|
302
|
+
* forwarding is preferred.
|
|
303
|
+
*/
|
|
304
|
+
function proxyToOpencodeServer(request, sandbox, server) {
|
|
305
|
+
return sandbox.containerFetch(request, server.port);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
294
308
|
* Proxy a request to the OpenCode web UI.
|
|
295
309
|
*
|
|
296
310
|
* This function handles the redirect and proxying only - you must start the
|
|
@@ -345,9 +359,9 @@ function proxyToOpencode(request, sandbox, server) {
|
|
|
345
359
|
return Response.redirect(url.toString(), 302);
|
|
346
360
|
}
|
|
347
361
|
}
|
|
348
|
-
return
|
|
362
|
+
return proxyToOpencodeServer(request, sandbox, server);
|
|
349
363
|
}
|
|
350
364
|
|
|
351
365
|
//#endregion
|
|
352
|
-
export { OpencodeStartupError, createOpencode, createOpencodeServer, proxyToOpencode };
|
|
366
|
+
export { OpencodeStartupError, createOpencode, createOpencodeServer, proxyToOpencode, proxyToOpencodeServer };
|
|
353
367
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createOpencodeClient: OpencodeClientFactory | undefined","env: Record<string, string>"],"sources":["../../src/opencode/types.ts","../../src/opencode/opencode.ts"],"sourcesContent":["import type { Config } from '@opencode-ai/sdk/v2';\nimport type { OpencodeClient } from '@opencode-ai/sdk/v2/client';\nimport { ErrorCode, type OpencodeStartupContext } from '@repo/shared/errors';\n\n/**\n * Configuration options for starting OpenCode server\n */\nexport interface OpencodeOptions {\n /** Port for OpenCode server (default: 4096) */\n port?: number;\n /** Working directory for OpenCode (default: container's cwd) */\n directory?: string;\n /** OpenCode configuration */\n config?: Config;\n}\n\n/**\n * Server lifecycle management\n */\nexport interface OpencodeServer {\n /** Port the server is running on */\n port: number;\n /** Base URL for SDK client (http://localhost:{port}) */\n url: string;\n /** Close the server gracefully */\n close(): Promise<void>;\n}\n\n/**\n * Result from createOpencode()\n * Client type comes from @opencode-ai/sdk (user's version)\n */\nexport interface OpencodeResult<TClient = OpencodeClient> {\n /** OpenCode SDK client with Sandbox transport */\n client: TClient;\n /** Server lifecycle management */\n server: OpencodeServer;\n}\n\n/**\n * Error thrown when OpenCode server fails to start\n */\nexport class OpencodeStartupError extends Error {\n public readonly code = ErrorCode.OPENCODE_STARTUP_FAILED;\n public readonly context: OpencodeStartupContext;\n\n constructor(\n message: string,\n context: OpencodeStartupContext,\n options?: ErrorOptions\n ) {\n super(message, options);\n this.name = 'OpencodeStartupError';\n this.context = context;\n }\n}\n","import type { Config } from '@opencode-ai/sdk/v2';\nimport type { OpencodeClient } from '@opencode-ai/sdk/v2/client';\nimport { createLogger, type Logger, type Process } from '@repo/shared';\nimport type { Sandbox } from '../sandbox';\nimport type { OpencodeOptions, OpencodeResult, OpencodeServer } from './types';\nimport { OpencodeStartupError } from './types';\n\n// Lazy logger creation to avoid global scope restrictions in Workers\nfunction getLogger(): Logger {\n return createLogger({ component: 'sandbox-do', operation: 'opencode' });\n}\n\nconst DEFAULT_PORT = 4096;\nconst OPENCODE_SERVE = (port: number) =>\n `opencode serve --port ${port} --hostname 0.0.0.0`;\n\n/**\n * Build the full command, optionally with a directory prefix.\n * If directory is provided, we cd to it first so OpenCode uses it as cwd.\n */\nfunction buildOpencodeCommand(port: number, directory?: string): string {\n const serve = OPENCODE_SERVE(port);\n return directory ? `cd ${directory} && ${serve}` : serve;\n}\n\ntype OpencodeClientFactory = (options: {\n baseUrl: string;\n fetch: typeof fetch;\n directory?: string;\n}) => OpencodeClient;\n\n// Dynamic import to handle peer dependency\nlet createOpencodeClient: OpencodeClientFactory | undefined;\n\nasync function ensureSdkLoaded(): Promise<void> {\n if (createOpencodeClient) return;\n\n try {\n const sdk = await import('@opencode-ai/sdk/v2/client');\n createOpencodeClient = sdk.createOpencodeClient as OpencodeClientFactory;\n } catch {\n throw new Error(\n '@opencode-ai/sdk is required for OpenCode integration. ' +\n 'Install it with: npm install @opencode-ai/sdk'\n );\n }\n}\n\n/**\n * Find an existing OpenCode server process running on the specified port.\n * Returns the process if found and still active, null otherwise.\n * Matches by the serve command pattern since directory prefix may vary.\n */\nasync function findExistingOpencodeProcess(\n sandbox: Sandbox<unknown>,\n port: number\n): Promise<Process | null> {\n const processes = await sandbox.listProcesses();\n const serveCommand = OPENCODE_SERVE(port);\n\n for (const proc of processes) {\n // Match commands that contain the serve command (with or without cd prefix)\n if (proc.command.includes(serveCommand)) {\n if (proc.status === 'starting' || proc.status === 'running') {\n return proc;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Ensures OpenCode server is running in the container.\n * Reuses existing process if one is already running on the specified port.\n * Handles concurrent startup attempts gracefully by retrying on failure.\n * Returns the process handle.\n */\nasync function ensureOpencodeServer(\n sandbox: Sandbox<unknown>,\n port: number,\n directory?: string,\n config?: Config\n): Promise<Process> {\n // Check if OpenCode is already running on this port\n const existingProcess = await findExistingOpencodeProcess(sandbox, port);\n if (existingProcess) {\n // Reuse existing process - wait for it to be ready if still starting\n if (existingProcess.status === 'starting') {\n getLogger().debug('Found starting OpenCode process, waiting for ready', {\n port,\n processId: existingProcess.id\n });\n try {\n await existingProcess.waitForPort(port, {\n mode: 'http',\n path: '/',\n timeout: 60_000\n });\n } catch (e) {\n const logs = await existingProcess.getLogs();\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command: existingProcess.command },\n { cause: e }\n );\n }\n }\n getLogger().debug('Reusing existing OpenCode process', {\n port,\n processId: existingProcess.id\n });\n return existingProcess;\n }\n\n // Try to start a new OpenCode server\n try {\n return await startOpencodeServer(sandbox, port, directory, config);\n } catch (startupError) {\n // Startup failed - check if another concurrent request started the server\n // This handles the race condition where multiple requests try to start simultaneously\n const retryProcess = await findExistingOpencodeProcess(sandbox, port);\n if (retryProcess) {\n getLogger().debug(\n 'Startup failed but found concurrent process, reusing',\n {\n port,\n processId: retryProcess.id\n }\n );\n // Wait for the concurrent server to be ready\n if (retryProcess.status === 'starting') {\n try {\n await retryProcess.waitForPort(port, {\n mode: 'http',\n path: '/',\n timeout: 60_000\n });\n } catch (e) {\n const logs = await retryProcess.getLogs();\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command: retryProcess.command },\n { cause: e }\n );\n }\n }\n return retryProcess;\n }\n\n // No concurrent server found - the failure was genuine\n throw startupError;\n }\n}\n\n/**\n * Internal function to start a new OpenCode server process.\n */\nasync function startOpencodeServer(\n sandbox: Sandbox<unknown>,\n port: number,\n directory?: string,\n config?: Config\n): Promise<Process> {\n getLogger().info('Starting OpenCode server', { port, directory });\n\n // Pass config via OPENCODE_CONFIG_CONTENT and also extract API keys to env vars\n // because OpenCode's provider auth looks for env vars like ANTHROPIC_API_KEY\n const env: Record<string, string> = {};\n\n if (config) {\n env.OPENCODE_CONFIG_CONTENT = JSON.stringify(config);\n\n // Extract API keys from provider config\n // Support both options.apiKey (official type) and legacy top-level apiKey\n if (\n config.provider &&\n typeof config.provider === 'object' &&\n !Array.isArray(config.provider)\n ) {\n for (const [providerId, providerConfig] of Object.entries(\n config.provider\n )) {\n if (providerId === 'cloudflare-ai-gateway') {\n continue;\n }\n\n // Try options.apiKey first (official Config type)\n let apiKey = providerConfig?.options?.apiKey;\n // Fall back to top-level apiKey for convenience\n if (!apiKey) {\n apiKey = (providerConfig as Record<string, unknown> | undefined)\n ?.apiKey as string | undefined;\n }\n if (typeof apiKey === 'string') {\n const envVar = `${providerId.toUpperCase()}_API_KEY`;\n env[envVar] = apiKey;\n }\n }\n\n const aiGatewayConfig = config.provider['cloudflare-ai-gateway'];\n if (aiGatewayConfig?.options) {\n const options = aiGatewayConfig.options as Record<string, unknown>;\n\n if (typeof options.accountId === 'string') {\n env.CLOUDFLARE_ACCOUNT_ID = options.accountId;\n }\n\n if (typeof options.gatewayId === 'string') {\n env.CLOUDFLARE_GATEWAY_ID = options.gatewayId;\n }\n\n if (typeof options.apiToken === 'string') {\n env.CLOUDFLARE_API_TOKEN = options.apiToken;\n }\n }\n }\n }\n\n const command = buildOpencodeCommand(port, directory);\n const process = await sandbox.startProcess(command, {\n env: Object.keys(env).length > 0 ? env : undefined\n });\n\n // Wait for server to be ready\n try {\n await process.waitForPort(port, {\n mode: 'http',\n path: '/',\n timeout: 60_000\n });\n getLogger().info('OpenCode server started successfully', {\n port,\n processId: process.id\n });\n } catch (e) {\n const logs = await process.getLogs();\n const error = e instanceof Error ? e : undefined;\n getLogger().error('OpenCode server failed to start', error, {\n port,\n stderr: logs.stderr\n });\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command },\n { cause: e }\n );\n }\n\n return process;\n}\n\n/**\n * Starts an OpenCode server inside a Sandbox container.\n *\n * This function manages the server lifecycle only - use `createOpencode()` if you\n * also need a typed SDK client for programmatic access.\n *\n * If an OpenCode server is already running on the specified port, this function\n * will reuse it instead of starting a new one.\n *\n * @param sandbox - The Sandbox instance to run OpenCode in\n * @param options - Configuration options\n * @returns Promise resolving to server handle { port, url, close() }\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencodeServer } from '@cloudflare/sandbox/opencode'\n *\n * const sandbox = getSandbox(env.Sandbox, 'my-agent')\n * const server = await createOpencodeServer(sandbox, {\n * directory: '/home/user/my-project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Or use Cloudflare AI Gateway (with unified billing, no provider keys needed).\n * // 'cloudflare-ai-gateway': {\n * // options: {\n * // accountId: env.CF_ACCOUNT_ID,\n * // gatewayId: env.CF_GATEWAY_ID,\n * // apiToken: env.CF_API_TOKEN\n * // },\n * // models: { 'anthropic/claude-sonnet-4-5-20250929': {} }\n * // }\n * }\n * }\n * })\n *\n * // Proxy requests to the web UI\n * return sandbox.containerFetch(request, server.port)\n *\n * // When done\n * await server.close()\n * ```\n */\nexport async function createOpencodeServer(\n sandbox: Sandbox<unknown>,\n options?: OpencodeOptions\n): Promise<OpencodeServer> {\n const port = options?.port ?? DEFAULT_PORT;\n const process = await ensureOpencodeServer(\n sandbox,\n port,\n options?.directory,\n options?.config\n );\n\n return {\n port,\n url: `http://localhost:${port}`,\n close: () => process.kill('SIGTERM')\n };\n}\n\n/**\n * Creates an OpenCode server inside a Sandbox container and returns a typed SDK client.\n *\n * This function is API-compatible with OpenCode's own createOpencode(), but uses\n * Sandbox process management instead of Node.js spawn. The returned client uses\n * a custom fetch adapter to route requests through the Sandbox container.\n *\n * If an OpenCode server is already running on the specified port, this function\n * will reuse it instead of starting a new one.\n *\n * @param sandbox - The Sandbox instance to run OpenCode in\n * @param options - Configuration options\n * @returns Promise resolving to { client, server }\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencode } from '@cloudflare/sandbox/opencode'\n *\n * const sandbox = getSandbox(env.Sandbox, 'my-agent')\n * const { client, server } = await createOpencode(sandbox, {\n * directory: '/home/user/my-project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Or use Cloudflare AI Gateway (with unified billing, no provider keys needed).\n * // 'cloudflare-ai-gateway': {\n * // options: {\n * // accountId: env.CF_ACCOUNT_ID,\n * // gatewayId: env.CF_GATEWAY_ID,\n * // apiToken: env.CF_API_TOKEN\n * // },\n * // models: { 'anthropic/claude-sonnet-4-5-20250929': {} }\n * // }\n * }\n * }\n * })\n *\n * // Use the SDK client for programmatic access\n * const session = await client.session.create()\n *\n * // When done\n * await server.close()\n * ```\n */\nexport async function createOpencode<TClient = OpencodeClient>(\n sandbox: Sandbox<unknown>,\n options?: OpencodeOptions\n): Promise<OpencodeResult<TClient>> {\n await ensureSdkLoaded();\n\n const server = await createOpencodeServer(sandbox, options);\n\n const clientFactory = createOpencodeClient;\n if (!clientFactory) {\n throw new Error('OpenCode SDK client unavailable.');\n }\n\n const client = clientFactory({\n baseUrl: server.url,\n fetch: (input, init?) =>\n sandbox.containerFetch(new Request(input, init), server.port)\n });\n\n return { client: client as TClient, server };\n}\n\n/**\n * Proxy a request to the OpenCode web UI.\n *\n * This function handles the redirect and proxying only - you must start the\n * server separately using `createOpencodeServer()`.\n *\n * Specifically handles:\n * 1. Ensuring the `?url=` parameter is set (required for OpenCode's frontend to\n * make API calls through the proxy instead of directly to localhost:4096)\n * 2. Proxying the request to the container\n *\n * @param request - The incoming HTTP request\n * @param sandbox - The Sandbox instance running OpenCode\n * @param server - The OpenCode server handle from createOpencodeServer()\n * @returns Response from OpenCode or a redirect response\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencodeServer, proxyToOpencode } from '@cloudflare/sandbox/opencode'\n *\n * export default {\n * async fetch(request: Request, env: Env) {\n * const sandbox = getSandbox(env.Sandbox, 'opencode')\n * const server = await createOpencodeServer(sandbox, {\n * directory: '/home/user/project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Optional: Route all providers through Cloudflare AI Gateway\n * 'cloudflare-ai-gateway': {\n * options: {\n * accountId: env.CF_ACCOUNT_ID,\n * gatewayId: env.CF_GATEWAY_ID,\n * apiToken: env.CF_API_TOKEN\n * }\n * }\n * }\n * }\n * })\n * return proxyToOpencode(request, sandbox, server)\n * }\n * }\n * ```\n */\nexport function proxyToOpencode(\n request: Request,\n sandbox: Sandbox<unknown>,\n server: OpencodeServer\n): Response | Promise<Response> {\n const url = new URL(request.url);\n\n // OpenCode's frontend defaults to http://127.0.0.1:4096 when hostname includes\n // \"localhost\" or \"opencode.ai\". The ?url= parameter overrides this behavior.\n // We only redirect GET requests for HTML pages (initial page load).\n // API calls (POST, PATCH, etc.) and asset requests are proxied directly\n // since redirecting POST loses the request body.\n if (!url.searchParams.has('url') && request.method === 'GET') {\n const accept = request.headers.get('accept') || '';\n const isHtmlRequest = accept.includes('text/html') || url.pathname === '/';\n if (isHtmlRequest) {\n url.searchParams.set('url', url.origin);\n return Response.redirect(url.toString(), 302);\n }\n }\n\n return sandbox.containerFetch(request, server.port);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["createOpencodeClient: OpencodeClientFactory | undefined","env: Record<string, string>"],"sources":["../../src/opencode/types.ts","../../src/opencode/opencode.ts"],"sourcesContent":["import type { Config } from '@opencode-ai/sdk/v2';\nimport type { OpencodeClient } from '@opencode-ai/sdk/v2/client';\nimport { ErrorCode, type OpencodeStartupContext } from '@repo/shared/errors';\n\n/**\n * Configuration options for starting OpenCode server\n */\nexport interface OpencodeOptions {\n /** Port for OpenCode server (default: 4096) */\n port?: number;\n /** Working directory for OpenCode (default: container's cwd) */\n directory?: string;\n /** OpenCode configuration */\n config?: Config;\n}\n\n/**\n * Server lifecycle management\n */\nexport interface OpencodeServer {\n /** Port the server is running on */\n port: number;\n /** Base URL for SDK client (http://localhost:{port}) */\n url: string;\n /** Close the server gracefully */\n close(): Promise<void>;\n}\n\n/**\n * Result from createOpencode()\n * Client type comes from @opencode-ai/sdk (user's version)\n */\nexport interface OpencodeResult<TClient = OpencodeClient> {\n /** OpenCode SDK client with Sandbox transport */\n client: TClient;\n /** Server lifecycle management */\n server: OpencodeServer;\n}\n\n/**\n * Error thrown when OpenCode server fails to start\n */\nexport class OpencodeStartupError extends Error {\n public readonly code = ErrorCode.OPENCODE_STARTUP_FAILED;\n public readonly context: OpencodeStartupContext;\n\n constructor(\n message: string,\n context: OpencodeStartupContext,\n options?: ErrorOptions\n ) {\n super(message, options);\n this.name = 'OpencodeStartupError';\n this.context = context;\n }\n}\n","import type { Config } from '@opencode-ai/sdk/v2';\nimport type { OpencodeClient } from '@opencode-ai/sdk/v2/client';\nimport { createLogger, type Logger, type Process } from '@repo/shared';\nimport type { Sandbox } from '../sandbox';\nimport type { OpencodeOptions, OpencodeResult, OpencodeServer } from './types';\nimport { OpencodeStartupError } from './types';\n\n// Lazy logger creation to avoid global scope restrictions in Workers\nfunction getLogger(): Logger {\n return createLogger({ component: 'sandbox-do', operation: 'opencode' });\n}\n\nconst DEFAULT_PORT = 4096;\nconst OPENCODE_STARTUP_TIMEOUT_MS = 180_000;\nconst OPENCODE_SERVE = (port: number) =>\n `opencode serve --port ${port} --hostname 0.0.0.0`;\n\n/**\n * Build the full command, optionally with a directory prefix.\n * If directory is provided, we cd to it first so OpenCode uses it as cwd.\n */\nfunction buildOpencodeCommand(port: number, directory?: string): string {\n const serve = OPENCODE_SERVE(port);\n return directory ? `cd ${directory} && ${serve}` : serve;\n}\n\ntype OpencodeClientFactory = (options: {\n baseUrl: string;\n fetch: typeof fetch;\n directory?: string;\n}) => OpencodeClient;\n\n// Dynamic import to handle peer dependency\nlet createOpencodeClient: OpencodeClientFactory | undefined;\n\nasync function ensureSdkLoaded(): Promise<void> {\n if (createOpencodeClient) return;\n\n try {\n const sdk = await import('@opencode-ai/sdk/v2/client');\n createOpencodeClient = sdk.createOpencodeClient as OpencodeClientFactory;\n } catch {\n throw new Error(\n '@opencode-ai/sdk is required for OpenCode integration. ' +\n 'Install it with: npm install @opencode-ai/sdk'\n );\n }\n}\n\n/**\n * Find an existing OpenCode server process running on the specified port.\n * Returns the process if found and still active, null otherwise.\n * Matches by the serve command pattern since directory prefix may vary.\n */\nasync function findExistingOpencodeProcess(\n sandbox: Sandbox<unknown>,\n port: number\n): Promise<Process | null> {\n const processes = await sandbox.listProcesses();\n const serveCommand = OPENCODE_SERVE(port);\n\n for (const proc of processes) {\n // Match commands that contain the serve command (with or without cd prefix)\n if (proc.command.includes(serveCommand)) {\n if (proc.status === 'starting' || proc.status === 'running') {\n return proc;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Ensures OpenCode server is running in the container.\n * Reuses existing process if one is already running on the specified port.\n * Handles concurrent startup attempts gracefully by retrying on failure.\n * Returns the process handle.\n */\nasync function ensureOpencodeServer(\n sandbox: Sandbox<unknown>,\n port: number,\n directory?: string,\n config?: Config\n): Promise<Process> {\n // Check if OpenCode is already running on this port\n const existingProcess = await findExistingOpencodeProcess(sandbox, port);\n if (existingProcess) {\n // Reuse existing process - wait for it to be ready if still starting\n if (existingProcess.status === 'starting') {\n getLogger().debug('Found starting OpenCode process, waiting for ready', {\n port,\n processId: existingProcess.id\n });\n try {\n await existingProcess.waitForPort(port, {\n mode: 'http',\n path: '/global/health',\n status: 200,\n timeout: OPENCODE_STARTUP_TIMEOUT_MS\n });\n } catch (e) {\n const logs = await existingProcess.getLogs();\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command: existingProcess.command },\n { cause: e }\n );\n }\n }\n getLogger().debug('Reusing existing OpenCode process', {\n port,\n processId: existingProcess.id\n });\n return existingProcess;\n }\n\n // Try to start a new OpenCode server\n try {\n return await startOpencodeServer(sandbox, port, directory, config);\n } catch (startupError) {\n // Startup failed - check if another concurrent request started the server\n // This handles the race condition where multiple requests try to start simultaneously\n const retryProcess = await findExistingOpencodeProcess(sandbox, port);\n if (retryProcess) {\n getLogger().debug(\n 'Startup failed but found concurrent process, reusing',\n {\n port,\n processId: retryProcess.id\n }\n );\n // Wait for the concurrent server to be ready\n if (retryProcess.status === 'starting') {\n try {\n await retryProcess.waitForPort(port, {\n mode: 'http',\n path: '/global/health',\n status: 200,\n timeout: OPENCODE_STARTUP_TIMEOUT_MS\n });\n } catch (e) {\n const logs = await retryProcess.getLogs();\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command: retryProcess.command },\n { cause: e }\n );\n }\n }\n return retryProcess;\n }\n\n // No concurrent server found - the failure was genuine\n throw startupError;\n }\n}\n\n/**\n * Internal function to start a new OpenCode server process.\n */\nasync function startOpencodeServer(\n sandbox: Sandbox<unknown>,\n port: number,\n directory?: string,\n config?: Config\n): Promise<Process> {\n getLogger().info('Starting OpenCode server', { port, directory });\n\n // Pass config via OPENCODE_CONFIG_CONTENT and also extract API keys to env vars\n // because OpenCode's provider auth looks for env vars like ANTHROPIC_API_KEY\n const env: Record<string, string> = {};\n\n if (config) {\n env.OPENCODE_CONFIG_CONTENT = JSON.stringify(config);\n\n // Extract API keys from provider config\n // Support both options.apiKey (official type) and legacy top-level apiKey\n if (\n config.provider &&\n typeof config.provider === 'object' &&\n !Array.isArray(config.provider)\n ) {\n for (const [providerId, providerConfig] of Object.entries(\n config.provider\n )) {\n if (providerId === 'cloudflare-ai-gateway') {\n continue;\n }\n\n // Try options.apiKey first (official Config type)\n let apiKey = providerConfig?.options?.apiKey;\n // Fall back to top-level apiKey for convenience\n if (!apiKey) {\n apiKey = (providerConfig as Record<string, unknown> | undefined)\n ?.apiKey as string | undefined;\n }\n if (typeof apiKey === 'string') {\n const envVar = `${providerId.toUpperCase()}_API_KEY`;\n env[envVar] = apiKey;\n }\n }\n\n const aiGatewayConfig = config.provider['cloudflare-ai-gateway'];\n if (aiGatewayConfig?.options) {\n const options = aiGatewayConfig.options as Record<string, unknown>;\n\n if (typeof options.accountId === 'string') {\n env.CLOUDFLARE_ACCOUNT_ID = options.accountId;\n }\n\n if (typeof options.gatewayId === 'string') {\n env.CLOUDFLARE_GATEWAY_ID = options.gatewayId;\n }\n\n if (typeof options.apiToken === 'string') {\n env.CLOUDFLARE_API_TOKEN = options.apiToken;\n }\n }\n }\n }\n\n const command = buildOpencodeCommand(port, directory);\n const process = await sandbox.startProcess(command, {\n env: Object.keys(env).length > 0 ? env : undefined\n });\n\n // Wait for server to be ready - check the actual health endpoint\n try {\n await process.waitForPort(port, {\n mode: 'http',\n path: '/global/health',\n status: 200,\n timeout: OPENCODE_STARTUP_TIMEOUT_MS\n });\n getLogger().info('OpenCode server started successfully', {\n port,\n processId: process.id\n });\n } catch (e) {\n const logs = await process.getLogs();\n const error = e instanceof Error ? e : undefined;\n getLogger().error('OpenCode server failed to start', error, {\n port,\n stderr: logs.stderr\n });\n throw new OpencodeStartupError(\n `OpenCode server failed to start. Stderr: ${logs.stderr || '(empty)'}`,\n { port, stderr: logs.stderr, command },\n { cause: e }\n );\n }\n\n return process;\n}\n\n/**\n * Starts an OpenCode server inside a Sandbox container.\n *\n * This function manages the server lifecycle only - use `createOpencode()` if you\n * also need a typed SDK client for programmatic access.\n *\n * If an OpenCode server is already running on the specified port, this function\n * will reuse it instead of starting a new one.\n *\n * @param sandbox - The Sandbox instance to run OpenCode in\n * @param options - Configuration options\n * @returns Promise resolving to server handle { port, url, close() }\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencodeServer } from '@cloudflare/sandbox/opencode'\n *\n * const sandbox = getSandbox(env.Sandbox, 'my-agent')\n * const server = await createOpencodeServer(sandbox, {\n * directory: '/home/user/my-project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Or use Cloudflare AI Gateway (with unified billing, no provider keys needed).\n * // 'cloudflare-ai-gateway': {\n * // options: {\n * // accountId: env.CF_ACCOUNT_ID,\n * // gatewayId: env.CF_GATEWAY_ID,\n * // apiToken: env.CF_API_TOKEN\n * // },\n * // models: { 'anthropic/claude-sonnet-4-5-20250929': {} }\n * // }\n * }\n * }\n * })\n *\n * // Proxy requests to the web UI\n * return sandbox.containerFetch(request, server.port)\n *\n * // When done\n * await server.close()\n * ```\n */\nexport async function createOpencodeServer(\n sandbox: Sandbox<unknown>,\n options?: OpencodeOptions\n): Promise<OpencodeServer> {\n const port = options?.port ?? DEFAULT_PORT;\n const process = await ensureOpencodeServer(\n sandbox,\n port,\n options?.directory,\n options?.config\n );\n\n return {\n port,\n url: `http://localhost:${port}`,\n close: () => process.kill('SIGTERM')\n };\n}\n\n/**\n * Creates an OpenCode server inside a Sandbox container and returns a typed SDK client.\n *\n * This function is API-compatible with OpenCode's own createOpencode(), but uses\n * Sandbox process management instead of Node.js spawn. The returned client uses\n * a custom fetch adapter to route requests through the Sandbox container.\n *\n * If an OpenCode server is already running on the specified port, this function\n * will reuse it instead of starting a new one.\n *\n * @param sandbox - The Sandbox instance to run OpenCode in\n * @param options - Configuration options\n * @returns Promise resolving to { client, server }\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencode } from '@cloudflare/sandbox/opencode'\n *\n * const sandbox = getSandbox(env.Sandbox, 'my-agent')\n * const { client, server } = await createOpencode(sandbox, {\n * directory: '/home/user/my-project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Or use Cloudflare AI Gateway (with unified billing, no provider keys needed).\n * // 'cloudflare-ai-gateway': {\n * // options: {\n * // accountId: env.CF_ACCOUNT_ID,\n * // gatewayId: env.CF_GATEWAY_ID,\n * // apiToken: env.CF_API_TOKEN\n * // },\n * // models: { 'anthropic/claude-sonnet-4-5-20250929': {} }\n * // }\n * }\n * }\n * })\n *\n * // Use the SDK client for programmatic access\n * const session = await client.session.create()\n *\n * // When done\n * await server.close()\n * ```\n */\nexport async function createOpencode<TClient = OpencodeClient>(\n sandbox: Sandbox<unknown>,\n options?: OpencodeOptions\n): Promise<OpencodeResult<TClient>> {\n await ensureSdkLoaded();\n\n const server = await createOpencodeServer(sandbox, options);\n\n const clientFactory = createOpencodeClient;\n if (!clientFactory) {\n throw new Error('OpenCode SDK client unavailable.');\n }\n\n const client = clientFactory({\n baseUrl: server.url,\n fetch: (input, init?) =>\n sandbox.containerFetch(new Request(input, init), server.port)\n });\n\n return { client: client as TClient, server };\n}\n\n/**\n * Proxy a request directly to the OpenCode server.\n *\n * Unlike `proxyToOpencode()`, this helper does not apply any web UI redirects\n * or query parameter rewrites. Use it for API/CLI traffic where raw request\n * forwarding is preferred.\n */\nexport function proxyToOpencodeServer(\n request: Request,\n sandbox: Sandbox<unknown>,\n server: OpencodeServer\n): Promise<Response> {\n return sandbox.containerFetch(request, server.port);\n}\n\n/**\n * Proxy a request to the OpenCode web UI.\n *\n * This function handles the redirect and proxying only - you must start the\n * server separately using `createOpencodeServer()`.\n *\n * Specifically handles:\n * 1. Ensuring the `?url=` parameter is set (required for OpenCode's frontend to\n * make API calls through the proxy instead of directly to localhost:4096)\n * 2. Proxying the request to the container\n *\n * @param request - The incoming HTTP request\n * @param sandbox - The Sandbox instance running OpenCode\n * @param server - The OpenCode server handle from createOpencodeServer()\n * @returns Response from OpenCode or a redirect response\n *\n * @example\n * ```typescript\n * import { getSandbox } from '@cloudflare/sandbox'\n * import { createOpencodeServer, proxyToOpencode } from '@cloudflare/sandbox/opencode'\n *\n * export default {\n * async fetch(request: Request, env: Env) {\n * const sandbox = getSandbox(env.Sandbox, 'opencode')\n * const server = await createOpencodeServer(sandbox, {\n * directory: '/home/user/project',\n * config: {\n * provider: {\n * anthropic: {\n * options: { apiKey: env.ANTHROPIC_KEY }\n * },\n * // Optional: Route all providers through Cloudflare AI Gateway\n * 'cloudflare-ai-gateway': {\n * options: {\n * accountId: env.CF_ACCOUNT_ID,\n * gatewayId: env.CF_GATEWAY_ID,\n * apiToken: env.CF_API_TOKEN\n * }\n * }\n * }\n * }\n * })\n * return proxyToOpencode(request, sandbox, server)\n * }\n * }\n * ```\n */\nexport function proxyToOpencode(\n request: Request,\n sandbox: Sandbox<unknown>,\n server: OpencodeServer\n): Response | Promise<Response> {\n const url = new URL(request.url);\n\n // OpenCode's frontend defaults to http://127.0.0.1:4096 when hostname includes\n // \"localhost\" or \"opencode.ai\". The ?url= parameter overrides this behavior.\n // We only redirect GET requests for HTML pages (initial page load).\n // API calls (POST, PATCH, etc.) and asset requests are proxied directly\n // since redirecting POST loses the request body.\n if (!url.searchParams.has('url') && request.method === 'GET') {\n const accept = request.headers.get('accept') || '';\n const isHtmlRequest = accept.includes('text/html') || url.pathname === '/';\n if (isHtmlRequest) {\n url.searchParams.set('url', url.origin);\n return Response.redirect(url.toString(), 302);\n }\n }\n\n return proxyToOpencodeServer(request, sandbox, server);\n}\n"],"mappings":"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"}
|
|
@@ -348,6 +348,62 @@ interface PtyOptions {
|
|
|
348
348
|
rows?: number;
|
|
349
349
|
}
|
|
350
350
|
//#endregion
|
|
351
|
+
//#region ../shared/dist/request-types.d.ts
|
|
352
|
+
/**
|
|
353
|
+
* Request types for API calls to the container
|
|
354
|
+
* Single source of truth for the contract between SDK clients and container handlers
|
|
355
|
+
*/
|
|
356
|
+
/**
|
|
357
|
+
* Request to execute a command
|
|
358
|
+
*/
|
|
359
|
+
interface ExecuteRequest {
|
|
360
|
+
command: string;
|
|
361
|
+
sessionId?: string;
|
|
362
|
+
background?: boolean;
|
|
363
|
+
timeoutMs?: number;
|
|
364
|
+
env?: Record<string, string | undefined>;
|
|
365
|
+
cwd?: string;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Request to start a background process
|
|
369
|
+
* Uses flat structure consistent with other endpoints
|
|
370
|
+
*/
|
|
371
|
+
interface StartProcessRequest {
|
|
372
|
+
command: string;
|
|
373
|
+
sessionId?: string;
|
|
374
|
+
processId?: string;
|
|
375
|
+
timeoutMs?: number;
|
|
376
|
+
env?: Record<string, string | undefined>;
|
|
377
|
+
cwd?: string;
|
|
378
|
+
encoding?: string;
|
|
379
|
+
autoCleanup?: boolean;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Request to expose a port
|
|
383
|
+
*/
|
|
384
|
+
interface ExposePortRequest {
|
|
385
|
+
port: number;
|
|
386
|
+
name?: string;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Response from the container after creating a backup archive
|
|
390
|
+
*/
|
|
391
|
+
interface CreateBackupResponse {
|
|
392
|
+
success: boolean;
|
|
393
|
+
/** Size of the archive in bytes */
|
|
394
|
+
sizeBytes: number;
|
|
395
|
+
/** Path to the archive file in the container */
|
|
396
|
+
archivePath: string;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Response from the container after restoring a backup
|
|
400
|
+
*/
|
|
401
|
+
interface RestoreBackupResponse {
|
|
402
|
+
success: boolean;
|
|
403
|
+
/** Directory that was restored */
|
|
404
|
+
dir: string;
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
351
407
|
//#region ../shared/dist/types.d.ts
|
|
352
408
|
interface BaseExecOptions {
|
|
353
409
|
/**
|
|
@@ -1009,8 +1065,41 @@ interface ExecutionSession {
|
|
|
1009
1065
|
deleteCodeContext(contextId: string): Promise<void>;
|
|
1010
1066
|
mountBucket(bucket: string, mountPath: string, options: MountBucketOptions): Promise<void>;
|
|
1011
1067
|
unmountBucket(mountPath: string): Promise<void>;
|
|
1068
|
+
createBackup(options: BackupOptions): Promise<DirectoryBackup>;
|
|
1069
|
+
restoreBackup(backup: DirectoryBackup): Promise<RestoreBackupResult>;
|
|
1012
1070
|
terminal(request: Request, options?: PtyOptions): Promise<Response>;
|
|
1013
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Options for creating a directory backup
|
|
1074
|
+
*/
|
|
1075
|
+
interface BackupOptions {
|
|
1076
|
+
/** Directory to back up (absolute path). Required. */
|
|
1077
|
+
dir: string;
|
|
1078
|
+
/** Human-readable name for this backup. Optional. */
|
|
1079
|
+
name?: string;
|
|
1080
|
+
/** Seconds until automatic garbage collection. Default: 259200 (3 days). No upper limit. */
|
|
1081
|
+
ttl?: number;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Handle representing a stored directory backup.
|
|
1085
|
+
* Serializable (two strings). The user stores this and passes it to restoreBackup().
|
|
1086
|
+
*/
|
|
1087
|
+
interface DirectoryBackup {
|
|
1088
|
+
/** Unique backup identifier */
|
|
1089
|
+
readonly id: string;
|
|
1090
|
+
/** Directory that was backed up */
|
|
1091
|
+
readonly dir: string;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Result returned from a successful restoreBackup() call
|
|
1095
|
+
*/
|
|
1096
|
+
interface RestoreBackupResult {
|
|
1097
|
+
success: boolean;
|
|
1098
|
+
/** The directory that was restored */
|
|
1099
|
+
dir: string;
|
|
1100
|
+
/** Backup ID that was restored */
|
|
1101
|
+
id: string;
|
|
1102
|
+
}
|
|
1014
1103
|
/**
|
|
1015
1104
|
* Supported S3-compatible storage providers
|
|
1016
1105
|
*/
|
|
@@ -1128,6 +1217,8 @@ interface ISandbox {
|
|
|
1128
1217
|
runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream>;
|
|
1129
1218
|
listCodeContexts(): Promise<CodeContext[]>;
|
|
1130
1219
|
deleteCodeContext(contextId: string): Promise<void>;
|
|
1220
|
+
createBackup(options: BackupOptions): Promise<DirectoryBackup>;
|
|
1221
|
+
restoreBackup(backup: DirectoryBackup): Promise<RestoreBackupResult>;
|
|
1131
1222
|
wsConnect(request: Request, port: number): Promise<Response>;
|
|
1132
1223
|
}
|
|
1133
1224
|
declare function isExecResult(value: any): value is ExecResult;
|
|
@@ -1320,6 +1411,31 @@ declare abstract class BaseHttpClient {
|
|
|
1320
1411
|
protected logError(operation: string, error: unknown): void;
|
|
1321
1412
|
}
|
|
1322
1413
|
//#endregion
|
|
1414
|
+
//#region src/clients/backup-client.d.ts
|
|
1415
|
+
/**
|
|
1416
|
+
* Client for backup operations.
|
|
1417
|
+
*
|
|
1418
|
+
* Handles communication with the container's backup endpoints.
|
|
1419
|
+
* The container creates/extracts squashfs archives locally.
|
|
1420
|
+
* R2 upload/download is handled by the Sandbox DO, not by this client.
|
|
1421
|
+
*/
|
|
1422
|
+
declare class BackupClient extends BaseHttpClient {
|
|
1423
|
+
/**
|
|
1424
|
+
* Tell the container to create a squashfs archive from a directory.
|
|
1425
|
+
* @param dir - Directory to back up
|
|
1426
|
+
* @param archivePath - Where the container should write the archive
|
|
1427
|
+
* @param sessionId - Session context
|
|
1428
|
+
*/
|
|
1429
|
+
createArchive(dir: string, archivePath: string, sessionId: string): Promise<CreateBackupResponse>;
|
|
1430
|
+
/**
|
|
1431
|
+
* Tell the container to restore a squashfs archive into a directory.
|
|
1432
|
+
* @param dir - Target directory
|
|
1433
|
+
* @param archivePath - Path to the archive file in the container
|
|
1434
|
+
* @param sessionId - Session context
|
|
1435
|
+
*/
|
|
1436
|
+
restoreArchive(dir: string, archivePath: string, sessionId: string): Promise<RestoreBackupResponse>;
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1323
1439
|
//#region src/clients/command-client.d.ts
|
|
1324
1440
|
/**
|
|
1325
1441
|
* Response interface for command execution
|
|
@@ -1692,6 +1808,7 @@ declare class UtilityClient extends BaseHttpClient {
|
|
|
1692
1808
|
* WebSocket mode reduces sub-request count when running inside Workers/Durable Objects.
|
|
1693
1809
|
*/
|
|
1694
1810
|
declare class SandboxClient {
|
|
1811
|
+
readonly backup: BackupClient;
|
|
1695
1812
|
readonly commands: CommandClient;
|
|
1696
1813
|
readonly files: FileClient;
|
|
1697
1814
|
readonly processes: ProcessClient;
|
|
@@ -1738,6 +1855,24 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
1738
1855
|
private keepAliveEnabled;
|
|
1739
1856
|
private activeMounts;
|
|
1740
1857
|
private transport;
|
|
1858
|
+
private backupBucket;
|
|
1859
|
+
/**
|
|
1860
|
+
* Serializes backup operations to prevent concurrent create/restore on the same sandbox.
|
|
1861
|
+
*
|
|
1862
|
+
* This is in-memory state — it resets if the Durable Object is evicted and
|
|
1863
|
+
* re-instantiated (e.g. after sleep). This is acceptable because the container
|
|
1864
|
+
* filesystem is also lost on eviction, so there is no archive to race on.
|
|
1865
|
+
*/
|
|
1866
|
+
private backupInProgress;
|
|
1867
|
+
/**
|
|
1868
|
+
* R2 presigned URL credentials for direct container-to-R2 transfers.
|
|
1869
|
+
* All four fields plus the R2 binding must be configured for backup to work.
|
|
1870
|
+
*/
|
|
1871
|
+
private r2AccessKeyId;
|
|
1872
|
+
private r2SecretAccessKey;
|
|
1873
|
+
private r2AccountId;
|
|
1874
|
+
private backupBucketName;
|
|
1875
|
+
private r2Client;
|
|
1741
1876
|
/**
|
|
1742
1877
|
* Default container startup timeouts (conservative for production)
|
|
1743
1878
|
* Based on Cloudflare docs: "Containers take several minutes to provision"
|
|
@@ -1972,7 +2107,7 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
1972
2107
|
* @param options - Configuration options
|
|
1973
2108
|
* @param options.hostname - Your Worker's domain name (required for preview URL construction)
|
|
1974
2109
|
* @param options.name - Optional friendly name for the port
|
|
1975
|
-
* @param options.token - Optional custom token for the preview URL (1-16 characters: lowercase letters, numbers,
|
|
2110
|
+
* @param options.token - Optional custom token for the preview URL (1-16 characters: lowercase letters, numbers, underscores)
|
|
1976
2111
|
* If not provided, a random 16-character token will be generated automatically
|
|
1977
2112
|
* @returns Preview URL information including the full URL, port number, and optional name
|
|
1978
2113
|
*
|
|
@@ -1985,9 +2120,9 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
1985
2120
|
* // With custom token for stable URLs across deployments
|
|
1986
2121
|
* const { url } = await sandbox.exposePort(8080, {
|
|
1987
2122
|
* hostname: 'example.com',
|
|
1988
|
-
* token: '
|
|
2123
|
+
* token: 'my_token_v1'
|
|
1989
2124
|
* });
|
|
1990
|
-
* // url: https://8080-sandbox-id-
|
|
2125
|
+
* // url: https://8080-sandbox-id-my_token_v1.example.com
|
|
1991
2126
|
*/
|
|
1992
2127
|
exposePort(port: number, options: {
|
|
1993
2128
|
name?: string;
|
|
@@ -2042,7 +2177,111 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
2042
2177
|
runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream>;
|
|
2043
2178
|
listCodeContexts(): Promise<CodeContext[]>;
|
|
2044
2179
|
deleteCodeContext(contextId: string): Promise<void>;
|
|
2180
|
+
/** UUID v4 format validator for backup IDs */
|
|
2181
|
+
private static readonly UUID_REGEX;
|
|
2182
|
+
/**
|
|
2183
|
+
* Validate that a directory path is safe for backup operations.
|
|
2184
|
+
* Rejects empty, relative, traversal, and null-byte paths.
|
|
2185
|
+
*/
|
|
2186
|
+
private static validateBackupDir;
|
|
2187
|
+
/**
|
|
2188
|
+
* Returns the R2 bucket or throws if backup is not configured.
|
|
2189
|
+
*/
|
|
2190
|
+
private requireBackupBucket;
|
|
2191
|
+
private static readonly PRESIGNED_URL_EXPIRY_SECONDS;
|
|
2192
|
+
/**
|
|
2193
|
+
* Ensure a dedicated session for backup operations exists.
|
|
2194
|
+
* Isolates backup shell commands (curl, stat, rm, mkdir) from user exec()
|
|
2195
|
+
* calls to prevent session state interference and interleaving.
|
|
2196
|
+
*/
|
|
2197
|
+
private ensureBackupSession;
|
|
2198
|
+
/**
|
|
2199
|
+
* Returns validated presigned URL configuration or throws if not configured.
|
|
2200
|
+
* All credential fields plus the R2 binding are required for backup to work.
|
|
2201
|
+
*/
|
|
2202
|
+
private requirePresignedUrlSupport;
|
|
2203
|
+
/**
|
|
2204
|
+
* Generate a presigned GET URL for downloading an object from R2.
|
|
2205
|
+
* The container can curl this URL directly without credentials.
|
|
2206
|
+
*/
|
|
2207
|
+
private generatePresignedGetUrl;
|
|
2208
|
+
/**
|
|
2209
|
+
* Generate a presigned PUT URL for uploading an object to R2.
|
|
2210
|
+
* The container can curl PUT to this URL directly without credentials.
|
|
2211
|
+
*/
|
|
2212
|
+
private generatePresignedPutUrl;
|
|
2213
|
+
/**
|
|
2214
|
+
* Upload a backup archive via presigned PUT URL.
|
|
2215
|
+
* The container curls the archive directly to R2, bypassing the DO.
|
|
2216
|
+
* ~24 MB/s throughput vs ~0.6 MB/s for base64 readFile.
|
|
2217
|
+
*/
|
|
2218
|
+
private uploadBackupPresigned;
|
|
2219
|
+
/**
|
|
2220
|
+
* Download a backup archive via presigned GET URL.
|
|
2221
|
+
* The container curls the archive directly from R2, bypassing the DO.
|
|
2222
|
+
* ~93 MB/s throughput vs ~0.6 MB/s for base64 writeFile.
|
|
2223
|
+
*/
|
|
2224
|
+
private downloadBackupPresigned;
|
|
2225
|
+
/**
|
|
2226
|
+
* Serialize backup operations on this sandbox instance.
|
|
2227
|
+
* Concurrent backup/restore calls are queued so the multi-step
|
|
2228
|
+
* create-archive → read → upload (or download → write → extract) flow
|
|
2229
|
+
* is not interleaved with another backup operation on the same directory.
|
|
2230
|
+
*/
|
|
2231
|
+
private enqueueBackupOp;
|
|
2232
|
+
/**
|
|
2233
|
+
* Create a backup of a directory and upload it to R2.
|
|
2234
|
+
*
|
|
2235
|
+
* Flow:
|
|
2236
|
+
* 1. Container creates squashfs archive from the directory
|
|
2237
|
+
* 2. Container uploads the archive directly to R2 via presigned URL
|
|
2238
|
+
* 3. DO writes metadata to R2
|
|
2239
|
+
* 4. Container cleans up the local archive
|
|
2240
|
+
*
|
|
2241
|
+
* The returned DirectoryBackup handle is serializable. Store it anywhere
|
|
2242
|
+
* (KV, D1, DO storage) and pass it to restoreBackup() later.
|
|
2243
|
+
*
|
|
2244
|
+
* Concurrent backup/restore calls on the same sandbox are serialized.
|
|
2245
|
+
*
|
|
2246
|
+
* Partially-written files in the target directory may not be captured
|
|
2247
|
+
* consistently. Completed writes are captured.
|
|
2248
|
+
*
|
|
2249
|
+
* NOTE: Expired backups are not automatically deleted from R2. Configure
|
|
2250
|
+
* R2 lifecycle rules on the BACKUP_BUCKET to garbage-collect objects
|
|
2251
|
+
* under the `backups/` prefix after the desired retention period.
|
|
2252
|
+
*/
|
|
2253
|
+
createBackup(options: BackupOptions): Promise<DirectoryBackup>;
|
|
2254
|
+
private doCreateBackup;
|
|
2255
|
+
/**
|
|
2256
|
+
* Restore a backup from R2 into a directory.
|
|
2257
|
+
*
|
|
2258
|
+
* Flow:
|
|
2259
|
+
* 1. DO reads metadata from R2 and checks TTL
|
|
2260
|
+
* 2. Container downloads the archive directly from R2 via presigned URL
|
|
2261
|
+
* 3. Container mounts the squashfs archive with FUSE overlayfs
|
|
2262
|
+
*
|
|
2263
|
+
* The target directory becomes an overlay mount with the backup as a
|
|
2264
|
+
* read-only lower layer and a writable upper layer for copy-on-write.
|
|
2265
|
+
* Any processes writing to the directory should be stopped first.
|
|
2266
|
+
*
|
|
2267
|
+
* **Mount Lifecycle**: The FUSE overlay mount persists only while the
|
|
2268
|
+
* container is running. When the sandbox sleeps or the container restarts,
|
|
2269
|
+
* the mount is lost and the directory becomes empty. Re-restore from the
|
|
2270
|
+
* backup handle to recover. This is an ephemeral restore, not a persistent
|
|
2271
|
+
* extraction.
|
|
2272
|
+
*
|
|
2273
|
+
* The backup is restored into `backup.dir`. This may differ from the
|
|
2274
|
+
* directory that was originally backed up, allowing cross-directory restore.
|
|
2275
|
+
*
|
|
2276
|
+
* Overlapping backups are independent: restoring a parent directory
|
|
2277
|
+
* overwrites everything inside it, including subdirectories that were
|
|
2278
|
+
* backed up separately. When restoring both, restore the parent first.
|
|
2279
|
+
*
|
|
2280
|
+
* Concurrent backup/restore calls on the same sandbox are serialized.
|
|
2281
|
+
*/
|
|
2282
|
+
restoreBackup(backup: DirectoryBackup): Promise<RestoreBackupResult>;
|
|
2283
|
+
private doRestoreBackup;
|
|
2045
2284
|
}
|
|
2046
2285
|
//#endregion
|
|
2047
|
-
export {
|
|
2048
|
-
//# sourceMappingURL=sandbox-
|
|
2286
|
+
export { Process as $, RequestConfig as A, ExecResult as B, CommandClient as C, Execution as Ct, ContainerStub as D, BaseApiResponse as E, BucketCredentials as F, GitCheckoutResult as G, FileChunk as H, BucketProvider as I, LogEvent as J, ISandbox as K, DirectoryBackup as L, SessionRequest as M, BackupOptions as N, ErrorResponse as O, BaseExecOptions as P, PortListResult as Q, ExecEvent as R, WriteFileRequest as S, CreateContextOptions as St, BackupClient as T, RunCodeOptions as Tt, FileMetadata as U, ExecutionSession as V, FileStreamEvent as W, PortCloseResult as X, MountBucketOptions as Y, PortExposeResult as Z, GitClient as _, ExecuteRequest as _t, CreateSessionRequest as a, ProcessOptions as at, MkdirRequest as b, PtyOptions as bt, DeleteSessionResponse as c, RestoreBackupResult as ct, ProcessClient as d, StreamOptions as dt, ProcessCleanupResult as et, PortClient as f, WaitForLogResult as ft, GitCheckoutRequest as g, isProcessStatus as gt, InterpreterClient as h, isProcess as ht, CommandsResponse as i, ProcessLogsResult as it, ResponseHandler as j, HttpClientOptions as k, PingResponse as l, SandboxOptions as lt, ExecutionCallbacks as m, isExecResult as mt, getSandbox as n, ProcessKillResult as nt, CreateSessionResponse as o, ProcessStartResult as ot, UnexposePortRequest as p, WaitForPortOptions as pt, ListFilesOptions as q, SandboxClient as r, ProcessListResult as rt, DeleteSessionRequest as s, ProcessStatus as st, Sandbox as t, ProcessInfoResult as tt, UtilityClient as u, SessionOptions as ut, FileClient as v, ExposePortRequest as vt, ExecuteResponse as w, ExecutionResult as wt, ReadFileRequest as x, CodeContext as xt, FileOperationRequest as y, StartProcessRequest as yt, ExecOptions as z };
|
|
2287
|
+
//# sourceMappingURL=sandbox-DGAjk7r3.d.ts.map
|