| name | cf-token-scope |
| description | This skill should be used to widen an existing Cloudflare API token's scope via Playwright dashboard automation, then run the ADR-130 retained-scope probe set verifying the target scope was added and none was dropped. |
cf-token-scope
Close the capability gap ADR-130 leaves open: there is no first-party path for
Cloudflare API-token scope changes, so each one is an ad-hoc dashboard trip
(third on record โ #6657 DNS, #6649 R2, #6755 Config Rules). This skill widens a
token via Playwright MCP dashboard automation and makes the ADR-130
retained-scope check a deterministic, fail-closed command.
The widen mutates a live production credential that four concerns depend on. A
dashboard "save" that replaces rather than appends scopes silently breaks
cache rules, WAF, single redirects, transform rules, and account bulk redirects
at once โ so the retained-scope probe is the load-bearing half, not the widen.
Usage
The deterministic core is a read-only probe script,
cf-token-scope.sh. It only ever probes โ run it as
often as needed; it mutates nothing.
bash plugins/soleur/skills/cf-token-scope/scripts/cf-token-scope.sh
bash plugins/soleur/skills/cf-token-scope/scripts/cf-token-scope.sh \
--target-entrypoint http_config_settings
bash plugins/soleur/skills/cf-token-scope/scripts/cf-token-scope.sh --dry-run
The token / zone / account (CF_API_TOKEN_RULESETS, CF_ZONE_ID,
CF_ACCOUNT_ID) and Doppler config (soleur / prd_terraform) are hardcoded:
the ADR-130 retained-scope URL set is meaningful only for the rulesets token, so
a --token-var knob pointed elsewhere would run the wrong URLs against the wrong
token.
Execution โ the 3-step widen flow
-
Pre-widen baseline. Run the probe. Before the widen, the target
entrypoint reads 403 (scope absent) while the known-granted controls read
the authorized signal. Record this output โ the added-scope check in step 3
compares against it.
-
Widen (Playwright MCP). Follow
widen-playbook.md: decide widen-vs-mint per
ADR-130, navigate to https://dash.cloudflare.com/profile/api-tokens, hand
off only the login/MFA gate to the operator, then edit the token โ
three-dot menu โ Edit โ Add more โ select the permission (same API family) โ
Continue to summary โ Update token. Editing permissions does not rotate
the token value, so no Doppler write follows.
The widen transits a full-power dashboard session (the cookie is an
account-wide bearer). Do not dump browser_network_requests /
browser_console_messages to files, scope screenshots to the edit control,
use snapshot-only navigation, and never call browser_evaluate with a
filename โ see the playbook's leak constraints.
-
Post-widen verification. Re-run the probe with
--target-entrypoint <phase>. Success = the target is currently authorized AND
every retained control stayed authorized AND the account-scheme control is an
authorized 200 โ exit 0, PASS: target scope present + PASS: no scope dropped. A single run cannot observe the 403 โ authorized transition, so
the tool prints "present", not "added" โ confirm the transition against your
step-1 baseline.
Probe-set contract (three-layer fail-closed classifier)
The probe set is the ADR-130 four-probe retained set plus Zone WAF โ four zone
phases (http_config_settings, http_request_dynamic_redirect,
http_request_cache_settings, http_request_firewall_custom) and one account
list (accounts/<acct>/rulesets). The classifier captures each response body
(not -o /dev/null) and decides in three layers:
- Status โ
403 / 000 / 5xx / empty / no-newline / non-numeric = FAIL.
- Body-shape โ a
200 passes only when the body has success == true AND
.result is an array. A degraded 200 ({"success":false,...} OR
{"success":true,"result":null}) is a FAIL. Never key on .result | length โ
jq reads null as 0 and would pass.
- Per-scheme control โ the account list is the account-scheme control and
must be an authorized
200 (an account 404 = FAIL). A zone 404 (ADR-130's
"phase exists, empty") is trusted only for http_config_settings โ the one
phase whose 403-on-missing-scope semantics ADR-130 empirically verified โ and
only under an authorized zone control (http_request_dynamic_redirect). Every
other phase's 404 fails closed: 403-on-missing is unverified there, so a
dropped scope that returned 404 must not read as green.
Exit codes
0 โ every retained scope authorized (and the target, if given).
2 โ usage error, missing prerequisite (curl / doppler / jq), or an absent Doppler secret.
3 โ probe failed: a scope was dropped, degraded, or denied.
Sharp Edges
- The probe set is a CANARY for the whole-list REPLACE failure mode, not
exhaustive per-permission coverage. A dashboard save that replaces (not
appends) drops all scopes at once โ every probe catches it. Zone WAF
(
http_request_firewall_custom) is now probed, but Transform Rules
(http_response_headers_transform) and account Filter Lists are not โ a
surgical single-permission drop of one of those can still pass. Extend the
entrypoint set if a future threat model needs per-permission coverage.
- The probe attests read reachability, not
:Edit retention. It issues GET,
so a dashboard REPLACE that re-adds a phase as Read-only (a one-click
operator error โ the CF dropdown lists Read/Edit side-by-side) returns an
authorized 200 and reads as green while the write/hijack capability was
dropped. Visually confirm the widened permission is Edit, not Read.
- Probe, never trust the Cloudflare UI permission label โ it is named
inconsistently across surfaces. The target entrypoint returning non-403 is the
ground truth (ADR-130).
- Update the scope ledger. After a widen, add the new permission to the
apps/web-platform/infra/variables.tf description of the widened token (ADR-130's
scope ledger), in the feature PR that consumes the widen.
- Never print the Bearer token. The script passes it from a private fd (not
argv),
--dry-run leaves it unexpanded, and it runs no set -x. When driving
the widen, keep the full-power-session leak constraints above.
- New phase โ enumerate before apply. If the widen enables a new ruleset
phase, confirm its entrypoint is
404/empty before any infra apply, or a
whole-list ruleset create clobbers dashboard rules (ADR-130, ADR-136).
Related
- ADR-130 โ widen-vs-narrow-alias rule, the four-probe retained-scope set, and the capability gap this skill closes.
- ADR-136 โ the standing pre-apply entrypoint-enumeration gate.
- provision-cloudflare โ mints tenant tokens (a distinct function; requires
User API Tokens:Edit, which no Soleur token holds).
- widen-playbook.md โ the Playwright MCP click-path and leak constraints.
- Learnings:
2026-03-21-cloudflare-api-token-permission-editing.md (the Playwright widen path), 2026-07-23-live-api-fail-closed-guard-counts-degraded-200-as-empty-and-control-probe-must-cover-every-scheme.md (the three-layer classifier).