{"record":{"id":"577657392fb48c4d","repo":"koala73/worldmonitor","slug":"expected-one-enabled-block-rule-named-description","errorCode":null,"errorMessage":"Expected one enabled block rule named ${description}","messagePattern":"Expected one enabled block rule named (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloudflare-agent-readiness.mjs","lineNumber":23,"sourceCode":"import { cloudflareRequest, resolveToken, resolveZoneId } from './cloudflare-cache-rule.mjs';\n\nconst FIREWALL_PHASE = 'http_request_firewall_custom';\nconst BLOCK_RULES = ['Block API Bots', 'Block Scriptlike UAs'];\n\n// This script owns the firewall half of agent readiness: the JSON block\n// responses on the two bot rules. The cache half — keeping the declared AI\n// agents off the shared HTML entry for `/` so middleware.ts can hand them\n// /home.md — lives in scripts/cloudflare-cache-rule.mjs since #7804, as a\n// carve-out inside the one managed document rule. The UA-keyed bypass this\n// script used to append LAST in the cache phase is retired there\n// (RETIRED_CACHE_RULES): two scripts each insisting on the last position would\n// have moved each other's rule on every run.\nexport function planAgentReadiness(firewall) {\n  const changes = [];\n  for (const description of BLOCK_RULES) {\n    const matches = firewall.rules.filter((rule) => rule.description === description);\n    if (matches.length !== 1 || matches[0].action !== 'block' || matches[0].enabled === false) {\n      throw new Error(`Expected one enabled block rule named ${description}`);\n    }\n    const rule = matches[0];\n    const response = {\n      status_code: 403,\n      content_type: 'application/json',\n      content: JSON.stringify(policy.blockedResponse),\n    };\n    if (!isDeepStrictEqual(rule.action_parameters?.response, response)) {\n      const definition = Object.fromEntries(Object.entries(rule).filter(([key]) =>\n        !['id', 'version', 'last_updated'].includes(key)));\n      changes.push({\n        phase: FIREWALL_PHASE, rulesetId: firewall.id, ruleId: rule.id,\n        description, method: 'PATCH',\n        body: { ...definition, action_parameters: { ...rule.action_parameters, response } },\n      });\n    }\n  }\n","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/cloudflare-agent-readiness.mjs#L5-L41","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Open the Cloudflare dashboard (or GET the phase entrypoint ruleset) and inspect rules matching the description; delete duplicates so exactly one remains.","Re-enable the rule and/or set its action back to 'block' if it was disabled or changed.","If no rule exists, create the expected block rule (or run the setup path) before running planAgentReadiness.","Re-run with --plan after the ruleset matches the expected one-rule-per-description shape."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const descriptions = BLOCK_RULES; // import from the script if exposed\nconst grouped = Object.groupBy(firewall.rules, (r) => r.description);\nfor (const d of descriptions) {\n  const m = grouped[d] ?? [];\n  if (m.length !== 1 || m[0].action !== 'block' || m[0].enabled === false) {\n    throw new Error(`ruleset drift on rule \"${d}\"; reconcile before planning`);\n  }\n}\nplanAgentReadiness(firewall);","typeGuard":null,"tryCatchPattern":"try {\n  const changes = planAgentReadiness(firewall);\n} catch (e) {\n  if (e.message.startsWith('Expected one enabled block rule named')) {\n    console.error('Ruleset drifted from expected shape; inspect and reconcile in the dashboard.');\n    process.exitCode = 2;\n    return;\n  }\n  throw e;\n}","preventionTips":["Manage these rules exclusively through the script; forbid manual dashboard edits.","Run --check in CI to detect drift early.","Use audit logs to find who duplicated/disabled rules.","Keep apply runs serialized (single pipeline, no parallel invocations)."],"tags":["cloudflare","firewall","drift"],"backgroundTag":"schema-validation-failed","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}