Skip to content

Latest commit

 

History

History
349 lines (281 loc) · 16.6 KB

File metadata and controls

349 lines (281 loc) · 16.6 KB

cfctl v2

cfctl is a governed Cloudflare control plane for macOS and Linux: an open-source Rust CLI that makes every change to your Cloudflare account reviewable before it happens, and provable after.

Cloudflare's API will let anything holding a token delete a zone, purge a cache, or rotate a secret in one unreviewed call — and an agent driving that API is one bad inference away from doing it. cfctl puts a hash-bound plan between intent and mutation: reads run freely, writes become a reviewed plan you approve by exact operation ID, and the result is verified against live Cloudflare state rather than assumed from a 200 response.

It has no MCP dependency, accepts natural-language intent through a local agent, and every command emits stable JSON for automation.

Install

./bootstrap.sh
cfctl version --json
cfctl doctor --json

bootstrap.sh requires a tracked-clean checkout and proves the installed binary is the exact HEAD commit. Both doctors must report the PATH entry resolving to the running executable; a different or missing PATH binary and drifted agent instructions are unhealthy states, not warnings.

The governed loop

Every mutation follows one path — deterministic resolution, a reviewed hash-bound plan, explicit or policy authority, one Cloudflare boundary, then operation-specific verification:

flowchart TD
    I[Intent] --> R["cfctl resolve"]
    R --> C[Selected capability]
    C --> G["cfctl guide"]
    G --> CALL["cfctl call"]
    CALL -->|read| EV[Redacted live evidence]
    CALL -->|write| P[Fully pinned PlanV2]
    P --> POL{Policy engine}
    POL -->|narrow safe class| RUN["cfctl plans run"]
    POL -->|everything else| APR["cfctl plans approve --yes"]
    APR --> RUN
    RUN --> B[One Cloudflare boundary]
    B --> V[Operation-specific verification]
    V --> DONE[Evidence and journal]
Loading

How cfctl works

cfctl is a local-first, catalog-driven control plane: it separates intent, live reads, durable authority, one Cloudflare boundary, verification, and evidence.

Will this mutate Cloudflare now? Discovery, guides, workspace inspection, and read capabilities do not write Cloudflare. A mutating cfctl call creates a plan; cfctl plans run is the normal write boundary. A token command with --under-policy may plan and run in one invocation only under an explicitly approved standing authority. Agent output, guide output, and approval alone do not mutate Cloudflare.

What grants authority? The deterministic policy engine grants automatic admission only to the narrow safe class. Otherwise authority is either explicit approval of one reviewed operation ID or explicit approval of one bounded standing token policy. A model never grants authority.

What is persisted? Under its managed state root, cfctl persists profile metadata, the live CapabilityV1 catalog and official-doc caches, workspace registrations and imports, plans, approval and admission checkpoints, transaction journals, standing-authority records, locks, and redacted evidence. Credential values remain in the platform secret store or an explicit mode-0600 sink. The source checkout's compat/v1 tree is inert migration evidence, not runtime state or a live catalog.

What happens after a failure or crash? Once consumption or a boundary attempt is durable, cfctl never guesses that replay is safe. Inspect plans status; use plans rectify to reconcile durable receipts and verification without replaying the original Cloudflare mutation.

What should I do next? Run cfctl version --json and both doctors before work; running-build, PATH-build, or managed-instruction drift is unhealthy. Read token permissions only with an explicit account context (keys permissions --account, adding --user only to select user ownership). Nested fixture basenames are skipped during broader workspace scans; fixture directories are opt-in roots and must be registered directly. Then resolve the intent deterministically (cfctl resolve), browse with cfctl catalog search only when exploring, and inspect the selected capability and its capability-specific guide before calling it.

Lifecycle

  1. Orient (none) — Check running and PATH build identity, local state, credentials, catalog health, and agent integration.
  2. Discover (none) — Resolve the intent to the catalog-selected capability and adapter; browse the catalog when exploring.
  3. Read (read) — Inspect exact live Cloudflare state and registered-workspace impact. Durable state: redacted live-read and source-config evidence
  4. Plan (none) — Bind the request, account, catalog, impact, cost, verification, and compensation contracts. Durable state: canonical pinned PlanV2, compatible PlanV1 journal projection, and PlanPrepared checkpoint
  5. Admit (none) — Apply policy, bind any explicit approval, acquire locks, and recheck drift. Durable state: approval, standing reservation, and consumption checkpoints
  6. Execute (write) — Persist the boundary attempt, then cross exactly one catalog-selected adapter boundary. Durable state: boundary-attempt and response checkpoints
  7. Verify (read) — Run the operation-specific verifier or record why rectification is required. Durable state: sink and verification receipts
  8. Close or rectify (none) — Close with evidence or reconcile the durable journal; any compensation is a new plan with independent authority. Durable state: terminal plan status and content-addressed evidence

