Skip to main content

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

  1. Author a PolicyBundle JSON file (schema below).
  2. POST it to https://<gateway>/v1/policies with a tenant API token. The gateway verifies the bundle, signs it, and bumps the tenant's policy_revision.
  3. Sync to endpoints: every pavri-endpointd instance subscribed to the tenant's PolicySync stream receives the new snapshot, verifies the signature, and applies it to its local policycache. The next tools/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:

FieldRequiredMeaning
tenant_idyesThe tenant the bundle applies to.
bundle_idyesStable identifier; the gateway uses this for diff and for audit.
rulesyesOrdered list. Per evaluator, first matching rule wins.

Rule shape

{
"rule_id": "<stable-id>",
"selector": "<selector-prefix>",
"effect": "deny",
"rule_type": "<rule-type>",
"parameters": { "values": { ... } }
}
FieldMeaning
rule_idStable identifier. Surfaced on every CodingActionEvent and EgressEvent so dashboard filters can pivot by rule. Keep stable across edits — change content, not ID.
selectorRoutes 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.
effectReserved 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_typeThe rule kind. Determines which parameters keys are meaningful.
parametersKey-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.

ParameterTypeMeaning
modestring_value"allowlist" or "denylist".
patternsstring_list_valueGlob 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 prefixEvaluator
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.