Skip to content

MCP Policy Files

This page documents the YAML schema consumed by assay mcp wrap.

Assay policy files decide:

  • which MCP tools are allowed or denied
  • how tool arguments are validated with JSON Schema
  • which tools need extra controls such as approval, scope restriction, or argument redaction

Supported Versions

  • version: "2.0": current format, with per-tool JSON Schema under schemas:
  • version: "1.0": legacy constraints: format

Assay still reads v1 policies, warns once, and can migrate them with assay policy migrate.

Minimal v2 Policy

version: "2.0"
name: "starter"

tools:
  allow: ["read_file", "list_dir"]
  deny: ["exec", "shell", "write_file"]

schemas:
  read_file:
    type: object
    additionalProperties: false
    properties:
      path:
        type: string
        pattern: "^/workspace/.*"
        minLength: 1
        maxLength: 4096
    required: ["path"]

  list_dir:
    type: object
    additionalProperties: false
    properties:
      path:
        type: string
        pattern: "^/workspace/.*"
        minLength: 1
        maxLength: 4096
    required: ["path"]

enforcement:
  unconstrained_tools: warn

Top-Level Fields

Field Type Meaning
version string Policy schema version. Use "2.0" for new files.
name string Optional human-readable label.
tools object Allow/deny lists and obligation controls.
allow / deny list Legacy aliases merged into tools.allow / tools.deny on load.
schemas map JSON Schema per tool. $defs is reserved for shared definitions.
constraints list Legacy v1 regex constraints. Deprecated.
enforcement object What to do with allowed tools that have no schema.
limits object Optional request and tool-call ceilings.
signatures object Optional tool-description integrity checks.
tool_pins map Cryptographic pins for expected tool identity.
discovery object Advanced runtime discovery settings.
runtime_monitor object Advanced runtime monitoring rules.
kill_switch object Advanced kill-switch triggers.

Unknown fields are ignored with a warning, so it is worth keeping this page and your checked-in policies aligned.

tools: Fields

The tools section handles both filtering and extra controls.

Field Type Meaning
allow list Allowed tool names or wildcard patterns.
deny list Blocked tool names or wildcard patterns.
allow_classes list Allow by tool taxonomy class.
deny_classes list Deny by tool taxonomy class.
approval_required list Tools that require a valid approval artifact.
approval_required_classes list Approval requirement by tool class.
restrict_scope list Tools whose arguments must match a scope contract.
restrict_scope_classes list Scope restriction by tool class.
restrict_scope_contract object Shared contract used for restrict_scope.
redact_args list Tools whose arguments should be redacted.
redact_args_classes list Redaction by tool class.
redact_args_contract object Shared contract used for redact_args.

Wildcards

Assay uses simple * wildcards, and the language has exactly five forms:

  • "*" matches all tools
  • "read_*" matches by prefix
  • "*_file" matches by suffix
  • "*search*" matches by substring
  • patterns without * are exact matches

In the four wildcard forms a * is unbounded: it crosses any character, including the . or __ a server may use to namespace its tools. "*" therefore admits tools that do not exist yet, including ones a future upstream adds.

A * that is neither the first nor the last character is a literal asterisk. There is no fifth wildcard form. read_*_file does not match read_config_file; it matches only a tool literally named read_*_file. The same goes for several interior stars, such as a*b*c, and for interior stars inside the substring form: *a*b* searches for the three characters a*b.

That one fails in the dangerous direction. In allow, an interior-star pattern matches nothing and the tool is refused, which is visible. In deny, it matches nothing and the tool is permitted -- by a line whose author believed it was blocking something. If a deny entry looks like a glob and is not one of the five forms above, it is not blocking what it names.

For the four forms that do expand, the risk is one-sided and a policy file does not show it. A wildcard in deny over-blocks, which fails visibly and safely. The same wildcard in allow over-permits, which fails silently. Azure RBAC measured this direction: about half of the 15,481 catalogued actions reach across Resource Providers under non-obvious wildcards, and the recommendation there was explicit enumeration rather than a more careful pattern language (arXiv 2506.10755v3). Prefer naming the tools you mean in allow, and keep "*" for the cases where admitting the unknown is the intent rather than the default.

Two corners, so they are stated rather than discovered. "**" (and any longer run of stars) is the substring form with an empty substring, so it matches everything, exactly as "*" does — it is not the mandate surface's crossing operator. An empty pattern "" has no star, so it is an exact match against the empty name and matches no real tool.

A different pattern language applies to mandate tool_patterns, where * stops at a . and ** crosses it, the way a filesystem glob treats /. The two are deliberately different and pinned against each other by a test; do not carry a pattern from one surface to the other without re-reading it.

JSON Schema in schemas:

Each tool can have a full JSON Schema for its argument object:

