170 lines
7.3 KiB
Python
170 lines
7.3 KiB
Python
"""HUX-06 provider-neutral friendly modes and scoped route selection.
|
|
|
|
Automatic modes express intent to Switchyard and never name a provider. An
|
|
exact route can only be selected through the explicit advanced path and must
|
|
exist in the operator-provided route catalog. Private mode is local-only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
from hux import contracts, rules
|
|
from hux.errors import Conflict, Invalid, NotFound
|
|
from hux.http import Request, Response, Router, page
|
|
from hux.store import now_iso
|
|
|
|
CARD = "HUX-06"
|
|
FAMILY = "mode_selections"
|
|
MODE_NAMES = ("fast", "thoughtful", "research", "create", "private")
|
|
MANUAL_ROUTE = re.compile(r"^atlas/manual/(codex|claude|local)/[a-z0-9][a-z0-9/-]{0,100}$")
|
|
MAX_CATALOG_ROUTES = 256
|
|
SCHEMAS = contracts.load_all()
|
|
|
|
|
|
def _body(request: Request) -> dict[str, Any]:
|
|
if not isinstance(request.body, dict):
|
|
raise Invalid("body must be a JSON object")
|
|
extra = set(request.body) - {"project_id", "mode", "advanced", "override_route_id"}
|
|
if extra:
|
|
raise Invalid("unexpected mode fields", sorted(extra))
|
|
return request.body
|
|
|
|
|
|
def _scope(request: Request, project_id: Any) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
"""Resolve a conversation and its mandatory owning project."""
|
|
from hux import organization
|
|
|
|
try:
|
|
conversation = request.store.get(organization.CONVERSATIONS, request.params["id"])
|
|
except (Invalid, NotFound) as error:
|
|
raise NotFound("conversation not found") from error
|
|
path_project = request.params.get("project_id")
|
|
actual = conversation.get("project_id")
|
|
if not isinstance(project_id, str) or project_id != path_project or not actual or project_id != actual:
|
|
raise NotFound("project or conversation not found")
|
|
try:
|
|
project = request.store.get(organization.PROJECTS, project_id)
|
|
except (Invalid, NotFound) as error:
|
|
raise NotFound("project or conversation not found") from error
|
|
return project, conversation
|
|
|
|
|
|
def _record_id(conversation_id: str) -> str:
|
|
digest = hashlib.sha256(conversation_id.encode()).hexdigest()[:24]
|
|
return f"mode_{digest}"
|
|
|
|
|
|
def _catalog() -> set[str]:
|
|
raw = os.environ.get("HUX_SWITCHYARD_ROUTE_CATALOG", "")
|
|
items = [item.strip() for item in raw.split(",") if item.strip()]
|
|
if len(items) > MAX_CATALOG_ROUTES:
|
|
return set()
|
|
return {item for item in items if MANUAL_ROUTE.fullmatch(item)}
|
|
|
|
|
|
def _contract(mode: str, advanced: bool, override: Any) -> dict[str, Any]:
|
|
if not isinstance(mode, str) or mode not in MODE_NAMES:
|
|
raise Invalid("unknown friendly mode")
|
|
if override is not None:
|
|
if not advanced or not isinstance(override, str) or not MANUAL_ROUTE.fullmatch(override):
|
|
raise Invalid("an exact route requires a valid advanced manual route")
|
|
if override not in _catalog():
|
|
raise Invalid("advanced route is not in the Switchyard catalog")
|
|
if mode == "private" and not override.startswith("atlas/manual/local/"):
|
|
raise Invalid("private mode is local-only")
|
|
elif advanced:
|
|
raise Invalid("advanced selection requires override_route_id")
|
|
try:
|
|
result = rules.mode_contract(mode, override)
|
|
except ValueError as error:
|
|
raise Invalid(str(error)) from error
|
|
problems = contracts.validate("mode.schema.json", result, SCHEMAS)
|
|
if problems:
|
|
raise Invalid("mode failed contract validation", problems)
|
|
if mode != "private" and result["switchyard"]["route_id"].startswith("atlas/manual/"):
|
|
raise Invalid("automatic modes may not pin a provider")
|
|
return result
|
|
|
|
|
|
def _fingerprint(body: dict[str, Any]) -> str:
|
|
allowed = {key: body.get(key) for key in ("project_id", "mode", "advanced", "override_route_id")}
|
|
return hashlib.sha256(json.dumps(allowed, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
|
|
|
|
def _replay(request: Request, key: str, fingerprint: str) -> dict[str, Any] | None:
|
|
for row in request.store.read(FAMILY, "idempotency"):
|
|
if row.get("key") != key:
|
|
continue
|
|
if row.get("fingerprint") != fingerprint:
|
|
raise Conflict("Idempotency-Key was already used for a different selection")
|
|
return request.store.get(FAMILY, row["id"])
|
|
return None
|
|
|
|
|
|
def list_modes(request: Request) -> Response:
|
|
"""Return all provider-neutral mode contracts."""
|
|
items = [_contract(name, False, None) for name in MODE_NAMES]
|
|
request.audit("modes.list", "modes")
|
|
return page(items)
|
|
|
|
|
|
def get_selection(request: Request) -> Response:
|
|
"""Return the selected mode for one bound conversation."""
|
|
project_id = request.params["project_id"]
|
|
_, conversation = _scope(request, project_id)
|
|
record = request.store.get(FAMILY, _record_id(conversation["id"]))
|
|
request.audit("modes.read", record["id"])
|
|
return Response(200, record, {"ETag": str(record["revision"]), "Cache-Control": "no-store"})
|
|
|
|
|
|
def put_selection(request: Request) -> Response:
|
|
"""Select a mode using If-Match and Idempotency-Key."""
|
|
body = _body(request)
|
|
_, conversation = _scope(request, body.get("project_id"))
|
|
key = request.idempotency_key()
|
|
if not key:
|
|
raise Invalid("Idempotency-Key is required")
|
|
expected = request.if_match()
|
|
if expected is None:
|
|
raise Invalid("If-Match is required; use 0 for the first selection")
|
|
fingerprint = _fingerprint(body)
|
|
mode = _contract(body.get("mode"), body.get("advanced") is True, body.get("override_route_id"))
|
|
record_id = _record_id(conversation["id"])
|
|
with request.store.lock(FAMILY):
|
|
replayed = _replay(request, key, fingerprint)
|
|
if replayed is not None:
|
|
request.audit("modes.select", replayed["id"], reason="idempotent_replay")
|
|
return Response(200, replayed, {"ETag": str(replayed["revision"]), "HUX-Replayed": "true", "Cache-Control": "no-store"})
|
|
exists = request.store.exists(FAMILY, record_id)
|
|
if expected != (request.store.get(FAMILY, record_id)["revision"] if exists else 0):
|
|
raise Conflict("mode selection revision does not match If-Match")
|
|
stamp = now_iso()
|
|
record = {
|
|
"id": record_id,
|
|
"schema": "hux.mode_selection.v1",
|
|
"owner": request.identity.subject,
|
|
"project_id": body["project_id"],
|
|
"conversation_id": conversation["id"],
|
|
"mode": mode,
|
|
"updated_at": stamp,
|
|
}
|
|
stored = request.store.put(FAMILY, record, expected_revision=expected)
|
|
request.store.append(FAMILY, "idempotency", {"key": key, "fingerprint": fingerprint, "id": record_id, "at": stamp})
|
|
with request.store.lock("conversations"):
|
|
current = request.store.get("conversations", conversation["id"])
|
|
request.store.put("conversations", {**current, "mode": mode["mode"], "updated_at": stamp}, current["revision"])
|
|
request.audit("modes.select", record_id)
|
|
return Response(200, stored, {"ETag": str(stored["revision"]), "Cache-Control": "no-store"})
|
|
|
|
|
|
def register(router: Router) -> None:
|
|
"""Attach HUX-06 routes."""
|
|
router.add("GET", "/hux/v1/modes", CARD, "modes.list", list_modes)
|
|
router.add("GET", "/hux/v1/projects/{project_id}/conversations/{id}/mode", CARD, "modes.read", get_selection)
|
|
router.add("PUT", "/hux/v1/projects/{project_id}/conversations/{id}/mode", CARD, "modes.select", put_selection)
|