Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

You are reading the development version (master). For the latest release (v3.5.0) see the stable docs.

Capability Policy Schema

The policy parameter on the MCP boruna_run tool (and the --policy <file> flag on the boruna CLI) accepts either:

  • A string shorthand: "allow-all" or "deny-all"
  • A Policy object matching the schema below

This page documents the object form. The machine-readable schema lives at policy.schema.json.

Object form

{
  // Policy schema version. Currently always 1. Optional.
  "schema_version": 1,

  // Default behavior for capabilities NOT listed in `rules`.
  // false = deny by default (allowlist mode); true = allow by default (denylist mode).
  // Required for predictable behavior — do not omit.
  "default_allow": false,

  // Per-capability rules. Keys are capability names (see table below).
  "rules": {
    "net.fetch": {
      "allow":  true,   // boolean, required
      "budget": 0       // u64, required. 0 = unlimited; otherwise hard ceiling on call count.
    }
  },

  // Optional network-specific controls. Applied when `net.fetch` is allowed.
  "net_policy": {
    "allowed_domains":      ["api.openai.com", "*.our-api.example"], // empty = all
    "allowed_methods":      ["GET", "POST"],                          // empty = all
    "max_response_bytes":   10485760,                                 // default 10 MB
    "timeout_ms":           30000,                                    // default 30 s
    "allow_redirects":      true                                      // default true
  }
}

Capability names

These are the strings you use as keys in rules. They mirror boruna_bytecode::Capability::name().

CapabilityKeyNotes
Network fetchnet.fetchHTTP GET/POST/etc. — also gated by net_policy
Filesystem readfs.read
Filesystem writefs.write
Database querydb.query
UI renderui.renderFramework view() output
Current timetime.nowNon-deterministic; deny in pure pipelines
Random numberrandomNon-deterministic; deny in pure pipelines
LLM callllm.callExternal model invocation — apply budget to cap cost
Spawn actoractor.spawn
Send to actoractor.send

The strict validator rejects aliases. Sprint 0.4-S15 locked the rule-key surface to canonical names only. A policy file with "net" as a rule key fails validation with error_kind: "policy.invalid_capability" and a hint to use "net.fetch". Aliases were silently no-ops at gateway-check time before — fixing that footgun was the point of 0.4-S15 (project convention #1: reject at parse, don’t silently override).

Examples

1. Allowlist domain only — deny everything except net.fetch to api.openai.com

{
  "default_allow": false,
  "rules": { "net.fetch": { "allow": true, "budget": 0 } },
  "net_policy": { "allowed_domains": ["api.openai.com"] }
}

2. Allow-all minus filesystem writes — useful for read-only workflows

{
  "default_allow": true,
  "rules": { "fs.write": { "allow": false, "budget": 0 } }
}

3. LLM call quota — cap LLM invocations at 5 per run

{
  "default_allow": true,
  "rules": { "llm.call": { "allow": true, "budget": 5 } }
}

When the budget is exceeded the run aborts with a runtime_error whose message references CapabilityBudgetExceeded(LlmCall).

Surprising behavior to know

  • default_allow defaults to false. A Policy {} (empty object) denies everything. Always set default_allow explicitly.
  • budget: 0 means unlimited, not “zero allowed.” Use { "allow": false, "budget": 0 } to deny.
  • String shorthand and object form are not mixable. Pass exactly one shape.
  • Unknown JSON shapes are rejected. Old MCP clients that accidentally posted typo’d strings (e.g. "alow-all") used to be silently treated as allow-all. They now return success: false, error_kind: "invalid_policy". This is intentional — silent fall-through to allow-all was the bug FleetQ reported.
  • Unknown fields are rejected (sprint 0.4-S15). A typo like "default_alow": true no longer parses as default_allow: false (silent default); it fails with error_kind: "policy.unknown_field".
  • Unsupported schema_version values are rejected. This binary supports schema_version: 1. Setting 2 or any other value fails with error_kind: "policy.unknown_schema_version".

CLI tooling (sprint 0.4-S15)

# Strict-validate a policy file. Designed as a CI gate.
boruna policy validate policies/prod.json
# → exit 0 + "OK: ..." on success
# → exit 2 + stderr "error: policy.<kind>: ..." on validation error
# → exit 1 + stderr "error: policy.io_error: ..." on file IO error

# Machine-parseable output:
boruna policy validate --json policies/prod.json
# → {"ok":true} or {"ok":false,"errors":[{"error_kind":"policy.unknown_field",...}]}

# Print the effective policy (denormalized).
boruna policy show policies/prod.json

The MCP server exposes the same validator as boruna_policy_validate.

Stable error_kind taxonomy

The strict validator emits these stable strings (project convention #2 — locked forever):

error_kindWhen
policy.io_errorFile missing or unreadable
policy.parse_errorJSON syntax error or value type mismatch
policy.unknown_schema_versionschema_version is set to an unsupported value
policy.unknown_fieldUnknown field at any level (top-level, net_policy, or inside a rule)
policy.invalid_capabilityA rule key is not a recognized canonical capability name
policy.invalid_net_policynet_policy value out of range or unknown method

The boruna_run MCP tool also emits the legacy error_kind: "invalid_policy" for non-object input (string typos, arrays, numbers). The new policy.* kinds apply to object-form payloads only — they are additive over invalid_policy, not a replacement.

Versioning

The schema carries schema_version: 1. Future breaking changes will bump this number; the MCP tool will continue to accept the old shape as long as schema_version matches a supported value. This field is what lets you cache (script_hash, policy_hash) results safely across binary upgrades. Sprint 0.4-S15 locked this contract: only 1 is currently accepted; new optional fields can be added at v1; shape changes require a version bump.

Hashing for caching

Because Policy is Serialize + Deserialize, you can hash a normalized policy for cache keys:

#![allow(unused)]
fn main() {
let bytes = serde_json::to_vec(&policy).unwrap();
let hash  = sha2::Sha256::digest(&bytes);
}

Pair hash(policy) with hash(source) to memoize deterministic runs. (The capability-set identity portion — making the hash stable across binary upgrades — is tracked separately in the project roadmap.)