> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Gating

> Runtime-owned R0-R3 policy evaluation, pending approvals, safe-draft fallback, and audit records.

*Mirrored from Meerkat Meta main at [26f5ae9da1fd](https://github.com/lukacf/meerkat/commit/26f5ae9da1fd3caac378f1cd847ce50f9ec5ac92) on 2026-11-03; Meta version 0.9.9 at that commit.*

Gating is a policy service owned by the Meta runtime. A caller submits an action, actor, and risk tier. The runtime evaluates that request, records the result, and may create a pending approval entry.

> [!WARNING]
>
> Gating does not perform the proposed action. The caller must enforce the returned `outcome` before it invokes delivery, changes state, or performs another side effect.

There is no gating module or gating MCP tool. The runtime implements the evaluate, pending, decide, and audit contracts directly.

> [!NOTE]
>
> With Console access control enabled, `gating.view` controls access to pending approvals and audit records, while `gating.decide` controls submission of a gating decision. These permissions do not grant access to the underlying tool, account or recipient.
>
> The `r0`-`r3` values below belong to the Meta gating service. They do not activate native R1/R2/R3 operation review or human-consent integration. An `allowed` result or approval record does not establish that the action ran; use the action's execution result to determine what happened.

## Risk tiers

`GatingRiskTier` is serialized in snake case as `r0` through `r3`:

| Tier | Default outcome | Runtime behavior |
| - | - | - |
| `r0` | `allowed` | Allow immediately and write an `evaluated` audit entry |
| `r1` | `allowed` | Allow immediately and write an `evaluated` audit entry |
| `r2` | `allowed_with_audit` | Allow with consequence-mode audit unless conflict handling falls back to `safe_draft` |
| `r3` | `pending_approval` | Create a pending entry unless conflict handling falls back to `safe_draft` |

For `r2` and `r3`, memory-conflict policy can override the default. It returns `safe_draft` when the referenced memory is conflicted, required conflict context is missing while conflicts exist, or the memory conflict lookup fails.

## Evaluation contract

The evaluate request has this wire shape:

```json theme={null}
{
  "action": "send_customer_update",
  "actor_id": "agent-lead",
  "risk_tier": "r3",
  "rationale": "The customer requested an update",
  "requested_approver": "human-reviewer",
  "approval_recipient": "review-queue",
  "approval_channel": "notification",
  "approval_timeout_ms": 60000,
  "entity": "customer:42",
  "topic": "outbound_status"
}
```

Only `action`, `actor_id`, and `risk_tier` are required. Meta mints the `action_id`. A `GatingEvaluateResult` returns:

* `action_id`, `action`, `actor_id`, and the effective `risk_tier`
* one of `allowed`, `allowed_with_audit`, `pending_approval`, or `safe_draft` in `outcome`
* `pending_id` when approval is pending
* `fallback_reason` when conflict policy forced `safe_draft`

> [!NOTE]
>
> When the stdio `rpc_gateway` loads an `actions` map from `runtime_options.gating_config_path`, the configured tier for a matching action is authoritative. It replaces a caller-supplied `risk_tier` before evaluation.

A configured gating file is loaded strictly. `runtime_options.gating_config_path` must be a string: a non-string value, or a path the gateway cannot open or read or whose contents it rejects, refuses initialization, while an absent or null value leaves gating off. An empty string refuses initialization too: it no longer switches gating off, as it did when the predecessor SDKs dropped it. To keep gating off, omit the key (or send null), and in the SDKs do not call `gating`. `meta_gateway` and the SDK builders skip the conventional `config/gating.toml` only when nothing is there (a missing or dangling parent directory counts as nothing there). Any other entry there, even a directory or a dangling `gating.toml` link, and any other lookup error goes to the same strict load. The loaders do not check the file type, so a FIFO at the path blocks startup instead of refusing it.

## Pending approval

An `r3` evaluation creates a `GatingPendingEntry` with the action and actor identity, optional requested approver and notification fields, and `created_at_ms` and `deadline_at_ms` timestamps.

If both `approval_recipient` and `approval_channel` are present, the runtime resolves a route, then commits the pending entry and its `pending_created` audit, and only then sends a `gating_approval_request` through the loaded router and delivery modules, so a notification never names a gate the runtime does not hold. A missing module or routing failure is recorded in the `pending_created` detail and as an `approval_notification` event with disposition `not_sent`. The send outcome is recorded as a separate `approval_notification` audit event with `disposition` `acknowledged`, `not_delivered`, `not_sent` (refused before the delivery module was called) or `unknown` (the module call failed, so it may or may not have gone out), plus the route and delivery ids. The outcome never changes the committed gate: there is no automatic retry, no restored or renewed entry, and an unknown outcome is not reported as delivered. After applying due timeout settlements, a refused decision leaves the requested pending entry, its position, the IDs and the audit unchanged. These commits are in memory; `gating_state_snapshot` exports state for a host to persist, and exporting it is not a durable commit.

The TypeScript and Python `GatingPendingEntry` projections retain the requested approver, notification recipient and channel, optional route and delivery references, deadline, rationale, and the recorded origin (identity with optional conversation and interaction). Their `GatingAuditEntry` projections keep the owner's `pending_id` and event `detail` as returned. The console shows the requested approver separately from the actor. Missing optional fields remain unknown; the actor is never substituted for the approver. Route and delivery IDs are notification references, not confirmation of receipt, a human decision, or action execution.

These fields describe the existing Meta gating owner. They do not install a native model reviewer or create action-bound native consent. A host approval surface must authenticate and map the deciding principal; displaying or submitting a requested approver string does not authenticate that principal.

The console decides as its authenticated principal. When the request names a `requested_approver`, the principal must be that approver id or hold it through the access configuration's `approvers` bindings (see [Access control](https://docs.rkat.ai/meta/concepts/access-control.md)); otherwise the decision is refused as an approver mismatch. Request fields such as `approver_id` are ignored for an authenticated principal. Bindings apply only to authenticated console decisions: an open console (no app authentication) takes `approver_id` from the request, so it is for trusted local use only. Several principals can hold one id, such as a shared role; the first eligible decision settles the request, and the audit detail records the approver id used (`approver_id`), the individual (`deciding_principal`) and how the id was held (`approver_binding`: `principal` or `held`). The gate refuses an approval when the actor is the principal or any id the principal holds. Hosts that call `meta/gating/decide` directly over the SDK transport are trusted embedding callers and name `approver_id` themselves.

Approval timeouts default to 60 seconds and are clamped to the runtime limits. Expiry is refreshed lazily when another gating operation evaluates, lists pending entries, decides, or reads the audit log. An expired entry is removed and recorded as:

```json theme={null}
{
  "event_type": "timeout_fallback",
  "outcome": "safe_draft",
  "detail": {
    "fallback": "safe_draft",
    "reason": "approval_timeout"
  }
}
```

Gating IDs (`gate-action-...`, `gate-pending-...` and `gate-audit-...`) are opaque strings. Each runtime mints them under its own random 128-bit epoch, so a restart or another runtime does not reissue an earlier ID (barring a random epoch collision). Do not parse or order them.

## Decisions

Resolve a pending entry with:

```json theme={null}
{
  "pending_id": "gate-pending-000002",
  "approver_id": "human-reviewer",
  "decision": "approve",
  "reason": "Reviewed against the customer request"
}
```

| Decision | Outcome | Audit event |
| - | - | - |
| `approve` | `allowed` | `approval_decided` |
| `reject` | `safe_draft` | `rejection_decided` |
| `escalate` | `pending_approval` | `escalation_decided` and a `pending_created` entry for the successor |

Escalation goes to a configured successor approver. The successor entry keeps the same action, candidate and deadline, and its `requested_approver` is the successor. Routes map the escalated entry's requested approver to a successor, with an optional default for entries that name none or have no specific route. Without a route, escalation is refused (`NoEscalationRoute`, or invalid params over JSON-RPC) and the entry stays pending until it is decided or times out. Clearing the requested approver is not proof that every `gating.decide` holder is qualified, so an unrouted escalation never opens the request to anyone. Embedding hosts configure routes with `set_gating_escalation_routes`. `rpc_gateway` reads an `escalation` object from `gating_config_path`, and `meta_gateway` reads an `[escalation]` table from `config/gating.toml` (`default = "..."` and an `[escalation.routes]` table). An unknown key inside the escalation configuration fails startup. Successors created before this change and restored from a saved snapshot have no requested approver, so any `gating.decide` holder can decide them until they are decided or expire. For `rpc_gateway`:

```json theme={null}
{
  "actions": {},
  "escalation": {
    "default": "role:on-call",
    "routes": { "person:alice": "role:release-manager" }
  }
}
```

Approval by the original `actor_id` is rejected. When `requested_approver` is set, every decision must come from that approver. Escalation creates a successor pending entry and returns its ID in `next_pending_id`; routing is described above.

## Audit records

`GatingAuditEntry` contains:

```rust theme={null}
pub struct GatingAuditEntry {
    pub audit_id: String,
    pub timestamp_ms: u64,
    pub event_type: String,
    pub action_id: String,
    pub pending_id: Option<String>,
    pub actor_id: String,
    pub risk_tier: GatingRiskTier,
    pub outcome: GatingOutcome,
    pub detail: serde_json::Value,
}
```

The current runtime writes these event types:

* `evaluated`
* `conflict_blocked`
* `memory_conflict_lookup_failed`
* `pending_created`
* `approval_decided`
* `rejection_decided`
* `escalation_decided`
* `timeout_fallback`

The audit log and pending queue are bounded runtime collections. They are not the unified event stream.

### Reading audit changes

`meta/gating/audit` accepts an optional `after_audit_id`. With it, the result lists up to `limit` entries recorded after that entry, oldest first; without it, the newest `limit` entries. Every result carries `next_cursor` (the last returned entry, the given cursor when nothing new was returned, or null after a gap with nothing returned) and `gap`. `gap: true` means the runtime's retained audit does not hold the cursor: the entry was evicted from the bounded audit, belongs to an earlier runtime whose audit was not restored, or is unknown. A restored audit keeps its entries' ids, so their cursors keep working. A malformed `after_audit_id` is refused as invalid params. The result then restarts from the oldest retained entry, and the reader must treat what it built so far as possibly incomplete. The audit is a bounded operational view, resumable with explicit gap detection; it is not a complete durable history.

The console route applies the same audience as pending entries: an entry recorded with an origin is visible only to callers who may view that origin's agent, and `next_cursor` never names an entry the caller cannot see. Hidden entries therefore never advance the console cursor: a stretch of them is read again on the next call, and a gap can be reported if the last visible entry is evicted meanwhile. A gap means resynchronize, not that a visible entry was lost.

`GET /console/gating/stream` is a live notification stream for the console. Each connection starts with `gating.resync_required` (`initial_sync`). After that, `gating.changed` means an entry the caller may see was recorded, and `gating.resync_required` (`gap`) means the caller should re-read from the start. Frames carry no gate IDs or outcomes; the caller reads them through `meta/gating/audit` with its cursor. Origin labels and roles are resolved from the live roster when the stream opens and whenever a new origin appears. `gating.view` and the origin audience are re-checked from the live access configuration on every change and every 15 seconds without one, which also applies approval expiry; losing `gating.view` ends the stream with `gating.access_revoked`. That periodic check covers access policy and approval expiry and is not a hard latency bound. The caller's token is checked separately: once it is no longer accepted, the stream ends without a frame, as every Console stream does. A burst of changes the caller cannot see produces no frame while the stream's cursor is retained; if the burst evicts it, the stream sends `gating.resync_required` (`gap`). A frame or an audit entry is information, never authorization: acting on a decision still goes through the current action and approval owners.

## Memory conflict integration

For `r2` and `r3`, the runtime checks the supplied `entity` and `topic` against memory conflicts. If the `memory` module is loaded, that core MCP boundary must provide the `memory.conflict_read` tool. Without a loaded memory module, the runtime uses its in-process conflict ledger.

## API methods

The JSON-RPC methods are:

* `meta/gating/evaluate`
* `meta/gating/pending`
* `meta/gating/decide`
* `meta/gating/audit`

## See also

* [Delivery](https://docs.rkat.ai/meta/concepts/delivery.md) - route and send after the caller accepts a gating outcome
* [Memory](https://docs.rkat.ai/meta/concepts/memory.md) - assertions and conflict handling
* [JSON-RPC API](https://docs.rkat.ai/meta/api/rpc.md) - exact method parameters and responses
* [Configuration](https://docs.rkat.ai/meta/reference/configuration.md) - runtime configuration


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.