Skip to content

Latest commit

 

History

History
253 lines (174 loc) · 23.1 KB

customization.md

File metadata and controls

253 lines (174 loc) · 23.1 KB

Customizing Cloudflare OS

This wrapper exposes controls at three depths. Start in the Admin UI, move to deployment configuration when the trust or infrastructure boundary changes, and write code only for capabilities that neither layer can express.

Admin UI

Use /admin for runtime policy that should not require a deployment:

  • Site name, logo, and accent color
  • Announcements and agent instructions
  • Connector availability and auto-provisioning policy
  • Signup behavior, featured blueprints, and output formats

Authentication and authorization are deliberately absent. Sign-in configuration and administrator identities remain deployment-controlled so a compromised admin session cannot redefine the trust boundary.

Branding

Set the site name, logo, and accent color from the General tab in /admin. Logo uploads accept PNG, JPEG, WebP, and SVG files up to 5 MB. The browser scales the longest edge to 256 pixels without cropping and converts the result to PNG. The server then checks the PNG header and rejects anything over 256 KB or 512 pixels before storing it in the deployment's blueprint-content R2 bucket. Square images work best.

The custom logo appears in the app chrome, sign-in screens, and browser tab on each user's next connection. Use Restore default to remove it.

Deployment configuration

deployment.jsonc is an annotated, non-secret control surface. Its groups map directly to generated Wrangler configuration:

Path Controls Choices
accountId Resource ownership A 32-character Cloudflare account ID
publicBaseUrl The deployment's public origin null to derive it from the router's custom domain; on a workers.dev route, the router's own https://<router-name>.<subdomain>.workers.dev
workers.*.name Stable Worker service identities Unique lowercase names; changing one creates a differently named Worker
workers.router.route The deployment's public address customDomain for production or workersDev: true for evaluation
gatekeepers Optional upstream Gatekeepers, one Worker each Slugs from upstream's catalog, each with a Worker name and optional vars; see Optional Gatekeepers
access Cloudflare Access trust and administrator list Access team issuer, application audience, and verified email list
aiGateway Deployment-managed model catalog Enabled by default over the Workers AI binding; which providers to advertise, and which gateway
context Context sharing boundary, snapshot KV, and optional Artifacts repositories null to scope data to the public origin, or a pinned stable label; automatic or existing KV; Git-backed collections disabled or enabled
customGatekeeper Whether the example integration deploys, and its identity and guidance enabled: false to deploy none; organization-specific display text
errorReporting Private explicit-issue destination Console Reporter enabled state, environment, and release metadata
resources Blueprint/avatar KV and blueprint-content R2 null to provision or explicit IDs/names to reuse
observability Worker telemetry Structured logs, invocation logs, traces, and sampling; see the observability guide

Secrets are never valid values in this file. Install them interactively with Wrangler against the Worker that consumes them.

Workers and routing

The deployment is up to seven Workers of its own, plus one per optional Gatekeeper. Keep their names unique: service bindings use these names, so update and deploy them together.

Worker Role
router Owns the public route and serves the frontend. Proxies /api and /blueprint-screenshot to the Workshop, and /gatekeeper/<name> to the Gatekeeper whose service binding matches.
workshop The Cloudflare OS backend, holding all user data in Durable Objects.
context The Context Gatekeeper.
scheduler The Scheduler Gatekeeper, which gives agents scheduled and recurring work.
websearch The Web Search Gatekeeper, which lets agents search the public web.
customGatekeeper This repository's example integration. Not deployed when customGatekeeper.enabled is false.
errorReporter The private explicit-issue destination.

Context, Scheduler and Web Search are ambient: upstream's release marks all three PREINSTALL, so the hosted flow installs them on every instance and this starter deploys them for the same reason. None takes configuration beyond its name. Web Search runs on its own Workers AI binding and searches through the Workshop's gateway, aiGateway.name, so its searches share that gateway's provider keys, limits and logs: the deploy sets it as the Worker's CF_AI_GATEWAY, as upstream's deploy service does. With aiGateway.enabled: false, or a gateway in another account, which the binding cannot reach, it searches through the account's default gateway instead, which Cloudflare creates on first use.

Web Search does need the account to pay for searches, which the free Workers AI allowance does not cover: chats can work while every search fails. Before enabling it, either load AI Gateway credits on the account or store a Ceramic API key on the gateway it searches through, under the alias default, since upstream pins Ceramic as the provider and AI Gateway uses no other alias for it. With credits, each search spends them, and there is no per-user limit. With a key, Ceramic bills you instead, and a search no longer falls back to credits: a revoked or empty key fails every search. On a public server, set Web Search to disabled in /admin unless that is acceptable: in the default mode, optional, onboarding offers it switched on. See upstream's Web Search guide.

