"""HUX-11 routes: capabilities, manifest, and trusted context bootstrap.""" from __future__ import annotations import hashlib import hmac import os import re import stat from pathlib import Path from typing import Any from hux import contracts, organization from hux.errors import Conflict, Forbidden, Invalid from hux.flags import CONTRACT_VERSION from hux.http import Request, Response, Router from hux.store import check_id, now_iso CONTEXT_FAMILY = "context_bindings" CONTEXT_SCHEMA = "hux.context_bootstrap.v1" CONTEXT_KEY_BYTES = 32 CONTEXT_MESSAGE = b"hux.context.id.v1" RAW_RE = re.compile(r"^[A-Za-z0-9._:/@+\-]{1,240}$") CONTEXT_ID_RE = re.compile(r"^(ses|conv|prj)_[0-9a-f]{32}$") SCHEMAS = contracts.load_all() def _context_key(environ: dict[str, str]) -> bytes: """Read the exact 0600 regular key file; inline keys and links fail closed.""" raw_path = environ.get("HUX_CONTEXT_KEY_FILE", "") if not raw_path: raise Forbidden("context identity key is unavailable") path = Path(raw_path) try: metadata = path.lstat() except OSError as error: raise Forbidden("context identity key is unavailable") from error if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_uid != os.geteuid(): raise Forbidden("context identity key is unsafe") try: value = path.read_bytes() except OSError as error: raise Forbidden("context identity key is unavailable") from error if len(value) != CONTEXT_KEY_BYTES: raise Forbidden("context identity key is invalid") return value def _source(body: dict[str, Any], name: str) -> str: value = body.get(name) if not isinstance(value, str) or not RAW_RE.fullmatch(value): raise Invalid(f"{name} is malformed") return value def derive_context_id(key: bytes, purpose: str, identity: Any, raw: str) -> str: """Derive one frozen HMAC-bound context id.""" prefix = {"session": "ses", "conversation": "conv", "project": "prj"}[purpose] message = b"\0".join( (CONTEXT_MESSAGE, purpose.encode(), identity.tenant_slot.encode(), identity.subject.encode(), raw.encode()) ) return f"{prefix}_{hmac.new(key, message, hashlib.sha256).hexdigest()[:32]}" def _exact_body(request: Request) -> tuple[dict[str, Any], str, str]: if not isinstance(request.body, dict): raise Invalid("body must be a JSON object") expected = {"raw_session_id", "project_source", "session_id", "conversation_id", "project_id"} if set(request.body) != expected: raise Invalid("context bootstrap fields are not exact") raw_session = _source(request.body, "raw_session_id") project_source = _source(request.body, "project_source") for name, prefix in (("session_id", "ses"), ("conversation_id", "conv"), ("project_id", "prj")): value = request.body.get(name) if not isinstance(value, str) or not CONTEXT_ID_RE.fullmatch(value) or not value.startswith(f"{prefix}_"): raise Invalid(f"{name} is malformed") check_id(value) return request.body, raw_session, project_source def _verify_record(record: dict[str, Any], schema: str, owner: str, project_id: str | None = None) -> None: if record.get("schema") != schema or record.get("owner") != owner: raise Conflict("context identity does not match existing record") if project_id is not None and record.get("project_id") != project_id: raise Conflict("context linkage does not match existing record") if contracts.validate_record(record, SCHEMAS): raise Conflict("existing context record is invalid") def _response(request: Request, project: dict[str, Any], conversation: dict[str, Any], created: dict[str, bool]) -> Response: body = { "schema": CONTEXT_SCHEMA, "contract_version": CONTRACT_VERSION, "identity": request.identity.record(), "session_id": request.body["session_id"], "conversation_id": conversation["id"], "project_id": project["id"], "created": created, "revisions": {"project": project["revision"], "conversation": conversation["revision"]}, } return Response(201 if any(created.values()) else 200, body) def bootstrap_context(request: Request) -> Response: """Create deterministic server-owned project and conversation records for trusted runtimes.""" if request.identity.trust not in {"relay", "worker"}: raise Forbidden("context bootstrap requires relay or worker trust") body, raw_session, project_source = _exact_body(request) idem = request.idempotency_key() if not idem: raise Invalid("Idempotency-Key is required") key = _context_key(request.flags._environ) expected = { "session_id": derive_context_id(key, "session", request.identity, raw_session), "conversation_id": derive_context_id(key, "conversation", request.identity, raw_session), "project_id": derive_context_id(key, "project", request.identity, project_source), } if any(not hmac.compare_digest(body[name], value) for name, value in expected.items()): raise Invalid("context identifiers do not match authenticated inputs") with request.store.lock(CONTEXT_FAMILY): binding = request.store.get(CONTEXT_FAMILY, body["session_id"]) if request.store.exists(CONTEXT_FAMILY, body["session_id"]) else None if binding: valid_binding = ( binding.get("schema") == "hux.context_binding.v1" and binding.get("id") == body["session_id"] and binding.get("owner") == request.identity.subject and all(binding.get(name) == body[name] for name in expected) ) if not valid_binding: raise Conflict("context binding does not match existing linkage") for row in request.store.read(CONTEXT_FAMILY, "idempotency"): if row.get("key") == idem and any(row.get(name) != body[name] for name in expected): raise Conflict("Idempotency-Key was used for another context") project = request.store.get(organization.PROJECTS, body["project_id"]) if request.store.exists(organization.PROJECTS, body["project_id"]) else None conversation = request.store.get(organization.CONVERSATIONS, body["conversation_id"]) if request.store.exists(organization.CONVERSATIONS, body["conversation_id"]) else None if project: _verify_record(project, "hux.project.v1", request.identity.subject) if conversation: _verify_record(conversation, "hux.conversation.v1", request.identity.subject, body["project_id"]) if project is None and request.store.count(organization.PROJECTS) >= organization.MAX_PROJECTS: raise Conflict("projects cap reached") if conversation is None and request.store.count(organization.CONVERSATIONS) >= organization.MAX_CONVERSATIONS: raise Conflict("conversations cap reached") stamp = now_iso() created = {"project": project is None, "conversation": conversation is None} project = project or request.store.put(organization.PROJECTS, organization.checked({ "schema": "hux.project.v1", "id": body["project_id"], "owner": request.identity.subject, "name": "Hermes", "tags": [], "pinned": False, "archived": False, "default_mode": "fast", "created_at": stamp, "updated_at": stamp, "revision": 1, })) conversation = conversation or request.store.put(organization.CONVERSATIONS, organization.checked({ "schema": "hux.conversation.v1", "id": body["conversation_id"], "owner": request.identity.subject, "project_id": body["project_id"], "title": "Hermes conversation", "tags": [], "pinned": False, "archived": False, "mode": "fast", "artifact_ids": [], "created_at": stamp, "updated_at": stamp, "revision": 1, })) if binding is None: request.store.put(CONTEXT_FAMILY, { "schema": "hux.context_binding.v1", "id": body["session_id"], "owner": request.identity.subject, "session_id": body["session_id"], "conversation_id": body["conversation_id"], "project_id": body["project_id"], "created_at": stamp, }) if not any(row.get("key") == idem for row in request.store.read(CONTEXT_FAMILY, "idempotency")): request.store.append(CONTEXT_FAMILY, "idempotency", {"key": idem, **expected}) request.audit("foundation.bootstrap", body["session_id"], reason="created" if any(created.values()) else "replayed") return _response(request, project, conversation, created) def capabilities(request: Request) -> Response: """``GET /hux/v1/capabilities``: what this tenant may call.""" request.audit("foundation.capabilities", "capabilities") return Response(200, request.flags.capabilities(request.identity, request.flags_build)) def manifest(request: Request) -> Response: """``GET /hux/v1/manifest``: the on-disk layout version for rollback readers.""" request.audit("foundation.manifest", "manifest") return Response(200, request.store.manifest(CONTRACT_VERSION)) def register(router: Router) -> None: """Attach HUX-11 routes.""" router.add("GET", "/hux/v1/capabilities", "HUX-11", "foundation.capabilities", capabilities) router.add("GET", "/hux/v1/manifest", "HUX-11", "foundation.manifest", manifest) router.add("POST", "/hux/v1/context/bootstrap", "HUX-11", "foundation.bootstrap", bootstrap_context, 4096)