From d9a4e0fed4d9e96ea3026d2f9c6e687f93d3aa76 Mon Sep 17 00:00:00 2001 From: jarvis Date: Tue, 29 Sep 2026 13:36:52 -0400 Subject: [PATCH] feat(cloudflare): advertise cf native CLI Signed-off-by: jarvis --- providers/cloudflare/README.md | 18 ++++++++++++++++++ specs/cloudflare-adapter.feature | 4 ++-- src/providers/cloudflare/adapter.ts | 11 ++++++----- test/providers/cloudflare-adapter.test.ts | 7 +++++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/providers/cloudflare/README.md b/providers/cloudflare/README.md index a93f669..2dec591 100644 --- a/providers/cloudflare/README.md +++ b/providers/cloudflare/README.md @@ -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: diff --git a/specs/cloudflare-adapter.feature b/specs/cloudflare-adapter.feature index b2558cd..5db17c6 100644 --- a/specs/cloudflare-adapter.feature +++ b/specs/cloudflare-adapter.feature @@ -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 diff --git a/src/providers/cloudflare/adapter.ts b/src/providers/cloudflare/adapter.ts index 260c4ff..fe5ad3d 100644 --- a/src/providers/cloudflare/adapter.ts +++ b/src/providers/cloudflare/adapter.ts @@ -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, @@ -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'), @@ -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: [], @@ -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) => { diff --git a/test/providers/cloudflare-adapter.test.ts b/test/providers/cloudflare-adapter.test.ts index acd827f..f2cbc33 100644 --- a/test/providers/cloudflare-adapter.test.ts +++ b/test/providers/cloudflare-adapter.test.ts @@ -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' }, + ], }) })