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
143 lines
5.6 KiB
Python
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
|