Three outcomes, not two
A governed call resolves one of three ways. Only one of them throws.
A park is success-with-deferral. It does not throw, and reading its result as if the work
had happened is the single most common governance bug.
asDecision folds on the body discriminator (status: "pending_approval"), not the HTTP
status — Http.request discards the status code, so 202-vs-200 is not observable from this
client at all. The body discriminator is the only signal that survives the transport, and the
API sends it on every gated route.For a boolean check without the fold, isPendingApproval(res) is exported too. See
Errors and Approvals.brain.forget is the sharp case
BrainHandle.forget returns a Decision<T> precisely so those two cannot be
confused. The legacy BrainClient.forget returns Promise<unknown> and a resolved promise
there reads as “it happened” — it does not.
The denial vocabulary
NaiveError.code stays typed as string — narrowing it would break every existing
e.code === "…" comparison in customer code. The closed union ships alongside it:
NAIVE_ERROR_CODES, by family:
- Governance —
capability_denied,approval_required,approval_expired,budget_exceeded,unpriceable,statute_denied,grant_expired,grant_spent,unnamed_approver,approver_set_empty,pending_limit_exceeded,enforcement_unavailable,snapshot_stale,unknown_action,unknown_resource,residency_violation,outside_active_window,rate_limited,concurrency_limited - Deploy —
manifest_stale,company_block_conflict,brain_retention_unsupported,residency_unsupported_on_runtime,plan_required - Brain —
brain_not_permitted,brain_not_bound,brain_invalid_input - Transport —
unauthorized,not_found,conflict,server_error,malformed_response,network,timeout,contract_version_mismatch,not_implemented
NAIVE_ERROR_CODES is exported as a runtime array, so you can switch exhaustively without
transcribing the list.
What the SDK cannot reach yet
POST /v1/policy/explain reports the engine’s verdict vocabulary — it says park, never
approve — plus the layer and the config path that decided. It is the fastest way to answer
“why was this denied” without reading a ledger that has no reader.The manifest-digest fence is not implemented
defineConfig returns your config by identity and computes no digest, so there is nothing for
the client to send and nothing to compare against the applied manifest. The
onDigestMismatch option is absent rather than accepted-and-ignored. digestsOf(err) is
exported and will decode a manifest_stale error if the API ever sends one; today nothing
does.
See also: Teams & the durable runtime · Errors ·
Account Kits ·
Governance gateway.