Policies and versions
- A policy has a key, a name, a description and a list of versions. Its rules are stored as data; you never deploy code to change a decision.
- A version is either
draft,publishedorsuperseded. It records the author, a change note and a content hash. Drafts never affect live decisions. - Publishing is four-eyes: the publisher must be a different person from the version's author. Publishing makes the version active and supersedes the previous one. To roll back, create a new draft with the earlier rules and publish it; history is never rewritten.
- Changes use optimistic concurrency: creating a version, publishing and changing status need
If-Match: <policy version>(missing: 428, stale: 409). - Every active policy contributes its published rules to one rule set; they are evaluated together. The baseline pack cannot be deactivated, and containment rules are managed through containment actions, not edited directly.
Rule documents
{
"id": "FIN-001",
"description": "Payments of £1,000 or more need approval",
"effect": "require_approval",
"priority": 100,
"reason_code": "PAYMENT_ABOVE_THRESHOLD",
"conditions": [
{"field": "tool.privilege_class", "op": "eq", "value": "financial"},
{"field": "action.amount", "op": "gte", "value": 1000}
],
"obligations": {"approvals_required": 1},
"enabled": true
}| Field | Required | Meaning |
|---|---|---|
id | Yes | Unique within the policy. Letters, digits, - and _ (up to 64). |
effect | Yes | allow, deny or require_approval. |
reason_code | Yes | UPPER_SNAKE_CASE code reported with the decision and in evidence, for example SECRET_DETECTED. |
conditions | Yes | 1 to 20 conditions. All must hold for the rule to match (logical AND). |
description | No | Human-readable explanation (up to 500 characters). |
obligations | No | For require_approval: approvals_required, 1 to 5 distinct approvers (default 1). |
enabled | No | Default true. Disabled rules are ignored. |
priority | No | Informational. Decisions combine by effect, not by priority. |
A condition is {"field", "op", "value"}: field is a dotted path into the decision input (for example tool.privilege_class). For an or, write two rules with the same effect.
Operators
| Operator | Holds when | If the field is missing |
|---|---|---|
eq | The field equals the value. | Does not hold |
neq | The field does not equal the value. | Holds |
in | The field equals one of the values (value must be a list). | Does not hold |
not_in | The field equals none of the values (value must be a list). | Holds |
gt, gte, lt, lte | Numeric comparison. Both sides must be numbers. | Does not hold |
exists | The field is present and not null. | Does not hold |
not_exists | The field is absent or null. | Holds |
contains | The field is a list containing the value. | Does not hold |
prefix | The field is a string starting with the value. | Does not hold |
Decision input
The gateway builds the decision input from the authenticated agent, the registry, inspection and the request. It never takes these values from the agent's own claims. The rule builder lists these fields:
| Field | Values |
|---|---|
agent.status | active, suspended, revoked |
agent.environment | development, staging, production |
agent.risk_score / agent.risk_band | 0–100 / low, moderate, high, critical, or unknown until the agent is scored (last computed score) |
agent.owner_attestation_current | true when an owner has attested recently |
agent.criticality | low, medium, high, critical |
agent.id | The agent's ID |
identity.auth_strength | Strength of the workload identity's authentication |
delegation.present / delegation.in_scope / delegation.subject | Whether the agent acts for a user, whether the tool is within that user's consented scopes, and the user |
tool.key | The tool key, for example payments.transfer.create |
tool.privilege_class | read, write, destructive, financial, admin |
tool.approval_class | none, single, dual |
tool.mcp_trust_state | approved, unknown, unsigned, quarantined (not_applicable outside MCP) |
tool.target_type | model, mcp_tool, api |
action.operation / action.target | Operation and target from the request |
action.environment | Environment of the action (defaults to the agent's) |
action.amount | Numeric amount parameter, when present |
action.channel | gateway (agent) or console (testing console) |
data.classification | public, internal, confidential, personal, regulated, restricted |
data.contains_personal_data | true when the target data resource holds personal data |
model.approved / model.approved_for_data | Whether the model is approved, and approved for this data classification |
inspection.secrets / inspection.prompt_injection / inspection.pii | Results of content inspection on parameters and justification |
binding.granted / binding.delegated / binding.constraints_satisfied | Whether a permission covers this tool and operation, whether it is delegated, and whether constraints such as max_amount hold |
rate.exceeded | true when the agent exceeded its per-tool rate (60 a minute by default) |
Decision precedence
Every rule from every active policy is evaluated. The decision is then the earliest of these that applies:
- Platform invariants deny. These cannot be overridden by any rule: agent not active (
AGENT_NOT_ACTIVE), gateway access disabled (GATEWAY_ACCESS_DISABLED), unknown tool (UNKNOWN_TOOL), blocked tool (TOOL_BLOCKED) and identity not active (IDENTITY_NOT_ACTIVE). - Any matching deny rule denies. Deny outranks approval and allow.
- Any matching approval rule requires approval, unless the request carries a valid approval token for this exact request. The number of approvers is the highest
approvals_requiredamong the matching approval rules. - A matching allow rule, or a verified approval, allows. With a verified approval the reasons also include
APPROVAL_VERIFIED. - Nothing matched: deny with
NO_MATCHING_ALLOW. The platform is default-deny.
The decision reports its reason codes (from the deciding rules), the IDs of every matching rule and of the deciding rules, the obligations, and the engine used. All of it is recorded in the evidence chain. If the policy engine fails, the decision fails closed (PDP_UNAVAILABLE_FAIL_CLOSED).
Baseline pack
Every organisation starts with the CydraGateway baseline pack. It can be changed by publishing a new version (four-eyes) but not deactivated.
| Rule | Effect | Reason code | Purpose |
|---|---|---|---|
| POL-001 | deny | AGENT_NOT_REGISTERED | Unregistered, inactive or wrong-organisation agent |
| POL-002 | require_approval | PRODUCTION_HIGH_IMPACT | Production write, destructive, financial or admin tool |
| POL-003 | deny | SECRET_DETECTED | High-confidence credential pattern in outbound parameters |
| POL-004 | deny | PROMPT_INJECTION | Instruction-override or exfiltration indicator |
| POL-005 | deny | SENSITIVE_DATA_TO_UNAPPROVED_MODEL | Regulated or restricted data to a model not approved for it |
| POL-006 | deny | UNTRUSTED_MCP_PRIVILEGED_TOOL | Privileged tool on an MCP server of unknown, unsigned or quarantined provenance |
| POL-007 | require_approval (2) | CRITICAL_AGENT_RISK | Critical Agent Risk Score (75 or more) and a non-read action |
| POL-008 | deny | RATE_LIMIT_EXCEEDED | Agent exceeded its tool request rate |
| POL-009 | require_approval | OWNER_ATTESTATION_EXPIRED | Production agent with an expired owner attestation, high-impact action |
| POL-010 | deny | AGENT_SUSPENDED | Suspended or revoked agent |
| GRANT-001 | deny | PERMISSION_NOT_GRANTED | No permission for this tool and operation |
| GRANT-002 | deny | DELEGATION_OUT_OF_SCOPE | Delegated tool used outside the user's consented scope |
| GRANT-003 | deny | PERMISSION_CONSTRAINT_EXCEEDED | Parameters exceed a permission constraint, such as a maximum amount |
| ALLOW-001 | allow | PERMISSION_GRANTED | Explicitly granted tool operation |
Examples
Dual approval for large payments in production
{
"id": "FIN-002",
"effect": "require_approval",
"reason_code": "HIGH_VALUE_PAYMENT",
"conditions": [
{"field": "tool.privilege_class", "op": "eq", "value": "financial"},
{"field": "action.environment", "op": "eq", "value": "production"},
{"field": "action.amount", "op": "gte", "value": 25000}
],
"obligations": {"approvals_required": 2}
}No personal data to email tools
{
"id": "DLP-001",
"effect": "deny",
"reason_code": "PERSONAL_DATA_TO_EMAIL",
"conditions": [
{"field": "tool.key", "op": "prefix", "value": "email."},
{"field": "inspection.pii", "op": "eq", "value": true}
]
}Freeze destructive actions for one high-risk agent
{
"id": "OPS-001",
"effect": "deny",
"reason_code": "CHANGE_FREEZE",
"conditions": [
{"field": "agent.risk_band", "op": "in", "value": ["high", "critical"]},
{"field": "tool.privilege_class", "op": "eq", "value": "destructive"}
]
}Testing
- Test this draft on a policy page runs a decision with the draft rules added to your active rule set (
POST /api/v1/policies/simulatewithdraft_rules). Nothing is stored or executed. - The Gateway console simulates or submits an action as one of your agents against the active rules, with the full decision, inspection results and timeline.
- Saving a version validates it: unique rule IDs, a known effect and operator, a reason code, at least one condition, and list values for
inandnot_in.
Golden decision cases
Decisions are made by one of two evaluators with identical semantics: Open Policy Agent (Rego) and an embedded evaluator. Both run against the same set of golden decision cases in every build, so they cannot drift apart. A selection:
| Case | Expected decision | Reason codes |
|---|---|---|
| Granted read in production | allow | PERMISSION_GRANTED |
| No rules at all | deny | NO_MATCHING_ALLOW |
| Production financial tool | require_approval (1) | PRODUCTION_HIGH_IMPACT |
| …the same with a verified approval | allow | APPROVAL_VERIFIED, PERMISSION_GRANTED, PRODUCTION_HIGH_IMPACT |
| Secret in parameters, even with approval | deny | SECRET_DETECTED |
| Suspended agent, even with no rules | deny | AGENT_NOT_ACTIVE |
| Unknown tool | deny | UNKNOWN_TOOL |
| Privileged tool on an unknown MCP server | deny | UNTRUSTED_MCP_PRIVILEGED_TOOL |
| Read tool on an unknown MCP server | allow | PERMISSION_GRANTED |
| Critical risk score and production financial tool | require_approval (2) | CRITICAL_AGENT_RISK, PRODUCTION_HIGH_IMPACT |
| Missing numeric field with gte | allow (the rule does not match) | PERMISSION_GRANTED |
| Boolean compared with a number | deny (no match) | NO_MATCHING_ALLOW |
| Disabled rule | ignored | PERMISSION_GRANTED |
See the architecture for where the decision sits in the ten-step workflow, and the quick start to write and test a rule of your own.
Applies to the CydraLabs proof-of-concept platform. Last updated 4 October 2026. Questions or corrections: contact us.