Skip to content

feat(aios-runtime): add turn middleware chain - #4

Merged
broomva merged 1 commit into
mainfrom
bro-422-turn-middleware
Apr 3, 2026
Merged

broomva merged 1 commit into
mainfrom
bro-422-turn-middleware

Conversation

@broomva

@broomva broomva commented Apr 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add an ordered TurnMiddleware chain to the canonical KernelRuntime::tick_on_branch path
  • introduce mutable TurnContext plus a terminal executor so middleware can rewrite turn input/state without bypassing runtime behavior
  • expose kernel-builder hooks and add integration coverage proving middleware can rewrite a turn

Validation

  • cargo fmt --all
  • cargo clippy --workspace -- -D warnings
  • cargo test --workspace

Linear: BRO-422

Summary by CodeRabbit

Release Notes

  • New Features

    • Kernel now supports an ordered middleware chain for turn processing, enabling customization of turn behavior before execution.
    • Middleware can access and modify turn context (input, state, and pending approvals) during processing.
  • Documentation

    • Updated architecture documentation to reflect the new turn middleware chain in the kernel tick lifecycle.

@linear

linear Bot commented Apr 2, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Apr 2, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A middleware system is introduced to the kernel runtime. The KernelBuilder now accepts configurable turn middlewares, KernelRuntime processes tick operations through an ordered middleware chain before terminal execution, and new public APIs (TurnContext, TurnMiddleware, TurnNext) enable custom middleware that can inspect and modify turn context.

Changes

Cohort / File(s) Summary
Kernel Runtime Middleware System
crates/aios-runtime/src/lib.rs
Refactored tick_on_branch to route through middleware pipeline. Added TurnContext, TurnMiddleware trait, TurnNext for chaining, and PassthroughTurnMiddleware. Extracted core logic to execute_turn. Extended KernelRuntime with turn_middlewares field and with_turn_middlewares(...) constructor.
Kernel Builder Integration
crates/aios-kernel/src/lib.rs
Added turn_middlewares field to KernelBuilder. Introduced turn_middlewares(...) builder method. Modified build() to pass middlewares to KernelRuntime::with_turn_middlewares(...) instead of new(). Extended tests with middleware that mutates turn context.
Dependencies & Configuration
crates/aios-runtime/Cargo.toml
Added async-trait workspace dependency to support async trait definitions.
Documentation Updates
crates/aios-runtime/README.md, docs/ARCHITECTURE.md, docs/STATUS.md
Updated README to document turn middleware composition and TurnContext. Revised ARCHITECTURE.md tick lifecycle to include middleware chain before core phases. Updated STATUS.md with implementation status and date.

Sequence Diagram