Commands

cfctl version --json
cfctl doctor --json
cfctl agents doctor --json
cfctl keys permissions --account <account-id> --json
cfctl keys permissions --user --account <account-id> --json
cfctl guide --topic standing-authority --json
cfctl resolve <intent> --json
cfctl catalog search <intent> --json
cfctl catalog show <capability-id> --json
cfctl guide <capability-id> --json
cfctl call <capability-id> --json
cfctl plans show <operation-id> --json
cfctl plans approve <operation-id> --yes --json
cfctl plans run <operation-id> --json
cfctl plans status <operation-id> --json
cfctl plans rectify <operation-id> --json

Public commands

cfctl "<natural-language request>"
cfctl version
cfctl auth login|status|profiles|use|logout|import-api-token|import-global-key
cfctl keys permissions|mint|rotate|revoke|policy
cfctl keys policy create|list|approve|revoke
cfctl catalog sync|search|show|changes|coverage
cfctl resolve "<natural-language intent>"
cfctl call <capability-id> [selectors/body]
cfctl guide <capability-id>
cfctl guide --topic system|standing-authority
cfctl plans show|approve|run|status|resume|rectify|cancel
cfctl policy admission stage|list|show|diff|approve|activate|rollback
cfctl policy cloudflare list|get|diff|plan
cfctl registry scopes list|discover|adopt|remove
cfctl registry sync|status|coverage|list|get|graph|diff|history|export|rebuild
cfctl registry declarations validate|diff|plan
cfctl registry ownership list|get|check
cfctl events sources|status|history|reconcile
cfctl events bridge inspect|prepare|status
cfctl workspace add|remove|discover|graph|audit
cfctl agents install|doctor|sync
cfctl docs search|changes|coverage
cfctl doctor
cfctl update
cfctl migrate v1

Every command has concise human output and stable --json output, shaped by the public contracts BuildInfoV1, CapabilityV1, CapabilityGuideV1, GuideTopicDocumentV1, PlanV1, PolicyDecisionV1, AgentActionV1, EvidenceV1, and ResultEnvelopeV2.

Authenticate

Day-to-day auth is a scoped API token, imported only through stdin — never argv — and pinned to one account:

printf '%s' "$CLOUDFLARE_API_TOKEN" | \
  cfctl auth import-api-token --account <account-id> --stdin

The token lives in the platform keyring (Keychain on macOS, Secret Service on Linux) and falls back to a mode-0600 file store when the keyring is unavailable; cfctl doctor reports which backend is active.

Two things that commonly bite:

  • If a wrapper routes stdin through cargo, pass a new mode-0600 file with --value-in instead, so the secret never touches stdin.
  • CFCTL_FORCE_IPV4=1 pins outbound calls to an IPv4 source, which an IP-allowlisted token needs when the host default-routes over IPv6.

OAuth with PKCE is available when you have a Cloudflare OAuth client (--client-id / CFCTL_OAUTH_CLIENT_ID). An emergency global key can be imported with cfctl auth import-global-key, and is never selected silently.

Read, then change

Reads run immediately and leave redacted evidence:

cfctl call zones-get --query name=example.com --json

A mutating call writes nothing. It produces a hash-bound plan you review by exact operation ID:

cfctl guide dns-records-for-a-zone-create-dns-record
cfctl call dns-records-for-a-zone-create-dns-record \
  --selector zone_id=<zone-id> \
  --body-json '{"type":"TXT","name":"_example","content":"hello"}'

cfctl plans show <operation-id> --json
cfctl plans approve <operation-id> --yes --json
cfctl plans run <operation-id> --json

Plans expire within 24 hours and any relevant drift invalidates approval. A hash-chained transaction journal persists every checkpoint from reviewed plan through close, so a plan consumed before a crash is never replayed — inspect plans status and reconcile with plans rectify. If you approved something you no longer want, plans cancel <operation-id> retires it immediately rather than leaving latent authority to expire.

A narrow safe class of known, scoped, reversible operations runs without separate approval. Deletes, purges, identity and ownership changes, external sends, billing actions, irreversible changes, and anything paid always require it — see runtime policy for the exact contract, and the security contract for per-capability invariants.

Secret outputs never reach stdout, plans, logs, or evidence. They require a new file sink, created mode 0600:

cfctl call cloudflare-tunnel-get-a-cloudflare-tunnel-token \
  --selector account_id=<account-id> \
  --selector tunnel_id=<tunnel-id> \
  --value-out /tmp/tunnel-token

Token creation is exposed only through the inventory-bound keys mint workflow, never a direct create call.

What cfctl refuses to do

