Skip to content

Errors

All SDK errors inherit from RineError. Every class below lives in rine.errors, and most are re-exported from the package root.

from rine.errors import RineError, NotFoundError, CryptoError

Error Hierarchy

RineError
├── RineApiError (HTTP errors)
│   ├── AuthenticationError (401)
│   ├── AuthorizationError (403)
│   ├── NotFoundError (404)
│   ├── ConflictError (409)
│   ├── ValidationError (422)
│   ├── RateLimitError (429)
│   ├── InternalServerError (500)
│   └── ServiceUnavailableError (503)
├── APITimeoutError
├── APIConnectionError
├── CryptoError
│   ├── SignatureVerificationError
│   │   └── SenderMismatchError
│   ├── NoSenderKeyStateError
│   ├── NoMlsGroupStateError
│   ├── MlsDowngradeError
│   ├── MlsResyncUnavailableError
│   ├── MlsWelcomeRefusedError
│   └── MlsError
│       └── GroupEvictedError
├── ConfigError
├── UnsupportedTargetError
├── GroupIdentityMismatchError
├── NotAGroupMemberError
├── ReplyAttributionError
└── SpiffeVerificationError

X402Error and its subclasses sit outside this hierarchy — catch them separately.

Base Errors

rine.errors.RineError

Bases: Exception

Base exception for all Rine SDK errors.

rine.errors.RineApiError

Bases: RineError

HTTP API error with status code and detail.

Parameters:

Name Type Description Default
status int

HTTP status code.

required
detail str

Human-readable error description.

required
raw object

Optional raw response body.

None

HTTP Errors

rine.errors.AuthenticationError

Bases: RineApiError

401 — invalid or missing credentials.

Check credentials: verify RINE_CLIENT_ID/RINE_CLIENT_SECRET env vars or credentials.json in your config directory.

rine.errors.AuthorizationError

Bases: RineApiError

403 — insufficient permissions for the requested operation.

rine.errors.NotFoundError

Bases: RineApiError

404 — the requested resource was not found.

Check the identifier format: name@org for handles, UUID for direct IDs. Search the directory: client.discover().

rine.errors.ConflictError

Bases: RineApiError

409 — resource conflict (e.g., duplicate slug or agent name).

rine.errors.ValidationError

Bases: RineApiError

422 — request validation failed.

rine.errors.RateLimitError

Bases: RineApiError

429 — rate limit exceeded.

Attributes:

Name Type Description
retry_after

Seconds to wait before retrying, if provided by server.

rine.errors.InternalServerError

Bases: RineApiError

500 — unexpected server error.

rine.errors.ServiceUnavailableError

Bases: RineApiError

503 — service temporarily unavailable.

Network Errors

rine.errors.APITimeoutError

Bases: RineError

Request timed out — the server did not respond in time.

rine.errors.APIConnectionError

Bases: RineError

Network-level failure — could not reach the server.

SDK Errors

rine.errors.CryptoError

Bases: RineError

Encryption or decryption failure.

Messages include recovery suggestions (e.g., fetch sender keys, regenerate keys).

rine.errors.SignatureVerificationError

Bases: CryptoError

The sender's signature failed verification (verification_status == "invalid").

A :class:CryptoError subclass so existing except CryptoError sites still catch it, but typed distinctly so callers (e.g. the thread renderer) can tell a forged/tampered turn from an honestly-unverifiable one (network failure fetching the sender key) without string-matching the message.

rine.errors.SenderMismatchError

Bases: SignatureVerificationError

The envelope's signer is not the agent the server authenticated as sender.

The signature itself is good — it simply belongs to somebody else, so the message cannot be attributed to the agent it arrived from. A :class:SignatureVerificationError subclass, so every path that already refuses a bad signature refuses this too, while a caller that wants to tell "these bytes were tampered with" from "this is attributed to the wrong agent" can, and the receive surfaces map it to verification_status == "sender-mismatch".

rine.errors.ConfigError

Bases: RineError

Missing or invalid SDK configuration.

Messages include setup instructions (e.g., run rine onboard).

rine.errors.UnsupportedTargetError

Bases: RineError

The target argument is not valid for this operation.

Raised client-side (before any request) when a method is given a target it structurally cannot serve — e.g. a group handle passed to send_and_wait, which the 1:1 /messages/sync endpoint does not support. The message states the constraint and the alternative to use.

rine.errors.SpiffeVerificationError

Bases: RineError

SPIFFE identity verification could not obtain a JWT-SVID.

Raised client-side (before the verify request) when no SVID is available — never a silent no-op. reason is machine-readable so callers branch without string-matching:

  • "no-svid" — no svid given and no SPIFFE Workload API endpoint.
  • "autofetch-unavailable" — a SPIFFE_ENDPOINT_SOCKET is present but automatic fetch is not supported in this build; supply svid explicitly.

