diff --git a/README.md b/README.md index a0809c7..3116024 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ cfctl guide edge.certificate order --zone example.com --host app.example.com --h cfctl hostname verify --file state/hostname/example.yaml ``` +Before choosing a write path, scan [docs/capabilities.md](docs/capabilities.md). +It is generated from the catalogs and shows the read/plan/apply/verify contract, +including preview requirements, destructive confirmations, lane policy, +selectors, and desired-state sync support. + Before credentials exist, `cfctl doctor` reports `bootstrap_required` and points at `cfctl bootstrap permissions`; `cfctl doctor --strict` still fails until a healthy token lane exists. @@ -208,7 +213,7 @@ zone, such as `CF_TOKEN_LANE=global CFCTL_PUBLIC_CONTRACT_ZONE=example.com operator policy for these credentials is in [docs/permission-doctrine.md](docs/permission-doctrine.md). -See [docs/runbooks/cfctl.md](docs/runbooks/cfctl.md) and [docs/capabilities.md](docs/capabilities.md) for the full reference. +See [docs/runbooks/cfctl.md](docs/runbooks/cfctl.md) and [docs/capabilities.md](docs/capabilities.md) for the full reference. `docs/capabilities.md` is generated from the catalogs and is the fastest way to see which surfaces are read-only, which operations are preview-gated, which destructive operations require confirmation, and which surfaces support desired-state sync. ## Layout @@ -315,7 +320,7 @@ targeted retry does not shrink accepted domains to only the retry subset. - [CFCTL_PROMPT.md](CFCTL_PROMPT.md) — strict embedding prompt for tool integrators - [docs/agent-landing.md](docs/agent-landing.md) - [docs/auth.md](docs/auth.md) -- [docs/capabilities.md](docs/capabilities.md) — generated from catalogs +- [docs/capabilities.md](docs/capabilities.md) — generated from catalogs; includes the read/plan/apply/verify operation matrix - [docs/config-standards.md](docs/config-standards.md) - [docs/cloudflare-doc-bank.md](docs/cloudflare-doc-bank.md) - [docs/runtime-policy.md](docs/runtime-policy.md) diff --git a/docs/agent-landing.md b/docs/agent-landing.md index ad80397..3a5ec88 100644 --- a/docs/agent-landing.md +++ b/docs/agent-landing.md @@ -23,7 +23,7 @@ Start by naming the job class: - External command auth bridge: run `cfctl env run --lane dev -- [args...]` when another repo owns deploy semantics but `cfctl` owns Cloudflare credential hydration. Never pass secrets as command args because argv is recorded as evidence. - Mutation: - read state, load standards, classify, guide, preview with `--plan`, apply with `--ack-plan `, then verify. + read state, scan the generated matrix in [docs/capabilities.md](capabilities.md), load standards, classify, guide, preview with `--plan`, apply with `--ack-plan `, then verify. - Runtime development: change `cfctl`, catalogs, docs, and contract checks together; do not document a public capability before it exists in the catalog and command surface. - Degraded trust: @@ -36,6 +36,7 @@ Do not turn a source-config audit into a live Cloudflare claim. If the question ```bash cfctl doctor cfctl surfaces +cfctl explain surfaces cfctl docs cfctl docs watch cfctl standards audit diff --git a/docs/capabilities.md b/docs/capabilities.md index 2372878..de68921 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -6,34 +6,104 @@ _Generated from `catalog/surfaces.json` and `catalog/runtime.json`. Edit the cat This table is the operable runtime surface. The standards layer and docs bank intentionally cover more Cloudflare territory than `cfctl` can currently mutate or verify directly. -| Surface | Read | Apply | Desired State | Standards | Docs Topics | Module | -| --- | --- | --- | --- | --- | --- | --- | -| `access.app` | yes | yes | yes | `access.app` | `zero-trust-api, api-auth` | `access_app` | -| `access.policy` | yes | yes | yes | `access.policy` | `zero-trust-api, api-auth` | `access_policy` | -| `api_gateway.discovery` | yes | no | no | `-` | `api-gateway, api-auth` | `-` | -| `api_gateway.operation` | yes | no | no | `-` | `api-gateway, api-auth` | `-` | -| `api_gateway.schema` | yes | no | no | `-` | `api-gateway, api-auth` | `-` | -| `audit.log` | yes | no | no | `-` | `audit-logs, api-auth` | `-` | -| `d1.database` | yes | no | no | `-` | `-` | `-` | -| `dns.record` | yes | yes | yes | `dns.record` | `api-auth` | `dns_record` | -| `edge.certificate` | yes | yes | no | `edge.certificate` | `advanced-certificates, api-auth` | `edge_certificate` | -| `email.routing_rule` | yes | yes | no | `-` | `email-routing, api-auth` | `-` | -| `logpush.job` | yes | yes | no | `-` | `-` | `-` | -| `pages.project` | yes | no | no | `-` | `-` | `-` | -| `queue` | yes | no | no | `-` | `-` | `-` | -| `r2.bucket` | yes | no | no | `-` | `-` | `-` | -| `tunnel` | yes | yes | yes | `tunnel` | `api-auth` | `tunnel` | -| `turnstile.widget` | yes | yes | no | `-` | `-` | `-` | -| `vulnerability_scanner.credential_set` | yes | no | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | -| `vulnerability_scanner.scan` | yes | no | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | -| `vulnerability_scanner.target_environment` | yes | no | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | -| `waiting_room` | yes | yes | no | `-` | `-` | `-` | -| `worker.route` | yes | yes | no | `worker.route` | `workers-routes, api-auth` | `worker_route` | -| `worker.script` | yes | yes | no | `-` | `-` | `worker_script` | -| `worker.secret` | yes | yes | no | `-` | `-` | `worker_secret` | -| `workflow` | yes | no | no | `-` | `-` | `-` | -| `zone` | yes | no | no | `-` | `-` | `-` | -| `zone.ruleset` | yes | yes | no | `-` | `ruleset-engine, api-auth` | `-` | +| Surface | Read | Can | Apply | Verify | Desired State | Standards | Docs Topics | Module | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `access.app` | yes | yes | yes | yes | yes | `access.app` | `zero-trust-api, api-auth` | `access_app` | +| `access.policy` | yes | yes | yes | yes | yes | `access.policy` | `zero-trust-api, api-auth` | `access_policy` | +| `api_gateway.discovery` | yes | yes | no | yes | no | `-` | `api-gateway, api-auth` | `-` | +| `api_gateway.operation` | yes | yes | no | yes | no | `-` | `api-gateway, api-auth` | `-` | +| `api_gateway.schema` | yes | yes | no | yes | no | `-` | `api-gateway, api-auth` | `-` | +| `audit.log` | yes | yes | no | yes | no | `-` | `audit-logs, api-auth` | `-` | +| `d1.database` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `dns.record` | yes | yes | yes | yes | yes | `dns.record` | `api-auth` | `dns_record` | +| `edge.certificate` | yes | yes | yes | yes | no | `edge.certificate` | `advanced-certificates, api-auth` | `edge_certificate` | +| `email.routing_rule` | yes | yes | yes | yes | no | `-` | `email-routing, api-auth` | `-` | +| `logpush.job` | yes | yes | yes | yes | no | `-` | `-` | `-` | +| `pages.project` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `queue` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `r2.bucket` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `tunnel` | yes | yes | yes | yes | yes | `tunnel` | `api-auth` | `tunnel` | +| `turnstile.widget` | yes | yes | yes | yes | no | `-` | `-` | `-` | +| `vulnerability_scanner.credential_set` | yes | yes | no | yes | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | +| `vulnerability_scanner.scan` | yes | yes | no | yes | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | +| `vulnerability_scanner.target_environment` | yes | yes | no | yes | no | `-` | `api-shield-vulnerability-scanner, api-auth` | `-` | +| `waiting_room` | yes | yes | yes | yes | no | `-` | `-` | `-` | +| `worker.route` | yes | yes | yes | yes | no | `worker.route` | `workers-routes, api-auth` | `worker_route` | +| `worker.script` | yes | yes | yes | yes | no | `-` | `-` | `worker_script` | +| `worker.secret` | yes | yes | yes | yes | no | `-` | `-` | `worker_secret` | +| `workflow` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `zone` | yes | yes | no | yes | no | `-` | `-` | `-` | +| `zone.ruleset` | yes | yes | yes | yes | no | `-` | `ruleset-engine, api-auth` | `-` | + +## Operation Contract Matrix + +This matrix is derived from the same catalogs used by `cfctl explain`, `cfctl classify`, `cfctl guide`, and the static verifier. It is the preflight view for deciding whether a surface is read-only, preview-gated, destructive, lane-sensitive, or desired-state-backed. + +| Surface | Operation | Risk | Preview | Lock | Verify After Apply | Confirmation | Allowed Lanes | Selectors | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `access.app` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `access.app` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: id | +| `access.app` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: id | +| `access.app` | `sync` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | state match: id, name, domain | +| `access.policy` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: app_id | +| `access.policy` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: app_id, policy_id | +| `access.policy` | `make-reusable` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: app_id, policy_id | +| `access.policy` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: app_id, policy_id | +| `access.policy` | `sync` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | state match: app_id, policy_id, name | +| `dns.record` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, name, type | +| `dns.record` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: zone; one of: id / name, type | +| `dns.record` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone; one of: id / name, type | +| `dns.record` | `upsert` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, name, type | +| `dns.record` | `sync` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | state match: zone, id, name, type | +| `edge.certificate` | `order` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone | +| `email.routing_rule` | `upsert` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, name, service | +| `logpush.job` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `logpush.job` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: id | +| `logpush.job` | `ownership` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `logpush.job` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: id | +| `logpush.job` | `validate-destination` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `logpush.job` | `validate-origin` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `logpush.job` | `validate-ownership` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `tunnel` | `cleanup-connections` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: id | +| `tunnel` | `configure` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: id | +| `tunnel` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `tunnel` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: id | +| `tunnel` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: id | +| `tunnel` | `sync` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | state match: id, name | +| `turnstile.widget` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | +| `turnstile.widget` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: sitekey | +| `turnstile.widget` | `rotate-secret` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: sitekey | +| `turnstile.widget` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: sitekey | +| `waiting_room` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone | +| `waiting_room` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: zone, id | +| `waiting_room` | `patch` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, id | +| `waiting_room` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, id | +| `worker.route` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: zone; one of: id / pattern | +| `worker.script` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: name | +| `worker.script` | `upsert` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: name, metadata, module | +| `worker.secret` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: script, name | +| `worker.secret` | `upsert` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: script, name | +| `zone.ruleset` | `update` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, id | + +## Read-Only Surfaces + +These surfaces are first-class read surfaces but do not expose `apply` or desired-state `sync` today. Mutation should not be inferred from an inventory script alone. + +| Surface | Public Actions | List Selectors | Inventory Backend | +| --- | --- | --- | --- | +| `api_gateway.discovery` | `list`, `get`, `verify`, `can` | required: zone | `scripts/cf_inventory_api_gateway.sh` | +| `api_gateway.operation` | `list`, `get`, `verify`, `can` | required: zone | `scripts/cf_inventory_api_gateway.sh` | +| `api_gateway.schema` | `list`, `get`, `verify`, `can` | required: zone | `scripts/cf_inventory_api_gateway.sh` | +| `audit.log` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_audit_logs.sh` | +| `d1.database` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_d1.sh` | +| `pages.project` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_pages.sh` | +| `queue` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_queues.sh` | +| `r2.bucket` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_r2.sh` | +| `vulnerability_scanner.credential_set` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_vulnerability_scanner.sh` | +| `vulnerability_scanner.scan` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_vulnerability_scanner.sh` | +| `vulnerability_scanner.target_environment` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_vulnerability_scanner.sh` | +| `workflow` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_workflows.sh` | +| `zone` | `list`, `get`, `verify`, `can` | - | `scripts/cf_inventory_zones.sh` | Composite lifecycle commands: - `cfctl hostname verify --file state/hostname/.yaml` diff --git a/docs/runbooks/cfctl.md b/docs/runbooks/cfctl.md index c72faa3..57cd8bb 100644 --- a/docs/runbooks/cfctl.md +++ b/docs/runbooks/cfctl.md @@ -28,6 +28,11 @@ It is built for agent and operator use: - `explain` for the machine-readable contract of a surface - `diff` for selective desired-state comparison +[docs/capabilities.md](../capabilities.md) is the generated repo-wide contract +matrix. Use it before writing to see read-only surfaces, preview requirements, +destructive confirmations, allowed lanes, selector requirements, verification +expectations, and desired-state sync support in one place. + ## Core Examples ```bash @@ -99,6 +104,7 @@ CF_TOKEN_LANE=global cfctl apply edge.certificate order --zone example.com --hos - commands emit a stable JSON result envelope - every result includes active auth lane and auth scheme - `standards` returns the canonical configuration standards catalog or one surface-specific standard set +- `docs/capabilities.md` is generated from `catalog/surfaces.json` and `catalog/runtime.json`; edit the catalogs when the public contract changes - `docs` returns the curated official Cloudflare docs bank, either as a compact overview or one tracked topic - `docs` includes freshness metadata so the bank does not masquerade as auto-refreshed truth - `standards audit` scans the active Wrangler footprint under a root and reports standards coverage plus per-file findings diff --git a/docs/runbooks/live-inventory.md b/docs/runbooks/live-inventory.md index 41bbe1f..4b51877 100644 --- a/docs/runbooks/live-inventory.md +++ b/docs/runbooks/live-inventory.md @@ -67,6 +67,11 @@ cfctl snapshot tunnel CF_TOKEN_LANE=global cfctl diff dns.record --zone example.com ``` +For the repo-wide read/write boundary, use +[../capabilities.md](../capabilities.md). It is generated from the catalogs and +separates read-only inventory surfaces from preview-gated apply and +desired-state sync surfaces. + ## Runtime-First Reads Use `cfctl` when you want a stable public interface: diff --git a/docs/runbooks/tool-choice.md b/docs/runbooks/tool-choice.md index eb272df..ec20df1 100644 --- a/docs/runbooks/tool-choice.md +++ b/docs/runbooks/tool-choice.md @@ -14,6 +14,12 @@ It gives agents and operators: - structured runtime results - consistent verification semantics +Use [../capabilities.md](../capabilities.md) when the first question is which +surface owns a task, whether that surface is read-only, whether an operation is +preview-gated or destructive, and which selectors are required. The matrix is +generated from `catalog/surfaces.json` and `catalog/runtime.json`, not hand-kept +operator prose. + ## Use Direct API Wrappers For - account-wide inventory diff --git a/scripts/render_capabilities_doc.py b/scripts/render_capabilities_doc.py index 1145efb..54ba080 100644 --- a/scripts/render_capabilities_doc.py +++ b/scripts/render_capabilities_doc.py @@ -12,24 +12,111 @@ def yes_no(value: bool) -> str: return "yes" if value else "no" +def inline_list(values: list[str]) -> str: + if not values: + return "-" + return ", ".join(f"`{value}`" for value in values) + + +def text_list(values: list[str]) -> str: + return ", ".join(values) if values else "-" + + +def operation_policy(runtime: dict, operation: str, operation_meta: dict) -> dict: + policy = runtime.get("policy", {}) + risk = operation_meta.get("risk") or "write" + defaults = dict(policy.get("operation_defaults", {}).get(risk, {})) + special = dict(policy.get("special_operations", {}).get(operation, {})) + merged = {**defaults, **special, **operation_meta} + merged["risk"] = risk + return merged + + +def selector_contract(action_meta: dict) -> str: + required = action_meta.get("required_selectors", []) + any_of = action_meta.get("selectors_any_of", []) + parts = [] + if required: + parts.append("required: " + ", ".join(required)) + if any_of: + parts.append("one of: " + " / ".join(", ".join(group) for group in any_of)) + return "; ".join(parts) if parts else "-" + + def render(root: Path) -> str: runtime = json.loads((root / "catalog/runtime.json").read_text()) surfaces = json.loads((root / "catalog/surfaces.json").read_text())["surfaces"] desired_state = runtime.get("desired_state", {}) rows = [] + operation_rows = [] + read_only_rows = [] for surface_name in sorted(surfaces): - surface = surfaces[surface_name] - actions = surface.get("actions", {}) - read_supported = any(actions.get(action, {}).get("supported") is True for action in ("list", "get", "verify")) - apply_supported = actions.get("apply", {}).get("supported") is True - desired = desired_state.get(surface_name, {}) - standards_ref = surface.get("standards_ref") or "-" - docs_topics = ", ".join(surface.get("docs_topics", [])) or "-" - module = surface.get("module") or "-" - rows.append( - f"| `{surface_name}` | {yes_no(read_supported)} | {yes_no(apply_supported)} | {yes_no(desired.get('supported', False))} | `{standards_ref}` | `{docs_topics}` | `{module}` |" - ) + surface = surfaces[surface_name] + actions = surface.get("actions", {}) + read_supported = any( + actions.get(action, {}).get("supported") is True + for action in ("list", "get", "verify") + ) + can_supported = actions.get("can", {}).get("supported") is True + apply_supported = actions.get("apply", {}).get("supported") is True + verify_supported = actions.get("verify", {}).get("supported") is True + desired = desired_state.get(surface_name, {}) + standards_ref = surface.get("standards_ref") or "-" + docs_topics = ", ".join(surface.get("docs_topics", [])) or "-" + module = surface.get("module") or "-" + rows.append( + f"| `{surface_name}` | {yes_no(read_supported)} | " + f"{yes_no(can_supported)} | {yes_no(apply_supported)} | " + f"{yes_no(verify_supported)} | {yes_no(desired.get('supported', False))} | " + f"`{standards_ref}` | `{docs_topics}` | `{module}` |" + ) + + if apply_supported: + operations = actions.get("apply", {}).get("operations", {}) + for operation, operation_meta in sorted(operations.items()): + policy = operation_policy(runtime, operation, operation_meta) + confirmation = ( + operation_meta.get("confirm") or policy.get("confirmation") or "-" + ) + selectors = selector_contract(operation_meta) + lanes = inline_list(policy.get("allowed_lanes", [])) + operation_rows.append( + f"| `{surface_name}` | `{operation}` | " + f"`{policy.get('risk', 'write')}` | " + f"{yes_no(policy.get('preview_required') is True)} | " + f"`{policy.get('lock_strategy', '-')}` | " + f"{yes_no(policy.get('verification_required') is True and verify_supported)} | " + f"`{confirmation}` | {lanes} | {selectors} |" + ) + + if desired.get("sync_supported") is True: + policy = operation_policy(runtime, "sync", {"risk": "write"}) + lanes = inline_list(policy.get("allowed_lanes", [])) + selectors = text_list(desired.get("match_selectors", [])) + operation_rows.append( + f"| `{surface_name}` | `sync` | `{policy.get('risk', 'write')}` | " + f"{yes_no(policy.get('preview_required') is True)} | " + f"`{policy.get('lock_strategy', '-')}` | " + f"{yes_no(policy.get('verification_required') is True and verify_supported)} | " + f"`-` | {lanes} | state match: {selectors} |" + ) + + if ( + read_supported + and not apply_supported + and desired.get("sync_supported") is not True + ): + public_actions = [ + action + for action in ("list", "get", "verify", "can") + if actions.get(action, {}).get("supported") is True + ] + read_only_rows.append( + f"| `{surface_name}` | {inline_list(public_actions)} | " + f"{selector_contract(actions.get('list', {}))} | " + f"`{surface.get('inventory_script', '-')}` |" + ) lines = [ "# Capabilities", @@ -40,10 +127,26 @@ def render(root: Path) -> str: "", "This table is the operable runtime surface. The standards layer and docs bank intentionally cover more Cloudflare territory than `cfctl` can currently mutate or verify directly.", "", - "| Surface | Read | Apply | Desired State | Standards | Docs Topics | Module |", - "| --- | --- | --- | --- | --- | --- | --- |", + "| Surface | Read | Can | Apply | Verify | Desired State | Standards | Docs Topics | Module |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |", *rows, "", + "## Operation Contract Matrix", + "", + "This matrix is derived from the same catalogs used by `cfctl explain`, `cfctl classify`, `cfctl guide`, and the static verifier. It is the preflight view for deciding whether a surface is read-only, preview-gated, destructive, lane-sensitive, or desired-state-backed.", + "", + "| Surface | Operation | Risk | Preview | Lock | Verify After Apply | Confirmation | Allowed Lanes | Selectors |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |", + *operation_rows, + "", + "## Read-Only Surfaces", + "", + "These surfaces are first-class read surfaces but do not expose `apply` or desired-state `sync` today. Mutation should not be inferred from an inventory script alone.", + "", + "| Surface | Public Actions | List Selectors | Inventory Backend |", + "| --- | --- | --- | --- |", + *read_only_rows, + "", "Composite lifecycle commands:", "- `cfctl hostname verify --file state/hostname/.yaml`", "- `cfctl hostname diff --file state/hostname/.yaml`", diff --git a/scripts/verify_static_contract.sh b/scripts/verify_static_contract.sh index f3af76d..08afdd2 100755 --- a/scripts/verify_static_contract.sh +++ b/scripts/verify_static_contract.sh @@ -660,7 +660,12 @@ assert_contains "config standards compatibility freshness" "Compatibility-date f assert_contains "runtime policy inactive legacy preview cleanup" "cfctl previews purge-inactive-legacy" "${ROOT_DIR}/docs/runtime-policy.md" assert_contains "capabilities operable note" "This table is the operable runtime surface." "${ROOT_DIR}/docs/capabilities.md" assert_contains "capabilities generated note" "_Generated from \`catalog/surfaces.json\` and \`catalog/runtime.json\`." "${ROOT_DIR}/docs/capabilities.md" -assert_contains "capabilities module column" "| Surface | Read | Apply | Desired State | Standards | Docs Topics | Module |" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities module column" "| Surface | Read | Can | Apply | Verify | Desired State | Standards | Docs Topics | Module |" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities contract matrix" "## Operation Contract Matrix" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities destructive contract" "| \`dns.record\` | \`delete\` | \`destructive\` | yes | \`lease\` | yes | \`delete\` | \`dev\`, \`global\` | required: zone; one of: id / name, type |" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities email routing contract" "| \`email.routing_rule\` | \`upsert\` | \`write\` | yes | \`apply\` | yes | \`-\` | \`dev\`, \`global\` | required: zone, name, service |" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities read-only surfaces" "## Read-Only Surfaces" "${ROOT_DIR}/docs/capabilities.md" +assert_contains "capabilities read-only warning" "Mutation should not be inferred from an inventory script alone." "${ROOT_DIR}/docs/capabilities.md" assert_contains "capabilities hostname composite" "Composite lifecycle commands:" "${ROOT_DIR}/docs/capabilities.md" assert_contains "docs bank tracked vs operable note" "Tracked here does not automatically mean operable through \`cfctl\` today" "${ROOT_DIR}/docs/cloudflare-doc-bank.md" assert_contains "docs bank audit logs" "Audit Logs v2" "${ROOT_DIR}/docs/cloudflare-doc-bank.md"