AI Demo Cloudflare AI security demo

Setting up the agent

The demo needs an AI client with two things wired up: a model reached through Cloudflare AI Gateway, and the five MCP servers reached through the MCP server portal. These are the two places the controls live.

Two endpoints, and no API key

Access secures both traffic paths and therefore every request is attributed to the person who made it.

Sign in as the demo user

Every demo is run as Alice Watson — , password Savetheinternet!1. When the agent connects to the portal you will be sent through Cloudflare Access and then FlareID.

Using the admin account

is the system administrator, and it exists only for the /admin page on each app. It's also the admin for the IDP if you want to make changes there.

First: authenticate each MCP server (one browser login each)

The script registers five MCP servers with Access and creates an Access application for each, but it cannot perform the upstream OAuth login those servers require. Until it is done each server sits in Waiting and the portal has nothing to offer your agent. Note authenticate as the CEO Nikita Chapman so you can auth all five servers. (Alice has no access to the financial Ledger app)

  1. In the Cloudflare dashboard, go to Zero Trust → Access controls → MCP Portals → MCP servers. All five (hr, crm, work, wiki, fin) should be listed, each showing Waiting.
  2. Click on the three dots next to each server → Authenticate server.
  3. Sign in as (password Savetheinternet!1). Cloudflare fetches the server's tools and the status becomes Ready.
  4. Repeat for crm, work, fin and wiki.

Setting up the clients

Cloudflare OS is deployed, but in my opinion not ready for demo. So you will need to install a local client, instructions below.

For both the Goose and Opencode demos, you should run a local VM and it has to be enrolled in the Cloudflare One client and authenticated as <code data-email="alice.watson"></code>.

Enrolling a device in this demo's Zero Trust organisation needs configuration. wire-access.mjs deliberately turns on install CA to system certificate store, because Gateway cannot inspect HTTPS unless the device trusts its certificate — so the Cloudflare One client puts this tenant's root certificate into your trust store, and from then on the demo account can decrypt that device's traffic. Windows it deploys automatically, on MacOS you need to go to Keychain Access and trust the certificates.

Cloudflare OS is the exception and needs none of this — it runs in a browser and authenticates through Access, so there is no device to enrol and no certificate to install. If you cannot make a VM, that is the client to use.

Client 1 — Goose (preferred)

On Windows, the script does all of this for you

Everything in this section - the Cloudflare One client, the Goose download, the provider, the portal extension, disabling the bundled extensions, and the keyring setting below - is done by one script. This lab's zone and team name are already baked into it, so it takes no arguments. Run it from an elevated PowerShell:

irm https://demo./goose-setup.ps1 -OutFile goose-setup.ps1
.\goose-setup.ps1

Two things it deliberately leaves to you, because neither can be scripted: signing in to the Cloudflare One client as Alice, and authorising the servers at the MCP portal the first time the agent reaches for a tool.

The steps below are what the script automates. Follow them by hand on macOS, or on Windows if you would rather see what it is doing.

Verified on Goose Desktop 1.53.0. Two things to add, in this order: a custom provider pointing at the AI Gateway, and a Streamable HTTP extension pointing at the portal.

  1. Add the provider. At first launch choose Connect to a provider → + Add a custom provider → Configure manually. Leave the provider type as OpenAI Compatible, name it anything (Cloudflare works), and set the API URL to https:///compat. Leave the API base path empty.
  2. Paste in one model under available models: @cf/google/gemma-4-26b-a4b-it.
  3. Add the portal. Extensions → + Add custom extension, type Streamable HTTP, name it Company AI Portal, endpoint https:///mcp. Add extension.
  4. Disable every other extension so the portal is the only one left enabled under Default Extensions. This matters as much here as the deny list does in opencode: Goose ships with developer tooling, and an agent that can read your filesystem will answer from it instead of from the company's systems.
  5. Send any prompt. You are redirected to the portal once, where you authorise the servers — and then never asked again. Note that Alice does not get to auth the MCP server for the finance app, because the Access policy limits her.
On Windows, set GOOSE_DISABLE_KEYRING first

On Windows the portal extension fails with “Failed to load extension” — or, in the CLI, Auth required, when send discover request — even though the browser says Authorization Success. The authorization did succeed; Goose then discarded it, because Windows Credential Manager caps a credential at 2560 characters and Cloudflare's OAuth tokens are longer than that. macOS Keychain has no such limit, which is the whole of the difference. Goose logs the real cause as a warning that never reaches the screen:

Failed to save credentials: Failed to access keyring: Attribute 'password encoded as UTF-16' is longer than platform limit of 2560 chars

So store credentials in a file instead. One line in %APPDATA%\Block\goose\config\config.yaml — the same file the extension lives in, and it covers the desktop app and the CLI together:

GOOSE_DISABLE_KEYRING: true

Or for a single terminal: $env:GOOSE_DISABLE_KEYRING = "1".

The token then sits unencrypted in secrets.yaml next to that config. For a throwaway demo tenant that is a fair trade; do not carry the setting to a machine whose credentials you care about.

One login, every server, and it sticks

This is the practical difference from Cloudflare OS, and worth knowing before you choose. Goose completes a single OAuth flow against the portal and can then call every server behind it, for the life of the token — no per-server grant, no per-chat grant, nothing to redo before a second run. Cloudflare OS instead issues a capability per server, scoped to the chat it was granted in.

Both are defensible, and the per-capability model is arguably the better security story.

Client 2 — opencode

It will not tell your audience why anything was blocked

Read this before choosing opencode. When the gateway refuses a call, opencode reports the whole of it as:

Provider request failed with HTTP 424

No reason, no policy name, nothing to read out. The gateway does send one — "Request content blocked due to DLP policy violations" — and opencode discards it. Goose prints it:

Request failed with status 424 Failed Dependency at
https://aig.<your-zone>/compat/v1/chat/completions:
Request content blocked due to DLP policy violations.

Six of the walkthrough's eight steps end in something being refused with an unclear UX. On opencode every one of those becomes you switching to the AI Gateway dashboard to explain what just happened, which works but isn't an ideal demo of the user experience.

Then two things to configure: a provider against AI Gateway's OpenAI-compatible endpoint, and an MCP server against the portal. There are two configuration shapes below, and do not pick between them by version number — that is the trap.

The Desktop app takes the 1.x config, whatever the version

Verified on opencode Desktop 2.0.22: the 1.x configuration works and the 2.x one does not. The desktop app's version number is its own, and it does not tell you which config schema it reads — a "2.0.x" desktop build still wants the 1.x shape.

opencode 1.x — and the Desktop apptop-level permission map · verified on Desktop 2.0.22 and on the 1.18.x CLI

In ~/.config/opencode/opencode.json.

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "cf-ai-demo": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Cloudflare AI Gateway (demo)",
      "options": {
        "baseURL": "https://aig./compat"
      },
      "models": {
        "workers-ai/@cf/google/gemma-4-26b-a4b-it": {
          "name": "Google Gemma 4 (Workers AI)"
        }
      }
    }
  },
  "mcp": {
    "servers": {
      "ai-demo": {
        "type": "remote",
        "url": "https://mcp./mcp",
        "codemode": false
      }
    }
  },
  "permission": {
    "execute": "deny",
    "bash": "deny",
    "edit": "deny",
    "read": "deny",
    "glob": "deny",
    "grep": "deny",
    "webfetch": "deny",
    "websearch": "deny",
    "subagent": "deny",
    "skill": "deny"
  }
}
opencode 2.x CLI onlyagents with a permissions array · rejected by Desktop 2.0.22

In ~/.config/opencode/opencode.json. Only for the 2.x CLI — the Desktop app rejects this shape, including Desktop 2.0.22, so use the card above for it.

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "cf-ai-demo": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Cloudflare AI Gateway (demo)",
      "options": {
        "baseURL": "https://aig./compat"
      },
      "models": {
        "workers-ai/@cf/google/gemma-4-26b-a4b-it": {
          "name": "Google Gemma 4 (Workers AI)"
        }
      }
    }
  },
  "mcp": {
    "servers": {
      "ai-demo": {
        "type": "remote",
        "url": "https://mcp./mcp",
        "codemode": false
      }
    }
  },
  "default_agent": "employee",
  "agents": {
    "employee": {
      "description": "An ordinary employee's assistant - company tools only",
      "mode": "primary",
      "steps": 10,
      "system": "You are the assistant of an employee at this company. Answer using only the company tools available to you. You have no filesystem, no shell and no access to the public internet. Call the tool that matches the question directly - do not search for tools and do not write code. If the tools cannot answer the question, say so plainly rather than guessing.",
      "permissions": [
        { "action": "execute",   "resource": "*", "effect": "deny" },
        { "action": "shell",     "resource": "*", "effect": "deny" },
        { "action": "edit",      "resource": "*", "effect": "deny" },
        { "action": "read",      "resource": "*", "effect": "deny" },
        { "action": "glob",      "resource": "*", "effect": "deny" },
        { "action": "grep",      "resource": "*", "effect": "deny" },
        { "action": "webfetch",  "resource": "*", "effect": "deny" },
        { "action": "websearch", "resource": "*", "effect": "deny" },
        { "action": "subagent",  "resource": "*", "effect": "deny" },
        { "action": "skill",     "resource": "*", "effect": "deny" }
      ]
    }
  }
}

