Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

Middleware

Middleware wraps command dispatch, forming an onion around the command call — cross-cutting concerns (logging, timing, auth, rate limiting) live here instead of in every command. It's Elyra's counterpart to Laravel's HTTP middleware.

The trait

use elyra::{command::BoxFuture, CommandRequest, Ctx, Middleware, Next, Result};

struct Timing;

impl Middleware for Timing {
    fn handle(&self, ctx: Ctx, req: CommandRequest, next: Next)
        -> BoxFuture<'static, Result<Vec<u8>>>
    {
        Box::pin(async move {
            let name = req.name.clone();
            let started = std::time::Instant::now();
            let out = next.run(ctx, req).await;   // continue the pipeline
            eprintln!("cmd {name} took {:?}", started.elapsed());
            out
        })
    }
}

Register it (outermost-first — the first added wraps the rest):

App::new().middleware(Timing).commands(commands![..]).run();

Types

  • CommandRequest — { name: String, args: Vec<u8>, origin: Origin }. args is the raw MessagePack body (opaque here, but available to inspect or short-circuit). origin says who's calling: Origin::Frontend (the app's webview, and Rust-side dispatch such as tests and live re-runs) or Origin::Agent { client } — an AI agent over MCP:

    if let Origin::Agent { client } = &req.origin {
        elyra::info!(target: "audit", "{client} called {}", req.name);
    }
    
  • Next — the continuation. Call next.run(ctx, req) to proceed to the next middleware, or — at the end of the chain — the command itself. Not calling it short-circuits (you return your own Result<Vec<u8>>).

  • Middleware must be Send + Sync + 'static (it runs on tokio).

Short-circuiting

fn handle(&self, ctx: Ctx, req: CommandRequest, next: Next)
    -> BoxFuture<'static, Result<Vec<u8>>>
{
    Box::pin(async move {
        if !authorized(&req) {
            return Err(elyra::Error::command("unauthorized"));
        }
        next.run(ctx, req).await
    })
}

An Err becomes an error response; the frontend promise rejects with a CommandError (same as a fallible command).

Ordering

Middleware runs in registration order, outermost first:

App::new()
    .middleware(Logging)   // outermost — sees the request first, response last
    .middleware(Auth)      // inner
    .commands(commands![..]);

Per-command middleware

Global middleware wraps every command. For middleware that only some commands need — an auth check, an audit log — register it under a name and let the command ask for it (Laravel's middleware aliases):

App::new()
    .middleware(Timing)                                 // global: every command
    .middleware_alias("auth", RequireSession)
    .middleware_alias("audit", AuditLog::new())
    .middleware_group("admin", ["auth", "audit"])       // several at once
    .commands(commands![delete_workspace, list_workspaces]);

#[command(middleware = ["admin"])]
async fn delete_workspace(ctx: Ctx, id: i64) -> Result<()> { /* … */ }

#[command]                                              // global middleware only
async fn list_workspaces(ctx: Ctx) -> Result<Vec<Workspace>> { /* … */ }
  • Order: global middleware runs outermost; then the command's own, in the order it lists them, with groups expanded in place. delete_workspace above runs Timing → RequireSession → AuditLog → the command.
  • Groups may contain aliases or other groups. A middleware reached twice (a group plus its member listed again) runs once, at its first position. A group cycle is an error.
  • A single name works too: #[command(middleware = "auth")].
  • Unknown names stop the app at startup — invalid middleware wiring: command delete_workspaceuses middlewareauht, which is not registered. A misspelt name must never mean the command runs without its auth check.

Per-command middleware and abilities solve different problems: an ability decides whether the webview may call a command at all, while middleware wraps every call, from the frontend or from Rust (TestApp, another command through the registry).

Related