Skip to content

Commit 8fc4e5e

Browse files
yzhouwangclaude
andcommitted
feat(hooks): silently abort on HookAbortError in run_agent + 3rd-party runner
Patch AstrBotDevs#2 added fail_closed=True semantics to the 6 LLM filter decorators and a HookAbortError catch in agent_sub_stages/internal.py. But two other sites still let the abort fall through their broad `except Exception`: - astr_agent_run_util.py:303 — the local agent runner's main loop. The broad catch built `Error occurred during AI execution. Error Type: HookAbortError. Error Message: ...` and called on_agent_done with that string as the LLMResponse, which (a) leaked the hook name + plugin path + reason to the user, and (b) re-fired the on_llm_response hook chain — the same chain that had just aborted — causing a cascade. - third_party.py:82 — same pattern in the third-party agent runner. Yields the error string to the user via the response chain. Fix: add a dedicated `except HookAbortError` ahead of the broad catch in both sites. On abort: - log the hook / plugin / handler / reason at error level (operator- facing only — never user-facing) - do NOT call on_agent_done (avoid the cascade) - send the persona's custom_error_message as a polite refusal if set; otherwise stay silent - in run_agent: stop the event so the respond stage doesn't wrap and forward an empty result This is the third in the SuperX governance hook safety suite (Patches AstrBotDevs#1=signature, AstrBotDevs#2=fail_closed, AstrBotDevs#3=silent-abort). Existing test_hook_fail_closed.py still passes (9/9). Worth adding a test for the run_agent path in a follow-up — the current suite covers context_utils.call_event_hook directly but not the runner's outer catch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce676e4 commit 8fc4e5e

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

astrbot/core/astr_agent_run_util.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from astrbot.core.agent.message import Message
99
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
1010
from astrbot.core.astr_agent_context import AstrAgentContext
11+
from astrbot.core.exceptions import HookAbortError
1112
from astrbot.core.message.components import BaseMessageComponent, Json, Plain
1213
from astrbot.core.message.message_event_result import (
1314
MessageChain,
@@ -300,6 +301,36 @@ async def run_agent(
300301

301302
break
302303

304+
except HookAbortError as e:
305+
# A fail_closed governance hook aborted the agent. We must NOT
306+
# echo the error to the user (it leaks internal hook names /
307+
# plugin paths / failure reasons), and we MUST NOT re-fire
308+
# on_agent_done with a synthesised error response — that would
309+
# cascade through the same hook chain that just aborted.
310+
#
311+
# Send the persona's custom_error_message (if any) as a polite
312+
# refusal placeholder; otherwise stay silent.
313+
if "stop_watcher" in locals() and not stop_watcher.done():
314+
stop_watcher.cancel()
315+
try:
316+
await stop_watcher
317+
except asyncio.CancelledError:
318+
pass
319+
logger.error(
320+
"AI execution aborted by fail_closed hook %s -> %s.%s: %s",
321+
e.hook_name,
322+
e.plugin_name,
323+
e.handler_name,
324+
e.reason,
325+
)
326+
placeholder = extract_persona_custom_error_message_from_event(astr_event)
327+
if placeholder:
328+
if agent_runner.streaming:
329+
yield MessageChain().message(placeholder)
330+
else:
331+
astr_event.set_result(MessageEventResult().message(placeholder))
332+
astr_event.stop_event()
333+
return
303334
except Exception as e:
304335
if "stop_watcher" in locals() and not stop_watcher.done():
305336
stop_watcher.cancel()

astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING
55

66
from astrbot.core import astrbot_config, logger
7+
from astrbot.core.exceptions import HookAbortError
78
from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
89
from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
910
DashscopeAgentRunner,
@@ -79,6 +80,19 @@ async def run_third_party_agent(
7980
yield resp.data["chain"], False
8081
elif resp.type == "err":
8182
yield resp.data["chain"], True
83+
except HookAbortError as e:
84+
# fail_closed hook aborted — silent refusal (or persona placeholder
85+
# via custom_error_message). Don't leak hook names / reasons.
86+
logger.error(
87+
"Third-party agent aborted by fail_closed hook %s -> %s.%s: %s",
88+
e.hook_name,
89+
e.plugin_name,
90+
e.handler_name,
91+
e.reason,
92+
)
93+
if custom_error_message:
94+
yield MessageChain().message(custom_error_message), True
95+
return
8296
except Exception as e:
8397
logger.error(f"Third party agent runner error: {e}")
8498
err_msg = custom_error_message

0 commit comments

Comments
 (0)