Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions providers/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ response without automatic write retries. Audit stores only the Agent,
operation, path template, selected scope, status, request ID, CF-Ray, and
duration.

## Native Cloudflare commands

The Resource advertises both Wrangler and `cf`. Run either through Realmroot
to use the Agent's approved Cloudflare authority:

```text
realmroot exec cloudflare -- cf zones list
realmroot exec cloudflare -- npx cf zones list
realmroot exec cloudflare -- wrangler deployments list
```

The CLI sets a process-local `CLOUDFLARE_API_TOKEN` and
`CLOUDFLARE_API_BASE_URL`. Cloudflare API v4 requests go through the local
broker and the Adapter's published operation and scope checks. Commands that
use an unpublished API operation fail at the Adapter. `cf` is currently in
open beta; commands using other Cloudflare origins or local-only services are
outside this API v4 broker contract.

## Regeneration

Set `CLOUDFLARE_API_TOKEN` only for the generator process and run:
Expand Down
4 changes: 2 additions & 2 deletions specs/cloudflare-adapter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ Feature: Cloudflare OAuth REST adapter
And no write request is automatically retried

@journey:cloudflare-native-tool-discovery @entrypoint:http
Scenario: Cloudflare advertises Wrangler execution
Scenario: Cloudflare advertises Wrangler and cf execution
When the Agent reads the Cloudflare Resource representation
Then it advertises a Wrangler integration with its supported executable names
Then it advertises Wrangler and cf integrations with their supported executable names
And the integration identifies the local API-base broker protocol it requires
And Wrangler-required Cloudflare routes missing from the official schema are explicitly pinned and scoped
And Wrangler can inspect, deploy, and delete a Worker through the broker
Expand Down
11 changes: 6 additions & 5 deletions src/providers/cloudflare/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function createCloudflareAdapter(
authorizationModel: 'external',
toolIntegrations: [
{ id: 'wrangler', executables: ['wrangler', 'npx', 'pnpm'], protocol: 'cloudflare-api-base' },
{ id: 'cf', executables: ['cf', 'npx', 'pnpm'], protocol: 'cloudflare-api-base' },
],
},
200,
Expand All @@ -94,10 +95,10 @@ export function createCloudflareAdapter(
app.get('/cloudflare/user/tokens/verify', async (c) => {
const principal = await dependencies.authenticator.authenticate(c.req.raw, resource)
const requiredScope = [...principal.scopes].sort().find((scope) => scope in cloudflareManifest.scopes)
if (!requiredScope) throw forbidden('The Agent token has no approved Cloudflare scope for Wrangler verification.')
if (!requiredScope) throw forbidden('The Agent token has no approved Cloudflare scope for CLI verification.')
const provider = await credential(principal.subject)
if (!provider.scopes.includes(requiredScope))
throw forbidden('The Cloudflare OAuth grant does not authorize Wrangler verification.')
throw forbidden('The Cloudflare OAuth grant does not authorize CLI verification.')
await dependencies.audit({
event: 'provider.operation',
requestId: c.get('requestId'),
Expand All @@ -122,10 +123,10 @@ export function createCloudflareAdapter(
app.get('/cloudflare/user', async (c) => {
const principal = await dependencies.authenticator.authenticate(c.req.raw, resource)
const requiredScope = [...principal.scopes].sort().find((scope) => scope in cloudflareManifest.scopes)
if (!requiredScope) throw forbidden('The Agent token has no approved Cloudflare authority for Wrangler identity.')
if (!requiredScope) throw forbidden('The Agent token has no approved Cloudflare authority for CLI identity.')
const provider = await credential(principal.subject)
if (!provider.scopes.includes(requiredScope))
throw forbidden('The Cloudflare OAuth grant does not authorize Wrangler identity.')
throw forbidden('The Cloudflare OAuth grant does not authorize CLI identity.')
return c.json({
success: true,
errors: [],
Expand Down Expand Up @@ -162,7 +163,7 @@ export function createCloudflareAdapter(
const principal = await dependencies.authenticator.authenticate(c.req.raw, resource)
const hasApprovedScope = [...principal.scopes].some((scope) => scope in cloudflareManifest.scopes)
if (!hasApprovedScope)
throw forbidden('The Agent token has no approved Cloudflare authority for Wrangler membership lookup.')
throw forbidden('The Agent token has no approved Cloudflare authority for CLI membership lookup.')
return c.json({ success: true, errors: [], messages: [], result: [], result_info: { count: 0 } })
})
app.all('/cloudflare/*', async (c) => {
Expand Down
7 changes: 5 additions & 2 deletions test/providers/cloudflare-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ describe('Cloudflare adapter', () => {
expect(url.searchParams.get('scope')).toBe('openid offline_access d1.read')
})

it('[spec: cloudflare-adapter/cloudflare-native-tool-discovery] advertises Wrangler execution', async () => {
it('[spec: cloudflare-adapter/cloudflare-native-tool-discovery] advertises Wrangler and cf execution', async () => {
const { app } = fixture()
const response = await app.request('/cloudflare')
await expect(response.json()).resolves.toMatchObject({
toolIntegrations: [{ id: 'wrangler', executables: ['wrangler', 'npx', 'pnpm'], protocol: 'cloudflare-api-base' }],
toolIntegrations: [
{ id: 'wrangler', executables: ['wrangler', 'npx', 'pnpm'], protocol: 'cloudflare-api-base' },
{ id: 'cf', executables: ['cf', 'npx', 'pnpm'], protocol: 'cloudflare-api-base' },
],
})
})

Expand Down
Loading