> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rotavision.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> How Sakshi fails on purpose: fail-open capture, fail-closed enforcement, the SDK exception model, HTTP status codes, and idempotency.

Sakshi governs production agents, so how it behaves when something goes wrong is
a design decision, not an accident. Two rules run through everything below:

* **Capture is fail-open.** Recording a decision must never take your agent
  down. If the platform is unreachable, the record is retried and then dropped
  with a warning, and your agent keeps running.
* **Enforcement is fail-closed.** Asking permission to act is different. If
  Sakshi cannot return an enforcement decision, the SDK raises rather than let
  an ungoverned action through.

<Note>
  Fail-open is the default for capture. Pass `fail_open=False` to
  `SakshiClient` for jobs where losing a record is worse than stopping, such as
  batch pipelines. Capture then delivers synchronously and raises on failure.
</Note>

## The SDK exception model

The SDK raises three exceptions, all from `enforce()`. Import them from the top
level:

```python theme={null}
from sakshi import (
    SakshiBlocked,
    SakshiSyncReviewRequired,
    SakshiEnforcementUnavailable,
)
```

| Exception                      | Raised when                                                                        | What it means                                                                                       |
| ------------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `SakshiBlocked`                | The envelope matched a **block** rule, or the **kill switch** has halted the agent | The action must not run. Stop it.                                                                   |
| `SakshiSyncReviewRequired`     | The envelope routed the action to **sync review**                                  | A human must decide before the action can run. Park it; the review item already exists server-side. |
| `SakshiEnforcementUnavailable` | Sakshi could not be reached for a decision                                         | Fail closed. Do not proceed as if allowed.                                                          |

`enforce()` returns an `EnforcementDecision` whose `outcome` is one of `auto`,
`async_review`, `sync_review`, or `block`. By default (`raise_on_hold=True`) a
`block` raises `SakshiBlocked` and a `sync_review` raises
`SakshiSyncReviewRequired`, so the allowed path is the one that returns.

### Handling enforcement

The common shape is a `try`/`except` around the intended action:

```python theme={null}
from sakshi import (
    SakshiBlocked,
    SakshiSyncReviewRequired,
    SakshiEnforcementUnavailable,
)

try:
    client.enforce(
        agent_id,
        "approve_loan",
        stakes=1_500_000,
        confidence=0.92,
    )
    # auto or async_review: proceed with the action
    approve(application)
except SakshiSyncReviewRequired:
    # a human must decide first; park the action, do not run it
    park_for_review(application)
except SakshiBlocked:
    # an envelope rule or the kill switch stopped it
    decline_or_defer(application)
except SakshiEnforcementUnavailable:
    # fail closed: no decision means no ungoverned action
    defer(application)
```

`async_review` allows the action to run now with review after the fact, so it
returns normally. If you would rather branch on the outcome yourself instead of
catching exceptions, pass `raise_on_hold=False` and read the decision:

```python theme={null}
decision = client.enforce(
    agent_id, "approve_loan",
    stakes=1_500_000, confidence=0.92,
    raise_on_hold=False,
)
if decision.allowed:            # auto or async_review
    approve(application)
else:                           # sync_review or block
    park_for_review(application)
```

`SakshiEnforcementUnavailable` is still raised even with `raise_on_hold=False`,
because an unreachable platform is not an outcome you can branch on. It is the
fail-closed guarantee.

### Handling capture

Capture through a `witness` session does not raise in the default fail-open
mode. The record is queued and delivered by a background worker; if delivery
fails after retries, the record is dropped and a warning is logged to the
`sakshi` logger. Your `with client.witness(...)` block always completes.

An exception inside the witness block is recorded as the decision's outcome and
re-raised, because failures are evidence too:

```python theme={null}
with client.witness(agent_id, client_ref="APP-2026-04471") as decision:
    decision.step("assess", risk_band="low")
    decision.action(outcome="approved")   # if this block raises,
                                           # the error is captured, then re-raised
```

With `fail_open=False`, capture delivers synchronously and raises the underlying
transport error instead of dropping the record. Use it only where a lost record
is worse than a stopped job.

## HTTP status codes

If you call the API directly, or want to reason about what the SDK is reacting
to, these are the statuses Sakshi returns.

| Status | Meaning                                                                                   | How to react                                                                                             |
| ------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `401`  | No token, or an invalid or revoked token                                                  | Check the `Authorization` header. Mint or refresh the credential. See [Authentication](/authentication). |
| `403`  | Valid token, insufficient scope: for example an ingest key attempting a governance action | Use an OIDC JWT for governance and admin actions. Ingest keys can only register and witness.             |
| `404`  | No such resource, or not visible to this principal                                        | Verify the id and that the token's org unit or team can see it.                                          |
| `409`  | Conflict with current state                                                               | Re-read the resource and reconcile. Do not blindly retry.                                                |
| `422`  | Request failed validation                                                                 | Fix the payload. The body names the offending fields.                                                    |
| `429`  | Rate limit exceeded                                                                       | Back off. The response carries `Retry-After` (seconds); wait that long before retrying.                  |
| `5xx`  | Server-side error, usually transient                                                      | Retry idempotent calls with backoff. Do not silently retry `enforce` or `handoff` (see below).           |

Validation errors (`422`) follow the standard schema for this API and list each
invalid field, so you can map the failure back to a specific parameter.

## Retries and idempotency

The SDK's **capture** layer retries transient failures with jittered
exponential backoff up to `max_retries=3` (configurable on the client). A fleet
of agents backing off in lockstep would hammer a recovering platform, so the
backoff is jittered. After the last attempt it either drops the record
(fail-open) or raises (fail\_open=False).

Not every call is safe to retry, so not every call is retried:

| Call                    | Idempotency                                                                | Retried by the SDK                                                      |
| ----------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `register`              | Idempotent by **name**: safe to call on every startup                      | Synchronous; you may retry                                              |
| `witness`               | Idempotent by **`client_ref`**: a redelivered record does not double-count | Yes, by the capture layer                                               |
| `enforce`               | **Not** idempotent                                                         | **No.** A failure raises `SakshiEnforcementUnavailable`                 |
| `request_human_handoff` | **Not** idempotent                                                         | **No.** Synchronous and raising; a lost handoff is a compliance failure |

<Warning>
  `enforce` and `handoff` are never retried automatically, because re-asking for
  a decision or re-requesting a human are not free to repeat. If you retry them
  yourself, guard the retry with your own idempotency key so you do not create
  duplicate evaluations or duplicate review items.
</Warning>

Pass a stable `client_ref` to `witness` so a retried capture reconciles to the
same record rather than writing a second one.

## Next steps

<CardGroup cols={2}>
  <Card title="Bound autonomy" icon="shield-halved" href="/guides/bound-autonomy">
    How envelopes route an action to auto, review, or block, and how the kill
    switch halts an agent.
  </Card>

  <Card title="Python SDK" icon="code" href="/sdk/python">
    The client, the witness session, and the full exception reference.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Ingest keys versus OIDC JWTs, and why a `403` usually means the wrong token
    type.
  </Card>

  <Card title="API reference" icon="terminal" href="/api-reference/introduction">
    Every endpoint and its response schema, generated from the OpenAPI spec.
  </Card>
</CardGroup>
