koala73/worldmonitor · error · Error

Expected one enabled block rule named ${description}

Error message

Expected one enabled block rule named ${description}

What it means

planAgentReadiness expects the Cloudflare firewall ruleset to contain exactly one rule per description in BLOCK_RULES, and that rule must be an enabled block rule. It throws when the count of rules matching a description is not 1, or the single match is not action 'block' or is disabled. This guards against duplicate or drifted rules before planning changes.

Solutions

  1. Open the Cloudflare dashboard (or GET the phase entrypoint ruleset) and inspect rules matching the description; delete duplicates so exactly one remains.
  2. Re-enable the rule and/or set its action back to 'block' if it was disabled or changed.
  3. If no rule exists, create the expected block rule (or run the setup path) before running planAgentReadiness.
  4. Re-run with --plan after the ruleset matches the expected one-rule-per-description shape.
Defensive patterns

Strategy: try-catch

Validate before calling

const descriptions = BLOCK_RULES; // import from the script if exposed
const grouped = Object.groupBy(firewall.rules, (r) => r.description);
for (const d of descriptions) {
  const m = grouped[d] ?? [];
  if (m.length !== 1 || m[0].action !== 'block' || m[0].enabled === false) {
    throw new Error(`ruleset drift on rule "${d}"; reconcile before planning`);
  }
}
planAgentReadiness(firewall);

Try / catch

try {
  const changes = planAgentReadiness(firewall);
} catch (e) {
  if (e.message.startsWith('Expected one enabled block rule named')) {
    console.error('Ruleset drifted from expected shape; inspect and reconcile in the dashboard.');
    process.exitCode = 2;
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: The zone's firewall phase has zero rules with a BLOCK_RULES description, two or more rules share the same description (e.g. from concurrent manual edits or double-apply), or the matching rule has action changed to something other than 'block' or enabled set to false.

Common situations: Someone manually edited or disabled the block rule in the Cloudflare dashboard, the apply script was run twice creating duplicates, or the ruleset was rebuilt from a different config losing the rule.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/577657392fb48c4d. Report an issue: GitHub.

Appendix: source

Thrown at scripts/cloudflare-agent-readiness.mjs:23

import { cloudflareRequest, resolveToken, resolveZoneId } from './cloudflare-cache-rule.mjs';

const FIREWALL_PHASE = 'http_request_firewall_custom';
const BLOCK_RULES = ['Block API Bots', 'Block Scriptlike UAs'];

// This script owns the firewall half of agent readiness: the JSON block
// responses on the two bot rules. The cache half — keeping the declared AI
// agents off the shared HTML entry for `/` so middleware.ts can hand them
// /home.md — lives in scripts/cloudflare-cache-rule.mjs since #7804, as a
// carve-out inside the one managed document rule. The UA-keyed bypass this
// script used to append LAST in the cache phase is retired there
// (RETIRED_CACHE_RULES): two scripts each insisting on the last position would
// have moved each other's rule on every run.
export function planAgentReadiness(firewall) {
  const changes = [];
  for (const description of BLOCK_RULES) {
    const matches = firewall.rules.filter((rule) => rule.description === description);
    if (matches.length !== 1 || matches[0].action !== 'block' || matches[0].enabled === false) {
      throw new Error(`Expected one enabled block rule named ${description}`);
    }
    const rule = matches[0];
    const response = {
      status_code: 403,
      content_type: 'application/json',
      content: JSON.stringify(policy.blockedResponse),
    };
    if (!isDeepStrictEqual(rule.action_parameters?.response, response)) {
      const definition = Object.fromEntries(Object.entries(rule).filter(([key]) =>
        !['id', 'version', 'last_updated'].includes(key)));
      changes.push({
        phase: FIREWALL_PHASE, rulesetId: firewall.id, ruleId: rule.id,
        description, method: 'PATCH',
        body: { ...definition, action_parameters: { ...rule.action_parameters, response } },
      });
    }
  }

View on GitHub (pinned to 7d06c8633d)