feat(gateway): composable output-guard pipeline + non-blocking suggest_actions tool - #77905
Draft
jeffreyhlin wants to merge 3 commits into
Draft
feat(gateway): composable output-guard pipeline + non-blocking suggest_actions tool#77905jeffreyhlin wants to merge 3 commits into
jeffreyhlin wants to merge 3 commits into
Conversation
Generalize the three scattered outbound checks (secret redaction, provider-error rewriting, silence-narration drop) into one ordered, fault-isolated validator chain in gateway/output_guards.py. Each guard owns a single concern and can rewrite or drop a message; new rules drop in as one function plus one registry entry. Adds two opt-in guards: em-dash stripping (gateway.guards.strip_em_dashes) and async link verification (gateway.guards.verify_links). Wired into the two existing chokepoints: - _sanitize_gateway_final_response (agent final replies) - DeliveryRouter._deliver_to_platform (cron deliveries) Legacy behaviour preserved exactly, with fallback paths so a pipeline error can never block delivery. 15 tests.
Generalize the clarify interaction pattern into a fire-and-forget affordance primitive. Where clarify blocks the agent on an A/B/C question, suggest_actions attaches N tappable actions to a message and ends the turn; tapping one starts a NEW turn seeded with that action's payload. - tools/suggest_actions_tool.py: schema + validation/normalization - tools/suggested_actions_gateway.py: non-blocking action registry (register/resolve/eviction/session-cleanup), no blocking Event - gateway/platforms/base.py: default numbered-list text fallback - gateway/platforms/telegram.py: native inline-button render + sa:<set_id>:<index> callback that injects a synthetic user turn - gateway/run.py: suggest_actions_callback bridge + session cleanup - tool_executor + agent_init + toolsets: dispatch and registration Degrades gracefully on platforms without buttons. 15 tests.
Rebasing onto current main surfaced two real defects in the guard pipeline commit: 1. _provider_error_guard was still Telegram-gated, a leftover from when _sanitize_gateway_final_response only covered Telegram. Upstream widened that security invariant to every chat surface (NousResearch#28533 -> NousResearch#39293), so WhatsApp/Slack/Signal/Matrix would have leaked raw provider error envelopes (which can carry bearer tokens). The guard now applies to every surface reaching it; programmatic surfaces are already excluded by _GATEWAY_RAW_TEXT_PLATFORMS upstream of the call. 2. The test fixture leaked the opt-in em-dash guard into unrelated tests via the module-level pipeline singleton. Env flags are now cleared before the reset on both setup and teardown. Also relaxes the secret-redaction assertion to test the behaviour contract (raw credential does not survive) instead of a specific mask marker, since the authoritative agent.redact redactor masks with '***' while the gateway fallback uses '[REDACTED]'.
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related extension points at the gateway's outbound seam, both generalizing
patterns that already existed as one-off code paths.
1. Composable output-guard pipeline (
gateway/output_guards.py)Three outbound checks previously lived as separate inline code paths:
_redact_gateway_user_facing_secrets)_looks_like_gateway_provider_error)_is_silence_narrationindelivery.py)This folds them into one ordered, fault-isolated validator chain. A guard is a
(text, ctx) -> GuardOutcome | Nonecallable that can rewrite or drop amessage; a drop short-circuits the rest. Guards may be sync or async. The
pipeline never raises: a guard that throws is logged and skipped, so a buggy
rule can never block delivery.
Wired into the two existing chokepoints (
_sanitize_gateway_final_responsefor agent replies,
DeliveryRouter._deliver_to_platformfor cron deliveries),both with fallbacks to the legacy inline path.
Two new guards ship off by default, enabled via
gateway.guards.*inconfig.yaml(no new env vars):strip_em_dashes— style rule as a guardverify_links— async; never emit a URL that doesn't resolveAdding a rule is now one function plus one registry entry, instead of another
inline branch at a call site.
2.
suggest_actionstool — non-blocking tappable follow-up actionsGeneralizes the
clarifyinteraction pattern. Whereclarifyblocks theagent on an A/B/C question,
suggest_actionsis fire-and-forget: the agentattaches up to 6 actions to a message and finishes its turn. Tapping one starts
a new turn seeded with that action's payload.
tools/suggest_actions_tool.py— schema + validation/normalizationtools/suggested_actions_gateway.py— non-blocking registry(register/resolve/eviction/session-cleanup). No blocking
Event, no timeoutthread; a per-session cap bounds memory without a reaper.
gateway/platforms/base.py— default numbered-list text fallback, so everyplatform degrades gracefully
plugins/platforms/telegram/adapter.py— native inline buttons(
sa:<set_id>:<index>); a tap resolves the payload server-side (Telegramcaps
callback_dataat 64 bytes) and injects a syntheticMessageEventthrough the normal
handle_messageentry pointAny turn that would end in "want me to do X or Y?" can become tappable, so
recommendation cards, cron confirmations, and snooze shortcuts all get the
affordance for free.
Registered in the existing
clarifytoolset rather than a new one, so thecore tool-schema footprint grows by exactly one tool.
Notes for review
HERMES_*config env vars; both new guards readconfig.yaml. TheHERMES_GUARD_*vars are test/debug overrides only.untouched.
suggest_actionsdispatch goes through_run_agent_tool_execution_middleware, matching the surrounding tools._provider_error_guarddeliberately applies to all chat surfaces, notjust Telegram, per the widened invariant in fix(gateway): quiet noisy Telegram errors and sanitize provider failures #28533 -> [Bug]: Gateway status/error sanitization is Telegram-only — WhatsApp/Discord/Slack/Signal leak internal noise to users #39293. Programmatic
surfaces are excluded upstream by
_GATEWAY_RAW_TEXT_PLATFORMS.Tests
30 new tests (
tests/gateway/test_output_guards.py,tests/tools/test_suggest_actions.py) covering pipeline mechanics(rewrite/drop/short-circuit/fault-isolation/sync-vs-async), each built-in
guard, tool validation, and registry lifecycle including eviction.
Verified green together with the suites this touches:
Assertions target behavior contracts rather than snapshots (e.g. the secret
test asserts the raw credential does not survive, not a specific mask marker,
since
agent.redactmasks with***while the gateway fallback uses[REDACTED]).