- 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
232 lines
7.9 KiB
Python
232 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Register the Phase C production Scheduler tasks (deploy-checklist helper).
|
|
|
|
Defines the four task payloads from docs/scheduler-tasks.md:
|
|
1. library_integrity_check - nightly 04:30
|
|
2. library_quality_report - Sunday 03:00
|
|
3. library_paperless_orphan_cleanup - daily 05:00
|
|
4. test_example_task - DISABLED (update, not create)
|
|
|
|
SAFETY MODEL
|
|
============
|
|
- DRY-RUN BY DEFAULT: without --execute the script only prints the exact
|
|
payloads it would send. Nothing is contacted except (optionally) the
|
|
Scheduler health endpoint.
|
|
- --execute performs the registration: create task if absent, update it
|
|
if present, and disable test_example_task.
|
|
- The Scheduler API location comes from the environment (SCHEDULER_URL);
|
|
there is no hardcoded production default.
|
|
- 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)
|
|
SCHEDULER_URL=http://scheduler-host:8090 \
|
|
python scripts/register_scheduler_tasks.py
|
|
|
|
# Register for real (deploy checklist step)
|
|
SCHEDULER_URL=http://scheduler-host:8090 \
|
|
python scripts/register_scheduler_tasks.py --execute
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import httpx
|
|
|
|
API_KEY_PLACEHOLDER = "${LIBRARY_API_KEY}"
|
|
PRODUCTION_TENANT = "jpmschweitzer"
|
|
LIBRARY_BASE_URL = "http://library-desk:8089"
|
|
|
|
#: Tasks to create-or-update (see docs/scheduler-tasks.md).
|
|
TASKS = [
|
|
{
|
|
"task_name": "library_integrity_check",
|
|
"service": "library-desk",
|
|
"executor": "rest_api_executor",
|
|
"priority": 60,
|
|
"description": (
|
|
"Nightly read-only integrity check for the library "
|
|
"(vectors/graph/wiki/collections)"
|
|
),
|
|
"enabled": True,
|
|
"max_retries": 2,
|
|
"timeout_seconds": 900,
|
|
"minute": 30,
|
|
"hour": 4,
|
|
"day_of_month": -1,
|
|
"month": -1,
|
|
"day_of_week": -1,
|
|
"config": {
|
|
"method": "POST",
|
|
"url": f"{LIBRARY_BASE_URL}/maintenance/integrity-check",
|
|
"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},
|
|
},
|
|
},
|
|
{
|
|
"task_name": "library_quality_report",
|
|
"service": "library-desk",
|
|
"executor": "rest_api_executor",
|
|
"priority": 60,
|
|
"description": (
|
|
"Weekly library quality report (dedup, stale pages, missing "
|
|
"metadata, integrity) written to the wiki"
|
|
),
|
|
"enabled": True,
|
|
"max_retries": 2,
|
|
"timeout_seconds": 1800,
|
|
"minute": 0,
|
|
"hour": 3,
|
|
"day_of_month": -1,
|
|
"month": -1,
|
|
"day_of_week": 6, # Sunday (0 = Monday)
|
|
"config": {
|
|
"method": "POST",
|
|
"url": f"{LIBRARY_BASE_URL}/maintenance/quality-report",
|
|
"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},
|
|
},
|
|
},
|
|
{
|
|
"task_name": "library_paperless_orphan_cleanup",
|
|
"service": "library-desk",
|
|
"executor": "rest_api_executor",
|
|
"priority": 60,
|
|
"description": (
|
|
"Daily cleanup of vectors/graph nodes for documents deleted "
|
|
"from Paperless-ngx"
|
|
),
|
|
"enabled": True,
|
|
"max_retries": 2,
|
|
"timeout_seconds": 900,
|
|
"minute": 0,
|
|
"hour": 5,
|
|
"day_of_month": -1,
|
|
"month": -1,
|
|
"day_of_week": -1,
|
|
"config": {
|
|
"method": "POST",
|
|
"url": (
|
|
f"{LIBRARY_BASE_URL}/maintenance/cleanup/paperless"
|
|
f"?user={PRODUCTION_TENANT}&dry_run=false"
|
|
),
|
|
"payload": {},
|
|
"auth": {"type": "bearer", "token": API_KEY_PLACEHOLDER},
|
|
},
|
|
},
|
|
]
|
|
|
|
#: Existing tasks to update in place.
|
|
TASK_UPDATES = [
|
|
{"task_name": "test_example_task", "updates": {"enabled": False}},
|
|
]
|
|
|
|
|
|
def dry_run(scheduler_url: str) -> None:
|
|
print("=" * 72)
|
|
print("DRY RUN - nothing will be sent. Re-run with --execute to register.")
|
|
print(f"Scheduler API: {scheduler_url or '(SCHEDULER_URL not set)'}")
|
|
print("=" * 72)
|
|
for task in TASKS:
|
|
print(f"\n--- create-or-update: POST {scheduler_url}/tasks "
|
|
f"(or PUT /tasks/{task['task_name']}) ---")
|
|
print(json.dumps(task, indent=2))
|
|
for update in TASK_UPDATES:
|
|
print(f"\n--- update: PUT {scheduler_url}/tasks/{update['task_name']} ---")
|
|
print(json.dumps(update["updates"], indent=2))
|
|
print("\nDry run complete: "
|
|
f"{len(TASKS)} task definition(s), {len(TASK_UPDATES)} update(s).")
|
|
|
|
|
|
def execute(scheduler_url: str) -> int:
|
|
failures = 0
|
|
with httpx.Client(base_url=scheduler_url, timeout=30.0) as client:
|
|
health = client.get("/health")
|
|
if health.status_code != 200:
|
|
print(f"ERROR: Scheduler health check failed: {health.status_code}")
|
|
return 1
|
|
|
|
for task in TASKS:
|
|
name = task["task_name"]
|
|
# 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)
|
|
action = "updated"
|
|
else:
|
|
resp = client.post("/tasks", json=payload)
|
|
action = "created"
|
|
if resp.status_code == 200:
|
|
print(f"[ok] {action} {name}")
|
|
else:
|
|
failures += 1
|
|
print(f"[FAIL] {action} {name}: {resp.status_code} {resp.text[:200]}")
|
|
|
|
for update in TASK_UPDATES:
|
|
name = update["task_name"]
|
|
if client.get(f"/tasks/{name}").status_code != 200:
|
|
print(f"[skip] {name} does not exist - nothing to disable")
|
|
continue
|
|
resp = client.put(f"/tasks/{name}", json=update["updates"])
|
|
if resp.status_code == 200:
|
|
print(f"[ok] updated {name}: {update['updates']}")
|
|
else:
|
|
failures += 1
|
|
print(f"[FAIL] update {name}: {resp.status_code} {resp.text[:200]}")
|
|
|
|
return 1 if failures else 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Register library-desk production Scheduler tasks "
|
|
"(dry-run by default)"
|
|
)
|
|
parser.add_argument(
|
|
"--execute",
|
|
action="store_true",
|
|
help="Actually register/update the tasks (default: dry-run print only)",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Explicitly print payloads without sending (the default behavior)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
scheduler_url = os.environ.get("SCHEDULER_URL", "").rstrip("/")
|
|
|
|
if not args.execute or args.dry_run:
|
|
dry_run(scheduler_url)
|
|
return 0
|
|
|
|
if not scheduler_url:
|
|
print("ERROR: SCHEDULER_URL must be set for --execute")
|
|
return 1
|
|
|
|
return execute(scheduler_url)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|