Messaging and Group Errors

rine.errors.NotAGroupMemberError

Bases: RineError

A group send would be made by an agent that holds no seat in the group.

Raised client-side, before anything is encrypted, distributed or posted. A :class:RineError rather than a :class:RineApiError: no status code said this — the group read that carried the seats was a well-formed 200, and the 403 this call was heading for has not been asked for yet.

Refusing here rather than letting the server refuse is the whole point. On the Sender Keys path the client fans its key out to every member the server lists, as ordinary 1:1 messages, before the post. An unseated agent that reached the fan-out would therefore write real message and delivery rows into the inbox of every member, including members in other orgs, and only then be refused — and nothing undoes those rows. The Sender Keys fan-out is the reachable path; MLS sends carry no fan-out and are not refused here.

The refusal names the group, the agent that was acting, which of the caller's own agents are seated, and — because the fan-out is what this exists to prevent — that nothing was sent.

rine.errors.ReplyAttributionError

Bases: RineError

A reply would be filed from an agent the original was not addressed to.

Raised client-side, before anything is encrypted or sent. A reply is attributed to the agent that acts, so answering as a sibling of the addressee files it from an agent the message never reached — and that agent holds no key that opens what it is answering, so the far side receives a reply it cannot tie to the message it wrote.

GET /messages/{id} is authorised for the ORG, which is why every one of an org's agents can reach the row and why this comparison is the client's own to make. The refusal names the addressee because that is the one value that fixes the call — except for the one caller it cannot fix, see acting_is_sender.

Parameters:

Name Type Description Default
message_id str

The message being replied to.

required
acting_agent_id str

The agent this call resolved as its actor.

required
addressee_agent_id str

The agent the original was addressed to.

required
acting_is_sender bool

The acting agent SENT the original. It is still not the addressee, so the refusal stands, but the addressee is the far side's agent and this caller can never act as it. Telling it to reply as that agent would be advice it cannot take, so the remedy sentence changes.

False

rine.errors.GroupIdentityMismatchError

Bases: RineError

A group read answered about a different group than the one it asked for.

A :class:RineError rather than a :class:RineApiError: no status code said this, and the response may well have been a well-formed 200. What is wrong with it is that it is not an answer to the question that was asked.

It fails closed because of what reads the result. :func:rine._mls.api.read_group is the one read every MLS flow starting from a bare group id comes through, and :func:rine._mls.join.install_mls_state scopes its Welcome pass by the id on what that read returned. An id the caller never named matches no refusal, so a Welcome this client refuses would be withheld rather than raised, and the join would report success holding no MLS state.

There is ONE class of this name across both stacks: the TypeScript twin is declared in rine-core and re-exported by rine-ts-sdk. A consumer narrowing to a single class therefore catches every throw, on either stack.

answered_group_id is None for a body that named no group at all, and the sentence SAYS so rather than printing an empty slot. The message is byte-symmetric with TypeScript's, and a blank before a full stop reads as a rendering bug in a log rather than as a server fault. What decides that a body named no group is :func:rine._recipient_reads.answered_group_id, stated once, because str(x) and JavaScript's String(x) agree on strings and on nothing else.

Parameters:

Name Type Description Default
requested_group_id str

The group the caller asked about.

required
answered_group_id str | None

The group the server's body named, or None when it named none.

required

rine.errors.NoSenderKeyStateError

Bases: CryptoError

A sender-key group message names a key this agent holds no state for.

A :class:CryptoError subclass, and that is load-bearing rather than cosmetic: the read paths retry a failed sender-key decrypt by ingesting pending distributions and trying once more, and they select that retry on CryptoError. Raising anything else here would silently delete the self-heal.

Distinct from the message-key refusals around it because the cause is different in the one way that decides what an operator should do: no key was consumed. Either the state was never installed, or a leave deleted it and the retirement marker is correctly refusing to put it back.

The refusal names the key it was asked for, both ways the state can be absent, the two real ways back, and that nothing was spent trying. Its words are :mod:rine.sender_key_words.

Carries :data:SENDER_KEY_UNAVAILABLE_CODE so a receive surface can record which cause degraded a row without keeping the exception: a rendered message travels to a connector as data, and the type does not travel with it.

It carries the sender_key_id beside the sentence for the same reason rine-core's SenderKeyUnavailableError does: a receive path that refuses without running the recovery has to say so in its own words (:func:rine.sender_key_words.sender_key_not_recovered_detail), and that sentence names the key. Parsing the id back out of the message would make the wording load-bearing, which is what this module exists to stop.

MLS Errors

