feat(webhook): let trusted relays hand off to bounded profiles - #92511
feat(webhook): let trusted relays hand off to bounded profiles#92511fangliquanflq wants to merge 8 commits into
Conversation
andrexibiza
left a comment
There was a problem hiding this comment.
Reviewed exact head a8e91abb5267b33d30ddbf07e3806afdb7d0a847 against current main / PR base 13f4cfebfafbce8ac9d1bf29f66731858ed638b5. The target/profile/toolset separation is mostly well-shaped: _hermes is resolved only after request authentication, stripped before filters/scripts/prompt/raw persistence, the target must be both route-allowlisted and actually served, toolsets are server-owned per target, egress does not select execution identity, and the new provenance makes the source/target split inspectable.
There is one blocking replay/idempotency authority hole before this can safely become the terminal/file-bearing relay described by #92493.
_validate_signature() authenticates the body (plus timestamp for generic V2), but _handle_webhook() later derives the dedupe key from X-GitHub-Delivery, svix-id, X-Request-ID, or a local timestamp. For generic V2 the signed material is only <timestamp>.<body>; X-Request-ID is not signed. GitHub likewise signs the body, not X-GitHub-Delivery; Linear is body-only; legacy generic V1 is explicitly documented in this file as replayable. _record_delivery_id() then stores only that caller/header-derived ID in the process-local _seen_deliveries dict.
That means the new privileged handoff can execute the same authenticated task more than once even though the docs now say “Existing delivery-ID idempotency prevents retry duplication.” A concrete generic-V2 sequence is:
- send a valid signed body containing
_hermes.target_profileand a terminal/file task withX-Request-ID: A; - replay the same body, timestamp, and valid signature inside the accepted 300-second window, changing only the unsigned
X-Request-IDtoB; - signature validation succeeds again and
_record_delivery_id("B")treats it as new, so the same privileged task is dispatched twice.
There is a second deterministic boundary on ordinary retries: _seen_deliveries is initialized empty in __init__, so restarting the gateway between two deliveries with the same authenticated delivery ID also admits the second execution. For a notification-only webhook that was already imperfect; once this PR allows a relay to grant terminal/file, duplicate execution can repeat writes or commands and directly violates #92493’s required retry/dedup guardrail.
Required repair: make trusted-handoff replay identity part of authenticated authority and durable across the execution boundary. I would not key privileged admission solely on an unsigned transport header or an in-memory cache. A signed provider message ID (Svix/Standard Webhooks style), or a server-required nonce/request ID inside the signed handoff envelope, can be claimed atomically and durably before dispatch; timestamp freshness remains useful but is not a dedupe authority by itself. Existing provider-native routes that cannot furnish a replay-resistant identity need an explicit policy for whether they are eligible for trusted handoff rather than inheriting the ordinary-webhook acceptance path automatically.
Please add production-path regressions for at least: (a) same valid generic-V2 handoff + changed X-Request-ID cannot execute twice, (b) the same authenticated handoff retried after adapter/gateway reconstruction cannot execute twice, and (c) the signed-ID path (Svix / Standard Webhooks once composed) still accepts one execution and classifies the retry deterministically.
Topology / merge order: #92493 by @mporenta is the immediate owning contract; broader #17415 by @yuzilongleif-collab already names deduplication, recursion/concurrency and audit as first-class handoff guardrails. Merged #83400 by @tmchow is complementary served-profile authority and this PR correctly builds on it. Open/non-mergeable #90589 by @jzOcb is adjacent webhook session-identity work (preserving Gilles Gameiro / Atroci provenance), not a duplicate; if that lane survives, its persistent/fallback session-key namespaces need to compose without making handoff route/toolset authority depend on parsing the current one-shot webhook:{route}:{delivery} chat-id shape. Open/non-mergeable #92024 is a competing webhook-auth evolution that adds a signed Standard-Webhooks message ID; that can provide one good replay identity but does not by itself close generic-V2/GitHub/Linear or restart durability here.
Exact-head hosted receipts are green: CI 32597272019, Docker 32597271607, Nix 32597271627. Those runs verify the current object but the new handoff suite does not exercise either replay sequence above.
What does this PR do?
Adds an explicit trusted profile handoff contract for authenticated static webhook routes. General-purpose relays can now select a route-allowlisted specialist profile with a separately configured toolset ceiling, while ordinary webhook routes retain the constrained safe defaults.
The selector is resolved from a reserved authenticated body envelope before task processing and is removed before prompt rendering, scripts, or raw payload persistence. Egress destinations such as Discord remain delivery-only and never grant execution identity.
Related Issue
Closes #92493
Type of Change
Changes Made
gateway/platforms/webhook.py- validates static handoff configuration, resolves allowlisted served profiles, enforces target-specific toolsets plus depth/concurrency bounds, strips authority metadata from task content, and preserves retry idempotency.gateway/session.py- persists descriptive execution provenance and exposes ingress, source profile, target profile, effective toolsets, and delivery destination in session diagnostics.tests/gateway/test_webhook_trusted_handoff.py- covers allowed and denied targets, toolset expansion attempts, free-form selector rejection, authentication, concurrency release, default-route behavior, provenance, and diagnostics.website/docs/user-guide/messaging/webhooks.md- documents configuration, request shape, security boundaries, and operational diagnostics.How to Test
gateway.multiplex_profilesand configure a static webhook route withallowed_target_profilesandallowed_target_toolsets._hermes.target_profileand_hermes.handoff_depth; verify the accepted event uses the selected profile and bounded toolsets while Discord remains the egress destination.Result: 136 passed.
Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
cli-config.yaml.exampleis N/A because these keys live in per-routeconfig.yamlCONTRIBUTING.mdandAGENTS.mdare N/A because the existing multiplex and webhook architecture is extended without changing contributor workflowScreenshots / Logs