schemas:
  create_ticket:
    type: object
    additionalProperties: false
    properties:
      title:
        type: string
        minLength: 5
        maxLength: 120
      priority:
        type: string
        enum: ["low", "medium", "high"]
      labels:
        type: array
        items:
          type: string
          maxLength: 32
    required: ["title", "priority"]

Recommended defaults for security-sensitive tools:

  • additionalProperties: false
  • minLength: 1 on required strings
  • explicit required: [...]
  • bounded arrays and strings

If a call violates the schema, Assay denies it with E_ARG_SCHEMA.

Shared Definitions With $defs

Assay supports local shared definitions via $defs. Use #/$defs/... references inside tool schemas. File and HTTP references are not retrieved during evaluation. Bundle external definitions into the policy document before execution.

schemas:
  $defs:
    safe_path:
      type: string
      pattern: "^/workspace/.*"
      minLength: 1
      maxLength: 4096

  read_file:
    type: object
    additionalProperties: false
    properties:
      path:
        $ref: "#/$defs/safe_path"
    required: ["path"]

Enforcement

Allowed tools can still be considered unsafe if you do not attach a schema. enforcement.unconstrained_tools decides what happens then:

enforcement:
  unconstrained_tools: warn

Supported values:

  • warn: allow the tool, but emit E_TOOL_UNCONSTRAINED
  • deny: block allowed tools that have no schema
  • allow: silently allow unconstrained tools

Limits

limits:
  max_requests_total: 1000
  max_tool_calls_total: 500

Exceeding these limits produces E_RATE_LIMIT.

Approval, Scope Restriction, and Redaction

These controls sit next to ordinary allow/deny rules:

tools:
  allow: ["read_file", "deploy_release", "create_ticket"]
  approval_required: ["deploy_release"]
  restrict_scope: ["read_file"]
  redact_args: ["create_ticket"]

  restrict_scope_contract:
    scope_type: "path_prefix"
    scope_value: "/workspace"
    scope_match_mode: "prefix"

  redact_args_contract:
    redaction_target: "args"
    redaction_mode: "mask"
    redaction_scope: "sensitive_fields"

Use these when you want:

  • explicit human approval for risky tools
  • runtime enforcement that file or resource arguments stay in-bounds
  • redaction of secrets before downstream logging or evidence export

When an approval_required obligation is evaluated from a supplied _meta.approval artifact, the decision event also carries a digest/profile for that structured approval artifact. This binds the projected approval summary fields to the retained _meta.approval basis under assay.approval_artifact.structured_meta_jcs.v0. It is not a rendered UI view claim, not raw byte retention, and not proof of what a user saw or intended.

Tool Pins

tool_pins protect against tool-definition drift by pinning the expected server, tool name, and hashes:

tool_pins:
  read_file:
    server_id: "filesystem-prod"
    tool_name: "read_file"
    # Replace these example values with the full 64-character lowercase SHA-256 hashes.
    schema_hash: "9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f9f4d4d0f"
    meta_hash: "42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e42f5df3e"

schema_hash is the hex SHA-256 of the RFC 8785 (JCS) canonical bytes of the tool's inputSchema, or of null when the tool declares none. meta_hash is the hex SHA-256 of the description bytes verbatim, or of the empty string when there is none. Because schema_hash is taken over canonical bytes, a server that reorders its keys or re-emits 100 as 1e2 does not drift; a changed schema does, with one exception.

RFC 8785 renders every number as an IEEE 754 double, so two numeric literals with the same double value become one pin. Below 2^53 that is only a spelling difference and there is nothing to detect. Above it, two literals can be different integers and still share a double, and then the pin stops seeing a real edit: a schema changed only from {"maximum": 9007199254740993} to {"maximum": 9007199254740992} keeps the same schema_hash while admitting a different set of arguments, because a validator still reads the two literals as different integers. A tool_pins entry does not detect that edit.

The exception is a property of the number, not of the keyword, so do not read any keyword list as the boundary. It applies wherever such a pair of integers is compared against an argument for order or identity — maximum, minimum, exclusiveMaximum, exclusiveMinimum, const and enum are where it is measured to bite today, and that is an example set, not a limit. It is bounded on the other side: past the 64-bit integer range the two literals stop being distinguishable anywhere, so they are one value rather than a missed edit.

An argument constraint written in this policy is unaffected, because it is validated from the schema you wrote here and not from the one the server declares.

Legacy v1 Compatibility

Legacy v1 policies use constraints::

version: "1.0"
deny: ["exec"]
constraints:
  - tool: "read_file"
    params:
      path:
        matches: "^/workspace/.*"

Assay loads this shape, warns, normalizes allow / deny into tools.*, and auto-migrates constraints into in-memory JSON Schemas.

To write the v2 form to disk:

assay policy migrate --input policy.yaml

To fail CI if deprecated constructs are still present:

assay policy validate --deny-deprecations --input policy.yaml

See Also