fix: make runtime prefetch task registration executable end-to-end

SchedulerClient.register_volatile_fetch (consolidation's prefetch
routing) registered tasks that were dead on arrival:
- JSON body stored under 'body', which rest_api_executor ignores
  (it only reads config['payload'])
- no auth block, so the scheduled POST to /volatile/fetch would 401
  against library-desk's verify_api_key
- user placed in the body while /volatile/fetch endpoints require it
  as a query parameter (RequiredUserQuery) - would 422 regardless

The task config now carries user in the URL query string (encoded),
an empty payload, and auth {type: bearer, token: ${LIBRARY_API_KEY}}
substituted Scheduler-side (never stored raw).

SchedulerClient also sent no Authorization to the Scheduler API itself,
so registration 401'd silently at consolidation time; it now sends
Bearer auth from the new scheduler_api_key setting.

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:16:09 +02:00
co-authored by Claude Fable 5
parent b4a5a92fee
commit bc68d3b691
5 changed files with 173 additions and 7 deletions
+39 -6
View File
@@ -8,10 +8,17 @@ Registers and manages scheduled tasks for prefetch operations
import httpx
import logging
from typing import Optional, Any
from urllib.parse import quote
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
#: Literal placeholder stored in task auth.token; the Scheduler's
#: rest_api_executor substitutes ${ENV_VAR} from ITS OWN environment at
#: execution time, so the raw library-desk key is never stored in the
#: scheduled_tasks.config column.
LIBRARY_API_KEY_PLACEHOLDER = "${LIBRARY_API_KEY}"
class SchedulerTask(BaseModel):
"""Task definition for scheduler registration."""
@@ -39,24 +46,38 @@ class SchedulerTask(BaseModel):
class SchedulerClient:
"""Client for external scheduler service."""
def __init__(self, base_url: str, timeout: float = 30.0):
def __init__(self, base_url: str, api_key: str = "", timeout: float = 30.0):
"""
Initialize scheduler client.
Args:
base_url: Scheduler API base URL (e.g., "http://scheduler:8090")
api_key: Bearer key for the Scheduler's task-management API.
The /tasks endpoints are guarded by verify_api_key (401 when
the Authorization header is missing), so without this key
every registration call fails.
timeout: HTTP request timeout in seconds
"""
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
self._client: Optional[httpx.AsyncClient] = None
if not api_key:
logger.warning(
"SchedulerClient created without an API key; task-management "
"calls will be rejected by the Scheduler (401)"
)
async def _get_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client."""
"""Get or create HTTP client (with Scheduler API Bearer auth)."""
if self._client is None or self._client.is_closed:
headers = (
{"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
)
self._client = httpx.AsyncClient(
base_url=self.base_url,
timeout=self.timeout,
headers=headers,
)
return self._client
@@ -301,13 +322,25 @@ class SchedulerClient:
day_of_week=schedule.get("day_of_week", -1),
config={
"method": "POST",
"url": f"http://library-desk:8089/volatile/fetch/{namespace}/{key}",
# /volatile/fetch endpoints take user as a REQUIRED QUERY
# parameter (RequiredUserQuery) - a body user would 422.
"url": (
f"http://library-desk:8089/volatile/fetch/"
f"{namespace}/{key}?user={quote(user, safe='')}"
),
"headers": {
"Content-Type": "application/json"
},
"body": {
"user": user
}
# rest_api_executor only reads config["payload"] as the JSON
# body (a "body" key is silently ignored).
"payload": {},
# Substituted from the Scheduler's environment at execution
# time; without it the scheduled POST 401s against
# library-desk's verify_api_key.
"auth": {
"type": "bearer",
"token": LIBRARY_API_KEY_PLACEHOLDER,
},
}
)
+7
View File
@@ -145,6 +145,13 @@ class Settings(BaseSettings):
# Scheduler Service
scheduler_url: str = Field(default="http://scheduler:8090", description="Scheduler service URL")
scheduler_api_key: str = Field(
default="",
description=(
"Bearer key for the Scheduler's auth-guarded task-management API; "
"required for runtime prefetch task registration"
),
)
@property
def qdrant_url(self) -> str:
+4 -1
View File
@@ -259,7 +259,10 @@ def get_scheduler_client() -> SchedulerClient:
Note: Used for registering prefetch tasks discovered during HybridRAG searches
"""
settings = get_settings()
client = SchedulerClient(base_url=settings.scheduler_url)
client = SchedulerClient(
base_url=settings.scheduler_url,
api_key=settings.scheduler_api_key,
)
logger.debug(f"Created Scheduler client: {settings.scheduler_url}")
return client