"""HUX-07 scoped multimodal metadata, lineage and transcript corrections. This service never accepts media bytes and has no capture endpoint. It only records metadata after an approved autonomy action, plus inert camera/screen intents that a human-facing client may send through the HUX-05 approval lane. """ from __future__ import annotations import hashlib import json from typing import Any from hux import contracts, redaction from hux.errors import Conflict, Forbidden, Invalid, NotFound, TooLarge from hux.http import Request, Response, Router, page from hux.store import check_id, new_id, now_iso CARD = "HUX-07" ITEMS = "multimodal_items" CORRECTIONS = "transcript_corrections" INTENTS = "capture_intents" MAX_ITEMS = 2000 MAX_CORRECTIONS = 10000 MAX_INTENTS = 1000 EXECUTABLE_MIMES = frozenset({"text/html", "application/xhtml+xml", "image/svg+xml", "application/xml", "text/xml"}) SAFE_SUFFIXES = (".jpg", ".jpeg", ".png", ".webp", ".wav", ".webm", ".ogg", ".mp4", ".pdf", ".txt") KIND_MIMES = { "image": frozenset({"image/jpeg", "image/png", "image/webp"}), "audio": frozenset({"audio/wav", "audio/webm", "audio/ogg"}), "video": frozenset({"video/webm", "video/mp4"}), "document": frozenset({"application/pdf", "text/plain"}), } ITEM_FIELDS = frozenset({"project_id", "kind", "source", "filename", "mime", "bytes", "hash", "approval_id", "lineage"}) SCHEMAS = contracts.load_all() SCHEMAS["multimodal.schema.json"] = contracts.load_schema("multimodal.schema.json") def _body(request: Request, allowed: frozenset[str]) -> dict[str, Any]: if not isinstance(request.body, dict): raise Invalid("body must be a JSON object") extra = set(request.body) - allowed if extra: raise Invalid("unexpected multimodal fields", sorted(extra)) return request.body def _scope(request: Request, project_id: Any) -> dict[str, Any]: from hux import organization try: conversation = request.store.get(organization.CONVERSATIONS, request.params["id"]) except (Invalid, NotFound) as error: raise NotFound("project or 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") if not organization.project_exists(request.store, project_id): raise NotFound("project or conversation not found") return conversation def _if_match(request: Request, current: int) -> None: expected = request.if_match() if expected is None: raise Invalid("If-Match is required") if expected != current: raise Conflict("revision does not match If-Match") def _key(request: Request) -> str: key = request.idempotency_key() if not key: raise Invalid("Idempotency-Key is required") return key def _digest(body: dict[str, Any]) -> str: return hashlib.sha256(json.dumps(body, sort_keys=True, separators=(",", ":")).encode()).hexdigest() def _replay(request: Request, family: str, key: str, digest: str) -> dict[str, Any] | None: for row in request.store.read(family, "idempotency"): if row.get("key") != key: continue if row.get("digest") != digest: raise Conflict("Idempotency-Key was already used with different metadata") return request.store.get(family, row["id"]) return None def _remember(request: Request, family: str, key: str, digest: str, record_id: str) -> None: request.store.append(family, "idempotency", {"key": key, "digest": digest, "id": record_id, "at": now_iso()}) def _validate(record: dict[str, Any], pointer: str) -> None: problems = contracts.validate("multimodal.schema.json", record, SCHEMAS, pointer) if problems: raise Invalid("multimodal record failed contract validation", problems) def _approval(request: Request, approval_id: Any, source: Any) -> str: from hux import policy try: approval = policy.load_approval(request.store, check_id(approval_id)) except (Invalid, NotFound) as error: raise Forbidden("approved autonomy action is required") from error capability = "external_side_effect" if source in {"camera", "screen"} else "artifact_write" if approval.get("status") != "approved" or approval.get("conversation_id") != request.params["id"]: raise Forbidden("approved autonomy action is required") if approval.get("capability") != capability: raise Forbidden("approval does not cover this multimodal action") return approval["id"] def _lineage(request: Request, body: Any, project_id: str) -> dict[str, Any] | None: if body is None: return None if not isinstance(body, dict) or set(body) - {"parent_item_id", "artifact_id", "artifact_version"}: raise Invalid("lineage must contain only a parent item or artifact version") if "parent_item_id" in body: if len(body) != 1: raise Invalid("parent-item lineage cannot also name an artifact") parent = request.store.get(ITEMS, check_id(body["parent_item_id"])) if parent["conversation_id"] != request.params["id"] or parent["project_id"] != project_id: raise NotFound("lineage item not found") return {"parent_item_id": parent["id"]} if set(body) != {"artifact_id", "artifact_version"} or not isinstance(body["artifact_version"], int): raise Invalid("artifact lineage needs artifact_id and artifact_version") from hux import artifacts artifact = request.store.get(artifacts.FAMILY, check_id(body["artifact_id"])) if artifact.get("project_id") != project_id or artifact.get("conversation_id") != request.params["id"]: raise NotFound("lineage artifact not found") version = next((row for row in artifact["versions"] if row["version"] == body["artifact_version"]), None) if version is None: raise NotFound("lineage artifact version not found") if version["content_ref"]["mime"].lower() in EXECUTABLE_MIMES or artifact.get("type") in {"html", "svg"}: raise Invalid("executable HTML and SVG lineage is not accepted") return {"artifact_id": artifact["id"], "artifact_version": version["version"]} def create_item(request: Request) -> Response: """Register metadata for approved media; bytes are never accepted here.""" body = _body(request, ITEM_FIELDS) conversation = _scope(request, body.get("project_id")) key, digest = _key(request), _digest(body) with request.store.lock(ITEMS): replayed = _replay(request, ITEMS, key, digest) if replayed is not None: return Response(200, replayed, {"ETag": str(replayed["revision"]), "HUX-Replayed": "true", "Cache-Control": "no-store"}) _if_match(request, 0) if request.store.count(ITEMS) >= MAX_ITEMS: raise TooLarge("multimodal item limit reached") filename, mime = body.get("filename"), body.get("mime") if not isinstance(filename, str) or not filename.lower().endswith(SAFE_SUFFIXES): raise Invalid("filename is missing or executable") if not isinstance(mime, str) or mime.lower() in EXECUTABLE_MIMES: raise Invalid("executable HTML, XML and SVG are not accepted") if mime.lower() not in KIND_MIMES.get(body.get("kind"), frozenset()): raise Invalid("kind and MIME type do not agree") approval_id = _approval(request, body.get("approval_id"), body.get("source")) lineage = _lineage(request, body.get("lineage"), body["project_id"]) record = { "schema": "hux.multimodal_item.v1", "id": new_id("mmi"), "owner": request.identity.subject, "project_id": body["project_id"], "conversation_id": conversation["id"], "kind": body.get("kind"), "source": body.get("source"), "filename": filename, "mime": mime.lower(), "bytes": body.get("bytes"), "hash": body.get("hash"), "approval_id": approval_id, "status": "metadata_only", "created_at": now_iso(), } if lineage: record["lineage"] = lineage _validate({**record, "revision": 1}, "/$defs/item") stored = request.store.put(ITEMS, record, expected_revision=0) _remember(request, ITEMS, key, digest, stored["id"]) request.audit("multimodal.create", stored["id"]) return Response(201, stored, {"ETag": "1", "Cache-Control": "no-store"}) def list_items(request: Request) -> Response: """List metadata in one project/conversation scope.""" project_id = request.params["project_id"] _scope(request, project_id) items = [row for row in request.store.scan(ITEMS) if row["project_id"] == project_id and row["conversation_id"] == request.params["id"]] request.audit("multimodal.list", request.params["id"]) response = page(items) response.headers["Cache-Control"] = "no-store" return response def get_item(request: Request) -> Response: """Read one metadata record only within its path scope.""" project_id = request.params["project_id"] _scope(request, project_id) try: item = request.store.get(ITEMS, check_id(request.params["item_id"])) except (Invalid, NotFound) as error: raise NotFound("multimodal item not found") from error if item["project_id"] != project_id or item["conversation_id"] != request.params["id"]: raise NotFound("multimodal item not found") request.audit("multimodal.read", item["id"]) return Response(200, item, {"ETag": str(item["revision"]), "Cache-Control": "no-store"}) def correct_transcript(request: Request) -> Response: """Append an immutable transcript correction and point the media head to it.""" body = _body(request, frozenset({"project_id", "replacement_text"})) _scope(request, body.get("project_id")) key, digest = _key(request), _digest({**body, "item_id": request.params["item_id"]}) with request.store.lock(CORRECTIONS): replayed = _replay(request, CORRECTIONS, key, digest) if replayed is not None: return Response(200, replayed, {"HUX-Replayed": "true", "Cache-Control": "no-store"}) item = request.store.get(ITEMS, check_id(request.params["item_id"])) if item["project_id"] != body["project_id"] or item["conversation_id"] != request.params["id"]: raise NotFound("multimodal item not found") if item["kind"] not in {"audio", "video"}: raise Invalid("only audio or video transcripts can be corrected") _if_match(request, item["revision"]) text = body.get("replacement_text") if not isinstance(text, str) or not text.strip() or len(text) > 10000: raise Invalid("replacement_text must contain 1 to 10000 characters") if request.store.count(CORRECTIONS) >= MAX_CORRECTIONS: raise TooLarge("transcript correction limit reached") record = { "schema": "hux.transcript_correction.v1", "id": new_id("trc"), "owner": request.identity.subject, "project_id": body["project_id"], "conversation_id": request.params["id"], "item_id": item["id"], "replacement_text": text, "created_at": now_iso(), } _validate({**record, "revision": 1}, "/$defs/correction") stored = request.store.put(CORRECTIONS, record, expected_revision=0) with request.store.lock(ITEMS): updated = request.store.put(ITEMS, {**item, "latest_correction_id": stored["id"]}, item["revision"]) _remember(request, CORRECTIONS, key, digest, stored["id"]) request.audit("multimodal.correct", stored["id"]) return Response(201, stored, {"ETag": str(updated["revision"]), "Cache-Control": "no-store"}) def create_capture_intent(request: Request) -> Response: """Record an inert camera/screen proposal; it never grants execution.""" body = _body(request, frozenset({"project_id", "source", "purpose"})) _scope(request, body.get("project_id")) key, digest = _key(request), _digest(body) with request.store.lock(INTENTS): replayed = _replay(request, INTENTS, key, digest) if replayed is not None: return Response(200, replayed, {"HUX-Replayed": "true", "Cache-Control": "no-store"}) _if_match(request, 0) if request.store.count(INTENTS) >= MAX_INTENTS: raise TooLarge("capture intent limit reached") purpose = body.get("purpose") if body.get("source") not in {"camera", "screen"} or not isinstance(purpose, str) or not purpose.strip(): raise Invalid("source and purpose are required") purpose = redaction.scrub_text(purpose[:280])[0] record = { "schema": "hux.capture_intent.v1", "id": new_id("cap"), "owner": request.identity.subject, "project_id": body["project_id"], "conversation_id": request.params["id"], "source": body["source"], "purpose": purpose, "status": "proposed", "requires_approval": True, "execution_allowed": False, "created_at": now_iso(), } _validate({**record, "revision": 1}, "/$defs/capture_intent") stored = request.store.put(INTENTS, record, expected_revision=0) _remember(request, INTENTS, key, digest, stored["id"]) request.audit("multimodal.intent", stored["id"]) return Response(201, stored, {"ETag": "1", "Cache-Control": "no-store"}) def register(router: Router) -> None: """Attach metadata-only HUX-07 routes.""" base = "/hux/v1/projects/{project_id}/conversations/{id}" router.add("POST", base + "/multimodal/items", CARD, "multimodal.create", create_item) router.add("GET", base + "/multimodal/items", CARD, "multimodal.list", list_items) router.add("GET", base + "/multimodal/items/{item_id}", CARD, "multimodal.read", get_item) router.add("POST", base + "/multimodal/items/{item_id}/transcript-corrections", CARD, "multimodal.correct", correct_transcript) router.add("POST", base + "/capture-intents", CARD, "multimodal.intent", create_capture_intent)