Skip to main content
Agent-scoped policies let you apply different compliance and enforcement settings to individual agents (identified by nodeId) within the same workspace. When no agent-scoped policy exists, the middleware treats the agent as having no enforcement (allow-all). Having the SDK present is not sufficient without an active policy.

How It Works

The middleware resolves policies using a scope hierarchy:
  1. Base policy: workspace-level policy (no nodeId), applies to all agents by default.
  2. Agent policy: node-specific policy keyed by nodeId, overrides the base policy for that agent.
When the middleware receives a tool call, it resolves the effective nodeId from the runtime agent state header (x-agent-state) and fetches the corresponding policy based on the configured scope.

Configuration

Remote Policy Source (Dynamic)

For production environments, configure the middleware to fetch policies dynamically per nodeId:
sec0SecurityMiddleware({
  policy: {
    source: "control-plane",
    scope: "auto",           // "base" | "agent" | "auto"
    nodeId: "ehr-agent",     // default nodeId when runtime header is missing
    fallbackToBase: true,    // fall back to base policy if agent policy is empty
    refreshTtlMs: 0,         // 0 = fetch every call; >0 = cache for N ms
  },
  controlPlaneUrl: process.env.SEC0_URL,
  auth: { apiKey: process.env.SEC0_API_KEY },
  // ... other middleware options
});
These examples focus on policy-source fields. Keep your base middleware options (signer, otel, sec0, and other required fields) from Quickstart.

Local Policy (Static)

For local development or simple setups, pass the policy YAML directly. Note that static policies apply uniformly. Agent scoping requires a remote source that resolves per-nodeId.
import { parsePolicyYaml } from "sec0-sdk/policy";

const policyYaml = `tenant: my-app\n...`;

sec0SecurityMiddleware({
  policy: parsePolicyYaml(policyYaml),
  // ... other middleware options
});

Policy Source Options

KeyTypeRequiredDescription
source"control-plane"YesIndicates remote policy fetch
scope"base", "agent", or "auto"NoWhich scope to fetch (default "auto")
nodeIdstringNoDefault nodeId when runtime nodeId is missing
fallbackToBasebooleanNoFall back to base policy when agent policy is empty (default true)
refreshTtlMsnumberNoCache TTL in ms; 0 fetches on every tool invocation (default 0)
level"gateway" or "middleware"NoPolicy tier to fetch (default "middleware")

Scope Behavior

ScopeRuntime nodeId presentRuntime nodeId absent
"base"Ignores nodeId; always fetches base policyFetches base policy
"agent"Fetches agent policy for runtime nodeIdUses configured nodeId; throws if both missing
"auto"Fetches agent policy for runtime nodeIdFalls back to configured nodeId, then base policy

How nodeId Is Resolved

The middleware determines the effective nodeId at runtime using this priority:
  1. Runtime header: extracted from the x-agent-state header on the incoming tool call (set by upstream agent decorators)
  2. Configured default: the nodeId field in the policy source config
  3. None: falls back to base policy (or throws in "agent" scope)
// The agent decorator sets nodeId automatically via agent state propagation
class EhrAgent {
  @sec0.agent()
  async run(ctx: any, input: any, manager: AgentManager) {
    // manager.agent carries the nodeId from sec0.config.yaml hop entry
    const headers = manager.getAgentStateHeaders();
    // headers include x-agent-state with encoded nodeId
  }
}

Example: Different Policies Per Agent

A common pattern is giving different agents different compliance rules. For example, an EHR agent may need HIPAA compliance packs while an order agent only needs basic security rules.
# sec0.config.yaml - each agent gets its own nodeId
app:
  hops:
    EhrAgent.run:
      type: agent
      nodeId: ehr-agent        # fetches policy scoped to "ehr-agent"
      agentName: ehr-agent
      agentVersion: "1.0.0"
    OrderAgent.run:
      type: agent
      nodeId: order-agent      # fetches policy scoped to "order-agent"
      agentName: order-agent
      agentVersion: "1.0.0"
// Middleware configured with auto scope - each call fetches
// the policy matching the calling agent's nodeId
sec0SecurityMiddleware({
  policy: {
    source: "control-plane",
    scope: "auto",
    fallbackToBase: true,
  },
  controlPlaneUrl: process.env.SEC0_URL,
  auth: { apiKey: process.env.SEC0_API_KEY },
  // ...
});
With this setup:
  • Tool calls from EhrAgent are enforced with the ehr-agent policy (e.g., HIPAA compliance packs)
  • Tool calls from OrderAgent are enforced with the order-agent policy (e.g., basic security rules)
  • If either agent has no scoped policy, it falls back to the base workspace policy
Having sec0-sdk/middleware in the runtime is not sufficient for enforcement. If the scoped policy for a nodeId is empty and fallbackToBase falls back to an empty base policy, enforcement is effectively off for that agent.