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";| Helper | Action | Runner behavior |
|---|---|---|
allow() | allow | Continue |
block(reason?, options?) | block | Emit guardrail trace event; return error result to model |
rewrite(payload, options?) | rewrite | Replace call args or tool result payload |
requireApproval(pending?, options?) | require_approval | Pause 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:
lm_middleware.beforeLmCalllm_guardrails.checkInput- Model inference (stream or batch)
lm_guardrails.checkOutputlm_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 });