Only the router takes a route; every other Worker is reachable only over service bindings, and the deploy turns off workers.dev and Preview URLs on all of them. That keeps the router the single Access-protected way in.

For production, set a Custom Domain on it:

"workers": { "router": { "name": "acme-os", "route": { "customDomain": "os.example.com" } } }

The hostname must belong to an active Cloudflare zone and cannot conflict with an existing CNAME. Wrangler creates the DNS record and certificate, and publicBaseUrl can stay null — the deploy derives the public origin from the domain. For evaluation, use the account's workers.dev subdomain instead:

"publicBaseUrl": "https://acme-os.<subdomain>.workers.dev",
"workers": { "router": { "name": "acme-os", "route": { "workersDev": true } } }

publicBaseUrl is required there, because nothing in deployment.jsonc knows your account's workers.dev subdomain. If using workers.dev that value must be https://<router-name>.<subdomain>.workers.dev. Two things read the origin — PUBLIC_BASE_URL, which upstream builds absolute links and OAuth redirect URIs from, and the Context sharing boundary under Storage — so a typo here would deploy successfully and then hide existing Context data and break every redirect.

On a custom domain the hostname is yours and has nothing to do with any Worker name, so pnpm cf-os check compares publicBaseUrl against customDomain instead: leave it null and the deploy derives the origin from the domain, or set it to exactly https://<customDomain>.

Optional Gatekeepers

Upstream ships more Gatekeepers than the ones this starter always deploys: GitHub, Google, Slack and others. pnpm cf-os init offers the ones the hosted deploy offers, each package once, from a catalog built from the pinned submodule; one the starter cannot deploy from a name alone (Email, or any with KV, R2, service or Artifacts bindings) is never offered, and selecting it fails pnpm cf-os check with the reason.

Each one is an entry under gatekeepers, keyed by its slug:

"gatekeepers": {
  "github": { "name": "acme-os-gk-github" },
  "mcpportal": {
    "name": "acme-os-gk-mcpportal",
    "vars": { "MCP_PORTAL_URL": "https://portal.example.com/mcp" }
  }
}
  • The slug is the hosted install slug (gatekeeper-mcp-portal is mcpportal). It names the GATEKEEPER_<SLUG> binding and the /gatekeeper/<slug> route.

  • name is its Worker's name, a permanent service identity like those under workers. init suggests <router name>-gk-<slug>, as the hosted flow names it.

  • vars are plain-text vars, merged over the package's own and replacing any set on the Worker another way; the deploy sets BASE_URL. One with a Workers AI binding of its own, such as Google's for reading Drive PDFs, also gets CF_AI_GATEWAY, set to aiGateway.name as for Web Search, unless aiGateway.enabled is false or the gateway is in another account; without it, those calls go through no gateway. The MCP portal needs MCP_PORTAL_URL: init asks for it, and check warns while it is unset.

  • Secrets go to the Worker, never into this file. Register <public origin>/gatekeeper/<slug>/oauth as the redirect URI of an OAuth app with the provider, then run pnpm cf-os secrets <slug>; init offers this when you deploy. By hand, after the first deploy:

    CLOUDFLARE_ACCOUNT_ID=your-account-id pnpm exec wrangler secret put CLIENT_ID --name acme-os-gk-github

    check warns while one is missing.

  • Removing an entry unbinds it. The next deploy drops its two bindings, so the Workshop stops offering it and users' connections to it disappear. Its Worker, and the Durable Objects holding its state, keep running unbound: the CLI never deletes a Worker. To delete it, CLOUDFLARE_ACCOUNT_ID=your-account-id pnpm exec wrangler delete --name <its Worker name>.

Sign-in methods

Cloudflare OS supports three ways to sign users in. This starter deploys Cloudflare Access.

Method How it works In this starter
Cloudflare Access Access verifies identity before the request reaches the Worker, and the Workshop trusts the signed Access JWT. The password login and signup pages are disabled. Deployed by default
Built-in password accounts Cloudflare OS serves its own username and password login plus signup. This is the upstream default. Requires deploy script changes
Auth Gatekeepers Gatekeepers that advertise providesAuth add "Continue with ..." buttons, alongside or instead of password login. Requires deploy script changes

Access mode is the default here because unauthenticated requests never reach application code. packages/cf-os/src/core/deployment.ts implements it by setting CF_ACCESS_ISS and CF_ACCESS_AUD on the Workshop and building the frontend with VITE_CF_ACCESS_MODE=true.

