Skip to content

Access decisions API

Every connector tool call is authorized by the gateway access layer and recorded, allowed or refused (see Access layer refusals). These endpoints read those records. All are GET, read-only, and scoped to one tenant.

Endpoint Who may call it What it returns
GET /admin/v1/gateways/{gw}/agents/{agent_id}/access-decisions The agent's creator, or a tenant admin The records of one agent
GET /admin/v1/access-decisions Holders of the REQUEST_LOGS_VIEW permission The tenant's records
GET /admin/v1/me/access-decisions Any signed-in user (not available under "View as User") Records where you are the owner or the acting user
GET /admin/v1/access-decisions/export REQUEST_LOGS_VIEW The tenant's records as CSV
GET /admin/v1/access-decisions/facets REQUEST_LOGS_VIEW The agents, people and connectors present in a time window, for filter pickers
GET /admin/v1/access-decisions/{id} See Single record One record, including its masked arguments

Platform admins pass tenant_id to the tenant-wide endpoints; for everyone else the tenant is always the caller's own.

Listing

The three list endpoints return newest first (ts_ms descending, then id descending):

{ "items": [ { "id": "12", "ts_ms": 1792367240289, "decision": "allow", "reason": "default_allow",
               "actor_kind": "agent", "agent_id": "...", "owner_id": "...", "tool": "search", "tool_class": "read",
               "args_masking": "masked", "args_sha256": "..." } ],
  "next_cursor": "1792367240289_12" }

next_cursor is null on the last page; pass it back as cursor to get the next page. Each item carries id (decimal string), ts_ms, decision, reason, rule_id, actor_kind, interactive (boolean), agent_id, agent_name, root_agent_id, owner_id, owner_name, acting_user_id, acting_user_name, connector_id, connector_name, tool, tool_class, request_id, gateway_id, args_masking, args_sha256 and agent_run_id; absent values are null. agent_run_id is the run that all records of a paused-and-resumed agent run share (each leg has its own request_id); it is null for records written before this field existed and for calls that are not part of an agent run, and those keep their request_id as the only link. Items never contain the tenant id or the tool arguments; use the single-record endpoint for the arguments.

Query parameter Meaning
decision allow or deny
agent_id, connector_id Restrict to one agent / connector (the agent endpoint takes agent_id from its path and rejects the query key)
user_id Tenant-wide and export only: records where this user is the owner or the acting user
from_ms, to_ms Inclusive time window, epoch milliseconds; from_ms must not be after to_ms
cursor The next_cursor of the previous page
limit 1 to 200, default 50; larger values are clamped to 200. A non-integer or 0 is 400 with code invalid_limit
tenant_id Tenant-wide, export and facets only; used by platform admins (see above)

Anything malformed is answered with 400 and {"error": "...", "code": "bad_param"}: an unknown or repeated parameter, an empty value, a bad decision, an id that is not 1 to 36 letters, digits or hyphens, a timestamp that is not 1 to 15 digits, or a cursor that is not <ts_ms>_<id>. A malformed value is never ignored; a non-admin's tenant_id is always replaced by the caller's own. An empty result is 200 with "items": [].

Tool class

Each record of a connector tool call made through the authorization step carries tool_class: read, write, destructive or null. It is the class the tool had at the moment of the decision, so changing a tool's class later never rewrites history. The CSV export has the same tool_class column, right after tool.

  • How it is resolved. An entry an admin set for the tool wins; otherwise the connector catalog's class applies; a tool that is unlisted, or whose class setting cannot be read, is destructive (fail closed). Both allow and deny records carry it, because the record describes the call whatever was decided.
  • null means no class is recorded: records written before this field existed, tools that are not connector tools, refusals recorded outside the authorization step (agent_capability, pii_egress_blocked, tool_not_offered), even when they name a connector, and the hold-outcome rows below (approval_awaiting, approval_approved, approval_rejected, approval_expired and approval_retry_*).
  • Held calls. The hold-outcome rows (approval_awaiting, approval_approved, approval_rejected, approval_expired and approval_retry_*) record the outcome of a human decision about a call, not an execution, and always carry tool_class: null. When an approved call executes, that execution is a separate allow record with its class. Two built-in exceptions: an approved built-in that is not an egress tool (for example code_interpreter, write_file) has its approval_approved row as its only record, with no second allow row; an approved built-in egress tool (fetch_url, agentic_fetch, web_search) writes its waiver allow record (rule_id approval:approved:<review>) at its own check. A rejected or expired call never ran and so has no classed allow record. approval_required (the refusal of a call that needs approval) and untrusted_egress (the refusal of a pre-approved write or destructive call, or of a built-in fetch_url / agentic_fetch / web_search call, in a run started by untrusted content) are classed deny records on every path: before the hold through the MCP proxy, and for an unattended agent call that cannot be held (for example an invoke with no run attached), where it is the only record.
  • Built-in tools. Records for the built-in egress tools (fetch_url, agentic_fetch, web_search) held or refused on an untrusted run have no connector and no class: connector_id and tool_class are null on every one of them, including the allow record of an approved call.
  • SIEM. The SIEM event does not carry tool_class; read it from this API or the CSV export.

