Policy Engine
Fine-grained rules that control which MCP tools agents can call, under what conditions, and whether violations are enforced or silently observed in shadow mode.
How It Works
The policy engine runs inside the ShieldAgent proxy pipeline, evaluated after authentication and before the security scanner. Every tools/call request is matched against your tenant's rule set. Other MCP methods (tools/list, resources/read, etc.) pass through without policy checks.
Default: implicit deny. If no policy rule matches a given (agentId, toolName) request, the engine denies it. Tools without explicit allow rules are blocked by default — no configuration required to stay safe.
Policy Schema
Each policy binds a tool name to an action and optionally adds conditions that must all be satisfied for the rule to fire.
| Field | Type | Description |
|---|---|---|
| toolName | string | The MCP tool name this rule applies to (e.g. bash, read_file, query_database) |
| action | allow | deny | shadow | What to do when the rule matches |
| agentId | UUID | null | Scope to a specific agent. null = applies to all agents in the tenant |
| conditions | array | null | Array of condition objects (AND logic). If omitted, the rule matches unconditionally |
Actions
| Action | Behavior |
|---|---|
| allow | Permit the tool call. Conditions are still checked. |
| deny | Block the tool call and return an error to the agent. |
| shadow | Used by the template system. Excluded from the live rule set — shadow mode enforcement is controlled separately via the cascade (see below). |
Scoping & Precedence
Agent-specific rules (non-null agentId) take priority over tenant-wide rules (agentId: null). Within the same specificity level, deny rules run before allow rules and conditional rules run before unconditional ones.
Condition Types
Conditions are optional predicates on a rule. When multiple conditions are present, all must match (AND logic). If any condition fails, the rule is skipped and evaluation continues to the next rule.
param_contains — Substring Match
Checks whether a request parameter contains a given substring (case-sensitive). Use dot-notation to address nested fields like arguments.command.
// Deny any bash call where the command contains "rm -rf"
{
"toolName": "bash",
"action": "deny",
"conditions": [
{
"type": "param_contains",
"param": "arguments.command",
"value": "rm -rf"
}
]
}param_matches — Regex Match
Checks whether a request parameter matches an ECMAScript regular expression. Useful for path allowlists or structured value patterns.
// Allow read_file only for paths under /app/data/
{
"toolName": "read_file",
"action": "allow",
"conditions": [
{
"type": "param_matches",
"param": "arguments.path",
"pattern": "^\/app\/data\/"
}
]
}time_window — UTC Hour Restriction
Restricts tool access to specific UTC hours using a half-open range [start, end). Combine with other conditions to enforce business-hours-only access.
// Allow query_database only during business hours (09:00–17:00 UTC)
{
"toolName": "query_database",
"action": "allow",
"conditions": [
{
"type": "time_window",
"allowedHours": [9, 17]
}
]
}rate_limit — Invocation Cap
Declares a maximum invocation rate for the tool. The policy evaluator records the threshold; enforcement is handled by the proxy's dedicated rate-limit pipeline stage.
// Cap send_email to 10 calls per minute
{
"toolName": "send_email",
"action": "allow",
"conditions": [
{
"type": "rate_limit",
"maxPerMinute": 10
}
]
}Shadow Mode
Shadow mode lets you roll out new policies safely. When enabled, the engine evaluates each request as normal but does not block it even if a deny rule matches. The decision is logged to the audit trail with outcome: 'shadow' and a shadowDeny: true flag so you can review impact before enforcing.
Three-Level Cascade
Shadow mode is resolved from most specific to least specific — the first non-null value wins:
| Level | How to configure |
|---|---|
| Global | Disable shadow mode in Settings → Security to enforce everywhere |
| Per-agent | PATCH /tenants/:tenantId/agents/:agentId with {"shadowMode": false} |
| Per-binding | PATCH /tenants/:tenantId/agents/:agentId/mcp-bindings/:mcpServerId with {"shadowMode": false} |
shadow mode = off once all agents are confirmed clean.Policy Templates
Templates are pre-built rule sets that you can apply to a tenant in one API call. Applying a template creates one policy record per rule, scoped tenant-wide. ShieldAgent ships system templates for common security, compliance, and development scenarios; you can also create custom templates for organization-specific rule sets.
| Type | Editable | Description |
|---|---|---|
| System | Read-only | Shipped with ShieldAgent. Security, compliance, and development presets. |
| Custom | Full CRUD | Created by your team. Organization-specific rule sets for your toolchain. |
Creating and Applying a Custom Template
# Create a custom template
curl -s -X POST https://api.shieldagent.io/tenants/:tenantId/policy-templates \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "Restrict destructive tools",
"description": "Deny dangerous shell commands for production agents",
"category": "security",
"rules": [
{
"toolName": "bash",
"action": "deny",
"conditions": [{"type": "param_contains", "param": "arguments.command", "value": "rm -rf"}]
},
{
"toolName": "bash",
"action": "deny",
"conditions": [{"type": "param_contains", "param": "arguments.command", "value": "DROP TABLE"}]
}
]
}'
# Apply a template (creates one policy per rule)
curl -s -X POST https://api.shieldagent.io/tenants/:tenantId/policy-templates/:templateId/apply \
-H 'Authorization: Bearer <token>'Hot-Reload Architecture
Policy changes take effect without restarting the proxy. The proxy maintains an in-memory compiled rule set per tenant and refreshes it automatically.
Cold-start hydration: On startup, the loader reads from Redis (when configured) for instant policy availability — no wait for the first poll.
Periodic poll: Every configured reload interval (default 30 s), the loader fetches GET /tenants/:tenantId/policies/compiled for each active tenant.
Change detection: Rules are hashed before recompilation is triggered. No-op polls have zero overhead.
Push invalidation: For critical changes (e.g. a new deny rule), call POST /tenants/:tenantId/policies/invalidate to force an immediate re-fetch without waiting for the poll.
| Setting | Default | Description |
|---|---|---|
| Hot-reload | true | Set to false to disable hot-reload entirely |
| Reload interval | 30000 | Polling interval in milliseconds |
| API URL | — | Management API base URL. Required to enable hot-reload |
API Reference
All policy endpoints require a bearer token. See Authentication for details.
Policies
policy:write—Create a policy rulepolicy:read—List all policies. Filter by ?agentId= to scope to one agentpolicy:read—Get a single policypolicy:write—Update a policy (partial — include only changed fields)policy:delete—Delete a policypolicy:read—Compiled rule set used by the proxy. Excludes shadow-action rulespolicy:write—Force immediate hot-reload in the proxyPolicy Templates
policy:read—List system templates and your tenant's custom templatespolicy:read—Get a templatepolicy:write—Create a custom templatepolicy:write—Update a custom template (system templates are read-only)policy:write—Delete a custom templatepolicy:write—Apply a template — creates one policy per rule, all scoped tenant-wideExample — Create a policy
curl -s -X POST https://api.shieldagent.io/tenants/:tenantId/policies \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"toolName": "bash",
"action": "deny",
"agentId": null,
"conditions": [
{
"type": "param_contains",
"param": "arguments.command",
"value": "rm -rf"
}
]
}'Required permissions
| Permission | Grants |
|---|---|
| policy:read | View policies, templates, and compiled rules |
| policy:write | Create / update policies and templates, invalidate cache, apply templates |
| policy:delete | Delete policies and custom templates |
Audit Trail
Every policy evaluation is appended to the immutable audit trail. Key fields on each event:
| Field | Value / meaning |
|---|---|
| outcome | allowed, denied, human_review_required, or shadow |
| matchedRuleId | UUID of the first matching rule; null for implicit deny |
| shadowDeny | true when shadow mode converted a deny to an allow |
| reason | Human-readable explanation of the decision |