To run another method, drop those two variables and the build flag, then set upstream's AUTH_GATEKEEPERS allowlist for provider sign-in. DISABLE_PASSWORD_AUTH=true makes a deployment provider-only. Upstream ignores it unless at least one auth Gatekeeper is allowlisted, so a deployment cannot lock everyone out. The wrapper's validation assumes Access mode, so review the upstream Workshop backend and frontend documentation before changing it.

The admins list gates /admin in every method.

Cloudflare Access

Create a self-hosted Access application covering the router's hostname. Then configure:

  • issuer: the team origin, such as https://acme.cloudflareaccess.com, with no path.
  • audience: the application's AUD tag.
  • admins: Access-verified email addresses allowed into /admin.

Access policies decide who can sign in. The admins list decides which signed-in identities can change runtime policy. Keep both narrow.

Storage

Wrangler supports automatic provisioning for KV and R2. Leave these values as null for a new deployment:

"context": {
  "sharingDomain": null,
  "kvNamespaceId": null
},
"resources": {
  "blueprintsKvNamespaceId": null,
  "avatarsKvNamespaceId": null,
  "blueprintContentBucket": null
}

Wrangler creates resources with the Worker name as a prefix and reconnects them on future deploys. To adopt existing data, replace the relevant null with a KV namespace ID or R2 bucket name.

context.sharingDomain is not storage but a data-isolation boundary: Context collections are visible only within it. null scopes them to the deployment's public origin, which is what the hosted deploy does. Changing the boundary hides existing collections even with the right KV bound, so pin it to a literal string when a hostname change must not move it:

"context": { "sharingDomain": "https://os.example.com" }

Context Artifacts

The Context Gatekeeper can use Artifacts as Git-compatible storage for Context collections. This is disabled when enabled is omitted or false and requires Artifacts access on the deployment account. Enable it without specifying a namespace to use gatekeeper-context-collections:

"artifacts": { "enabled": true }

To isolate repositories under another stable namespace, add the optional property:

"artifacts": {
  "enabled": true,
  "namespace": "acme-context-collections"
}

Artifacts creates the namespace implicitly when the first repository is created. Keep the selected namespace stable: existing Git-backed collections refer to repositories in it. Disabling the binding later stops repository refresh and token management but does not delete repositories; the last synchronized Context content remains readable. Write tokens grant repository mutation authority, so protect them like other credentials and revoke them when no longer needed.

AI models

Every provider, Workers AI included, is reached through AI Gateway. The transport is the Workshop's WORKERS_AI binding, which is pre-authenticated inside your own account — so the default configuration needs no API token at all:

"aiGateway": {
  "enabled": true,
  "name": "default",
  "accountId": null,
  "providers": ["cloudflare"]
}

Cloudflare can create the default gateway on first use. accountId: null means the gateway lives in the deployment's own account, which is what makes the binding transport usable.

The binding stays bound whatever you configure here: as well as carrying gateway traffic, it is what the agent's webFetch tool runs document-to-Markdown conversion on.

Configuration Result
enabled: true, providers: ["cloudflare"] Workers AI models over the binding. No token, no keys of your own. The default.
Add anthropic or openai Their models appear too. Keys live on the gateway (Unified Billing or BYOK), not in this repository. Still no token.
Add google Needs CF_AI_GATEWAY_API_TOKEN. pi's Google adapter refuses a custom fetch, so Google inference cannot ride the binding.
accountId set to another account Needs CF_AI_GATEWAY_API_TOKEN. The binding only reaches gateways in the Worker's own account, so the generated config sets CF_AI_GATEWAY_USE_BINDING: "false" and the HTTPS transport takes over.
enabled: false No deployment-managed catalog. Each user supplies their own model API keys — and a Workshop migrated from the hosted deploy will show an empty model picker.

pnpm cf-os check reports which of the last two applies before it deploys anything.

When a token is required

Only the two rows above need one. Create a narrowly scoped API token following the current AI Gateway authentication guidance — a Run + Read token; current guidance calls for Account permissions AI Gateway - Read, AI Gateway - Edit, and Workers AI - Read. Install it without putting the value on the command line:

CLOUDFLARE_ACCOUNT_ID=your-account-id pnpm exec wrangler secret put CF_AI_GATEWAY_API_TOKEN --name your-workshop-worker

Note: Use the accountId from your own deployment.jsonc, i.e the account the Workshop deploys to.

In exactly those cases the generated Wrangler config declares the secret as required, so the deploy fails clearly if it is missing. On the default path it does not, so a deployment that needs no token is never blocked waiting for one.

Observability

