fix: WS5 hazards batch - CORS, scheduler auth, reranker dedupe, wiring, write txns
- CORS: drop allow_credentials (wildcard origin + credentials told
browsers to attach credentials for any site); origins configurable via
CORS_ALLOW_ORIGINS (default * is safe without credentials). Verified
live: preflight no longer advertises access-control-allow-credentials.
- Scheduler tasks: auth moved from a plain Authorization header (which
the Scheduler's rest_api_executor does NOT env-substitute) to its
auth {type: bearer, token: ${LIBRARY_API_KEY}} block, substituted from
the Scheduler's own environment at execution time. The registrar no
longer resolves the real key client-side, so it can never be persisted
into the scheduled_tasks.config JSONB column. Also fixed: JSON bodies
moved from the ignored "body" key to "payload" (the executor only
reads config["payload"], so the tasks would have POSTed empty bodies
and failed required-user validation).
- Reranker: parsed ranking indices are deduplicated preserving first
occurrence (an LLM answer like "3,3,1" duplicated a result).
- HybridRAG wiring consolidated into dependencies.get_hybrid_rag_service
(now including volatile_service); the inline copies in /query/hybrid
and /wiki/pages/smart-create are gone - smart-create previously ran
without the volatile leg, and the singleton was unused.
- Remaining Neo4j writes (GraphService ingestion/deletes/purges/entity
mentions, webhook rename+delete cleanup, document-sync _index_graph,
consolidation mark-processed/add-entity) moved from auto-commit
execute_query to execute_write managed transactions with retry.
Verified end-to-end on the local dev server as llm_tester: /query/hybrid
200 with all five legs ok (volatile now active), background persistence
landed as one transaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -17,10 +17,13 @@ SAFETY MODEL
|
||||
if present, and disable test_example_task.
|
||||
- The Scheduler API location comes from the environment (SCHEDULER_URL);
|
||||
there is no hardcoded production default.
|
||||
- The library-desk API key is embedded as the literal placeholder
|
||||
``${LIBRARY_API_KEY}`` in dry-run output. On --execute it is replaced
|
||||
with the LIBRARY_API_KEY environment variable (required then, never
|
||||
printed).
|
||||
- NO SECRET IS EVER STORED: the library-desk API key is referenced as the
|
||||
literal placeholder ``${LIBRARY_API_KEY}`` inside the task's
|
||||
``auth.token`` field. The Scheduler's rest_api_executor substitutes
|
||||
``${ENV_VAR}`` placeholders from ITS OWN environment at execution time
|
||||
(it substitutes url/payload/auth — NOT plain headers), so the raw token
|
||||
never lands in the scheduled_tasks.config JSONB column. The Scheduler
|
||||
container must therefore have LIBRARY_API_KEY in its environment.
|
||||
|
||||
Usage:
|
||||
# Preview (default)
|
||||
@@ -28,12 +31,11 @@ Usage:
|
||||
python scripts/register_scheduler_tasks.py
|
||||
|
||||
# Register for real (deploy checklist step)
|
||||
SCHEDULER_URL=http://scheduler-host:8090 LIBRARY_API_KEY=... \
|
||||
SCHEDULER_URL=http://scheduler-host:8090 \
|
||||
python scripts/register_scheduler_tasks.py --execute
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -66,11 +68,11 @@ TASKS = [
|
||||
"config": {
|
||||
"method": "POST",
|
||||
"url": f"{LIBRARY_BASE_URL}/maintenance/integrity-check",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY_PLACEHOLDER}",
|
||||
},
|
||||
"body": {"user": PRODUCTION_TENANT},
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
# rest_api_executor sends config["payload"] as the JSON body and
|
||||
# substitutes ${ENV_VAR} in auth.token from the Scheduler's env.
|
||||
"payload": {"user": PRODUCTION_TENANT},
|
||||
"auth": {"type": "bearer", "token": API_KEY_PLACEHOLDER},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -93,16 +95,14 @@ TASKS = [
|
||||
"config": {
|
||||
"method": "POST",
|
||||
"url": f"{LIBRARY_BASE_URL}/maintenance/quality-report",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY_PLACEHOLDER}",
|
||||
},
|
||||
"body": {
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"payload": {
|
||||
"user": PRODUCTION_TENANT,
|
||||
"stale_days": 30,
|
||||
"dedup_threshold": 0.9,
|
||||
"write_page": True,
|
||||
},
|
||||
"auth": {"type": "bearer", "token": API_KEY_PLACEHOLDER},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -128,8 +128,8 @@ TASKS = [
|
||||
f"{LIBRARY_BASE_URL}/maintenance/cleanup/paperless"
|
||||
f"?user={PRODUCTION_TENANT}&dry_run=false"
|
||||
),
|
||||
"headers": {"Authorization": f"Bearer {API_KEY_PLACEHOLDER}"},
|
||||
"body": {},
|
||||
"payload": {},
|
||||
"auth": {"type": "bearer", "token": API_KEY_PLACEHOLDER},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -140,16 +140,6 @@ TASK_UPDATES = [
|
||||
]
|
||||
|
||||
|
||||
def substitute_api_key(task: dict, api_key: str) -> dict:
|
||||
"""Return a deep copy of the task with the API-key placeholder filled in."""
|
||||
resolved = copy.deepcopy(task)
|
||||
headers = resolved.get("config", {}).get("headers", {})
|
||||
for name, value in headers.items():
|
||||
if API_KEY_PLACEHOLDER in value:
|
||||
headers[name] = value.replace(API_KEY_PLACEHOLDER, api_key)
|
||||
return resolved
|
||||
|
||||
|
||||
def dry_run(scheduler_url: str) -> None:
|
||||
print("=" * 72)
|
||||
print("DRY RUN - nothing will be sent. Re-run with --execute to register.")
|
||||
@@ -166,7 +156,7 @@ def dry_run(scheduler_url: str) -> None:
|
||||
f"{len(TASKS)} task definition(s), {len(TASK_UPDATES)} update(s).")
|
||||
|
||||
|
||||
def execute(scheduler_url: str, api_key: str) -> int:
|
||||
def execute(scheduler_url: str) -> int:
|
||||
failures = 0
|
||||
with httpx.Client(base_url=scheduler_url, timeout=30.0) as client:
|
||||
health = client.get("/health")
|
||||
@@ -176,7 +166,9 @@ def execute(scheduler_url: str, api_key: str) -> int:
|
||||
|
||||
for task in TASKS:
|
||||
name = task["task_name"]
|
||||
payload = substitute_api_key(task, api_key)
|
||||
# Sent verbatim: the ${LIBRARY_API_KEY} placeholder is resolved
|
||||
# by the Scheduler at execution time, never stored as a raw key.
|
||||
payload = task
|
||||
exists = client.get(f"/tasks/{name}").status_code == 200
|
||||
if exists:
|
||||
resp = client.put(f"/tasks/{name}", json=payload)
|
||||
@@ -231,13 +223,8 @@ def main() -> int:
|
||||
if not scheduler_url:
|
||||
print("ERROR: SCHEDULER_URL must be set for --execute")
|
||||
return 1
|
||||
api_key = os.environ.get("LIBRARY_API_KEY", "")
|
||||
if not api_key:
|
||||
print("ERROR: LIBRARY_API_KEY must be set for --execute "
|
||||
"(it fills the Authorization header placeholder)")
|
||||
return 1
|
||||
|
||||
return execute(scheduler_url, api_key)
|
||||
return execute(scheduler_url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user