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). Bothallowanddenyrecords carry it, because the record describes the call whatever was decided. nullmeans 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_expiredandapproval_retry_*).- Held calls. The hold-outcome rows (
approval_awaiting,approval_approved,approval_rejected,approval_expiredandapproval_retry_*) record the outcome of a human decision about a call, not an execution, and always carrytool_class: null. When an approved call executes, that execution is a separateallowrecord with its class. Two built-in exceptions: an approved built-in that is not an egress tool (for examplecode_interpreter,write_file) has itsapproval_approvedrow as its only record, with no secondallowrow; an approved built-in egress tool (fetch_url,agentic_fetch,web_search) writes its waiverallowrecord (rule_idapproval:approved:<review>) at its own check. A rejected or expired call never ran and so has no classedallowrecord.approval_required(the refusal of a call that needs approval) anduntrusted_egress(the refusal of a pre-approved write or destructive call, or of a built-infetch_url/agentic_fetch/web_searchcall, in a run started by untrusted content) are classeddenyrecords 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_idandtool_classarenullon every one of them, including theallowrecord 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_approvedis 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_approvedand then the normal execution authorization row, which can itself be a deny (for example a tool policy). An approved builtin tool produces only theapproval_approvedrow. - 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
allowwithrule_idtask_envelope:outside_task_approved:<review_id>(the review id is also therule_idof 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_expiredrows hold onlyargs_sha256(args_maskingisunavailable). Theapproval_awaitingrow of the samerule_idholds 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_expiredrecord. 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_idandroot_agent_idand under the samerule_idas 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'sapproval_awaitingrows 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) oragent_constraint_malformed(unreadable stored value). A held call is recorded asapproval_awaiting;agent_constraint_writeandagent_constraint_egressarehold_reasonvalues on the review only, never decision reasons.rule_idisagent_constraint:<trigger>:<action>:<tool>. Organisation denies (consent, envelope refusal, tool policy, argument rules) answer first. After that, an agentneverbeats every hold:approval_required,untrusted_egress, the owner tool gate,outside_taskescalation 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 countagent_constraintoragent_constraint_malformedrecords. - 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_malformedoragent_protection;approval_pending_capandapproval_retry_*still count (see Automatic deny-burst detection). A separate signal countsapproval_rejectedandapproval_expiredrecords 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_sha256of 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:
denyapproval_required(the refusal) anddenyapproval_awaiting(the hold, withrule_id= the approval id). Theapproval_awaitingrow is best effort. - The retry that runs writes
approval_approvedbefore the tool runs, then the normal execution row. Ifapproval_approvedcannot be written the call does not run. - Every refused retry writes one
approval_retry_*row. Polling a pending approval withparams._metawrites oneapproval_requiredrow 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
denyapproval_requiredrow (no new hold). After approval the repeat writesdenyapproval_required, thenallowapproval_approved, then the normal execution row (allowdefault_allow); it runs once. A call refused by the per-token (per-grant) cap writesdenyapproval_requiredand then onedenyapproval_pending_caprow, and no hold. - SIEM:
approval_retry_mismatchandapproval_retry_unknownare 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 otherapproval_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 |