Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/cloudflare-os/llms.txt

Use this file to discover all available pages before exploring further.

Security in Cloudflare OS is built into every layer of the architecture — not bolted on afterward. From the sandbox that isolates each Gadget’s code, to the capability model that requires explicit introductions before any external resource can be accessed, to the observer tracking system that prevents data leaks when you share a Gadget with a teammate, the system is designed so that non-technical users can “go nuts” and nothing bad will happen.

Gadget sandboxing

Every Gadget runs in a two-layer sandbox: one for the server and one for the client.

Server sandbox

The server-side code runs in a Dynamic Worker Facet — a Cloudflare Worker spun up on demand and scoped to this Gadget. Internet access is disabled at the runtime level: the Gadget’s server cannot make arbitrary outbound HTTP requests. It can only communicate with the external world through explicitly configured Workers Bindings — service bindings to Gatekeeper Workers that the user has consciously set up.
The underlying runtime, workerd, is open source on GitHub and fully auditable. The sandbox is not a policy layer on top of a general-purpose runtime — it is enforced by the runtime itself.

Client sandbox

The client-side code runs in a sandboxed <iframe> with both Content-Security-Policy restrictions and the sandbox attribute applied. The iframe cannot reach the network directly. Its only communication channel with the outside world is a Cap’n Web RPC session provided to it over postMessage() from the parent Workshop frame. This design means a security bug in a user’s custom Gadget UI code cannot leak data to an attacker on the internet — the browser itself enforces the isolation.

Client–server communication

The only path between the Gadget’s client and server is Cap’n Web RPC over postMessage. This is not just a security constraint — it also means every Gadget automatically exposes a structured, typed API that AI agents can call directly using Code Mode, with no extra integration work required.

Capability-based access control

By default, every agent and every Gadget has access to nothing. Even if your deployment has been configured with Gatekeeper integrations for Google, GitHub, Linear, and others, agents and Gadgets do not get access to any of them automatically.

Explicit introductions

Access is granted by introducing an agent or Gadget to a specific resource — pasting a URL, selecting a document from a picker, or approving an agent’s request for a connection. Each introduction creates a scoped Gatekeeper binding covering exactly that resource, not the user’s entire account on the service. This is a deliberate departure from how most agent harnesses work, where MCP servers are configured once and their full capabilities are ambiently available in every chat session. In Cloudflare OS, each agent is restricted to only the access it actually needs for the job at hand.

No ambient authority by default

A Gatekeeper must never assert its own ambience. Resources become ambient (auto-injected into every agent context) only through explicit admin configuration — the three-state auto-provisioning mode (disabled / optional / enabled) in the Admin Panel. See the Admin Panel guide for details.

disabled

The Gatekeeper is offered to no one, including existing account holders.

optional

Users can opt in from the Connectors page. Nothing is auto-provisioned.

enabled

An account is automatically provisioned for every user and silently folded into their agent context.

Human-in-the-loop approvals

All write actions performed by an agent or Gadget are routed through the Gatekeeper approval queue before they take effect. This is not a synchronous gate that blocks the agent — it is designed so that approvals can happen asynchronously, after the agent has finished its work.

How asynchronous approval works

1

Agent submits an action

When an agent (or Gadget code) calls a write method on a Gatekeeper, the Gatekeeper submits an action description to the approval queue via submitAction() and returns immediately.
2

Gatekeeper simulates the result

The Gatekeeper updates its local cache or simulation state to reflect what the world would look like if the action had been applied. Subsequent reads by the agent return the simulated state.
3

Agent continues unblocked

The agent proceeds with follow-on work, potentially queuing up more dependent actions, all against the simulated state.
4

User reviews in bulk

When the agent finishes, the user can review the queued actions in the Workshop UI and approve or reject them in bulk — at a time that is convenient for them.
5

Actions are applied or rolled back

Approved actions are forwarded to the external service via applyAction(). Rejected actions trigger rejectAction(), which clears the simulation state (and may trigger a Gadget restart if rolling back is not otherwise possible).
This pattern prevents the common failure mode of users disabling approvals due to friction — because the agent never has to stop and wait, there is no friction to disable.

Observer tracking

