Unified Auth + Cluster-as-Driver Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Collapse authentication onto a single Bearer API-key credential, make the cluster driver an ordinary always-registered driver, and stop a sibling push from making the hub check itself — without building the source router (deferred to Slice B).
Architecture: The API-key middleware becomes the one auth gate; push_to_hub sends a Bearer key instead of HMAC. ClusterDriver is registered unconditionally and declares skip_checkers = True, which the webhook path forwards into the pipeline run so the orchestrator drops the CHECK stage for cluster payloads. Dead CLUSTER_ROLE and WEBHOOK_SECRET_CLUSTER are retired in favour of HUB_API_KEY. The per-driver HMAC scaffold stays in the tree, dormant.
Tech Stack: Django 5.2, pytest + pytest-django, bats (shell tests), uv.
Design doc: docs/plans/2026-07-23-unified-auth-cluster-driver-design.md
Conventions (from AGENTS.md): absolute imports; 100% branch coverage on changed code; uv run pytest, uv run black ., uv run ruff check .; bats via ./bin/tests/test_helper/bats-core/bin/bats <file>. Commit frequently. TDD throughout.
Task 1: create_api_key management command
Files:
- Create:
config/management/__init__.py(empty) - Create:
config/management/commands/__init__.py(empty) - Create:
config/management/commands/create_api_key.py - Test:
config/_tests/commands/__init__.py(empty, if missing) andconfig/_tests/commands/test_create_api_key.py
Step 1: Write the failing test
# config/_tests/commands/test_create_api_key.py
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
from config.models import APIKey
class CreateApiKeyCommandTests(TestCase):
def test_mints_key_and_prints_raw_token_once(self):
out = StringIO()
call_command("create_api_key", "--name", "agent web-03", stdout=out)
output = out.getvalue()
# Exactly one key persisted, hash only (40 hex bytes = token_hex(20)).
key = APIKey.objects.get(name="agent web-03")
self.assertEqual(len(key.key), 64) # sha256 hex digest
# The raw 40-char token is printed once and is NOT the stored hash.
self.assertIn(key.prefix, output)
self.assertNotIn(key.key, output) # never print the digest
def test_requires_name(self):
with self.assertRaises(Exception):
call_command("create_api_key")
Step 2: Run test to verify it fails
Run: uv run pytest config/_tests/commands/test_create_api_key.py -v Expected: FAIL — Unknown command: 'create_api_key'.
Step 3: Write minimal implementation
# config/management/commands/create_api_key.py
"""Mint an API key and print the raw token once.
Usage:
python manage.py create_api_key --name "agent web-03"
"""
from django.core.management.base import BaseCommand, CommandError
from config.models import APIKey
class Command(BaseCommand):
help = "Create an API key and print its raw token (shown once, never stored)."
def add_arguments(self, parser):
parser.add_argument("--name", required=True, help="Human-readable label for the key.")
def handle(self, *args, **options):
name = options["name"].strip()
if not name:
raise CommandError("--name must not be empty.")
api_key = APIKey.objects.create(name=name)
raw = getattr(api_key, "_raw_key", "")
if not raw: # pragma: no cover - defensive; save() always sets it on create
raise CommandError("Key generation failed.")
self.stdout.write(self.style.SUCCESS(f"API key '{name}' created."))
self.stdout.write("")
self.stdout.write("Raw token (shown once — store it now):")
self.stdout.write(f" {raw}")
self.stdout.write("")
self.stdout.write(f"Prefix (safe to reference): {api_key.prefix}")
Also create the empty __init__.py files listed above.
Step 4: Run test to verify it passes
Run: uv run pytest config/_tests/commands/test_create_api_key.py -v Expected: PASS (both tests).
Step 5: Commit
git add config/management config/_tests/commands
git commit -m "feat(config): add create_api_key management command"
Task 1b: Admin reveals the raw API key once on creation
Context: APIKeyAdmin already exists and is registered (config/admin.py, via ConfigAppConfig.ready()), with a masked_key display. The gap: creating a key in admin never shows the raw token, so admin can’t actually provision an agent key. The model sets the raw token on the transient _raw_key during save(); capture it in save_model and flash it once.
Files:
- Modify:
config/admin.py(APIKeyAdmin) - Test:
config/_tests/test_api_key_admin.py(extend)
Step 1: Write the failing test
def test_save_model_flashes_raw_token_once_on_create(self):
from unittest.mock import MagicMock, patch
from django.contrib import admin
model_admin = admin.site._registry[APIKey]
request = MagicMock()
obj = APIKey(name="agent web-03")
with patch("config.admin.messages") as messages:
model_admin.save_model(request, obj, form=MagicMock(), change=False)
# The raw token (40 hex chars) is surfaced exactly once via messages.success/warning.
flashed = " ".join(str(c) for c in messages.mock_calls)
assert obj._raw_key in flashed
def test_save_model_no_token_on_edit(self):
from unittest.mock import MagicMock, patch
from django.contrib import admin
model_admin = admin.site._registry[APIKey]
obj = APIKey.objects.create(name="existing")
request = MagicMock()
with patch("config.admin.messages") as messages:
model_admin.save_model(request, obj, form=MagicMock(), change=True)
assert not any("Raw token" in str(c) for c in messages.mock_calls)
Step 2: Run to verify it fails
Run: uv run pytest config/_tests/test_api_key_admin.py -k save_model -v Expected: FAIL — APIKeyAdmin has no save_model.
Step 3: Implementation
In config/admin.py, add the messages import and a save_model to APIKeyAdmin:
from django.contrib import messages
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
raw = getattr(obj, "_raw_key", "")
if not change and raw:
messages.warning(
request,
f"Raw token for '{obj.name}' (shown once — copy it now): {raw}",
)
Step 4: Run to verify it passes
Run: uv run pytest config/_tests/test_api_key_admin.py -v Expected: PASS.
Step 5: Commit
git add config/admin.py config/_tests/test_api_key_admin.py
git commit -m "feat(admin): reveal raw API key once when created via admin"
Task 2: skip_checkers on drivers + orchestrator honours it
Files:
- Modify:
apps/alerts/drivers/base.py:66-71(add class attribute) - Modify:
apps/alerts/drivers/cluster.py(set attribute onClusterDriver) - Modify:
apps/orchestration/orchestrator.py:262-265(stage selection) - Test:
apps/orchestration/_tests/(mirror; use the existing orchestrator test module) andapps/alerts/_tests/drivers/test_cluster.py
Step 1: Write the failing tests
Driver attribute (add to the cluster driver test module):
# apps/alerts/_tests/drivers/test_cluster.py (add to existing file)
from apps.alerts.drivers.cluster import ClusterDriver
from apps.alerts.drivers.base import BaseAlertDriver
def test_cluster_declares_skip_checkers():
assert ClusterDriver().skip_checkers is True
def test_base_driver_defaults_skip_checkers_false():
assert BaseAlertDriver.skip_checkers is False
Orchestrator honours skip_checkers in the payload — add a unit test asserting the stage list. Locate the existing orchestrator test module (search run_pipeline), and add:
def test_skip_checkers_payload_omits_check_stage(self):
from apps.orchestration.orchestrator import PipelineOrchestrator, STAGE_ORDER
from apps.orchestration.models import PipelineStage
orch = PipelineOrchestrator()
result = orch.run_pipeline(
payload={"skip_checkers": True, "alerts": []},
source="cluster",
)
run = result_to_run(result) # or fetch PipelineRun by result.run_id
stages = [e.stage for e in run.stage_executions.all()]
assert PipelineStage.CHECK not in stages
assert PipelineStage.NOTIFY in stages
Note: match the assertion style already used in the orchestrator tests (they may assert on
result.stages_completed). Prefer assertingPipelineStage.CHECK not in result.stages_completedif that attribute exists — read the existing test first.
Step 2: Run tests to verify they fail
Run: uv run pytest apps/alerts/_tests/drivers/test_cluster.py apps/orchestration/_tests -k "skip_checkers" -v Expected: FAIL — AttributeError: skip_checkers and CHECK still present.
Step 3: Write minimal implementation
apps/alerts/drivers/base.py — add under the existing class attributes:
class BaseAlertDriver(ABC):
"""Abstract base class for alert source drivers."""
name: str = "base"
signature_header: str | None = None
signature_algorithm: str = "sha256"
# Slice A interim rule (Slice B's router will generalise this): payloads from
# this source already carry their own diagnostics, so the receiving instance
# must not re-run local checkers against itself.
skip_checkers: bool = False
apps/alerts/drivers/cluster.py — on ClusterDriver, next to signature_header:
name = "cluster"
signature_header = "X-Cluster-Signature"
skip_checkers = True
apps/orchestration/orchestrator.py — replace the stage-selection block:
# Determine which stages to run.
checks_only = payload.get("checks_only", False)
skip_checkers = payload.get("skip_checkers", False)
if checks_only:
active_stages = [PipelineStage.CHECK]
final_status = PipelineStatus.CHECKED
elif skip_checkers:
active_stages = [s for s in STAGE_ORDER if s != PipelineStage.CHECK]
final_status = PipelineStatus.NOTIFIED
else:
active_stages = STAGE_ORDER
final_status = PipelineStatus.NOTIFIED
Step 4: Run tests to verify they pass
Run: uv run pytest apps/alerts/_tests/drivers/test_cluster.py apps/orchestration/_tests -k "skip_checkers" -v Expected: PASS.
Step 5: Commit
git add apps/alerts/drivers/base.py apps/alerts/drivers/cluster.py apps/orchestration/orchestrator.py apps/alerts/_tests apps/orchestration/_tests
git commit -m "feat(pipeline): drivers can declare skip_checkers; orchestrator honours it"
Task 3: Register ClusterDriver unconditionally
Files:
- Modify:
apps/alerts/drivers/__init__.py:44-55(remove theCLUSTER_ENABLEDgate) - Test:
apps/alerts/_tests/drivers/test_registry.py(create or extend)
Step 1: Write the failing test
# apps/alerts/_tests/drivers/test_registry.py
from apps.alerts.drivers import DRIVER_REGISTRY
from apps.alerts.drivers.cluster import ClusterDriver
def test_cluster_is_always_registered():
assert DRIVER_REGISTRY.get("cluster") is ClusterDriver
Step 2: Run test to verify it fails
Run: uv run pytest apps/alerts/_tests/drivers/test_registry.py -v Expected: FAIL if the test environment has CLUSTER_ENABLED unset (cluster absent).
Step 3: Write minimal implementation
In apps/alerts/drivers/__init__.py, add "cluster": ClusterDriver directly to DRIVER_REGISTRY and delete _register_cluster_driver() and its call:
DRIVER_REGISTRY: dict[str, type[BaseAlertDriver]] = {
"alertmanager": AlertManagerDriver,
"grafana": GrafanaDriver,
"pagerduty": PagerDutyDriver,
"datadog": DatadogDriver,
"newrelic": NewRelicDriver,
"opsgenie": OpsGenieDriver,
"zabbix": ZabbixDriver,
"cluster": ClusterDriver,
"generic": GenericWebhookDriver,
}
(Keep generic last — detect_driver still skips it until the end.)
Step 4: Run test to verify it passes
Run: uv run pytest apps/alerts/_tests/drivers/ -v Expected: PASS. Also check no test relied on cluster being absent when CLUSTER_ENABLED=0 — search _register_cluster_driver and CLUSTER_ENABLED in apps/alerts/_tests and update/remove stale cases.
Step 5: Commit
git add apps/alerts/drivers/__init__.py apps/alerts/_tests/drivers
git commit -m "refactor(alerts): register ClusterDriver unconditionally"
Task 4: Webhook path forwards skip_checkers
Files:
- Modify:
apps/alerts/views.py(after the driver is resolved, before enqueue ~line 58-90) - Test:
apps/alerts/_tests/views/test_webhook.py(extend)
Step 1: Write the failing test
Assert that a cluster payload enqueues a run whose payload carries skip_checkers=True. Mock run_pipeline_task.delay and inspect kwargs. Read the existing webhook test for the established mocking pattern first.
def test_cluster_webhook_sets_skip_checkers(self):
from unittest.mock import patch
payload = {"source": "cluster", "instance_id": "web-03", "alerts": []}
with patch("apps.orchestration.tasks.run_pipeline_task.delay") as delay:
delay.return_value.id = "abc"
self.client.post(
"/alerts/webhook/cluster/",
data=payload, content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.raw_key}", # from a created APIKey
)
sent_payload = delay.call_args.kwargs["payload"]
assert sent_payload.get("skip_checkers") is True
If
API_KEY_AUTH_ENABLEDdefaults to on in tests, create anAPIKeyinsetUpand pass its raw token, oroverride_settings(API_KEY_AUTH_ENABLED=False)for this unit — match what the existing webhook tests already do.
Step 2: Run test to verify it fails
Run: uv run pytest apps/alerts/_tests/views/test_webhook.py -k skip_checkers -v Expected: FAIL — skip_checkers absent from the enqueued payload.
Step 3: Write minimal implementation
In apps/alerts/views.py, after sig_driver is resolved (the block that sets sig_driver from get_driver/detect_driver), add:
# Interim (Slice A): a driver that already carries diagnostics tells the
# pipeline to skip local checkers. Slice B replaces this with routing.
if sig_driver and getattr(sig_driver, "skip_checkers", False):
payload["skip_checkers"] = True
Place it before both the Celery-enqueue branch and the synchronous fallback so both see the flag.
Step 4: Run test to verify it passes
Run: uv run pytest apps/alerts/_tests/views/test_webhook.py -v Expected: PASS.
Step 5: Verify the synchronous fallback
Read apps/alerts/services.py AlertOrchestrator.process_webhook. If it runs checkers directly, ensure it also respects payload["skip_checkers"]; if it does not run checkers at all, note that in the commit message and move on (no change needed).
Step 6: Commit
git add apps/alerts/views.py apps/alerts/_tests/views/test_webhook.py
git commit -m "feat(alerts): webhook forwards driver skip_checkers into the run"
Task 5: push_to_hub authenticates with a Bearer API key
Files:
- Modify:
apps/alerts/management/commands/push_to_hub.py:55and the header block (~104-108) - Test:
apps/alerts/_tests/commands/test_push_to_hub.py(extend)
Step 1: Write the failing test
def test_push_sends_bearer_key_and_no_signature(self):
from unittest.mock import patch, MagicMock
from django.test import override_settings
with override_settings(HUB_URL="https://hub.example.com", HUB_API_KEY="tok123"):
with patch("apps.alerts.management.commands.push_to_hub.safe_urlopen") as urlopen:
resp = MagicMock(); resp.status = 202; resp.read.return_value = b"{}"
urlopen.return_value.__enter__.return_value = resp
call_command("push_to_hub")
request = urlopen.call_args.args[0]
assert request.headers["Authorization"] == "Bearer tok123"
assert "X-cluster-signature" not in {k.lower(): v for k, v in request.headers.items()}
def test_push_errors_without_api_key(self):
from django.test import override_settings
from django.core.management.base import CommandError
with override_settings(HUB_URL="https://hub.example.com", HUB_API_KEY=""):
with self.assertRaises(CommandError):
call_command("push_to_hub")
urllib.request.Requestcapitalises header keys (Authorization). Assert with the exact casingRequeststores, or normalise as above. Read the existing push_to_hub test to reuse itssafe_urlopenmock.
Step 2: Run tests to verify they fail
Run: uv run pytest apps/alerts/_tests/commands/test_push_to_hub.py -k "bearer or without_api_key" -v Expected: FAIL — still reads WEBHOOK_SECRET_CLUSTER / sends HMAC.
Step 3: Write minimal implementation
Replace the secret line (push_to_hub.py:55):
api_key = getattr(settings, "HUB_API_KEY", "")
if not api_key:
raise CommandError(
"HUB_API_KEY is not configured. Set it in .env to enable agent mode."
)
Replace the header-building block (drop HMAC):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
Remove now-unused imports (hmac, hashlib) if nothing else uses them — run ruff to confirm.
Step 4: Run tests to verify they pass
Run: uv run pytest apps/alerts/_tests/commands/test_push_to_hub.py -v Expected: PASS.
Step 5: Commit
git add apps/alerts/management/commands/push_to_hub.py apps/alerts/_tests/commands/test_push_to_hub.py
git commit -m "feat(cluster): push_to_hub authenticates with Bearer HUB_API_KEY, drops HMAC"
Task 6: Settings — add HUB_API_KEY, retire WEBHOOK_SECRET_CLUSTER & CLUSTER_ROLE
Files:
- Modify:
config/settings.py:211-214 - Test: covered indirectly; add a settings smoke assertion in
config/_tests/if a settings test module exists.
Step 1: Implementation
In config/settings.py, replace the cluster block:
CLUSTER_ENABLED = os.environ.get("CLUSTER_ENABLED", "0") == "1"
HUB_URL = os.environ.get("HUB_URL", "")
INSTANCE_ID = os.environ.get("INSTANCE_ID", "")
HUB_API_KEY = os.environ.get("HUB_API_KEY", "")
Delete the WEBHOOK_SECRET_CLUSTER line. Grep the tree for any remaining WEBHOOK_SECRET_CLUSTER or CLUSTER_ROLE reader and confirm only intended edits remain:
Run: git grep -n "WEBHOOK_SECRET_CLUSTER\|CLUSTER_ROLE" -- ':!docs/plans/*' Expected after all tasks: no non-test, non-historical hits.
Step 2: Run the full suite
Run: uv run pytest -q Expected: PASS (fix any test that referenced WEBHOOK_SECRET_CLUSTER).
Step 3: Commit
git add config/settings.py
git commit -m "feat(config): add HUB_API_KEY setting, retire WEBHOOK_SECRET_CLUSTER"
Task 7: Preflight cluster checks
Files:
- Modify:
apps/checkers/preflight/checks.py:383-430(check_cluster_coherence) - Test:
apps/checkers/_tests/preflight/test_checks.py(update cluster cases)
Step 1: Update the failing tests
Change the existing cluster-coherence tests to expect HUB_API_KEY instead of WEBHOOK_SECRET_CLUSTER: agent mode warns when HUB_API_KEY is empty; hub mode no longer errors on a missing secret (auth is now the API-key middleware + a created APIKey, which preflight cannot see, so downgrade the hub check to informational).
Step 2: Run to verify they fail
Run: uv run pytest apps/checkers/_tests/preflight/test_checks.py -k cluster -v Expected: FAIL.
Step 3: Implementation
Rewrite check_cluster_coherence to read HUB_API_KEY for agent mode and drop the WEBHOOK_SECRET_CLUSTER reads:
def check_cluster_coherence() -> list[CheckResult]:
results: list[CheckResult] = []
hub_url = getattr(settings, "HUB_URL", "")
cluster_enabled = getattr(settings, "CLUSTER_ENABLED", False)
api_key = getattr(settings, "HUB_API_KEY", "")
instance_id = getattr(settings, "INSTANCE_ID", "")
if hub_url and cluster_enabled:
return [CheckResult(level="error",
message="Cluster conflict: both HUB_URL and CLUSTER_ENABLED=1",
hint="An instance cannot be both agent and hub.")]
if hub_url: # agent
if not api_key:
results.append(CheckResult(level="warn",
message="Agent mode: HUB_API_KEY is empty",
hint="Set it to the token created on the hub via create_api_key."))
if not instance_id:
results.append(CheckResult(level="warn",
message="Agent mode: INSTANCE_ID is empty",
hint="Set it to identify this agent."))
if cluster_enabled and not hub_url: # hub
results.append(CheckResult(level="ok",
message="Hub mode: ensure API_KEY_AUTH_ENABLED=1 and an APIKey exists for agents"))
if not results:
role = "agent" if hub_url else ("hub" if cluster_enabled else "standalone")
results.append(CheckResult(level="ok", message=f"Cluster: {role}"))
return results
Step 4: Run to verify they pass
Run: uv run pytest apps/checkers/_tests/preflight/test_checks.py -k cluster -v Expected: PASS.
Step 5: Commit
git add apps/checkers/preflight/checks.py apps/checkers/_tests/preflight/test_checks.py
git commit -m "feat(preflight): cluster checks use HUB_API_KEY, not WEBHOOK_SECRET_CLUSTER"
Task 8: Installer cluster step
Files:
- Modify:
bin/install/cluster.sh - Test:
bin/tests/(addbin/tests/test_cluster.batsif none, or extend an existing one)
Step 1: Write the failing bats test
Assert the script no longer writes CLUSTER_ROLE or WEBHOOK_SECRET_CLUSTER, and that hub role mentions create_api_key. Since the script is interactive, test the non-interactive assertions you can (e.g. --help/syntax, and grep the script for the removed tokens). Minimum viable regression:
@test "cluster.sh no longer references CLUSTER_ROLE or WEBHOOK_SECRET_CLUSTER" {
run grep -E "CLUSTER_ROLE|WEBHOOK_SECRET_CLUSTER" "$BIN_DIR/install/cluster.sh"
assert_failure # grep finds nothing → exit 1
}
@test "cluster.sh references HUB_API_KEY and create_api_key" {
run grep -q "HUB_API_KEY" "$BIN_DIR/install/cluster.sh"; assert_success
run grep -q "create_api_key" "$BIN_DIR/install/cluster.sh"; assert_success
}
Step 2: Run to verify it fails
Run: ./bin/tests/test_helper/bats-core/bin/bats bin/tests/test_cluster.bats Expected: FAIL.
Step 3: Implementation
Rewrite bin/install/cluster.sh role handling:
- Drop the
CLUSTER_ROLEprompt anddotenv_set ... CLUSTER_ROLE. - Keep role selection locally (agent/hub/both) only to branch prompts.
- Agent/both: prompt
HUB_URL,INSTANCE_ID, andHUB_API_KEY(masked) instead ofWEBHOOK_SECRET_CLUSTER. - Hub/both: set
CLUSTER_ENABLED=1; print guidance to mint a key:uv run python manage.py create_api_key --name "<agent-name>"and paste the token into each agent’sHUB_API_KEY. - Keep the agent
push_to_hub --dry-runverification.
Step 4: Run to verify it passes
Run: ./bin/tests/test_helper/bats-core/bin/bats bin/tests/test_cluster.bats Expected: PASS. Also bash -n bin/install/cluster.sh.
Step 5: Commit
git add bin/install/cluster.sh bin/tests/test_cluster.bats
git commit -m "feat(installer): cluster step uses HUB_API_KEY + create_api_key, drops dead knobs"
Task 9: .env.sample
Files:
- Modify:
.env.sample:18,38-43
Step 1: Implementation
- Line 18:
API_KEY_AUTH_ENABLED=1. - Replace the cluster block:
# Cluster (multi-instance) — see docs. An agent pushes to a hub; a hub accepts pushes.
# Agent: set HUB_URL + INSTANCE_ID + HUB_API_KEY (token created on the hub).
# Hub: set CLUSTER_ENABLED=1 and create an APIKey via `manage.py create_api_key`.
# HUB_URL=https://monitoring-hub.example.com
# CLUSTER_ENABLED=0
# INSTANCE_ID=
# HUB_API_KEY=
Step 2: Verify
Run: git grep -n "WEBHOOK_SECRET_CLUSTER\|CLUSTER_ROLE" .env.sample Expected: no hits.
Step 3: Commit
git add .env.sample
git commit -m "docs(env): default API_KEY_AUTH_ENABLED=1; cluster uses HUB_API_KEY"
Task 10: set_production.sh ensures auth on
Files:
- Modify:
bin/set_production.sh(add step after DEBUG=0) - Test:
bin/tests/test_set_production.bats(extend)
Step 1: Write the failing test
@test "set_production.sh sets API_KEY_AUTH_ENABLED=1" {
local tmp; tmp="$(mktemp -d)"
printf 'DJANGO_ENV=dev\nDJANGO_SECRET_KEY=x\nDJANGO_ALLOWED_HOSTS=e.com\n' > "$tmp/.env"
stub="$(mktemp -d)"; printf '#!/usr/bin/env bash\necho uv "$*"\n' > "$stub/uv"; chmod +x "$stub/uv"
export PROJECT_DIR="$tmp"
PATH="$stub:$PATH" run "$BIN_DIR/set_production.sh"
assert_success
run grep -q "API_KEY_AUTH_ENABLED=1" "$tmp/.env"; assert_success
}
Step 2: Run to verify it fails
Run: ./bin/tests/test_helper/bats-core/bin/bats bin/tests/test_set_production.bats Expected: FAIL.
Step 3: Implementation
After the DJANGO_DEBUG=0 block in bin/set_production.sh:
# API key auth on in production (config.W002)
dotenv_set "$ENV_FILE" "API_KEY_AUTH_ENABLED" "1"
CHANGES+=("API_KEY_AUTH_ENABLED=1")
Update the --help text list accordingly.
Step 4: Run to verify it passes
Run: ./bin/tests/test_helper/bats-core/bin/bats bin/tests/test_set_production.bats Expected: PASS.
Step 5: Commit
git add bin/set_production.sh bin/tests/test_set_production.bats
git commit -m "feat(set-production): enable API_KEY_AUTH_ENABLED so W002 clears"
Task 11: Docs — cluster migration note
Files:
- Modify:
docs/Deployment.md(cluster section, if present) orapps/alerts/AGENTS.md - Modify:
AGENTS.mdcluster references if any mentionWEBHOOK_SECRET_CLUSTER
Step 1: Implementation
Add a short “Cluster auth (migration)” note: agents authenticate with HUB_API_KEY (a Bearer token created on the hub via manage.py create_api_key); WEBHOOK_SECRET_CLUSTER and CLUSTER_ROLE are removed. Cutover: create key → set HUB_API_KEY on each agent → ensure API_KEY_AUTH_ENABLED=1 on the hub → verify with push_to_hub --dry-run.
Grep docs for stale references:
Run: git grep -n "WEBHOOK_SECRET_CLUSTER\|CLUSTER_ROLE" -- docs ':!docs/plans/*' Update non-historical hits. (Leave docs/plans/* untouched — historical records.)
Step 2: Commit
git add docs AGENTS.md apps/alerts/AGENTS.md
git commit -m "docs: cluster auth migration to HUB_API_KEY"
Task 12: Full verification
Step 1: Run everything
uv run black . --check
uv run ruff check .
uv run pytest -q
uv run coverage run -m pytest && uv run coverage report # 100% on changed lines
./bin/tests/test_helper/bats-core/bin/bats bin/tests/*.bats
uv run pip-audit --strict --desc
uv run python manage.py check # config.W002 gone when API_KEY_AUTH_ENABLED=1 + prod
Step 2: Manual smoke (optional, local)
uv run python manage.py create_api_key --name "smoke"
# copy token; with API_KEY_AUTH_ENABLED=1:
# POST /alerts/webhook/cluster/ without Bearer -> 401
# POST with Bearer <token> and a cluster payload -> 202/200, no CHECK stage in the run
Step 3: Acceptance-criteria checklist (from the design doc §Acceptance criteria) — confirm all six.
Step 4: Final commit / open PR
git push -u origin design/unified-auth-cluster-driver
gh pr create --base main --title "feat: unified API-key auth + cluster-as-driver (slice A)" --body "<summary + link to design doc>"
Notes for the executor
- Read the existing test module before writing each test — reuse the established fixtures (APIKey creation,
safe_urlopenmock, orchestrator result helpers). The snippets above are illustrative; match local style and assertion helpers. WEBHOOK_SECRET_CLUSTER/CLUSTER_ROLEmust end with zero non-historical hits — the Task 6 and Task 11 greps are the gate.- Do not touch
docs/plans/*except to add this plan — they are immutable history. - Coverage: every changed branch needs a test (100% on changed lines). The
skip_checkerselif and theHUB_API_KEY-missing error are easy misses.