Dispatch Pipeline Key Sync

When to use: CF Worker p24-meta-dispatcher returns HTTP 500 after a secrets-sync.yml run, or dispatch-to-queue.yml fails with auth errors after a SOPS credential rotation.


Problem

CF Worker Wrangler secrets are an isolated silo NOT covered by secrets-sync.yml. Every time monitoring.env.sops changes, the CF Worker retains the old value until manually updated.

Keys that MUST stay in sync between SOPS and CF Worker Wrangler:

KeySOPS fileWrangler secret nameCF Worker
SUPABASE_SERVICE_KEYsecrets/monitoring.env.sopsSUPABASE_SERVICE_KEYp24-meta-dispatcher
QUEUE_API_KEYsecrets/monitoring.env.sopsQUEUE_API_KEYp24-meta-dispatcher

GH_APP_PRIVATE_KEY (used by dispatch-to-queue.yml) is stored as a GH Secret and is also NOT covered by secrets-sync.yml — see section below.

PRIORITIES_REPORTER_KEYS (used by the /priorities endpoint, #5573) is a sixth Wrangler secret in the same isolated silo — see the dedicated section below.

Automation: PR #3082 adds secrets/monitoring.env.sops to the push.paths trigger of deploy-meta-dispatcher.yml, which already contains wrangler secret put steps for all 5 CF Worker secrets. After PR #3082 merges, this sync is automatic.


Symptoms

SymptomLikely cause
dispatch-to-queue.yml fails with “A JSON web token could not be decoded”GH_APP_PRIVATE_KEY GH Secret stale after SOPS rotation of GITHUB_APP_PRIVATE_KEY_B64 in n8n-bms4.env.sops
dispatch-to-queue.yml fails with HTTP 401 from CF WorkerQUEUE_API_KEY GH Secret out of sync with CF Worker Wrangler secret
CF Worker returns HTTP 500 (“Invalid API key”)SUPABASE_SERVICE_KEY in Wrangler is revoked or stale

Confirmation

# 1. Check CF Worker health
$status = (Invoke-WebRequest -Uri "https://p24-meta-dispatcher.radieu.workers.dev/health" -Method GET -UseBasicParsing).StatusCode
Write-Host "CF Worker health: $status"  # expect 200 (after PR #3094 merges)
 
# 2. Check recent dispatch-to-queue.yml runs
gh run list --repo radieu/p24-infra --workflow dispatch-to-queue.yml --limit 5
 
# 3. Check GH Secret last-updated time
gh secret list --repo radieu/p24-infra | Select-String "QUEUE_API_KEY|GH_APP_PRIVATE_KEY"

Fix: Update CF Worker Wrangler secrets (delegate to secret-manager)

Spawn a secret-manager background agent per CLAUDE.md §Role Enforcement.

Agent(
  description: "secret-manager — sync SUPABASE_SERVICE_KEY + QUEUE_API_KEY to CF Worker",
  prompt: """
  Read C:\\Users\\konar\\.claude\\agent-prompts\\roles\\secret-manager.md and adopt that persona.
  Working directory: C:\\code_2026\\p24-infra
  Task:
  1. Extract SUPABASE_SERVICE_KEY from secrets/monitoring.env.sops (safe pattern — never print value)
  2. Run: echo $val | npx wrangler secret put SUPABASE_SERVICE_KEY --name p24-meta-dispatcher
  3. Verify: GET https://p24-meta-dispatcher.radieu.workers.dev/health returns 200
  4. Repeat for QUEUE_API_KEY if also out of sync
  IMPORTANT: do not display any secret values in your response.
  """
)

Fix: Update GH_APP_PRIVATE_KEY GH Secret (after n8n-bms4.env.sops rotation)

When GITHUB_APP_PRIVATE_KEY_B64 is rotated in n8n-bms4.env.sops, delegate to secret-manager:

Task: Extract GITHUB_APP_PRIVATE_KEY_B64 from secrets/n8n-bms4.env.sops,
base64-decode to PEM, set as GH Secret:
  gh secret set GH_APP_PRIVATE_KEY --repo radieu/p24-infra --body $pem

Fix: Update QUEUE_API_KEY GH Secret (after monitoring.env.sops rotation)

Task: Extract QUEUE_API_KEY from secrets/monitoring.env.sops,
set as GH Secret: gh secret set QUEUE_API_KEY --repo radieu/p24-infra --body $val

PRIORITIES_REPORTER_KEYS — the /priorities endpoint’s per-repo keys (#5573)

The scoped priorities-register endpoint (POST /priorities/entry, POST /priorities/log on p24-meta-dispatcher) lets the 15-20 parallel coding agents write dev_r_priorities / dev_r_agent_sessions without each holding the high-privilege SUPABASE_ACCESS_TOKEN Management API PAT that scripts/priorities.py uses. Its auth is a per-repo reporter key — deliberately NOT the shared QUEUE_API_KEY.

KeyWrangler secret nameCF WorkerShape
PRIORITIES_REPORTER_KEYSPRIORITIES_REPORTER_KEYSp24-meta-dispatcherJSON map {"<owner>/<repo>":"<key>", …}

How the auth works: the Worker parses the JSON map once per request and matches the presented Authorization: Bearer <key> (constant-time) against the map’s values. The reporting repo is derived from which key matched — never from the request body — so a session cannot spoof its origin. When the secret is unbound or malformed, the endpoint fails closed (401).

Why a JSON map (Option A), not N separate secrets (Option B): adding a new repo is a secret-only change (edit the one map value + wrangler secret put) with no code edit or redeploy. Per-repo revoke/rotate works either way — drop or replace one entry in the map.

Adding / rotating a repo’s reporter key (delegate to secret-manager)

This is a credential operation — a dev-coder / sys-admin session must NOT do it inline. Spawn a secret-manager agent (per CLAUDE.md §Role Enforcement):

Task (secret-manager):
1. Generate a fresh random key for <owner>/<repo> (e.g. `openssl rand -hex 32`) — never print it.
2. Store it in that repo's own secrets/*.env.sops as PRIORITIES_REPORTER_KEY (same delivery
   mechanism as QUEUE_API_KEY today — the consuming repo reads it from its env and sends it as the
   Bearer token). It is a DIFFERENT value from QUEUE_API_KEY.
3. Merge it into the p24-meta-dispatcher PRIORITIES_REPORTER_KEYS JSON map and push the whole map:
     echo '<json-map>' | npx wrangler secret put PRIORITIES_REPORTER_KEYS --name p24-meta-dispatcher
   (read the existing map first, add/replace the single `"<owner>/<repo>": "<key>"` entry, keep the
   others intact — this is a full-value overwrite, not an append).
4. Verify: a POST /priorities/log with the new key returns 200; a wrong key returns 401.
IMPORTANT: do not display any secret values in your response.

Deploy-workflow wiring (done — #5628): deploy-meta-dispatcher.yml now has a “Bind PRIORITIES_REPORTER_KEYS (aggregate per-repo map)” step that re-pushes this secret on every deploy, so a redeploy no longer risks leaving it clobbered. Because the map has no single SOPS home, the step reconstructs it at deploy time: it decrypts each consumer repo’s own SOPS file (secrets/{et-operational-platform,brandpilot,art-agency,whatsup}.env.sops), reads that repo’s PRIORITIES_REPORTER_KEY, and assembles the { "<owner>/<repo>": "<key>" } JSON map from an explicit label→file table in the workflow (the label must match #5578’s bound values exactly — e.g. radieu/Art-Agency is capitalised unlike its lowercase filename). It fails closed: a missing/empty key aborts the deploy rather than pushing a partial map. The four consumer SOPS paths are on the workflow’s push.paths trigger, so a reporter-key rotation auto-re-pushes the map. Onboarding a new repo to the map is still a secret-manager op (mint key + write its SOPS), plus one new line in that workflow table.


Prevention

PR #3082 addresses this: any push to main touching monitoring.env.sops automatically re-runs the full wrangler secret put sequence via deploy-meta-dispatcher.yml.


Escalation

If CF Worker returns 500 after sync: check wrangler tail --name p24-meta-dispatcher for error details, verify Supabase JWT is valid, or create a [SECRET-REQUEST] issue for a fresh service_role key.


Known outage history

DateDurationRoot causeFix
2026-07-06~4h (16:15–20:45 UTC)SUPABASE_SERVICE_KEY in CF Worker was a revoked sb_secret_* Named API Key; GH_APP_PRIVATE_KEY stale after PR #3017Replaced via wrangler + updated GH Secret (PR #3077)
Session 37~1hQUEUE_API_KEY in CF Worker out of sync after SOPS rotationManual wrangler secret put
Session 44~2hSUPABASE_SERVICE_KEY in SOPS had 119-char corrupted value (Windows encoding)PR #2961 fixed SOPS; PR #2956 fixed CF Worker