Coding Agent Protection — Policy authoring (raw JSON)
CAP-1.5 will ship a higher-level DSL with friendly authoring affordances
(visual builder, lint rules, dry-run preview). Until then, policies are
authored as raw PolicyBundle JSON posted directly to the gateway's
/v1/policies endpoint.
This page documents the JSON shape, the supported rule types, and the authoring lifecycle. It is intended for platform engineers who need to ship a tenant-specific policy ahead of the DSL release.
Lifecycle
- Author a
PolicyBundleJSON file (schema below). - POST it to
https://<gateway>/v1/policieswith a tenant API token. The gateway verifies the bundle, signs it, and bumps the tenant'spolicy_revision. - Sync to endpoints: every
pavri-endpointdinstance subscribed to the tenant's PolicySync stream receives the new snapshot, verifies the signature, and applies it to its localpolicycache. The nexttools/call(or egress request) sees the new rules.
Hot-swap is sub-second end-to-end across the sync stream and sub-millisecond at the per-frame eval boundary; see MCP broker for the in-broker hot-swap behavior.
PolicyBundle shape
{
"tenant_id": "acme",
"bundle_id": "bundle-2026-04-29-1",
"rules": [
{
"rule_id": "deny-fs-write-outside-workspace",
"selector": "endpoint.action.mcp_tool_call:",
"effect": "deny",
"rule_type": "tool_acl",
"parameters": {
"values": {
"mode": {"string_value": "denylist"},
"patterns": {"string_list_value": {"values": [
"endpoint.action.mcp_tool_call:filesystem-server.write_*"
]}}
}
}
},
{
"rule_id": "allow-corporate-egress",
"selector": "egress:",
"effect": "allow",
"rule_type": "egress",
"parameters": {
"values": {
"mode": {"string_value": "allowlist"},
"patterns": {"string_list_value": {"values": [
"egress://*.corp.example.com",
"egress://api.openai.com"
]}}
}
}
}
]
}
Top-level fields:
| Field | Required | Meaning |
|---|---|---|
tenant_id | yes | The tenant the bundle applies to. |
bundle_id | yes | Stable identifier; the gateway uses this for diff and for audit. |
rules | yes | Ordered list. Per evaluator, first matching rule wins. |
Rule shape
{
"rule_id": "<stable-id>",
"selector": "<selector-prefix>",
"effect": "deny",
"rule_type": "<rule-type>",
"parameters": { "values": { ... } }
}
| Field | Meaning |
|---|---|
rule_id | Stable identifier. Surfaced on every CodingActionEvent and EgressEvent so dashboard filters can pivot by rule. Keep stable across edits — change content, not ID. |
selector | Routes the rule to the matching evaluator. The local policycache registers evaluators by selector prefix; the rule reaches the evaluator whose prefix is the longest match against selector. |
effect | Reserved field; the active value is computed by the evaluator from mode (allowlist vs denylist). The gateway requires this field present for forward compatibility with explicit-deny semantics in CAP-1.7. Use "deny". |
rule_type | The rule kind. Determines which parameters keys are meaningful. |
parameters | Key-value attributes. The shape is {"values": {"<key>": {"<kind>": <v>}}} where <kind> is one of string_value, string_list_value, bool_value, int_value. This is the proto wire shape; the higher-level DSL in CAP-1.5 will hide it. |
Supported rule types
tool_acl
Owned by selectors endpoint.action.mcp_tool_call: and tool:.
Enforces an allowlist or denylist of MCP tool-call patterns.
| Parameter | Type | Meaning |
|---|---|---|
mode | string_value | "allowlist" or "denylist". |
patterns | string_list_value | Glob patterns matched against the full action string (e.g., endpoint.action.mcp_tool_call:filesystem-server.read_*). Supports *, ?, [abc]. Case-sensitive on POSIX. |
Semantics: per evaluator, the first rule whose mode is recognized
produces the decision. A pattern miss in allowlist mode → deny; a
pattern miss in denylist mode → allow.
egress
Owned by selector egress:. Enforces an allowlist or denylist of egress
destinations (host + optional port + optional path) for the proxy-svc.
Same mode / patterns shape as tool_acl. Patterns match against the
full egress URL with the egress:// prefix preserved (e.g.,
egress://api.example.com:443/*).
Selectors and routing
The local policycache dispatches an action to one evaluator: the
first registered evaluator whose Selectors() slice contains a prefix
that matches the action.
| Action prefix | Evaluator |
|---|---|
endpoint.action.mcp_tool_call: | ToolAclEvaluator |
tool: | ToolAclEvaluator (legacy SDK selector) |
egress: | EgressEvaluator |
If no evaluator owns the action's prefix, the cache returns
ActionNoMatch. Most enforcement points treat no_match as allow
(observe-mode fallback) until an explicit gate is configured.
Posting a bundle
Replace <gateway> and <tenant-token>:
curl -fsSL -X POST "https://<gateway>/v1/policies" \
-H "Authorization: Bearer <tenant-token>" \
-H "Content-Type: application/json" \
--data-binary @bundle.json
The gateway responds with the assigned policy_revision. Watch the
sensor logs to confirm the snapshot is applied:
policycache: applied snapshot snap-<hash> for org-<id> (revision=<n>)
Cross-language semantics
The semantics of tool_acl are locked across the Go (broker / proxy-svc)
and Python (legacy SDK) implementations by a shared conformance suite at
tests/conformance/policy/. Both runners must produce identical
decisions for every fixture. If you add a fixture that depends on a
new behavior, see the suite's README for the runner contract.
Future: CAP-1.5 DSL
The DSL ships as PolicyBundle.dsl (string) alongside the raw rules
and is compiled by the gateway into the same proto rule list. Tenants
authoring policies through the dashboard never see the raw shape; the
raw form remains for programmatic clients (CI checks, infra-as-code,
policy-as-code) and for break-glass authoring during incident response.