Reason codes

reason says why a record was allowed or refused. decision is always allow or deny; for held tool calls the outcome is carried in reason.

reason decision Meaning
default_allow allow No rule refused the call. On an agent run this also records every allowed built-in tool call (for example read_file, write_file, search_knowledge, web_search, fetch_url; the row's connector_id and tool_class are empty), written before the call runs; when the row cannot be written, an unattended run refuses the call with record_write_failed, a Try it run proceeds (an internal error of the recorder itself refuses the call on every run, Try it included). Not recorded: chat turns, search_connector_tools, calls to tools that were not offered to the agent, and duplicate calls that were dropped. A resumed approved non-egress built-in call keeps its approval_approved row and writes no second default_allow row (an approved egress built-in writes its waiver allow record instead, see Held calls). The arguments of a built-in call (write_file content, code_interpreter code and so on) are stored masked, size-capped and erasable exactly like connector arguments
provider_native_search allow The model provider ran its own web search inside the model call (Anthropic web search tool, Gemini or Vertex Google Search grounding) on an agent run. The gateway only offers that search for the call; the provider chooses and runs the queries, so the gateway cannot allow or block each search and records it after the response shows that a search ran. One row per model call that searched, however many queries it made; no query text is stored (args_masking is withheld). The row's tool is web_search, its rule_id native_search:<arm> (anthropic_server_tool, gemini_grounding). A model call whose response does not show a search leaves no row
actor_invalid deny The caller could not be identified
tenant_mismatch deny The call belongs to a different organisation
agent_token_invalid deny The agent credential is invalid
consent_missing deny No consent was given
consent_tool_not_granted deny The consent does not cover this tool
tool_policy deny A tool policy blocked the call
record_write_failed deny The decision could not be recorded, so the call was refused
layer_error deny Internal error while checking access
agent_capability deny The agent is not allowed to use this tool
pii_egress_blocked deny Personal data would have left the gateway
inherent_search deny A model that searches the web on every call was refused on a run that had processed outside content, and no non-searching sibling or search-off switch exists, and the run cannot be held for approval (a held call is recorded as approval_awaiting instead). The row's tool is model:<id>. Nothing was sent to the provider
native_grounding deny A Gemini or Vertex call whose Google Search grounding would be injected was refused on a run that had processed outside content because a hold for approval was attempted and failed, or the client had already disconnected (a held call is recorded as approval_awaiting instead). A call that cannot be held at all leaves no row; it is sent without grounding. The row's tool is model:<id>. Nothing was sent to the provider
agent_protection deny A model call refused by the agent's protection: an external model for a protected agent, or an external model on a gateway that does not mask personal data for a masked agent. One row per refused call (the agent's model, each fallback, a model call after a tool result, an internal call such as the agentic-fetch summary). The row's tool is model:<id>, its rule_id agent_protection:<protection>:<provider>. Nothing was sent to the provider. Not counted by the deny-burst detector (an owner's setting working as designed)
approval_awaiting deny The run was suspended and the hold was created. The tool has not run
approval_approved allow The holder's approval took effect when the run resumed. It is not proof that the tool ran, except for an approved non-egress built-in call, where it is the only record of the execution
approval_rejected deny The holder denied the hold. The tool never runs
approval_expired deny The hold passed its deadline undecided. The tool never runs
approval_retry_rejected deny A proxy retry named a hold the holder rejected
approval_retry_expired deny A proxy retry named a hold past its window (or whose stored call was already cleared)
approval_retry_used deny A proxy retry named a hold that already ran (also an id-less retry that lost the race for the same approval to an identical id-less call)
approval_retry_mismatch deny A proxy retry differs from the held call (token, tool or arguments) or belongs to another agent
approval_retry_unknown deny A proxy retry named an id that does not exist for this tenant
project_lock deny A proxy call was refused because the project is locked
egress_blocked deny The connector's outbound tool calls are blocked by policy. Nothing was sent
connector_url_blocked deny A member-owned connector's address points to a private or blocked network, so the call was not sent. A name that does not resolve, and an address that cannot be parsed (for example one with user information before the host), is not recorded here: the call is still not sent and the caller gets an upstream failure
approval_pending_cap deny A proxy call without an approval id was refused because the token (for an OAuth grant token: the grant) already has the maximum number of open pending holds (mcp_proxy_max_pending_holds). No hold was created

