hermes: bound publication titles by UTF-8 bytes
This commit is contained in:
parent
3231e1af07
commit
cac4c206cd
@ -93,6 +93,13 @@ def payload_digest(payload: Any) -> str:
|
|||||||
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_utf8(value: str, maximum: int) -> str:
|
||||||
|
"""Bound display text by UTF-8 bytes without splitting a character."""
|
||||||
|
if maximum < 0:
|
||||||
|
raise ProtocolError("text byte limit is invalid")
|
||||||
|
return value.encode("utf-8", "replace")[:maximum].decode("utf-8", "ignore")
|
||||||
|
|
||||||
|
|
||||||
def derive_ordinal_key(master: bytes, ordinal: int) -> bytes:
|
def derive_ordinal_key(master: bytes, ordinal: int) -> bytes:
|
||||||
"""Derive one cryptographically isolated worker authority from the pool root."""
|
"""Derive one cryptographically isolated worker authority from the pool root."""
|
||||||
if ordinal not in range(3) or not 32 <= len(master) <= 4096:
|
if ordinal not in range(3) or not 32 <= len(master) <= 4096:
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from execution_pool_protocol import (
|
|||||||
ProtocolError,
|
ProtocolError,
|
||||||
atomic_json,
|
atomic_json,
|
||||||
canonical_json,
|
canonical_json,
|
||||||
|
truncate_utf8,
|
||||||
)
|
)
|
||||||
ROOT = Path(os.environ.get("HERMES_WORKER_ROOT", "/workspace"))
|
ROOT = Path(os.environ.get("HERMES_WORKER_ROOT", "/workspace"))
|
||||||
ORDINAL = int(os.environ.get("HERMES_WORKER_ORDINAL", "-1"))
|
ORDINAL = int(os.environ.get("HERMES_WORKER_ORDINAL", "-1"))
|
||||||
@ -380,7 +381,7 @@ def execute(assignment: dict[str, Any]) -> None:
|
|||||||
"finish",
|
"finish",
|
||||||
binding=binding,
|
binding=binding,
|
||||||
payload=result_payload,
|
payload=result_payload,
|
||||||
title=str(structured.get("summary") or f"Hermes task {binding['task_id']}")[:240],
|
title=truncate_utf8(str(structured.get("summary") or f"Hermes task {binding['task_id']}"), 512),
|
||||||
body=json.dumps(structured, indent=2, sort_keys=True)[:12000],
|
body=json.dumps(structured, indent=2, sort_keys=True)[:12000],
|
||||||
)
|
)
|
||||||
if not response.get("ack", {}).get("accepted"):
|
if not response.get("ack", {}).get("accepted"):
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from execution_pool_protocol import ProtocolError, canonical_json, read_key
|
from execution_pool_protocol import ProtocolError, canonical_json, read_key, truncate_utf8
|
||||||
from execution_pool_scm import Boundary
|
from execution_pool_scm import Boundary
|
||||||
|
|
||||||
ROOT = Path(os.environ.get("HERMES_WORKER_ROOT", "/workspace"))
|
ROOT = Path(os.environ.get("HERMES_WORKER_ROOT", "/workspace"))
|
||||||
@ -88,7 +88,7 @@ def bootstrap(key: bytes, raw_assignment: Any) -> dict[str, Any]:
|
|||||||
boundary = Boundary(key)
|
boundary = Boundary(key)
|
||||||
assignment = boundary.verify(raw_assignment)
|
assignment = boundary.verify(raw_assignment)
|
||||||
structured = _completed_result(assignment)
|
structured = _completed_result(assignment)
|
||||||
title = structured["summary"].strip()[:512]
|
title = truncate_utf8(structured["summary"].strip(), 512)
|
||||||
body = json.dumps(structured, indent=2, sort_keys=True)
|
body = json.dumps(structured, indent=2, sort_keys=True)
|
||||||
return boundary.resume_artifact(assignment, {"title": title, "body": body}, structured)
|
return boundary.resume_artifact(assignment, {"title": title, "body": body}, structured)
|
||||||
|
|
||||||
|
|||||||
@ -459,6 +459,30 @@ def test_mediator_bootstrap_recovers_one_unique_nested_session_result(tmp_path,
|
|||||||
assert resume_bootstrap._completed_result(exact) == completed
|
assert resume_bootstrap._completed_result(exact) == completed
|
||||||
|
|
||||||
|
|
||||||
|
def test_mediator_bootstrap_truncates_multibyte_title_at_the_byte_limit(tmp_path, monkeypatch):
|
||||||
|
"""Receipt title limits count UTF-8 bytes while preserving full evidence."""
|
||||||
|
exact = protocol.sign_envelope(
|
||||||
|
KEY, "assignment", binding(), payload(root_task_id="t_deadbeef", continuation_kind="repair")
|
||||||
|
)
|
||||||
|
log = tmp_path / "session-state" / "metis" / "t_deadbeef" / "42.log"
|
||||||
|
log.parent.mkdir(parents=True)
|
||||||
|
completed = {**RESULT, "summary": "界" * 200}
|
||||||
|
log.write_bytes(protocol.canonical_json({"structured_output": completed}) + b"\n")
|
||||||
|
monkeypatch.setattr(resume_bootstrap, "ROOT", tmp_path)
|
||||||
|
seen = {}
|
||||||
|
monkeypatch.setattr(
|
||||||
|
scm.Boundary, "resume_artifact",
|
||||||
|
lambda _self, _assignment, request, structured: seen.update(
|
||||||
|
request=request, structured=structured
|
||||||
|
) or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
resume_bootstrap.bootstrap(KEY, exact)
|
||||||
|
|
||||||
|
assert len(seen["request"]["title"].encode()) <= 512
|
||||||
|
assert seen["structured"]["summary"] == completed["summary"]
|
||||||
|
|
||||||
|
|
||||||
def test_mediator_bootstrap_rejects_a_symlinked_log_ancestor(tmp_path, monkeypatch):
|
def test_mediator_bootstrap_rejects_a_symlinked_log_ancestor(tmp_path, monkeypatch):
|
||||||
exact = protocol.sign_envelope(KEY, "assignment", binding(), payload())
|
exact = protocol.sign_envelope(KEY, "assignment", binding(), payload())
|
||||||
outside = tmp_path / "outside"
|
outside = tmp_path / "outside"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user