Troubleshooting and FAQ
When someone is refused, the fastest way to find out why is the decision id. This page shows what a refusal looks like in each API dialect, how to replay it in the IAM center, what 403 and 503 mean, and answers the questions that come up most often.
Requires a Console release with the IAM engine
Decision ids, the Access check and the authorization_unavailable behaviour are part of the IAM engine.
Decision ids
Every refusal carries a decision id, so that the person who was refused and the person who investigates talk about the same event:
- a
403from the permission gate hasdecisionIdin the body and anX-Decision-Idheader; OpenAI- and Anthropic-compatible routes put it inerror.decision_id; - a refusal to act for a user (401, 403 or 503) carries one too, in the same places;
- a refusal at a portal shows the person a reference code, which is a decision id;
- paste it into the IAM center (Settings → IAM center → Access check → Look up a denied request), or call
GET /api/iam/decisions/{decisionId}: you get the audit row and a ready-to-run access check that reproduces the decision, the policies that decided it by name (with whether each is still in force) and, for a portal refusal, the action, the portal or agent and the channel; - a decision resolves while its audit row is in the 30-day window the Denials feed looks at.
What a refusal looks like
{
"error": "Forbidden: mcp requires write permission",
"message": "Forbidden: mcp requires write permission",
"code": "forbidden",
"service": "mcp",
"required": "write",
"decisionId": "7c0f6b2e-5d1a-4c1e-9a53-0d9b8a1f2e44"
}{
"error": {
"message": "Forbidden: mcp requires write permission",
"type": "permission_error",
"code": "forbidden",
"decision_id": "7c0f6b2e-5d1a-4c1e-9a53-0d9b8a1f2e44"
}
}{
"type": "error",
"error": {
"type": "permission_error",
"message": "Forbidden: mcp requires write permission",
"decision_id": "7c0f6b2e-5d1a-4c1e-9a53-0d9b8a1f2e44"
}
}error is the human-readable reason, so a client that only shows body.error shows the reason.
Refusal codes
| Status | Code | Meaning |
|---|---|---|
| 403 | forbidden | The caller's effective level on the service is below what the call needs. Carries service, required and decisionId. |
| 403 | route_not_declared | The route declares no authorization. A healthy build has none; GET /api/iam/engine/stats lists them. |
| 403 | session_forbidden | A session was started by someone else and the caller may not view or change it. See agent sessions. |
| 403 | action_forbidden | The caller may not answer a pending tool approval or question. See approvals. |
| 403 | files_write_required | Naming the bucket of an agent's session files needs write access to that bucket. |
| 403 | permit_requires_admin | An enabled allow policy can only be saved by an owner or admin. |
| 403 | ip_not_allowed | The token has an IP allowlist and the call came from another address. Written to the audit log. |
| 403 | delegation_refused, project_not_accessible | See acting for a user. |
| 401 | delegation_not_enabled, invalid_assertion, expired, untrusted_key, unknown_principal, disabled, seat_limit, conflict | See acting for a user. |
| 503 | authorization_unavailable | The engine could not decide. Not a permission error: retry. |
| 503 | delegation_unavailable | The stores behind an acting-for-a-user call could not answer. Retry. |
When authorization is unavailable
If the engine cannot decide a request (the person's data cannot be read, the engine fails), the request is refused, never allowed and never decided by some other rule set:
HTTP/1.1 503 Service Unavailable
Retry-After: 5
X-Decision-Id: 0b6f2c3e-...
{ "error": "Authorization is temporarily unavailable", "code": "authorization_unavailable", "decisionId": "0b6f2c3e-..." }OpenAI-compatible routes answer with type: "server_error", code: "authorization_unavailable" and decision_id; Anthropic-compatible routes with type: "api_error" and decision_id. A 503 is not a permission error: clients that retry on 503 (the Console SDK does, and honours Retry-After) simply try again. It is counted, logged and written to the audit log (outcome denied, metadata.code: "authorization_unavailable", also for GET), so it shows up in the Denials feed and resolves through its decision id.
Three safeguards keep that rare:
- The server will not start without a working engine. At boot the engine loads Cedar, compiles its schema and the built-in policies and evaluates a few canned decisions. If any step fails the process exits with an explanation instead of serving requests it cannot decide, so a broken installation shows up when you deploy it, not as failing requests.
- A broken custom statement does not take the workspace down. A statement that no longer loads is skipped and reported (the engine is
degraded); the rest of the policy set applies. - Readiness reports it.
GET /api/health/readyhas anauthorization-enginecheck:ok,degraded(requests were refused recently, or statements were skipped) ordown(the canned decisions fail; the endpoint answers503). If your orchestrator probes only the TCP port, point the readiness probe at this endpoint. Counters and the policy set of the workspace are inGET /api/iam/engine/stats.
The Access check
The Access check tab (Settings → IAM center → Access check) answers "can this person or token do this, and which rule decides?". Choose a person or token, an action (a service level) and, if you want, a project, channel, IP address and time. The result says Allowed or Denied and names the deciding rule: a built-in one (a role, a project limit, a token scope) or one of your policies. It does not change anything, so people who may only view the IAM center can use it, and nothing is recorded: a simulation is not traffic.
Extra inputs appear when the action needs them:
| For | The form also asks |
|---|---|
A portal action in the portal channel | Through portal; a group can be the principal ("View as") |
agent.approve, agent.session_view_others, agent.session_manage_others | Session started by; for approvals also The agent accepts as approvers |
| A rule with an IP condition | IP address |
| A rule with a validity window | At |
The Access check API
POST /api/iam/simulate (needs read on iam; at most 120 calls a minute per person):
curl -X POST https://console.example.com/api/iam/simulate \
-H "Content-Type: application/json" --cookie "token=..." \
-d '{
"principal": { "type": "user", "id": "<user id>" },
"action": "models.write",
"resource": { "kind": "service", "service": "models" },
"context": { "projectId": "<project id>", "channel": "dashboard" }
}'principal:{ "type": "user" | "token" | "group", "id" }. A group principal is accepted only for the portal actions in theportalchannel.resource:{ "kind": "service", "service" },{ "kind": "resource", "type", "id" }or{ "kind": "project", "projectId" }.context:projectId,channel(dashboard,api,portal),ip,at(ISO timestamp), and for the portal and session actionsportalId,embedded,test,ownerIdandapprovers(owner,owner_or_admin,project_members).
The answer:
{
"allowed": false,
"reasons": [
{ "cedarId": "custom:freeze-model-hub", "policyId": "freeze-model-hub", "managed": false, "name": "Freeze Model Hub for contractors", "effect": "forbid" }
],
"defaultDeny": false,
"principalSummary": { "type": "user", "id": "…", "label": "Ann Lee", "role": "user", "groups": [] },
"projectId": "…",
"enforcedAtGate": true
}defaultDeny is true when nothing allowed the request (as opposed to a rule forbidding it). enforcedAtGate is false for a check about an individual resource, where the answer is what the policies say rather than what a real request would meet. session and portal describe what a session or portal check assumed.
The audit trail
IAM center → Audit lists denials and policy changes, with CSV export (GET /api/iam/audit and GET /api/iam/audit/export, kind=denials or kind=changes). A person's drawer under Principals shows their recent Activity. The Overview tab shows the latest denials and anything that needs attention.
FAQ
Do I have to write policies? No. Roles, group grants, project permissions and token scopes already decide everything. Policies are for exceptions.
Can I turn the engine off? No. There is one decision path. That is also why the engine is checked at boot.
A member lost access after the upgrade. Open their decision id in the Access check, or look at the person under Principals: the effective level of each service is there, with where it comes from (the role, a direct grant, a group, a token scope, a project limit or a policy). Most often it is a project permission (see Project permissions).
A person can open a service in one project but not another. Project service permissions cap the workspace level inside a project. Open the project's Members → Project service permissions and look at the service: No access caps it at none there.
An API token gets 403 but its owner can do the call. The token's scope is a cap: a service that is not in a scoped token's scope resolves to none, and {} allows nothing. Also check the token's project limits.
A token can do more than I expected. A token takes its owner's group grants too. Replace it with a scoped token, or take the owner out of the group.
Why does a deny rule not stop writes to an administration service? Changes there are checked as Write and need Admin on top, so deny Write to block changes.
IP rules never match behind my load balancer. Set TRUSTED_PROXIES (see Client address and TRUSTED_PROXIES).
The server does not start: "Authorization engine self-test failed". The message says which step failed (loading Cedar, the schema, the built-in policies or a canned decision). Check that the installation has its production dependencies (@cedar-policy/cedar-wasm), that the image is the one built for this release, and the log lines before the message.
I locked everybody out with a policy. Sign in as the workspace owner and disable or delete the rule. See Owner safety.
Is a background job restricted by a policy? No. Scheduled work runs without a principal; a policy restricts the person who starts something. See What policies do not govern.

