"""HUX-04 artifact workspace: typed, versioned, diffable outputs. Every artifact is one ``hux.artifact.v1`` document under the caller's tenant subtree. Content lives in the content-addressed blob store; the document only carries ``content_ref`` hashes, so a version can never be rewritten once it has been appended (SO-30). Blobs are served inside a JSON envelope with ``nosniff`` and never as an executable response type (SO-32). Every referenced id is resolved under the caller's own subtree (SO-33); a foreign or unknown id is ``404`` so ids cannot be probed. Sharing is out of scope for this increment: ``access.mode`` is always ``owner`` (SO-34). """ from __future__ import annotations import base64 import hashlib from typing import Any from hux import diffs from hux.contracts import load_all, validate_record from hux.errors import Conflict, Invalid, NotFound, TooLarge from hux.http import Request, Response, Router, page from hux.identity import Identity from hux.store import TenantStore, check_id, new_id, now_iso MAX_UPLOAD_BODY = 34 * 1024 * 1024 # 25 MiB of content plus base64 overhead (SO-31, SO-54) def _project_exists(store, project_id: str) -> bool: """Ask the organization family when it is present; otherwise only the id shape is known.""" try: from hux.organization import project_exists except ModuleNotFoundError: # pragma: no cover - organization is always shipped with artifacts return True return project_exists(store, project_id) FAMILY = "artifacts" MAX_VERSION_BYTES = 25 * 1024 * 1024 MAX_VERSIONS = 200 MAX_ARTIFACTS = 2000 PAGE_SIZE = 50 ARTIFACT_TYPES = ("markdown", "code", "html", "svg", "image", "json", "csv", "document", "audio") SENSITIVITIES = ("public", "personal", "sensitive", "restricted") _SCHEMAS: dict[str, dict[str, Any]] = {} def _schemas() -> dict[str, dict[str, Any]]: if not _SCHEMAS: _SCHEMAS.update(load_all()) return _SCHEMAS # -- helpers shared with other lanes ------------------------------------------- def artifact_exists(store: TenantStore, artifact_id: str) -> bool: """True when ``artifact_id`` is a well-formed id owned by the store's tenant.""" try: return store.exists(FAMILY, artifact_id) except Invalid: return False def artifact_titles(store: TenantStore, ids: list[str]) -> dict[str, str]: """Map each resolvable artifact id to its title; unknown ids are omitted.""" titles: dict[str, str] = {} for artifact_id in ids: if artifact_exists(store, artifact_id): titles[artifact_id] = store.get(FAMILY, artifact_id)["title"] return titles def emit_event(store: TenantStore, identity: Identity, artifact: dict[str, Any], kind: str, summary: str, detail: dict[str, Any]) -> None: """Record an activity event through the events lane when it is present. Imported lazily so module import order never matters; a missing events module (earlier build) is not an error because the artifact write itself is the source of truth. """ conversation_id = artifact.get("conversation_id") if conversation_id is None: return try: from hux.events import emit except ModuleNotFoundError: return version = artifact["current_version"] evidence = [{"kind": "artifact_version", "id": f"{artifact['id']}@{version}", "hash": artifact["versions"][-1]["content_ref"]["hash"]}] emit(store, identity, conversation_id, kind, summary, detail=detail, evidence=evidence, sensitivity=artifact["sensitivity"]) def replay(store: TenantStore, family: str, key: str) -> str | None: """Record id previously created under an Idempotency-Key, if any.""" if not key: return None for row in store.read(family, "idempotency"): if row.get("key") == key: return row["id"] return None def remember(store: TenantStore, family: str, key: str, record_id: str) -> None: """Persist an Idempotency-Key to record id mapping.""" if key: store.append(family, "idempotency", {"key": key, "id": record_id, "at": now_iso()}) # -- record shaping ------------------------------------------------------------ def _actor(identity: Identity) -> dict[str, str]: if identity.trust == "worker": return {"type": "system", "id": "worker"} return {"type": "user", "id": identity.subject} def _body(request: Request) -> dict[str, Any]: if not isinstance(request.body, dict): raise Invalid("body must be a JSON object") return request.body def _string(body: dict[str, Any], key: str, limit: int, required: bool = True) -> str | None: value = body.get(key) if value is None: if required: raise Invalid(f"{key} is required") return None if not isinstance(value, str) or not value.strip() or len(value) > limit: raise Invalid(f"{key} must be a non-empty string of at most {limit} characters") return value def secret_hits(data: bytes) -> list[str]: """Secret-pattern classes found in text content; binary content is not scanned (SO-12).""" from hux.redaction import scrub_text try: text = data.decode("utf-8") except UnicodeDecodeError: return [] return scrub_text(text)[1] def _content(body: dict[str, Any]) -> tuple[bytes, str]: """Decode the submitted content and return ``(bytes, mime)``.""" text, encoded = body.get("content"), body.get("content_base64") if isinstance(text, str) and encoded is None: data = text.encode("utf-8") elif isinstance(encoded, str) and text is None: try: data = base64.b64decode(encoded, validate=True) except (ValueError, TypeError) as error: raise Invalid("content_base64 is not valid base64") from error else: raise Invalid("exactly one of content (utf-8 text) or content_base64 is required") if len(data) > MAX_VERSION_BYTES: raise TooLarge("version exceeds 25 MiB") mime = _string(body, "mime", 120, required=False) or "application/octet-stream" return data, mime def _store_content(store: TenantStore, body: dict[str, Any]) -> dict[str, Any]: """Hash, verify against any client-supplied hash, store the blob, return ``content_ref``.""" data, mime = _content(body) digest = hashlib.sha256(data).hexdigest() claimed = body.get("hash") if claimed is not None and claimed != f"sha256:{digest}": raise Invalid("hash does not match the submitted content") store.put_blob(digest, data) return {"hash": f"sha256:{digest}", "bytes": len(data), "mime": mime} def _optional_id(body: dict[str, Any], key: str) -> str | None: value = body.get(key) return None if value is None else check_id(value) def _load_owned(request: Request) -> dict[str, Any]: """The artifact named in the path; unknown, malformed or foreign is 404.""" try: artifact = request.store.get(FAMILY, request.params["id"]) except Invalid as error: raise NotFound("artifact not found") from error if artifact.get("owner") != request.identity.subject: raise NotFound("artifact not found") return artifact def _version(artifact: dict[str, Any], number: Any) -> dict[str, Any]: if not isinstance(number, int) or isinstance(number, bool): raise Invalid("version must be an integer") for entry in artifact["versions"]: if entry["version"] == number: return entry raise NotFound(f"version {number} not found") def _lineage(request: Request, body: dict[str, Any]) -> dict[str, Any] | None: """Resolve ``lineage`` under the caller's subtree; anything else is 404 (SO-33).""" lineage = body.get("lineage") if lineage is None: return None if not isinstance(lineage, dict): raise Invalid("lineage must be an object") try: parent = request.store.get(FAMILY, check_id(lineage.get("artifact_id"))) except (Invalid, NotFound) as error: raise NotFound("lineage artifact not found") from error if parent.get("owner") != request.identity.subject: raise NotFound("lineage artifact not found") version = _version(parent, lineage.get("version")) return {"artifact_id": parent["id"], "version": version["version"]} def append_version(store: TenantStore, artifact: dict[str, Any], entry: dict[str, Any], expected_revision: int | None) -> dict[str, Any]: """Append an immutable version entry and persist the document. The version number must be exactly ``current_version + 1``; any attempt to write an existing number is a Conflict so history is never rewritten. """ if any(v["version"] == entry["version"] for v in artifact["versions"]): raise Conflict(f"version {entry['version']} already exists") if entry["version"] != artifact["current_version"] + 1: raise Conflict("versions are appended in order") if len(artifact["versions"]) >= MAX_VERSIONS: raise TooLarge("artifact has reached 200 versions") updated = {**artifact, "versions": [*artifact["versions"], entry], "current_version": entry["version"], "updated_at": now_iso()} _check(updated) return store.put(FAMILY, updated, expected_revision=expected_revision) def _check(artifact: dict[str, Any]) -> None: problems = validate_record(artifact, _schemas()) if problems: raise Invalid("artifact does not satisfy hux.artifact.v1", problems) # -- handlers ------------------------------------------------------------------ def create(request: Request) -> Response: """``POST /hux/v1/artifacts``: create an artifact with version 1.""" body = _body(request) key = request.idempotency_key() with request.store.lock(FAMILY): existing = replay(request.store, FAMILY, key) if existing is not None: request.audit("artifacts.create", existing, reason="idempotent_replay") return Response(200, request.store.get(FAMILY, existing), {"HUX-Replayed": "true"}) if request.store.count(FAMILY) >= MAX_ARTIFACTS: raise TooLarge("tenant has reached 2000 artifacts") artifact_type = _string(body, "type", 40) if artifact_type not in ARTIFACT_TYPES: raise Invalid("unknown artifact type") sensitivity = body.get("sensitivity", "personal") if sensitivity not in SENSITIVITIES: raise Invalid("unknown sensitivity") hits = secret_hits(_content(body)[0]) if hits: sensitivity = "restricted" stamp = now_iso() version: dict[str, Any] = {"version": 1, "created_at": stamp, "created_by": _actor(request.identity), "content_ref": _store_content(request.store, body)} for field, limit in (("message_id", 120), ("note", 200)): if _string(body, field, limit, required=False) is not None: version[field] = body[field] lineage = _lineage(request, body) if lineage is not None: version["lineage"] = lineage artifact: dict[str, Any] = { "schema": "hux.artifact.v1", "id": new_id("art"), "owner": request.identity.subject, "type": artifact_type, "title": _string(body, "title", 200), "current_version": 1, "versions": [version], "sensitivity": sensitivity, "created_at": stamp, "updated_at": stamp, "revision": 1, "access": {"mode": "owner"}, } for field in ("conversation_id", "project_id"): if _optional_id(body, field) is not None: artifact[field] = body[field] if _string(body, "language", 40, required=False) is not None: artifact["language"] = body["language"] _check(artifact) stored = request.store.put(FAMILY, artifact) remember(request.store, FAMILY, key, stored["id"]) request.audit("artifacts.create", stored["id"], reason="secret_pattern_restricted" if hits else "") emit_event(request.store, request.identity, stored, "artifact.created", f"Created {stored['type']} artifact", {"artifact_id": stored["id"], "version": 1}) return Response(201, stored, {"ETag": str(stored["revision"])}) def list_artifacts(request: Request) -> Response: """``GET /hux/v1/artifacts?conversation_id=&project_id=&cursor=``: owned artifacts, oldest first.""" filters = {k: request.query[k] for k in ("conversation_id", "project_id") if request.query.get(k)} cursor = request.query.get("cursor", "0") if not cursor.isdigit(): raise Invalid("cursor must be a non-negative integer") items = [ artifact for artifact in request.store.scan(FAMILY) if artifact.get("owner") == request.identity.subject and all(artifact.get(k) == v for k, v in filters.items()) ] start = int(cursor) window = items[start : start + PAGE_SIZE] request.audit("artifacts.list", "artifacts") return page(window, str(start + PAGE_SIZE) if len(items) > start + PAGE_SIZE else None) def get(request: Request) -> Response: """``GET /hux/v1/artifacts/{id}``: one artifact document.""" artifact = _load_owned(request) request.audit("artifacts.get", artifact["id"]) return Response(200, artifact, {"ETag": str(artifact["revision"])}) def add_version(request: Request) -> Response: """``POST /hux/v1/artifacts/{id}/versions``: append a new immutable version (If-Match required).""" body = _body(request) expected = request.if_match() if expected is None: raise Invalid("If-Match is required to append a version") key = request.idempotency_key() with request.store.lock(FAMILY): artifact = _load_owned(request) existing = replay(request.store, FAMILY, key) if existing is not None and existing.split("@")[0] == artifact["id"]: request.audit("artifacts.version", existing, reason="idempotent_replay") return Response(200, artifact, {"HUX-Replayed": "true", "ETag": str(artifact["revision"])}) if expected != artifact["revision"]: raise Conflict(f"revision {expected} does not match current revision {artifact['revision']}", [str(artifact["revision"])]) number = artifact["current_version"] + 1 hits = secret_hits(_content(body)[0]) if hits: artifact = {**artifact, "sensitivity": "restricted"} entry: dict[str, Any] = {"version": number, "created_at": now_iso(), "created_by": _actor(request.identity), "content_ref": _store_content(request.store, body), "diff_from": artifact["current_version"]} if body.get("diff_from") is not None: entry["diff_from"] = _version(artifact, body["diff_from"])["version"] for field, limit in (("message_id", 120), ("note", 200)): if _string(body, field, limit, required=False) is not None: entry[field] = body[field] lineage = _lineage(request, body) if lineage is not None: entry["lineage"] = lineage stored = append_version(request.store, artifact, entry, expected) remember(request.store, FAMILY, key, f"{stored['id']}@{number}") request.audit("artifacts.version", f"{stored['id']}@{number}", reason="secret_pattern_restricted" if hits else "") emit_event(request.store, request.identity, stored, "artifact.version", f"New version {number}", {"artifact_id": stored["id"], "version": number}) return Response(201, stored, {"ETag": str(stored["revision"])}) def _version_number(request: Request) -> int: raw = request.params["n"] if not raw.isdigit() or int(raw) < 1: raise Invalid("version must be a positive integer") return int(raw) def get_version(request: Request) -> Response: """``GET /hux/v1/artifacts/{id}/versions/{n}``: version metadata plus content.""" artifact = _load_owned(request) entry = _version(artifact, _version_number(request)) data = request.store.get_blob(entry["content_ref"]["hash"].split(":", 1)[1]) text = diffs.decode_text(data) if diffs.is_text_type(artifact["type"]) else None body: dict[str, Any] = {"artifact_id": artifact["id"], "type": artifact["type"], "version": entry} if text is None: body["content_base64"] = base64.b64encode(data).decode("ascii") else: body["content"] = text request.audit("artifacts.get_version", f"{artifact['id']}@{entry['version']}") headers = {"X-Content-Type-Options": "nosniff", "Content-Disposition": "attachment", "ETag": str(artifact["revision"])} return Response(200, body, headers) def diff(request: Request) -> Response: """``GET /hux/v1/artifacts/{id}/versions/{n}/diff?from=``: unified diff for text, sizes and hashes otherwise.""" artifact = _load_owned(request) to_entry = _version(artifact, _version_number(request)) raw_from = request.query.get("from", "") if raw_from and not raw_from.isdigit(): raise Invalid("from must be a version number") from_number = int(raw_from) if raw_from else to_entry.get("diff_from", to_entry["version"]) from_entry = _version(artifact, from_number) older = request.store.get_blob(from_entry["content_ref"]["hash"].split(":", 1)[1]) newer = request.store.get_blob(to_entry["content_ref"]["hash"].split(":", 1)[1]) request.audit("artifacts.diff", f"{artifact['id']}@{from_number}..{to_entry['version']}") return Response(200, diffs.unified(artifact["type"], older, newer, from_number, to_entry["version"]), {"X-Content-Type-Options": "nosniff"}) def promote(request: Request) -> Response: """``POST /hux/v1/artifacts/{id}/promote``: mark the current version as the project's copy.""" body = _body(request) project_id = check_id(body.get("project_id")) if not _project_exists(request.store, project_id): raise NotFound("project not found") expected = request.if_match() with request.store.lock(FAMILY): artifact = _load_owned(request) if expected is not None and expected != artifact["revision"]: raise Conflict(f"revision {expected} does not match current revision {artifact['revision']}", [str(artifact["revision"])]) version = artifact["current_version"] if body.get("version") is not None: version = _version(artifact, body["version"])["version"] stamp = now_iso() updated = {**artifact, "project_id": project_id, "promotion": {"project_id": project_id, "version": version, "at": stamp}, "updated_at": stamp} _check(updated) stored = request.store.put(FAMILY, updated, expected_revision=artifact["revision"]) request.audit("artifacts.promote", f"{stored['id']}@{version}", reason="" if expected is not None else "unconditional_write") emit_event(request.store, request.identity, stored, "artifact.promoted", f"Promoted version {version} to project", {"artifact_id": stored["id"], "version": version, "project_id": project_id}) return Response(200, stored, {"ETag": str(stored["revision"])}) def register(router: Router) -> None: """Attach HUX-04 routes.""" card = "HUX-04" router.add("POST", "/hux/v1/artifacts", card, "artifacts.create", create, max_body=MAX_UPLOAD_BODY) router.add("GET", "/hux/v1/artifacts", card, "artifacts.list", list_artifacts) router.add("GET", "/hux/v1/artifacts/{id}", card, "artifacts.get", get) router.add("POST", "/hux/v1/artifacts/{id}/versions", card, "artifacts.version", add_version, max_body=MAX_UPLOAD_BODY) router.add("GET", "/hux/v1/artifacts/{id}/versions/{n}", card, "artifacts.get_version", get_version) router.add("GET", "/hux/v1/artifacts/{id}/versions/{n}/diff", card, "artifacts.diff", diff) router.add("POST", "/hux/v1/artifacts/{id}/promote", card, "artifacts.promote", promote)