Expect to meet the wall early, and read it as the design working. cfctl catalogs every Cloudflare operation but executes a mutation only when its contract is fully known — risk, effect, cost, permission, entitlement, verification, and rollback or explicit irreversibility. Most mutating operations are blocked on incomplete upstream metadata — the executable core is a deliberate minority — and cfctl will not fabricate a permission or a price to close that gap: a guessed one is worse than an honest block. The live ratio is whatever catalog coverage reports; this file does not restate a number it cannot keep true.

cfctl catalog coverage --json          # what is executable, and what is not
cfctl call workflow.telemetry.audit-account --json # bounded proof preview; blocked/gapped steps have no runnable call
cfctl catalog show <capability-id>     # exactly which fields are missing

Blocked capabilities stay discoverable and explain themselves. Most of the root cause is upstream and filed in upstream schema gaps.

Workspaces and agents

Registered roots bound all discovery — cfctl never scans outside them:

cfctl workspace add /absolute/repository/root --account <account-id>
cfctl workspace remove /absolute/repository/root
cfctl workspace discover --json
cfctl workspace audit --json

Removing a root stops future discovery and removes its account pin while preserving historical graph and evidence records. Discovery excludes nested generated, cache, fixture, vendor, and nested-repository paths unless they are registered directly. It inventories Git repositories even when they carry no Cloudflare configuration, and links Wrangler TOML/JSON/JSONC, Terraform HCL/JSON, and Pulumi YAML to catalog targets with current-content, HEAD-content, and exact worktree-diff hashes, so dirty or unmanaged dependencies stay visible in a plan.

The registry is a rebuildable local projection, not a replacement authority:

cfctl registry sync --json
cfctl registry coverage --json
cfctl registry list --json
cfctl registry diff --json

Catalog operations, source configuration, desired declarations under CFCTL_HOME/config/registry/declarations/, live observations, and evidence remain distinct. Coverage stays partial whenever a live normalization provider, permission, or fresh observation is missing. registry rebuild creates a consistent SQLite backup before reconstructing derived rows.

Admission policy is staged, reviewed, approved, and atomically activated as a separate local policy input. A bundle can tighten the compiled hard safety floor but cannot override ambiguity, incomplete contracts, unknown cost, secret hazards, stale observations, or pinned-state drift:

cfctl policy admission list --json

New mutation plans carry a PlanV2 pin set while the PlanV1 body remains readable for compatibility. Historical unconsumed PlanV1 mutations must be replanned before approval or execution.

Events are durable reconciliation triggers, never observed resource truth:

cfctl events sources --json
cfctl events status --json
cfctl events bridge inspect --json
cfctl call events-consume-queue-batch \
  --selector account_id=<account-id> --selector queue_id=<queue-id> \
  --selector subscription_id=<subscription-or-webhook-id> \
  --body-json '{"batch_size":100,"visibility_timeout_ms":60000}' --json

The synthetic event-batch capability is the only Queue pull/ack lane. Each batch is one fully pinned ordinary PlanV2 with explicit approval and the reviewed USD 0.00016 maximum; raw pull and acknowledgement capabilities remain blocked. Event evidence and derived reconciliation jobs commit atomically before exact lease acknowledgement. The inbound Worker under bridge/event-ingress is managed with Bun (bun install --frozen-lockfile, bun run check); preparing its manifest does not deploy it.

cfctl agents install --all-detected
cfctl agents doctor

Natural language launches one configured agent (CFCTL_AGENT; default codex, also claude, cursor, gemini). The agent turns intent into a deterministic cfctl resolve match and governed commands — model output never grants authority or mutates Cloudflare directly. Quote natural language: a bare unknown token fails closed with a did-you-mean, so a typo is never an agent launch. Browser and Computer Use are available only for cataloged governed_ui capabilities, under the same account binding, redaction, approval, and evidence rules.

Evidence and state

Meaningful operations leave redacted, content-addressed local evidence, with the evidence class distinguishing source config, live reads, plans, applies, post-change verification, agent actions, and local proof. Artifact presence is not verification.

cfctl migrate v1 copies safe desired state and non-secret evidence into content-addressed imports, skipping secret-shaped paths and never importing credentials implicitly. Retained v1 data is quarantined under compat/v1/ as inert migration evidence; the live catalog is managed under CFCTL_HOME.

Development

Rust 1.97 is pinned. The local proof lane is cargo xtask verify, which also needs cargo-deny and Gitleaks. Remote CI is intentionally absent. See CONTRIBUTING.md for setup, the pre-push gate, and the assembly, signing, and publishing lanes.

Published releases are unsigned by operator decision, with integrity from SHA256SUMS, reproducible double-builds, SPDX SBOMs, and commit-bound provenance. Install by direct download plus checksum verification, the release's Homebrew formula, or a source build.

cfctl.io registration, site publication, publisher-domain verification, permanent Cloudflare OAuth promotion, and release publication all require explicit operator action and are never silently performed or claimed.