Reason and error codes

Every refusal in SharedOS is a code, not a thrown exception. This page is what each one means and what to do about it.

Denied is not failed

Three statuses appear across ToolResult, ResourceResult, MessageDeliveryResult, and ExecutionResult:

StatusMeaningRetry?
succeededIt happened
deniedAuthorization refused it. Nothing ranNo — change the grant
failedIt was allowed, and something broke while doing itMaybe — check retryable

ExecutionResult adds cancelled for a deadline or host cancellation.

Over HTTP all four are 200. A 403 means the request never reached the kernel's decision. Client code that only checks the HTTP status will read denials as successes.

Authorization reason codes

AuthorizationDecision.reasonCode, and the reason field on authorization.checked audit events.

CodeMeansFix
allowedA grant matched
no_matching_grantNothing in context.grants covers this resource and actionSee the checklist below
grant_exhaustedA matching grant exists but its maxUses is spentIssue a new grant; usage is not resettable
invalid_contextThe AccessContext failed its schemaA host bug. Build the context server-side
invalid_requestThe resource or action failed its schemaCheck path segments and action naming
usage_store_unavailableThe grant has maxUses and there is no usageStore, or it threwSupply CapabilityAuthorizer({ usageStore })
delegation_chain_unavailableThe grant was derived and there is no chainResolverSupply CapabilityAuthorizer({ chainResolver })
delegation_chain_brokenAn ancestor is missing, revoked, or expiredWorking as intended — upstream authority ended

When you get no_matching_grant and expected otherwise

Walk these in order. Every one of them produces the identical code.

  1. context.authority does not equal grant.issuer. The most common cause. authority is whose grants are being exercised, not who owns the data. For a grant Alice issued it is Alice; for a grant Bob derived from it, it is Bob.
  2. context.actor does not equal grant.subject. The grant was issued to someone else.
  3. context.purpose is not in constraints.purposes. Purpose is matched exactly, not by prefix.
  4. context.now is outside notBefore / expiresAt.
  5. namespaceId differs. Grants never cross worlds or tenants.
  6. The path is not covered. scope: "exact" matches only that path. scope: "descendants" matches the path and below — and segments are compared as segments, so cell-3 never covers cell-30.
  7. The action is not listed. actions has no wildcard expansion.
  8. A grantVerifier returned false or threw. A throw is treated as false.
  9. The capability is spread across grants. One requirement must be satisfied by one grant. Path from one and action from another is refused deliberately.

tool_unavailable covers three different situations

kernel.invokeTool returns denied with tool_unavailable — and the same message — when the tool is not registered for this context, when its namespace is disabled, and when no grant makes it discoverable. That is deliberate: the caller learns it cannot use the tool, not which of the three reasons applies.

The specific reason is in the audit trail. An authorization.checked event is recorded immediately before, carrying the real reason code:

authorization.checked  denied  files/Work/Finance  <- grant_exhausted
tool.invoked           denied  files.read         <- tool_unavailable

If you are debugging a tool_unavailable and have no audit sink wired, wire one first.

Tool invocation

CodeStatusMeans
tool_unavailabledeniedNot registered, namespace off, or not discoverable — see above
tool_not_availabledeniedA runtime asked for a tool outside its permission-filtered turn
no_matching_grantdeniedThe exact argument-selected resource is not authorized
invalid_tool_argumentsfailedparseArguments rejected the call
invalid_tool_requirementfailedresolveRequirement returned something outside the declared ceiling
tool_requirement_resolution_failedfailedresolveRequirement threw
tool_catalog_unavailablefailedA ContextToolProvider threw. The catalog is never partially returned
tool_execution_failedfailedYour invoke threw
invalid_tool_resultfailedYour handler returned something that is not a ToolResult
trace_mismatchfailedcall.traceId does not match the context

Resources

CodeStatusMeans
resource_provider_not_foundfailedNo provider registered for that namespace
resource_execution_failedfailedYour provider threw
invalid_resource_resultfailedYour provider returned a malformed ResourceResult, or one whose operationId does not match

Messages

CodeStatusMeans
message_transport_not_configuredfailedNo messageTransport was supplied to the kernel
message_context_mismatchfailedThe envelope disagrees with the context
receiver_mismatchfailedThe delivered receiver is not the addressed one
message_requirement_resolution_failedfailedThe capability resolver threw
message_delivery_failedfailedYour transport threw
invalid_message_receiptfailedYour transport returned a malformed delivery result

Turns

CodeStatusMeans
actor_mismatchdeniedThe turn's agent is not the admitted one
no_matching_grantdeniedNo sharedos.execution / invoke grant for the target agent
step_limit_exceededfailedStandardRuntime hit its driver step budget
tool_call_limit_exceededfailedThe envelope's maxToolCalls was reached
driver_failedfailedYour AgentTurnDriver threw
invalid_driver_decisionfailedThe driver returned something that is not a valid decision
runtime_failedfailedA RuntimePlugin threw
invalid_runtime_outcomefailedA plugin returned a malformed outcome
turn_cancelledcancelledDeadline expired, or the host aborted

HTTP

StatusCodeMeans
400invalid_jsonBody is not JSON
400invalid_requestBody does not match the v1 contract
403permission_deniedAn error carrying that code reached the handler
404not_foundUnknown path
405method_not_allowedWrong verb
500invalid_access_contextresolveContext returned an invalid context
500internal_errorAnything else; details never leak

Delegation refusals

deriveGrant returns { ok: false, reason } rather than clamping — a silently narrowed delegation reads as accepted, and the delegator then believes it passed on more than it did.

ReasonMeans
namespace_mismatchThe child names a different world
issuer_is_not_the_holderOnly the parent's subject may pass it on
parent_not_delegableThe parent has no delegationDepth, or it is already zero
depth_exhaustedThe child asked for a longer chain than was received
capability_not_within_parentWider or sibling path, an unheld action, an exact parent widened into a subtree, or one child capability assembled from several parent ones
purpose_not_within_parentA purpose the parent does not carry
window_not_within_parentA validity window outside the parent's
bounded_parent_not_delegableA maxUses parent. Sharing one budget across a chain needs cross-grant accounting, so it is refused rather than multiplied
empty_capabilitiesNothing was actually delegated

Execution events

ExecutionResult.events, append-only and ordered by sequence.

TypeWhen
turn.startedAdmission passed; the runtime is about to run
tool.requestedThe runtime asked for a call, before authorization
tool.completedAny outcome — succeeded, denied, or failed
runtime.eventA plugin's own event, wrapped rather than trusted
turn.completedThe runtime finished
turn.failedThe runtime or driver failed
turn.deniedAdmission or context validation refused the turn
turn.cancelledDeadline expired or the host cancelled

Audit events

TypeOutcomes
authorization.checkedallowed, denied
resource.invokedsucceeded, denied, failed
tool.invokedsucceeded, denied, failed
tool.catalog.listedsucceeded
tool.namespace.catalog.listedsucceeded
tool.namespace.selection.updatedsucceeded, failed
message.sentsucceeded, denied, failed

Every event carries version, type, outcome, at, traceId, namespaceId, actor, authority, owner, purpose, and where applicable resource, action, grantId, operationId, tool, messageId, receiver, reason, and metadata.

Wire onAuditError to alerting. A dropped audit write must not pass silently — it is the only record that separates "was allowed to" from "did it and nobody stopped it".

Contract limits

Rejected by the schemas, so they hold identically on both boundaries.

LimitValueLimitValue
Turn timeout≤ 600,000 msTool calls per turn≤ 10,000
Steps per turn≤ 1,000Tools per request≤ 512
Path segments≤ 64Segment length≤ 256 chars
Capabilities per grant≤ 64Actions per capability≤ 64
Purposes per grant≤ 64Purpose length≤ 512 chars
Delegation chain≤ 16Namespaces per catalog≤ 256
Search query / grep pattern≤ 8,192 charsSearch results≤ 100
Grep context≤ 100 lines per sideTool description≤ 8,192 chars
Capsule encoded≤ 128 KBCapsule item content≤ 96 KB, ≤ 12 items

Path segments additionally reject separators, traversal markers, and control characters. A filesystem-backed provider must still resolve beneath its own root and reject link escapes — the contract cannot see your disk.