The Boundaries That Make Generated Code Usable

Cloudflare OS gives a Gadget enough runtime to behave like a real application while denying it ordinary, ambient network access. This chapter focuses on the boundaries that make that compromise work: the workspace coordinator, the stateful Gadget runtime, the browser iframe, the RPC bridge, and the Gatekeeper session behind a named binding. The practical result is a way to trace a call and identify whether a control comes from isolation, from authorization, or from the application’s own code.

The operating-system analogy has a useful center

Cloudflare OS compares its parts with parts of a conventional operating system. The Workshop backend is analogous to a kernel. Gadgets resemble processes. Blueprints resemble executables. Users remain users, and shared permissions resemble access-control rules. Gatekeepers are compared with device drivers because they connect programs and users to external services in the way drivers connect software to devices.

The analogy is useful when it points to ownership and mediation. A process does not normally get to redefine the kernel’s memory boundaries, and a Gadget should not get to redefine the Workshop’s rules for its bindings. A driver presents a controlled interface to a device, and a Gatekeeper presents a controlled interface to a remote service. The analogy is not a claim that a Gadget is a process in every operating-system sense. It is a way to ask who owns a resource, which interface exposes it, and which layer can refuse a request.

The analogy also leaves something important out. Traditional operating systems do not usually treat AI agents as a separate kind of principal. Cloudflare OS does. Its agents write code and execute it on demand, so treating an agent as if it were simply another human user would hide the difference between a person’s intent and generated instructions. The system instead gives an agent an environment of named capabilities and routes those capabilities through the workspace.

That layered ownership begins with the workspace itself. The repository describes each workspace as a Durable Object, a stateful server-side object whose storage and coordination can persist beyond one request. The workspace’s Overseer keeps track of Gadgets, bindings, chats, sharing, actions, and the Gatekeeper facets installed beneath it. A Gadget is then loaded as its own Dynamic Worker Facet, allowing its code and application state to be isolated within the workspace’s larger coordination boundary.

What each runtime layer owns

The first layer is the Overseer. It is not the Gadget’s business logic. It is the authority that constructs the environment in which the Gadget runs, resolves a binding, creates or retrieves the relevant facet, and applies workspace-level rules. When code or a client asks to reach a workpiece, the request does not simply turn into a raw service lookup. The workspace resolves the target and supplies a capability appropriate to the caller.

The second layer is the Gadget Facet. The Gadget’s server code is loaded into a dynamic Worker definition, and the exported server class is treated as a Durable Object class. That gives the application a stateful home: it can store application data and respond to calls over time rather than rebuilding its entire world on every request. The facet is also where the Gadget’s code runs with the environment assembled for it. The code can see its own server capability and its named bindings, not an unfiltered view of the whole workspace.

The third layer is the browser UI. A Gadget’s client code is not served as an ordinary page with unrestricted access to the host application. The Workshop places it in an iframe created from a generated document. The document carries a restrictive content-security policy, and the iframe is sandboxed. The client code is given a special gadget object that represents an RPC connection to its server-side Gadget. It does not receive the server object as a local JavaScript object; calls cross a deliberate communication boundary.

The fourth layer is a Gatekeeper Facet. A Gatekeeper represents a particular external resource connection inside the workspace. The shared interface describes it as a Durable Object Facet below the Overseer, and says that the Gadget receives a session capability rather than direct access to the user’s broad service account. The Gatekeeper knows how to interpret its service’s resource identifiers and permission model. The Gadget knows only the interface the Gatekeeper exposes.

This division prevents a component list from becoming the architecture. The important relationships are directional. The Overseer creates and governs facets. The browser calls the Gadget through a message-port RPC session. The Gadget can resolve a named binding. The named binding routes through the Overseer to a Gatekeeper session. At every step, the layer handing out the next capability can reject the request or give a narrower object than the caller expected.

Consider a Gadget that displays a list stored in its server-side state. The browser does not read the Durable Object’s storage directly. The client invokes a method over the MessagePort session; the server method reads its own state and returns a result. If a second browser changes that state through another method, the Gadget can notify both clients through retained RPC callbacks. The browser remains a renderer and caller, while the Durable Object remains the owner of the application state.

Now change the example so that the Gadget needs a document from an external service. The client still calls the Gadget, but the Gadget’s server code must use a named binding. That binding is resolved through the workspace rather than by importing a general-purpose HTTP client with credentials. The Gatekeeper behind the binding knows which resource it represents and receives the session’s authorization context. If the binding is absent, the application cannot manufacture it by reading another account record. If the Gatekeeper rejects the session, the Gadget sees a failed resource operation rather than a secretly widened environment.

This path creates a useful separation between application state and resource state. The Gadget can store a summary or a user’s workflow state in its own Durable Object without becoming the owner of the remote document. The Gatekeeper can enforce the external service’s rules without giving the remote service a direct channel into the browser. The Overseer can associate the request with the caller and the workspace before the Gatekeeper returns data. The system remains a chain of capabilities instead of one process holding every object at once.

The chain also makes transitions visible. Loading a new committed Gadget version can create or restart the corresponding facet. Reloading the iframe can dispose the browser-side RPC session and establish a replacement. Adding a binding changes what the Gadget can ask the Overseer to resolve. Removing or invalidating a connection changes which calls can succeed. These are not merely deployment events; they change the set of live capabilities that code can exercise.

