{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://occasio.ai/schemas/occasio-policy.schema.json",
  "title": "Occasio Policy",
  "description": "Schema for ~/.occasio/policy.yml — the single document that governs how Occasio handles tool calls and tool results. Authoritative reference: src/policy/loader.js (parser) and src/policy/validate.js (linter). Stable across v0.6.x.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "version": {
      "type": "integer",
      "description": "Policy schema version. 1 = base; 2 = adds the identity gate (deny_commands, identity_approval). Both are accepted.",
      "enum": [1, 2]
    },
    "block_secrets_in_tool_results": {
      "type": "boolean",
      "description": "Block the request when a tool result contains a recognised secret pattern. The model never sees the secret; a synthetic refusal is returned instead."
    },
    "redact_secrets_in_tool_results": {
      "type": "boolean",
      "description": "Replace recognised secrets with [REDACTED] in-place rather than blocking outright. Mutually intended with block_secrets_in_tool_results=false."
    },
    "distill_tool_results": {
      "type": "boolean",
      "description": "Distil long tool outputs (file reads, grep results) before sending to the model. Reduces token cost; the full raw output is still saved locally."
    },
    "block_requests_over_budget": {
      "type": "boolean",
      "description": "Block outbound requests once the session spend reaches the --budget limit. Returns HTTP 402 without invoking the model."
    },
    "entropy_secret_detection": {
      "type": "boolean",
      "description": "Opt-in (default false). When true, tool results are also scanned with the richer prefix/JWT/.env/Shannon-entropy detectors (src/scanner/detectors.js) in addition to the built-in pattern scanner. Off by default so existing behaviour is unchanged."
    },
    "tools": {
      "type": "object",
      "description": "Per-tool routing. When present, REPLACES the built-in defaults entirely — every tool not listed here will PASS to the cloud. Use sparingly; the defaults are usually what you want.",
      "additionalProperties": {
        "$ref": "#/$defs/toolEntry"
      }
    },
    "deny_paths": {
      "type": "array",
      "description": "Filesystem prefixes denied for read_file, find_files, and grep. Comparisons are on the symlink-resolved absolute path; case-insensitive on Windows. ~ is expanded to the user's home directory.",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true
    },
    "allow_paths": {
      "type": "array",
      "description": "Filesystem prefixes allowed for read_file, find_files, and grep. When non-empty, access is restricted to these prefixes; anything outside is blocked. Empty (the default) means no allowlist.",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true
    },
    "deny_patterns": {
      "type": "object",
      "description": "Custom regex patterns that, when matched in a tool result, are treated like secret-scanner hits — blocked or redacted depending on the global flags. Each entry is label: \"regex-string\".",
      "additionalProperties": {
        "type": "string",
        "minLength": 1,
        "description": "JavaScript-flavored regular expression source (without leading/trailing slashes)."
      }
    },
    "deny_commands": {
      "type": "object",
      "description": "Identity gate (v2) — hard-deny shell commands by regex. Each entry is label: { command_regex: \"regex-string\" }; the label is the deny reason. A match on a Bash/PowerShell command returns a synthetic BLOCK so the command never executes and the agent never sees its output. Empty (the default) means no command is denied.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": ["command_regex"],
        "properties": {
          "command_regex": {
            "type": "string",
            "minLength": 1,
            "description": "JavaScript-flavored regular expression source (without leading/trailing slashes), tested against the raw shell command string."
          }
        }
      }
    },
    "identity_approval": {
      "type": "object",
      "description": "Identity gate (v2) — shell commands that borrow an identity (ssh into a server, drive a cloud control plane, escalate to root) and must be gated behind human approval. Each entry is label: { command_regex, actor_type?, target_class?, reason? }. A match returns a fail-closed BLOCK that an AI agent cannot satisfy on its own: it may request the identity, not assume it.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": ["command_regex"],
        "properties": {
          "command_regex": {
            "type": "string",
            "minLength": 1,
            "description": "JavaScript-flavored regular expression source (without leading/trailing slashes), tested against the raw shell command string."
          },
          "actor_type": {
            "type": "string",
            "minLength": 1,
            "description": "Which actor this rule gates. Default: ai_agent."
          },
          "target_class": {
            "type": "string",
            "enum": ["local_dev", "staging", "production", "secrets", "cloud", "unknown"],
            "description": "The blast-radius class of the identity being borrowed."
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "description": "Stable reason code surfaced in the refusal and the audit row."
          }
        }
      }
    },
    "limits": {
      "type": "object",
      "additionalProperties": false,
      "description": "Per-round volume caps — a runaway-agent guard. Each key is a positive integer; absent (or omitted) means that limit is not enforced. When a round exceeds a limit, the run is halted with a synthetic refusal and a `limit_exceeded` audit row is recorded.",
      "properties": {
        "max_tool_calls_per_round": {
          "type": "integer",
          "minimum": 1,
          "description": "Maximum tool_use blocks the model may emit in a single assistant turn (round)."
        },
        "max_bash_calls_per_round": {
          "type": "integer",
          "minimum": 1,
          "description": "Maximum shell (bash/powershell) tool calls within a single round."
        },
        "max_bytes_to_model_per_round": {
          "type": "integer",
          "minimum": 1,
          "description": "Maximum bytes of locally-intercepted tool output that may re-enter the model in a single round (sum of kept bytes)."
        }
      }
    }
  },
  "$defs": {
    "toolEntry": {
      "type": "object",
      "additionalProperties": false,
      "required": ["action"],
      "properties": {
        "action": {
          "type": "string",
          "enum": ["PASS", "LOCAL", "TRANSFORM"],
          "description": "What the dispatcher does with this tool call. PASS forwards to the cloud; LOCAL executes on the user's machine; TRANSFORM executes locally then applies a shaping step."
        },
        "transform": {
          "type": "string",
          "description": "Required when action is TRANSFORM. Built-in values: redact-secrets, distill-output. Other names are forward-compatible but only execute if the dispatcher knows about them.",
          "minLength": 1
        },
        "executor": {
          "type": "string",
          "description": "Optional executor name. Default: native.",
          "minLength": 1
        },
        "classifier": {
          "type": "string",
          "description": "Optional input-validation classifier. Built-in values: read-input-validator, glob-input-validator, grep-input-validator, todo-write-validator, todo-read-validator, bash-allowlist, powershell-allowlist.",
          "minLength": 1
        },
        "reason": {
          "type": "string",
          "description": "Optional reason string surfaced in audit rows when this entry's action fires.",
          "minLength": 1
        }
      },
      "allOf": [
        {
          "if": { "properties": { "action": { "const": "TRANSFORM" } }, "required": ["action"] },
          "then": { "required": ["transform"] }
        }
      ]
    }
  }
}
