@tanstack/ai-sandbox-cloudflare 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/esm/agent.d.ts +4 -0
  2. package/dist/esm/agent.js +9 -21
  3. package/dist/esm/chat-coordinator.js +144 -132
  4. package/dist/esm/chat-coordinator.js.map +1 -1
  5. package/dist/esm/container-coordinator.js +251 -247
  6. package/dist/esm/container-coordinator.js.map +1 -1
  7. package/dist/esm/coordinator.d.ts +3 -2
  8. package/dist/esm/coordinator.js +204 -184
  9. package/dist/esm/coordinator.js.map +1 -1
  10. package/dist/esm/durability.d.ts +32 -0
  11. package/dist/esm/durability.js +104 -0
  12. package/dist/esm/durability.js.map +1 -0
  13. package/dist/esm/factory.js +98 -59
  14. package/dist/esm/factory.js.map +1 -1
  15. package/dist/esm/handle.js +205 -203
  16. package/dist/esm/handle.js.map +1 -1
  17. package/dist/esm/index.js +2 -8
  18. package/dist/esm/preview-tool.d.ts +7 -1
  19. package/dist/esm/preview-tool.js +75 -32
  20. package/dist/esm/preview-tool.js.map +1 -1
  21. package/dist/esm/protocol.js +61 -50
  22. package/dist/esm/protocol.js.map +1 -1
  23. package/dist/esm/provider.js +43 -62
  24. package/dist/esm/provider.js.map +1 -1
  25. package/dist/esm/public-host.js +84 -39
  26. package/dist/esm/public-host.js.map +1 -1
  27. package/dist/esm/run-log-do.d.ts +19 -5
  28. package/dist/esm/run-log-do.js +196 -121
  29. package/dist/esm/run-log-do.js.map +1 -1
  30. package/dist/esm/run-log.d.ts +127 -0
  31. package/dist/esm/run-log.js +198 -0
  32. package/dist/esm/run-log.js.map +1 -0
  33. package/dist/esm/runner.js +146 -95
  34. package/dist/esm/runner.js.map +1 -1
  35. package/dist/esm/web-crypto.js +27 -16
  36. package/dist/esm/web-crypto.js.map +1 -1
  37. package/dist/esm/worker.js +84 -72
  38. package/dist/esm/worker.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/agent.ts +26 -0
  41. package/src/coordinator.ts +36 -14
  42. package/src/durability.ts +164 -0
  43. package/src/handle.ts +5 -0
  44. package/src/run-log-do.ts +85 -20
  45. package/src/run-log.ts +352 -0
  46. package/dist/esm/agent.js.map +0 -1
  47. package/dist/esm/index.js.map +0 -1
@@ -1,69 +1,108 @@
1
- import { defineSandbox, defineWorkspace } from "@tanstack/ai-sandbox";
2
- import { Sandbox } from "@cloudflare/sandbox";
3
1
  import { cloudflareSandbox } from "./provider.js";
2
+ import { resolvePreviewHost } from "./public-host.js";
3
+ import "./coordinator.js";
4
4
  import { ChatSandboxCoordinator } from "./chat-coordinator.js";
5
5
  import { ContainerSandboxCoordinator } from "./container-coordinator.js";
6
6
  import { createSandboxAgentWorker } from "./worker.js";
7
- import "cloudflare:workers";
8
- import "@tanstack/ai";
9
- import { resolvePreviewHost } from "./public-host.js";
7
+ import { Sandbox } from "@cloudflare/sandbox";
8
+ import { defineSandbox, defineWorkspace } from "@tanstack/ai-sandbox";
9
+ //#region src/factory.ts
10
+ /**
11
+ * `createCloudflareSandboxAgent` — the headline DX: one configured function call
12
+ * returns the Durable Object coordinator, the Sandbox DO, and the Worker fetch
13
+ * handler, so a Cloudflare app's whole `worker.ts` is just export wiring:
14
+ *
15
+ * ```ts
16
+ * const agent = createCloudflareSandboxAgent({
17
+ * adapter: () => claudeCodeText('sonnet'),
18
+ * })
19
+ * export const RunCoordinator = agent.Coordinator
20
+ * export const Sandbox = agent.Sandbox
21
+ * export default agent.worker
22
+ * ```
23
+ *
24
+ * Two modes, switched by `config.mode`:
25
+ * - `'do-drives'` (default) → a {@link ChatSandboxCoordinator}: the DO runs
26
+ * `chat()` itself and hosts the MCP tool-bridge.
27
+ * - `'colocated'` → a {@link ContainerSandboxCoordinator}: an in-container
28
+ * runner runs `chat()`; the DO is a thin coordinator that executes host tools.
29
+ *
30
+ * Env bindings (set in `wrangler.jsonc`):
31
+ * - `RUN_COORDINATOR` — this coordinator DO's own namespace (so the Worker can
32
+ * address it by `threadId`). Class name: whatever you export `Coordinator` as.
33
+ * - `Sandbox` — the `@cloudflare/sandbox` Sandbox DO namespace (the container
34
+ * hosts). Bind the exported `Sandbox` class.
35
+ * - `PUBLIC_HOSTNAME` — OPTIONAL. Hostname the CONTAINER uses to reach the Worker's
36
+ * tool-bridge / tool-exec endpoint. Unset → request-derived (local dev →
37
+ * `host.docker.internal`). See `resolveBridgeOrigin`.
38
+ * - `PREVIEW_HOSTNAME` — OPTIONAL. Custom domain (with a `*.<domain>` route) for
39
+ * browser-facing `exposePort` preview URLs. Unset → request-derived (local dev →
40
+ * `localhost`); REQUIRED on a `*.workers.dev` deploy, which has no wildcard
41
+ * subdomains. See `resolvePreviewHost`.
42
+ * - The harness's API key (`ANTHROPIC_API_KEY` for Claude Code, `CODEX_API_KEY` for
43
+ * codex, …) — supplied by YOUR app, never by the package. Declare it as a secret
44
+ * on the run's workspace (via a `sandbox`/`workspace` resolver) and add the field
45
+ * to your own env type; the coordinator injects each declared secret into the
46
+ * sandbox env by name. The package itself is harness-agnostic and binds no key.
47
+ *
48
+ * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack
49
+ * AI types; not runtime-verified in this repo (no Workers runtime here).
50
+ */
51
+ /** Build the default per-thread Cloudflare sandbox for the DO-drives mode. */
10
52
  function defaultSandbox(env, input, workspace) {
11
- return defineSandbox({
12
- id: "cf-edge-agent",
13
- provider: cloudflareSandbox({
14
- binding: env.Sandbox,
15
- // Browser-facing preview host: `PREVIEW_HOSTNAME` if set, else derived from
16
- // the trigger request (local dev → `localhost`; deployed → a custom domain,
17
- // since `*.workers.dev` has no wildcard). See `resolvePreviewHost`.
18
- previewHostname: resolvePreviewHost(env, input)
19
- }),
20
- workspace: workspace ?? // The container image ships the harness CLI; no source to clone, and NO auth
21
- // secrets — the package is harness-agnostic, so it can't know which key the
22
- // CLI needs. Supply the harness's API key via a `workspace` with `secrets`
23
- // (or a custom `sandbox` resolver), e.g.:
24
- // workspace: defineWorkspace({
25
- // source: { type: 'none' },
26
- // secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }),
27
- // })
28
- defineWorkspace({ source: { type: "none" } }),
29
- // One sandbox per thread, so a follow-up run resumes the same workspace.
30
- lifecycle: { reuse: "thread" }
31
- });
53
+ return defineSandbox({
54
+ id: "cf-edge-agent",
55
+ provider: cloudflareSandbox({
56
+ binding: env.Sandbox,
57
+ previewHostname: resolvePreviewHost(env, input)
58
+ }),
59
+ workspace: workspace ?? defineWorkspace({ source: { type: "none" } }),
60
+ lifecycle: { reuse: "thread" }
61
+ });
32
62
  }
