186 lines
9.4 KiB
Python
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)
|