mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
refactor: pass the approval request id explicitly, not through platform metadata
The runner tunnelled the approval request id to Discord as a private `_approval_request_id` key inside the platform metadata dict. That dict goes to every adapter that renders approval buttons. The relay adapter forwards it verbatim, so the private key reached the external connector in every approval prompt frame. Add an optional `request_id` to `send_exec_approval` and to `ExecApprovalPrompt`, have the runner pass `approval_data["request_id"]`, and have Discord read `prompt.request_id`. The metadata passed to the adapters is back to the thread routing it carried before the PR (`None` in a plain chat). The always-true `"timeout_s" in approval_data` guard goes with it. Native-button adapters render through `_send_exec_approval_prompt`, whose signature is unchanged. The only `send_exec_approval` override, `LocalSessionAdapter`, takes `**kwargs`, as does every test double, so no other adapter or double changes.
This commit is contained in:
1 parent
ac50e08b54
commit
cfed2ca71f
6 files changed
+18
-19
No files matched your search
@@ -1578,6 +1578,7 @@ class ExecApprovalPrompt:
|
||||
description: str
|
||||
smart_denied: bool
|
||||
metadata: Optional[dict[str, Any]] = None
|
||||
request_id: Optional[str] = None # the ``tools.approval`` request this card answers (and only that one)
|
||||
|
||||
@property
|
||||
def choices(self) -> list[str]:
|
||||
@@ -2790,16 +2791,15 @@ class BasePlatformAdapter(ABC):
|
||||
async def send_exec_approval(
|
||||
self, chat_id: str, command: str, session_key: str, description: Optional[str] = None,
|
||||
metadata: Optional[dict[str, Any]] = None, allow_permanent: bool = True, allow_session: bool = True,
|
||||
smart_denied: bool = False,
|
||||
smart_denied: bool = False, request_id: Optional[str] = None,
|
||||
) -> SendResult:
|
||||
"""Interactive exec-approval prompt; a press resolves via
|
||||
``tools.approval.resolve_gateway_approval``. Text and choice set are shared; adapters
|
||||
render them natively in ``_send_exec_approval_prompt``."""
|
||||
"""Interactive exec-approval prompt; a press resolves via ``tools.approval.resolve_gateway_approval``.
|
||||
Text and choice set are shared; adapters render them natively in ``_send_exec_approval_prompt``."""
|
||||
if description is None:
|
||||
description = ea_default_reason_text()
|
||||
prompt = ExecApprovalPrompt(
|
||||
chat_id=chat_id, session_key=session_key, metadata=metadata, command=str(command or ""),
|
||||
description=description, smart_denied=smart_denied,
|
||||
description=description, smart_denied=smart_denied, request_id=request_id,
|
||||
text=self._format_exec_approval(command, description, smart_denied),
|
||||
actions=self._exec_approval_actions(
|
||||
allow_permanent=allow_permanent, allow_session=allow_session, smart_denied=smart_denied))
|
||||
|
||||
@@ -725,14 +725,11 @@ class TurnRunner(GatewayTurnProgressMixin, GatewaySessionAgentMixin):
|
||||
# Check the *class*, not the instance — MagicMock auto-creates attributes in tests.
|
||||
if _renders_exec_approval_buttons(type(adapter)):
|
||||
try:
|
||||
approval_metadata = ctx._status_thread_metadata
|
||||
if "timeout_s" in approval_data:
|
||||
approval_metadata = dict(approval_metadata or {})
|
||||
approval_metadata["_approval_request_id"] = approval_data.get("request_id")
|
||||
fut = self._schedule(
|
||||
adapter.send_exec_approval(
|
||||
chat_id=ctx._status_chat_id, command=cmd, session_key=ctx.session_key or "",
|
||||
description=desc, metadata=approval_metadata, **flags,
|
||||
description=desc, metadata=ctx._status_thread_metadata,
|
||||
request_id=approval_data.get("request_id"), **flags,
|
||||
),
|
||||
"send_exec_approval scheduling error",
|
||||
)
|
||||
|
||||
@@ -5727,7 +5727,7 @@ class DiscordAdapter(DiscordAuthorizationMixin, DiscordMediaMixin, DiscordThread
|
||||
allowed_role_ids=self._allowed_role_ids, require_admin=require_admin,
|
||||
admin_user_ids=admin_user_ids, allow_permanent="always" in choices,
|
||||
allow_session="session" in choices, smart_denied=prompt.smart_denied,
|
||||
request_id=(prompt.metadata or {}).get("_approval_request_id"),
|
||||
request_id=prompt.request_id,
|
||||
)
|
||||
view_holder.append(view)
|
||||
send_kwargs: dict[str, Any] = {"content": content, "embed": embed, "view": view}
|
||||
|
||||
@@ -30,7 +30,7 @@ async def _pending_card(monkeypatch):
|
||||
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
|
||||
await adapter.send_exec_approval(
|
||||
chat_id="555", command="ls", session_key=session,
|
||||
metadata={"_approval_request_id": "request-1"},
|
||||
request_id="request-1",
|
||||
)
|
||||
return adapter, sent["view"], entry, message
|
||||
|
||||
@@ -192,7 +192,7 @@ async def test_external_answer_then_stale_click_preserves_resolved_observer(monk
|
||||
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
|
||||
await adapter.send_exec_approval(
|
||||
chat_id="555", command="ls", session_key=session,
|
||||
metadata={"_approval_request_id": "request-1"},
|
||||
request_id="request-1",
|
||||
)
|
||||
view = sent["view"]
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_exec_approval_view_honors_discord_timeout_with_request_id(monkeyp
|
||||
|
||||
result = await adapter.send_exec_approval(
|
||||
chat_id="555", command="ls", session_key="agent:other:discord:555",
|
||||
metadata={"_approval_request_id": "request-1"},
|
||||
request_id="request-1",
|
||||
)
|
||||
|
||||
assert result.success
|
||||
@@ -96,7 +96,7 @@ async def test_expired_card_cannot_resolve_a_different_pending_request(monkeypat
|
||||
session = "agent:other:discord:555"
|
||||
await adapter.send_exec_approval(
|
||||
chat_id="555", command="ls", session_key=session,
|
||||
metadata={"_approval_request_id": "expired"},
|
||||
request_id="expired",
|
||||
)
|
||||
other = _ApprovalEntry({"request_id": "new", "command": "rm other"})
|
||||
monkeypatch.setitem(approval._gateway_queues, session, [other])
|
||||
@@ -148,7 +148,7 @@ async def test_card_retires_when_backend_request_ends(monkeypatch, expire_during
|
||||
adapter._client = SimpleNamespace(get_channel=lambda _id: channel)
|
||||
await adapter.send_exec_approval(
|
||||
chat_id="555", command="ls", session_key=session,
|
||||
metadata={"_approval_request_id": "request-1"},
|
||||
request_id="request-1",
|
||||
)
|
||||
view = sent["view"]
|
||||
if not expire_during_send:
|
||||
|
||||
@@ -31,6 +31,7 @@ class _ButtonAdapter:
|
||||
self.sends: list[str] = []
|
||||
self.edits: list[tuple] = []
|
||||
self.card_metadata: list[Any] = []
|
||||
self.card_request_ids: list[Any] = []
|
||||
self.send_metadata: list[Any] = []
|
||||
self._editable = editable
|
||||
|
||||
@@ -39,6 +40,7 @@ class _ButtonAdapter:
|
||||
|
||||
async def send_exec_approval(self, *a: Any, **k: Any) -> SendResult:
|
||||
self.card_metadata.append(k.get("metadata"))
|
||||
self.card_request_ids.append(k.get("request_id"))
|
||||
return SendResult(success=True, message_id="card-1")
|
||||
|
||||
async def send(self, chat_id: str, message: str, **k: Any) -> SendResult:
|
||||
@@ -112,9 +114,9 @@ def test_prompt_carries_request_identity(pending_entry):
|
||||
data = dict(pending_entry.data, timeout_s=3600)
|
||||
_runner(adapter)._approval_notify_sync(data)
|
||||
|
||||
assert adapter.card_metadata == [{
|
||||
"thread_id": "t1", "_approval_request_id": pending_entry.data["request_id"],
|
||||
}]
|
||||
assert adapter.card_request_ids == [pending_entry.data["request_id"]]
|
||||
# Platform metadata stays the thread routing only: the relay forwards it verbatim.
|
||||
assert adapter.card_metadata == [{"thread_id": "t1"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text_fallback", [False, True])
|
||||
|
||||
Reference in new issue
Block a user