Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd 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.
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.
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: 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:
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
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); 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:
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:
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:
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:
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 - route and send after the caller accepts a gating outcome
  • Memory - assertions and conflict handling
  • JSON-RPC API - exact method parameters and responses
  • Configuration - runtime configuration