Held tool calls (approvals)

A tool call that an agent's approval gate holds for a human is recorded once per held call and phase. rule_id is the id of the review the hold belongs to, so the rows of one hold are found by that id. For approval_approved and approval_rejected the acting_user_id is the person who decided.

  • Approved calls. approval_approved is written when the run resumes, before the tool runs; it records that the decision took effect, not that the tool executed. An approved MCP call produces two rows: approval_approved and then the normal execution authorization row, which can itself be a deny (for example a tool policy). An approved builtin tool produces only the approval_approved row.
  • Outside-task approvals. When the second row of an approved MCP call was held because it was outside the run's task envelope, it is an allow with rule_id task_envelope:outside_task_approved:<review_id> (the review id is also the rule_id of the hold's own rows). Only the outside-task verdict is waived; a tool policy, parameter rule or consent deny still applies.
  • Fail closed. An approved call whose record cannot be written does not run: the agent is told it "could not be recorded in the access decision record and was not run". The awaiting and expired records are best effort; a failed write is logged and the hold or expiry stands.
  • Expired rows carry no arguments. The approval_expired rows hold only args_sha256 (args_masking is unavailable). The approval_awaiting row of the same rule_id holds the masked arguments.
  • Expiry gap. Expiry records are written by the expiry sweep after the review is marked expired. If the gateway stops in between, the review is expired but has no approval_expired record. The review item stays authoritative, and the gap is on the deny side.
  • Held calls inside a sub-agent. A sub-agent's held call is recorded with the sub-agent's own agent_id and root_agent_id and under the same rule_id as the parent hold. Rejection and expiry write one row per nested call. If the sub-agent's stored state cannot be read, no rows are written for its held calls on reject or expiry, and a warning with the review id is logged. Approving a nested-held call resumes the sub-agent: the held call runs once, in the sub-agent's own turn, under the same live checks as any approved call, and its result goes back to the calling agent. Calls the sub-agent had already executed before the hold are never run again, and calls after it in the same batch were not run. If the sub-agent can no longer be resumed (its stored state cannot be read, or it is no longer allowed to run), the held action is not run and the calling agent is told so; no terminal row is written in that case either, so the sub-agent's approval_awaiting rows stay open (the expiry gap above).
  • Agent constraints. A call refused or held by an agent's safety constraint is a classed record with reason agent_constraint (refused: never, or a hold that cannot be held here) or agent_constraint_malformed (unreadable stored value). A held call is recorded as approval_awaiting; agent_constraint_write and agent_constraint_egress are hold_reason values on the review only, never decision reasons. rule_id is agent_constraint:<trigger>:<action>:<tool>. Organisation denies (consent, envelope refusal, tool policy, argument rules) answer first. After that, an agent never beats every hold: approval_required, untrusted_egress, the owner tool gate, outside_task escalation and the constraint holds. The call is refused and no approval is created. Model-call search-grounding holds are outside agent-constraint scope and are unchanged. The deny-burst detector does not count agent_constraint or agent_constraint_malformed records.
  • Repeated rejections pause the agent. The deny-burst detector does not count approval_required, approval_awaiting, untrusted_egress, approval_rejected, approval_expired, project_lock, egress_blocked, connector_url_blocked, agent_paused, agent_constraint, agent_constraint_malformed or agent_protection; approval_pending_cap and approval_retry_* still count (see Automatic deny-burst detection). A separate signal counts approval_rejected and approval_expired records of one stored agent, once per review (rule_id), and pauses the agent at the tenant's threshold (see Automatic repeated-rejection detection). An expired review whose record was lost (the expiry gap above) is not counted.
  • Sweep cap. One sweep expires at most 200 reviews. A larger backlog is expired, and its records written, over the following sweeps.

Argument hashes of large integers

args_sha256 is computed over a canonical encoding of the arguments. An integral argument from 2^53 up to 2^63 (for example a 16-digit card or account number) used to be hashed in exponent form (9.79212341234123e+15) and is now encoded as its exact digits (9792123412341230). Consequences of this one-time change:

  • args_sha256 of a call with such an argument differs from the value recorded before the change. Earlier records are not rewritten.
  • A held call through the MCP proxy that was held before the change and is retried afterwards no longer matches the stored arguments. It fails closed and is held again; this lasts at most one hold lifetime.
  • The cache key and the content hash of untrusted content over a body with such a number change once; the next request is a cache miss.
  • MCP servers and other egress consumers receive a whole number whose magnitude is 1e14 or more but below 2^63 as plain digits instead of an exponent-form float; one of magnitude 2^63 or more keeps exponent form (1e+19).
  • In a request body, an integer literal a 64-bit double cannot hold exactly (such as 9007199254740993) or beyond 2^63, or a number with more than 17 significant digits, is refused (number_not_exact); in a held call's arguments, such text is hashed as the raw argument string. The gateway's own canonical encoding always passes this check. An accepted value of magnitude 2^63 or more keeps its exponent form, so its hash does not change (see Numbers in the request body).

