> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sec0.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Middleware

> Wrap tool servers with the current Sec0 middleware presets, adapters, and runtime enforcement path.

`sec0-sdk/middleware` is the in-process enforcement layer for tool servers. It wraps any `McpServerLike` server, evaluates policy on every tool invocation, emits signed audit envelopes, and can delegate final allow or deny decisions to either the local runtime adapter or a remote runtime service.

Use it when you want to secure a tool server inside the same process as your application. For outbound messages, raw API calls, or other application-level checks that are not naturally wrapped as server calls, use [Guard API](/docs/guard).

## Choose an Entrypoint

The public middleware surface has three integration styles:

| API                               | Use it when                                                                                                             |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `sec0SecurityMiddleware(options)` | You want full control over adapters and runtime wiring                                                                  |
| `sec0LocalMiddleware(options)`    | You want a zero-control-plane integration for local policy, local runtime decisions, and local audit                    |
| `sec0HostedMiddleware(options)`   | You want control-plane policy fetch, approval verification, escalation creation, and optional remote runtime evaluation |

The preset builders are also exported when you want to inspect or extend the generated options before wrapping the server:

* `createLocalSec0Preset(...)`
* `createHostedSec0Preset(...)`

## Local Preset

Use the local preset first unless you already need hosted policy or approvals:

```typescript theme={null}
import fs from "node:fs";
import { parsePolicyYaml } from "sec0-sdk/policy";
import { LocalDevSigner } from "sec0-sdk/signer";
import { sec0LocalMiddleware } from "sec0-sdk/middleware";

const server = createYourMcpServer();
const policy = parsePolicyYaml(fs.readFileSync("./policy.yaml", "utf8"));

sec0LocalMiddleware({
  policy,
  signer: LocalDevSigner.fromKeyRef(policy.signing.key_ref),
  otel: {
    endpoint: policy.observability.otlp_endpoint,
    serviceName: "orders-mcp",
    serviceVersion: "1.0.0",
    environment: "dev",
    tenant: policy.tenant,
  },
  sec0: { dir: ".sec0", retentionDays: 30 },
  telemetry: { enabled: false },
})(server);
```

This preset wires:

* a static policy provider
* a local runtime adapter
* a local audit sink
* no-op approval verification and escalation reporting

## Hosted Preset

Switch to the hosted preset when policy should come from the control plane or when denies need approval verification and escalation creation:

```typescript theme={null}
import { sec0HostedMiddleware } from "sec0-sdk/middleware";
import { LocalDevSigner } from "sec0-sdk/signer";

sec0HostedMiddleware({
  policy: {
    source: "control-plane",
    level: "middleware",
    scope: "auto",
    refreshTtlMs: 60_000,
  },
  auth: { apiKey: process.env.SEC0_API_KEY! },
  signer: LocalDevSigner.fromKeyRef("file://./.sec0/keys/ed25519.key"),
  otel: {
    endpoint: "http://127.0.0.1:4318",
    serviceName: "orders-mcp",
    serviceVersion: "1.0.0",
    environment: "prod",
    tenant: "my-app",
  },
  sec0: { dir: ".sec0", retentionDays: 30 },
  runtime: {
    enforcement: {
      mode: "remote",
      failureMode: "local",
      remote: {
        endpoint: process.env.SEC0_RUNTIME_URL!,
        apiKey: process.env.SEC0_RUNTIME_API_KEY,
        timeoutMs: 3000,
      },
    },
  },
})(server);
```

Hosted mode keeps the same wrapping behavior, but the preset changes the adapter wiring:

* policy can be fetched from the control plane
* approval verification can be delegated to the control plane
* escalation creation can be delegated to the control plane
* runtime decisions can stay local or be delegated to a remote runtime endpoint

`auth` is the credential used for policy and approval APIs. The top-level `apiKey` option is separate and only helps auto-wire audit uploads when you are not supplying `sec0.presign` yourself.

## Full-Control Wiring with `sec0SecurityMiddleware`

Use the base entrypoint when you need custom adapter implementations:

```typescript theme={null}
import { sec0SecurityMiddleware, type MiddlewareOptions } from "sec0-sdk/middleware";
import type {
  PolicyProvider,
  ApprovalVerifier,
  EscalationReporter,
  AuditSink,
  RuntimeInvoker,
} from "sec0-sdk/core";

const policyProvider: PolicyProvider = {
  async getPolicy() {
    return { policy, hash: "static-policy-hash" };
  },
};

const approvalVerifier: ApprovalVerifier = { async verify() { return null; } };
const escalationReporter: EscalationReporter = {
  async create(input) {
    return { id: `esc-${input.violation}`, status: "pending" };
  },
};
const auditSink: AuditSink = { async append() { /* write to your sink */ } };
const runtimeInvoker: RuntimeInvoker = {
  async evaluate() {
    return {
      protocolVersion: "2026-02-01",
      adapterMode: "local",
      evaluationSource: "local",
      decision: "allow",
      reasons: [],
      obligations: [],
      auditRefs: [],
    };
  },
};

const options: MiddlewareOptions = {
  policy,
  signer,
  otel,
  sec0: { dir: ".sec0" },
  adapters: {
    policyProvider,
    approvalVerifier,
    escalationReporter,
    auditSink,
    runtimeInvoker,
  },
};

sec0SecurityMiddleware(options)(server);
```

The contracts above are exported from `sec0-sdk/core`.

## What the Middleware Enforces

When the wrapper is installed, the middleware:

1. Freezes the tool registry and blocks handler swaps after wrapping.
2. Hashes server and handler source to detect version or code drift.
3. Evaluates policy on every `callTool`.
4. Applies optional Agent Guard, compliance, skill scan, SAST, and DAST hooks.
5. Emits a signed audit envelope for allow and deny outcomes.
6. Adds escalation metadata when the deny reason is configured for escalation.

Common deny reasons include:

* `tool_not_in_allowlist`
* `version_unpinned`
* `missing_idempotency_for_side_effect`
* `agent_guard_failed`
* `server_code_changed`
* `tool_code_changed`
* `skill_scan_failed`
* `sast_failed`
* `dast_failed`

See [Policy Schema Reference](/docs/reference/policy-schema) for the full deny and escalate reason set.

## Policy Sources

`policy` accepts three shapes:

* a parsed `PolicyObject`
* a YAML string
* a control-plane source descriptor

```typescript theme={null}
sec0SecurityMiddleware({
  policy: {
    source: "control-plane",
    level: "middleware",
    scope: "agent",
    nodeId: "payments-agent",
    fallbackToBase: true,
    refreshTtlMs: 60_000,
  },
  auth: { apiKey: process.env.SEC0_API_KEY },
  signer,
  otel,
  sec0: { dir: ".sec0" },
})(server);
```

Use `scope: "base"` when the same middleware policy applies to every node. Use `scope: "agent"` or `scope: "auto"` when the executing `nodeId` should influence policy lookup.

## Remote Runtime Decisions

By default, middleware decisions are evaluated locally. To move final allow or deny resolution behind a remote service:

```typescript theme={null}
sec0SecurityMiddleware({
  policy,
  signer,
  otel,
  sec0: { dir: ".sec0" },
  runtime: {
    enforcement: {
      mode: "remote",
      protocolVersion: "2026-02-01",
      failureMode: "local",
      remote: {
        endpoint: process.env.SEC0_RUNTIME_URL!,
        apiKey: process.env.SEC0_RUNTIME_API_KEY,
        timeoutMs: 3000,
        maxRetries: 2,
      },
    },
  },
})(server);
```

`failureMode: "local"` keeps a local fallback when the remote runtime is unavailable. `allow` and `deny` are available when you want fail-open or fail-closed behavior instead.

## Crossing a Network Boundary

When tool execution crosses a network boundary, use the bridge function:

```typescript theme={null}
import { callToolViaGateway } from "sec0-sdk/middleware";

const result = await callToolViaGateway({
  gatewayBaseUrl: "https://your-gateway:8088",
  server: "vision-mcp",
  toolAtVersion: "fetch@1.0",
  args: { url: "https://api.example.com/data" },
  authHeader: `Bearer ${userAccessToken ?? process.env.SVC_TOKEN}`,
  cause: { traceId, spanId },
  agentState: manager.agent.snapshot(),
});
```

If you already have agent metadata but not a full `agentState` payload, use `callToolViaGatewayWithAgent(...)` instead.

## Reference

* [Middleware Options Reference](/docs/reference/middleware-options)
* [Policy Schema Reference](/docs/reference/policy-schema)
* [Bridge Options Reference](/docs/reference/bridge-options)
