Summary
Declaring custom methods on a Cloudflare.Container shape and calling them from the bound DO throws TypeError: <method> is not a function. Only fetch/getTcpPort are ever wired. The container tutorial shows agent.exec("echo hi") as a working RPC call, but the container runtime never registers shape methods.
Repro
export class Sandbox extends Cloudflare.Container<Sandbox, {
exec: (cmd: string) => Effect.Effect<{ stdout: string; exitCode: number }>;
}>()("Sandbox", { main: import.meta.filename }) {}
// Sandbox.runtime.ts
export default Sandbox.make(Effect.sync(() => Sandbox.of({
exec: (cmd) => /* ... */,
fetch: Effect.succeed(HttpServerResponse.text("ok")),
})));
// inside the DO
const sandbox = yield* Cloudflare.Container.bind(Sandbox);
const container = yield* Cloudflare.start(sandbox);
yield* container.exec("echo hi"); // TypeError: container.exec is not a function
Confirmed on 2.0.0-beta.57. The live started handle has keys running, destroy, signal, getTcpPort, setInactivityTimeout, interceptOutboundHttp, interceptAllOutboundHttp, monitor, start, fetch — no exec.
Root cause
Platform hands the full shape to serve (Platform.ts:341):
runtimeContext.serve?.(impl.fetch, { shape: impl })
The Worker runtime captures it (WorkerRuntimeContext.ts:94 — if (options?.shape) userShape = options.shape) and DurableObjectBridge proxies the calls over DO RPC, so Worker/DO shape methods work. The Container runtime's serve takes only the handler and drops { shape } (Cloudflare/Container/Container.ts:243):
const serve = <Req = never>(handler: HttpEffect<Req>) => /* serves only impl.fetch */
bindContainer returns lifecycle methods cast satisfies Container as Shape (ContainerBinding.ts:72), and start returns { ...container, getTcpPort, fetch } (StartContainer.ts:82) — so no shape method is ever present on the handle. The as Shape cast is why container.exec(...) type-checks but is undefined at runtime.
Note
The tutorial (tutorial/cloudflare/containers.mdx) presents agent.exec("echo hi") returning { stdout, exitCode } as if RPC works. Either containers should wire shape methods (an HTTP/RPC bridge over getTcpPort, mirroring the Worker/DO path), or the docs should show only getTcpPort(port).fetch(...).
I'd love to work on a fix or an implementation if I get an ok and a steer on the intended plan.
Summary
Declaring custom methods on a
Cloudflare.Containershape and calling them from the bound DO throwsTypeError: <method> is not a function. Onlyfetch/getTcpPortare ever wired. The container tutorial showsagent.exec("echo hi")as a working RPC call, but the container runtime never registers shape methods.Repro
Confirmed on
2.0.0-beta.57. The live started handle has keysrunning, destroy, signal, getTcpPort, setInactivityTimeout, interceptOutboundHttp, interceptAllOutboundHttp, monitor, start, fetch— noexec.Root cause
Platformhands the full shape toserve(Platform.ts:341):The Worker runtime captures it (
WorkerRuntimeContext.ts:94—if (options?.shape) userShape = options.shape) andDurableObjectBridgeproxies the calls over DO RPC, so Worker/DO shape methods work. The Container runtime'sservetakes only the handler and drops{ shape }(Cloudflare/Container/Container.ts:243):bindContainerreturns lifecycle methods castsatisfies Container as Shape(ContainerBinding.ts:72), andstartreturns{ ...container, getTcpPort, fetch }(StartContainer.ts:82) — so no shape method is ever present on the handle. Theas Shapecast is whycontainer.exec(...)type-checks but isundefinedat runtime.Note
The tutorial (
tutorial/cloudflare/containers.mdx) presentsagent.exec("echo hi")returning{ stdout, exitCode }as if RPC works. Either containers should wire shape methods (an HTTP/RPC bridge overgetTcpPort, mirroring the Worker/DO path), or the docs should show onlygetTcpPort(port).fetch(...).I'd love to work on a fix or an implementation if I get an ok and a steer on the intended plan.