63
+ /** Resolve the coordinator DO that owns a thread's runs (`RUN_COORDINATOR`). */
33
64
  function resolveCoordinator(env, threadId) {
34
- return env.RUN_COORDINATOR.get(env.RUN_COORDINATOR.idFromName(threadId));
65
+ return env.RUN_COORDINATOR.get(env.RUN_COORDINATOR.idFromName(threadId));
35
66
  }
36
67
  function createCloudflareSandboxAgent(config) {
37
- const worker = createSandboxAgentWorker(resolveCoordinator);
38
- if (config.mode === "colocated") {
39
- const colocated = config;
40
- class ConfiguredContainerCoordinator extends ContainerSandboxCoordinator {
41
- config(input) {
42
- return {
43
- hostTools: colocated.tools?.(input, this.env) ?? [],
44
- workspace: colocated.workspace,
45
- harness: colocated.harness,
46
- model: colocated.model
47
- };
48
- }
49
- }
50
- return { Coordinator: ConfiguredContainerCoordinator, Sandbox, worker };
51
- }
52
- const doDrives = config;
53
- class ConfiguredChatCoordinator extends ChatSandboxCoordinator {
54
- config(input) {
55
- const tools = doDrives.tools?.(input, this.env);
56
- return {
57
- adapter: doDrives.adapter(input, this.env),
58
- sandbox: doDrives.sandbox?.(input, this.env) ?? defaultSandbox(this.env, input, doDrives.workspace),
59
- ...tools !== void 0 ? { tools } : {},
60
- ...doDrives.systemPrompts !== void 0 ? { systemPrompts: doDrives.systemPrompts } : {}
61
- };
62
- }
63
- }
64
- return { Coordinator: ConfiguredChatCoordinator, Sandbox, worker };
68
+ const worker = createSandboxAgentWorker(resolveCoordinator);
69
+ if (config.mode === "colocated") {
70
+ const colocated = config;
71
+ class ConfiguredContainerCoordinator extends ContainerSandboxCoordinator {
72
+ config(input) {
73
+ return {
74
+ hostTools: colocated.tools?.(input, this.env) ?? [],
75
+ workspace: colocated.workspace,
76
+ harness: colocated.harness,
77
+ model: colocated.model
78
+ };
79
+ }
80
+ }
81
+ return {
82
+ Coordinator: ConfiguredContainerCoordinator,
83
+ Sandbox,
84
+ worker
85
+ };
86
+ }
87
+ const doDrives = config;
88
+ class ConfiguredChatCoordinator extends ChatSandboxCoordinator {
89
+ config(input) {
90
+ const tools = doDrives.tools?.(input, this.env);
91
+ return {
92
+ adapter: doDrives.adapter(input, this.env),
93
+ sandbox: doDrives.sandbox?.(input, this.env) ?? defaultSandbox(this.env, input, doDrives.workspace),
94
+ ...tools !== void 0 ? { tools } : {},
95
+ ...doDrives.systemPrompts !== void 0 ? { systemPrompts: doDrives.systemPrompts } : {}
96
+ };
97
+ }
98
+ }
99
+ return {
100
+ Coordinator: ConfiguredChatCoordinator,
101
+ Sandbox,
102
+ worker
103
+ };
65
104
  }