Earlier, a subnormal float argument (below about 2.2e-308) also changed its hash once, when the canonical encoding switched to the shortest round-trip text.

Held calls through the MCP proxy

A write call through the MCP proxy is held without a run. These holds have no run, so no agent run resumes; the client retries instead.

  • A held call writes two rows: deny approval_required (the refusal) and deny approval_awaiting (the hold, with rule_id = the approval id). The approval_awaiting row is best effort.
  • The retry that runs writes approval_approved before the tool runs, then the normal execution row. If approval_approved cannot be written the call does not run.
  • Every refused retry writes one approval_retry_* row. Polling a pending approval with params._meta writes one approval_required row per poll.
  • A client that cannot send an approval id repeats the identical call. While the hold is pending, each repeat is refused by the gate and writes one deny approval_required row (no new hold). After approval the repeat writes deny approval_required, then allow approval_approved, then the normal execution row (allow default_allow); it runs once. A call refused by the per-token (per-grant) cap writes deny approval_required and then one deny approval_pending_cap row, and no hold.
  • SIEM: approval_retry_mismatch and approval_retry_unknown are not routine workflow. They mean a retry with another token, other arguments or a guessed id, so alert on them rather than excluding them with the other approval_ reasons.

SIEM

The default SIEM filter forwards denies only, so approval_awaiting, approval_rejected and approval_expired are streamed; approval_approved is not (unless the destination opts in to all access decisions). These rows are expected workflow, not attacks: a deny-rate detector should exclude reasons starting with approval_.

Filter choices

GET /admin/v1/access-decisions/facets returns {"agents": [...], "people": [...], "connectors": [...], "truncated": false}; each item is {"id": "...", "name": "..." | null}. It accepts only from_ms, to_ms and, for platform admins, tenant_id (any other or repeated key is 400). people is the union of owners and acting users. The result is computed from the most recent 20,000 records of the window and capped at 200 entries per list; truncated is true when either bound cut the result. Only your own organisation's records are considered.

Single record

GET /admin/v1/access-decisions/{id} returns the same fields plus args_masked (the masked arguments, when stored) and args_masking. {id} is a positive decimal number of at most 15 digits. A record that does not exist, a malformed id, a record you may not read, and a record or caller without an organisation are all answered 404 {"error":"not_found"}, so a caller cannot probe which ids exist. You may read a record of your own tenant if you hold REQUEST_LOGS_VIEW, are its owner or acting user, or created the agent concerned. Platform admins may read any record.

CSV export

GET /admin/v1/access-decisions/export accepts the filters above except cursor and limit, and streams text/csv (CRLF line endings, header row first, Content-Disposition: attachment) and is sent with Cache-Control: no-store. Cells that start with a formula character are escaped. When to_ms is omitted the window ends at the request start; rows recorded during the export with an earlier timestamp may be missing. An export is capped at 50,000 rows: a larger match is refused before any byte is sent with 422 and code export_too_large, with max_rows: 50000 in the body (narrow the time window). A store failure after the first byte aborts the connection, so a truncated file never looks complete. Columns, in order: id, ts_ms, time_utc (ISO 8601 with milliseconds), decision, reason, rule_id, actor_kind, interactive (true/false), agent_id, agent_name, root_agent_id, owner_id, owner_name, acting_user_id, acting_user_name, connector_id, connector_name, tool, tool_class, request_id, gateway_id, args_masking, args_masked, args_sha256, agent_run_id (appended last; the earlier column positions are unchanged). The SIEM export is unchanged.

After an organisation is purged

When an organisation is purged, its decisions younger than 730 days are kept as audit evidence with the masked arguments (args_masked) erased (the argument hash args_sha256 is kept, the sealed audit mirror binds it; owner and acting user stay as pseudonymous ids), and deleted at 730 days. They are no longer returned by the list, the single record, the facets or the export, because the organisation no longer exists.

Errors

Status Meaning
400 Malformed query (see above), or tenant_id missing/invalid for a platform admin
401 Not signed in
403 Missing permission (list, export, facets, per-agent); a gateway you may not reach (per-agent list); impersonation_forbidden while an admin is impersonating (single record and /me list); feature_disabled on the per-agent list when the tenant has agents disabled
404 Record not found or not readable; per-agent list: agent not found or not created by the caller (indistinguishable by design)
422 export_too_large
503 The store could not be read. This is never reported as an empty list