The runtime does not eliminate ordinary application failures. A Gadget can expose a method that throws, store inconsistent data, or misuse a resource that it was legitimately given. A Gatekeeper can reject a remote operation because the connected account lacks permission or because credentials have expired. A browser can lose its message port while the server remains alive. The design goal is narrower: each failure should occur at an identifiable boundary, rather than being hidden inside an all-powerful application process.

classDiagram class Overseer { workspace policy sharing and action state } class GadgetFacet { application state server code } class BrowserIframe { client UI } class MessagePortRPC { bidirectional calls } class NamedBinding { explicit capability edge } class GatekeeperFacet { external resource session } Overseer --> GadgetFacet : loads and governs Overseer --> GatekeeperFacet : creates and mediates BrowserIframe --> MessagePortRPC : uses MessagePortRPC --> GadgetFacet : calls GadgetFacet --> NamedBinding : resolves NamedBinding --> GatekeeperFacet : routes

The stateful design also explains why a Gadget is not just a generated page. A page can render a result and disappear. A Gadget can keep application state in its server-side Durable Object, expose methods to multiple clients, and broadcast changes through callbacks and subscriptions. The repository’s client guidance treats bidirectional RPC as a way to implement realtime updates: a browser can subscribe, and the server can call the retained callback when state changes. That is the runtime basis for multiplayer behavior, not an incidental UI feature.

Two sandboxes, two failure surfaces

The server and browser halves of a Gadget are isolated in different ways. The server-side dynamic Worker is created with global outbound access disabled. Its code cannot simply call fetch() to any service it chooses. The environment assembled by the Overseer contains the Gadget’s own capability and the binding loopbacks made available to that execution. If the application needs a remote resource, it must use one of those bindings.

This control is strong because it removes the easiest path from generated code to the public internet. It is also limited. Disabling outbound access does not tell the code whether a given Gatekeeper session should be allowed to read a document or submit an action. It only ensures that the code cannot bypass the service mediation by making a direct network request. The authorization decision still belongs to the Gatekeeper and the workspace policies around it.

The browser half has a different threat surface. The UI is untrusted client code running in a document with a null origin. The generated HTML sets a content-security policy whose default is to deny resources, whose connection policy denies ordinary network connections, and whose script policy allows only the embedded data-based code needed to start the UI. The iframe is sandboxed with scripts enabled, but without the normal authority of a same-origin application page.

The client still needs to do useful work, so the Workshop creates a MessageChannel. The injected client code sends one port to the parent during a handshake and constructs a Cap’n Web RPC session over the other. The parent checks that a message came from the expected iframe and from the null origin before opening the server-side connection. The client can then call methods on its gadget stub, while console messages and selected events can travel back to the Workshop.

This is a narrow bridge, not a claim that the browser UI has become trusted. The UI can call whatever methods the returned Gadget capability exposes, and the Gadget’s own code can still contain bugs. The point is to keep the browser from silently becoming a second route to the internet or to the rest of the Workshop. A user-activated external link may be treated differently from programmatic network access, but the application’s normal data path remains the explicit RPC channel.

The boundaries fail in different ways. A server-side attempt to call a remote URL directly should fail because the worker has no global outbound route. A client-side attempt to open a network connection should be blocked by the iframe’s sandbox and policy. A call over RPC may still fail because the Gadget method rejects it, the workspace cannot resolve the capability, the Gatekeeper refuses the session, or the remote service denies the account. These are not interchangeable failures. Isolation prevents a class of bypasses; authorization decides whether an allowed path may be used.

The browser controls are defense in depth rather than a universal proof. The source describes the client as sandboxed to the maximum extent allowed by browsers, and browser behavior remains part of the security boundary. The code also has to manage iframe reloads and RPC reconnections: a stale client stub can be disposed when the frame or backend connection changes, and calls may wait for a replacement capability. Reliability here is part of security’s practical meaning. A boundary that vanishes during a reconnect would not be a useful boundary, so the host treats the connection lifecycle explicitly.

Why RPC and bindings are authority, not configuration

It is tempting to think of the Gadget’s environment as a configuration object: a set of names that happen to point to services. That description is too weak. A named binding is a capability because possessing the live object behind the name is what makes an operation possible.

The distinction matters in two places. First, the environment given to a Gadget is assembled by the Overseer from the Gadget’s recorded bindings. The code does not receive every connected account owned by the user. Second, the agent’s execution environment is built from the current chat’s named bindings. The agent can use the capabilities already present in that environment, but a request for a new connection is a separate, user-mediated event rather than an invisible expansion of the map.

The same routing mechanism protects the boundary between a Gadget and a Gatekeeper. The Gadget sees a loopback that can resolve a session when it is used. The Overseer can associate that call with the Gadget, the agent, or another caller, and can apply workspace rules before the Gatekeeper session is opened. The Gatekeeper then receives its own approval queue and observation authorizer. This chain gives later policy layers a place to act; a raw credential passed directly into generated code would remove most of those control points.

The design also gives capability lifetime meaning. Some agent-side worktree bindings are valid only during the executeCode call that created them. A retained stub cannot be used later to revive an old worktree state or attach itself to a new execution. Other capabilities can be intentionally made persistent for hooks, but that persistence is created through a separate restoration mechanism rather than by accident. The system is therefore tracking not only what a caller can reach, but when and under which context the reachability is valid.

That is the precise point where the architecture moves from isolation to authorization. The sandbox says, “this code cannot open any arbitrary network path.” The binding says, “this code has a path to this kind of resource.” The Gatekeeper and Overseer then decide what that path may return, what it may change, and which other users may observe the result. The next checkpoint will connect these boundaries back to the Gadget lifecycle before examining those external-service decisions in detail.

References