# Guardrail

> An assertion about agent behavior.

`@kindgi/specs/guardrail.schema.json`, schema version 1.2.0.

An assertion about agent behavior. Checked at runtime, where `halt` stops the turn, `retry` asks the model for a new answer, and every other action is recorded, AND in CI (blocks builds). Same definition, two enforcement paths — no drift between what tests check and what prod enforces. Zero-LLM checks are default (fast, deterministic); LLM-judge scorers are an opt-in class with explicit cost declaration. Schema-version 1.1.0 matches the `Guardrail` type: `check` is required, a `retry` object, when given, needs `maxAttempts`, `kind` and `on-violation` accept any non-empty string (the built-ins are listed as `examples`), and `sandbox` / `limits` / `network` / `needsSpec` are allowed.

- `id` (string, required): Unique guardrail identifier.

- `name` (string): Human-readable name.

- `description` (string): What this guardrail guarantees, and what happens if violated.

- `kind` (string, required): How the guardrail is checked. Open string: the engine dispatches it to the execution strategy registered for that kind, and `validateGuardrail` requires a registered check of the same kind. Built-in kinds: 'zero-llm' = pure function over run trace (default, fast, deterministic). 'llm-judge' = uses a model to score (opt-in, costs money). 'external' = evaluated outside the engine by a caller-registered strategy (the built-in 'external' strategy returns an error). Adapters can register strategies for their own kinds.

- `check` (string, required): Id of the check in the CheckRegistry. For zero-llm: a built-in check ('must-cite', 'never-call-tool', 'max-tool-calls', 'output-matches', 'tool-order', 'required-substring', 'forbidden-substring') or a custom check id. For llm-judge: the id of a registered check of kind 'llm-judge' (the judge is configured by config and judgeCapabilities). For external and adapter kinds: an id the kind's strategy understands. Required since schema-version 1.1.0 (`validateGuardrail` already failed without a registered check).

- `config` (map of any): Check-specific configuration. Interpreted by the check implementation.

- `configSchema` (object): JSON Schema (Draft 2020-12) of the check's config, for a check that is pack code: the pack index's `configSchema`, which a deployment keeps on the guardrails it registers. The runtime checks a guardrail's `config` against it at registration, for any guardrail naming that check, and the pack service checks it on every call. Optional; a runtime-declaration field like `codeArtifactRef`.

