Maniac Docs
Middleware

Middleware & guardrails

LMMiddleware and ToolMiddleware hooks plus guardrail decisions — allow, block, rewrite, and requireApproval.

Middleware & guardrails

Middleware transforms requests and results in place. Guardrails inspect payloads and return a GuardrailDecision that can allow, block, rewrite, or pause for human approval.

Import base classes from @maniac-ai/agents/middleware and decision helpers from @maniac-ai/agents/schemas.

Middleware interfaces

interface LMMiddleware {
  beforeLmCall?(ctx: LMCallContext, req: InferenceRequest): Promise<InferenceRequest>;
  afterLmCall?(ctx, req, resp): Promise<InferenceResponse>;
}

interface ToolMiddleware {
  beforeInvoke?(ctx: ToolCallContext, call: RuntimeToolCall): Promise<RuntimeToolCall>;
  afterInvoke?(ctx, call, result): Promise<ToolResult>;
}

Base classes

BaseLMMiddleware and BaseToolMiddleware provide no-op defaults — subclass and override only the hooks you need.

import { BaseToolMiddleware } from "@maniac-ai/agents/middleware";

class RedactSecrets extends BaseToolMiddleware {
  async afterInvoke(_ctx, call, result) {
    if (typeof result.content === "string") {
      return { ...result, content: result.content.replace(/sk-\w+/g, "[REDACTED]") };
    }
    return result;
  }
}

Middleware runs before guardrails on the inbound path and after guardrails on the outbound path.

Guardrail interfaces

interface LMGuardrail {
  checkInput?(ctx, req): Promise<GuardrailDecision>;
  checkOutput?(ctx, req, resp): Promise<GuardrailDecision>;
}

interface ToolGuardrail {
  checkCall?(ctx, call): Promise<GuardrailDecision>;
  checkResult?(ctx, call, result): Promise<GuardrailDecision>;
}

BaseLMGuardrail, BaseToolGuardrail, AllowingLMGuardrail, and AllowingToolGuardrail ship in @maniac-ai/agents/middleware.

Guardrail decisions

import { allow, block, rewrite, requireApproval } from "@maniac-ai/agents/schemas";

type GuardrailAction = "allow" | "rewrite" | "block" | "require_approval";
HelperActionRunner behavior
allow()allowContinue
block(reason?, options?)blockEmit guardrail trace event; return error result to model
rewrite(payload, options?)rewriteReplace call args or tool result payload
requireApproval(pending?, options?)require_approvalPause run — same path as policy HITL

Block example

import { BaseToolGuardrail, allow, block } from "@maniac-ai/agents";

class BlockShell extends BaseToolGuardrail {
  async checkCall(_ctx, call) {
    if (call.toolset === "shell") {
      return block("shell access is disabled in this environment");
    }
    return allow();
  }
}

Rewrite example

import { rewrite } from "@maniac-ai/agents/schemas";

async checkCall(_ctx, call) {
  if (call.tool === "send_email" && !call.args.confirmed) {
    return rewrite(
      { ...call.args, dry_run: true },
      { reason: "forcing dry run until user confirms" }
    );
  }
  return allow();
}

Require approval example

import { requireApproval } from "@maniac-ai/agents/schemas";

async checkCall(ctx, call) {
  if (call.tool === "transfer_funds") {
    return requireApproval(
      { tool: call.tool, args: call.args },
      { reason: "financial transfer requires human sign-off" }
    );
  }
  return allow();
}

LM guardrail path

For each LM iteration:

  1. lm_middleware.beforeLmCall
  2. lm_guardrails.checkInput
  3. Model inference (stream or batch)
  4. lm_guardrails.checkOutput
  5. lm_middleware.afterLmCall

Input block short-circuits before the model call. Output block replaces the assistant message with an error.

Trace events

Blocked guardrails emit guardrail trace events with the reason and matched guardrail id. Stream them via runAgentStream or export with OTelTracer.

Tools tagged requires_approval

Individual tools can declare requires_approval: true in their definition. The runner treats this like a guardrail require_approval — useful for ChatToolset channel writes without a custom guardrail class.

import { ChatToolset } from "@maniac-ai/agents/channels";

const chat = new ChatToolset({ requireApproval: true });

On this page