# Overview

> What Cloudflare OS exposes: a router origin, a Workers kernel, gadget sandboxes, and gatekeeper connectors. Covers the package layout, the OS-analogy mapping to real directories, runtime assumptions (Workers, Durable Objects, Worker Loader, Cap'n Web), and the first routes to read.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `README.md`
- `AGENTS.md`
- `package.json`
- `pnpm-workspace.yaml`
- `packages/router/src/index.ts`
- `packages/workshop-shared/src/api.ts`

---

---
title: "Overview"
description: "What Cloudflare OS exposes: a router origin, a Workers kernel, gadget sandboxes, and gatekeeper connectors. Covers the package layout, the OS-analogy mapping to real directories, runtime assumptions (Workers, Durable Objects, Worker Loader, Cap'n Web), and the first routes to read."
---

Cloudflare OS is a pnpm monorepo (`packages/*`, root package name `gadgets`) that deploys as a set of Cloudflare Workers behind one public origin. `packages/router` is that origin: it path-routes to `WORKSHOP_BACKEND`, to any bound `GATEKEEPER_*` service, or to `ASSETS`. `packages/workshop-backend` is the kernel worker, `packages/workshop-frontend` is a client-side SPA, `packages/workshop-shared` defines the Cap'n Web RPC contract spoken between them over a single long-lived WebSocket, and each `packages/gatekeeper-*` is a separate Worker mediating one external service.

## Runtime assumptions

| Assumption | Where it shows up |
| --- | --- |
| Cloudflare Workers | `workshop-backend` and every `gatekeeper-*` package runs as its own Worker; the router is a Worker too |
| Service bindings | Gatekeeper installation is a `GATEKEEPER_*` service binding on the router — no router code change |
| Durable Objects | Gatekeepers own their state in DOs (e.g. `gatekeeper-context` uses `ContextCollectionDurableObject`, `UserLibraryDurableObject`, `LibraryRegistryDurableObject`, plus a KV namespace) |
| Cap'n Web RPC | Client↔backend API in `packages/workshop-shared/src/api.ts`; gatekeepers expose "a clean Cap'n Web API to the service" |
| Sandboxed gadget frames | Gadgets run in an iframe with no outside-world access except `postMessage()` to the parent, through which they speak RPC to the Workshop |
| workerd / wrangler locally | `pnpm run-local` runs the whole stack on wrangler and workerd; `pnpm-workspace.yaml` overrides `workerd` to `>=1.20260623.1` |
| Pinned Node types | `@types/node` pinned to `26.1.0` via workspace overrides |

<Note>
`pnpm-workspace.yaml` sets `minimumReleaseAge: 1440` (24h) to match the CI supply-chain policy, with `capnweb`, `capnweb-validate`, `workerd`, and `@cloudflare/workerd-*` excluded. Local installs cannot commit a too-fresh lockfile.
</Note>

## What the product exposes

Three surfaces, per the README:

1. An agent chat UI for asking agents to do tasks, preloaded with knowledge about how your company operates.
2. Sandboxed application development — agents build "gadgets" (small personal apps) that can be safely shared.
3. Gatekeepers, a security framework applying guardrails to both agents and apps.

A gadget is a *private instance* of an application per user, running in its own sandbox, rather than a call out to shared SaaS. Two consequences the README calls out: the app cannot leak your data through its own bug, because the sandbox controls all access to the private instance; and the code is freely modifiable by prompting an agent, because of the first point. Blueprints are the "template" analogue — unlike an office template, a blueprint specifies a whole application, and users can create blueprints from their own gadgets and share them.

## Package layout

:::files
```
packages/
├── router/                    # public origin; path-routes by binding set
│   └── src/index.ts
├── workshop-backend/          # the kernel Worker
│   └── format-blueprints/     # shipped output-format blueprints, committed as data
├── workshop-frontend/         # SPA: React, Kumo UI, Phosphor icons, Vite
├── workshop-shared/           # Cap'n Web RPC API definitions
│   └── src/api.ts
├── configurator-ui/           # type-only helpers for gatekeeper configurator UI modules
├── mcp-shared/                # library behind gatekeeper-mcp and gatekeeper-mcp-portal
├── gatekeeper-context/        # Context Library connector
├── gatekeeper-scheduler/      # Scheduled Tasks connector
└── gatekeeper-*/              # one Worker per external service integration
```
:::

### Package responsibilities

| Package | Role |
| --- | --- |
| `packages/router` | The public origin. Routes by path prefix; also carries the inbound `email()` handler. Doubles as the dev router. |
| `packages/workshop-backend` | The kernel: defines the architecture, held to a higher review bar than UI or gatekeeper code. Also owns `format-blueprints/`. |
| `packages/workshop-frontend` | Pure single-page app, entirely client-side, speaking RPC over a persistent WebSocket. React + Kumo UI + Phosphor + Vite. |
| `packages/workshop-shared` | The RPC interface between client and server. Cap'n Web, browser-capable over WebSocket. |
| `packages/configurator-ui` | Type-only component helpers for optional gatekeeper resource configurator UI modules, compiled by `scripts/build-gatekeeper-configurator.mjs` during package builds. |
| `packages/mcp-shared` | Not a Worker — a library holding the MCP client, OAuth chain, account DO base, resource-URL scope grammar, and queued-action store. |
| `packages/gatekeeper-*` | One Worker per integration: OAuth flows plus sandboxed access to external APIs. |

## Router: the first route to read

`packages/router/src/index.ts` is the shortest complete statement of the system's shape. Routing config *is* the binding set.

```ts
// packages/router/src/index.ts
for (const key of Object.keys(env)) {
  if (!key.startsWith("GATEKEEPER_")) continue;
  const suffix = key.slice("GATEKEEPER_".length).toLowerCase().replaceAll("_", "-");
  const prefix = `/gatekeeper/${suffix}`;
  if (url.pathname === prefix || url.pathname.startsWith(prefix + "/")) {
    return (env[key] as Fetcher).fetch(req);
  }
}
```

Resolution order, top to bottom:

| Path | Destination |
| --- | --- |
| `/gatekeeper/<suffix>` and `/gatekeeper/<suffix>/*` | The matching `GATEKEEPER_*` service binding (`_` → `-`, lowercased). Gatekeeper OAuth redirects land here, at `/gatekeeper/<name>/oauth` — there are no backend `/auth` callbacks. |
| `/api`, `/api/*` | `WORKSHOP_BACKEND` |
| `/blueprint-screenshot`, `/blueprint-screenshot/*` | `WORKSHOP_BACKEND` |
| everything else, when `ASSETS` is bound | `ASSETS` (production; the `wrangler.jsonc` assets stanza) |
| everything else, when `ASSETS` is absent | `WORKSHOP_BACKEND` (dev fallback) |

Installing a gatekeeper therefore means re-deploying the router with one more service binding — no code or config change in the router.

<Info>
The `Env` interface declares `ASSETS?: Fetcher` as present in production and absent in dev. `GATEKEEPER_EMAIL?: Service<EmailEntrypoint>` is described in-source as dormant until custom domains plus Email Routing exist; the `email()` handler ships anyway and rejects the message with `"No email gatekeeper is installed on this instance."` when the binding is missing.
</Info>

## Request topology

```mermaid
flowchart TB
  subgraph client["Browser"]
    SPA["workshop-frontend SPA"]
    GADGET["gadget iframe<br/>(postMessage only)"]
  end
  subgraph origin["Public origin"]
    ROUTER["packages/router<br/>src/index.ts"]
  end
  subgraph workers["Workers"]
    BACKEND["workshop-backend<br/>(kernel)"]
    GK["gatekeeper-* Workers<br/>context, scheduler, mcp, ..."]
    ASSETS["ASSETS<br/>(prod only)"]
  end
  subgraph state["Gatekeeper-owned state"]
    DO["Durable Objects"]
    KV["KV namespace"]
  end
  subgraph ext["External services"]
    SVC["third-party APIs / OAuth"]
  end

  SPA -->|"WS /api — Cap'n Web"| ROUTER
  SPA -->|"static assets"| ROUTER
  GADGET -->|"postMessage RPC"| SPA
  ROUTER -->|"/api/*, /blueprint-screenshot/*"| BACKEND
  ROUTER -->|"/gatekeeper/&lt;name&gt;/*"| GK
  ROUTER -->|"fallback"| ASSETS
  BACKEND -->|"service binding"| GK
  GK --> DO
  GK --> KV
  GK -->|"sdkFetch — endpoint + SSRF checks"| SVC
```

## Why a fat client, not SSR

`packages/workshop-shared/src/api.ts` records the reasoning inline: the UI is likely open often or always, so startup time matters less and assets are usually cached; gadgets are sandboxed *client-side* as well as server-side, which requires running code in the browser, so a gadget cannot plausibly be server-rendered; a clean client/server API boundary makes alternative clients easier; and SPA is simpler to reason about. The RPC socket is opened immediately at startup and kept open for the whole session lifetime, reconnecting as needed. Through the `postMessage()` exchange, the Workshop hands the gadget a stub pointing at the gadget's own server-side Durable Object interface.

## The RPC surface entry point

`PublicApi` in `packages/workshop-shared/src/api.ts` is the internet-facing half and shows the shape of everything else:

| Member | Purpose |
| --- | --- |
| `getServerConfig()` | Deployment-level boot config (auth mode, available sign-in vendors, whether the Cloudflare limits flow is enabled). Contains no secrets. |
| `startGatekeeperLogin(vendorId)` | Returns `{ url, attempt }`; the client opens `url` in a new tab and awaits `attempt.wait()`. Vendor must be auth-capable and allowlisted per `ServerConfig.authVendors`, else throws. |
| `authenticate(token)` | Authenticates with a stored token, returning `AuthenticatedApi`. |
| `authenticateFromCfAccess()` | Authenticates from an existing Cloudflare Access session. |
| `login(username, passwordHash)` | Returns a token, or `null` on no-such-user / wrong password. |
| `createAccount(username, displayName, passwordHash)` | Returns a token, or `null` if the username exists. |
| `getBlueprint(id)` | Blueprint metadata; no auth required — knowing the ID suffices, since a blueprint is "just data". |
| `downloadBlueprint(id)` | Streams a `.gadget` archive containing `BlueprintMetadata` plus the current code snapshot, not the full KV record. |

`LoginAttempt` is a capability: holding the stub is the right to receive the resulting session token, and disposing it abandons the attempt and cancels the server-side wait.

<Warning>
`login()` / `createAccount()` take a client-derived `passwordHash`, not a password: `argon2id` with `salt = SERVICE_SALT + utf8(username)`, `parallelism: 1`, `iterations: 3`, `memorySize: 64MiB`, `hashLength: 32`. `SERVICE_SALT` is the 16-byte constant exported from `api.ts`. The server hashes again before storage and never sees the plaintext password. Both methods may be disabled when the deployment uses SSO.
</Warning>

## Gatekeepers: capability-based mediation

A gatekeeper is created when an agent or gadget is introduced to an external resource. Per the README, it wraps the service's native API in a clean Cap'n Web API, handles authorization (e.g. OAuth), enforces narrow access to only the specific resource the user intended, logs every action the gadget or agent performs, and offers human approval for any side-effecting action.

The asynchronous approval model is the notable departure from synchronous human-in-the-loop: when an action needs approval, the gatekeeper *simulates* the outcome locally and tells the agent it completed, serving simulated results on read-back so the agent can keep queueing work. The user approves or rejects later, in bulk or one at a time.

### Ambient accounts and provisioning modes

A vendor may declare `VendorDescription.autoProvisionsAccount`, minting a connected account with no OAuth flow through `GatekeeperVendor.createAccount()` — which takes no user identity. The deployment admin then picks a per-vendor mode in the admin Gatekeepers panel, resolved in `provisioning-policy.ts`:

| Mode | Behavior |
| --- | --- |
| `enabled` | Auto-provisions the account for every user; forced, and hidden from the Connectors list |
| `optional` (default) | Each user opts in from the Connectors page |
| `disabled` | Offered to no one; existing accounts go dormant |

The account is persisted in the user DO like any connected account, and the account capability — not an asserted identity — is the authority thereafter.

An account (`GatekeeperUser`) declares in its `AccountDescription` whether it provides an agent **singleton** (`singleton: { tsType }`) and/or a **management UI** (`providesUi`). These are orthogonal: an account can declare either, both, or neither. Singletons are auto-provided to the owner's workspaces as an ambient gatekeeper record, folded into each chat's env as a named chat binding using the gatekeeper's `suggestedBindingName` (see `prepareChatBindings` in `overseer.ts`), which the agent reads in `executeCode` via `getSession` / `getAgentCatalog`, with each read recorded as an observation. It is not bound to any gadget by default; the agent can wire it in with `setGadgetBinding` when a gadget's persistent code needs it. Management UIs are hosted at `/gatekeepers/$appId` — the vendor id, e.g. `/gatekeepers/context` — via `startAppUi({ isAdmin })`.

<Warning>
Capability-based security rule from `AGENTS.md`: a resource becomes "ambient" (auto-injected) **only** by user or admin configuration. A gatekeeper must never assert its own ambience.
</Warning>

### MCP trust boundary

`packages/mcp-shared` backs two connectors — `gatekeeper-mcp` (endpoints a user pastes) and `gatekeeper-mcp-portal` (one admin-configured portal). The trust boundary is `tools.ts`, and nothing outside it reads a tool's annotations: a tool the server declares `readOnlyHint: true` runs as an observation; everything else is queued for approval. Auto-*applying* a write additionally requires a `vetted` endpoint, which only the portal can produce, via `MCP_PORTAL_TRUST_ANNOTATIONS`. OAuth uses the official `@modelcontextprotocol/client`, and SDK OAuth operations must always be given `sdkFetch(...)` so every request and redirect retains endpoint and SSRF checks.

### Reference connectors

<AccordionGroup>
<Accordion title="gatekeeper-context — Context Library">
An account providing a singleton read session plus a management UI, for authoring collections of context documents that agents read as observations. Collections are **private** (owned by one account, readable/writable only by it) or **public** (created and edited only by deployment admins, readable by everyone and auto-enabled for all users). State lives in `ContextCollectionDurableObject` (content), `UserLibraryDurableObject` (each account's private collections), and `LibraryRegistryDurableObject` (the domain's public set), plus a KV namespace. All data is namespaced by a `sharingDomain` taken from the binding's props (see `domain.ts`), so multiple workshops sharing one gatekeeper instance stay isolated.

Bound as `GATEKEEPER_CONTEXT`. Declares `autoProvisionsAccount` and mints a `ContextAccount` via `createAccount()` — no user identity is passed in; the account keys private data by its own generated `accountId`. Exposes `getSession()`, `getAgentCatalog()`, and `startAppUi({ isAdmin })`. Its UI is a single-file React SPA in `app/` (Vite + Tailwind + Kumo) bundled by `build-app.mjs` into `src/generated/app.txt`.
</Accordion>
<Accordion title="gatekeeper-scheduler — Scheduled Tasks">
An auto-provisioned gatekeeper whose account provides an ambient singleton for registering persistent workspace callbacks, plus a read-only management UI.
</Accordion>
</AccordionGroup>

## The OS analogy, mapped to directories

The README uses "operating system" in two senses: an OS for *the company* to be productive with AI safely enough that the security team can sleep at night, and an OS for AI workloads, analogous to how a traditional OS manages compute workloads. The mapping to real code:

```text
  OS concept            Cloudflare OS
  ─────────────────     ────────────────────────────────────────────
  kernel            →   packages/workshop-backend
                        (reviewers read every line; small, elegant diffs)
  syscall ABI       →   packages/workshop-shared/src/api.ts
                        (Cap'n Web RPC; every exported member doc-commented)
  process           →   a gadget: a per-user private app instance
  process sandbox   →   iframe with postMessage() as its only channel out
                        + server-side Durable Object per gadget
  device drivers    →   packages/gatekeeper-*  (one Worker per service)
  capabilities      →   gatekeeper accounts and ambient records
  shell / desktop   →   packages/workshop-frontend
  init / dispatcher →   packages/router
```

<Info>
Per `AGENTS.md`, `workshop-backend` is explicitly "the kernel: it defines the architecture and is held to a higher bar than UI/gatekeeper code." Concrete kernel rules: doc-comment **every** exported member of the `workshop-shared` public API (types, consts, and functions — not just interfaces); never introduce a hand-written interface that mirrors an RPC interface plus an `as unknown as` cast; prefer reusing existing mechanisms over adding parallel ones; and split large changes by concern into separate PRs, or at minimum group commits so `workshop-backend` / `workshop-shared` can be reviewed apart from UI.
</Info>

## Root commands

| Command | What it does |
| --- | --- |
| `pnpm run-local` | `node scripts/run-local.mjs` — runs the whole stack locally on wrangler and workerd. Not for production. |
| `pnpm build` | `pnpm run --recursive build` |
| `pnpm test` | `node --test scripts/*.test.js && pnpm run --recursive --if-present test` |
| `pnpm dev-server` | `node run-dev-server.js` |
| `pnpm dev-client` | Vite dev server in `packages/workshop-frontend` |
| `pnpm lint` | `lint:check` (oxlint) then `types:check` (recursive `tsc --noEmit`) |
| `pnpm lint:check` / `pnpm lint:fix` | `oxlint` / `oxlint --fix` |
| `pnpm types:check` | `pnpm run --recursive --if-present types:check` |
| `pnpm clean` | `pnpm run --recursive clean` |
| `pnpm import:format-blueprint` | Replace a shipped format blueprint: `<export.gadget> <blueprintId>`; add one with `<export.gadget> --new <name>` |

<Steps>
<Step title="Install pnpm">
Cloudflare OS is pnpm-only. Install pnpm from https://pnpm.io/.
</Step>
<Step title="Run the stack">
```bash
pnpm run-local
```
</Step>
<Step title="Open the origin">
Visit http://localhost:8787. In `run-local` mode the backend has a static `assets` binding (with `run_worker_first` for the API routes) and serves the pre-built SPA, which is why the router's dev fallback to `WORKSHOP_BACKEND` works.
</Step>
<Step title="Exercise it">
Try "Make slides for my upcoming meeting with a customer." (uses the built-in slides blueprint), "Make a collaborative whiteboard app." (creates a new app from scratch), or "Make a tic tac toe game." followed by "I'll be X and you be O. I've made my first move. Your turn."

Prompts like "Make an issue dashboard for this GitHub repo." or "Fix the typos in this Google Doc." need an attached resource and a configured GitHub or Google integration.
</Step>
</Steps>

<Warning>
`pnpm dev-client` runs Vite on port 3000 and you should open localhost:3000 directly. The router deliberately does not forward frontend requests to Vite: HMR's socket gets disconnected every time wrangler restarts workerd.
</Warning>

## Format blueprints

`packages/workshop-backend/format-blueprints/` holds the **output format** blueprints the deployment ships with, committed as data: a `<name>.gadget` archive plus a `<name>.json` sidecar giving its `blueprintId`, prose, and `output` presentation. `scripts/build-format-blueprints.mjs` globs that directory into the gitignored `src/generated/format-blueprints.ts`, so `build`, `types:check`, and `test` all run the generator first. `FORMAT_BLUEPRINTS_DIR` overrides the directory, letting a fork ship its own set without touching the submodule.

<Warning>
Never edit a `blueprintId` after deploy. Install and promotion are keyed on it, and a rename orphans the old entry.
</Warning>

## Project status

This repository is version 2, a complete rewrite of Cloudflare OS on a new foundation. As of the August 2026 release it is described as very capable but with many rough edges — treat it as an early-access release. The stated intent of open-sourcing is not that other companies run "Cloudflare OS", but that they fork it into "*Your Company* OS". `pnpm run-local` is explicitly not meant for production use; the alternative path is deploying to your own Cloudflare account.

## Next

<CardGroup cols={2}>
<Card title="Installation" href="/installation">
Prerequisites and the three install paths: `pnpm run-local`, the hosted deploy wizard, and the starter repository.
</Card>
<Card title="Quickstart" href="/quickstart">
Bring the stack up on workerd, reach http://localhost:8787, and run the first prompts.
</Card>
<Card title="Routing and worker bindings" href="/routing-and-bindings">
The full router resolution order, backend bindings, and Durable Object migration tags.
</Card>
<Card title="Gadgets and sandboxing" href="/gadgets-and-sandboxing">
Dynamic Worker isolation through `LOADER`, the CSP-restricted iframe, and the Cap'n Web bridge over postMessage.
</Card>
<Card title="Gatekeeper protocol" href="/gatekeeper-protocol">
`GatekeeperVendor`, `GatekeeperUser`, `AccountDescription`, and `autoProvisionsAccount` mode resolution.
</Card>
<Card title="RPC API reference" href="/rpc-api-reference">
`PublicApi`, `AuthenticatedApi`, `AdminApi`, `Overseer`, and the stub-disposal constraints.
</Card>
<Card title="Local development" href="/local-development">
The two-terminal `pnpm dev-server` / `pnpm dev-client` workflow and its flags.
</Card>
<Card title="Developer conventions and contributing" href="/conventions-and-contributing">
Kernel review standards, doc-comment rules, and the RPC mirror-interface prohibition.
</Card>
</CardGroup>