sequenceDiagram
    participant Client
    participant KernelRuntime
    participant TurnContext
    participant Middleware1
    participant MiddlewareN
    participant TurnNext
    participant ExecuteTurn

    Client->>KernelRuntime: tick_on_branch(input)
    activate KernelRuntime
    
    KernelRuntime->>TurnContext: build(session, branch, input, state)
    activate TurnContext
    TurnContext->>TurnContext: set pending_approvals, estimated_mode
    deactivate TurnContext
    
    KernelRuntime->>TurnNext: run(middlewares[0], ctx)
    activate TurnNext
    
    TurnNext->>Middleware1: process(ctx, next)
    activate Middleware1
    note over Middleware1: inspect/mutate ctx
    Middleware1->>TurnNext: next.run()
    deactivate Middleware1
    
    TurnNext->>MiddlewareN: process(ctx, next)
    activate MiddlewareN
    note over MiddlewareN: inspect/mutate ctx
    MiddlewareN->>TurnNext: next.run()
    deactivate MiddlewareN
    
    TurnNext->>ExecuteTurn: execute_turn(ctx)
    activate ExecuteTurn
    note over ExecuteTurn: sense, estimate, gate, execute, commit, reflect
    ExecuteTurn-->>TurnNext: TickOutput
    deactivate ExecuteTurn
    
    deactivate TurnNext
    KernelRuntime-->>Client: TickOutput
    deactivate KernelRuntime
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop through the middleware maze,
Where turn contexts dance in ordered phase,
KernelRuntime chains with practiced grace,
Each middleware gets its time and place,
The kernel ticks with poise today! 🌿✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(aios-runtime): add turn middleware chain' directly and concisely summarizes the main change: introducing a turn middleware chain feature to the aios-runtime crate, which is the primary focus of all the modifications across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bro-422-turn-middleware

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/aios-runtime/src/lib.rs`:
- Around line 59-68: TurnContext's session/branch identity and workspace
metadata are publicly mutable, allowing middleware to change session_id,
branch_id or manifest.workspace_root; make these fields immutable by removing
pub from session_id, branch_id and manifest (or make TurnContext a
private/internal struct) and provide read-only accessors (e.g., session_id(),
branch_id(), manifest()) and a constructor/builder for creating TurnContext
instances; update execute_turn and any callers to use the new getters instead of
direct field access so all mutations must go through controlled runtime APIs.
- Around line 327-355: The code currently only checks the session and then runs
middlewares; instead, validate the branch lifecycle before constructing
TurnContext and invoking TurnNext::new(...).run(...) — ensure you look up the
branch by branch_id (using the same session lookup pattern around
self.sessions.lock() or the branch-store accessor), reject missing branches,
reject merged/read-only branches, enforce bounded fork sequence and disallow
using "main" as a merge source, and return an error early (before creating
TurnContext or calling turn_middlewares) so middleware never executes for
invalid/merged branches; modify the block that builds manifest/state and
immediately add a branch validation step referencing session_id and branch_id,
then only proceed to build TurnContext and call TurnNext::new(self,
&self.turn_middlewares).run(&mut ctx).await when the branch check passes.
- Around line 70-98: The current TurnMiddleware::process signature allows
middleware to return or mutate TickOutput and skip the terminal runtime path
(bypassing KernelRuntime::execute_turn), so change the middleware shape to
enforce the runtime path and explicit post-processing: replace the single
process method with explicit before/after hooks (e.g., before(&self, ctx: &mut
TurnContext) -> Result<()> and after(&self, ctx: &mut TurnContext, output: &mut
TickOutput) -> Result<()>) or otherwise require that process always call
TurnNext::run(ctx) and only perform side effects via an after hook; update
TurnNext/TurnNext::run to always delegate to KernelRuntime::execute_turn for the
terminal step and then invoke middleware after hooks in order, and adjust call
sites that implement TurnMiddleware to implement the new before/after methods
(references: TurnMiddleware, TurnNext, TurnNext::run, TurnContext, TickOutput,
KernelRuntime::execute_turn).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 922933a7-4253-4c3a-b931-6cd72728f725

📥 Commits

Reviewing files that changed from the base of the PR and between b8d2b31 and c8e55cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • crates/aios-kernel/src/lib.rs
  • crates/aios-runtime/Cargo.toml
  • crates/aios-runtime/README.md
  • crates/aios-runtime/src/lib.rs
  • docs/ARCHITECTURE.md
  • docs/STATUS.md

Comment on lines +59 to +68
#[derive(Debug, Clone)]
pub struct TurnContext {
pub session_id: SessionId,
pub branch_id: BranchId,
pub manifest: SessionManifest,
pub input: TickInput,
pub state: AgentStateVector,
pub pending_approvals: Vec<ApprovalTicket>,
pub mode: OperatingMode,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Keep session/branch identity and workspace metadata immutable.

execute_turn() later trusts ctx.session_id, ctx.branch_id, and ctx.manifest.workspace_root for event appends, tool execution, checkpointing, and persisted state. With these fields publicly mutable, a buggy middleware can redirect a turn into another session/branch or arbitrary workspace path without going through any runtime guard.

🔒 Suggested shape
 pub struct TurnContext {
-    pub session_id: SessionId,
-    pub branch_id: BranchId,
-    pub manifest: SessionManifest,
+    session_id: SessionId,
+    branch_id: BranchId,
+    manifest: SessionManifest,
     pub input: TickInput,
     pub state: AgentStateVector,
     pub pending_approvals: Vec<ApprovalTicket>,
     pub mode: OperatingMode,
 }
+
+impl TurnContext {
+    pub fn session_id(&self) -> &SessionId {
+        &self.session_id
+    }
+
+    pub fn branch_id(&self) -> &BranchId {
+        &self.branch_id
+    }
+
+    pub fn manifest(&self) -> &SessionManifest {
+        &self.manifest
+    }
+}

As per coding guidelines, "Prefer deterministic file paths under session workspace" and "Route session state changes through aios-runtime."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aios-runtime/src/lib.rs` around lines 59 - 68, TurnContext's
session/branch identity and workspace metadata are publicly mutable, allowing
middleware to change session_id, branch_id or manifest.workspace_root; make
these fields immutable by removing pub from session_id, branch_id and manifest
(or make TurnContext a private/internal struct) and provide read-only accessors
(e.g., session_id(), branch_id(), manifest()) and a constructor/builder for
creating TurnContext instances; update execute_turn and any callers to use the
new getters instead of direct field access so all mutations must go through
controlled runtime APIs.

Comment on lines +70 to +98
#[async_trait]
pub trait TurnMiddleware: Send + Sync {
async fn process(&self, ctx: &mut TurnContext, next: TurnNext<'_>) -> Result<TickOutput>;
}

pub struct TurnNext<'a> {
runtime: &'a KernelRuntime,
middlewares: &'a [Arc<dyn TurnMiddleware>],
}