- `action` ([Action](https://docs.kindgi.com/v0.5.0/reference/schemas/guardrail.md#action), required)

- `severity` (`"info"` | `"warn"` | `"error"` | `"critical"`)

- `scope` ([Scope](https://docs.kindgi.com/v0.5.0/reference/schemas/guardrail.md#scope)): When this guardrail applies: 'always' (every run), 'ci-only' (blocks CI, not runtime), 'runtime-only' (runtime enforcement, not CI), or a per-agent/per-flow selector.

- `at` (`"before-tool"` | `"after-tool"` | `"model-step"` | `"final-answer"`): Where in the agent's turn the check runs. 'before-tool': a call the model asked for, before it runs and before any approval is asked (the trace's `call`). 'after-tool': a call that ran, before the model sees its result (`call` and `result`). 'model-step': each model reply that asks for tool calls, before any of them runs (`step`). 'final-answer': the answer that would be stored (`output`). The check must support the point (its `points`). 'escalate' and 'compensate' are recorded at the final answer only.

- `sees` (`"turn"` | `"conversation"`): What the check sees of the conversation: 'turn', this turn only, or 'conversation', with the earlier turns the model was given for this turn too (the trace's `earlier`).

- `budget` (object): Cost and time budget per invocation. The agent loop enforces `maxLatencyMs` at 'before-tool', 'after-tool' and 'model-step' (absent: 10 s for zero-llm, 60 s for llm-judge, 30 s for any other kind), and a check that runs past it fails as one that couldn't run (`check-timed-out`). It isn't enforced at the final answer, and `maxCostUsd` isn't enforced.

  - `maxCostUsd` (number)
  - `maxLatencyMs` (integer)

- `sandbox` (`"none"` | `"context-isolated"` | `"strict"`): Isolation posture the runtime enforces around the check's handler. Same values as a tool's `sandbox`. Added in schema-version 1.1.0.

- `limits` (object): Sandbox-enforced resource caps while the check runs. Same shape as a tool's `limits`. Added in schema-version 1.1.0.

  - `memMB` (integer, required)
  - `cpuMs` (integer, required)

- `network` (one of the objects below): Network egress policy the sandbox honors while the check runs. Discriminated on `kind`; same shape as a tool's `network`. Added in schema-version 1.1.0.

  - an object
    - `kind` (`"none"` | `"unrestricted"`, required)

  - an object

    - `kind` (`"allowlist"`, required)
    - `hosts` (array of string, required)

- `needsSpec` (object): Typed discriminated needs of the check (env / secrets / config / capabilities / bindings). Same shape as a tool's `needsSpec`. Added in schema-version 1.1.0.

  - `env` (map of object)
  - `secrets` (map of object)
  - `config` (map of object)
  - `capabilities` (array of string)
  - `bindings` (array of string)

- `codeArtifactRef` (one of the objects below): Handler-artifact pointer for the guardrail's check implementation. Discriminated on `kind`: `oci` is the deploy-pipeline shape (image + module path + artifactVersion); `filesystem` is the local-development shape — absolute host path at the check module. Production servers SHOULD reject `filesystem`.

  - an object

    - `kind` (`"oci"`, required)
    - `imageRef` (string, required)
    - `modulePath` (string, required)
    - `artifactVersion` (string, required)

  - an object

    - `kind` (`"filesystem"`, required)
    - `modulePath` (string, required)

- `checkBuiltIn` (`true`): Set by the CLI on a guardrail that names a built-in check (`check`) without shipping a check of its own: its `codeArtifactRef` is only the file it's declared in. A runtime warns about a guardrail naming a built-in, with a code artifact and without this marker (built by an older CLI): its pack may ship its own check under the built-in's id, which the built-in replaces.

- `judgeCapabilities` (map of any): For llm-judge guardrails: capability declaration for the judge model. Routed through `@kindgi/capabilities` over the providers bound for evaluation; when the caller passes a workspace policy (an agent turn passes its own), the judge is routed under it. BYO judges supported.

## Definitions

### Action

- `on-violation` (string, required): What a failed check does to the agent turn. Open string: any non-empty name is accepted here. The runtime acts on 'halt': the turn fails with `guardrail-violation` and its response is not stored. It acts on 'retry' too: at the final answer, the answer goes back to the model with the check's reason, for a new one; at 'before-tool' the call doesn't run and the model gets the reason as its result; at 'after-tool' the result is withheld and the reason stored in its place; at 'model-step' the reply goes back and none of its calls run. Either way until `retry.maxAttempts` run out, and then the turn fails the same way. Any other name is recorded as a violation, as 'log-only' is, and the turn completes. A check that can't run acts the same way, with a reason that names why. Built-in actions: 'halt' = fail the turn. 'retry' = ask for a new answer, refuse the call, or withhold its result. 'log-only' = record, don't block. 'escalate' and 'compensate' are recorded like 'log-only' in 0.1: nothing escalates or calls a compensating tool.
- `retry` (object): For on-violation = 'retry': counts what this guardrail fails in a turn (answers, replies, calls or results, as its `at` says), the first one included (with maxAttempts 3 it sends back, refuses or withholds up to twice, and the third failure ends the turn). Left out, 2. The turn's maxSteps, and the agent loop's 32 model calls, bound it too.
  - `maxAttempts` (integer, required)
- `escalateTo` (string): For on-violation = 'escalate': a reviewer role or queue id. In 0.1 nothing escalates.
- `compensateWith` (string): For on-violation = 'compensate': the compensating tool's id. In 0.1 that tool isn't called.

### Scope

- `when` (`"always"` | `"ci-only"` | `"runtime-only"`)
- `agents` (array of string): Agent ids this guardrail applies to. Empty = all agents.
- `flows` (array of string): Flow ids this guardrail applies to. Empty = all flows.
- `tenants` (array of string): Tenant ids this guardrail applies to. Empty = all tenants.
- `tools` (array of string): At 'before-tool' and 'after-tool': the tools whose calls it checks, by id. A trailing '_' matches every id with that prefix ('acme.woo._'); a '\*' anywhere else matches nothing, so it's refused, as an empty list is. Absent = every tool.
