{
  "$comment": "Histos decision vocabulary — Draft 0.1. Part of the cross-language contract: every engine must use these codes with these meanings and map them to the same public_reason. THREE NAMESPACES, because they answer different questions and a conformance suite must tell them apart: RUNTIME (a GateDecision.rule — this call was refused), POLICY (a PolicyError.code — this policy cannot be loaded at all), REVIEW (advisory findings from review_policy; not yet coded, deliberately). Adding a code is a format change; changing what one means is a breaking change.",
  "version": "0.1",
  "public_reasons": {
    "$comment": "What the AGENT is told. Deliberately non-coaching: no threshold, no allowlist entry, no tenant name, no field name that reveals the rule's shape. The rich detail (rule/field/expected/received/remedy) goes to the developer and the audit sink only.",
    "allow": "OK",
    "deny": "ACTION_NOT_AUTHORIZED",
    "require_confirmation": "CONFIRMATION_REQUIRED",
    "redact": "OUTPUT_REDACTED"
  },
  "escalation": {
    "$comment": "The seam to a semantic tier, and the one place this deterministic format touches a probabilistic one. Written down here because it is a cross-implementation obligation, not an engine detail.",
    "marker": "escalate",
    "marker_meaning": "A boolean on the decision, set on every one of the four codes below. It RECORDS that meaning was consulted, or should have been, so an audit trail can separate those calls. It is not an effect and must never be read as one: no implementation may have a code path in which a decision is allowed because this flag is set.",
    "collapse": "An engine with no tier wired MUST answer no_escalation_tier / deny for a tool the policy marks `escalate`. There is no configuration — no default tier, no permissive mode, no on_missing — by which the absence of a tier becomes an allow. An implementation that offers one is not compatible.",
    "codes": [
      "escalated",
      "escalation_denied",
      "escalation_error",
      "no_escalation_tier"
    ]
  },
  "codes": [
    {
      "code": "allow",
      "effect": "allow",
      "phase": "pre",
      "meaning": "Every check passed."
    },
    {
      "code": "confirmed",
      "effect": "allow",
      "phase": "pre",
      "meaning": "A required confirmation was satisfied by a trusted out-of-band approval."
    },
    {
      "code": "unknown_tool",
      "effect": "deny",
      "phase": "pre",
      "meaning": "No contract exists for this tool. Deny-by-default: an ungoverned tool is never callable."
    },
    {
      "code": "no_arg_schema",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The tool has a contract but no argument schema, so its arguments cannot be validated. Fail closed rather than wave the call through."
    },
    {
      "code": "no_principal",
      "effect": "deny",
      "phase": "pre",
      "meaning": "No trusted principal was bound. Identity is bound out-of-band by the host; the engine never infers one."
    },
    {
      "code": "rbac",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The principal's role has no grant for this tool, directly or through inheritance."
    },
    {
      "code": "arg_schema",
      "effect": "deny",
      "phase": "pre",
      "meaning": "An argument failed the tool's declared schema: type, required, enum, numeric range, string length, pattern, or array element.",
      "detail": [
        "field",
        "expected",
        "received"
      ]
    },
    {
      "code": "arg_binding_unresolved",
      "effect": "deny",
      "phase": "pre",
      "meaning": "A bound argument could not be filled because the principal lacks the trusted attribute. The engine never injects a missing value.",
      "detail": [
        "field"
      ]
    },
    {
      "code": "resource_constraint",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The resolved resource does not satisfy a constraint for this principal — typically the accessed resource is not theirs.",
      "detail": [
        "field",
        "expected",
        "received"
      ]
    },
    {
      "code": "no_resource_resolver",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The tool declares resource-bound constraints but the host configured no resolver, so ownership cannot be established."
    },
    {
      "code": "resource_not_found",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The resolver reported that the resource does not exist. Distinct from a mismatch and from a resolver failure."
    },
    {
      "code": "resolver_error",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The resource resolver raised, or is async while the tool is sync. Fail closed."
    },
    {
      "code": "canary_exfil",
      "effect": "deny",
      "phase": "pre",
      "meaning": "A planted canary token appeared in an argument, verbatim or after the defined normalization."
    },
    {
      "code": "secret_detected",
      "effect": "deny",
      "phase": "pre",
      "meaning": "An argument contains a checksum-verified secret (PAN, IBAN, decodable JWT). Structural-only matches are redacted on output rather than denied.",
      "detail": [
        "received"
      ]
    },
    {
      "code": "injection_pattern",
      "effect": "deny",
      "phase": "pre",
      "meaning": "OPTIONAL, opt-in only. A static content rule matched. Heuristic, off by default, and not part of the deterministic core."
    },
    {
      "code": "exfiltration_pattern",
      "effect": "deny",
      "phase": "pre",
      "meaning": "OPTIONAL, opt-in only. A static content rule matched. Heuristic, off by default, and not part of the deterministic core."
    },
    {
      "code": "rate_limit",
      "effect": "deny",
      "phase": "pre",
      "meaning": "Calls per rolling window exceeded for this principal and tool."
    },
    {
      "code": "budget",
      "effect": "deny",
      "phase": "pre",
      "meaning": "Total call budget exhausted for this principal and tool."
    },
    {
      "code": "limit_store_capacity",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The in-process limit store reached its configured maximum number of identity/tool keys. Fail closed rather than allocate unbounded attacker-chosen identity state."
    },
    {
      "code": "requires_confirmation",
      "effect": "require_confirmation",
      "phase": "pre",
      "meaning": "The tool needs a trusted out-of-band approval bound to this exact action. The agent cannot satisfy this itself."
    },
    {
      "code": "confirm_error",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The confirmation callback raised, or is async while the tool is sync. Fail closed — never treat an unavailable approver as approval."
    },
    {
      "code": "escalated",
      "effect": "allow",
      "phase": "pre",
      "meaning": "The tool is marked `escalate` and the host's semantic tier let the call continue. The tier can only release a call the deterministic chain already allowed; it can never allow one the chain refused, so this code never appears where another code would have denied."
    },
    {
      "code": "escalation_denied",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The host's semantic tier refused the call. The tier's verdict is read as a boolean: anything other than a truthy result is a refusal."
    },
    {
      "code": "escalation_error",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The escalate callback raised, or is async while the tool is sync. Fail closed — an unreachable tier is never read as an approving one, and a coroutine object is never read as a verdict."
    },
    {
      "code": "no_escalation_tier",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The policy marks this tool `escalate` and the engine has no semantic tier wired. THE COLLAPSE, and the property the seam exists for: an engine that cannot judge meaning refuses the call rather than proceeding without judging it. Every implementation must reproduce this — a port that treats a missing tier as 'nothing to check' turns the same policy into a weaker one.",
      "detail": [
        "expected",
        "received"
      ]
    },
    {
      "code": "internal_error",
      "effect": "deny",
      "phase": "pre",
      "meaning": "A check raised unexpectedly. Fail closed. This is a bug; the reason carries the exception."
    },
    {
      "code": "post_redaction",
      "effect": "redact",
      "phase": "post",
      "meaning": "The result was returned with content removed: canary tokens, sensitive fields the role may not see, recognised secrets, or undeclared fields dropped by output projection.",
      "detail": [
        "redactions"
      ]
    },
    {
      "code": "exception_redaction",
      "effect": "redact",
      "phase": "post",
      "meaning": "The tool raised, and its error text carried canary tokens or recognised secrets, which were removed before the error reached the caller. An exception is the other way a tool returns content to the model, so it goes through the same content controls — but only the ones that apply to unstructured text: output projection, strict returns and sensitive-field redaction all need a declared return shape an exception does not have.",
      "detail": [
        "redactions"
      ]
    },
    {
      "code": "output_schema",
      "effect": "deny | redact",
      "phase": "post",
      "meaning": "The tool's output did not match its declared return schema. Under strict returns this is handled per on_violation, because name-based redaction cannot protect a secret in an undeclared field."
    },
    {
      "code": "confirm_suspended",
      "effect": "require_confirmation",
      "phase": "pre",
      "meaning": "The host's confirm callback signalled that the run is parked awaiting an out-of-band approval, by raising one of the types passed as confirm_suspends. Not a denial and not an allow: no decision has been made yet and the tool has not run. Recorded so the trail shows that a human was asked; an engine that lets a suspension leave without a record cannot show that."
    },
    {
      "code": "confirm_cancelled",
      "effect": "require_confirmation",
      "phase": "pre",
      "meaning": "The host cancelled execution while the gate was awaiting out-of-band confirmation. The cancellation is recorded and propagated unchanged; the tool has not run."
    },
    {
      "code": "pre_cancelled",
      "effect": "deny",
      "phase": "pre",
      "meaning": "Execution was cancelled while the deterministic pre-gate awaited a host callback such as the resource resolver or semantic tier. The cancellation is recorded and propagated unchanged; no pre-gate decision completed and the tool has not run."
    },
    {
      "code": "unnameable_args",
      "effect": "deny",
      "phase": "pre",
      "meaning": "The call passed positional arguments to a tool whose parameters cannot be named — it takes *args, or exposes no introspectable signature — so the argument schema, the trusted bindings and the audit trail have nothing to attach to. A tool with an ordinary signature is bound by name and never reaches this. An engine that instead validates positional arguments by position is not compatible: a policy names its fields."
    },
    {
      "code": "uninspectable_output",
      "effect": "deny",
      "phase": "post",
      "meaning": "The tool returned, or raised an error carrying, a value whose payload is behind an iteration the post chain never performs — a generator, a coroutine, an iterator, a view, or any other non-string iterable that is not a container the post chain walks — at the top level or nested inside one that is. Distinct from output_schema, which means the output WAS read and did not match: here nothing read it, so every output control would report allow on content it never saw. The tool has already run by then, so the denial stops the unscanned payload reaching the model; it does not undo the call. An engine that instead allows such a value is not compatible."
    }
  ],
  "policy_codes": {
    "duplicate_key": "A repeated mapping key. Never last-one-wins — the engine refuses to guess.",
    "invalid_binding": "A `bind` value is not exactly `principal.<attr>` — bindings are substitutions, not expressions.",
    "invalid_grant": "A role's `allow` entry is not a plain tool name.",
    "not_an_object": "The top level is not an object/mapping.",
    "not_found": "The policy file does not exist.",
    "tools_not_a_mapping": "`tools` is a list (the pre-0.1 shape) rather than a mapping keyed by tool name.",
    "unknown_key": "A key this engine does not understand, at any level. Loading partially would enforce partially.",
    "unparseable": "The document is not valid JSON/YAML, or is empty.",
    "unsafe_pattern": "A `pattern` whose shape admits runaway backtracking. `re` is a backtracking engine with no step budget that holds the GIL, so a catastrophic pattern cannot be interrupted once it starts and the only fail-closed answer is to refuse it before it can run. Part of the format, not an implementation detail: an engine that accepts a pattern this corpus says to refuse is not compatible.",
    "unsupported_feature": "requires.features names a capability this engine does not implement.",
    "unsupported_version": "The document's schema_version is not implemented by this engine.",
    "policy_invalid": "Any other structural problem (unclassified; new cases should get their own code)."
  },
  "review_codes": {
    "$comment": "Not yet coded — review_policy currently emits prose. Coding these is a prerequisite for policy-analysis tooling and is tracked in docs/roadmap.md, not implied here."
  }
}