impl<'a> TurnNext<'a> {
fn new(runtime: &'a KernelRuntime, middlewares: &'a [Arc<dyn TurnMiddleware>]) -> Self {
Self {
runtime,
middlewares,
}
}

pub async fn run(self, ctx: &mut TurnContext) -> Result<TickOutput> {
match self.middlewares.split_first() {
Some((middleware, remaining)) => {
middleware
.process(ctx, TurnNext::new(self.runtime, remaining))
.await
}
None => self.runtime.execute_turn(ctx).await,
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

This API still lets middleware bypass the terminal runtime path.

A middleware can return its own TickOutput or error without calling next.run(ctx), and it can also rewrite the TickOutput after next returns. That skips or desynchronizes event emission, policy/tool enforcement, sequence accounting, and persisted state, which is the opposite of the contract this feature is supposed to preserve.

🧭 Safer shape
 #[async_trait]
 pub trait TurnMiddleware: Send + Sync {
-    async fn process(&self, ctx: &mut TurnContext, next: TurnNext<'_>) -> Result<TickOutput>;
+    async fn process(&self, ctx: &mut TurnContext) -> Result<()>;
 }
-        TurnNext::new(self, &self.turn_middlewares)
-            .run(&mut ctx)
-            .await
+        for middleware in &self.turn_middlewares {
+            middleware.process(&mut ctx).await?;
+        }
+        self.execute_turn(&mut ctx).await

If you need post-processing, split it into explicit before/after hooks instead of letting middleware own TickOutput.

As per coding guidelines, "Emit events for meaningful transitions and preserve monotonic sequence per session and per branch" and "Route tool execution flows through aios-tools + aios-sandbox."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aios-runtime/src/lib.rs` around lines 70 - 98, The current
TurnMiddleware::process signature allows middleware to return or mutate
TickOutput and skip the terminal runtime path (bypassing
KernelRuntime::execute_turn), so change the middleware shape to enforce the
runtime path and explicit post-processing: replace the single process method
with explicit before/after hooks (e.g., before(&self, ctx: &mut TurnContext) ->
Result<()> and after(&self, ctx: &mut TurnContext, output: &mut TickOutput) ->
Result<()>) or otherwise require that process always call TurnNext::run(ctx) and
only perform side effects via an after hook; update TurnNext/TurnNext::run to
always delegate to KernelRuntime::execute_turn for the terminal step and then
invoke middleware after hooks in order, and adjust call sites that implement
TurnMiddleware to implement the new before/after methods (references:
TurnMiddleware, TurnNext, TurnNext::run, TurnContext, TickOutput,
KernelRuntime::execute_turn).

Comment on lines +327 to +355
let (manifest, state) = {
let sessions = self.sessions.lock();
let session = sessions
.get(session_id.as_str())
.with_context(|| format!("session not found: {session_id}"))?;
(session.manifest.clone(), session.state_vector.clone())
};

let pending_approvals = self
.approvals
.list_pending(session_id.clone())
.await
.unwrap_or_default();
let mode = self.estimate_mode(&state, pending_approvals.len());

let mut ctx = TurnContext {
session_id: session_id.clone(),
branch_id: branch_id.clone(),
manifest,
input,
state,
pending_approvals,
mode,
};

TurnNext::new(self, &self.turn_middlewares)
.run(&mut ctx)
.await
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject invalid or merged branches before entering middleware.

This path only verifies that the session exists. A tick against a nonexistent or read-only branch still runs middleware and fails only when execute_turn() appends the first event, which widens the branch read-only boundary around user-supplied extension code.

🛑 Fail closed before middleware
-        let (manifest, state) = {
+        let (manifest, state) = {
             let sessions = self.sessions.lock();
             let session = sessions
                 .get(session_id.as_str())
                 .with_context(|| format!("session not found: {session_id}"))?;
+            let branch = session
+                .branches
+                .get(branch_id)
+                .with_context(|| format!("branch not found: {}", branch_id.as_str()))?;
+            if let Some(target) = &branch.merged_into {
+                bail!(
+                    "branch {} is merged into {} and is read-only",
+                    branch_id.as_str(),
+                    target.as_str()
+                );
+            }
             (session.manifest.clone(), session.state_vector.clone())
         };

Based on learnings, "Enforce branch lifecycle invariants: bounded fork sequence, merged branches read-only, main not used as merge source."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aios-runtime/src/lib.rs` around lines 327 - 355, The code currently
only checks the session and then runs middlewares; instead, validate the branch
lifecycle before constructing TurnContext and invoking
TurnNext::new(...).run(...) — ensure you look up the branch by branch_id (using
the same session lookup pattern around self.sessions.lock() or the branch-store
accessor), reject missing branches, reject merged/read-only branches, enforce
bounded fork sequence and disallow using "main" as a merge source, and return an
error early (before creating TurnContext or calling turn_middlewares) so
middleware never executes for invalid/merged branches; modify the block that
builds manifest/state and immediately add a branch validation step referencing
session_id and branch_id, then only proceed to build TurnContext and call
TurnNext::new(self, &self.turn_middlewares).run(&mut ctx).await when the branch
check passes.

@broomva
broomva merged commit c079c2e into main Apr 3, 2026
3 checks passed
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.

1 participant