Create or replace the org's MCP policy
Upserts the org's policy and records a new revision. Restricted to admins: this is the write that changes what gate enforces.
The Rego is compiled before the write is accepted, whether it was generated from rules or authored by hand. This is the only place the author sees a compile error: OPA activates a bundle only if it compiles, so a broken policy served to gate leaves the PREVIOUSLY activated policy in force and the console would show the broken one as current.
Sending authored_by: rego when the stored policy is rules walks the one-way door described on GateMCPPolicyAuthoringMode. The stored rules are retained, not cleared.
Header Parameters
- SlashID-OrgID string required
The organization ID
Example: af5fbd30-7ce7-4548-8b30-4cd59cb2aba1
- application/json
Request Body required
- enabled boolean required
- default_effect string required
Possible values: [
deny,allow]What gate does with an MCP call that no rule matches.
denyfails closed: a call is allowed only when an allow rule matches it.allowfails open: a call is allowed unless a deny rule matches it. This is the single most consequential setting on the policy, which is why it is a first-class field rather than something an operator has to read out of the generated Rego'sdefault allowline. - on_missing_context string
Possible values: [
deny,ignore]What happens when gate cannot supply what a rule needs — an unverified token, attributes it could not resolve, or a request carrying no time.
deny: the request is denied.ignore: the rule is skipped — which means DENY RULES STOP DENYING for as long as the data is unavailable. Since an attacker who can make the resolver unreachable can choose that state,ignoreis only safe when the policy's deny rules are not the control being relied on.New policies default to
deny. Policies that existed before this field was introduced are set toignore, so their behaviour did not change under them. - authored_by string required
Possible values: [
rules,rego]How the current
regowas produced.rules— generated fromrules, and safe to regenerate: editing the rules overwrites it.rego— a human edited the Rego directly, so the builder must not regenerate over it.The transition
rules->regois a ONE-WAY DOOR by construction: the builder emits Rego but cannot parse arbitrary Rego back into rules, so round-tripping would mean discarding the human's edits.rulesis retained after the switch so the UI can offer an explicit "revert to the rule builder" that names what it will discard. rules object[]
Required when
authored_byisrules; the server generatesregofrom them and ignores anyregosent alongside. Ignored whenauthored_byisrego.id string requiredStable identifier. Generated Rego rule names are derived from it, so keeping it stable across edits makes a policy diff show what actually changed rather than a wholesale renumbering.
description stringFree text, emitted as a comment above the generated rule.
effect string requiredPossible values: [
allow,deny]What this rule decides when it matches. The effect also decides the QUANTIFIER the generator uses over a multi-tool payload: a deny rule matches if ANY member of the batch matches, an allow rule only if EVERY member does. Reversing that would let a batch smuggle a denied tool alongside an allowed one.
conditions object[] required
Possible values:
>= 1field string requiredPossible values: Value must match regular expression
^(tool|method|resource_uri|prompt|http_path|http_host|http_method|is_batch|unparsable_calls|subject|issuer|group|tag|attribute:[A-Za-z0-9_-]+|claim:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*|argument:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*)$What a condition matches on. Each value maps to a key gate really populates on its OPA input; an unpopulated key would produce a rule that silently never fires.
tool,method— matched over the request's CALLS (input.request.mcp.calls[]), never the scalarinput.request.mcp.tool, which gate deliberately leaves empty for batched payloads so that a scalar-keyed policy cannot be slipped past by batching.subject,issuer— the token'ssub/issclaims.resource_uri,prompt— matched over the request's CALLS for the same reason, viainput.request.mcp.calls[].resource_uri/.prompt. Gate writes these keys only for a call that actually names a resource or a prompt, so a batch mixing a tool call with a resource read has calls where the key is absent. Such a call is invisible to the condition rather than matching it — "resource_uri is not one of X" constrains the calls that read a resource and says nothing about the ordinary tool call sharing the payload.http_path,http_host,http_method— the transport request rather than its MCP payload, viainput.request.http.*. For rules about where a request is going rather than what it asks for.is_batch— whether the payload carried more than one JSON-RPC call, viainput.request.jsonrpc.is_batch. Itsvaluesmust be exactly["true"]or["false"]: they are compared as booleans, and any other value would compile to a comparison that can never hold.unparsable_calls— how many batch elements gate could not read as JSON-RPC, viainput.request.jsonrpc.unparsable_calls. A COUNT compared as a number, so itsvaluesmust be whole non-negative numbers written as strings, e.g.["0"]. Because the operators here are set membership rather than comparison, "the batch contained something unreadable" is expressed asunparsable_calls not_in ["0"].group— the caller's group membership, resolved by gate from SlashID.tag— the NAMES of the entity tags on the caller's identity in the NHI graph, resolved by gate from SlashID alongside the groups. Names rather than ids, so the value is the one the console shows. Only the graph plane carries these: a caller matched as a SlashID person reports an empty list, which makes a tag condition false for them rather than undefined.attribute:<name>— a resolved caller attribute, e.g.attribute:department.claim:<dotted.path>— a claim read straight off the token, e.g.claim:realm_access.roles.argument:<dotted.path>— an argument of the tool being called, e.g.argument:path. SINGLE-CALL ONLY: gate populatesinput.request.mcp.argumentsjust for a one-call payload, so a rule keyed here does not constrain a batch. There is no per-call arguments collection to iterate, so this cannot be fixed on our side; pair it with anis_batchrule if batches must also be constrained.subject,issuer,group,tag,attribute:andclaim:describe the CALLER, and each requires gate to have verified the token's signature — an unverifiedsubis a value the caller chose, so a rule keyed on one would let them select whose permissions they receive. Seeon_missing_contextfor what happens when gate has not verified it. Thehttp_*andis_batchfields carry no such requirement: they are gate's own observations of the transport, not the caller's assertions.A free-form string rather than an enum because
attribute:andclaim:are parameterized. The character set is restricted because attribute and claim names reach the generated Rego as PATH SEGMENTS, which — unlike values — cannot be protected by string quoting.operator string requiredPossible values: [
in,not_in]How the field is compared to
values. Values are OR'd within a condition; conditions are AND'd within a rule.values string[] requiredPossible values:
>= 1Matched case-sensitively, exactly as gate reports the value.
active_window object
When a rule is in force.
Evaluated against the REQUEST's own time (
input.request.time), not the evaluator's clock, so replaying a recorded request in a dry-run judges it by when it actually happened rather than by when the replay runs.daysis tested against the request's LOCAL day. A range that wraps midnight (from>to) therefore means "beforetoOR afterfrom, on any listed day" — a Friday 22:00-06:00 window is in force Friday 00:00-06:00 and Friday 22:00-24:00, NOT Saturday morning. Say "Friday night into Saturday" with two windows on two rules, or list both days.days string[] requiredPossible values: [
mon,tue,wed,thu,fri,sat,sun],>= 1Days the window applies on, in the window's own timezone.
from string requiredPossible values: Value must match regular expression
^([01][0-9]|2[0-3]):[0-5][0-9]$Inclusive local start, HH:MM.
to string requiredPossible values: Value must match regular expression
^([01][0-9]|2[0-3]):[0-5][0-9]$Exclusive local end, HH:MM. 09:00-17:00 does not include 17:00:00.
Equal to
fromis rejected: it reads as both "always" (a full 24h wrap) and "never" (an empty range), so it has to be said a different way.timezone string requiredIANA name, e.g. "Europe/London". Validated against the tzdata the server carries when the policy is saved. A named zone rather than a fixed offset so the window keeps meaning "office hours" across a daylight-saving transition.
- rego string
Required when
authored_byisrego. Compiled server-side before the save is accepted, because an uncompilable bundle does not replace gate's active policy — it leaves the PREVIOUS one in force, so save time is the only moment the author is still there to see the error. - note string
Free-text reason recorded on the revision.
- 200
- 400
- 401
- 403
OK
- application/json
- Schema
- Example (from schema)
Schema
meta object
pagination object
limit integeroffset integertotal_count int64cursor_pagination object
Cursors are opaque. Follow the tokens the server returns; never construct, parse or modify one.
limit integernext_cursor stringOpaque token addressing the next page. Absent on the last page.
prev_cursor stringOpaque token addressing the previous page. Absent on the first page.
last_cursor stringOpaque token addressing the final page directly, so a client can jump to the end without a total count.
total_count int64Total number of matching items. Returned on the FIRST page only; clients cache it for the remainder of the walk.
errors object[]
httpcode integermessage stringcode stringMachine-readable reason, present only for errors that define one. Values today:
unserved_org_region(the organization is homed in a region this deployment does not serve),wrong_region(the request must be sent to the organization's home region),region_route_missing(this instance has no route to the organization's home region).result object
enabled booleanWhen false the bundle endpoint serves a permit-everything policy. The row and its revision history are kept, so disabling never destroys the record of what was once enforced.
default_effect stringPossible values: [
deny,allow]What gate does with an MCP call that no rule matches.
denyfails closed: a call is allowed only when an allow rule matches it.allowfails open: a call is allowed unless a deny rule matches it. This is the single most consequential setting on the policy, which is why it is a first-class field rather than something an operator has to read out of the generated Rego'sdefault allowline.on_missing_context stringPossible values: [
deny,ignore]What happens when gate cannot supply what a rule needs — an unverified token, attributes it could not resolve, or a request carrying no time.
deny: the request is denied.ignore: the rule is skipped — which means DENY RULES STOP DENYING for as long as the data is unavailable. Since an attacker who can make the resolver unreachable can choose that state,ignoreis only safe when the policy's deny rules are not the control being relied on.New policies default to
deny. Policies that existed before this field was introduced are set toignore, so their behaviour did not change under them.authored_by stringPossible values: [
rules,rego]How the current
regowas produced.rules— generated fromrules, and safe to regenerate: editing the rules overwrites it.rego— a human edited the Rego directly, so the builder must not regenerate over it.The transition
rules->regois a ONE-WAY DOOR by construction: the builder emits Rego but cannot parse arbitrary Rego back into rules, so round-tripping would mean discarding the human's edits.rulesis retained after the switch so the UI can offer an explicit "revert to the rule builder" that names what it will discard.rego stringThe Rego gate actually enforces.
rules object[]
The structured rules. Authoritative only while
authored_byisrules; retained but stale once a human has edited the Rego.id stringStable identifier. Generated Rego rule names are derived from it, so keeping it stable across edits makes a policy diff show what actually changed rather than a wholesale renumbering.
description stringFree text, emitted as a comment above the generated rule.
effect stringPossible values: [
allow,deny]What this rule decides when it matches. The effect also decides the QUANTIFIER the generator uses over a multi-tool payload: a deny rule matches if ANY member of the batch matches, an allow rule only if EVERY member does. Reversing that would let a batch smuggle a denied tool alongside an allowed one.
conditions object[]
Possible values:
>= 1field stringPossible values: Value must match regular expression
^(tool|method|resource_uri|prompt|http_path|http_host|http_method|is_batch|unparsable_calls|subject|issuer|group|tag|attribute:[A-Za-z0-9_-]+|claim:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*|argument:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*)$What a condition matches on. Each value maps to a key gate really populates on its OPA input; an unpopulated key would produce a rule that silently never fires.
tool,method— matched over the request's CALLS (input.request.mcp.calls[]), never the scalarinput.request.mcp.tool, which gate deliberately leaves empty for batched payloads so that a scalar-keyed policy cannot be slipped past by batching.subject,issuer— the token'ssub/issclaims.resource_uri,prompt— matched over the request's CALLS for the same reason, viainput.request.mcp.calls[].resource_uri/.prompt. Gate writes these keys only for a call that actually names a resource or a prompt, so a batch mixing a tool call with a resource read has calls where the key is absent. Such a call is invisible to the condition rather than matching it — "resource_uri is not one of X" constrains the calls that read a resource and says nothing about the ordinary tool call sharing the payload.http_path,http_host,http_method— the transport request rather than its MCP payload, viainput.request.http.*. For rules about where a request is going rather than what it asks for.is_batch— whether the payload carried more than one JSON-RPC call, viainput.request.jsonrpc.is_batch. Itsvaluesmust be exactly["true"]or["false"]: they are compared as booleans, and any other value would compile to a comparison that can never hold.unparsable_calls— how many batch elements gate could not read as JSON-RPC, viainput.request.jsonrpc.unparsable_calls. A COUNT compared as a number, so itsvaluesmust be whole non-negative numbers written as strings, e.g.["0"]. Because the operators here are set membership rather than comparison, "the batch contained something unreadable" is expressed asunparsable_calls not_in ["0"].group— the caller's group membership, resolved by gate from SlashID.tag— the NAMES of the entity tags on the caller's identity in the NHI graph, resolved by gate from SlashID alongside the groups. Names rather than ids, so the value is the one the console shows. Only the graph plane carries these: a caller matched as a SlashID person reports an empty list, which makes a tag condition false for them rather than undefined.attribute:<name>— a resolved caller attribute, e.g.attribute:department.claim:<dotted.path>— a claim read straight off the token, e.g.claim:realm_access.roles.argument:<dotted.path>— an argument of the tool being called, e.g.argument:path. SINGLE-CALL ONLY: gate populatesinput.request.mcp.argumentsjust for a one-call payload, so a rule keyed here does not constrain a batch. There is no per-call arguments collection to iterate, so this cannot be fixed on our side; pair it with anis_batchrule if batches must also be constrained.subject,issuer,group,tag,attribute:andclaim:describe the CALLER, and each requires gate to have verified the token's signature — an unverifiedsubis a value the caller chose, so a rule keyed on one would let them select whose permissions they receive. Seeon_missing_contextfor what happens when gate has not verified it. Thehttp_*andis_batchfields carry no such requirement: they are gate's own observations of the transport, not the caller's assertions.A free-form string rather than an enum because
attribute:andclaim:are parameterized. The character set is restricted because attribute and claim names reach the generated Rego as PATH SEGMENTS, which — unlike values — cannot be protected by string quoting.operator stringPossible values: [
in,not_in]How the field is compared to
values. Values are OR'd within a condition; conditions are AND'd within a rule.values string[]Possible values:
>= 1Matched case-sensitively, exactly as gate reports the value.
active_window object
When a rule is in force.
Evaluated against the REQUEST's own time (
input.request.time), not the evaluator's clock, so replaying a recorded request in a dry-run judges it by when it actually happened rather than by when the replay runs.daysis tested against the request's LOCAL day. A range that wraps midnight (from>to) therefore means "beforetoOR afterfrom, on any listed day" — a Friday 22:00-06:00 window is in force Friday 00:00-06:00 and Friday 22:00-24:00, NOT Saturday morning. Say "Friday night into Saturday" with two windows on two rules, or list both days.days string[]Possible values: [
mon,tue,wed,thu,fri,sat,sun],>= 1Days the window applies on, in the window's own timezone.
from stringPossible values: Value must match regular expression
^([01][0-9]|2[0-3]):[0-5][0-9]$Inclusive local start, HH:MM.
to stringPossible values: Value must match regular expression
^([01][0-9]|2[0-3]):[0-5][0-9]$Exclusive local end, HH:MM. 09:00-17:00 does not include 17:00:00.
Equal to
fromis rejected: it reads as both "always" (a full 24h wrap) and "never" (an empty range), so it has to be said a different way.timezone stringIANA name, e.g. "Europe/London". Validated against the tzdata the server carries when the policy is saved. A named zone rather than a fixed offset so the window keeps meaning "office hours" across a daylight-saving transition.
revision int64Monotonic per policy, incremented on every save. Gate's bundle fetch is conditional on this via ETag.
created_at date-timeupdated_at date-time
{
"meta": {
"pagination": {
"limit": 0,
"offset": 0,
"total_count": 0
},
"cursor_pagination": {
"limit": 0,
"next_cursor": "string",
"prev_cursor": "string",
"last_cursor": "string",
"total_count": 0
}
},
"errors": [
{
"httpcode": 0,
"message": "string",
"code": "string"
}
],
"result": {
"enabled": true,
"default_effect": "deny",
"on_missing_context": "deny",
"authored_by": "rules",
"rego": "string",
"rules": [
{
"id": "string",
"description": "string",
"effect": "allow",
"conditions": [
{
"field": "string",
"operator": "in",
"values": [
"string"
]
}
],
"active_window": {
"days": [
"mon"
],
"from": "string",
"to": "string",
"timezone": "string"
}
}
],
"revision": 0,
"created_at": "2005-12-24T18:29:30.033157Z",
"updated_at": "2005-12-24T18:29:30.033157Z"
}
}
The policy does not compile, or the rule set is structurally invalid. The body carries every diagnostic, not just the first, so an editor can mark all offending lines at once.
- application/json
- Schema
- Example (from schema)
Schema
meta object
pagination object
limit integeroffset integertotal_count int64cursor_pagination object
Cursors are opaque. Follow the tokens the server returns; never construct, parse or modify one.
limit integernext_cursor stringOpaque token addressing the next page. Absent on the last page.
prev_cursor stringOpaque token addressing the previous page. Absent on the first page.
last_cursor stringOpaque token addressing the final page directly, so a client can jump to the end without a total count.
total_count int64Total number of matching items. Returned on the FIRST page only; clients cache it for the remainder of the walk.
errors object[]
httpcode integermessage stringcode stringMachine-readable reason, present only for errors that define one. Values today:
unserved_org_region(the organization is homed in a region this deployment does not serve),wrong_region(the request must be sent to the organization's home region),region_route_missing(this instance has no route to the organization's home region).result object
valid booleandiagnostics object[]
Fatal problems. A non-empty list means the policy is rejected.
line integer1-based. Zero when the error carries no position.
column integer1-based. Zero when the error carries no position.
code stringOPA's error code (e.g.
rego_parse_error,rego_unsafe_var_error), or one of this API's own:wrong_package,missing_decision_rule,non_boolean_decision,invalid_rule_set.message stringwarnings string[]Non-fatal problems that would let the policy compile and activate while not doing what its author meant — most importantly, reading the scalar
input.request.mcp.tool, which is empty for batches.rego stringThe generated document, present only when the request supplied
rules. Returned even whenvalidis false, so the builder can show the author what was generated next to what was wrong with it.
{
"meta": {
"pagination": {
"limit": 0,
"offset": 0,
"total_count": 0
},
"cursor_pagination": {
"limit": 0,
"next_cursor": "string",
"prev_cursor": "string",
"last_cursor": "string",
"total_count": 0
}
},
"errors": [
{
"httpcode": 0,
"message": "string",
"code": "string"
}
],
"result": {
"valid": true,
"diagnostics": [
{
"line": 0,
"column": 0,
"code": "string",
"message": "string"
}
],
"warnings": [
"string"
],
"rego": "string"
}
}
Unauthorized
- application/json
- Schema
- Example (from schema)
Schema
meta object
pagination object
limit integeroffset integertotal_count int64cursor_pagination object
Cursors are opaque. Follow the tokens the server returns; never construct, parse or modify one.
limit integernext_cursor stringOpaque token addressing the next page. Absent on the last page.
prev_cursor stringOpaque token addressing the previous page. Absent on the first page.
last_cursor stringOpaque token addressing the final page directly, so a client can jump to the end without a total count.
total_count int64Total number of matching items. Returned on the FIRST page only; clients cache it for the remainder of the walk.
errors object[]
httpcode integermessage stringcode stringMachine-readable reason, present only for errors that define one. Values today:
unserved_org_region(the organization is homed in a region this deployment does not serve),wrong_region(the request must be sent to the organization's home region),region_route_missing(this instance has no route to the organization's home region).
{
"meta": {
"pagination": {
"limit": 0,
"offset": 0,
"total_count": 0
},
"cursor_pagination": {
"limit": 0,
"next_cursor": "string",
"prev_cursor": "string",
"last_cursor": "string",
"total_count": 0
}
},
"errors": [
{
"httpcode": 0,
"message": "string",
"code": "string"
}
]
}
Forbidden
- application/json
- Schema
- Example (from schema)
Schema
meta object
pagination object
limit integeroffset integertotal_count int64cursor_pagination object
Cursors are opaque. Follow the tokens the server returns; never construct, parse or modify one.
limit integernext_cursor stringOpaque token addressing the next page. Absent on the last page.
prev_cursor stringOpaque token addressing the previous page. Absent on the first page.
last_cursor stringOpaque token addressing the final page directly, so a client can jump to the end without a total count.
total_count int64Total number of matching items. Returned on the FIRST page only; clients cache it for the remainder of the walk.
errors object[]
httpcode integermessage stringcode stringMachine-readable reason, present only for errors that define one. Values today:
unserved_org_region(the organization is homed in a region this deployment does not serve),wrong_region(the request must be sent to the organization's home region),region_route_missing(this instance has no route to the organization's home region).
{
"meta": {
"pagination": {
"limit": 0,
"offset": 0,
"total_count": 0
},
"cursor_pagination": {
"limit": 0,
"next_cursor": "string",
"prev_cursor": "string",
"last_cursor": "string",
"total_count": 0
}
},
"errors": [
{
"httpcode": 0,
"message": "string",
"code": "string"
}
]
}