jenkins c089a5ec2a hermes(runtime): emit artifacts and web sources from real tool output
HUX-04/HUX-08: after a successful tool execution the hux-runtime plugin
now registers freshly written files as typed artifacts (create or
immutable version by conversation-scoped title, bounded to 1 MiB,
deterministic idempotency keys) and records up to three deduplicated
web sources from real network/web_search output. Emitters are fail-open
and can never break the tool result. Delivered through the plugin
ConfigMap; 95% branch coverage; quality contract updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:35:12 -03:00

143 lines
5.6 KiB
Python

"""Fail-open post-tool emitters: artifacts from real writes, sources from real web output.
HUX-04 requires agent-created files to appear in the artifact workspace
automatically; HUX-08 requires real web tool output to leave source
records behind. Both emitters run strictly after a successful tool
execution, never raise into the tool path, never send secrets beyond the
written content itself (the service applies its own secret scrubbing and
forces ``restricted`` sensitivity on hits), and stay bounded.
"""
from __future__ import annotations
import base64
import hashlib
import re
from pathlib import Path
from typing import Any
MAX_ARTIFACT_BYTES = 1024 * 1024
MAX_RESULT_SCAN_BYTES = 64 * 1024
MAX_SOURCES_PER_CALL = 3
PATH_KEYS = ("path", "file_path", "filename", "target_path", "output_path")
URL = re.compile(r"""https?://[^\s"'<>)\]]{8,500}""")
TYPE_BY_SUFFIX = {
".md": "markdown", ".markdown": "markdown", ".html": "html", ".htm": "html",
".svg": "svg", ".json": "json", ".csv": "csv", ".png": "image", ".jpg": "image",
".jpeg": "image", ".gif": "image", ".webp": "image", ".pdf": "document",
".txt": "document", ".rst": "document", ".wav": "audio", ".mp3": "audio",
}
MIME_BY_TYPE = {
"markdown": "text/markdown", "code": "text/plain", "html": "text/html",
"svg": "image/svg+xml", "image": "application/octet-stream", "json": "application/json",
"csv": "text/csv", "document": "text/plain", "audio": "application/octet-stream",
}
def _written_path(args: Any) -> Path | None:
"""The one plausible filesystem target of a write tool, or None."""
if not isinstance(args, dict):
return None
for key in PATH_KEYS:
value = args.get(key)
if isinstance(value, str) and value and len(value) < 4096:
path = Path(value)
try:
if path.is_file() and not path.is_symlink():
return path
except OSError:
return None
return None
def _artifact_body(path: Path, data: bytes, conversation_id: str, project_id: str) -> dict[str, Any]:
artifact_type = TYPE_BY_SUFFIX.get(path.suffix.lower(), "code")
body: dict[str, Any] = {
"type": artifact_type,
"title": path.name[-200:],
"mime": MIME_BY_TYPE[artifact_type],
"conversation_id": conversation_id,
"project_id": project_id,
}
try:
body["content"] = data.decode("utf-8", errors="strict")
except UnicodeDecodeError:
body["content_base64"] = base64.b64encode(data).decode("ascii")
return body
def record_artifact(client: Any, call: Any, args: Any) -> str | None:
"""Create or version one artifact for a freshly written file; never raises."""
try:
path = _written_path(args)
if path is None or path.stat().st_size > MAX_ARTIFACT_BYTES:
return None
data = path.read_bytes()
digest = hashlib.sha256(data).hexdigest()[:32]
body = _artifact_body(path, data, call.conversation_id, call.project_id)
listing = client.get("/hux/v1/artifacts", {"conversation_id": call.conversation_id, "cursor": "0"})
items = listing.body.get("items", []) if isinstance(listing.body, dict) else []
existing = next(
(item for item in items
if isinstance(item, dict) and item.get("title") == body["title"]),
None,
)
if existing is None:
created = client.post(
"/hux/v1/artifacts", body,
idempotency_key=f"artifact-sync:new:{digest}"[:120],
)
return created.body.get("id") if isinstance(created.body, dict) else None
version = {key: body[key] for key in ("mime",) if key in body}
version.update({k: v for k, v in body.items() if k.startswith("content")})
version["diff_from"] = existing.get("current_version")
client.request(
"POST", f"/hux/v1/artifacts/{existing['id']}/versions", version,
idempotency_key=f"artifact-sync:{existing['id']}:{digest}"[:120],
if_match=existing.get("revision"),
)
return existing.get("id")
except Exception:
return None
def record_sources(client: Any, result: Any) -> int:
"""Record up to three deduplicated web sources found in real tool output."""
try:
if isinstance(result, bytes):
text = result[:MAX_RESULT_SCAN_BYTES].decode("utf-8", errors="replace")
elif isinstance(result, str):
text = result[:MAX_RESULT_SCAN_BYTES]
else:
return 0
recorded = 0
seen: set[str] = set()
for match in URL.finditer(text):
url = match.group(0).rstrip(".,;")
if url in seen:
continue
seen.add(url)
digest = hashlib.sha256(url.encode()).hexdigest()[:32]
client.post(
"/hux/v1/sources",
{"kind": "web", "title": url[:300], "uri": url[:2000], "classification": "unknown"},
idempotency_key=f"source-sync:{digest}"[:120],
)
recorded += 1
if recorded >= MAX_SOURCES_PER_CALL:
break
return recorded
except Exception:
return 0
def sync_after_tool(client: Any, call: Any, capability: str, args: Any, result: Any) -> None:
"""Route one successful tool execution to its emitters; always returns."""
try:
if capability == "write_files":
record_artifact(client, call, args)
elif capability in {"network", "web_search"}:
record_sources(client, result)
except Exception:
return