Request lifecycle
policy_id returns 400 Project is not linked to a policy.
Decision fields
| Field | Meaning |
|---|---|
decision | Raw rule outcome — one of allow, rewrite, summary, escalate, refuse |
effective_decision | What was actually applied. In shadow/non-enforced canary, this equals allow even when decision != allow |
enforced | Boolean — was the decision acted on (true) or only logged (false) |
rollout_mode | shadow, canary, enforced, or rollback |
reason_code | Uppercase code mirroring the decision: ALLOW, REWRITE, SUMMARY, ESCALATE, REFUSE. Policies can override with a custom reason_codes list. |
triggered_categories | Moderation categories that matched |
allowlist_hits / denylist_hits | Which terms matched |
policy_target | chat.completions, messages, responses, mcp_tool |
policy_user | Subject from X-Policy-User header (if sent) |
quota_subject | Effective per-user subject (policy_user, or falls back to user_id) |
Policy events
Three event classes are emitted to the Pub/Sub topicpolicy-events:
EnforcementEvent
Fires on every governed request.SimulationEvent
Fires when the console’s policy simulator is used (/api/policy-gateway/simulate). Adds:
RevisionEvent
Fires on policy create/update/delete.Delivery semantics
- Events are emitted fire-and-forget from a background daemon thread.
- If the Pub/Sub publish fails, the event is dropped (no on-disk retry queue).
- Connectors downstream of Pub/Sub retry with exponential backoff and honor destination-specific batching.
Caveats
- Shadow mode still runs rules. Every allowlist / denylist / category check happens even in shadow — it just doesn’t block. This is what makes dry-run measurement possible.
- No per-rule rollout overrides. Rollout mode is a single knob at the policy level. Adding a rule doesn’t let you ramp that rule independently.
- Same policy applies to every target. A single policy is used for
chat.completions,messages,responses, andmcp_tool. There’s no per-target rule tree. - Allowlist exclusivity. A non-empty allowlist forces
refuseon any message without an allowlist hit, regardless of denylist/category. - Canary is probabilistic. Two identical requests at
percentage=50can have different outcomes.