When you share a Gadget that uses Gatekeeper bindings, the sharing system must ensure that your collaborators cannot use the Gadget as a back channel to access data they couldn’t access themselves. This is enforced through observer tracking.

The problem observer tracking solves

Without observer tracking, sharing a Gadget that has read from your private Google Doc would effectively grant a collaborator indirect read access to that document — even if they don’t have permission on Google’s side. The earlier approach was a blunt prohibitAllSharing flag: any Gatekeeper that handled sensitive data could mark itself as non-shareable, which locked down the entire Gadget for everyone. Observer tracking replaces that with a per-user, per-Gatekeeper check.

How observer verification works

1

Collaborator opens the Gadget

When Bob opens a Gadget shared by Alice, the Overseer determines that Bob is a non-owner collaborator and checks whether he has been verified as an observer for each of the Gadget’s Gatekeeper bindings.
2

Bob provides his connected accounts

For each Gatekeeper binding in scope (all bindings for build collaborators; named bindings only for use collaborators), Bob must have his own connected account for that vendor. If he doesn’t, he is prompted to connect or select one.
3

Bob's account mints a verifier

Bob’s connected account calls GatekeeperUser.getVerifier(), which returns an opaque GatekeeperUserVerifier service stub. This verifier represents Bob’s vendor-level identity without revealing it to the Overseer.
4

The Gatekeeper checks access

The Overseer passes the verifier to Gatekeeper.addObserver(observerId, verifier). The Gatekeeper — the authority on its own resource’s ACL — unwraps the verifier (via a non-standard method it defined on its own verifier class) and checks whether Bob’s account can access everything the Gadget has read so far. If not, addObserver() throws and Bob is denied.
5

Bob is registered as an observer

If all checks pass, Bob is stored as an observer with a random, opaque observer ID (not his email or profile ID, to prevent gatekeeper authors from parsing identity out of it). Re-verification runs on every subsequent open.

Observer IDs and forward exclusion

The observer ID is opaque by design: it is a randomly generated string with no connection to a user’s email or profile ID. Gatekeepers that want to block a specific future observation from reaching a specific observer can do so by listing that observer’s ID in ObservationDescription.excludeObservers. The Overseer will block the observation unless every named observer has already lost access in the sharing graph.

Comparison with prohibitAllSharing

MechanismBehavior
prohibitAllSharing: trueBlunt all-or-nothing: the Gadget cannot be shared with anyone once this flag is set.
Observer trackingPer-user, per-Gatekeeper: collaborators who can independently access the underlying data are admitted; others are blocked at verification time.
Observer tracking is a work in progress. The getVerifier() / addObserver() / removeObserver() interfaces are committed and enforced by the Overseer, but per-Gatekeeper implementations are being rolled out progressively. Until a specific Gatekeeper implements its strategy, some Gatekeepers fall back to the prohibitAllSharing behavior. Check the individual Gatekeeper READMEs for current status.

What happens when a collaborator loses access

When a collaborator is removed from the sharing graph, the Overseer also tears down their observer record and calls removeObserver(observerId) on all relevant Gatekeeper facets. This is idempotent — Gatekeepers are required to handle unknown IDs gracefully. The Overseer also aborts the active session, forcing a reconnect that re-evaluates the now-changed permission graph. See the Sharing and Collaboration guide for details on live session termination.

Admin security boundaries

The Admin Panel allows deployment customization through the AdminConfig object — site branding, agent instructions, Gatekeeper provisioning modes, and so on. However, certain settings are deliberately kept out of the Admin Panel and remain env-var driven.
Authentication and authorization configuration is not editable through the Admin Panel. Sign-in providers (AUTH_GATEKEEPERS), password login (DISABLE_PASSWORD_AUTH), and the admin user list (ADMINS) all remain environment variable–driven. This ensures that a compromised admin session cannot be used to lock out legitimate admins or add unauthorized sign-in methods. See the Admin Panel guide for the soft-config settings that are available through the panel.
The AdminSettings Durable Object owns the authoritative AdminConfig and mirrors it to a single KV key for fast hot-path reads. The AdminApi capability is only minted for users listed in the ADMINS env var — non-admins receive null from getAdminApi() and cannot call any admin methods.

Build docs developers (and LLMs) love