66
- export {
67
- createCloudflareSandboxAgent
68
- };
69
- //# sourceMappingURL=factory.js.map
105
+ //#endregion
106
+ export { createCloudflareSandboxAgent };
107
+
108
+ //# sourceMappingURL=factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","sources":["../../src/factory.ts"],"sourcesContent":["/**\n * `createCloudflareSandboxAgent` — the headline DX: one configured function call\n * returns the Durable Object coordinator, the Sandbox DO, and the Worker fetch\n * handler, so a Cloudflare app's whole `worker.ts` is just export wiring:\n *\n * ```ts\n * const agent = createCloudflareSandboxAgent({\n * adapter: () => claudeCodeText('sonnet'),\n * })\n * export const RunCoordinator = agent.Coordinator\n * export const Sandbox = agent.Sandbox\n * export default agent.worker\n * ```\n *\n * Two modes, switched by `config.mode`:\n * - `'do-drives'` (default) → a {@link ChatSandboxCoordinator}: the DO runs\n * `chat()` itself and hosts the MCP tool-bridge.\n * - `'colocated'` → a {@link ContainerSandboxCoordinator}: an in-container\n * runner runs `chat()`; the DO is a thin coordinator that executes host tools.\n *\n * Env bindings (set in `wrangler.jsonc`):\n * - `RUN_COORDINATOR` — this coordinator DO's own namespace (so the Worker can\n * address it by `threadId`). Class name: whatever you export `Coordinator` as.\n * - `Sandbox` — the `@cloudflare/sandbox` Sandbox DO namespace (the container\n * hosts). Bind the exported `Sandbox` class.\n * - `PUBLIC_HOSTNAME` — OPTIONAL. Hostname the CONTAINER uses to reach the Worker's\n * tool-bridge / tool-exec endpoint. Unset → request-derived (local dev →\n * `host.docker.internal`). See `resolveBridgeOrigin`.\n * - `PREVIEW_HOSTNAME` — OPTIONAL. Custom domain (with a `*.<domain>` route) for\n * browser-facing `exposePort` preview URLs. Unset → request-derived (local dev →\n * `localhost`); REQUIRED on a `*.workers.dev` deploy, which has no wildcard\n * subdomains. See `resolvePreviewHost`.\n * - The harness's API key (`ANTHROPIC_API_KEY` for Claude Code, `CODEX_API_KEY` for\n * codex, …) — supplied by YOUR app, never by the package. Declare it as a secret\n * on the run's workspace (via a `sandbox`/`workspace` resolver) and add the field\n * to your own env type; the coordinator injects each declared secret into the\n * sandbox env by name. The package itself is harness-agnostic and binds no key.\n *\n * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack\n * AI types; not runtime-verified in this repo (no Workers runtime here).\n */\nimport { defineSandbox, defineWorkspace } from '@tanstack/ai-sandbox'\nimport { Sandbox } from '@cloudflare/sandbox'\nimport { cloudflareSandbox } from './provider'\nimport { ChatSandboxCoordinator } from './chat-coordinator'\nimport { ContainerSandboxCoordinator } from './container-coordinator'\nimport { createSandboxAgentWorker } from './worker'\nimport { resolvePreviewHost } from './coordinator'\nimport type { ChatCoordinatorEnv, ChatRunConfig } from './chat-coordinator'\nimport type {\n ContainerCoordinatorEnv,\n ContainerRunConfig,\n} from './container-coordinator'\nimport type { HarnessId } from './protocol'\nimport type { SandboxCoordinator, StartRunInput } from './coordinator'\nimport type { AnyTextAdapter, AnyTool, SystemPrompt } from '@tanstack/ai'\nimport type {\n SandboxDefinition,\n WorkspaceDefinition,\n} from '@tanstack/ai-sandbox'\n\n/**\n * The base Env every generated app binds: the coordinator's own namespace, the\n * Sandbox namespace, the OPTIONAL bridge/preview hostnames (request-derived when\n * unset), and the Anthropic key. The two modes extend this with exactly the\n * coordinator base each one requires.\n */\nexport interface SandboxAgentEnv\n extends ChatCoordinatorEnv, ContainerCoordinatorEnv {\n /** This coordinator DO's own namespace (so the Worker can address it). */\n RUN_COORDINATOR: DurableObjectNamespace<SandboxCoordinator<SandboxAgentEnv>>\n /**\n * Custom domain (with a `*.<domain>` route) for browser-facing `exposePort`\n * preview URLs. Optional: unset → request-derived (local dev → `localhost`).\n * REQUIRED on a `*.workers.dev` deploy (no wildcard subdomains). Distinct from\n * `PUBLIC_HOSTNAME`, which is the CONTAINER→Worker bridge host. See\n * {@link resolvePreviewHost}.\n */\n PREVIEW_HOSTNAME?: string\n}\n\n/** Shared config across both modes. */\ninterface BaseAgentConfig<TEnv extends SandboxAgentEnv> {\n /** chat()-provided server tools, resolved per run (DO-drives: bridged over MCP). */\n tools?: (input: StartRunInput, env: TEnv) => Array<AnyTool>\n}\n\n/** DO-drives config: the DO runs `chat()` with the given adapter. */\nexport interface DoDrivesAgentConfig<\n TEnv extends SandboxAgentEnv,\n> extends BaseAgentConfig<TEnv> {\n mode?: 'do-drives'\n /** The harness/text adapter `chat()` runs, resolved per run. */\n adapter: (input: StartRunInput, env: TEnv) => AnyTextAdapter\n /**\n * Base system prompts prepended to every run's `chat()` (DO-drives only — the DO\n * runs `chat()` itself). The natural home for transport-level guidance the agent\n * needs regardless of what it builds — e.g. `systemPrompts: [PREVIEW_GUIDANCE]`\n * so previews don't reload-loop. See {@link PREVIEW_GUIDANCE}.\n */\n systemPrompts?: Array<SystemPrompt>\n /**\n * The sandbox the agent runs in, resolved per run. When omitted, a default\n * Cloudflare sandbox (one per thread, no source clone, NO auth secrets) is built\n * from the `Sandbox` binding and the resolved preview host, optionally\n * bootstrapping `workspace`. Supply the harness's API key either here (a custom\n * `sandbox` resolver whose workspace declares the secret) or via `workspace`\n * below — the package binds no key of its own.\n */\n sandbox?: (input: StartRunInput, env: TEnv) => SandboxDefinition\n /**\n * Workspace for the default sandbox (ignored when `sandbox` is provided). This is\n * where a default-sandbox app declares its harness auth, e.g.\n * `defineWorkspace({ source: { type: 'none' }, secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }) })`.\n */\n workspace?: WorkspaceDefinition\n}\n\n/** Co-located config: an in-container runner runs `chat()`. */\nexport interface ColocatedAgentConfig<\n TEnv extends SandboxAgentEnv,\n> extends BaseAgentConfig<TEnv> {\n mode: 'colocated'\n /** Which in-sandbox harness the runner spawns. */\n harness: HarnessId\n /** Model id passed to that harness. */\n model: string\n /** Workspace the in-container runner bootstraps for the agent. */\n workspace: WorkspaceDefinition\n}\n\nexport type CloudflareSandboxAgentConfig<TEnv extends SandboxAgentEnv> =\n | DoDrivesAgentConfig<TEnv>\n | ColocatedAgentConfig<TEnv>\n\n/** What {@link createCloudflareSandboxAgent} returns: the app's whole worker. */\nexport interface CloudflareSandboxAgent<TEnv extends SandboxAgentEnv> {\n /** The coordinator Durable Object class — export as your `RUN_COORDINATOR` binding. */\n Coordinator: new (\n ctx: DurableObjectState,\n env: TEnv,\n ) => SandboxCoordinator<TEnv>\n /** The `@cloudflare/sandbox` Sandbox DO class — export for the `Sandbox` binding. */\n Sandbox: typeof Sandbox\n /** The Worker fetch handler — `export default` it. */\n worker: ExportedHandler<TEnv>\n}\n\n/** Build the default per-thread Cloudflare sandbox for the DO-drives mode. */\nfunction defaultSandbox<TEnv extends SandboxAgentEnv>(\n env: TEnv,\n input: StartRunInput,\n workspace: WorkspaceDefinition | undefined,\n): SandboxDefinition {\n return defineSandbox({\n id: 'cf-edge-agent',\n provider: cloudflareSandbox({\n binding: env.Sandbox,\n // Browser-facing preview host: `PREVIEW_HOSTNAME` if set, else derived from\n // the trigger request (local dev → `localhost`; deployed → a custom domain,\n // since `*.workers.dev` has no wildcard). See `resolvePreviewHost`.\n previewHostname: resolvePreviewHost(env, input),\n }),\n workspace:\n workspace ??\n // The container image ships the harness CLI; no source to clone, and NO auth\n // secrets — the package is harness-agnostic, so it can't know which key the\n // CLI needs. Supply the harness's API key via a `workspace` with `secrets`\n // (or a custom `sandbox` resolver), e.g.:\n // workspace: defineWorkspace({\n // source: { type: 'none' },\n // secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }),\n // })\n defineWorkspace({ source: { type: 'none' } }),\n // One sandbox per thread, so a follow-up run resumes the same workspace.\n lifecycle: { reuse: 'thread' },\n })\n}\n\n/** Resolve the coordinator DO that owns a thread's runs (`RUN_COORDINATOR`). */\nfunction resolveCoordinator<TEnv extends SandboxAgentEnv>(\n env: TEnv,\n threadId: string,\n): DurableObjectStub<SandboxCoordinator<TEnv>> {\n return env.RUN_COORDINATOR.get(env.RUN_COORDINATOR.idFromName(threadId))\n}\n\nexport function createCloudflareSandboxAgent<\n TEnv extends SandboxAgentEnv = SandboxAgentEnv,\n>(config: CloudflareSandboxAgentConfig<TEnv>): CloudflareSandboxAgent<TEnv> {\n const worker = createSandboxAgentWorker<TEnv>(resolveCoordinator)\n\n if (config.mode === 'colocated') {\n const colocated = config\n class ConfiguredContainerCoordinator extends ContainerSandboxCoordinator<TEnv> {\n protected override config(input: StartRunInput): ContainerRunConfig {\n return {\n hostTools: colocated.tools?.(input, this.env) ?? [],\n workspace: colocated.workspace,\n harness: colocated.harness,\n model: colocated.model,\n }\n }\n }\n return { Coordinator: ConfiguredContainerCoordinator, Sandbox, worker }\n }\n\n const doDrives = config\n class ConfiguredChatCoordinator extends ChatSandboxCoordinator<TEnv> {\n protected override config(input: StartRunInput): ChatRunConfig {\n const tools = doDrives.tools?.(input, this.env)\n return {\n adapter: doDrives.adapter(input, this.env),\n sandbox:\n doDrives.sandbox?.(input, this.env) ??\n defaultSandbox(this.env, input, doDrives.workspace),\n ...(tools !== undefined ? { tools } : {}),\n ...(doDrives.systemPrompts !== undefined\n ? { systemPrompts: doDrives.systemPrompts }\n : {}),\n }\n }\n }\n return { Coordinator: ConfiguredChatCoordinator, Sandbox, worker }\n}\n"],"names":[],"mappings":";;;;;;;;;AAqJA,SAAS,eACP,KACA,OACA,WACmB;AACnB,SAAO,cAAc;AAAA,IACnB,IAAI;AAAA,IACJ,UAAU,kBAAkB;AAAA,MAC1B,SAAS,IAAI;AAAA;AAAA;AAAA;AAAA,MAIb,iBAAiB,mBAAmB,KAAK,KAAK;AAAA,IAAA,CAC/C;AAAA,IACD,WACE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,gBAAgB,EAAE,QAAQ,EAAE,MAAM,OAAA,GAAU;AAAA;AAAA,IAE9C,WAAW,EAAE,OAAO,SAAA;AAAA,EAAS,CAC9B;AACH;AAGA,SAAS,mBACP,KACA,UAC6C;AAC7C,SAAO,IAAI,gBAAgB,IAAI,IAAI,gBAAgB,WAAW,QAAQ,CAAC;AACzE;AAEO,SAAS,6BAEd,QAA0E;AAC1E,QAAM,SAAS,yBAA+B,kBAAkB;AAEhE,MAAI,OAAO,SAAS,aAAa;AAC/B,UAAM,YAAY;AAAA,IAClB,MAAM,uCAAuC,4BAAkC;AAAA,MAC1D,OAAO,OAA0C;AAClE,eAAO;AAAA,UACL,WAAW,UAAU,QAAQ,OAAO,KAAK,GAAG,KAAK,CAAA;AAAA,UACjD,WAAW,UAAU;AAAA,UACrB,SAAS,UAAU;AAAA,UACnB,OAAO,UAAU;AAAA,QAAA;AAAA,MAErB;AAAA,IAAA;AAEF,WAAO,EAAE,aAAa,gCAAgC,SAAS,OAAA;AAAA,EACjE;AAEA,QAAM,WAAW;AAAA,EACjB,MAAM,kCAAkC,uBAA6B;AAAA,IAChD,OAAO,OAAqC;AAC7D,YAAM,QAAQ,SAAS,QAAQ,OAAO,KAAK,GAAG;AAC9C,aAAO;AAAA,QACL,SAAS,SAAS,QAAQ,OAAO,KAAK,GAAG;AAAA,QACzC,SACE,SAAS,UAAU,OAAO,KAAK,GAAG,KAClC,eAAe,KAAK,KAAK,OAAO,SAAS,SAAS;AAAA,QACpD,GAAI,UAAU,SAAY,EAAE,MAAA,IAAU,CAAA;AAAA,QACtC,GAAI,SAAS,kBAAkB,SAC3B,EAAE,eAAe,SAAS,kBAC1B,CAAA;AAAA,MAAC;AAAA,IAET;AAAA,EAAA;AAEF,SAAO,EAAE,aAAa,2BAA2B,SAAS,OAAA;AAC5D;"}
1
+ {"version":3,"file":"factory.js","names":[],"sources":["../../src/factory.ts"],"sourcesContent":["/**\n * `createCloudflareSandboxAgent` — the headline DX: one configured function call\n * returns the Durable Object coordinator, the Sandbox DO, and the Worker fetch\n * handler, so a Cloudflare app's whole `worker.ts` is just export wiring:\n *\n * ```ts\n * const agent = createCloudflareSandboxAgent({\n * adapter: () => claudeCodeText('sonnet'),\n * })\n * export const RunCoordinator = agent.Coordinator\n * export const Sandbox = agent.Sandbox\n * export default agent.worker\n * ```\n *\n * Two modes, switched by `config.mode`:\n * - `'do-drives'` (default) → a {@link ChatSandboxCoordinator}: the DO runs\n * `chat()` itself and hosts the MCP tool-bridge.\n * - `'colocated'` → a {@link ContainerSandboxCoordinator}: an in-container\n * runner runs `chat()`; the DO is a thin coordinator that executes host tools.\n *\n * Env bindings (set in `wrangler.jsonc`):\n * - `RUN_COORDINATOR` — this coordinator DO's own namespace (so the Worker can\n * address it by `threadId`). Class name: whatever you export `Coordinator` as.\n * - `Sandbox` — the `@cloudflare/sandbox` Sandbox DO namespace (the container\n * hosts). Bind the exported `Sandbox` class.\n * - `PUBLIC_HOSTNAME` — OPTIONAL. Hostname the CONTAINER uses to reach the Worker's\n * tool-bridge / tool-exec endpoint. Unset → request-derived (local dev →\n * `host.docker.internal`). See `resolveBridgeOrigin`.\n * - `PREVIEW_HOSTNAME` — OPTIONAL. Custom domain (with a `*.<domain>` route) for\n * browser-facing `exposePort` preview URLs. Unset → request-derived (local dev →\n * `localhost`); REQUIRED on a `*.workers.dev` deploy, which has no wildcard\n * subdomains. See `resolvePreviewHost`.\n * - The harness's API key (`ANTHROPIC_API_KEY` for Claude Code, `CODEX_API_KEY` for\n * codex, …) — supplied by YOUR app, never by the package. Declare it as a secret\n * on the run's workspace (via a `sandbox`/`workspace` resolver) and add the field\n * to your own env type; the coordinator injects each declared secret into the\n * sandbox env by name. The package itself is harness-agnostic and binds no key.\n *\n * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack\n * AI types; not runtime-verified in this repo (no Workers runtime here).\n */\nimport { defineSandbox, defineWorkspace } from '@tanstack/ai-sandbox'\nimport { Sandbox } from '@cloudflare/sandbox'\nimport { cloudflareSandbox } from './provider'\nimport { ChatSandboxCoordinator } from './chat-coordinator'\nimport { ContainerSandboxCoordinator } from './container-coordinator'\nimport { createSandboxAgentWorker } from './worker'\nimport { resolvePreviewHost } from './coordinator'\nimport type { ChatCoordinatorEnv, ChatRunConfig } from './chat-coordinator'\nimport type {\n ContainerCoordinatorEnv,\n ContainerRunConfig,\n} from './container-coordinator'\nimport type { HarnessId } from './protocol'\nimport type { SandboxCoordinator, StartRunInput } from './coordinator'\nimport type { AnyTextAdapter, AnyTool, SystemPrompt } from '@tanstack/ai'\nimport type {\n SandboxDefinition,\n WorkspaceDefinition,\n} from '@tanstack/ai-sandbox'\n\n/**\n * The base Env every generated app binds: the coordinator's own namespace, the\n * Sandbox namespace, the OPTIONAL bridge/preview hostnames (request-derived when\n * unset), and the Anthropic key. The two modes extend this with exactly the\n * coordinator base each one requires.\n */\nexport interface SandboxAgentEnv\n extends ChatCoordinatorEnv, ContainerCoordinatorEnv {\n /** This coordinator DO's own namespace (so the Worker can address it). */\n RUN_COORDINATOR: DurableObjectNamespace<SandboxCoordinator<SandboxAgentEnv>>\n /**\n * Custom domain (with a `*.<domain>` route) for browser-facing `exposePort`\n * preview URLs. Optional: unset → request-derived (local dev → `localhost`).\n * REQUIRED on a `*.workers.dev` deploy (no wildcard subdomains). Distinct from\n * `PUBLIC_HOSTNAME`, which is the CONTAINER→Worker bridge host. See\n * {@link resolvePreviewHost}.\n */\n PREVIEW_HOSTNAME?: string\n}\n\n/** Shared config across both modes. */\ninterface BaseAgentConfig<TEnv extends SandboxAgentEnv> {\n /** chat()-provided server tools, resolved per run (DO-drives: bridged over MCP). */\n tools?: (input: StartRunInput, env: TEnv) => Array<AnyTool>\n}\n\n/** DO-drives config: the DO runs `chat()` with the given adapter. */\nexport interface DoDrivesAgentConfig<\n TEnv extends SandboxAgentEnv,\n> extends BaseAgentConfig<TEnv> {\n mode?: 'do-drives'\n /** The harness/text adapter `chat()` runs, resolved per run. */\n adapter: (input: StartRunInput, env: TEnv) => AnyTextAdapter\n /**\n * Base system prompts prepended to every run's `chat()` (DO-drives only — the DO\n * runs `chat()` itself). The natural home for transport-level guidance the agent\n * needs regardless of what it builds — e.g. `systemPrompts: [PREVIEW_GUIDANCE]`\n * so previews don't reload-loop. See {@link PREVIEW_GUIDANCE}.\n */\n systemPrompts?: Array<SystemPrompt>\n /**\n * The sandbox the agent runs in, resolved per run. When omitted, a default\n * Cloudflare sandbox (one per thread, no source clone, NO auth secrets) is built\n * from the `Sandbox` binding and the resolved preview host, optionally\n * bootstrapping `workspace`. Supply the harness's API key either here (a custom\n * `sandbox` resolver whose workspace declares the secret) or via `workspace`\n * below — the package binds no key of its own.\n */\n sandbox?: (input: StartRunInput, env: TEnv) => SandboxDefinition\n /**\n * Workspace for the default sandbox (ignored when `sandbox` is provided). This is\n * where a default-sandbox app declares its harness auth, e.g.\n * `defineWorkspace({ source: { type: 'none' }, secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }) })`.\n */\n workspace?: WorkspaceDefinition\n}\n\n/** Co-located config: an in-container runner runs `chat()`. */\nexport interface ColocatedAgentConfig<\n TEnv extends SandboxAgentEnv,\n> extends BaseAgentConfig<TEnv> {\n mode: 'colocated'\n /** Which in-sandbox harness the runner spawns. */\n harness: HarnessId\n /** Model id passed to that harness. */\n model: string\n /** Workspace the in-container runner bootstraps for the agent. */\n workspace: WorkspaceDefinition\n}\n\nexport type CloudflareSandboxAgentConfig<TEnv extends SandboxAgentEnv> =\n | DoDrivesAgentConfig<TEnv>\n | ColocatedAgentConfig<TEnv>\n\n/** What {@link createCloudflareSandboxAgent} returns: the app's whole worker. */\nexport interface CloudflareSandboxAgent<TEnv extends SandboxAgentEnv> {\n /** The coordinator Durable Object class — export as your `RUN_COORDINATOR` binding. */\n Coordinator: new (\n ctx: DurableObjectState,\n env: TEnv,\n ) => SandboxCoordinator<TEnv>\n /** The `@cloudflare/sandbox` Sandbox DO class — export for the `Sandbox` binding. */\n Sandbox: typeof Sandbox\n /** The Worker fetch handler — `export default` it. */\n worker: ExportedHandler<TEnv>\n}\n\n/** Build the default per-thread Cloudflare sandbox for the DO-drives mode. */\nfunction defaultSandbox<TEnv extends SandboxAgentEnv>(\n env: TEnv,\n input: StartRunInput,\n workspace: WorkspaceDefinition | undefined,\n): SandboxDefinition {\n return defineSandbox({\n id: 'cf-edge-agent',\n provider: cloudflareSandbox({\n binding: env.Sandbox,\n // Browser-facing preview host: `PREVIEW_HOSTNAME` if set, else derived from\n // the trigger request (local dev → `localhost`; deployed → a custom domain,\n // since `*.workers.dev` has no wildcard). See `resolvePreviewHost`.\n previewHostname: resolvePreviewHost(env, input),\n }),\n workspace:\n workspace ??\n // The container image ships the harness CLI; no source to clone, and NO auth\n // secrets — the package is harness-agnostic, so it can't know which key the\n // CLI needs. Supply the harness's API key via a `workspace` with `secrets`\n // (or a custom `sandbox` resolver), e.g.:\n // workspace: defineWorkspace({\n // source: { type: 'none' },\n // secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }),\n // })\n defineWorkspace({ source: { type: 'none' } }),\n // One sandbox per thread, so a follow-up run resumes the same workspace.\n lifecycle: { reuse: 'thread' },\n })\n}\n\n/** Resolve the coordinator DO that owns a thread's runs (`RUN_COORDINATOR`). */\nfunction resolveCoordinator<TEnv extends SandboxAgentEnv>(\n env: TEnv,\n threadId: string,\n): DurableObjectStub<SandboxCoordinator<TEnv>> {\n return env.RUN_COORDINATOR.get(env.RUN_COORDINATOR.idFromName(threadId))\n}\n\nexport function createCloudflareSandboxAgent<\n TEnv extends SandboxAgentEnv = SandboxAgentEnv,\n>(config: CloudflareSandboxAgentConfig<TEnv>): CloudflareSandboxAgent<TEnv> {\n const worker = createSandboxAgentWorker<TEnv>(resolveCoordinator)\n\n if (config.mode === 'colocated') {\n const colocated = config\n class ConfiguredContainerCoordinator extends ContainerSandboxCoordinator<TEnv> {\n protected override config(input: StartRunInput): ContainerRunConfig {\n return {\n hostTools: colocated.tools?.(input, this.env) ?? [],\n workspace: colocated.workspace,\n harness: colocated.harness,\n model: colocated.model,\n }\n }\n }\n return { Coordinator: ConfiguredContainerCoordinator, Sandbox, worker }\n }\n\n const doDrives = config\n class ConfiguredChatCoordinator extends ChatSandboxCoordinator<TEnv> {\n protected override config(input: StartRunInput): ChatRunConfig {\n const tools = doDrives.tools?.(input, this.env)\n return {\n adapter: doDrives.adapter(input, this.env),\n sandbox:\n doDrives.sandbox?.(input, this.env) ??\n defaultSandbox(this.env, input, doDrives.workspace),\n ...(tools !== undefined ? { tools } : {}),\n ...(doDrives.systemPrompts !== undefined\n ? { systemPrompts: doDrives.systemPrompts }\n : {}),\n }\n }\n }\n return { Coordinator: ConfiguredChatCoordinator, Sandbox, worker }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqJA,SAAS,eACP,KACA,OACA,WACmB;CACnB,OAAO,cAAc;EACnB,IAAI;EACJ,UAAU,kBAAkB;GAC1B,SAAS,IAAI;GAIb,iBAAiB,mBAAmB,KAAK,KAAK;EAChD,CAAC;EACD,WACE,aASA,gBAAgB,EAAE,QAAQ,EAAE,MAAM,OAAO,EAAE,CAAC;EAE9C,WAAW,EAAE,OAAO,SAAS;CAC/B,CAAC;AACH;;AAGA,SAAS,mBACP,KACA,UAC6C;CAC7C,OAAO,IAAI,gBAAgB,IAAI,IAAI,gBAAgB,WAAW,QAAQ,CAAC;AACzE;AAEA,SAAgB,6BAEd,QAA0E;CAC1E,MAAM,SAAS,yBAA+B,kBAAkB;CAEhE,IAAI,OAAO,SAAS,aAAa;EAC/B,MAAM,YAAY;EAClB,MAAM,uCAAuC,4BAAkC;GAC7E,OAA0B,OAA0C;IAClE,OAAO;KACL,WAAW,UAAU,QAAQ,OAAO,KAAK,GAAG,KAAK,CAAC;KAClD,WAAW,UAAU;KACrB,SAAS,UAAU;KACnB,OAAO,UAAU;IACnB;GACF;EACF;EACA,OAAO;GAAE,aAAa;GAAgC;GAAS;EAAO;CACxE;CAEA,MAAM,WAAW;CACjB,MAAM,kCAAkC,uBAA6B;EACnE,OAA0B,OAAqC;GAC7D,MAAM,QAAQ,SAAS,QAAQ,OAAO,KAAK,GAAG;GAC9C,OAAO;IACL,SAAS,SAAS,QAAQ,OAAO,KAAK,GAAG;IACzC,SACE,SAAS,UAAU,OAAO,KAAK,GAAG,KAClC,eAAe,KAAK,KAAK,OAAO,SAAS,SAAS;IACpD,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;IACvC,GAAI,SAAS,kBAAkB,KAAA,IAC3B,EAAE,eAAe,SAAS,cAAc,IACxC,CAAC;GACP;EACF;CACF;CACA,OAAO;EAAE,aAAa;EAA2B;EAAS;CAAO;AACnE"}
@@ -1,208 +1,210 @@
1
1
  import { createExecBackedGit } from "@tanstack/ai-sandbox";
2
- const CLOUDFLARE_CAPS = {
3
- fs: true,
4
- exec: true,
5
- env: true,
6
- ports: true,
7
- backgroundProcesses: true,
8
- // No writable host→process stdin; stdin-fed harnesses use file-redirection.
9
- writableStdin: false,
10
- snapshots: false,
11
- networkPolicy: false,
12
- durableFilesystem: false,
13
- fork: false
2
+ //#region src/handle.ts
3
+ /**
4
+ * SandboxHandle backed by a Cloudflare Sandbox (Containers + Durable Objects),
5
+ * via `@cloudflare/sandbox`. Runs at the edge inside a Worker.
6
+ *
7
+ * fs is implemented over `exec` with base64 piping (binary-safe), matching the
8
+ * Docker provider. The container disk is EPHEMERAL (wiped to the image on
9
+ * restart) and snapshots are not yet GA, so `capabilities.snapshots` and
10
+ * `durableFilesystem` are false — `withSandbox` re-bootstraps under the same
11
+ * identity across cold starts.
12
+ *
13
+ * LIMITATION: Cloudflare background processes do not expose a writable host→
14
+ * process stdin, so `spawn().stdin.write` throws. This is advertised via
15
+ * `capabilities.writableStdin: false`; harness adapters that feed a prompt over
16
+ * stdin (e.g. the Claude Code adapter) detect this and instead deliver the
17
+ * prompt via a file + shell stdin-redirection (`claude -p … < file`), which the
18
+ * in-container shell handles with no host-side stdin write. `exec` (one-shot)
19
+ * and streamed stdout from `spawn` both work fully.
20
+ *
21
+ * NOTE: not runtime-verified in this repo (requires a Workers runtime); it
22
+ * compiles against the real `@cloudflare/sandbox` types and follows the proven
23
+ * provider contract.
24
+ */
25
+ var CLOUDFLARE_CAPS = {
26
+ fs: true,
27
+ exec: true,
28
+ env: true,
29
+ ports: true,
30
+ backgroundProcesses: true,
31
+ writableStdin: false,
32
+ killableProcesses: false,
33
+ snapshots: false,
34
+ networkPolicy: false,
35
+ durableFilesystem: false,
36
+ fork: false
14
37
  };
38
+ /** POSIX single-quote escape for embedding paths in `sh -c`. */
15
39
  function q(value) {
16
- return `'${value.replace(/'/g, `'\\''`)}'`;
40
+ return `'${value.replace(/'/g, `'\\''`)}'`;
17
41
  }
