fix: send Scheduler API Bearer auth from the task registrar

The Scheduler's task-management endpoints (GET/POST /tasks, PUT
/tasks/{name}) are guarded by verify_api_key, but execute() built a bare
httpx.Client with no Authorization header: the existence probe 401'd
(misread as 'task absent') and every POST/PUT registration failed, so
--execute was never runnable end-to-end against the real Scheduler.

--execute now requires SCHEDULER_API_KEY from the environment (never
stored) and sends Authorization: Bearer on all registrar HTTP calls.
Deploy notes updated alongside the LIBRARY_API_KEY requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
2026-07-14 15:13:16 +02:00
co-authored by Claude Fable 5
parent 9ceec1464a
commit b4a5a92fee
4 changed files with 85 additions and 3 deletions
+4
View File
@@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Weekly quality report** — `POST /maintenance/quality-report {user}` runs the duplicate scan, flags stale pages (not updated in N days AND ≤ M SearchQuery hits from the graph data), lists pages missing tags/description, folds in the latest integrity-check results (Redis-cached or run inline), and writes a dated report page to `users/{user}/system/quality-reports/YYYY-MM-DD` (same-day reruns update the same page — the page id is remembered in Redis because the Wiki.js listing lags page creation). Response returns the full report content + page path. Verified end-to-end against the local dev server as `llm_tester`. - **Weekly quality report** — `POST /maintenance/quality-report {user}` runs the duplicate scan, flags stale pages (not updated in N days AND ≤ M SearchQuery hits from the graph data), lists pages missing tags/description, folds in the latest integrity-check results (Redis-cached or run inline), and writes a dated report page to `users/{user}/system/quality-reports/YYYY-MM-DD` (same-day reruns update the same page — the page id is remembered in Redis because the Wiki.js listing lags page creation). Response returns the full report content + page path. Verified end-to-end against the local dev server as `llm_tester`.
- **Nightly integrity check** — `POST /maintenance/integrity-check {user}` (read-only: reports, never auto-fixes) reports per tenant: wiki pages with ZERO vectors in Qdrant (silent-skip reindex victims), orphaned vectors whose wiki page no longer exists, unexpected Qdrant collections (test-tenant residue and unknown namespaces flagged; other services' collections counted as foreign), Neo4j Document nodes without wiki counterparts, plus counts and duration. The latest report is cached in Redis (30 days) so the weekly quality report can fold it in. - **Nightly integrity check** — `POST /maintenance/integrity-check {user}` (read-only: reports, never auto-fixes) reports per tenant: wiki pages with ZERO vectors in Qdrant (silent-skip reindex victims), orphaned vectors whose wiki page no longer exists, unexpected Qdrant collections (test-tenant residue and unknown namespaces flagged; other services' collections counted as foreign), Neo4j Document nodes without wiki counterparts, plus counts and duration. The latest report is cached in Redis (30 days) so the weekly quality report can fold it in.
### Fixed (Phase D review batch)
- **Registrar authenticates to the Scheduler API** - `scripts/register_scheduler_tasks.py --execute` sent no `Authorization` header while the Scheduler's task-management endpoints are Bearer-guarded (`verify_api_key`: 401 on missing key), so the existence probes 401'd (misread as "task absent") and every registration failed; only the public `/health` gate passed. `--execute` now requires `SCHEDULER_API_KEY` in the environment (refuses to run without it, key is never stored) and sends `Authorization: Bearer $SCHEDULER_API_KEY` on all of its own HTTP calls. Deploy note updated alongside the existing `LIBRARY_API_KEY` requirement.
### Fixed (hazards batch) ### Fixed (hazards batch)
- **CORS wildcard + credentials removed** - `allow_origins=["*"]` combined with `allow_credentials=True` told browsers to attach credentials for any site. Credentials are now disabled (all real callers are server-to-server and use the `Authorization` header, which wildcard-origin CORS without credentials still permits) and the origin list is configurable via `CORS_ALLOW_ORIGINS` (comma-separated, default `*`). - **CORS wildcard + credentials removed** - `allow_origins=["*"]` combined with `allow_credentials=True` told browsers to attach credentials for any site. Credentials are now disabled (all real callers are server-to-server and use the `Authorization` header, which wildcard-origin CORS without credentials still permits) and the origin list is configurable via `CORS_ALLOW_ORIGINS` (comma-separated, default `*`).
+8
View File
@@ -12,11 +12,19 @@ SCHEDULER_URL=http://<scheduler-host>:8090 \
# Actually register/update the tasks (deploy checklist step): # Actually register/update the tasks (deploy checklist step):
SCHEDULER_URL=http://<scheduler-host>:8090 \ SCHEDULER_URL=http://<scheduler-host>:8090 \
SCHEDULER_API_KEY=<scheduler-api-key> \
.venv/bin/python scripts/register_scheduler_tasks.py --execute .venv/bin/python scripts/register_scheduler_tasks.py --execute
``` ```
Conventions: Conventions:
- The Scheduler's task-management endpoints (`GET`/`POST /tasks`,
`PUT /tasks/{name}`) require `Authorization: Bearer $SCHEDULER_API_KEY`.
The registrar reads `SCHEDULER_API_KEY` from the environment for its
own HTTP calls (`--execute` refuses to run without it); the key is
never stored. This is separate from `LIBRARY_API_KEY` below, which the
Scheduler container needs at task **execution** time.
- All tasks call the **production** library-desk container - All tasks call the **production** library-desk container
(`http://library-desk:8089`) with the explicit production tenant (`http://library-desk:8089`) with the explicit production tenant
`user=jpmschweitzer` (there is no default tenant — Phase B). `user=jpmschweitzer` (there is no default tenant — Phase B).
+22 -3
View File
@@ -17,6 +17,11 @@ SAFETY MODEL
if present, and disable test_example_task. if present, and disable test_example_task.
- The Scheduler API location comes from the environment (SCHEDULER_URL); - The Scheduler API location comes from the environment (SCHEDULER_URL);
there is no hardcoded production default. there is no hardcoded production default.
- The Scheduler's task-management endpoints are themselves guarded by
Bearer auth (verify_api_key). --execute therefore requires
SCHEDULER_API_KEY in the environment; the registrar sends it as
``Authorization: Bearer <key>`` on its own HTTP calls. It is read from
the environment only and never stored anywhere.
- NO SECRET IS EVER STORED: the library-desk API key is referenced as the - NO SECRET IS EVER STORED: the library-desk API key is referenced as the
literal placeholder ``${LIBRARY_API_KEY}`` inside the task's literal placeholder ``${LIBRARY_API_KEY}`` inside the task's
``auth.token`` field. The Scheduler's rest_api_executor substitutes ``auth.token`` field. The Scheduler's rest_api_executor substitutes
@@ -32,6 +37,7 @@ Usage:
# Register for real (deploy checklist step) # Register for real (deploy checklist step)
SCHEDULER_URL=http://scheduler-host:8090 \ SCHEDULER_URL=http://scheduler-host:8090 \
SCHEDULER_API_KEY=<scheduler-api-key> \
python scripts/register_scheduler_tasks.py --execute python scripts/register_scheduler_tasks.py --execute
""" """
@@ -156,9 +162,16 @@ def dry_run(scheduler_url: str) -> None:
f"{len(TASKS)} task definition(s), {len(TASK_UPDATES)} update(s).") f"{len(TASKS)} task definition(s), {len(TASK_UPDATES)} update(s).")
def execute(scheduler_url: str) -> int: def execute(scheduler_url: str, scheduler_api_key: str) -> int:
failures = 0 failures = 0
with httpx.Client(base_url=scheduler_url, timeout=30.0) as client: # The Scheduler's task-management endpoints require Bearer auth
# (verify_api_key: 401 when missing, 403 when wrong). Without this
# header the existence probes 401 (misread as "task absent") and
# every POST/PUT fails.
auth_headers = {"Authorization": f"Bearer {scheduler_api_key}"}
with httpx.Client(
base_url=scheduler_url, timeout=30.0, headers=auth_headers
) as client:
health = client.get("/health") health = client.get("/health")
if health.status_code != 200: if health.status_code != 200:
print(f"ERROR: Scheduler health check failed: {health.status_code}") print(f"ERROR: Scheduler health check failed: {health.status_code}")
@@ -224,7 +237,13 @@ def main() -> int:
print("ERROR: SCHEDULER_URL must be set for --execute") print("ERROR: SCHEDULER_URL must be set for --execute")
return 1 return 1
return execute(scheduler_url) scheduler_api_key = os.environ.get("SCHEDULER_API_KEY", "")
if not scheduler_api_key:
print("ERROR: SCHEDULER_API_KEY must be set for --execute "
"(the Scheduler's task endpoints require Bearer auth)")
return 1
return execute(scheduler_url, scheduler_api_key)
if __name__ == "__main__": if __name__ == "__main__":
+51
View File
@@ -128,3 +128,54 @@ class TestSchedulerTaskDefinitions:
assert "DRY RUN" in out assert "DRY RUN" in out
assert "library_integrity_check" in out assert "library_integrity_check" in out
assert mod.API_KEY_PLACEHOLDER in out # placeholder, never a real key assert mod.API_KEY_PLACEHOLDER in out # placeholder, never a real key
def test_execute_requires_scheduler_api_key(self, capsys, monkeypatch):
"""--execute must refuse to run without SCHEDULER_API_KEY (the
Scheduler's task endpoints are Bearer-guarded; without the key
every probe 401s and registration silently fails)."""
mod = self._load_module()
monkeypatch.setattr(
"sys.argv", ["register_scheduler_tasks.py", "--execute"]
)
monkeypatch.setenv("SCHEDULER_URL", "http://scheduler.test:8090")
monkeypatch.delenv("SCHEDULER_API_KEY", raising=False)
def _boom(*args, **kwargs):
raise AssertionError("must not contact the Scheduler without a key")
monkeypatch.setattr(mod.httpx, "Client", _boom)
assert mod.main() == 1
assert "SCHEDULER_API_KEY" in capsys.readouterr().out
def test_execute_sends_scheduler_bearer_auth(self, monkeypatch):
"""The registrar's own HTTP client must carry
Authorization: Bearer $SCHEDULER_API_KEY on every call."""
import httpx as real_httpx
mod = self._load_module()
seen = {"auth_headers": [], "paths": []}
def handler(request: real_httpx.Request) -> real_httpx.Response:
seen["auth_headers"].append(request.headers.get("Authorization"))
path = request.url.path
seen["paths"].append(f"{request.method} {path}")
if path == "/health":
return real_httpx.Response(200, json={"status": "healthy"})
if request.method == "GET" and path.startswith("/tasks/"):
return real_httpx.Response(404) # not registered yet
return real_httpx.Response(200, json={"ok": True})
real_client = real_httpx.Client
def client_factory(**kwargs):
kwargs["transport"] = real_httpx.MockTransport(handler)
return real_client(**kwargs)
monkeypatch.setattr(mod.httpx, "Client", client_factory)
assert mod.execute("http://scheduler.test:8090", "sched-key-123") == 0
assert seen["auth_headers"], "no HTTP calls were made"
assert all(h == "Bearer sched-key-123" for h in seen["auth_headers"])
# All three tasks created (404 probe -> POST /tasks)
assert seen["paths"].count("POST /tasks") == len(mod.TASKS)