Client 3 — Cloudflare OS

./deploy.sh deploys Cloudflare OS at , and the five apps carry a Company AI tile in their launcher pointing at it. Set DEPLOY_CLOUDFLARE_OS="false" to skip it.

Worth knowing why you might prefer it: every model call it makes routes through the AI Gateway named in its config, so the DLP profiles and guardrails apply to every agent turn rather than only the leg a desktop client chooses to send, and it authenticates users through Access, so the agent runs as the signed-in person.

It needs three things done once per person, in the browser, and no API can do any of them — a model and a connector both belong to a user account and are configured over that user's own session:

  1. Add the model. Settings → Choose your model → Add new model → Other Cloudflare Workers AI…, then the bare id @cf/google/gemma-4-26b-a4b-it. No key is requested: gateway mode authenticates on the Workers AI binding. The id takes no workers-ai/ prefix here — that belongs to other clients' naming.
  2. Connect the portal account. On /gatekeepers, add the portal. It runs an OAuth flow against Access. This connects the account and grants no servers — see the next step, which is not on that page.
  3. Grant the servers you need, one at a time. Open a chat, click the + in the message composer (the control labelled "Files, connections and skills"), then Add a new connection. Choose Company AI Portal, keep the account already connected, pick a Server, choose All tools, and confirm. Repeat per server. The picker lists five similar invented product names, so pick by what the step needs rather than by what sounds right:
    • WorkBox MCP — inbox and calendar. Step 4 needs this one.
    • Pipeline MCP — the CRM. Step 8 needs this one.
    • WorkWeek MCP — HR. Only for the per-app scripts.
    • Nexus MCP — the wiki. Only for the per-app scripts, and the one most easily picked by mistake when you wanted the calendar.
    • Ledger MCP — finance. Do not grant it; Alice is not entitled to it and its absence is the point of step 8.
    So for the walkthrough: two passes, WorkBox and Pipeline.

Running without the protection layer first

If the suite was deployed with DEPLOY_PROTECTION=false there is no portal and no AI Gateway yet. Point the client at the MCP servers individually and at the model provider directly:

{
  "mcp": {
    "servers": {
      "workweek": { "type": "remote", "url": "https://hr-mcp./mcp" },
      "pipeline": { "type": "remote", "url": "https://crm-mcp./mcp" },
      "relay":    { "type": "remote", "url": "https://work-mcp./mcp" },
      "nexus":    { "type": "remote", "url": "https://wiki-mcp./mcp" },
      "ledger":   { "type": "remote", "url": "https://fin-mcp./mcp" }
    }
  }
}

Each server runs its own OAuth 2.1 flow and delegates login to Cloudflare Access, so you will sign in as Alice once per server — except ledger, which will refuse her, because its Access application allows only the leadership team. Every demo script works in this mode — that is the "before" half of each one.

Tool names change when you go through the portal

The portal namespaces every tool with its server id, so list_employees becomes hr_list_employees, get_pipeline_summary becomes crm_get_pipeline_summary, and so on with work_, wiki_ and fin_. The demo scripts name the underlying tool; your transcript will show the prefixed one.

Check it works

Before running any script, ask the agent something harmless that proves both legs are live:

Who am I, and which tools do you have available?

You should see Alice Watson come back from the whoami tool, a list of tools from the four apps she can reach — Ledger's will be absent, by design — and, if the protection layer is deployed, a corresponding request in the AI Gateway log and in the MCP portal log.