The starter enables structured custom logs and a private console-backed Error Reporter, while invocation logs, traces, and browser reporting remain separate controls. See Observability and error reporting for signal selection, sampling, triage, privacy, source maps, frontend reporting, and external destinations.

Custom Gatekeepers

Keep deployment-owned Gatekeepers under packages/, outside the cloudflare-os submodule. packages/cf-os/src/core/deployment.ts binds this repository's example as GATEKEEPER_CUSTOM and Context as GATEKEEPER_CONTEXT, twice each: on the Workshop with the GatekeeperVendor entrypoint for RPC, and on the router with no entrypoint, where the binding name is what routes /gatekeeper/custom and /gatekeeper/context to it. A Gatekeeper that serves HTTP — an OAuth redirect, for instance — needs both.

The minimal example flow is:

  1. types.d.ts defines the API visible to TypeScript callers.
  2. CustomSessionImpl.getDeploymentInfo() authorizes an observation before returning data.
  3. CustomGatekeeper reads deployment values and creates the session.
  4. CustomAccount exposes that session as a singleton.
  5. GatekeeperVendor advertises credential-free auto-provisioning.
  6. The Workshop service binding makes the vendor available to Cloudflare OS.

Read the package guide and upstream write-gatekeeper skill before adding OAuth, URL-scoped resources, writes, simulations, hooks, configurator UI, or stricter observer verification.

Code extensions

Prefer wrapper-owned Workers and service bindings over patches inside the submodule. Modify upstream only when a Worker boundary cannot express the behavior, and keep the change as a reviewable upstream commit or fork rather than a generated overlay.

Upgrade

npx cf-os@latest update brings the project up to date with the starter: it merges the starter's main (fetched from its URL, so no remote is needed), checks the submodule out at the pin that merge brings, runs pnpm install, and writes .cf-os/update.md. The brief lists both commit ranges and any conflicts, which git leaves in place, and gives a coding agent the steps to finish and summarise the update, starting from the operator skill's updating guide. The starter's pin has already been reviewed against the starter, so for that path the list below is a check on your own customisations. --dry-run lists what would be merged, and git reset --keep <old HEAD> (the brief names it) undoes it.

Moving the submodule past the starter's pin is a review of its own. npx cf-os@latest update --upstream picks upstream's main as the target and stages it without committing; or check out the commit by hand. It first merges the starter's main, committed, as plain update does, so run that and commit it first to leave the staged change a gitlink-only review. Then:

  1. Record the current cloudflare-os gitlink for rollback.
  2. Update the submodule to the intended upstream commit.
  3. Review the Wrangler base-config changes for the Workshop, router and the three ambient Gatekeepers (Context, Scheduler, Web Search), along with the Gatekeeper contracts. pnpm lint catches one class of drift for you: packages/cf-os/src/core/deployment-config.ts reuses upstream's WranglerConfig, so a changed key there is a type error, not a silently dropped one. A new upstream Gatekeeper arrives as optional; to deploy it everywhere, as the hosted topology does, it needs a Worker and bindings in packages/cf-os/src/core/deployment.ts and a row in the naming table in core/naming.ts.
  4. Compare three entries of cloudflare-os/pnpm-workspace.yaml's catalog: with this repository's: capnweb, typescript and @cloudflare/workers-types. Only those three have to match. The packages here link: to the submodule's rather than sharing a workspace with them. So the submodule's dependencies come from its own lockfile, and this catalog does not govern them. Types still cross the link, though. Each install has its own capnweb copy whatever the version, and today only its types cross. Keeping the ranges in step keeps those types compatible, and keeps the two copies from diverging if runtime code ever crosses too.
  5. Run pnpm install, then pnpm lint and pnpm cf-os check. The root postinstall (cf-os install) reinstalls the submodule's workspace from its lockfile. optimisticRepeatInstall: false makes it run even when this repository's own lockfile has not changed, which is what a gitlink bump looks like from here. If the hook was skipped (--ignore-scripts), pnpm cf-os install --check and every build, test and deploy entry point stop and tell you to run pnpm cf-os install.
  6. Deploy and verify Access, administrator access, storage, configured AI, Context, Scheduler, Web Search, custom observations, a connection through each optional Gatekeeper, and the Error Reporter query surface.
  7. If needed, restore the previous gitlink and redeploy, or use Workers rollback when bindings remain compatible. Neither works across a Durable Object migration: once a pin that adds a migrations tag to a Worker's wrangler.jsonc has deployed, that Worker can only be repaired forward. Check for new tags in step 3, the Workshop's especially, before deploying.

Do not update the submodule blindly. The deploy derives from upstream configs so incompatible base changes remain visible during review and checks.