From 86279160b03b6be9b0e0a59eaac6a9a9e36af2b5 Mon Sep 17 00:00:00 2001 From: wesleysimplicio <6108320+wesleysimplicio@users.noreply.github.com> Date: Mon, 18 May 2026 20:22:22 -0700 Subject: [PATCH] fix(kanban): persist worker session metadata on completion Salvages #25579 by @wesleysimplicio. Stamps task_runs.metadata.worker_session_id from HERMES_SESSION_ID on kanban_complete. Cherry-picked the substantive commit (not the AUTHOR_MAP fixup tip) onto current main. --- tests/tools/test_kanban_tools.py | 53 ++++++++++++++++++++++++++++++++ tools/kanban_tools.py | 15 +++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index 1dbd72ad937..b50f35b0682 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -128,6 +128,7 @@ def worker_env(monkeypatch, tmp_path): home.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_PROFILE", "test-worker") + monkeypatch.delenv("HERMES_SESSION_ID", raising=False) from pathlib import Path as _Path monkeypatch.setattr(_Path, "home", lambda: tmp_path) @@ -310,6 +311,58 @@ def test_complete_metadata_round_trips_through_show(worker_env): assert shown["runs"][-1]["metadata"] == handoff +def test_complete_stamps_worker_session_id_from_env(monkeypatch, worker_env): + from tools import kanban_tools as kt + + monkeypatch.setenv("HERMES_SESSION_ID", "session-trusted") + metadata = {"files": 2, "worker_session_id": "user-spoof"} + + out = kt._handle_complete({ + "summary": "done by scoped worker", + "metadata": metadata, + }) + assert json.loads(out)["ok"] is True + assert metadata["worker_session_id"] == "user-spoof" + + from hermes_cli import kanban_db as kb + conn = kb.connect() + try: + run = kb.latest_run(conn, worker_env) + assert run.metadata == { + "files": 2, + "worker_session_id": "session-trusted", + } + finally: + conn.close() + + +def test_complete_does_not_stamp_worker_session_id_without_scoped_task( + monkeypatch, worker_env +): + from tools import kanban_tools as kt + + monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) + monkeypatch.setenv("HERMES_SESSION_ID", "session-trusted") + + out = kt._handle_complete({ + "task_id": worker_env, + "summary": "done outside worker scope", + "metadata": {"files": 2, "worker_session_id": "user-provided"}, + }) + assert json.loads(out)["ok"] is True + + from hermes_cli import kanban_db as kb + conn = kb.connect() + try: + run = kb.latest_run(conn, worker_env) + assert run.metadata == { + "files": 2, + "worker_session_id": "user-provided", + } + finally: + conn.close() + + def test_complete_with_result_only(worker_env): """`result` alone (without summary) is accepted for legacy compat.""" from tools import kanban_tools as kt diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index eaf32a3a374..8f0f8334b64 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -112,6 +112,20 @@ def _worker_run_id(task_id: str) -> Optional[int]: return None +def _stamp_worker_session_metadata( + task_id: str, metadata: Optional[dict] +) -> Optional[dict]: + """Add trusted worker session id metadata for this worker's own task.""" + if os.environ.get("HERMES_KANBAN_TASK") != task_id: + return metadata + session_id = os.environ.get("HERMES_SESSION_ID") + if not session_id: + return metadata + stamped = dict(metadata or {}) + stamped["worker_session_id"] = session_id + return stamped + + def _enforce_worker_task_ownership(tid: str) -> Optional[str]: """Reject worker-driven destructive calls on foreign task IDs. @@ -432,6 +446,7 @@ def _handle_complete(args: dict, **kw) -> str: return tool_error( f"metadata must be an object/dict, got {type(metadata).__name__}" ) + metadata = _stamp_worker_session_metadata(tid, metadata) try: kb, conn = _connect() try: