mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(gateway): redact credentials from TUI approval prompts (#48456)
Follow-up to #50767, which redacted the chat-platform (_approval_notify_sync) and SSE/API (_approval_notify) approval transports. The TUI JSON-RPC transport is the third egress and was missed: three register_gateway_notify callbacks in tui_gateway/server.py emitted the raw approval_data — including the unredacted command Tirith flagged — straight to the TUI client via _emit. Route all three registrations through a new module-level _emit_approval_request() helper that redacts payload['command'] via the shared gateway.run._redact_approval_command seam before emitting, matching the pattern used for the other two transports. Completes the whole-bug-class fix for #48456. Tests: assert the helper emits a redacted command (real credential pattern), handles missing/None command, and a wiring guard that no registration emits the raw payload directly (only the helper may). Both mutation-checked. The #48456 fix series originated from @liuhao1024's #48462 — credit to them for the original report and chat-platform fix; this completes the remaining transport. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
parent
0e69cd4b37
commit
c080b2dc3e
2 changed files with 84 additions and 3 deletions
66
tests/gateway/test_tui_approval_redaction.py
Normal file
66
tests/gateway/test_tui_approval_redaction.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Regression test for TUI approval-prompt credential redaction (#48456).
|
||||
|
||||
Follow-up to #50767, which redacted the chat-platform and SSE/API approval
|
||||
transports. The TUI JSON-RPC transport is the third egress: three
|
||||
`register_gateway_notify` callbacks in `tui_gateway/server.py` emit the raw
|
||||
`approval_data` (with an unredacted `command`) to the TUI client. They now
|
||||
route through the module-level `_emit_approval_request` helper, which redacts
|
||||
`payload["command"]` via the shared `gateway.run._redact_approval_command` seam
|
||||
before emitting.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTuiApprovalEmitRedaction:
|
||||
def test_emit_approval_request_redacts_command_in_payload(self, monkeypatch):
|
||||
from tui_gateway import server as tui_server
|
||||
|
||||
emitted = {}
|
||||
monkeypatch.setattr(
|
||||
tui_server, "_emit",
|
||||
lambda event, sid, payload=None: emitted.update(
|
||||
{"event": event, "sid": sid, "payload": payload}
|
||||
),
|
||||
)
|
||||
raw = "curl -H 'Authorization: token ghp_01...6789' https://api.github.com"
|
||||
tui_server._emit_approval_request("sess-1", {"command": raw, "description": "x"})
|
||||
|
||||
assert emitted["event"] == "approval.request"
|
||||
# credential removed, non-command field + command structure preserved
|
||||
assert "ghp_01...6789" not in emitted["payload"]["command"]
|
||||
assert emitted["payload"]["description"] == "x"
|
||||
assert "github.com" in emitted["payload"]["command"]
|
||||
|
||||
def test_emit_approval_request_handles_missing_command(self, monkeypatch):
|
||||
from tui_gateway import server as tui_server
|
||||
|
||||
emitted = {}
|
||||
monkeypatch.setattr(
|
||||
tui_server, "_emit",
|
||||
lambda event, sid, payload=None: emitted.update({"payload": payload}),
|
||||
)
|
||||
tui_server._emit_approval_request("s", {"description": "no command here"})
|
||||
assert emitted["payload"] == {"description": "no command here"}
|
||||
tui_server._emit_approval_request("s", None)
|
||||
assert emitted["payload"] == {}
|
||||
|
||||
def test_no_raw_command_emit_in_approval_registrations(self):
|
||||
"""Every register_gateway_notify approval callback must route through the
|
||||
redacting `_emit_approval_request` helper — no registration may emit the
|
||||
raw payload via `_emit("approval.request", ...)` directly. The ONLY
|
||||
allowed raw emit is inside the helper itself."""
|
||||
from tui_gateway import server as tui_server
|
||||
|
||||
src = inspect.getsource(tui_server)
|
||||
raw_emits = src.count('_emit("approval.request"')
|
||||
assert raw_emits == 1, (
|
||||
f'expected exactly 1 raw _emit("approval.request") (inside the '
|
||||
f"redacting helper), found {raw_emits} — a registration may be "
|
||||
f"emitting the unredacted command"
|
||||
)
|
||||
assert "_emit_approval_request(sid, data)" in src, (
|
||||
"registration lambdas must route through _emit_approval_request"
|
||||
)
|
||||
|
|
@ -806,6 +806,21 @@ def _emit(event: str, sid: str, payload: dict | None = None):
|
|||
write_json({"jsonrpc": "2.0", "method": "event", "params": params})
|
||||
|
||||
|
||||
def _emit_approval_request(sid: str, data: dict | None) -> None:
|
||||
"""Emit an ``approval.request`` event to the TUI client with the command
|
||||
redacted. The approval payload is built from the RAW command string, so a
|
||||
credential-shaped value Tirith flagged would otherwise be echoed verbatim
|
||||
to the TUI client (#48456 — third egress transport alongside the chat
|
||||
platforms and the SSE/API stream fixed in #50767). Reuse the shared gateway
|
||||
seam so all approval transports redact consistently."""
|
||||
payload = dict(data or {})
|
||||
if "command" in payload:
|
||||
from gateway.run import _redact_approval_command
|
||||
|
||||
payload["command"] = _redact_approval_command(payload.get("command"))
|
||||
_emit("approval.request", sid, payload)
|
||||
|
||||
|
||||
def _status_update(sid: str, kind: str, text: str | None = None):
|
||||
body = (text if text is not None else kind).strip()
|
||||
if not body:
|
||||
|
|
@ -1040,7 +1055,7 @@ def _start_agent_build(sid: str, session: dict) -> None:
|
|||
)
|
||||
|
||||
register_gateway_notify(
|
||||
key, lambda data: _emit("approval.request", sid, data)
|
||||
key, lambda data: _emit_approval_request(sid, data)
|
||||
)
|
||||
notify_registered = True
|
||||
load_permanent_allowlist()
|
||||
|
|
@ -2554,7 +2569,7 @@ def _sync_session_key_after_compress(
|
|||
try:
|
||||
register_gateway_notify(
|
||||
new_session_id,
|
||||
lambda data: _emit("approval.request", sid, data),
|
||||
lambda data: _emit_approval_request(sid, data),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -3916,7 +3931,7 @@ def _init_session(
|
|||
try:
|
||||
from tools.approval import register_gateway_notify, load_permanent_allowlist
|
||||
|
||||
register_gateway_notify(key, lambda data: _emit("approval.request", sid, data))
|
||||
register_gateway_notify(key, lambda data: _emit_approval_request(sid, data))
|
||||
load_permanent_allowlist()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue