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
This commit is contained in:
jenkins 2026-08-24 04:35:12 -03:00
parent 2eedcd2066
commit c089a5ec2a
7 changed files with 307 additions and 1 deletions

View File

@ -671,6 +671,7 @@ spec:
items:
- {key: __init__.py, path: __init__.py}
- {key: context_ids.py, path: context_ids.py}
- {key: emitters.py, path: emitters.py}
- {key: runtime.py, path: runtime.py}
- {key: tool_policy.py, path: tool_policy.py}
- {key: plugin.yaml, path: plugin.yaml}

View File

@ -200,6 +200,7 @@ configMapGenerator:
files:
- __init__.py=plugins/hux-runtime/__init__.py
- context_ids.py=plugins/hux-runtime/context_ids.py
- emitters.py=plugins/hux-runtime/emitters.py
- runtime.py=plugins/hux-runtime/runtime.py
- tool_policy.py=plugins/hux-runtime/tool_policy.py
- plugin.yaml=plugins/hux-runtime/plugin.yaml

View File

@ -35,3 +35,15 @@ Quality registration must include `services/hermes/plugins/hux-runtime/*.py`
as source and `testing/tests/test_hermes_hux_runtime_plugin.py` as its focused
test. The current focused gate is 54 passing tests with every production file
above 95% line and branch coverage.
## Post-tool emitters (HUX-04 / HUX-08)
`emitters.sync_after_tool` runs only after a successful tool execution and
never raises into the tool path. A successful `write_files` tool whose
arguments name a real, non-symlink file of at most 1 MiB creates (or, when a
same-titled artifact already exists in the conversation, appends an immutable
version to) a typed artifact through the loopback API; the service's secret
scrubbing and `restricted` forcing still apply. A successful `network` or
`web_search` tool records up to three deduplicated `web` sources found in the
first 64 KiB of real tool output. Both use deterministic idempotency keys, so
retries never duplicate records.

View File

@ -0,0 +1,142 @@
"""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

View File

@ -19,6 +19,7 @@ from .hux_hook import (
)
from .context_ids import ContextIds, ContextUnavailable
from .emitters import sync_after_tool
from .tool_policy import classify
BLOCK_SCHEMA = "hux.tool_block.v1"
@ -228,7 +229,12 @@ class Runtime:
except BaseException:
self._after(call, tool_name, argument_hash, False, 0, started)
raise
self._after(call, tool_name, argument_hash, _result_ok(result), _result_size(result), started)
ok = _result_ok(result)
self._after(call, tool_name, argument_hash, ok, _result_size(result), started)
if ok:
# HUX-04/08: real written files and real web output leave records
# behind. Emitters are bounded and can never break the tool result.
sync_after_tool(self.client, call, policy.capability, args, result)
return result
def _after(

View File

@ -137,6 +137,7 @@
"services/hermes/plugins/auto-router/hux_mode.py",
"services/hermes/plugins/hux-runtime/__init__.py",
"services/hermes/plugins/hux-runtime/context_ids.py",
"services/hermes/plugins/hux-runtime/emitters.py",
"services/hermes/plugins/hux-runtime/runtime.py",
"services/hermes/plugins/hux-runtime/tool_policy.py"
],
@ -246,6 +247,7 @@
"services/hermes/plugins/auto-router/hux_mode.py",
"services/hermes/plugins/hux-runtime/__init__.py",
"services/hermes/plugins/hux-runtime/context_ids.py",
"services/hermes/plugins/hux-runtime/emitters.py",
"services/hermes/plugins/hux-runtime/runtime.py",
"services/hermes/plugins/hux-runtime/tool_policy.py"
],
@ -446,6 +448,7 @@
"services/hermes/plugins/auto-router/hux_mode.py",
"services/hermes/plugins/hux-runtime/__init__.py",
"services/hermes/plugins/hux-runtime/context_ids.py",
"services/hermes/plugins/hux-runtime/emitters.py",
"services/hermes/plugins/hux-runtime/runtime.py",
"services/hermes/plugins/hux-runtime/tool_policy.py"
],
@ -565,6 +568,7 @@
"services/hermes/plugins/auto-router/hux_mode.py",
"services/hermes/plugins/hux-runtime/__init__.py",
"services/hermes/plugins/hux-runtime/context_ids.py",
"services/hermes/plugins/hux-runtime/emitters.py",
"services/hermes/plugins/hux-runtime/runtime.py",
"services/hermes/plugins/hux-runtime/tool_policy.py"
]

View File

@ -0,0 +1,140 @@
"""HUX-04/08 post-tool emitters: real files and real web output leave records."""
from __future__ import annotations
import base64
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[2]
PLUGIN = ROOT / "services" / "hermes" / "plugins" / "hux-runtime"
SPEC = importlib.util.spec_from_file_location("hermes_hux_emitters", PLUGIN / "emitters.py")
assert SPEC and SPEC.loader
emitters = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = emitters
SPEC.loader.exec_module(emitters)
CALL = SimpleNamespace(conversation_id="conv_" + "c" * 32, project_id="prj_" + "a" * 32)
class Client:
"""Capture every emitter call; scriptable listing and failures."""
def __init__(self, items=None, fail=None):
self.items = items or []
self.fail = fail
self.calls = []
def get(self, path, query=None):
self.calls.append(("GET", path, query, None))
if self.fail == "get":
raise OSError("down")
return SimpleNamespace(body={"items": self.items})
def post(self, path, body, idempotency_key=None):
self.calls.append(("POST", path, body, idempotency_key))
if self.fail == "post":
raise OSError("down")
return SimpleNamespace(body={"id": "art_new0001"})
def request(self, method, path, body=None, *, idempotency_key=None, if_match=None):
self.calls.append((method, path, body, idempotency_key, if_match))
if self.fail == "request":
raise OSError("down")
return SimpleNamespace(body={})
def test_new_text_file_creates_a_typed_artifact(tmp_path):
target = tmp_path / "report.md"
target.write_text("# hello\n")
client = Client()
assert emitters.record_artifact(client, CALL, {"path": str(target)}) == "art_new0001"
method, path, body, key = client.calls[-1]
assert (method, path) == ("POST", "/hux/v1/artifacts")
assert body["type"] == "markdown" and body["title"] == "report.md"
assert body["content"] == "# hello\n" and "content_base64" not in body
assert body["conversation_id"] == CALL.conversation_id
assert key.startswith("artifact-sync:new:")
def test_binary_file_is_base64_and_unknown_suffix_is_code(tmp_path):
target = tmp_path / "tool.bin"
target.write_bytes(b"\xff\xfe\x00binary")
client = Client()
emitters.record_artifact(client, CALL, {"file_path": str(target)})
body = client.calls[-1][2]
assert body["type"] == "code"
assert base64.b64decode(body["content_base64"]) == b"\xff\xfe\x00binary"
assert "content" not in body
def test_existing_title_appends_an_immutable_version(tmp_path):
target = tmp_path / "main.py"
target.write_text("print(2)\n")
existing = {"id": "art_1", "title": "main.py", "revision": 3, "current_version": 3}
client = Client(items=[existing])
assert emitters.record_artifact(client, CALL, {"path": str(target)}) == "art_1"
method, path, body, key, if_match = client.calls[-1]
assert (method, path) == ("POST", "/hux/v1/artifacts/art_1/versions")
assert body["diff_from"] == 3 and body["content"] == "print(2)\n"
assert if_match == 3 and key.startswith("artifact-sync:art_1:")
def test_artifact_emitter_is_bounded_and_fail_open(tmp_path):
client = Client()
# No plausible path, missing file, oversized file, symlink, service down.
assert emitters.record_artifact(client, CALL, {"other": 1}) is None
assert emitters.record_artifact(client, CALL, {"path": str(tmp_path / "gone")}) is None
big = tmp_path / "big.txt"
big.write_bytes(b"x" * (emitters.MAX_ARTIFACT_BYTES + 1))
assert emitters.record_artifact(client, CALL, {"path": str(big)}) is None
target = tmp_path / "real.txt"
target.write_text("ok")
link = tmp_path / "link.txt"
link.symlink_to(target)
assert emitters.record_artifact(client, CALL, {"path": str(link)}) is None
assert emitters.record_artifact(client, CALL, "not-a-dict") is None
assert emitters.record_artifact(Client(fail="get"), CALL, {"path": str(target)}) is None
assert emitters.record_artifact(Client(fail="post"), CALL, {"path": str(target)}) is None
existing = {"id": "art_1", "title": "real.txt", "revision": 1, "current_version": 1}
assert emitters.record_artifact(Client(items=[existing], fail="request"), CALL, {"path": str(target)}) is None
def test_web_sources_are_deduplicated_and_capped():
client = Client()
text = " ".join(
["see https://example.org/a and https://example.org/a again,",
"plus https://example.org/b, https://example.org/c and https://example.org/d"]
)
assert emitters.record_sources(client, text) == emitters.MAX_SOURCES_PER_CALL
posts = [call for call in client.calls if call[0] == "POST"]
assert [c[2]["uri"] for c in posts] == [
"https://example.org/a", "https://example.org/b", "https://example.org/c",
]
assert all(c[2]["kind"] == "web" and c[3].startswith("source-sync:") for c in posts)
def test_source_emitter_handles_bytes_non_text_and_failures():
assert emitters.record_sources(Client(), b"https://example.org/bytes ok") == 1
assert emitters.record_sources(Client(), {"not": "text"}) == 0
assert emitters.record_sources(Client(fail="post"), "https://example.org/x") == 0
assert emitters.record_sources(Client(), "no urls here") == 0
def test_sync_router_dispatches_by_capability(tmp_path):
target = tmp_path / "out.json"
target.write_text("{}")
client = Client()
emitters.sync_after_tool(client, CALL, "write_files", {"path": str(target)}, "ok")
assert any(call[1] == "/hux/v1/artifacts" for call in client.calls)
client = Client()
emitters.sync_after_tool(client, CALL, "network", {}, "https://example.org/z")
assert any(call[1] == "/hux/v1/sources" for call in client.calls)
client = Client()
emitters.sync_after_tool(client, CALL, "shell", {}, "https://example.org/z")
assert client.calls == []
# A broken call scope can never raise into the tool path.
emitters.sync_after_tool(None, None, "write_files", {"path": str(target)}, "ok")