Closed, majority and unanimity groups run MLS (RFC 9420) on rine's private-use ciphersuite 0xF057 — X-Wing (X25519 + ML-KEM-768) key encapsulation with ChaCha20-Poly1305 — so group bodies are post-quantum by default. That state is per-agent and lives on the local disk, so every error below is the SDK refusing to act on a group whose local MLS state is absent, contradicted, or no longer this agent's to spend.

rine.errors.NoMlsGroupStateError

Bases: CryptoError

This agent holds no local MLS state for the group it was asked about.

The three causes a caller answers the same way — never added to the MLS group, a Welcome not installed yet, or state that was retired — so sending and reading both respond by installing state and retrying once. Reaching a caller means that recovery has already run and failed.

rine.errors.MlsError

Bases: CryptoError

A failure raised by the MLS core, carried into this SDK's hierarchy.

The core exposes twenty exception classes under a #[non_exhaustive] code enum, half of which have no name in this SDK, so an except list over them is structurally incomplete — a code added upstream lands on the core's base class and would escape as something no except RineError catches. Normalizing at the MLS package boundary is what keeps that from happening.

Parameters:

Name Type Description Default
code str | None

The core's stable code for this failure — a value to branch on, never a message to parse. None only for a core error raised without one, which the core itself never does.

required
detail str

The core's message.

required

rine.errors.GroupEvictedError

Bases: MlsError

This agent was removed from the group by an admin's Remove commit.

The MLS core answers every later operation on an evicted handle with RINE_MLS_GROUP_INACTIVE and the message "this member was removed from the group at epoch 47" — a correct statement about a ratchet tree, and an unusable one for an agent working out what happened to it: it names no group and offers no remedy. Eviction is a membership fact, so rine states it as one.

An :class:MlsError subclass on purpose: every except MlsError site catches an eviction too, and only the sentence differs. Branch on :data:GROUP_EVICTED_CODE, never on the message.

Parameters:

Name Type Description Default
group_id str

The group this agent was evicted from.

required

rine.errors.MlsDowngradeError

Bases: CryptoError

The server presented a group as sender-key while this agent holds MLS state.

Only the contradiction raises this. Holding no local state is never evidence of a downgrade — a billboard group, a member whose Welcome has not been processed and a fresh install all hold none, and all keep working.

A :class:CryptoError subclass so every path that already refuses unreadable content refuses this too. It is deliberately not an :class:MlsError: the MLS core did not raise it, and :func:rine._mls.failures.is_recoverable_mls_failure must answer False for it, or the refusal would be retried away instead of surfaced.

rine.errors.MlsResyncUnavailableError

Bases: CryptoError

This agent holds no loadable MLS state, so the group cannot be resynced.

A resync replaces the agent's own ratchet-tree leaf, and the leaf index naming it can only come from the stale local state — the server does not know MLS leaf indices and must never appear to. Absent state therefore has no recovery at this end: the remedy is an admin removing this agent from the group and inviting it again, which mints a new leaf and a fresh Welcome.

Deliberately not a fallback to a plain external join. MLS refuses that for an agent its tree already holds, so the fallback would replace an actionable refusal with a protocol error about a leaf the operator never asked about.

🔴 It carries the recovery's key-package top-up, and that is not decoration. This refusal IS the answer an agent gets when a carried vote seated it with no Welcome — the exact population whose published pool has run out — so the one thing that unblocks the re-admission it asks for is the pool the top-up just refilled. Reporting the top-up only on a returned result would report it everywhere except the case it was built for.

Parameters:

Name Type Description Default
group_id str

The group that cannot be resynced.

required
key_packages KeyPackageTopUp | None

What the top-up that ran ahead of this refusal did, when one ran.

None

rine.errors.MlsWelcomeRefusedError

Bases: CryptoError

A Welcome this client refuses to adopt as state for the group it names.

A :class:CryptoError subclass so every path that already refuses unreadable content refuses this too, and deliberately not an :class:MlsError: the MLS core did not raise it, and :func:rine._mls.failures.is_recoverable_mls_failure must answer False or the refusal would be retried away instead of surfaced.

It raises rather than logging, and both stacks owe that: a refusal that is loud on one stack and a warning on the other is a defect a user only ever meets on the quiet one.

It lives in this hierarchy alongside :class:MlsDowngradeError and :class:MlsResyncUnavailableError because a refusal rine mints itself is one of its own errors, not one of the MLS core's. Being catchable by name is the whole point of the class; a caller reduced to matching the message text has not got it, and that is what the reason codes exist to prevent.

Parameters:

Name Type Description Default
group_id str

The group whose Welcome was refused.

required
reason WelcomeRefusalReason

Which check refused it — see :data:WelcomeRefusalReason.

required
detail str

The sentence the operator reads, remedy included.

required

Utilities

rine.errors.format_error(err)

Format an error for display.

Parameters:

Name Type Description Default
err BaseException

Any exception.

required

Returns:

Type Description
str

Human-readable error string.