186 lines
9.4 KiB
Python

"""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)