Spaces:
Running
Running
Download _utils/_share_contract.py from scikit-plots/ai: direct link, hf CLI and curl.
- Browser
- Download file 44.1 kB
-
https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_share_contract.py
- Command line
-
hf download hf://spaces/scikit-plots/ai/_utils/_share_contract.py
-
curl -L -o _share_contract.py https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_share_contract.py
44.1 kB
| """ | |
| Security contract for server-backed conversation shares. | |
| The browser is untrusted. Share requests carry structured conversation data and | |
| an allowlisted representation id; callers never choose response MIME types or | |
| submit rendered HTML for the server to host. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import hmac | |
| import html | |
| import json | |
| import math | |
| import re | |
| import secrets | |
| from typing import Any | |
| from urllib.parse import urlsplit, urlunsplit | |
| SHARE_SCHEMA_VERSION = "2.1" | |
| SHARE_ACCEPTED_SCHEMA_VERSIONS = frozenset({"2.0", "2.1"}) | |
| SHARE_FORMATS: dict[str, tuple[str, str]] = { | |
| "html": ("text/html; charset=utf-8", ".html"), | |
| "json": ("application/json; charset=utf-8", ".json"), | |
| "txt": ("text/plain; charset=utf-8", ".txt"), | |
| "yaml": ("application/yaml", ".yaml"), | |
| "toml": ("application/toml", ".toml"), | |
| } | |
| def share_artifact_filename(fmt: str) -> str: | |
| """ | |
| Return the stable human-facing filename for a Global Share artifact. | |
| The public read capability is intentionally excluded from the filename. | |
| """ | |
| fmt = validate_share_format(fmt) | |
| _mime, ext = SHARE_FORMATS[fmt] | |
| return f"ai-conversation-global-share-{fmt}{ext}" | |
| MAX_SHARE_RECORDS = 1000 | |
| MAX_SHARE_TEXT_CHARS = 200_000 | |
| MAX_SHARE_METADATA_CHARS = 2048 | |
| MAX_SHARE_RESOURCES_PER_MESSAGE = 512 | |
| MAX_SHARE_RESOURCES_TOTAL = 4096 | |
| MAX_SAFE_INTEGER = 9_007_199_254_740_991 | |
| _SHARE_ID_RE = re.compile( | |
| r"^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$" | |
| ) | |
| class ShareValidationError(ValueError): | |
| """Raised when an untrusted share snapshot violates the public contract.""" | |
| def _bounded_string( | |
| value: Any, *, limit: int, field: str, nullable: bool = True | |
| ) -> str | None: | |
| if value is None and nullable: | |
| return None | |
| if not isinstance(value, str): | |
| raise ShareValidationError( | |
| f"{field} must be a string" + (" or null" if nullable else "") | |
| ) | |
| if len(value) > limit: | |
| raise ShareValidationError(f"{field} is too long") | |
| return value | |
| def _bounded_int(value: Any, *, field: str, nullable: bool = True) -> int | None: | |
| if value is None and nullable: | |
| return None | |
| if isinstance(value, bool) or not isinstance(value, int): | |
| raise ShareValidationError( | |
| f"{field} must be an integer" + (" or null" if nullable else "") | |
| ) | |
| return value | |
| def _safe_scalar(value: Any, *, field: str) -> str | int | float | bool | None: | |
| if value is None or isinstance(value, (str, bool, int)): | |
| if isinstance(value, str) and len(value) > MAX_SHARE_METADATA_CHARS: | |
| raise ShareValidationError(f"{field} is too long") | |
| return value | |
| if isinstance(value, float) and math.isfinite(value): | |
| return value | |
| raise ShareValidationError(f"{field} must be a finite primitive value") | |
| def sanitize_share_page_url(value: Any) -> str: | |
| """Return an HTTP(S) source URL without credentials, query, or fragment.""" | |
| if not isinstance(value, str) or not value: | |
| return "" | |
| if len(value) > 8192: # ruff: ignore[magic-value-comparison] | |
| return "" | |
| try: | |
| parts = urlsplit(value) | |
| except ValueError: | |
| return "" | |
| if parts.scheme.lower() not in {"http", "https"} or not parts.hostname: | |
| return "" | |
| host = parts.hostname | |
| if ":" in host and not host.startswith("["): | |
| host = f"[{host}]" | |
| try: | |
| port = parts.port | |
| except ValueError: | |
| return "" | |
| if port is not None: | |
| host = f"{host}:{port}" | |
| return urlunsplit((parts.scheme.lower(), host, parts.path or "/", "", "")) | |
| _RESOURCE_KINDS = frozenset( | |
| { | |
| "text", | |
| "image", | |
| "vector_image", | |
| "audio", | |
| "video", | |
| "data", | |
| "file", | |
| "replay", | |
| "page", | |
| "pdf", | |
| "archive", | |
| } | |
| ) | |
| _RESOURCE_DELIVERIES = frozenset({"context", "raw", "not_sent"}) | |
| _RESOURCE_INTENTS = frozenset({"", "auto", "raw", "extract", "context"}) | |
| def _resource_count( | |
| value: Any, *, field: str, maximum: int = MAX_SHARE_RESOURCES_PER_MESSAGE | |
| ) -> int: | |
| if ( | |
| isinstance(value, bool) | |
| or not isinstance(value, int) | |
| or value < 0 | |
| or value > maximum | |
| ): | |
| raise ShareValidationError( | |
| f"{field} must be an integer between 0 and {maximum}" | |
| ) | |
| return value | |
| def _resource_bool(value: Any, *, field: str) -> bool: | |
| if not isinstance(value, bool): | |
| raise ShareValidationError(f"{field} must be a boolean") | |
| return value | |
| def _safe_relative_path(value: Any, *, field: str) -> str: | |
| text = _bounded_string(value, limit=1024, field=field) or "" | |
| if not text: | |
| return "" | |
| text = text.replace("\\", "/") | |
| if text.startswith("/") or re.match(r"^[A-Za-z]:", text): | |
| return "" | |
| parts = [part for part in text.split("/") if part not in {"", "."}] | |
| if any( | |
| part == ".." # lint | |
| or any(ord(ch) < 32 for ch in part) # ruff: ignore[magic-value-comparison] | |
| for part in parts | |
| ): | |
| return "" | |
| return "/".join(parts) | |
| def _safe_resource_name(value: Any, *, field: str, limit: int = 512) -> str: | |
| text = _bounded_string(value, limit=limit, field=field, nullable=False) or "" | |
| return ( | |
| "".join( | |
| ch # lint | |
| for ch in text # lint | |
| if ord(ch) >= 32 # ruff: ignore[magic-value-comparison] | |
| ) | |
| .replace("/", "_") | |
| .replace("\\", "_")[:limit] | |
| ) | |
| def _canonical_resource_item( | |
| raw: Any, record_index: int, item_index: int, *, allow_source_urls: bool | |
| ) -> dict[str, Any]: | |
| prefix = f"records[{record_index}].resources.items[{item_index}]" | |
| if not isinstance(raw, dict): | |
| raise ShareValidationError(f"{prefix} must be an object") | |
| kind = _bounded_string( | |
| raw.get("kind"), limit=32, field=f"{prefix}.kind", nullable=False | |
| ) | |
| if kind not in _RESOURCE_KINDS: | |
| raise ShareValidationError(f"{prefix}.kind is not allowed") | |
| delivery = _bounded_string( | |
| raw.get("delivery"), limit=32, field=f"{prefix}.delivery", nullable=False | |
| ) | |
| if delivery not in _RESOURCE_DELIVERIES: | |
| raise ShareValidationError(f"{prefix}.delivery is not allowed") | |
| intent = ( | |
| _bounded_string(raw.get("intent"), limit=32, field=f"{prefix}.intent") or "" | |
| ) | |
| if intent not in _RESOURCE_INTENTS: | |
| raise ShareValidationError(f"{prefix}.intent is not allowed") | |
| size = _bounded_int(raw.get("size"), field=f"{prefix}.size", nullable=False) | |
| line_count = _bounded_int( | |
| raw.get("lineCount"), field=f"{prefix}.lineCount", nullable=False | |
| ) | |
| if size is None or size < 0 or size > MAX_SAFE_INTEGER: | |
| raise ShareValidationError(f"{prefix}.size is out of range") | |
| if ( | |
| line_count is None # lint | |
| or line_count < 0 # lint | |
| or line_count > 1_000_000 # ruff: ignore[magic-value-comparison] | |
| ): | |
| raise ShareValidationError(f"{prefix}.lineCount is out of range") | |
| badge = ( | |
| _bounded_string( | |
| raw.get("badge"), limit=12, field=f"{prefix}.badge", nullable=False | |
| ) | |
| or "FILE" | |
| ) | |
| badge = re.sub(r"[^A-Za-z0-9+._-]", "", badge).upper()[:12] or "FILE" | |
| modality = ( | |
| _bounded_string(raw.get("modality"), limit=32, field=f"{prefix}.modality") | |
| or kind | |
| ) | |
| modality = re.sub(r"[^A-Za-z0-9_-]", "", modality)[:32] | |
| source_kind = ( | |
| _bounded_string(raw.get("sourceKind"), limit=24, field=f"{prefix}.sourceKind") | |
| or "" | |
| ) | |
| source_kind = re.sub(r"[^A-Za-z0-9_-]", "", source_kind)[:24] | |
| archive_name = ( | |
| _bounded_string( | |
| raw.get("archiveName"), limit=512, field=f"{prefix}.archiveName" | |
| ) | |
| or "" | |
| ) | |
| if archive_name: | |
| archive_name = _safe_resource_name(archive_name, field=f"{prefix}.archiveName") | |
| item = { | |
| "name": _safe_resource_name(raw.get("name"), field=f"{prefix}.name"), | |
| "badge": badge, | |
| "kind": kind, | |
| "size": size, | |
| "lineCount": line_count, | |
| "included": _resource_bool(raw.get("included"), field=f"{prefix}.included"), | |
| "localOnly": _resource_bool(raw.get("localOnly"), field=f"{prefix}.localOnly"), | |
| "delivery": delivery, | |
| "modality": modality, | |
| "intent": intent, | |
| "replay": _resource_bool(raw.get("replay"), field=f"{prefix}.replay"), | |
| "boundedExcerpt": _resource_bool( | |
| raw.get("boundedExcerpt"), field=f"{prefix}.boundedExcerpt" | |
| ), | |
| "status": ( | |
| _bounded_string(raw.get("status"), limit=96, field=f"{prefix}.status") or "" | |
| ), | |
| "type": ( | |
| _bounded_string(raw.get("type"), limit=120, field=f"{prefix}.type") or "" | |
| ), | |
| "relativePath": _safe_relative_path( | |
| raw.get("relativePath"), field=f"{prefix}.relativePath" | |
| ), | |
| "sourceKind": source_kind, | |
| "archiveName": archive_name, | |
| } | |
| if kind == "page": | |
| role = ( | |
| _bounded_string( | |
| raw.get("contextRole"), limit=16, field=f"{prefix}.contextRole" | |
| ) | |
| or "pinned" | |
| ) | |
| item["contextRole"] = "current" if role == "current" else "pinned" | |
| item["sourceUrl"] = ( | |
| sanitize_share_page_url(raw.get("sourceUrl")) if allow_source_urls else "" | |
| ) | |
| return item | |
| def _resource_aggregates(items: list[dict[str, Any]]) -> dict[str, int]: | |
| return { | |
| "includedCount": sum(1 for item in items if item["included"]), | |
| "localOnlyCount": sum(1 for item in items if item["localOnly"]), | |
| "contextCount": sum(1 for item in items if item["delivery"] == "context"), | |
| "rawCount": sum(1 for item in items if item["delivery"] == "raw"), | |
| "notSentCount": sum(1 for item in items if item["delivery"] == "not_sent"), | |
| "pageCount": sum(1 for item in items if item["kind"] == "page"), | |
| "replayCount": sum( | |
| 1 for item in items if item["kind"] == "replay" or item["replay"] | |
| ), | |
| "totalBytes": min(MAX_SAFE_INTEGER, sum(item["size"] for item in items)), | |
| } | |
| def _canonical_resources( | |
| raw: Any, record_index: int, *, allow_source_urls: bool | |
| ) -> dict[str, Any]: | |
| prefix = f"records[{record_index}].resources" | |
| if not isinstance(raw, dict): | |
| raise ShareValidationError(f"{prefix} must be an object") | |
| raw_items = raw.get("items") | |
| if not isinstance(raw_items, list): | |
| raise ShareValidationError(f"{prefix}.items must be an array") | |
| if len(raw_items) > MAX_SHARE_RESOURCES_PER_MESSAGE: | |
| raise ShareValidationError(f"{prefix}.items contains too many resources") | |
| items = [ | |
| _canonical_resource_item( | |
| item, record_index, i, allow_source_urls=allow_source_urls | |
| ) | |
| for i, item in enumerate(raw_items) | |
| ] | |
| derived = _resource_aggregates(items) | |
| total = _resource_count( | |
| raw.get("totalCount", len(items)), field=f"{prefix}.totalCount" | |
| ) | |
| if total < len(items): | |
| raise ShareValidationError(f"{prefix}.totalCount cannot be smaller than items") | |
| def merged(name: str) -> int: | |
| value = _resource_count(raw.get(name, derived[name]), field=f"{prefix}.{name}") | |
| return max(derived[name], min(total, value)) | |
| total_bytes = _bounded_int( | |
| raw.get("totalBytes", derived["totalBytes"]), | |
| field=f"{prefix}.totalBytes", | |
| nullable=False, | |
| ) | |
| if total_bytes is None or total_bytes < 0 or total_bytes > MAX_SAFE_INTEGER: | |
| raise ShareValidationError(f"{prefix}.totalBytes is out of range") | |
| omitted = _resource_count( | |
| raw.get("omittedCount", max(0, total - len(items))), | |
| field=f"{prefix}.omittedCount", | |
| ) | |
| omitted = max(omitted, total - len(items)) | |
| complete = raw.get("complete") | |
| if not isinstance(complete, bool): | |
| raise ShareValidationError(f"{prefix}.complete must be a boolean") | |
| version = _resource_count( | |
| raw.get("version", 2), field=f"{prefix}.version", maximum=2 | |
| ) | |
| if version != 2: # ruff: ignore[magic-value-comparison] | |
| raise ShareValidationError(f"{prefix}.version must be 2") | |
| return { | |
| "version": 2, | |
| "totalCount": total, | |
| "includedCount": merged("includedCount"), | |
| "localOnlyCount": merged("localOnlyCount"), | |
| "contextCount": merged("contextCount"), | |
| "rawCount": merged("rawCount"), | |
| "notSentCount": merged("notSentCount"), | |
| "pageCount": merged("pageCount"), | |
| "replayCount": merged("replayCount"), | |
| "totalBytes": max(derived["totalBytes"], total_bytes), | |
| "itemCount": len(items), | |
| "omittedCount": omitted, | |
| "complete": complete and omitted == 0 and total == len(items), | |
| "items": items, | |
| } | |
| def _canonical_record( | |
| raw: Any, index: int, safe_page: str | None, session_id: str | None | |
| ) -> dict[str, Any]: | |
| if not isinstance(raw, dict): | |
| raise ShareValidationError(f"records[{index}] must be an object") | |
| role = raw.get("role") | |
| if role not in {"user", "assistant", "error"}: | |
| raise ShareValidationError(f"records[{index}].role is not allowed") | |
| text = _bounded_string( | |
| raw.get("text"), | |
| limit=MAX_SHARE_TEXT_CHARS, | |
| field=f"records[{index}].text", | |
| nullable=False, | |
| ) | |
| turn_index = _bounded_int( | |
| raw.get("turn_index"), field=f"records[{index}].turn_index", nullable=False | |
| ) | |
| message_index = _bounded_int( | |
| raw.get("message_index"), | |
| field=f"records[{index}].message_index", | |
| nullable=False, | |
| ) | |
| ts = _bounded_int(raw.get("ts"), field=f"records[{index}].ts") | |
| ts_iso = _bounded_string( | |
| raw.get("ts_iso"), limit=128, field=f"records[{index}].ts_iso" | |
| ) | |
| if raw.get("resources") is not None and role != "user": | |
| raise ShareValidationError( | |
| f"records[{index}].resources is allowed only for user messages" | |
| ) | |
| return { | |
| "turn_index": turn_index, | |
| "message_index": message_index, | |
| "role": role, | |
| "text": text, | |
| "ts": ts, | |
| "ts_iso": ts_iso, | |
| "model_id": _bounded_string( | |
| raw.get("model_id"), | |
| limit=MAX_SHARE_METADATA_CHARS, | |
| field=f"records[{index}].model_id", | |
| ), | |
| "model_provider": _bounded_string( | |
| raw.get("model_provider"), | |
| limit=MAX_SHARE_METADATA_CHARS, | |
| field=f"records[{index}].model_provider", | |
| ), | |
| "model_name": _bounded_string( | |
| raw.get("model_name"), | |
| limit=MAX_SHARE_METADATA_CHARS, | |
| field=f"records[{index}].model_name", | |
| ), | |
| "feedback_rating_value": _safe_scalar( | |
| raw.get("feedback_rating_value"), | |
| field=f"records[{index}].feedback_rating_value", | |
| ), | |
| "feedback_rating_label": _bounded_string( | |
| raw.get("feedback_rating_label"), | |
| limit=MAX_SHARE_METADATA_CHARS, | |
| field=f"records[{index}].feedback_rating_label", | |
| ), | |
| "feedback_message": _bounded_string( | |
| raw.get("feedback_message"), | |
| limit=MAX_SHARE_TEXT_CHARS, | |
| field=f"records[{index}].feedback_message", | |
| ), | |
| "resources": ( | |
| _canonical_resources( | |
| raw.get("resources"), index, allow_source_urls=bool(safe_page) | |
| ) | |
| if raw.get("resources") is not None and role == "user" | |
| else None | |
| ), | |
| # Never trust duplicated per-record identity/page claims from the client; | |
| # bind them to the canonical session values reconstructed above. | |
| "session_id": session_id, | |
| "page_url": safe_page, | |
| } | |
| def _build_turns(records: list[dict[str, Any]]) -> list[dict[str, Any]]: | |
| turns: list[dict[str, Any]] = [] | |
| current: dict[str, Any] | None = None | |
| for row in records: | |
| if row["role"] == "user": | |
| current = { | |
| "turn_index": row["turn_index"], | |
| "user": { | |
| "text": row["text"], | |
| "ts": row["ts"], | |
| "ts_iso": row["ts_iso"], | |
| "resources": row.get("resources"), | |
| }, | |
| "assistant": None, | |
| } | |
| turns.append(current) | |
| elif ( | |
| row["role"] == "assistant" | |
| and current is not None | |
| and current["assistant"] is None | |
| ): | |
| current["assistant"] = { | |
| "text": row["text"], | |
| "ts": row["ts"], | |
| "ts_iso": row["ts_iso"], | |
| "model_id": row["model_id"], | |
| "model_provider": row["model_provider"], | |
| "model_name": row["model_name"], | |
| "feedback_rating_value": row["feedback_rating_value"], | |
| "feedback_rating_label": row["feedback_rating_label"], | |
| "feedback_message": row["feedback_message"], | |
| } | |
| return turns | |
| def canonicalize_share_snapshot(raw: Any) -> dict[str, Any]: | |
| """Validate and reconstruct the allowlisted canonical schema-v2.1 Share snapshot.""" | |
| if not isinstance(raw, dict): | |
| raise ShareValidationError("snapshot must be an object") | |
| if raw.get("schema_version") not in SHARE_ACCEPTED_SCHEMA_VERSIONS: | |
| raise ShareValidationError( | |
| "snapshot.schema_version must be one of: '2.0', '2.1'" | |
| ) | |
| raw_session = raw.get("session") | |
| if not isinstance(raw_session, dict): | |
| raise ShareValidationError("snapshot.session must be an object") | |
| session_id = _bounded_string(raw_session.get("id"), limit=256, field="session.id") | |
| safe_page = sanitize_share_page_url(raw_session.get("page_url")) or None | |
| session = { | |
| "id": session_id, | |
| "page_url": safe_page, | |
| "page_title": _bounded_string( | |
| raw_session.get("page_title"), limit=2048, field="session.page_title" | |
| ), | |
| "assistant_name": ( | |
| _bounded_string( | |
| raw_session.get("assistant_name"), | |
| limit=256, | |
| field="session.assistant_name", | |
| ) | |
| or "AI Assistant" | |
| ), | |
| "exported_at": _bounded_int( | |
| raw_session.get("exported_at"), field="session.exported_at" | |
| ), | |
| "exported_at_iso": _bounded_string( | |
| raw_session.get("exported_at_iso"), | |
| limit=128, | |
| field="session.exported_at_iso", | |
| ), | |
| } | |
| raw_records = raw.get("records") | |
| if not isinstance(raw_records, list) or not raw_records: | |
| raise ShareValidationError("snapshot.records must be a non-empty array") | |
| if len(raw_records) > MAX_SHARE_RECORDS: | |
| raise ShareValidationError("snapshot.records contains too many messages") | |
| records = [ | |
| _canonical_record(row, i, safe_page, session_id) | |
| for i, row in enumerate(raw_records) | |
| ] | |
| if ( | |
| sum((row.get("resources") or {}).get("itemCount", 0) for row in records) | |
| > MAX_SHARE_RESOURCES_TOTAL | |
| ): | |
| raise ShareValidationError( | |
| "snapshot.records contains too many resource metadata rows" | |
| ) | |
| # Never accept caller-supplied turns/unknown root data as trusted. Turns are | |
| # a derived view of validated records and are rebuilt server-side. | |
| return { | |
| "schema_version": SHARE_SCHEMA_VERSION, | |
| "session": session, | |
| "turns": _build_turns(records), | |
| "records": records, | |
| } | |
| def validate_share_format(value: Any) -> str: | |
| if not isinstance(value, str) or value not in SHARE_FORMATS: | |
| raise ShareValidationError("format must be one of: html, json, txt, yaml, toml") | |
| return value | |
| def _resource_summary(manifest: dict[str, Any] | None) -> str: | |
| if not manifest or not manifest.get("totalCount"): | |
| return "" | |
| return ( | |
| f"Resources used for this question: {manifest['totalCount']}" | |
| f" · context {manifest['contextCount']}" | |
| f" · raw {manifest['rawCount']}" | |
| f" · not-sent {manifest['notSentCount']}" | |
| ) | |
| def _resource_text_lines(manifest: dict[str, Any] | None) -> list[str]: | |
| if not manifest or not manifest.get("totalCount"): | |
| return [] | |
| lines = [f"[{_resource_summary(manifest)}]"] | |
| for item in manifest.get("items") or []: | |
| line = f"- [{item.get('badge') or 'FILE'}] {item.get('name') or 'file'}" | |
| if item.get("status"): | |
| line += f" — {item['status']}" | |
| lines.append(line) | |
| if manifest.get("omittedCount"): | |
| count = manifest["omittedCount"] | |
| lines.append( | |
| f"- … {count} resource metadata row{'s' if count != 1 else ''} omitted from restored state" | |
| ) | |
| return lines | |
| def _resource_html(manifest: dict[str, Any] | None) -> str: | |
| if not manifest or not manifest.get("totalCount"): | |
| return "" | |
| rows: list[str] = [] | |
| for item in manifest.get("items") or []: | |
| status = ( | |
| f'<span class="resource-status">{html.escape(str(item.get("status") or ""))}</span>' | |
| if item.get("status") | |
| else "" | |
| ) | |
| rows.append( | |
| '<li class="resource-card">' | |
| f'<span class="resource-badge">{html.escape(str(item.get("badge") or "FILE"))}</span>' | |
| f'<span class="resource-name">{html.escape(str(item.get("name") or "file"))}</span>{status}</li>' | |
| ) | |
| if manifest.get("omittedCount"): | |
| count = manifest["omittedCount"] | |
| rows.append( | |
| f'<li class="resource-card omitted">… {count} resource metadata row{"s" if count != 1 else ""} omitted</li>' | |
| ) | |
| return ( | |
| '<section class="resources"><div class="resource-summary">' | |
| + html.escape(_resource_summary(manifest)) | |
| + '</div><ul class="resource-list">' | |
| + "".join(rows) | |
| + "</ul></section>" | |
| ) | |
| def _render_html(snapshot: dict[str, Any]) -> str: | |
| session = snapshot["session"] | |
| assistant_name = html.escape(str(session.get("assistant_name") or "AI Assistant")) | |
| page_title = html.escape(str(session.get("page_title") or "Shared conversation")) | |
| page_url = str(session.get("page_url") or "") | |
| source = "" | |
| if page_url: | |
| escaped_url = html.escape(page_url, quote=True) | |
| source = f'<p class="source">Source: <a href="{escaped_url}" rel="noopener noreferrer">{escaped_url}</a></p>' | |
| messages: list[str] = [] | |
| for row in snapshot["records"]: | |
| role = row["role"] | |
| label = ( | |
| "You" | |
| if role == "user" | |
| else ("Error" if role == "error" else assistant_name) | |
| ) | |
| text = html.escape(str(row.get("text") or "")) | |
| cls = ( | |
| "user" if role == "user" else ("error" if role == "error" else "assistant") | |
| ) | |
| meta_parts: list[str] = [] | |
| if row.get("ts_iso"): | |
| meta_parts.append(html.escape(str(row["ts_iso"]))) | |
| if row.get("model_name"): | |
| meta_parts.append(html.escape(str(row["model_name"]))) | |
| if row.get("model_provider"): | |
| meta_parts.append(html.escape(str(row["model_provider"]))) | |
| if ( | |
| row.get("feedback_rating_label") | |
| or row.get("feedback_rating_value") is not None | |
| ): | |
| rating = str( | |
| row.get("feedback_rating_label") or row.get("feedback_rating_value") | |
| ) | |
| if row.get("feedback_rating_value") is not None and row.get( | |
| "feedback_rating_label" | |
| ): | |
| rating += f" ({row['feedback_rating_value']})" | |
| meta_parts.append("Rating: " + html.escape(rating)) | |
| if row.get("feedback_message"): | |
| meta_parts.append("Feedback: " + html.escape(str(row["feedback_message"]))) | |
| meta = f'<div class="meta">{" · ".join(meta_parts)}</div>' if meta_parts else "" | |
| resources = _resource_html(row.get("resources")) if role == "user" else "" | |
| messages.append( | |
| f'<article class="msg {cls}"><div class="role">{label}</div>' | |
| f"{resources}<pre>{text}</pre>{meta}</article>" | |
| ) | |
| # No scripts and no remote resources. CSP on the HTTP response is the | |
| # primary policy; this meta tag protects downloaded/copied representations. | |
| return ( | |
| """<!doctype html> | |
| <html lang="en"><head><meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <meta name="referrer" content="no-referrer"> | |
| <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'"> | |
| <title>Shared AI conversation</title><style> | |
| :root{font-family:system-ui,sans-serif;color-scheme:light dark}body{margin:0;background:Canvas;color:CanvasText}.wrap{max-width:850px;margin:auto;padding:24px}.head{border-bottom:1px solid color-mix(in srgb,CanvasText 20%,transparent);padding-bottom:16px}.source{overflow-wrap:anywhere}.source a{color:inherit}.msg{margin:18px 0;padding:14px;border:1px solid color-mix(in srgb,CanvasText 18%,transparent);border-radius:12px}.msg.user{margin-left:10%}.msg.error{border-style:dashed}.role{font-weight:700;margin-bottom:8px}.resources{margin:0 0 10px}.resource-summary{font-size:.78rem;opacity:.7}.resource-list{list-style:none;padding:0;margin:6px 0;display:grid;gap:4px}.resource-card{display:flex;gap:6px;flex-wrap:wrap;font-size:.78rem}.resource-badge{font-weight:700}.resource-status{opacity:.65}.msg pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;margin:0}.meta{opacity:.65;font-size:.8rem;margin-top:8px} | |
| </style></head><body><main class="wrap"><header class="head"><h1>""" | |
| + assistant_name | |
| + " — Shared conversation</h1><p>" | |
| + page_title | |
| + "</p>" | |
| + source | |
| + "</header>" | |
| + "".join(messages) | |
| + "</main></body></html>" | |
| ) | |
| def _render_text(snapshot: dict[str, Any]) -> str: | |
| lines: list[str] = [] | |
| session = snapshot["session"] | |
| lines.append( | |
| f"{session.get('assistant_name') or 'AI Assistant'} — Shared conversation" | |
| ) | |
| lines.append(f"Schema: {snapshot.get('schema_version') or SHARE_SCHEMA_VERSION}") | |
| if session.get("page_title"): | |
| lines.append(f"Page title: {session['page_title']}") | |
| if session.get("page_url"): | |
| lines.append(f"Source: {session['page_url']}") | |
| if session.get("exported_at_iso"): | |
| lines.append(f"Exported: {session['exported_at_iso']}") | |
| lines.append("") | |
| for row in snapshot["records"]: | |
| role = row["role"] | |
| label = ( | |
| "USER" if role == "user" else ("ERROR" if role == "error" else "ASSISTANT") | |
| ) | |
| meta: list[str] = [] | |
| if row.get("ts_iso"): | |
| meta.append(str(row["ts_iso"])) | |
| if role in {"assistant", "error"} and ( | |
| row.get("model_name") or row.get("model_id") | |
| ): | |
| model = str(row.get("model_name") or row.get("model_id")) | |
| if row.get("model_provider"): | |
| model += f" · {row['model_provider']}" | |
| meta.append(model) | |
| lines.append(f"[{label}]" + (f" [{' · '.join(meta)}]" if meta else "")) | |
| if role == "user": | |
| lines.extend(_resource_text_lines(row.get("resources"))) | |
| if role in {"assistant", "error"} and ( | |
| row.get("feedback_rating_label") | |
| or row.get("feedback_rating_value") is not None | |
| ): | |
| bits = [] | |
| if row.get("feedback_rating_label"): | |
| bits.append(str(row["feedback_rating_label"])) | |
| if row.get("feedback_rating_value") is not None: | |
| bits.append(str(row["feedback_rating_value"])) | |
| lines.append(f"[Rating: {' · '.join(bits)}]") | |
| if role in {"assistant", "error"} and row.get("feedback_message"): | |
| lines.append(f"[Feedback: {row['feedback_message']}]") | |
| lines.extend([str(row.get("text") or ""), ""]) | |
| return "\n".join(lines).rstrip() + "\n" | |
| def _yaml_scalar(value: Any) -> str: | |
| if value is None: | |
| return "null" | |
| if isinstance(value, bool): | |
| return "true" if value else "false" | |
| if isinstance(value, (int, float)) and not isinstance(value, bool): | |
| if isinstance(value, float) and not math.isfinite(value): | |
| return "null" | |
| return str(value) | |
| return json.dumps(str(value), ensure_ascii=False) | |
| def _yaml_value(value: Any, indent: int = 0) -> str: | |
| pad = " " * indent | |
| if isinstance(value, list): | |
| if not value: | |
| return pad + "[]" | |
| rows: list[str] = [] | |
| for item in value: | |
| if isinstance(item, (dict, list)): | |
| child = _yaml_value(item, indent + 2).splitlines() | |
| rows.append(pad + "- " + child[0][indent + 2 :]) | |
| rows.extend(child[1:]) | |
| else: | |
| rows.append(pad + "- " + _yaml_scalar(item)) | |
| return "\n".join(rows) | |
| if isinstance(value, dict): | |
| if not value: | |
| return pad + "{}" | |
| rows = [] | |
| for key, item in value.items(): | |
| qkey = json.dumps(str(key), ensure_ascii=False) | |
| if isinstance(item, (dict, list)): | |
| rows.append(f"{pad}{qkey}:\n{_yaml_value(item, indent + 2)}") | |
| else: | |
| rows.append(f"{pad}{qkey}: {_yaml_scalar(item)}") | |
| return "\n".join(rows) | |
| return pad + _yaml_scalar(value) | |
| def _render_yaml(snapshot: dict[str, Any]) -> str: | |
| return _yaml_value(snapshot) + "\n" | |
| def _toml_scalar(value: Any) -> str | None: | |
| if isinstance(value, str): | |
| return json.dumps(value, ensure_ascii=False) | |
| if isinstance(value, bool): | |
| return "true" if value else "false" | |
| if isinstance(value, (int, float)) and not isinstance(value, bool): | |
| if isinstance(value, float) and not math.isfinite(value): | |
| return None | |
| return str(value) | |
| return None | |
| def _toml_fields( | |
| lines: list[str], obj: dict[str, Any], omit: set[str] | None = None | |
| ) -> None: | |
| skipped = omit or set() | |
| for key, value in obj.items(): | |
| if key in skipped or value is None: | |
| continue | |
| rendered = _toml_scalar(value) | |
| if rendered is not None: | |
| lines.append(f"{key} = {rendered}") | |
| def _toml_resources( | |
| lines: list[str], table: str, manifest: dict[str, Any] | None | |
| ) -> None: | |
| if not manifest or not manifest.get("totalCount"): | |
| return | |
| lines.append(f"[{table}]") | |
| _toml_fields(lines, manifest, {"items"}) | |
| for item in manifest.get("items") or []: | |
| lines.append(f"[[{table}.items]]") | |
| _toml_fields(lines, item) | |
| def _render_toml(snapshot: dict[str, Any]) -> str: | |
| lines = [ | |
| "# AI Assistant conversation export", | |
| "# schema v2.1 semantics: omitted optional values represent null", | |
| f"schema_version = {json.dumps(str(snapshot.get('schema_version') or SHARE_SCHEMA_VERSION), ensure_ascii=False)}", | |
| "", | |
| "[session]", | |
| ] | |
| _toml_fields(lines, snapshot.get("session") or {}) | |
| for turn in snapshot.get("turns") or []: | |
| lines.extend(["", "[[turns]]"]) | |
| if turn.get("turn_index") is not None: | |
| lines.append(f"turn_index = {turn['turn_index']}") | |
| if isinstance(turn.get("user"), dict): | |
| lines.append("[turns.user]") | |
| _toml_fields(lines, turn["user"], {"resources"}) | |
| _toml_resources( | |
| lines, "turns.user.resources", turn["user"].get("resources") | |
| ) | |
| if isinstance(turn.get("assistant"), dict): | |
| lines.append("[turns.assistant]") | |
| _toml_fields(lines, turn["assistant"]) | |
| for record in snapshot.get("records") or []: | |
| lines.extend(["", "[[records]]"]) | |
| _toml_fields(lines, record, {"resources"}) | |
| _toml_resources(lines, "records.resources", record.get("resources")) | |
| return "\n".join(lines) + "\n" | |
| def render_share(snapshot: dict[str, Any], fmt: str) -> tuple[str, str, str]: | |
| """Render a validated snapshot using a server-owned representation.""" | |
| fmt = validate_share_format(fmt) | |
| mime, ext = SHARE_FORMATS[fmt] | |
| if fmt == "html": | |
| content = _render_html(snapshot) | |
| elif fmt == "json": | |
| content = json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n" | |
| elif fmt == "yaml": | |
| content = _render_yaml(snapshot) | |
| elif fmt == "toml": | |
| content = _render_toml(snapshot) | |
| else: | |
| content = _render_text(snapshot) | |
| return content, mime, ext | |
| def render_share_viewer_shell( | |
| read_path: str = "/v1/share/read", download_path: str = "/v1/share/download" | |
| ) -> str: | |
| """ | |
| Return the fixed-path public Share viewer. | |
| The public read capability remains in ``location.hash`` and is sent to the | |
| fixed read endpoint only in a JSON request body. The shell renders all | |
| conversation values with DOM ``textContent`` and never injects untrusted HTML. | |
| """ | |
| read_path_json = json.dumps(str(read_path), ensure_ascii=False) | |
| download_path_json = json.dumps(str(download_path), ensure_ascii=False) | |
| template = r"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>Shared AI conversation</title><style>:root{font-family:system-ui,sans-serif;color-scheme:light dark}body{margin:0;background:Canvas;color:CanvasText}.wrap{max-width:850px;margin:auto;padding:24px}.head{border-bottom:1px solid currentColor;padding-bottom:16px}.source{overflow-wrap:anywhere}.source a{color:inherit}.msg{margin:18px 0;padding:14px;border:1px solid currentColor;border-radius:12px}.msg.user{margin-left:10%}.msg.error{border-style:dashed}.role{font-weight:700;margin-bottom:8px}.meta{margin-top:8px;font-size:.82rem;opacity:.72;overflow-wrap:anywhere}.resources{margin:0 0 10px;padding:10px;border:1px solid color-mix(in srgb,currentColor 28%,transparent);border-radius:9px}.resource-summary{font-size:.8rem;opacity:.76;margin-bottom:6px}.resource-list{display:grid;gap:6px}.resource-card{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;font-size:.82rem}.resource-badge{font-size:.7rem;font-weight:700;border:1px solid currentColor;border-radius:5px;padding:1px 5px}.resource-source{color:inherit}.feedback{margin-top:8px;padding:8px 10px;border-left:3px solid currentColor;white-space:pre-wrap;overflow-wrap:anywhere}.artifact{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:0 0 18px;padding:10px 12px;border:1px solid currentColor;border-radius:10px}.artifact-name{font-size:.82rem;opacity:.8;overflow-wrap:anywhere}.download{font:inherit;padding:7px 11px;border:1px solid currentColor;border-radius:8px;background:Canvas;color:CanvasText;cursor:pointer}.download[disabled]{opacity:.55;cursor:wait}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;margin:0}.error-note{border:1px dashed currentColor;padding:14px;border-radius:12px}</style></head><body><main id="app" class="wrap"><p>Loading shared conversation…</p></main><script>(()=>{'use strict';const app=document.getElementById('app');const fail=(m)=>{app.replaceChildren();const p=document.createElement('p');p.className='error-note';p.textContent=m;app.appendChild(p);};let raw=(location.hash||'').slice(1);if(raw.startsWith('share='))raw=raw.slice(6);try{raw=decodeURIComponent(raw)}catch(_e){}if(!/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(raw)){fail('This Share link is invalid or incomplete.');return;}const readJson=async(r)=>{const max=4*1024*1024;const h=r.headers&&r.headers.get?r.headers.get('content-length'):null;if(h!=null&&String(h).trim()!==''){if(!/^\d+$/.test(String(h).trim())||Number(h)>max)throw new Error('Share response is too large.');}if(!r.body||typeof r.body.getReader!=='function'||typeof TextDecoder!=='function')throw new Error('Bounded Share reader unavailable.');const rd=r.body.getReader(),dec=new TextDecoder(),parts=[];let n=0;try{for(;;){const x=await rd.read();if(x.done)break;const v=x.value||new Uint8Array(0);n+=Number(v.byteLength||v.length||0);if(n>max)throw new Error('Share response is too large.');parts.push(dec.decode(v,{stream:true}));}parts.push(dec.decode());}catch(e){try{await rd.cancel()}catch(_e){}throw e;}finally{try{rd.releaseLock()}catch(_e){}}return JSON.parse(parts.join(''));};const readDownload=async(r)=>{const max=8*1024*1024;const h=r.headers&&r.headers.get?r.headers.get('content-length'):null;if(h!=null&&String(h).trim()!==''){if(!/^\d+$/.test(String(h).trim())||Number(h)>max)throw new Error('Shared artifact is too large to download.');}if(!r.body||typeof r.body.getReader!=='function'||typeof TextDecoder!=='function')throw new Error('Bounded Share download reader unavailable.');const rd=r.body.getReader(),dec=new TextDecoder(),parts=[];let n=0;try{for(;;){const x=await rd.read();if(x.done)break;const v=x.value||new Uint8Array(0);n+=Number(v.byteLength||v.length||0);if(n>max)throw new Error('Shared artifact is too large to download.');parts.push(dec.decode(v,{stream:true}));}parts.push(dec.decode());}catch(e){try{await rd.cancel()}catch(_e){}throw e;}finally{try{rd.releaseLock()}catch(_e){}}return parts.join('');};fetch(__READ_PATH__,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shareId:raw}),cache:'no-store',credentials:'omit',redirect:'error',referrerPolicy:'no-referrer'}).then(async r=>{if(!r.ok){throw new Error(r.status===410?'This Share has expired.':r.status===404?'This Share is unavailable.':'Could not load this Share.');}return await readJson(r);}).then(data=>{app.replaceChildren();const fmt=String(data.format||'txt').toLowerCase();const ext={json:'.json',html:'.html',txt:'.txt',yaml:'.yaml',toml:'.toml'}[fmt]||'.txt';const fallback='ai-conversation-global-share-'+fmt+ext;const requested=String(data.filename||'');const filename=/^ai-conversation-global-share-(?:json\.json|html\.html|txt\.txt|yaml\.yaml|toml\.toml)$/.test(requested)?requested:fallback;const bar=document.createElement('section');bar.className='artifact';bar.setAttribute('aria-label','Shared artifact');const artifactName=document.createElement('span');artifactName.className='artifact-name';artifactName.textContent='Global Share · '+fmt.toUpperCase()+' · '+filename;const download=document.createElement('button');download.type='button';download.className='download';download.textContent='Download '+fmt.toUpperCase();download.addEventListener('click',async()=>{if(download.disabled)return;download.disabled=true;const prior=download.textContent;download.textContent='Downloading…';try{const r=await fetch(__DOWNLOAD_PATH__,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shareId:raw}),cache:'no-store',credentials:'omit',redirect:'error',referrerPolicy:'no-referrer'});if(!r.ok)throw new Error(r.status===410?'This Share has expired.':r.status===404?'This Share is unavailable.':'Could not download this Share.');const text=await readDownload(r);const type=String((r.headers&&r.headers.get&&r.headers.get('content-type'))||data.mimeType||'text/plain;charset=utf-8');const blob=new Blob([text],{type});const objectUrl=URL.createObjectURL(blob);try{const a=document.createElement('a');a.href=objectUrl;a.download=filename;a.rel='noopener';a.style.display='none';document.body.appendChild(a);a.click();a.remove();}finally{setTimeout(()=>URL.revokeObjectURL(objectUrl),0);}}catch(e){fail(e&&e.message?e.message:'Could not download this Share.');return;}finally{download.disabled=false;download.textContent=prior;}});bar.append(artifactName,download);app.appendChild(bar);if(data.format==='html'&&data.snapshot&&data.snapshot.session&&Array.isArray(data.snapshot.records)){const snap=data.snapshot;const h=document.createElement('header');h.className='head';const h1=document.createElement('h1');h1.textContent=(snap.session.assistant_name||'AI Assistant')+' — Shared conversation';h.appendChild(h1);if(snap.session.page_title){const p=document.createElement('p');p.textContent=snap.session.page_title;h.appendChild(p);}if(snap.session.page_url){const p=document.createElement('p');p.className='source';p.append('Source: ');const a=document.createElement('a');a.href=snap.session.page_url;a.rel='noopener noreferrer';a.referrerPolicy='no-referrer';a.textContent=snap.session.page_url;p.appendChild(a);h.appendChild(p);}app.appendChild(h);const appendMeta=(article,row)=>{const bits=[];if(row.ts_iso)bits.push(String(row.ts_iso));const model=String(row.model_name||row.model_id||'');const provider=String(row.model_provider||'');if(model)bits.push(provider?model+' · '+provider:model);else if(provider)bits.push(provider);if(row.feedback_rating_label!=null||row.feedback_rating_value!=null){let rating='Rating: '+String(row.feedback_rating_label||'rated');if(row.feedback_rating_value!=null)rating+=' ('+String(row.feedback_rating_value)+')';bits.push(rating);}if(bits.length){const meta=document.createElement('div');meta.className='meta';meta.textContent=bits.join(' · ');article.appendChild(meta);}if(row.feedback_message){const note=document.createElement('div');note.className='feedback';note.textContent='Feedback: '+String(row.feedback_message);article.appendChild(note);}};const appendResources=(article,manifest)=>{if(!manifest||!Array.isArray(manifest.items)||Number(manifest.totalCount||0)<=0)return;const section=document.createElement('section');section.className='resources';section.setAttribute('aria-label','Resources used for this question');const summary=document.createElement('div');summary.className='resource-summary';summary.textContent='Resources: '+String(manifest.totalCount||manifest.items.length)+' · context '+String(manifest.contextCount||0)+' · raw '+String(manifest.rawCount||0)+' · not-sent '+String(manifest.notSentCount||0);section.appendChild(summary);const list=document.createElement('div');list.className='resource-list';for(const item of manifest.items){const card=document.createElement('div');card.className='resource-card';const badge=document.createElement('span');badge.className='resource-badge';badge.textContent=String(item.badge||'FILE');const label=document.createElement('span');label.textContent=String(item.name||'Resource')+(item.status?' — '+String(item.status):'');card.append(badge,label);const source=String(item.sourceUrl||'');if(/^https?:\/\//i.test(source)){const a=document.createElement('a');a.className='resource-source';a.href=source;a.rel='noopener noreferrer';a.referrerPolicy='no-referrer';a.textContent='Source';card.appendChild(a);}list.appendChild(card);}section.appendChild(list);article.appendChild(section);};for(const row of snap.records){const article=document.createElement('article');article.className='msg '+(row.role==='user'?'user':row.role==='error'?'error':'assistant');const role=document.createElement('div');role.className='role';role.textContent=row.role==='user'?'You':row.role==='error'?'Error':(snap.session.assistant_name||'AI Assistant');article.appendChild(role);if(row.role==='user')appendResources(article,row.resources);const pre=document.createElement('pre');pre.textContent=String(row.text||'');article.appendChild(pre);appendMeta(article,row);app.appendChild(article);}return;}const pre=document.createElement('pre');pre.textContent=String(data.content||'');app.appendChild(pre);}).catch(e=>fail(e&&e.message?e.message:'Could not load this Share.'));})();</script></body></html>""" | |
| return template.replace("__READ_PATH__", read_path_json).replace( | |
| "__DOWNLOAD_PATH__", download_path_json | |
| ) | |
| def generate_edit_token() -> str: | |
| return secrets.token_urlsafe(32) | |
| def hash_edit_token(token: str) -> str: | |
| return hashlib.sha256(token.encode("utf-8")).hexdigest() | |
| def verify_edit_token(token: str, expected_hash: str) -> bool: | |
| if not token or not expected_hash: | |
| return False | |
| candidate = hash_edit_token(token) | |
| return hmac.compare_digest(candidate, expected_hash) | |
| def valid_share_id(value: str) -> bool: | |
| return bool(_SHARE_ID_RE.fullmatch(value or "")) | |