Skip to main content

Replay recent traffic against a draft policy​

Compiles the draft, replays this org's recently recorded requests through it, and reports what would change. Nothing is saved and nothing gate enforces is touched.

"Would this rule have blocked anything real?" is the question an operator actually has before saving, and it is not answerable from the rule text. It IS answerable from traffic that already happened.

Every replayed request lands in one of THREE buckets, not two: allowed, denied, or unevaluable. Recorded telemetry is lossy by design — mcp_visibility suppresses fields, and a recorded batch may carry no per-call pairing — so some requests cannot be scored. Counting those as "would not have matched" would manufacture confidence about exactly the rules the operator is least sure of.

Header Parameters
  • SlashID-OrgID string required

    The organization ID

    Example: af5fbd30-7ce7-4548-8b30-4cd59cb2aba1
Request Body required
  • rules object[] required
  • id string required

    Stable 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 string

    Free text, emitted as a comment above the generated rule.

  • effect string required

    Possible 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: >= 1

  • field string required

    Possible 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 scalar input.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's sub / iss claims.

    resource_uri, prompt — matched over the request's CALLS for the same reason, via input.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, via input.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, via input.request.jsonrpc.is_batch. Its values must 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, via input.request.jsonrpc.unparsable_calls. A COUNT compared as a number, so its values must 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 as unparsable_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 populates input.request.mcp.arguments just 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 an is_batch rule if batches must also be constrained.

    subject, issuer, group, tag, attribute: and claim: describe the CALLER, and each requires gate to have verified the token's signature — an unverified sub is a value the caller chose, so a rule keyed on one would let them select whose permissions they receive. See on_missing_context for what happens when gate has not verified it. The http_* and is_batch fields 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: and claim: 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 required

    Possible 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[] required

    Possible values: >= 1

    Matched 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.

    days is tested against the request's LOCAL day. A range that wraps midnight (from > to) therefore means "before to OR after from, 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[] required

    Possible values: [mon, tue, wed, thu, fri, sat, sun], >= 1

    Days the window applies on, in the window's own timezone.

  • from string required

    Possible values: Value must match regular expression ^([01][0-9]|2[0-3]):[0-5][0-9]$

    Inclusive local start, HH:MM.

  • to string required

    Possible 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 from is 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 required

    IANA 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.

  • default_effect string required

    Possible values: [deny, allow]

    What gate does with an MCP call that no rule matches.

    deny fails closed: a call is allowed only when an allow rule matches it. allow fails 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's default allow line.

  • 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, ignore is 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 to ignore, so their behaviour did not change under them.

  • since date-time

    Replay requests recorded at or after this instant. Defaults to 24 hours ago, and is floored at 30 days ago: limit bounds the rows returned, not the history scanned to find them, so an older instant is silently treated as 30 days ago rather than rejected.

  • limit integer

    Possible values: >= 1 and <= 100

    How many recorded requests to replay. Default 100. Bounded because this compiles a policy and evaluates it once per request.

Responses

The replay completed.


Schema
  • meta object
  • pagination object
  • limit integer
  • offset integer
  • total_count int64
  • cursor_pagination object

    Cursors are opaque. Follow the tokens the server returns; never construct, parse or modify one.

  • limit integer
  • next_cursor string

    Opaque token addressing the next page. Absent on the last page.

  • prev_cursor string

    Opaque token addressing the previous page. Absent on the first page.

  • last_cursor string

    Opaque token addressing the final page directly, so a client can jump to the end without a total count.

  • total_count int64

    Total number of matching items. Returned on the FIRST page only; clients cache it for the remainder of the walk.

  • errors object[]
  • httpcode integer
  • message string
  • code string

    Machine-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
  • evaluated integer

    How many recorded requests were replayed, scoreable or not.

  • allowed integer
  • denied integer
  • unevaluable integer

    Requests the draft could not be scored against. Reported as its own number rather than folded into allowed, because a dry-run whose third bucket is large is telling the operator their sample cannot answer their question.

  • unevaluable_reasons string[]

    Possible values: [batch_call_pairing_unavailable, mcp_not_recorded, tool_not_recorded, subject_unverified, attributes_unavailable, claims_unavailable, time_not_recorded]

    The distinct reasons encountered, so the operator can act on them.

  • changes object[]

    Only requests the draft decides DIFFERENTLY from the saved policy. The full replay is summarised in the counts; this list is what an operator actually reads before saving. Bounded — compare its length against changes_total to detect truncation.

  • request_id string
  • occurred_at date-time
  • subject string
  • method string
  • tools string[]
  • current_decision string

    Possible values: [allow, deny, unknown]

    What the currently SAVED policy decided, replayed the same way.

    unknown when the SAVED policy reads something this recorded request cannot supply — most commonly an attribute or group rule the draft has since dropped. It is reported rather than folded into allow, because assuming "allowed" there both invents a decision gate may never have made and SUPPRESSES the change entry whenever the draft also allows, hiding exactly the requests a draft would newly let through.

  • draft_decision string

    Possible values: [allow, deny]

  • matched_rule_ids string[]

    Which of the draft's rules fired, so a surprise is traceable to a rule.

  • changes_total integer

    How many replayed requests the draft decides differently, IN FULL. changes is capped, so its length saturates and must never be rendered as the number of affected requests: this is the blast radius of saving the draft, and under-reporting it is the dangerous direction. Always >= the length of changes.