18
- class OutputQueue {
19
- buffer = [];
20
- waiters = [];
21
- ended = false;
22
- push(value) {
23
- const waiter = this.waiters.shift();
24
- if (waiter) waiter({ value, done: false });
25
- else this.buffer.push(value);
26
- }
27
- end() {
28
- this.ended = true;
29
- let waiter = this.waiters.shift();
30
- while (waiter) {
31
- waiter({ value: void 0, done: true });
32
- waiter = this.waiters.shift();
33
- }
34
- }
35
- async *[Symbol.asyncIterator]() {
36
- while (!this.ended || this.buffer.length > 0) {
37
- if (this.buffer.length > 0) {
38
- yield this.buffer.shift();
39
- continue;
40
- }
41
- const next = await new Promise(
42
- (resolve) => this.waiters.push(resolve)
43
- );
44
- if (next.done) return;
45
- yield next.value;
46
- }
47
- }
48
- }
49
- class CloudflareHandle {
50
- id;
51
- provider = "cloudflare";
52
- workspaceRoot;
53
- capabilities = CLOUDFLARE_CAPS;
54
- fs;
55
- git;
56
- process;
57
- ports;
58
- env;
59
- sandbox;
60
- workdir;
61
- previewHostname;
62
- constructor(id, sandbox, workdir, previewHostname) {
63
- this.id = id;
64
- this.sandbox = sandbox;
65
- this.workdir = workdir;
66
- this.workspaceRoot = workdir;
67
- this.previewHostname = previewHostname;
68
- this.process = {
69
- exec: (command, opts) => this.exec(command, opts),
70
- spawn: (command, opts) => this.spawnProcess(command, opts)
71
- };
72
- this.fs = {
73
- read: async (p) => {
74
- const r = await this.exec(`base64 ${q(this.abs(p))}`);
75
- if (r.exitCode !== 0) throw new Error(`read failed: ${r.stderr.trim()}`);
76
- return Buffer.from(r.stdout, "base64").toString("utf8");
77
- },
78
- readBytes: async (p) => {
79
- const r = await this.exec(`base64 ${q(this.abs(p))}`);
80
- if (r.exitCode !== 0) throw new Error(`read failed: ${r.stderr.trim()}`);
81
- return new Uint8Array(Buffer.from(r.stdout, "base64"));
82
- },
83
- write: async (p, data) => {
84
- const abs = this.abs(p);
85
- const b64 = Buffer.from(
86
- typeof data === "string" ? Buffer.from(data, "utf8") : data
87
- ).toString("base64");
88
- const dir = abs.replace(/\/[^/]*$/, "") || "/";
89
- const r = await this.exec(
90
- `mkdir -p ${q(dir)} && printf %s ${q(b64)} | base64 -d > ${q(abs)}`
91
- );
92
- if (r.exitCode !== 0)
93
- throw new Error(`write failed: ${r.stderr.trim()}`);
94
- },
95
- list: async (p) => {
96
- const r = await this.exec(`ls -1Ap ${q(this.abs(p))}`);
97
- if (r.exitCode !== 0) throw new Error(`list failed: ${r.stderr.trim()}`);
98
- return r.stdout.split("\n").filter((line) => line.trim() !== "").map((entry) => {
99
- const isDir = entry.endsWith("/");
100
- const name = isDir ? entry.slice(0, -1) : entry;
101
- return {
102
- name,
103
- path: `${p.replace(/\/$/, "")}/${name}`,
104
- type: isDir ? "dir" : "file"
105
- };
106
- });
107
- },
108
- mkdir: async (p) => {
109
- await this.exec(`mkdir -p ${q(this.abs(p))}`);
110
- },
111
- remove: async (p) => {
112
- await this.exec(`rm -rf ${q(this.abs(p))}`);
113
- },
114
- rename: async (from, to) => {
115
- await this.exec(`mv ${q(this.abs(from))} ${q(this.abs(to))}`);
116
- },
117
- exists: async (p) => {
118
- const r = await this.exec(`test -e ${q(this.abs(p))}`);
119
- return r.exitCode === 0;
120
- }
121
- };
122
- this.git = createExecBackedGit(this.process, this.workdir);
123
- this.ports = {
124
- connect: (port) => this.connectPort(port)
125
- };
126
- this.env = {
127
- set: (vars) => this.sandbox.setEnvVars(vars)
128
- };
129
- }
130
- abs(p) {
131
- if (this.workdir === "/workspace") return p;
132
- if (p === "/workspace") return this.workdir;
133
- if (p.startsWith("/workspace/")) {
134
- return `${this.workdir}/${p.slice("/workspace/".length)}`;
135
- }
136
- return p;
137
- }
138
- async exec(command, opts) {
139
- const result = await this.sandbox.exec(command, {
140
- ...opts?.cwd ? { cwd: this.abs(opts.cwd) } : { cwd: this.workdir },
141
- ...opts?.env ? { env: opts.env } : {}
142
- });
143
- return {
144
- stdout: result.stdout,
145
- stderr: result.stderr,
146
- exitCode: result.exitCode
147
- };
148
- }
149
- spawnProcess(command, opts) {
150
- const stdout = new OutputQueue();
151
- const stderr = new OutputQueue();
152
- const settled = this.sandbox.exec(command, {
153
- ...opts?.cwd ? { cwd: this.abs(opts.cwd) } : { cwd: this.workdir },
154
- ...opts?.env ? { env: opts.env } : {},
155
- stream: true,
156
- onOutput: (stream, data) => {
157
- if (stream === "stdout") stdout.push(data);
158
- else stderr.push(data);
159
- }
160
- });
161
- const exitPromise = settled.then(
162
- (result) => {
163
- stdout.end();
164
- stderr.end();
165
- return result.exitCode;
166
- },
167
- (error) => {
168
- stdout.end();
169
- stderr.end();
170
- throw error;
171
- }
172
- );
173
- return Promise.resolve({
174
- pid: -1,
175
- stdout,
176
- stderr,
177
- stdin: {
178
- write: () => Promise.reject(
179
- new Error(
180
- "cloudflare: background processes do not expose stdin. Use exec(), or a stdin-capable provider (local-process / docker) for stdin-fed harnesses."
181
- )
182
- ),
183
- end: () => Promise.resolve()
184
- },
185
- wait: () => exitPromise,
186
- kill: () => Promise.resolve()
187
- });
188
- }
189
- async connectPort(port) {
190
- if (this.previewHostname === void 0) {
191
- throw new Error(
192
- "cloudflare: ports.connect requires a previewHostname. Pass previewHostname (your Worker request hostname) to cloudflareSandbox(...)."
193
- );
194
- }
195
- const { url } = await this.sandbox.exposePort(port, {
196
- hostname: this.previewHostname
197
- });
198
- return { url };
199
- }
200
- async destroy() {
201
- await this.sandbox.destroy();
202
- }
203
- }
204
- export {
205
- CLOUDFLARE_CAPS,
206
- CloudflareHandle
42
+ /** A push-driven async string queue used to adapt CF's onOutput callback. */
43
+ var OutputQueue = class {
44
+ buffer = [];
45
+ waiters = [];
46
+ ended = false;
47
+ push(value) {
48
+ const waiter = this.waiters.shift();
49
+ if (waiter) waiter({
50
+ value,
51
+ done: false
52
+ });
53
+ else this.buffer.push(value);
54
+ }
55
+ end() {
56
+ this.ended = true;
57
+ let waiter = this.waiters.shift();
58
+ while (waiter) {
59
+ waiter({
60
+ value: void 0,
61
+ done: true
62
+ });
63
+ waiter = this.waiters.shift();
64
+ }
65
+ }
66
+ async *[Symbol.asyncIterator]() {
67
+ while (!this.ended || this.buffer.length > 0) {
68
+ if (this.buffer.length > 0) {
69
+ yield this.buffer.shift();
70
+ continue;
71
+ }
72
+ const next = await new Promise((resolve) => this.waiters.push(resolve));
73
+ if (next.done) return;
74
+ yield next.value;
75
+ }
76
+ }
77
+ };
78
+ var CloudflareHandle = class {
79
+ id;
80
+ provider = "cloudflare";
81
+ workspaceRoot;
82
+ capabilities = CLOUDFLARE_CAPS;
83
+ fs;
84
+ git;
85
+ process;
86
+ ports;
87
+ env;
88
+ sandbox;
89
+ workdir;
90
+ previewHostname;
91
+ constructor(id, sandbox, workdir, previewHostname) {
92
+ this.id = id;
93
+ this.sandbox = sandbox;
94
+ this.workdir = workdir;
95
+ this.workspaceRoot = workdir;
96
+ this.previewHostname = previewHostname;
97
+ this.process = {
98
+ exec: (command, opts) => this.exec(command, opts),
99
+ spawn: (command, opts) => this.spawnProcess(command, opts)
100
+ };
101
+ this.fs = {
102
+ read: async (p) => {
103
+ const r = await this.exec(`base64 ${q(this.abs(p))}`);
104
+ if (r.exitCode !== 0) throw new Error(`read failed: ${r.stderr.trim()}`);
105
+ return Buffer.from(r.stdout, "base64").toString("utf8");
106
+ },
107
+ readBytes: async (p) => {
108
+ const r = await this.exec(`base64 ${q(this.abs(p))}`);
109
+ if (r.exitCode !== 0) throw new Error(`read failed: ${r.stderr.trim()}`);
110
+ return new Uint8Array(Buffer.from(r.stdout, "base64"));
111
+ },
112
+ write: async (p, data) => {
113
+ const abs = this.abs(p);
114
+ const b64 = Buffer.from(typeof data === "string" ? Buffer.from(data, "utf8") : data).toString("base64");
115
+ const dir = abs.replace(/\/[^/]*$/, "") || "/";
116
+ const r = await this.exec(`mkdir -p ${q(dir)} && printf %s ${q(b64)} | base64 -d > ${q(abs)}`);
117
+ if (r.exitCode !== 0) throw new Error(`write failed: ${r.stderr.trim()}`);
118
+ },
119
+ list: async (p) => {
120
+ const r = await this.exec(`ls -1Ap ${q(this.abs(p))}`);
121
+ if (r.exitCode !== 0) throw new Error(`list failed: ${r.stderr.trim()}`);
122
+ return r.stdout.split("\n").filter((line) => line.trim() !== "").map((entry) => {
123
+ const isDir = entry.endsWith("/");
124
+ const name = isDir ? entry.slice(0, -1) : entry;
125
+ return {
126
+ name,
127
+ path: `${p.replace(/\/$/, "")}/${name}`,
128
+ type: isDir ? "dir" : "file"
129
+ };
130
+ });
131
+ },
132
+ mkdir: async (p) => {
133
+ await this.exec(`mkdir -p ${q(this.abs(p))}`);
134
+ },
135
+ remove: async (p) => {
136
+ await this.exec(`rm -rf ${q(this.abs(p))}`);
137
+ },
138
+ rename: async (from, to) => {
139
+ await this.exec(`mv ${q(this.abs(from))} ${q(this.abs(to))}`);
140
+ },
141
+ exists: async (p) => {
142
+ return (await this.exec(`test -e ${q(this.abs(p))}`)).exitCode === 0;
143
+ }
144
+ };
145
+ this.git = createExecBackedGit(this.process, this.workdir);
146
+ this.ports = { connect: (port) => this.connectPort(port) };
147
+ this.env = { set: (vars) => this.sandbox.setEnvVars(vars) };
148
+ }
149
+ abs(p) {
150
+ if (this.workdir === "/workspace") return p;
151
+ if (p === "/workspace") return this.workdir;
152
+ if (p.startsWith("/workspace/")) return `${this.workdir}/${p.slice(11)}`;
153
+ return p;
154
+ }
155
+ async exec(command, opts) {
156
+ const result = await this.sandbox.exec(command, {
157
+ ...opts?.cwd ? { cwd: this.abs(opts.cwd) } : { cwd: this.workdir },
158
+ ...opts?.env ? { env: opts.env } : {}
159
+ });
160
+ return {
161
+ stdout: result.stdout,
162
+ stderr: result.stderr,
163
+ exitCode: result.exitCode
164
+ };
165
+ }
166
+ spawnProcess(command, opts) {
167
+ const stdout = new OutputQueue();
168
+ const stderr = new OutputQueue();
169
+ const exitPromise = this.sandbox.exec(command, {
170
+ ...opts?.cwd ? { cwd: this.abs(opts.cwd) } : { cwd: this.workdir },
171
+ ...opts?.env ? { env: opts.env } : {},
172
+ stream: true,
173
+ onOutput: (stream, data) => {
174
+ if (stream === "stdout") stdout.push(data);
175
+ else stderr.push(data);
176
+ }
177
+ }).then((result) => {
178
+ stdout.end();
179
+ stderr.end();
180
+ return result.exitCode;
181
+ }, (error) => {
182
+ stdout.end();
183
+ stderr.end();
184
+ throw error;
185
+ });
186
+ return Promise.resolve({
187
+ pid: -1,
188
+ stdout,
189
+ stderr,
190
+ stdin: {
191
+ write: () => Promise.reject(/* @__PURE__ */ new Error("cloudflare: background processes do not expose stdin. Use exec(), or a stdin-capable provider (local-process / docker) for stdin-fed harnesses.")),
192
+ end: () => Promise.resolve()
193
+ },
194
+ wait: () => exitPromise,
195
+ kill: () => Promise.resolve()
196
+ });
197
+ }
198
+ async connectPort(port) {
199
+ if (this.previewHostname === void 0) throw new Error("cloudflare: ports.connect requires a previewHostname. Pass previewHostname (your Worker request hostname) to cloudflareSandbox(...).");
200
+ const { url } = await this.sandbox.exposePort(port, { hostname: this.previewHostname });
201
+ return { url };
202
+ }
203
+ async destroy() {
204
+ await this.sandbox.destroy();
205
+ }
207
206
  };
208
- //# sourceMappingURL=handle.js.map
207
+ //#endregion
208
+ export { CLOUDFLARE_CAPS, CloudflareHandle };
209
+
210
+ //# sourceMappingURL=handle.js.map