Skip to content

feat(capsule): short-circuit interceptor chain with veto semantics#587

Merged
joshuajbouw merged 2 commits intomainfrom
feat/interceptor-chain
Mar 22, 2026
Merged

feat(capsule): short-circuit interceptor chain with veto semantics#587
joshuajbouw merged 2 commits intomainfrom
feat/interceptor-chain

Conversation

@joshuajbouw
Copy link
Contributor

Linked Issue

Closes #585

Summary

Evolve the interceptor priority system from "ordered execution of all matching interceptors" to a middleware chain with short-circuit/veto capability. Interceptors return Continue, Final, or Deny to control the chain. A guard at priority 10 can veto an event before the core handler at priority 100 ever sees it.

Changes

  • Add InterceptResult enum (Continue/Final/Deny) to capsule.rs with wire format decoder
  • Change invoke_interceptor return type from CapsuleResult<Vec<u8>> to CapsuleResult<InterceptResult> across all trait implementations (Capsule, ExecutionEngine, WasmEngine, McpEngine)
  • WASM engine: decode guest bytes via InterceptResult::from_guest_bytes() — backward compatible (empty bytes = Continue)
  • Dispatcher: multi-interceptor events run sequentially in priority order as a chain; single-interceptor events use existing per-capsule queue fast path
  • Update hooks trigger (sys.rs) to handle all three result variants
  • Tests for Deny short-circuit, Final short-circuit, and wire format round-trip

Prior Art

  • Bevy ECS — system ordering with run conditions
  • Express.js / Koanext() middleware chain
  • Envoy/Istio — HTTP filter chain with early termination
  • Linux netfilter — NF_ACCEPT / NF_DROP / NF_QUEUE
  • DOM events — stopPropagation() / preventDefault()

Test Plan

Automated

  • cargo test --workspace passes (24 dispatcher tests including 3 new chain tests)
  • No new clippy warnings

Manual

  • Deploy guard capsule at priority 10 with Deny, verify core at 100 never fires
  • Deploy cache capsule at priority 30 with Final, verify core at 100 never fires
  • Existing capsules (no discriminant byte) continue working unchanged

Checklist

  • Linked to an issue
  • CHANGELOG.md updated under [Unreleased]

Interceptors now return InterceptResult (Continue/Final/Deny) instead
of raw bytes. The dispatcher runs matching interceptors sequentially
in priority order and halts the chain on Final or Deny.

- Continue: pass (possibly modified) payload to next interceptor
- Final: short-circuit with response, no further interceptors fire
- Deny: short-circuit with audit-logged reason, no further interceptors

Wire format: discriminant byte (0x00/0x01/0x02) + payload. Empty bytes
treated as Continue for backward compatibility with existing capsules.

Single-interceptor events use the existing per-capsule queue fast path.
Multi-interceptor events run as a sequential chain in a spawned task.

Prior art: Bevy ECS (system ordering + run conditions), Express.js
(next() middleware), Envoy (filter chain), Linux netfilter (NF_ACCEPT/
NF_DROP), DOM events (stopPropagation).

Closes #585
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the interceptor system by implementing a middleware chain with short-circuit capabilities. This change allows for more flexible and powerful control over event processing, enabling features like input guards and caching layers. The introduction of the InterceptResult enum and the corresponding logic in the dispatcher and WASM engine provides a robust and backward-compatible mechanism for managing the interceptor chain.

Highlights

  • Interceptor Chain Evolution: The interceptor priority system has been evolved into a middleware chain with short-circuit/veto capability, allowing interceptors to return Continue, Final, or Deny to control the chain.
  • InterceptResult Enum: An InterceptResult enum (Continue/Final/Deny) has been added to capsule.rs with a wire format decoder to manage the flow of the interceptor chain.
  • WASM Engine Compatibility: The WASM engine now decodes guest bytes via InterceptResult::from_guest_bytes(), ensuring backward compatibility where empty bytes are treated as Continue.
  • Dispatcher Updates: The dispatcher has been updated to run multi-interceptor events sequentially in priority order as a chain, while single-interceptor events continue to use the existing per-capsule queue fast path.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


A byte is sent with care, Through chains of filters there. Deny, or Final say, To change the flow today, And alter fate's affair.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable short-circuiting mechanism for the interceptor chain, allowing interceptors to return Continue, Final, or Deny to control the execution flow. The implementation is well-structured, with a clear InterceptResult enum and backward-compatible wire format handling. The dispatcher logic is updated to manage the chain, including a fast-path for single-interceptor events, and the new functionality is thoroughly covered by tests.

I've found one issue in the single-interceptor dispatch logic where the interceptor's result was previously ignored. My review includes a suggestion to address this, ensuring proper handling of Result outcomes and preventing silent failures, consistent with repository guidelines.

Move has_valid_segments and topic_matches from dispatcher.rs into
topic.rs. Pure extraction — no logic changes. Reduces dispatcher.rs
from 1072 to 936 lines.

Also addresses review: explicitly handle all InterceptResult variants
in the single-interceptor dispatch path instead of Ok(_).
@joshuajbouw joshuajbouw merged commit ef6d281 into main Mar 22, 2026
13 checks passed
@joshuajbouw joshuajbouw deleted the feat/interceptor-chain branch March 22, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: short-circuit interceptor chain with veto semantics

1 participant