use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use crate::error::derive::WyrdError as WyrdErrorMeta;
pub mod derive {
pub use wyrd_error_derive::WyrdError;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct WyrdProblem {
#[serde(rename = "type")]
pub r#type: String,
pub title: String,
pub status: u16,
pub detail: String,
pub code: String,
pub details: serde_json::Value,
pub remediation: String,
}
pub mod storage {
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::error::derive::WyrdError;
#[derive(Debug, Clone, Error, Serialize, Deserialize, schemars::JsonSchema, WyrdError)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "variant", content = "data", rename_all = "snake_case")]
pub enum WyrdStorageError {
#[error("tenant path validation rejected upload: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_TENANT_PATH_MISMATCH",
status = 400,
title = "Tenant path validation rejected upload init",
remediation = "Ensure relative_path contains no `..`, no leading `/`, and only [A-Za-z0-9._-] characters."
)]
TenantPathMismatch {
detail: String,
},
#[error("artifact size {actual} exceeds backend limit {limit}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_ARTIFACT_TOO_LARGE",
status = 400,
title = "Artifact size exceeds backend capacity",
remediation = "Split the artifact into multiple cards or use a backend with higher per-object limits."
)]
ArtifactTooLarge {
actual: u64,
limit: u64,
},
#[error("expected_sha256 is not a valid base64 SHA-256: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_SHA256_INVALID",
status = 400,
title = "Invalid SHA-256 in upload init",
remediation = "Recompute SHA-256 of the artifact and base64-encode the 32-byte digest."
)]
Sha256Invalid {
detail: String,
},
#[error("expected_size_bytes is invalid: {0}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_SIZE_INVALID",
status = 400,
title = "Invalid expected_size_bytes",
remediation = "Pass a u64 byte length that matches the actual file size."
)]
SizeInvalid(u64),
#[error("sha256 mismatch: expected {expected}, computed {actual}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_SHA256_MISMATCH",
status = 400,
title = "Stored object SHA-256 does not match expected_sha256",
remediation = "Recompute the SHA-256 of the artifact bytes and retry."
)]
Sha256Mismatch {
expected: String,
actual: String,
},
#[error("size mismatch: expected {expected}, stored {actual}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_SIZE_MISMATCH",
status = 400,
title = "Stored object byte length does not match expected_size_bytes",
remediation = "Verify the file size you reported in upload_init matches the bytes you uploaded."
)]
SizeMismatch {
expected: u64,
actual: u64,
},
#[error("invalid upload_id: {reason}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_INVALID_UPLOAD_ID",
status = 400,
title = "Upload id not recognised",
remediation = "Use the upload_id returned by POST /v1/cards/upload/init."
)]
InvalidUploadId {
reason: String,
},
#[error("invalid source uri: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_INVALID_URI",
status = 400,
title = "Source URI failed to parse",
remediation = "Pass an absolute URI with a scheme and authority recognised by the configured backend."
)]
InvalidUri {
detail: String,
},
#[error("source uri tenant prefix invalid: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_400_TENANT_PREFIX_INVALID",
status = 400,
title = "Source URI tenant prefix is not a UUIDv7",
remediation = "Object URIs must start with the caller's data_tenant_id."
)]
TenantPrefixInvalid {
detail: String,
},
#[error("upload belongs to a different tenant")]
#[wyrd_error(
code = "WYRD_STORAGE_403_UPLOAD_FOREIGN_TENANT",
status = 403,
title = "Upload belongs to a different tenant",
remediation = "Authenticate with a token whose data_tenant_id matches the upload's tenant prefix."
)]
TenantPathForeign,
#[error("object not found at {storage_path}")]
#[wyrd_error(
code = "WYRD_STORAGE_404_OBJECT_NOT_FOUND",
status = 404,
title = "Object not found in backend",
remediation = "Verify card_uid and relative_path; re-upload if the object was deleted."
)]
ObjectNotFound {
storage_path: String,
},
#[error("upload not found")]
#[wyrd_error(
code = "WYRD_STORAGE_404_UPLOAD_NOT_FOUND",
status = 404,
title = "Upload id not found",
remediation = "Use the upload_id returned by POST /v1/cards/upload/init."
)]
UploadNotFound,
#[error("upload is not pending")]
#[wyrd_error(
code = "WYRD_STORAGE_409_UPLOAD_NOT_PENDING",
status = 409,
title = "Cannot complete or abort a terminal upload",
remediation = "Inspect upload status; only pending uploads can be completed or aborted."
)]
UploadNotPending,
#[error("encryption required but backend HEAD did not advertise SSE")]
#[wyrd_error(
code = "WYRD_STORAGE_409_ENCRYPTION_MISSING",
status = 409,
title = "Server-side encryption verification failed",
remediation = "Configure backend server-side encryption and retry."
)]
EncryptionMissing,
#[error("object precondition failed during read")]
#[wyrd_error(
code = "WYRD_STORAGE_412_PRECONDITION",
status = 412,
title = "Object changed during download",
remediation = "Delete the partial file and re-issue download init for a fresh URL."
)]
PreconditionFailed,
#[error("range not satisfiable")]
#[wyrd_error(
code = "WYRD_STORAGE_416_RANGE_NOT_SATISFIABLE",
status = 416,
title = "Source changed mid-download",
remediation = "Delete the partial file and sidecar, then re-download."
)]
RangeNotSatisfiable,
#[error("backend storage operation failed: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_500_BACKEND",
status = 500,
title = "Backend storage operation failed",
remediation = "Retry the operation. If failures persist, check backend dashboard and storage server logs."
)]
Backend {
detail: String,
},
#[error("storage settings invalid at boot: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_500_CONFIG_INVALID",
status = 500,
title = "Storage settings invalid",
remediation = "Inspect server startup logs; fix the named env var and restart."
)]
ConfigInvalid {
detail: String,
},
#[error("backend {backend} credential chain returned no credentials")]
#[wyrd_error(
code = "WYRD_STORAGE_500_CREDENTIAL_CHAIN",
status = 500,
title = "Backend credential chain failed",
remediation = "Verify backend credentials are reachable in the server environment."
)]
CredentialChain {
backend: String,
},
#[error("S3 bucket lifecycle missing AbortIncompleteMultipartUpload rule")]
#[wyrd_error(
code = "WYRD_STORAGE_500_LIFECYCLE_MISSING",
status = 500,
title = "S3 lifecycle missing required rule",
remediation = "Apply the recommended lifecycle policy for incomplete multipart uploads."
)]
LifecycleMissing,
#[error("presigned URL refused as expired: {detail}")]
#[wyrd_error(
code = "WYRD_STORAGE_503_PRESIGN_EXPIRED",
status = 503,
title = "Presigned URL expired",
remediation = "Call upload part-url or download init to obtain a fresh URL and retry."
)]
PresignExpired {
detail: String,
},
#[error("backend transiently unavailable: status {status}")]
#[wyrd_error(
code = "WYRD_STORAGE_503_BACKEND_UNAVAILABLE",
status = 503,
title = "Backend transiently unavailable",
remediation = "Retry with exponential backoff."
)]
BackendUnavailable {
status: u16,
},
}
}
pub use storage::WyrdStorageError;
#[derive(Debug, Clone, Error, Serialize, Deserialize, schemars::JsonSchema, WyrdErrorMeta)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WyrdError {
#[error("[WYRD_SPEC_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_VALIDATION",
status = 400,
title = "Validation failed",
remediation = "Check the submitted Wyrd request fields against the published schema and retry."
)]
Validation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_IO] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_IO",
status = 400,
title = "Loader IO error",
remediation = "Check file permissions and that all referenced files exist."
)]
LoaderIo {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_YAML_SYNTAX] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_YAML_SYNTAX",
status = 400,
title = "YAML syntax error",
remediation = "Fix the YAML syntax error and retry."
)]
LoaderYamlSyntax {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_INVALID_ENVELOPE] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_INVALID_ENVELOPE",
status = 400,
title = "Invalid card envelope",
remediation = "Ensure the card has apiVersion, kind, metadata, and spec fields."
)]
LoaderInvalidEnvelope {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_PATH_ESCAPE] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_PATH_ESCAPE",
status = 400,
title = "Path reference escapes workspace",
remediation = "Use relative paths within the workspace root."
)]
LoaderPathEscape {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_PATH_ABSOLUTE_ADVISORY] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_PATH_ABSOLUTE_ADVISORY",
status = 400,
title = "Absolute path reference breaks portability",
remediation = "Use relative paths for portability across environments."
)]
LoaderPathAbsoluteAdvisory {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_LOADER_400_CONFIG_LOAD_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_LOADER_400_CONFIG_LOAD_FAILED",
status = 400,
title = "Config load failed",
remediation = "Check wyrd.toml syntax and structure."
)]
LoaderConfigLoadFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_404_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_404_NOT_FOUND",
status = 404,
title = "Resource not found",
remediation = "Check that the referenced Wyrd resource exists."
)]
NotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_409_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_409_CONFLICT",
status = 409,
title = "Conflict",
remediation = "Refresh the resource and retry with the current version or idempotency key."
)]
Conflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_500_INTERNAL] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_500_INTERNAL",
status = 500,
title = "Internal error",
remediation = "Retry later or inspect server logs using the request ID."
)]
Internal {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_502_UPSTREAM_FAILURE] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_502_UPSTREAM_FAILURE",
status = 502,
title = "Upstream dependency failed",
remediation = "Check the upstream dependency health and retry policy."
)]
UpstreamFailure {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_504_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_504_TIMEOUT",
status = 504,
title = "Timeout",
remediation = "Retry with a longer timeout or reduce the request scope."
)]
Timeout {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_VALA_400_TASK_DAG_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_VALA_400_TASK_DAG_INVALID",
status = 400,
title = "Eval task DAG validation failed",
remediation = "Remove cycles, self-dependencies, or references to missing eval tasks."
)]
ValaTaskDagInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_VALA_400_EVAL_REF_KIND_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_VALA_400_EVAL_REF_KIND_MISMATCH",
status = 400,
title = "Eval reference kind mismatch",
remediation = "Use the required CardRef kind for the eval field being validated."
)]
ValaEvalRefKindMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_UNAUTHENTICATED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_UNAUTHENTICATED",
status = 401,
title = "Not authenticated",
remediation = "Present a valid Wyrd token as `X-Wyrd-Access-Token: Bearer <token>`."
)]
Unauthenticated {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_TOKEN_EXPIRED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_TOKEN_EXPIRED",
status = 401,
title = "Token expired",
remediation = "Re-authenticate to obtain a fresh token."
)]
TokenExpired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_INVALID_TOKEN] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_INVALID_TOKEN",
status = 401,
title = "Invalid token",
remediation = "Re-authenticate to obtain a valid token signed by the current issuer key."
)]
InvalidToken {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_BAD_TOKEN_FORMAT] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_BAD_TOKEN_FORMAT",
status = 400,
title = "Access token header malformed",
remediation = "Use `X-Wyrd-Access-Token: Bearer <token>` with a single Bearer credential."
)]
BadTokenFormat {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_UNSUPPORTED_GRANT_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_UNSUPPORTED_GRANT_TYPE",
status = 400,
title = "Unsupported grant_type",
remediation = "Use one of the supported grants listed in `details.supported_grant_types`."
)]
UnsupportedGrantType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_DEVICE_AUTHORIZATION] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_DEVICE_AUTHORIZATION",
status = 400,
title = "Device authorization did not issue a credential",
remediation = "Keep polling on `authorization_pending`, poll five seconds slower on `slow_down`, and start a new login on any other `details.error`."
)]
DeviceAuthorization {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_INVALID_STATE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_INVALID_STATE",
status = 400,
title = "Invalid OIDC state",
remediation = "Restart the login flow. The callback state is single-use and must match the server-stored login state."
)]
InvalidState {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_INVALID_NONCE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_INVALID_NONCE",
status = 400,
title = "Invalid OIDC nonce",
remediation = "Restart the login flow. The ID token nonce must match the server-stored login nonce."
)]
InvalidNonce {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_REFRESH_REUSED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_REFRESH_REUSED",
status = 401,
title = "Refresh token reused",
remediation = "Re-authenticate. The refresh token family has been revoked because a rotated token was reused."
)]
RefreshReused {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_REFRESH_REVOKED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_REFRESH_REVOKED",
status = 401,
title = "Refresh token revoked",
remediation = "Re-authenticate to obtain a new refresh token."
)]
RefreshRevoked {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_DELEGATION_DEPTH_EXCEEDED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_DELEGATION_DEPTH_EXCEEDED",
status = 400,
title = "Delegation chain exceeds maximum depth",
remediation = "Reduce the delegation chain; the caller's existing `act` chain plus the requested hop must be at most 5 layers."
)]
DelegationDepthExceededIssue {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_403_UNREPRESENTABLE_DELEGATION] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_403_UNREPRESENTABLE_DELEGATION",
status = 403,
title = "Delegated permissions cannot be represented",
remediation = "The subject's and actor's shared permissions cannot be represented by token scopes, so the exchange issued no token. Exchange with grants whose exact shared authority fits the supported scopes, such as both parties holding the same explicit gateway deployment or model grant."
)]
UnrepresentableDelegation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_PRINCIPAL_KIND_CARD_KIND_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_PRINCIPAL_KIND_CARD_KIND_MISMATCH",
status = 400,
title = "CardRef cannot be bound to a non-human principal",
remediation = "Only Service and Agent cards bind to non-human principals. Re-issue the request with a `card_ref` referencing a Service or Agent card."
)]
PrincipalKindCardKindMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_INVALID_CARD_REF_VERSION] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_INVALID_CARD_REF_VERSION",
status = 400,
title = "Non-human principal CardRef.version must be an exact Pin",
remediation = "Re-issue the request with `card_ref.version` set to an exact `Pin`, for example `1.2.3`; version requirements are rejected for card-bound principals."
)]
InvalidCardRefVersion {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_CREDENTIAL_REVOKED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_CREDENTIAL_REVOKED",
status = 401,
title = "Credential revoked",
remediation = "The credential was explicitly revoked. Re-authenticate or request a new credential."
)]
CredentialRevoked {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_API_KEY_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_API_KEY_INVALID",
status = 401,
title = "API key not found, revoked, or hash mismatch",
remediation = "Re-issue a key with `wyrd auth issue-key <card_ref>` and update the deployment secret."
)]
ApiKeyInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_INVALID_CARD_REF] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_INVALID_CARD_REF",
status = 401,
title = "Non-User token card_ref claim absent or malformed",
remediation = "Re-issue the token via `POST /auth/token`; ensure the bound non-human principal has a valid structured card_ref."
)]
InvalidCardRef {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_413_CARD_SCOPE_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_413_CARD_SCOPE_TOO_LARGE",
status = 413,
title = "Card scope too large",
remediation = "Reduce or split the principal's declared card-ref graph so its transitive observation-target scope fits the token size limit, or issue a narrower card-bound principal."
)]
CardScopeTooLarge {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_401_DELEGATION_DEPTH_EXCEEDED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_401_DELEGATION_DEPTH_EXCEEDED",
status = 401,
title = "Delegation chain in token exceeds maximum depth",
remediation = "The presented JWT's `act` chain exceeds the verifier's maximum delegation depth. Re-issue from a shorter chain."
)]
DelegationDepthExceededVerify {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_404_PRINCIPAL_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_404_PRINCIPAL_NOT_FOUND",
status = 404,
title = "Requested principal not found in tenant",
remediation = "The principal named by the `subject_token` or `actor_token` no longer exists in the current tenant. Re-authenticate both parties and retry the exchange."
)]
PrincipalNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_409_ADMIN_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_409_ADMIN_CONFLICT",
status = 409,
title = "Admin resource already exists or is still referenced",
remediation = "A trusted issuer or workload binding with this key already exists, or the issuer still has live bindings. Use a distinct key, or delete the bindings first (or with --cascade)."
)]
AdminConflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_404_ADMIN_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_404_ADMIN_NOT_FOUND",
status = 404,
title = "Admin resource not found in tenant",
remediation = "Confirm the issuer URL (and subject for bindings) and that the resource exists in the current tenant."
)]
AdminNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_HUMAN_CONNECTION_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_HUMAN_CONNECTION_REQUIRED",
status = 400,
title = "Human login uses the tenant OIDC connection",
remediation = "Trusted issuers and `[[trusted_issuers]]` accept only `principal_kind = workload`. Configure human sign-in through the tenant OIDC connection API: PUT /v1/identity/oidc/candidate, POST /v1/identity/oidc/candidate/test, then POST /v1/identity/oidc/candidate/activate."
)]
HumanConnectionRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_409_CONNECTION_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_409_CONNECTION_CONFLICT",
status = 409,
title = "Tenant OIDC connection conflict",
remediation = "Re-read GET /v1/identity/oidc/connections and retry with the current `expected_revision`. Activation also requires a valid recovery API key for a headless principal of this tenant that holds `identity_connections:write`."
)]
ConnectionConflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_409_CONNECTION_NOT_TESTED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_409_CONNECTION_NOT_TESTED",
status = 409,
title = "Tenant OIDC candidate is not freshly tested",
remediation = "Run POST /v1/identity/oidc/candidate/test against this exact candidate revision, complete the provider sign-in at the returned authorization_url, and activate within 15 minutes of that successful test."
)]
ConnectionNotTested {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_400_UNSUPPORTED_CLIENT_AUTH] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_400_UNSUPPORTED_CLIENT_AUTH",
status = 400,
title = "Unsupported OIDC client authentication",
remediation = "Use `SecretBasic` or `SecretPost` with a client secret, or `Public` without one. `private_key_jwt` is not supported for tenant connections."
)]
UnsupportedClientAuth {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_400_INVALID_CONFIGURATION] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_400_INVALID_CONFIGURATION",
status = 400,
title = "Invalid gateway configuration",
remediation = "Correct the field named in `details.field`. Path and body names must match, credential bindings and secret backends must be configured on the server, and deployments must reference an active credential for the same provider."
)]
GatewayInvalidConfiguration {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_404_RESOURCE_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_404_RESOURCE_NOT_FOUND",
status = 404,
title = "Gateway resource not found in tenant",
remediation = "List provider credentials or deployments to confirm the name exists in the current tenant."
)]
GatewayResourceNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_409_RESOURCE_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_409_RESOURCE_CONFLICT",
status = 409,
title = "Gateway resource conflicts with its current state",
remediation = "A revoked credential cannot be replaced, a credential referenced by a deployment cannot be deleted, a pricing version's content is immutable, and an identical batch creation still pending cannot be repeated. Use a new name or version, remove the referencing deployment first, or vary the batch metadata."
)]
GatewayResourceConflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_400_INVALID_REQUEST] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_400_INVALID_REQUEST",
status = 400,
title = "Invalid gateway request",
remediation = "Correct the field named in `details.field`. Models use the `<provider>/<model>` projection and a fallback override is a non-empty, duplicate-free list that excludes the requested model."
)]
GatewayInvalidRequest {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_404_MODEL_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_404_MODEL_UNAVAILABLE",
status = 404,
title = "Gateway model unavailable",
remediation = "Request a model the tenant has deployed for this operation, or ask a tenant administrator to add a deployment or fallback candidate."
)]
GatewayModelUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_422_COST_UNBOUNDED] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_422_COST_UNBOUNDED",
status = 422,
title = "Gateway call cost cannot be bounded",
remediation = "A budget applies or unknown cost is rejected, so the call needs active pricing for its model and a bounded request such as a maximum output token count."
)]
GatewayCostUnbounded {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_429_LIMIT_EXCEEDED] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_429_LIMIT_EXCEEDED",
status = 429,
title = "Gateway limit exceeded",
remediation = "Retry after `details.retry_after_seconds`, reduce concurrency, or ask a tenant administrator to raise the limit."
)]
GatewayLimitExceeded {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_429_BUDGET_EXCEEDED] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_429_BUDGET_EXCEEDED",
status = 429,
title = "Gateway budget exceeded",
remediation = "Wait for the next budget period, request a smaller bounded call, or ask a tenant administrator to raise the budget."
)]
GatewayBudgetExceeded {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_502_UPSTREAM_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_502_UPSTREAM_UNAVAILABLE",
status = 502,
title = "Gateway upstream unavailable",
remediation = "Retry with backoff. If failures persist, a tenant administrator should check the deployment's provider, credential, and fallback configuration."
)]
GatewayUpstreamUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_GATEWAY_504_DEADLINE_EXCEEDED] {message}")]
#[wyrd_error(
code = "WYRD_GATEWAY_504_DEADLINE_EXCEEDED",
status = 504,
title = "Gateway deadline exceeded",
remediation = "Retry with a longer deadline or a smaller request."
)]
GatewayDeadlineExceeded {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_503_VERIFY_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_503_VERIFY_UNAVAILABLE",
status = 503,
title = "Auth verify backend unavailable",
remediation = "Retry with backoff. Do not re-authenticate; this is an infrastructure failure, not a credential failure."
)]
AuthVerifyUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_503_DISCOVERY_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_503_DISCOVERY_UNAVAILABLE",
status = 503,
title = "OIDC discovery unavailable",
remediation = "Retry with backoff. Do not re-authenticate; this is an infrastructure or issuer-discovery failure."
)]
DiscoveryUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_VALIDATION_400_MISSING_REQUIRED_FIELD] {message}")]
#[wyrd_error(
code = "WYRD_VALIDATION_400_MISSING_REQUIRED_FIELD",
status = 400,
title = "Required request field is missing",
remediation = "Include the named field in the request; see the route's OpenAPI schema for the full required set."
)]
MissingRequiredField {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PERMISSION_401_UNAUTHENTICATED] {message}")]
#[wyrd_error(
code = "WYRD_PERMISSION_401_UNAUTHENTICATED",
status = 401,
title = "Authentication required",
remediation = "Send a valid `X-Wyrd-Access-Token: Bearer <token>` header before invoking permission-protected routes."
)]
PermissionUnauthenticated {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PERMISSION_403_DENIED_RBAC] {message}")]
#[wyrd_error(
code = "WYRD_PERMISSION_403_DENIED_RBAC",
status = 403,
title = "Permission denied (RBAC)",
remediation = "Request the required role from a workspace admin."
)]
PermissionDeniedRbac {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PERMISSION_500_ROLE_CORRUPT] {message}")]
#[wyrd_error(
code = "WYRD_PERMISSION_500_ROLE_CORRUPT",
status = 500,
title = "Role row's permissions JSONB is corrupt",
remediation = "Inspect the offending row in `wyrd.auth_roles`; restore or rewrite the permissions JSONB with a valid permission array. This is not retryable."
)]
RoleCorrupt {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_RBAC_409_BUILTIN_ROLE_IMMUTABLE_NAME] {message}")]
#[wyrd_error(
code = "WYRD_RBAC_409_BUILTIN_ROLE_IMMUTABLE_NAME",
status = 409,
title = "Builtin role names are immutable",
remediation = "Builtin role names are durable identifiers used by JWT role resolution and re-seeding. Create a replacement role instead of renaming a builtin role."
)]
BuiltinRoleImmutableName {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_QUERY_400_INVALID_SYNTAX] {message}")]
#[wyrd_error(
code = "WYRD_QUERY_400_INVALID_SYNTAX",
status = 400,
title = "Invalid query syntax",
remediation = "Fix the query string at the reported offset and retry. See the metadata query grammar."
)]
QueryInvalidSyntax {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_QUERY_400_INVALID_FIELD] {message}")]
#[wyrd_error(
code = "WYRD_QUERY_400_INVALID_FIELD",
status = 400,
title = "Invalid query field or operator",
remediation = "Reference a supported field and use an operator valid for its type. Ordering operators require a numeric or timestamp field."
)]
QueryInvalidField {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_QUERY_400_TOO_COMPLEX] {message}")]
#[wyrd_error(
code = "WYRD_QUERY_400_TOO_COMPLEX",
status = 400,
title = "Query too complex",
remediation = "Reduce the number of predicates or the nesting depth and retry."
)]
QueryTooComplex {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_INVALID_CARD_SPEC] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_INVALID_CARD_SPEC",
status = 400,
title = "Card spec failed type-driven deserialization",
remediation = "Verify the kind/spec field combination matches the documented Wyrd v1 schema for that kind."
)]
RegistryInvalidCardSpec {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_UNHYDRATED_ARTIFACT] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_UNHYDRATED_ARTIFACT",
status = 400,
title = "Hydrated bundle is missing artifact payloads",
remediation = "Run wyrd get without --metadata-only and load the complete bundle."
)]
SdkUnhydratedArtifact {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_RUNTIME_HYDRATION_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_RUNTIME_HYDRATION_FAILED",
status = 400,
title = "Runtime Card hydration failed",
remediation = "Provide the required custom interface/load arguments or regenerate a complete compatible bundle."
)]
SdkRuntimeHydrationFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_INVALID_STATE_BUNDLE] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_INVALID_STATE_BUNDLE",
status = 400,
title = "Invalid WyrdState bundle",
remediation = "Regenerate the bundle with wyrd get and load the complete output."
)]
SdkInvalidStateBundle {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_404_UNKNOWN_ALIAS] {message}")]
#[wyrd_error(
code = "WYRD_SDK_404_UNKNOWN_ALIAS",
status = 404,
title = "Unknown WyrdState alias",
remediation = "Use an alias returned by the loaded WyrdState bundle."
)]
SdkUnknownAlias {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_404_UNKNOWN_VERIFIER] {message}")]
#[wyrd_error(
code = "WYRD_SDK_404_UNKNOWN_VERIFIER",
status = 404,
title = "Verifier is not bound to this subject",
remediation = "Name a Verifier bound in `verified_by` to the view's subject Card, or open the view on the Card that binds it."
)]
SdkUnknownVerifier {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_409_BIFROST_ALREADY_STARTED] {message}")]
#[wyrd_error(
code = "WYRD_SDK_409_BIFROST_ALREADY_STARTED",
status = 409,
title = "WyrdState Bifrost writer is already started",
remediation = "Reuse the state's existing writer, or shut it down and create a new WyrdState to start again."
)]
SdkBifrostAlreadyStarted {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_409_TELEMETRY_PROVIDER_EXISTS] {message}")]
#[wyrd_error(
code = "WYRD_SDK_409_TELEMETRY_PROVIDER_EXISTS",
status = 409,
title = "A tracer provider is already installed",
remediation = "Add the Wyrd span exporter to the application's existing provider instead of calling start_telemetry."
)]
SdkTelemetryProviderExists {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_504_TELEMETRY_FLUSH_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_SDK_504_TELEMETRY_FLUSH_TIMEOUT",
status = 504,
title = "Telemetry flush timed out",
remediation = "Check that the Wyrd server's OTLP endpoint is reachable, raise the OpenTelemetry SDK export timeout if spans are large, and retry flush or shutdown."
)]
SdkTelemetryFlushTimeout {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_503_TELEMETRY_FLUSH_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_SDK_503_TELEMETRY_FLUSH_FAILED",
status = 503,
title = "Telemetry flush failed",
remediation = "Check the OpenTelemetry exporter's logs and the state's credentials, then retry flush or shutdown."
)]
SdkTelemetryFlushFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_BIFROST_NOT_STARTED] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_BIFROST_NOT_STARTED",
status = 400,
title = "WyrdState Bifrost writer is not started",
remediation = "Call start_bifrost on the state before opening a run and emitting observations."
)]
SdkBifrostNotStarted {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_409_BIFROST_CLOSED] {message}")]
#[wyrd_error(
code = "WYRD_SDK_409_BIFROST_CLOSED",
status = 409,
title = "WyrdState Bifrost writer is closed",
remediation = "Create a new WyrdState and start its Bifrost writer; a shut-down state stays closed to writes."
)]
SdkBifrostClosed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_INVALID_OBSERVATION] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_INVALID_OBSERVATION",
status = 400,
title = "Invalid observation input",
remediation = "Emit a flat object of boolean, integer, finite float, or string values, with valid feature names and media bindings."
)]
SdkInvalidObservation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SDK_400_CARD_KIND_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_SDK_400_CARD_KIND_MISMATCH",
status = 400,
title = "WyrdState Card kind mismatch",
remediation = "Use an alias for a Card of the expected kind."
)]
SdkCardKindMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_INVALID_VERSION_BLOCK] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_INVALID_VERSION_BLOCK",
status = 400,
title = "The version block is invalid for this operation",
remediation = "Check the error message for the specific constraint: Service and Agent cards require an exact semver pin; version components must fit i64; a scoped bump must stay within the authored range."
)]
RegistryInvalidVersionBlock {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_SPEC_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_SPEC_TOO_LARGE",
status = 400,
title = "Card spec exceeds MAX_SPEC_BYTES (256 KiB)",
remediation = "Reduce the spec size or split into multiple cards. If artifacts are inlined, move them to an Artifact card with object_store backing."
)]
RegistrySpecTooLarge {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_VERSION_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_VERSION_REQUIRED",
status = 400,
title = "`metadata.version` is present but empty",
remediation = "Provide a valid semver string (e.g. `\"1.0.0\"`) or omit `metadata.version` entirely to let the server auto-assign the next version."
)]
RegistryVersionRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_LIST_LIMIT_OUT_OF_RANGE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_LIST_LIMIT_OUT_OF_RANGE",
status = 400,
title = "List `limit` is zero or exceeds the per-tenant cap",
remediation = "Pass a `limit` in `1..=LIST_LIMIT_MAX` (currently 200)."
)]
RegistryListLimitOutOfRange {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_CARD_REF_UID_NOT_RESOLVABLE_HERE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_CARD_REF_UID_NOT_RESOLVABLE_HERE",
status = 400,
title = "`card_ref.uid` was populated for a registry lookup that resolves by (space, name, version)",
remediation = "Submit the request with `card_ref.uid = None`; the registry resolves by identity tuple. Use `get_card_by_uid` if you have a `card_uid`."
)]
RegistryCardRefUidNotResolvableHere {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_REQUIREMENT_NOT_RESOLVABLE_HERE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_REQUIREMENT_NOT_RESOLVABLE_HERE",
status = 400,
title = "`card_ref.version` is a Requirement; this endpoint accepts a Pin",
remediation = "Resolve the Requirement to a Pin via the future `resolve_card_ref` endpoint, or pass a Pin directly."
)]
RegistryRequirementNotResolvableHere {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_404_CARD_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_404_CARD_NOT_FOUND",
status = 404,
title = "No card matches the supplied identity",
remediation = "Check (space, kind, name, version) or the `card_uid` is correct and the card is registered in the current tenant."
)]
RegistryCardNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_422_UNRESOLVED_DEPENDENCY] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_422_UNRESOLVED_DEPENDENCY",
status = 422,
title = "Card reference could not be resolved",
remediation = "Register the referenced card in the current tenant before submitting this card."
)]
RegistryUnresolvedDependency {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_500_VERSION_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_500_VERSION_CONFLICT",
status = 500,
title = "Card uid mismatch for same identity",
remediation = "Internal invariant violation; report with the request id and audit_id."
)]
RegistryVersionConflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_409_SPEC_DRIFT] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_409_SPEC_DRIFT",
status = 409,
title = "Re-apply with the same identity but a different spec_hash; same-version cards are immutable",
remediation = "Bump `metadata.version` to publish a new spec, or revert your spec to match the registered version."
)]
RegistrySpecDrift {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_503_REGISTRY_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_503_REGISTRY_UNAVAILABLE",
status = 503,
title = "Transient Postgres or RLS misconfiguration; retry with backoff",
remediation = "Retry with exponential backoff (60s cap). If persistent, check Postgres connectivity, the tenant row, and the RLS role bindings."
)]
RegistryUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_DEPENDENCY_CYCLE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_DEPENDENCY_CYCLE",
status = 400,
title = "Card dependency cycle",
remediation = "Remove the cyclic CardRef dependency and submit an acyclic card graph."
)]
RegistryDependencyCycle {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_UNRESOLVED_INLINE_REFERENCE] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_UNRESOLVED_INLINE_REFERENCE",
status = 400,
title = "Unresolved inline CardRef",
remediation = "Resolve nested inline file references in the loader before submitting the card."
)]
RegistryUnresolvedInlineReference {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_HEAVY_ARTIFACT_NOT_SOLE_SUBMISSION] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_HEAVY_ARTIFACT_NOT_SOLE_SUBMISSION",
status = 400,
title = "Heavy artifact submission must be standalone",
remediation = "Register the artifact-bearing card alone, then reference it from a later composite request."
)]
RegistryHeavyArtifactNotSoleSubmission {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_UNRESOLVED_PATH_REF] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_UNRESOLVED_PATH_REF",
status = 400,
title = "Unresolved path CardRef",
remediation = "Expand path references in the loader before submitting the card."
)]
RegistryUnresolvedPathRef {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_IDEMPOTENCY_KEY_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_IDEMPOTENCY_KEY_REQUIRED",
status = 400,
title = "Idempotency key required",
remediation = "Provide a stable Idempotency-Key header and retry the registration."
)]
RegistryIdempotencyKeyRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_IDEMPOTENCY_KEY_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_IDEMPOTENCY_KEY_INVALID",
status = 400,
title = "Invalid idempotency key",
remediation = "Provide an 8-256 character Idempotency-Key without whitespace."
)]
RegistryIdempotencyKeyInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_DUPLICATE_SUBMISSION] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_DUPLICATE_SUBMISSION",
status = 400,
title = "Duplicate card submission",
remediation = "Submit each (kind, space, name, version) identity at most once per request."
)]
SpecDuplicateSubmission {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_MANIFEST_HASH_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_MANIFEST_HASH_MISMATCH",
status = 400,
title = "Artifact manifest hash mismatch",
remediation = "Recompute the JCS manifest hash and retry."
)]
RegistryManifestHashMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_INVALID_ARTIFACT_PATH] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_INVALID_ARTIFACT_PATH",
status = 400,
title = "Invalid artifact path",
remediation = "Use a relative forward-slash path without empty or parent-traversal segments."
)]
RegistryInvalidArtifactPath {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_UPLOAD_INTERRUPTED] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_UPLOAD_INTERRUPTED",
status = 400,
title = "Upload interrupted",
remediation = "Retry the upload while the registration operation remains pending."
)]
RegistryUploadInterrupted {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_400_CURSOR_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_400_CURSOR_MISMATCH",
status = 400,
title = "Cursor does not match filters",
remediation = "Discard the cursor and restart pagination with the current filters."
)]
RegistryCursorMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_409_IDEMPOTENCY_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_409_IDEMPOTENCY_CONFLICT",
status = 409,
title = "Idempotency key conflict",
remediation = "Use the original request with this key or choose a new key for different content."
)]
RegistryIdempotencyConflict {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_410_OPERATION_EXPIRED] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_410_OPERATION_EXPIRED",
status = 410,
title = "Registration operation expired",
remediation = "Start a new registration with a new idempotency key."
)]
RegistryOperationExpired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_REGISTRY_507_ARTIFACT_VERIFY_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_REGISTRY_507_ARTIFACT_VERIFY_FAILED",
status = 507,
title = "Artifact verification failed",
remediation = "Re-upload the artifact and retry finalization."
)]
RegistryArtifactVerifyFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_DUPLICATE_VERIFICATION_BINDING] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_DUPLICATE_VERIFICATION_BINDING",
status = 400,
title = "Duplicate verification binding",
remediation = "Remove the duplicate verified_by entry; one subject occurrence binds each Verifier version once."
)]
SpecDuplicateVerificationBinding {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_INVALID_VERIFIER_REF_KIND] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_INVALID_VERIFIER_REF_KIND",
status = 400,
title = "Invalid Verifier reference kind",
remediation = "Set verified_by[..].verifier to a Verifier CardRef."
)]
SpecInvalidVerifierRefKind {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_INVALID_BINDING_REF_KIND] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_INVALID_BINDING_REF_KIND",
status = 400,
title = "Invalid verification binding reference kind",
remediation = "Set runs_on to a Trigger CardRef or inline TriggerSpec, and each on_failure entry to an Operator CardRef or inline OperatorSpec."
)]
SpecInvalidBindingRefKind {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_DUPLICATE_BINDING_OPERATOR] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_DUPLICATE_BINDING_OPERATOR",
status = 400,
title = "Duplicate binding Operator",
remediation = "Remove the duplicate on_failure Operator from this binding."
)]
SpecDuplicateBindingOperator {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_VERIFICATION_404_BINDING_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_404_BINDING_NOT_FOUND",
status = 404,
title = "Verification binding not found",
remediation = "Read the owner Card's card.status.verification.binding_ids and retry with one of those binding IDs."
)]
VerificationBindingNotFound {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_404_RUN_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_404_RUN_NOT_FOUND",
status = 404,
title = "Verification run not found",
remediation = "Use the run_id returned by POST /v1/verification/runs in the same tenant."
)]
VerificationRunNotFound {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_400_INVALID_TARGET] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_400_INVALID_TARGET",
status = 400,
title = "Invalid verification run target",
remediation = "Target an existing binding, or an active Verifier Card UID plus an active subject Card UID in this tenant, whose implementation accepts the requested input."
)]
VerificationInvalidTarget {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_400_INVALID_WINDOW] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_400_INVALID_WINDOW",
status = 400,
title = "Invalid Drift window",
remediation = "Send a UTC half-open drift_window with start before end spanning at most 31 days."
)]
VerificationInvalidWindow {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_409_VERIFIER_NOT_READY] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_409_VERIFIER_NOT_READY",
status = 409,
title = "Verifier not ready",
remediation = "Poll the Verifier Card's card.status.verification.baseline until it is ready, then retry."
)]
VerificationNotReady {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_400_INPUT_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_400_INPUT_INVALID",
status = 400,
title = "Invalid verification input",
remediation = "Send { verifier_card_uid, card_uid, input } with input.kind eval_record (a context object) or drift_samples (columns of number, string, or null values)."
)]
VerificationInputInvalid {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_413_INPUT_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_413_INPUT_TOO_LARGE",
status = 413,
title = "Verification input too large",
remediation = "Keep the body within 1 MiB, drift_samples within 64 columns of 100,000 values, and eval_record.context within 256 KiB; split larger input across requests."
)]
VerificationInputTooLarge {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_404_TARGET_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_404_TARGET_NOT_FOUND",
status = 404,
title = "Verification target not found",
remediation = "Use an active Verifier Card UID and an active subject Card UID registered in the caller's tenant."
)]
VerificationTargetNotFound {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_409_BASELINE_NOT_READY] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_409_BASELINE_NOT_READY",
status = 409,
title = "Verification baseline not ready",
remediation = "Poll the Verifier Card's card.status.verification.baseline until it is ready, then retry."
)]
VerificationBaselineNotReady {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_409_BASELINE_LEGACY] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_409_BASELINE_LEGACY",
status = 409,
title = "Verification baseline legacy",
remediation = "Register a new Verifier version so its baseline is refitted under the current format."
)]
VerificationBaselineLegacy {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_422_INPUT_INCOMPATIBLE] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_422_INPUT_INCOMPATIBLE",
status = 422,
title = "Verification input incompatible",
remediation = "Supply every feature the Verifier scores, with the value type its baseline or profile expects."
)]
VerificationInputIncompatible {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_422_INPUT_UNSUPPORTED] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_422_INPUT_UNSUPPORTED",
status = 422,
title = "Verification input unsupported",
remediation = "Direct execution runs Drift and assertion or LLM-judge Evals only; run trace or agent assertion Evals through observations."
)]
VerificationInputUnsupported {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_502_DEPENDENCY_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_502_DEPENDENCY_FAILED",
status = 502,
title = "Verification dependency failed",
remediation = "The judge provider failed after the task's own retries; retry the request deliberately once the provider recovers."
)]
VerificationDependencyFailed {
message: String,
details: Value,
},
#[error("[WYRD_VERIFICATION_504_EXECUTION_TIMED_OUT] {message}")]
#[wyrd_error(
code = "WYRD_VERIFICATION_504_EXECUTION_TIMED_OUT",
status = 504,
title = "Verification execution timed out",
remediation = "Reduce the supplied input or the Verifier's judge work so one execution completes within 60 seconds."
)]
VerificationExecutionTimedOut {
message: String,
details: Value,
},
#[error("[WYRD_SPEC_400_INVALID_OPERATOR] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_INVALID_OPERATOR",
status = 400,
title = "Invalid Operator",
remediation = "Reference only failure-context fields in {{field}} templates, keep the URL origin literal https, and do not author server-owned headers."
)]
SpecInvalidOperator {
message: String,
details: Value,
},
#[error("[WYRD_SPEC_400_OPERATOR_CONNECTION_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_OPERATOR_CONNECTION_UNAVAILABLE",
status = 400,
title = "Operator connection unavailable",
remediation = "Create or re-enable an active connection of the Operator's provider with this name in the tenant, whose HTTP origin and auth scheme match the Operator."
)]
SpecOperatorConnectionUnavailable {
message: String,
details: Value,
},
#[error("[WYRD_OPERATOR_404_CONNECTION_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_OPERATOR_404_CONNECTION_NOT_FOUND",
status = 404,
title = "Operator connection not found",
remediation = "List GET /v1/operator-connections in the same tenant and retry with one of those connection IDs."
)]
OperatorConnectionNotFound {
message: String,
details: Value,
},
#[error("[WYRD_OPERATOR_409_CONNECTION_CONFLICT] {message}")]
#[wyrd_error(
code = "WYRD_OPERATOR_409_CONNECTION_CONFLICT",
status = 409,
title = "Operator connection name conflict",
remediation = "Choose another name, or PATCH the existing connection to rotate its secret."
)]
OperatorConnectionConflict {
message: String,
details: Value,
},
#[error("[WYRD_OPERATOR_400_INVALID_CONNECTION] {message}")]
#[wyrd_error(
code = "WYRD_OPERATOR_400_INVALID_CONNECTION",
status = 400,
title = "Invalid Operator connection",
remediation = "Send the provider-tagged shape matching the connection's provider with a non-empty secret and a normalized https origin."
)]
OperatorConnectionInvalid {
message: String,
details: Value,
},
#[error("[WYRD_OPERATOR_503_KEY_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_OPERATOR_503_KEY_UNAVAILABLE",
status = 503,
title = "Operator connection key unavailable",
remediation = "Restore the configured Operator key provider so it can supply the active key, then retry."
)]
OperatorKeyUnavailable {
message: String,
details: Value,
},
#[error("[WYRD_SPEC_400_TRIGGER_ACTIVATION_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_TRIGGER_ACTIVATION_MISMATCH",
status = 400,
title = "Trigger activation does not match Verifier",
remediation = "Use a schedule Trigger for a drift Verifier and an observations_ready Trigger for an eval Verifier."
)]
SpecTriggerActivationMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_INVALID_SERVICE_COMPONENT_KIND] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_INVALID_SERVICE_COMPONENT_KIND",
status = 400,
title = "Invalid Service component kind",
remediation = "Move Verifier, Trigger, Operator, Audit, and Source cards out of Service.components; attach a Verifier with verified_by."
)]
SpecInvalidServiceComponentKind {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_400_UNBOUND_VERIFIER_PEER] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_400_UNBOUND_VERIFIER_PEER",
status = 400,
title = "Unbound Verifier peer",
remediation = "Add the Verifier CardRef to the Service, a Service component, or a submitted Agent verified_by list, or register the shared Verifier separately."
)]
SpecUnboundVerifierPeer {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TS_501_HEAVY_UPLOAD_DEFERRED] {message}")]
#[wyrd_error(
code = "WYRD_TS_501_HEAVY_UPLOAD_DEFERRED",
status = 501,
title = "TypeScript heavy upload is deferred",
remediation = "Use the CLI or Python SDK for heavy artifact uploads."
)]
TsHeavyUploadDeferred {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AUTH_403_PRINCIPAL_ORPHANED] {message}")]
#[wyrd_error(
code = "WYRD_AUTH_403_PRINCIPAL_ORPHANED",
status = 403,
title = "The backing Service or Agent card has been soft-deleted",
remediation = "Re-register the card or use a different principal to obtain a token."
)]
PrincipalOrphaned {
message: String,
details: serde_json::Value,
},
#[error(transparent)]
#[wyrd_error(delegate)]
Storage {
#[from]
#[serde(flatten)]
error: WyrdStorageError,
},
#[error(transparent)]
#[wyrd_error(delegate)]
Vala {
#[from]
#[serde(flatten)]
error: crate::vala::error::BifrostError,
},
#[error("[WYRD_DATA_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_VALIDATION",
status = 400,
title = "DataCard validation failed",
remediation = "Fix the DataCard schema, artifact, stats, or interface metadata."
)]
DataValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DATA_400_UNKNOWN_DATA_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_UNKNOWN_DATA_TYPE",
status = 400,
title = "Unknown DataCard data type",
remediation = "Pass a supported data object, path, or explicit DataInterface."
)]
DataUnknownDataType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DATA_400_INVALID_SPLIT_RULE] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_INVALID_SPLIT_RULE",
status = 400,
title = "Invalid DataCard split rule",
remediation = "Use a supported split operator and reference schema columns that exist."
)]
DataInvalidSplitRule {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DATA_400_TARGET_COLUMN_UNKNOWN] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_TARGET_COLUMN_UNKNOWN",
status = 400,
title = "Unknown DataCard target column",
remediation = "Declare target columns that exist in the DataCard schema."
)]
DataTargetColumnUnknown {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DATA_400_INVALID_INTERFACE_OPTION] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_INVALID_INTERFACE_OPTION",
status = 400,
title = "Invalid DataCard interface option",
remediation = "Use one of the supported interface option values."
)]
DataInvalidInterfaceOption {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DATA_400_INTERFACE_METADATA_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_DATA_400_INTERFACE_METADATA_REQUIRED",
status = 400,
title = "DataCard interface metadata required",
remediation = "Pass an explicit DataInterface with enough metadata to describe the data source."
)]
DataInterfaceMetadataRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DRIFT_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_DRIFT_400_VALIDATION",
status = 400,
title = "Drift Verifier validation failed",
remediation = "Fix the drift implementation profile, condition, or signal fields."
)]
DriftValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DRIFT_400_SIGNAL_METHOD_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_DRIFT_400_SIGNAL_METHOD_MISMATCH",
status = 400,
title = "Drift signal/method mismatch",
remediation = "Use a signal variant supported by the chosen DriftMethod (see DriftSpec docs)."
)]
DriftSignalMethodMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_DRIFT_400_PROFILE_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_DRIFT_400_PROFILE_REQUIRED",
status = 400,
title = "Drift profile required",
remediation = "Attach the method-matching DriftProfile (PsiProfile, SpcProfile, or CustomProfile)."
)]
DriftProfileRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_UNKNOWN_MODEL_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_UNKNOWN_MODEL_TYPE",
status = 400,
title = "Unknown ModelCard model type",
remediation = "Pass a supported model object or an explicit ModelInterface."
)]
ModelUnknownModelType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SOURCE_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_SOURCE_400_VALIDATION",
status = 400,
title = "Source card validation failed",
remediation = "Fix Source card connection coordinates or auth env-var names and retry."
)]
SourceValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_ORIGIN_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_ORIGIN_400_VALIDATION",
status = 400,
title = "Card origin validation failed",
remediation = "Provide a non-empty repo and a 7-40 character lowercase hex commit; omit empty path values."
)]
OriginValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_VALIDATION",
status = 400,
title = "ModelCard validation failed",
remediation = "Fix the ModelCard signature, framework version, or interface configuration."
)]
ModelValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_MISSING_SIGNATURE] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_MISSING_SIGNATURE",
status = 400,
title = "Missing model signature",
remediation = "Pass signature fields explicitly or build a ModelSignature before registration."
)]
ModelMissingSignature {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_DTYPE_NORMALIZE_FAILED] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_DTYPE_NORMALIZE_FAILED",
status = 400,
title = "Could not normalize signature dtype",
remediation = "Provide a FieldSpec with a canonical Arrow logical dtype string."
)]
ModelDtypeNormalizeFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_SHAPE_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_SHAPE_INVALID",
status = 400,
title = "Invalid model signature shape",
remediation = "Use Dim::Fixed(n) with n > 0 or Dim::Dynamic(name) for unknown dimensions."
)]
ModelShapeInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_HF_REVISION_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_HF_REVISION_INVALID",
status = 400,
title = "Invalid HuggingFace revision",
remediation = "Pass a 7-40 character lowercase hex revision from the HuggingFace repo."
)]
ModelHfRevisionInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_HF_TASK_MISSING] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_HF_TASK_MISSING",
status = 400,
title = "Missing HuggingFace task",
remediation = "Set HuggingfaceMeta.hf_task before saving."
)]
ModelHfTaskMissing {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_400_CUSTOM_LOADER_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_400_CUSTOM_LOADER_INVALID",
status = 400,
title = "Invalid custom loader",
remediation = "Set CustomMeta.loader_module and CustomMeta.loader_class to non-empty values."
)]
ModelCustomLoaderInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_MODEL_501_SERIALIZER_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_MODEL_501_SERIALIZER_UNAVAILABLE",
status = 501,
title = "Required serializer unavailable",
remediation = "Install the Wyrd Python extra for the required framework serializer."
)]
ModelSerializerUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_INVALID_VARIABLE_NAME] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_INVALID_VARIABLE_NAME",
status = 400,
title = "Invalid variable name",
remediation = "Variable names must match ^[a-zA-Z_][a-zA-Z0-9_]*$."
)]
PromptInvalidVariableName {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_UNDECLARED_MEDIA_PLACEHOLDER] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_UNDECLARED_MEDIA_PLACEHOLDER",
status = 422,
title = "Undeclared media placeholder in request",
remediation = "Bind only media placeholders the Prompt references as ${media:name}."
)]
PromptUndeclaredMediaPlaceholder {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_MEDIA_PLACEHOLDER_NOT_ISOLATED] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_MEDIA_PLACEHOLDER_NOT_ISOLATED",
status = 422,
title = "Media placeholder is not isolated",
remediation = "Ensure each ${media:name} token occupies its own native text part before binding."
)]
PromptMediaPlaceholderNotIsolated {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_MEDIA_IN_SYSTEM_MESSAGE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_MEDIA_IN_SYSTEM_MESSAGE",
status = 400,
title = "Media placeholder in system message",
remediation = "Move media placeholders to user or assistant content."
)]
PromptMediaInSystemMessage {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_UNSUPPORTED_MEDIA_FOR_PROVIDER] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_UNSUPPORTED_MEDIA_FOR_PROVIDER",
status = 400,
title = "Unsupported media for provider",
remediation = "Use a media source supported by the selected provider."
)]
PromptUnsupportedMediaForProvider {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_INVALID_MEDIA_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_INVALID_MEDIA_TYPE",
status = 400,
title = "Invalid media type",
remediation = "Supply the required MIME type and a provider-supported URI."
)]
PromptInvalidMediaType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_MISSING_MEDIA_VARIABLE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_MISSING_MEDIA_VARIABLE",
status = 422,
title = "Media variable missing",
remediation = "Bind every declared media variable before rendering the prompt."
)]
PromptMissingMediaVariable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_MEDIA_NOT_REGULAR_FILE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_MEDIA_NOT_REGULAR_FILE",
status = 400,
title = "Media path is not a regular file",
remediation = "Pass a regular local file path."
)]
PromptMediaNotRegularFile {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_MEDIA_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_MEDIA_TOO_LARGE",
status = 400,
title = "Media file too large",
remediation = "Use a smaller media file or upload it to the provider and bind a file URI."
)]
PromptMediaTooLarge {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_MEDIA_INVALID_EXTENSION] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_MEDIA_INVALID_EXTENSION",
status = 400,
title = "Media file extension is unsupported",
remediation = "Use a supported image or document extension."
)]
PromptMediaInvalidExtension {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_500_MEDIA_IO] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_500_MEDIA_IO",
status = 500,
title = "Media IO failure",
remediation = "Inspect the OS error; usually missing path or permission denied."
)]
PromptMediaIo {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_EMPTY_MODEL] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_EMPTY_MODEL",
status = 400,
title = "Prompt model is empty",
remediation = "Set prompt.model to a non-empty provider model id."
)]
PromptEmptyModel {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_INVALID_RESPONSE_SCHEMA] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_INVALID_RESPONSE_SCHEMA",
status = 400,
title = "JsonSchema response_type schema is not an object",
remediation = "The schema of ResponseType::JsonSchema must be a JSON object."
)]
PromptInvalidResponseSchema {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_INVALID_OUTPUT_SCHEMA] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_INVALID_OUTPUT_SCHEMA",
status = 422,
title = "Invalid output schema",
remediation = "Pass a dict[str, type], raw JSON Schema object, ResponseFormat, or pydantic BaseModel subclass."
)]
PromptInvalidOutputSchema {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_PYDANTIC_REQUIRED] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_PYDANTIC_REQUIRED",
status = 422,
title = "Pydantic required",
remediation = "Install pydantic or use the stdlib dict[str, type] output schema path."
)]
PromptPydanticRequired {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_LOADER_BAD_EXTENSION] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_LOADER_BAD_EXTENSION",
status = 400,
title = "Loader extension not supported",
remediation = "Use .json, .yaml, or .yml."
)]
PromptLoaderBadExtension {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_500_LOADER_IO] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_500_LOADER_IO",
status = 500,
title = "Loader IO failure",
remediation = "Inspect the OS error; usually missing path or permission denied."
)]
PromptLoaderIo {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_500_SERIALIZE_REQUEST] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_500_SERIALIZE_REQUEST",
status = 500,
title = "Serialize native request failed during validation",
remediation = "The native request could not be serialized to JSON. Capture the spec and file an issue."
)]
PromptSerializeRequest {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_401_PROVIDER_AUTH] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_401_PROVIDER_AUTH",
status = 401,
title = "Provider authentication failed",
remediation = "Check the provider credentials environment variables."
)]
PromptProviderAuth {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_429_PROVIDER_RATE_LIMIT] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_429_PROVIDER_RATE_LIMIT",
status = 429,
title = "Provider rate limit",
remediation = "Back off, lower concurrency, or upgrade provider tier."
)]
PromptProviderRateLimit {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_502_PROVIDER_UPSTREAM] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_502_PROVIDER_UPSTREAM",
status = 502,
title = "Provider upstream error",
remediation = "Retry with backoff; provider returned a 5xx."
)]
PromptProviderUpstream {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_504_PROVIDER_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_504_PROVIDER_TIMEOUT",
status = 504,
title = "Provider request timed out",
remediation = "Retry; raise the per-request timeout if this is chronic."
)]
PromptProviderTimeout {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_PROVIDER_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_PROVIDER_MISMATCH",
status = 400,
title = "Request shape did not match the dialed provider",
remediation = "Pick the matching provider client or dispatch through the runtime registry."
)]
PromptProviderMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_INVALID",
status = 400,
title = "Invalid Prompt field",
remediation = "Fix the field named in details.field: author either portable system/messages/settings/response_format or native format/body, with provider and model once."
)]
PromptInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_SETTINGS_PROVIDER_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_SETTINGS_PROVIDER_MISMATCH",
status = 400,
title = "model_settings provider mismatch",
remediation = "Pass the settings type for this provider, or pass a dict."
)]
PromptSettingsProviderMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_400_SETTINGS_DECODE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_400_SETTINGS_DECODE",
status = 400,
title = "model_settings decode failed",
remediation = "Check model_settings field names and types; unknown fields are allowed and pass through."
)]
PromptSettingsDecode {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_MISSING_VARIABLE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_MISSING_VARIABLE",
status = 422,
title = "Render variable missing",
remediation = "Supply every declared variable when calling Prompt::render."
)]
PromptMissingVariable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_501_UNSUPPORTED_HANDOFF] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_501_UNSUPPORTED_HANDOFF",
status = 501,
title = "Unsupported message conversion",
remediation = "Use a same-provider handoff or convert manually."
)]
PromptUnsupportedHandoff {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_PROMPT_422_RESPONSE_DECODE] {message}")]
#[wyrd_error(
code = "WYRD_PROMPT_422_RESPONSE_DECODE",
status = 422,
title = "Response did not match requested schema",
remediation = "Inspect the provider response or relax the prompt response schema."
)]
PromptResponseDecode {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_VALIDATION] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_VALIDATION",
status = 422,
title = "AgentCard validation failed",
remediation = "Fix the Agent Card envelope, metadata, prompt reference, tools, or run_config fields."
)]
AgentValidation {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_MISSING_NAME] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_MISSING_NAME",
status = 422,
title = "AgentCard name is missing",
remediation = "Set a card name before saving or projecting the agent to a card envelope."
)]
AgentMissingName {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_MISSING_VERSION] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_MISSING_VERSION",
status = 422,
title = "AgentCard version is missing",
remediation = "Set a concrete semantic version before saving or projecting the agent to a card envelope."
)]
AgentMissingVersion {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_404_PROMPT_CARD] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_404_PROMPT_CARD",
status = 404,
title = "Prompt Card not found",
remediation = "Load or register the referenced Prompt Card in the local prompt registry before resolving the agent."
)]
AgentPromptCardNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_404_RUNTIME_LOCAL_TOOL_NOT_FOUND] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_404_RUNTIME_LOCAL_TOOL_NOT_FOUND",
status = 404,
title = "Runtime-local tool not found",
remediation = "Register the named tool in the local Skald tool registry before loading the Agent Card."
)]
AgentRuntimeLocalToolNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_RUNTIME_LOCAL_TOOLS_NOT_REGISTRABLE] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_RUNTIME_LOCAL_TOOLS_NOT_REGISTRABLE",
status = 422,
title = "Runtime-local tools are not registrable",
remediation = "Remove runtime-local tool names before registration; local save, load, and run remain available."
)]
AgentRuntimeLocalToolsNotRegistrable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_CALLBACK_RETURN_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_CALLBACK_RETURN_TYPE",
status = 422,
title = "Callback returned wrong type",
remediation = "Return None to continue, return a replacement value of the expected type, or raise an exception to abort the run."
)]
AgentCallbackReturnType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_499_CALLBACK_ABORTED] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_499_CALLBACK_ABORTED",
status = 499,
title = "Callback aborted run",
remediation = "Inspect the callback exception and either return None to continue or return a valid replacement value."
)]
AgentCallbackAborted {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_LOOP_MESSAGE_TYPE] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_LOOP_MESSAGE_TYPE",
status = 422,
title = "Loop message type mismatch",
remediation = "Re-render the conversation message against the active provider; loop messages must match the provider's message schema (OpenAi / Anthropic / Gemini / Custom)."
)]
AgentLoopMessageType {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_404_TOOL] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_404_TOOL",
status = 404,
title = "Tool not registered for agent",
remediation = "Register the tool before running the agent or remove the tool call from the provider response."
)]
AgentToolNotFound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_404_TOOL_NOT_IN_AGENT] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_404_TOOL_NOT_IN_AGENT",
status = 404,
title = "Tool not attached to agent",
remediation = "Attach the runtime-local tool to this agent or adjust the model response."
)]
AgentToolNotInAgent {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_409_PROVIDER_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_409_PROVIDER_MISMATCH",
status = 409,
title = "Provider mismatch",
remediation = "Run the agent with a prompt targeting the same provider as the agent's resolved prompt."
)]
AgentProviderMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_412_DELEGATION_DEPTH] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_412_DELEGATION_DEPTH",
status = 412,
title = "Agent delegation depth exceeded",
remediation = "Reduce nested agent-as-tool calls or restructure the workflow so the delegation chain stays within the cap."
)]
AgentDelegationDepth {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_INVALID_ARGUMENT] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_INVALID_ARGUMENT",
status = 422,
title = "Invalid argument",
remediation = "Correct the argument value; see details for the expected type or constraint."
)]
AgentInvalidArgument {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_PROMPT] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_PROMPT",
status = 422,
title = "Agent prompt rendering failed",
remediation = "Inspect the prompt template and the variables passed to the agent run."
)]
AgentPromptRender {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_STRUCTURED_DECODE] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_STRUCTURED_DECODE",
status = 422,
title = "Structured response decode failed",
remediation = "Inspect the model output; structured-output prompts must return a JSON object."
)]
AgentStructuredDecode {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_422_TOOL_ARGS] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_422_TOOL_ARGS",
status = 422,
title = "Tool arguments invalid",
remediation = "Adjust the provider-emitted tool arguments to match the tool input schema."
)]
AgentToolArgs {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_500_CALLBACK_PANIC] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_500_CALLBACK_PANIC",
status = 500,
title = "User-supplied callback panicked",
remediation = "Inspect the panic payload and fix the callback implementation; callbacks must not panic."
)]
AgentCallbackPanic {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_500_JOURNAL] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_500_JOURNAL",
status = 500,
title = "Journal append failed",
remediation = "Inspect the journal backend; for the no-op journal this should never fire."
)]
AgentJournalAppend {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_500_MAX_ITERATIONS] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_500_MAX_ITERATIONS",
status = 500,
title = "Agent exceeded max iterations",
remediation = "Raise the run's max_iterations or adjust the agent prompt and tools so the loop can terminate."
)]
AgentMaxIterations {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_502_PROVIDER] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_502_PROVIDER",
status = 502,
title = "Provider call failed",
remediation = "Inspect the provider backend and retry once it is healthy."
)]
AgentProviderCall {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_AGENT_504_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_AGENT_504_TIMEOUT",
status = 504,
title = "Agent run exceeded configured timeout",
remediation = "Increase the run timeout or reduce iteration count and tool latency."
)]
AgentTimeout {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SESSION_500_APPEND] {message}")]
#[wyrd_error(
code = "WYRD_SESSION_500_APPEND",
status = 500,
title = "Session memory append failed",
remediation = "Inspect the session backend and ensure the caller has write permission for the session."
)]
SessionAppendFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SESSION_500_RECENT] {message}")]
#[wyrd_error(
code = "WYRD_SESSION_500_RECENT",
status = 500,
title = "Session memory recent fetch failed",
remediation = "Inspect the session backend; ensure it is reachable and the session id exists."
)]
SessionRecentFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_400_INVALID_SCHEMA] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_400_INVALID_SCHEMA",
status = 400,
title = "Tool declaration malformed",
remediation = "Inspect the tool declaration name, description, and JSON input schema."
)]
ToolInvalidSchema {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_404_NOT_REGISTERED] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_404_NOT_REGISTERED",
status = 404,
title = "Tool name not registered",
remediation = "Register the tool before resolving it, or check the name against the registry's available tools."
)]
ToolNotRegistered {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_409_NAME_TAKEN] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_409_NAME_TAKEN",
status = 409,
title = "Tool name already registered",
remediation = "Pick a unique tool name or clear the registry before registering the replacement."
)]
ToolNameTaken {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_422_INPUT] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_422_INPUT",
status = 422,
title = "Tool invocation input did not match the declared input schema",
remediation = "Adjust the tool call arguments to match the schema the tool declares."
)]
ToolInvalidInput {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_500_CALL] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_500_CALL",
status = 500,
title = "Tool invocation failed during execution",
remediation = "Check the tool's underlying cause and fix the tool implementation or its inputs."
)]
ToolInvocationFailed {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TOOL_500_OUTPUT] {message}")]
#[wyrd_error(
code = "WYRD_TOOL_500_OUTPUT",
status = 500,
title = "Tool output could not be serialized to JSON",
remediation = "Ensure the tool output type serializes to a JSON-compatible value."
)]
ToolOutputSerialization {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_RUNTIME_404_PROVIDER] {message}")]
#[wyrd_error(
code = "WYRD_RUNTIME_404_PROVIDER",
status = 404,
title = "Provider not registered in the runtime",
remediation = "Register a provider client for the requested provider before dispatching the request."
)]
RuntimeProviderNotRegistered {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_RUNTIME_422_RESPONSE_DECODE] {message}")]
#[wyrd_error(
code = "WYRD_RUNTIME_422_RESPONSE_DECODE",
status = 422,
title = "Provider response did not match the prompt schema",
remediation = "Inspect the provider output against the prompt's declared response schema."
)]
RuntimeResponseDecode {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SERVER_503_NOT_READY] {message}")]
#[wyrd_error(
code = "WYRD_SERVER_503_NOT_READY",
status = 503,
title = "Server not ready",
remediation = "Wait for readiness probes to recover; check the `details.checks` object for the failing component and reason code."
)]
ServerNotReady {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SERVER_503_SERVICE_UNAVAILABLE] {message}")]
#[wyrd_error(
code = "WYRD_SERVER_503_SERVICE_UNAVAILABLE",
status = 503,
title = "Service unavailable",
remediation = "Retry with exponential backoff; the server is shedding load to protect inflight requests."
)]
ServiceUnavailable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SERVER_504_REQUEST_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_SERVER_504_REQUEST_TIMEOUT",
status = 504,
title = "Request timeout",
remediation = "Reduce the request size or split the operation; the server's per-request timeout was exceeded."
)]
RequestTimeout {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_SPEC_413_PAYLOAD_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_SPEC_413_PAYLOAD_TOO_LARGE",
status = 413,
title = "Payload too large",
remediation = "Reduce the request body size and retry."
)]
PayloadTooLarge {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TESTING_500_HARNESS_START] {message}")]
#[wyrd_error(
code = "WYRD_TESTING_500_HARNESS_START",
status = 500,
title = "Test harness failed to start",
remediation = "Check the test fixture logs"
)]
HarnessStart {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TESTING_500_HARNESS_BOUND] {message}")]
#[wyrd_error(
code = "WYRD_TESTING_500_HARNESS_BOUND",
status = 500,
title = "Test harness failed to bind listener",
remediation = "Check the port availability"
)]
HarnessBound {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_TESTING_500_HARNESS_BOOTSTRAP] {message}")]
#[wyrd_error(
code = "WYRD_TESTING_500_HARNESS_BOOTSTRAP",
status = 500,
title = "Test harness failed to bootstrap default principal",
remediation = "Check the role configuration"
)]
HarnessBootstrap {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CFG_400_INVALID_TOML] {message}")]
#[wyrd_error(
code = "WYRD_CFG_400_INVALID_TOML",
status = 400,
title = "wyrd.toml failed TOML syntax parse",
remediation = "Check the TOML syntax in your wyrd.toml file, or the file path if the read itself failed."
)]
CfgInvalidToml {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CFG_400_SCHEMA_MISMATCH] {message}")]
#[wyrd_error(
code = "WYRD_CFG_400_SCHEMA_MISMATCH",
status = 400,
title = "wyrd.toml schema mismatch",
remediation = "Compare the file against the documented wyrd.toml schema in architecture/wyrd-design.md."
)]
CfgSchemaMismatch {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CFG_400_NAME_DEFAULT_REJECTED] {message}")]
#[wyrd_error(
code = "WYRD_CFG_400_NAME_DEFAULT_REJECTED",
status = 400,
title = "wyrd.toml cannot default `name`",
remediation = "Remove the `name` key from [defaults] or [kind.<X>]. Every card must author its own name."
)]
CfgNameDefaultRejected {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_400_CONFIG_INVALID] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_400_CONFIG_INVALID",
status = 400,
title = "Client configuration is invalid",
remediation = "Correct the client configuration field named in details and rebuild the client."
)]
ClientConfigInvalid {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_401_NO_CREDENTIALS] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_401_NO_CREDENTIALS",
status = 401,
title = "No client credentials available",
remediation = "Set WYRD_ACCESS_TOKEN, WYRD_WORKLOAD_TOKEN with WYRD_TENANT, or WYRD_API_KEY, pass an explicit client credential, or add a default api_key to the Wyrd credentials file."
)]
ClientNoCredentials {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_401_SAVED_LOGIN_UNUSABLE] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_401_SAVED_LOGIN_UNUSABLE",
status = 401,
title = "Saved user login cannot be used",
remediation = "Run `wyrd auth login --server <url> --tenant <tenant>` again, select the intended tenant with the client tenant option or WYRD_TENANT, or pass an explicit credential. The details reason names the failure."
)]
ClientSavedLoginUnusable {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_413_PAYLOAD_TOO_LARGE] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_413_PAYLOAD_TOO_LARGE",
status = 413,
title = "Sealed batch exceeds the client message ceiling",
remediation = "Reduce the row or batch size, or raise max_message_bytes within the server's accepted ceiling."
)]
ClientPayloadTooLarge {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_422_ROW_DESERIALIZATION] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_422_ROW_DESERIALIZATION",
status = 422,
title = "Query row deserialization failed",
remediation = "Align the caller's row type with the columns and types the query selects."
)]
ClientRowDeserialization {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_429_QUEUE_FULL] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_429_QUEUE_FULL",
status = 429,
title = "Client ingestion queue is saturated",
remediation = "No row of the refused record was admitted: flush or back off, then resubmit the same record. Sustained refusals mean the producer outpaces delivery; every refusal bumps the drop counter."
)]
ClientQueueFull {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_503_TRANSPORT_DOWN] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_503_TRANSPORT_DOWN",
status = 503,
title = "Client transport is unavailable",
remediation = "Verify the endpoint, DNS, TLS material, and network reachability, then retry once the transport recovers."
)]
ClientTransportDown {
message: String,
details: serde_json::Value,
},
#[error("[WYRD_CLIENT_504_FLUSH_TIMEOUT] {message}")]
#[wyrd_error(
code = "WYRD_CLIENT_504_FLUSH_TIMEOUT",
status = 504,
title = "Client flush exceeded its drain deadline",
remediation = "Increase flush_timeout_ms or reduce the in-flight batch size so the drain can complete."
)]
ClientFlushTimeout {
message: String,
details: serde_json::Value,
},
}
impl WyrdError {
#[must_use]
pub fn problem(&self) -> WyrdProblem {
serde_json::from_value(self.as_problem_json())
.expect("WyrdError problem JSON must match WyrdProblem")
}
#[must_use]
pub fn as_problem_json(&self) -> serde_json::Value {
let (message, details) = self.message_details();
serde_json::json!({
"type": format!("https://wyrd.dev/problems/{}", self.code()),
"title": self.title(),
"status": self.status(),
"detail": message,
"code": self.code(),
"details": details,
"remediation": self.remediation(),
})
}
fn message_details(&self) -> (Cow<'_, str>, serde_json::Value) {
match self {
Self::Validation { message, details }
| Self::LoaderIo { message, details }
| Self::LoaderYamlSyntax { message, details }
| Self::LoaderInvalidEnvelope { message, details }
| Self::LoaderPathEscape { message, details }
| Self::LoaderPathAbsoluteAdvisory { message, details }
| Self::LoaderConfigLoadFailed { message, details }
| Self::NotFound { message, details }
| Self::Conflict { message, details }
| Self::Internal { message, details }
| Self::UpstreamFailure { message, details }
| Self::Timeout { message, details }
| Self::ValaTaskDagInvalid { message, details }
| Self::ValaEvalRefKindMismatch { message, details }
| Self::Unauthenticated { message, details }
| Self::TokenExpired { message, details }
| Self::InvalidToken { message, details }
| Self::BadTokenFormat { message, details }
| Self::UnsupportedGrantType { message, details }
| Self::InvalidState { message, details }
| Self::DeviceAuthorization { message, details }
| Self::InvalidNonce { message, details }
| Self::RefreshReused { message, details }
| Self::RefreshRevoked { message, details }
| Self::DelegationDepthExceededIssue { message, details }
| Self::UnrepresentableDelegation { message, details }
| Self::PrincipalKindCardKindMismatch { message, details }
| Self::InvalidCardRefVersion { message, details }
| Self::CredentialRevoked { message, details }
| Self::ApiKeyInvalid { message, details }
| Self::InvalidCardRef { message, details }
| Self::CardScopeTooLarge { message, details }
| Self::DelegationDepthExceededVerify { message, details }
| Self::PrincipalNotFound { message, details }
| Self::AdminConflict { message, details }
| Self::AdminNotFound { message, details }
| Self::HumanConnectionRequired { message, details }
| Self::ConnectionConflict { message, details }
| Self::ConnectionNotTested { message, details }
| Self::UnsupportedClientAuth { message, details }
| Self::GatewayInvalidConfiguration { message, details }
| Self::GatewayResourceNotFound { message, details }
| Self::GatewayResourceConflict { message, details }
| Self::GatewayInvalidRequest { message, details }
| Self::GatewayModelUnavailable { message, details }
| Self::GatewayCostUnbounded { message, details }
| Self::GatewayLimitExceeded { message, details }
| Self::GatewayBudgetExceeded { message, details }
| Self::GatewayUpstreamUnavailable { message, details }
| Self::GatewayDeadlineExceeded { message, details }
| Self::AuthVerifyUnavailable { message, details }
| Self::DiscoveryUnavailable { message, details }
| Self::MissingRequiredField { message, details }
| Self::PermissionUnauthenticated { message, details }
| Self::PermissionDeniedRbac { message, details }
| Self::RoleCorrupt { message, details }
| Self::BuiltinRoleImmutableName { message, details }
| Self::QueryInvalidSyntax { message, details }
| Self::QueryInvalidField { message, details }
| Self::QueryTooComplex { message, details }
| Self::SourceValidation { message, details }
| Self::OriginValidation { message, details }
| Self::DataValidation { message, details }
| Self::DataUnknownDataType { message, details }
| Self::DataInvalidSplitRule { message, details }
| Self::DataTargetColumnUnknown { message, details }
| Self::DataInvalidInterfaceOption { message, details }
| Self::DataInterfaceMetadataRequired { message, details }
| Self::DriftValidation { message, details }
| Self::DriftSignalMethodMismatch { message, details }
| Self::DriftProfileRequired { message, details }
| Self::ModelUnknownModelType { message, details }
| Self::ModelValidation { message, details }
| Self::ModelMissingSignature { message, details }
| Self::ModelDtypeNormalizeFailed { message, details }
| Self::ModelShapeInvalid { message, details }
| Self::ModelHfRevisionInvalid { message, details }
| Self::ModelHfTaskMissing { message, details }
| Self::ModelCustomLoaderInvalid { message, details }
| Self::ModelSerializerUnavailable { message, details }
| Self::PromptInvalidVariableName { message, details }
| Self::PromptUndeclaredMediaPlaceholder { message, details }
| Self::PromptMediaPlaceholderNotIsolated { message, details }
| Self::PromptMediaInSystemMessage { message, details }
| Self::PromptUnsupportedMediaForProvider { message, details }
| Self::PromptInvalidMediaType { message, details }
| Self::PromptMissingMediaVariable { message, details }
| Self::PromptMediaNotRegularFile { message, details }
| Self::PromptMediaTooLarge { message, details }
| Self::PromptMediaInvalidExtension { message, details }
| Self::PromptMediaIo { message, details }
| Self::PromptEmptyModel { message, details }
| Self::PromptInvalidResponseSchema { message, details }
| Self::PromptInvalidOutputSchema { message, details }
| Self::PromptPydanticRequired { message, details }
| Self::PromptLoaderBadExtension { message, details }
| Self::PromptLoaderIo { message, details }
| Self::PromptSerializeRequest { message, details }
| Self::PromptProviderAuth { message, details }
| Self::PromptProviderRateLimit { message, details }
| Self::PromptProviderUpstream { message, details }
| Self::PromptProviderTimeout { message, details }
| Self::PromptProviderMismatch { message, details }
| Self::PromptSettingsProviderMismatch { message, details }
| Self::PromptSettingsDecode { message, details }
| Self::PromptMissingVariable { message, details }
| Self::PromptUnsupportedHandoff { message, details }
| Self::PromptResponseDecode { message, details }
| Self::PromptInvalid { message, details }
| Self::AgentValidation { message, details }
| Self::AgentMissingName { message, details }
| Self::AgentMissingVersion { message, details }
| Self::AgentPromptCardNotFound { message, details }
| Self::AgentRuntimeLocalToolNotFound { message, details }
| Self::AgentRuntimeLocalToolsNotRegistrable { message, details }
| Self::AgentCallbackReturnType { message, details }
| Self::AgentCallbackAborted { message, details }
| Self::AgentLoopMessageType { message, details }
| Self::CfgInvalidToml { message, details }
| Self::CfgSchemaMismatch { message, details }
| Self::CfgNameDefaultRejected { message, details }
| Self::RegistryInvalidCardSpec { message, details }
| Self::SdkUnhydratedArtifact { message, details }
| Self::SdkRuntimeHydrationFailed { message, details }
| Self::SdkInvalidStateBundle { message, details }
| Self::SdkUnknownAlias { message, details }
| Self::SdkUnknownVerifier { message, details }
| Self::SdkBifrostAlreadyStarted { message, details }
| Self::SdkTelemetryProviderExists { message, details }
| Self::SdkTelemetryFlushTimeout { message, details }
| Self::SdkTelemetryFlushFailed { message, details }
| Self::SdkBifrostNotStarted { message, details }
| Self::SdkBifrostClosed { message, details }
| Self::SdkInvalidObservation { message, details }
| Self::SdkCardKindMismatch { message, details }
| Self::RegistryInvalidVersionBlock { message, details }
| Self::RegistrySpecTooLarge { message, details }
| Self::RegistryVersionRequired { message, details }
| Self::RegistryListLimitOutOfRange { message, details }
| Self::RegistryCardRefUidNotResolvableHere { message, details }
| Self::RegistryRequirementNotResolvableHere { message, details }
| Self::RegistryCardNotFound { message, details }
| Self::RegistryUnresolvedDependency { message, details }
| Self::RegistryVersionConflict { message, details }
| Self::RegistrySpecDrift { message, details }
| Self::RegistryUnavailable { message, details }
| Self::RegistryDependencyCycle { message, details }
| Self::RegistryUnresolvedInlineReference { message, details }
| Self::RegistryHeavyArtifactNotSoleSubmission { message, details }
| Self::RegistryUnresolvedPathRef { message, details }
| Self::RegistryIdempotencyKeyRequired { message, details }
| Self::RegistryIdempotencyKeyInvalid { message, details }
| Self::RegistryManifestHashMismatch { message, details }
| Self::RegistryInvalidArtifactPath { message, details }
| Self::RegistryUploadInterrupted { message, details }
| Self::RegistryCursorMismatch { message, details }
| Self::RegistryIdempotencyConflict { message, details }
| Self::RegistryOperationExpired { message, details }
| Self::RegistryArtifactVerifyFailed { message, details }
| Self::SpecDuplicateVerificationBinding { message, details }
| Self::SpecDuplicateSubmission { message, details }
| Self::SpecInvalidVerifierRefKind { message, details }
| Self::SpecInvalidBindingRefKind { message, details }
| Self::SpecDuplicateBindingOperator { message, details }
| Self::VerificationBindingNotFound { message, details }
| Self::VerificationRunNotFound { message, details }
| Self::VerificationInvalidTarget { message, details }
| Self::VerificationInvalidWindow { message, details }
| Self::VerificationNotReady { message, details }
| Self::VerificationInputInvalid { message, details }
| Self::VerificationInputTooLarge { message, details }
| Self::VerificationTargetNotFound { message, details }
| Self::VerificationBaselineNotReady { message, details }
| Self::VerificationBaselineLegacy { message, details }
| Self::VerificationInputIncompatible { message, details }
| Self::VerificationInputUnsupported { message, details }
| Self::VerificationDependencyFailed { message, details }
| Self::VerificationExecutionTimedOut { message, details }
| Self::SpecInvalidOperator { message, details }
| Self::SpecOperatorConnectionUnavailable { message, details }
| Self::OperatorConnectionNotFound { message, details }
| Self::OperatorConnectionConflict { message, details }
| Self::OperatorConnectionInvalid { message, details }
| Self::OperatorKeyUnavailable { message, details }
| Self::SpecTriggerActivationMismatch { message, details }
| Self::SpecInvalidServiceComponentKind { message, details }
| Self::SpecUnboundVerifierPeer { message, details }
| Self::TsHeavyUploadDeferred { message, details }
| Self::PrincipalOrphaned { message, details }
| Self::ServerNotReady { message, details }
| Self::ServiceUnavailable { message, details }
| Self::RequestTimeout { message, details }
| Self::PayloadTooLarge { message, details }
| Self::HarnessStart { message, details }
| Self::HarnessBound { message, details }
| Self::HarnessBootstrap { message, details }
| Self::AgentToolNotFound { message, details }
| Self::AgentToolNotInAgent { message, details }
| Self::AgentProviderMismatch { message, details }
| Self::AgentDelegationDepth { message, details }
| Self::AgentInvalidArgument { message, details }
| Self::AgentPromptRender { message, details }
| Self::AgentStructuredDecode { message, details }
| Self::AgentToolArgs { message, details }
| Self::AgentCallbackPanic { message, details }
| Self::AgentJournalAppend { message, details }
| Self::AgentMaxIterations { message, details }
| Self::AgentProviderCall { message, details }
| Self::AgentTimeout { message, details }
| Self::SessionAppendFailed { message, details }
| Self::SessionRecentFailed { message, details }
| Self::ToolInvalidSchema { message, details }
| Self::ToolNotRegistered { message, details }
| Self::ToolNameTaken { message, details }
| Self::ToolInvalidInput { message, details }
| Self::ToolInvocationFailed { message, details }
| Self::ToolOutputSerialization { message, details }
| Self::RuntimeProviderNotRegistered { message, details }
| Self::RuntimeResponseDecode { message, details }
| Self::ClientConfigInvalid { message, details }
| Self::ClientNoCredentials { message, details }
| Self::ClientSavedLoginUnusable { message, details }
| Self::ClientPayloadTooLarge { message, details }
| Self::ClientRowDeserialization { message, details }
| Self::ClientQueueFull { message, details }
| Self::ClientTransportDown { message, details }
| Self::ClientFlushTimeout { message, details } => {
(Cow::Borrowed(message.as_str()), details.clone())
}
Self::Storage { error } => (
Cow::Owned(error.to_string()),
serde_json::to_value(error).unwrap_or_else(|_| serde_json::json!({})),
),
Self::Vala { error } => (
Cow::Owned(error.to_string()),
serde_json::to_value(error).unwrap_or_else(|_| serde_json::json!({})),
),
}
}
#[must_use]
pub fn internal_from_skald(code: &'static str, message: String) -> Self {
Self::Internal {
message,
details: serde_json::json!({ "skald_code": code }),
}
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_invalid_card_spec(message: impl Into<String>) -> Self {
Self::RegistryInvalidCardSpec {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_invalid_version_block(message: impl Into<String>) -> Self {
Self::RegistryInvalidVersionBlock {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_spec_too_large(actual: usize, limit: usize) -> Self {
Self::RegistrySpecTooLarge {
message: format!("spec is {actual} bytes, exceeds {limit} byte limit"),
details: serde_json::json!({ "actual": actual, "limit": limit }),
}
}
#[must_use]
pub fn registry_version_required(message: impl Into<String>) -> Self {
Self::RegistryVersionRequired {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn query_invalid_syntax(
message: impl Into<String>,
offset: usize,
expected: impl Into<String>,
found: impl Into<String>,
) -> Self {
Self::QueryInvalidSyntax {
message: message.into(),
details: serde_json::json!({
"offset": offset,
"expected": expected.into(),
"found": found.into(),
}),
}
}
#[must_use]
pub fn query_empty_group() -> Self {
Self::QueryInvalidField {
message: "empty boolean group (and/or with no children)".into(),
details: serde_json::json!({ "reason": "empty_group" }),
}
}
#[must_use]
pub fn query_invalid_field_detail(
message: impl Into<String>,
detail: crate::query::QueryFieldErrorDetail<'_>,
) -> Self {
Self::QueryInvalidField {
message: message.into(),
details: detail.to_json(),
}
}
#[must_use]
pub fn query_too_complex(
message: impl Into<String>,
cap: &'static str,
actual: usize,
limit: usize,
) -> Self {
Self::QueryTooComplex {
message: message.into(),
details: serde_json::json!({ "cap": cap, "actual": actual, "limit": limit }),
}
}
#[must_use]
pub fn registry_card_not_found(message: impl Into<String>) -> Self {
Self::RegistryCardNotFound {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_spec_drift(
card_uid: impl Into<String>,
stored_hash: impl Into<String>,
submitted_hash: impl Into<String>,
) -> Self {
let stored = stored_hash.into();
let submitted = submitted_hash.into();
let uid = card_uid.into();
Self::RegistrySpecDrift {
message: format!(
"spec hash mismatch for card {uid}: stored {stored}, submitted {submitted}"
),
details: serde_json::json!({
"stored_hash": stored,
"submitted_hash": submitted,
}),
}
}
#[must_use]
pub fn registry_unavailable(message: impl Into<String>) -> Self {
Self::RegistryUnavailable {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_list_limit_out_of_range(limit: u32, max: u32) -> Self {
Self::RegistryListLimitOutOfRange {
message: format!("limit {limit} is out of range 1..={max}"),
details: serde_json::json!({ "limit": limit, "max": max }),
}
}
#[must_use]
pub fn registry_card_ref_uid_not_resolvable_here(message: impl Into<String>) -> Self {
Self::RegistryCardRefUidNotResolvableHere {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn registry_requirement_not_resolvable_here(message: impl Into<String>) -> Self {
Self::RegistryRequirementNotResolvableHere {
message: message.into(),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn from_card_uid_error(e: crate::ids::IdError) -> Self {
Self::Internal {
message: format!("card_uid construction failed: {e}"),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn from_spec_canonicalization(e: crate::envelope::SpecCanonicalizationError) -> Self {
Self::RegistryInvalidCardSpec {
message: format!("spec canonicalization failed: {e}"),
details: serde_json::json!({}),
}
}
#[must_use]
pub fn from_spec_serialization(e: serde_json::Error) -> Self {
Self::Internal {
message: format!("spec serialization failed: {e}"),
details: serde_json::json!({}),
}
}
}
impl From<skald_spec::SkaldError> for WyrdError {
fn from(error: skald_spec::SkaldError) -> Self {
let message = error.to_string();
match error {
skald_spec::SkaldError::MissingVariable(name) => Self::PromptMissingVariable {
message,
details: serde_json::json!({ "name": name }),
},
skald_spec::SkaldError::MediaPlaceholderNotFound { name } => {
Self::PromptUndeclaredMediaPlaceholder {
message,
details: serde_json::json!({ "name": name }),
}
}
skald_spec::SkaldError::MediaPlaceholderNotIsolated { name } => {
Self::PromptMediaPlaceholderNotIsolated {
message,
details: serde_json::json!({ "name": name }),
}
}
skald_spec::SkaldError::MediaInSystemMessage { name } => {
Self::PromptMediaInSystemMessage {
message,
details: serde_json::json!({ "name": name }),
}
}
skald_spec::SkaldError::UnsupportedMediaForProvider { provider, kind } => {
Self::PromptUnsupportedMediaForProvider {
message,
details: serde_json::json!({
"provider": format!("{provider:?}"),
"kind": format!("{kind:?}"),
}),
}
}
skald_spec::SkaldError::InvalidMediaType(source) => Self::PromptInvalidMediaType {
message,
details: serde_json::json!({ "source": source }),
},
skald_spec::SkaldError::MissingMediaVariable { name } => {
Self::PromptMissingMediaVariable {
message,
details: serde_json::json!({ "name": name }),
}
}
skald_spec::SkaldError::MediaNotRegularFile { path } => {
Self::PromptMediaNotRegularFile {
message,
details: serde_json::json!({ "path": path }),
}
}
skald_spec::SkaldError::MediaTooLarge { path, size, limit } => {
Self::PromptMediaTooLarge {
message,
details: serde_json::json!({ "path": path, "size": size, "limit": limit }),
}
}
skald_spec::SkaldError::MediaInvalidExtension { path, kind } => {
Self::PromptMediaInvalidExtension {
message,
details: serde_json::json!({ "path": path, "kind": format!("{kind:?}") }),
}
}
skald_spec::SkaldError::MediaIo(source) => Self::PromptMediaIo {
message,
details: serde_json::json!({ "source": source }),
},
skald_spec::SkaldError::UnsupportedConversion { src, dst } => {
Self::PromptUnsupportedHandoff {
message,
details: serde_json::json!({
"src": format!("{src:?}"),
"dst": format!("{dst:?}"),
}),
}
}
skald_spec::SkaldError::SettingsProviderMismatch { expected, got } => {
Self::PromptSettingsProviderMismatch {
message,
details: serde_json::json!({
"expected": format!("{expected:?}"),
"got": format!("{got:?}"),
}),
}
}
skald_spec::SkaldError::SettingsDecode {
provider,
message: source,
} => Self::PromptSettingsDecode {
message,
details: serde_json::json!({
"provider": format!("{provider:?}"),
"source": source,
}),
},
skald_spec::SkaldError::PromptInvalid {
field,
message: source,
} => Self::PromptInvalid {
message,
details: serde_json::json!({ "field": field, "source": source }),
},
other => Self::internal_from_skald(other.code(), other.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::WyrdError;
const PYTHON_BOUNDARY_CODES: &[(&str, u16)] = &[
("WYRD_AGENT_404_TOOL", 404),
("WYRD_AGENT_404_TOOL_NOT_IN_AGENT", 404),
("WYRD_AGENT_409_PROVIDER_MISMATCH", 409),
("WYRD_AGENT_412_DELEGATION_DEPTH", 412),
("WYRD_AGENT_422_INVALID_ARGUMENT", 422),
("WYRD_AGENT_422_LOOP_MESSAGE_TYPE", 422),
("WYRD_AGENT_422_PROMPT", 422),
("WYRD_AGENT_422_STRUCTURED_DECODE", 422),
("WYRD_AGENT_422_TOOL_ARGS", 422),
("WYRD_AGENT_500_CALLBACK_PANIC", 500),
("WYRD_AGENT_500_JOURNAL", 500),
("WYRD_AGENT_500_MAX_ITERATIONS", 500),
("WYRD_AGENT_502_PROVIDER", 502),
("WYRD_AGENT_504_TIMEOUT", 504),
("WYRD_SESSION_500_APPEND", 500),
("WYRD_SESSION_500_RECENT", 500),
("WYRD_TOOL_400_INVALID_SCHEMA", 400),
("WYRD_TOOL_404_NOT_REGISTERED", 404),
("WYRD_TOOL_409_NAME_TAKEN", 409),
("WYRD_TOOL_422_INPUT", 422),
("WYRD_TOOL_500_CALL", 500),
("WYRD_TOOL_500_OUTPUT", 500),
("WYRD_RUNTIME_404_PROVIDER", 404),
("WYRD_RUNTIME_422_RESPONSE_DECODE", 422),
("WYRD_CLIENT_400_CONFIG_INVALID", 400),
("WYRD_CLIENT_401_NO_CREDENTIALS", 401),
("WYRD_CLIENT_401_SAVED_LOGIN_UNUSABLE", 401),
("WYRD_CLIENT_413_PAYLOAD_TOO_LARGE", 413),
("WYRD_CLIENT_422_ROW_DESERIALIZATION", 422),
("WYRD_CLIENT_429_QUEUE_FULL", 429),
("WYRD_CLIENT_503_TRANSPORT_DOWN", 503),
("WYRD_CLIENT_504_FLUSH_TIMEOUT", 504),
];
#[test]
fn python_boundary_codes_resolve_through_the_catalog() {
for (code, status) in PYTHON_BOUNDARY_CODES {
let error = WyrdError::from_code(
code,
"boundary failure".to_owned(),
serde_json::json!({ "field": "value" }),
)
.unwrap_or_else(|| panic!("{code} has no derive-backed catalog variant"));
assert_eq!(error.code(), *code, "reconstruction changed the code");
assert_eq!(error.status(), *status, "{code} status regressed");
assert!(!error.title().is_empty(), "{code} has no title");
assert!(
error.remediation().len() > 20,
"{code} remediation is not actionable: {}",
error.remediation()
);
let problem = error.as_problem_json();
assert_eq!(problem["code"], *code);
assert_eq!(problem["status"], *status);
assert_eq!(problem["detail"], "boundary failure");
assert_eq!(problem["details"]["field"], "value");
}
}
#[test]
fn bifrost_schema_parse_is_catalog_owned() {
let error = WyrdError::Vala {
error: crate::vala::error::BifrostError::SchemaParse {
detail: "column 'age' is not an i64".to_owned(),
},
};
assert_eq!(error.code(), "WYRD_VALA_400_SCHEMA_PARSE");
assert_eq!(error.status(), 400);
assert_eq!(
error.as_problem_json()["details"]["data"]["detail"],
"column 'age' is not an i64"
);
}
#[test]
fn auth_codes_match_status() {
for error in auth_errors() {
let status = error
.code()
.split('_')
.nth(2)
.expect("error code has status segment")
.parse::<u16>()
.expect("status segment is numeric");
assert_eq!(status, error.status());
}
}
#[test]
fn auth_variant_problem_json_roundtrips() {
let error = WyrdError::Unauthenticated {
message: "missing bearer token".to_owned(),
details: serde_json::json!({ "header": "x-wyrd-access-token" }),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_AUTH_401_UNAUTHENTICATED");
assert_eq!(problem["status"], 401);
assert_eq!(problem["title"], "Not authenticated");
assert_eq!(
problem["remediation"],
"Present a valid Wyrd token as `X-Wyrd-Access-Token: Bearer <token>`."
);
assert_eq!(problem["detail"], "missing bearer token");
assert_eq!(problem["details"]["header"], "x-wyrd-access-token");
assert!(
problem["type"]
.as_str()
.expect("problem type is a string")
.ends_with("WYRD_AUTH_401_UNAUTHENTICATED")
);
}
#[test]
fn auth_variants_serde_tag() {
let error = WyrdError::PermissionDeniedRbac {
message: "card write is required".to_owned(),
details: serde_json::json!({ "required": { "resource": "cards", "action": "write" } }),
};
let value = serde_json::to_value(error).expect("auth error serializes");
assert_eq!(value["kind"], "permission_denied_rbac");
}
#[test]
fn permission_rbac_problem_json_carries_required() {
let error = WyrdError::PermissionDeniedRbac {
message: "principal lacks cards/write".to_owned(),
details: serde_json::json!({
"required": { "resource": "cards", "action": "write" },
"principal": "018f5f1f-0000-7000-8000-000000000001",
}),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_PERMISSION_403_DENIED_RBAC");
assert_eq!(problem["status"], 403);
assert_eq!(problem["title"], "Permission denied (RBAC)");
assert_eq!(problem["details"]["required"]["resource"], "cards");
assert_eq!(problem["details"]["required"]["action"], "write");
}
#[test]
fn api_key_invalid_remediation_names_issue_key() {
let error = WyrdError::ApiKeyInvalid {
message: "api key rejected".to_owned(),
details: serde_json::json!({}),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_AUTH_401_API_KEY_INVALID");
assert!(
problem["remediation"]
.as_str()
.expect("remediation is string")
.contains("wyrd auth issue-key")
);
}
#[test]
fn invalid_card_ref_version_is_distinct_from_verifier_code() {
let issuer_error = WyrdError::InvalidCardRefVersion {
message: "card_ref.version was a requirement".to_owned(),
details: serde_json::json!({ "got": "^1" }),
};
let verifier_error = WyrdError::InvalidCardRef {
message: "card_ref claim missing".to_owned(),
details: serde_json::json!({}),
};
assert_eq!(
issuer_error.code(),
"WYRD_AUTH_400_INVALID_CARD_REF_VERSION"
);
assert_eq!(issuer_error.status(), 400);
assert_eq!(verifier_error.code(), "WYRD_AUTH_401_INVALID_CARD_REF");
assert_eq!(verifier_error.status(), 401);
}
#[test]
fn token_expired_problem_json() {
let error = WyrdError::TokenExpired {
message: "token expired".to_owned(),
details: serde_json::json!({}),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_AUTH_401_TOKEN_EXPIRED");
assert_eq!(problem["status"], 401);
assert_eq!(problem["title"], "Token expired");
assert!(
problem["type"]
.as_str()
.expect("problem type is a string")
.ends_with("WYRD_AUTH_401_TOKEN_EXPIRED")
);
}
#[test]
fn token_expired_serde_tag() {
let error = WyrdError::TokenExpired {
message: "token expired".to_owned(),
details: serde_json::json!({}),
};
let value = serde_json::to_value(error).expect("auth error serializes");
assert_eq!(value["kind"], "token_expired");
}
#[test]
fn invalid_token_problem_json() {
let error = WyrdError::InvalidToken {
message: "signature mismatch".to_owned(),
details: serde_json::json!({}),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_AUTH_401_INVALID_TOKEN");
assert_eq!(problem["status"], 401);
assert_eq!(problem["title"], "Invalid token");
assert!(
problem["type"]
.as_str()
.expect("problem type is a string")
.ends_with("WYRD_AUTH_401_INVALID_TOKEN")
);
}
#[test]
fn invalid_token_serde_tag() {
let error = WyrdError::InvalidToken {
message: "signature mismatch".to_owned(),
details: serde_json::json!({}),
};
let value = serde_json::to_value(error).expect("auth error serializes");
assert_eq!(value["kind"], "invalid_token");
}
#[test]
fn credential_revoked_problem_json() {
let error = WyrdError::CredentialRevoked {
message: "api key wyrd_sk_abc revoked".to_owned(),
details: serde_json::json!({ "prefix": "wyrd_sk_abc" }),
};
let problem = error.as_problem_json();
assert_eq!(problem["code"], "WYRD_AUTH_401_CREDENTIAL_REVOKED");
assert_eq!(problem["status"], 401);
assert_eq!(problem["title"], "Credential revoked");
assert!(
problem["type"]
.as_str()
.expect("problem type is a string")
.ends_with("WYRD_AUTH_401_CREDENTIAL_REVOKED")
);
}
#[test]
fn credential_revoked_serde_tag() {
let error = WyrdError::CredentialRevoked {
message: "credential revoked".to_owned(),
details: serde_json::json!({}),
};
let value = serde_json::to_value(error).expect("auth error serializes");
assert_eq!(value["kind"], "credential_revoked");
}
fn auth_errors() -> Vec<WyrdError> {
vec![
WyrdError::Unauthenticated {
message: "missing bearer token".to_owned(),
details: serde_json::json!({}),
},
WyrdError::TokenExpired {
message: "token expired".to_owned(),
details: serde_json::json!({}),
},
WyrdError::InvalidToken {
message: "token rejected".to_owned(),
details: serde_json::json!({}),
},
WyrdError::BadTokenFormat {
message: "X-Wyrd-Access-Token is not a compact Wyrd JWT".to_owned(),
details: serde_json::json!({}),
},
WyrdError::UnsupportedGrantType {
message: "unsupported grant_type".to_owned(),
details: serde_json::json!({ "supported_grant_types": ["wyrd_api_key"] }),
},
WyrdError::InvalidState {
message: "state was missing or replayed".to_owned(),
details: serde_json::json!({}),
},
WyrdError::DeviceAuthorization {
message: "the person has not approved this device code yet".to_owned(),
details: serde_json::json!({ "error": "authorization_pending" }),
},
WyrdError::InvalidNonce {
message: "id token nonce mismatch".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RefreshReused {
message: "rotated refresh token reused".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RefreshRevoked {
message: "refresh token revoked".to_owned(),
details: serde_json::json!({}),
},
WyrdError::DelegationDepthExceededIssue {
message: "delegation chain would exceed depth 5".to_owned(),
details: serde_json::json!({ "max": 5 }),
},
WyrdError::UnrepresentableDelegation {
message: "delegated permissions cannot be represented".to_owned(),
details: serde_json::json!({}),
},
WyrdError::PrincipalKindCardKindMismatch {
message: "model cards cannot back service accounts".to_owned(),
details: serde_json::json!({ "card_kind": "model" }),
},
WyrdError::InvalidCardRefVersion {
message: "card_ref.version must be pinned".to_owned(),
details: serde_json::json!({ "got": "^1" }),
},
WyrdError::ApiKeyInvalid {
message: "api key rejected".to_owned(),
details: serde_json::json!({}),
},
WyrdError::InvalidCardRef {
message: "card_ref missing".to_owned(),
details: serde_json::json!({}),
},
WyrdError::DelegationDepthExceededVerify {
message: "delegation chain exceeds depth 5".to_owned(),
details: serde_json::json!({ "depth": 6, "max": 5 }),
},
WyrdError::PrincipalNotFound {
message: "requested subject not found".to_owned(),
details: serde_json::json!({}),
},
WyrdError::AuthVerifyUnavailable {
message: "permission resolver unavailable".to_owned(),
details: serde_json::json!({}),
},
WyrdError::DiscoveryUnavailable {
message: "oidc discovery unavailable".to_owned(),
details: serde_json::json!({}),
},
WyrdError::PermissionUnauthenticated {
message: "permission check requires authentication".to_owned(),
details: serde_json::json!({}),
},
WyrdError::PermissionDeniedRbac {
message: "permission missing".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RoleCorrupt {
message: "role permissions failed to decode".to_owned(),
details: serde_json::json!({ "role": "bad_role" }),
},
WyrdError::BuiltinRoleImmutableName {
message: "builtin role name cannot change".to_owned(),
details: serde_json::json!({ "constraint": "auth_builtin_role_immutable_name" }),
},
WyrdError::CredentialRevoked {
message: "credential revoked".to_owned(),
details: serde_json::json!({}),
},
WyrdError::PrincipalOrphaned {
message: "backing card deleted".to_owned(),
details: serde_json::json!({}),
},
]
}
}
#[cfg(test)]
mod wyrd_cfg_tests {
use super::*;
use serde_json::json;
#[test]
fn wyrd_cfg_400_invalid_toml_round_trip() {
let e = WyrdError::CfgInvalidToml {
message: "expected `=` after key".into(),
details: json!({ "path": "/tmp/wyrd.toml", "source": "toml" }),
};
let json_str = serde_json::to_string(&e).unwrap();
let back: WyrdError = serde_json::from_str(&json_str).unwrap();
assert_eq!(back.code(), "WYRD_CFG_400_INVALID_TOML");
assert_eq!(back.status(), 400);
assert!(!back.remediation().is_empty());
}
#[test]
fn wyrd_cfg_400_schema_mismatch_round_trip() {
let e = WyrdError::CfgSchemaMismatch {
message: "unknown field `defaut`".into(),
details: json!({
"path": "/tmp/wyrd.toml",
"serde_message": "unknown field `defaut`, expected one of …",
}),
};
let json_str = serde_json::to_string(&e).unwrap();
let back: WyrdError = serde_json::from_str(&json_str).unwrap();
assert_eq!(back.code(), "WYRD_CFG_400_SCHEMA_MISMATCH");
assert_eq!(back.status(), 400);
assert!(!back.remediation().is_empty());
}
#[test]
fn wyrd_cfg_400_name_default_rejected_round_trip() {
let e = WyrdError::CfgNameDefaultRejected {
message: "`name` may not be defaulted; remove from table `[defaults]`".into(),
details: json!({ "table": "[defaults]" }),
};
let json_str = serde_json::to_string(&e).unwrap();
let back: WyrdError = serde_json::from_str(&json_str).unwrap();
assert_eq!(back.code(), "WYRD_CFG_400_NAME_DEFAULT_REJECTED");
assert_eq!(back.status(), 400);
assert!(!back.remediation().is_empty());
}
}
#[cfg(test)]
mod error_registry_tests {
use crate::error::WyrdError;
fn registry_errors() -> Vec<WyrdError> {
vec![
WyrdError::RegistryInvalidCardSpec {
message: "unknown kind".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryInvalidVersionBlock {
message: "service cards require a pin version".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistrySpecTooLarge {
message: "spec exceeds 256 KiB".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryVersionRequired {
message: "metadata.version is required".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryListLimitOutOfRange {
message: "limit out of range".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryCardRefUidNotResolvableHere {
message: "uid not resolvable here".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryRequirementNotResolvableHere {
message: "requirement version not accepted here".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryCardNotFound {
message: "card not found".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryVersionConflict {
message: "version conflict".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistrySpecDrift {
message: "spec changed for immutable version".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryUnavailable {
message: "registry unavailable".to_owned(),
details: serde_json::json!({}),
},
WyrdError::PrincipalOrphaned {
message: "backing card deleted".to_owned(),
details: serde_json::json!({}),
},
]
}
#[test]
fn every_registry_variant_has_problem_json_round_trip() {
for err in registry_errors() {
let problem = err.as_problem_json();
let code = problem["code"].as_str().expect("code is a string");
assert!(
code.starts_with("WYRD_REGISTRY_") || code.starts_with("WYRD_AUTH_"),
"unexpected code prefix: {code}"
);
assert!(problem["status"].as_u64().unwrap() >= 400);
assert!(problem["title"].as_str().is_some());
}
}
#[test]
fn typed_problem_matches_problem_json() {
let error = WyrdError::RegistryUnavailable {
message: "transient".to_owned(),
details: serde_json::json!({"retryable": true}),
};
assert_eq!(
serde_json::to_value(error.problem()).expect("problem serializes"),
error.as_problem_json()
);
}
#[test]
fn registry_unavailable_status_is_503() {
let err = WyrdError::RegistryUnavailable {
message: "transient".to_owned(),
details: serde_json::json!({}),
};
let problem = err.as_problem_json();
assert_eq!(problem["status"], 503);
assert_eq!(problem["code"], "WYRD_REGISTRY_503_REGISTRY_UNAVAILABLE");
}
#[test]
fn spec_drift_status_is_409() {
let err = WyrdError::RegistrySpecDrift {
message: "drift".to_owned(),
details: serde_json::json!({}),
};
let problem = err.as_problem_json();
assert_eq!(problem["status"], 409);
assert_eq!(problem["code"], "WYRD_REGISTRY_409_SPEC_DRIFT");
}
#[test]
fn registry_card_not_found_status_is_404() {
let err = WyrdError::RegistryCardNotFound {
message: "not found".to_owned(),
details: serde_json::json!({}),
};
let problem = err.as_problem_json();
assert_eq!(problem["status"], 404);
assert_eq!(problem["code"], "WYRD_REGISTRY_404_CARD_NOT_FOUND");
}
#[test]
fn registry_400_variants_all_return_status_400() {
let cases = vec![
WyrdError::RegistryInvalidCardSpec {
message: "bad spec".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryInvalidVersionBlock {
message: "bad version".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistrySpecTooLarge {
message: "too large".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryVersionRequired {
message: "version required".to_owned(),
details: serde_json::json!({}),
},
WyrdError::RegistryListLimitOutOfRange {
message: "limit out of range".to_owned(),
details: serde_json::json!({}),
},
];
for err in cases {
let problem = err.as_problem_json();
assert_eq!(
problem["status"], 400,
"expected status 400 for {}",
problem["code"]
);
}
}
#[test]
fn registry_version_conflict_status_is_500() {
let err = WyrdError::RegistryVersionConflict {
message: "uid mismatch".to_owned(),
details: serde_json::json!({}),
};
let problem = err.as_problem_json();
assert_eq!(problem["status"], 500);
assert_eq!(problem["code"], "WYRD_REGISTRY_500_VERSION_CONFLICT");
}
#[test]
fn principal_orphaned_status_matches_embedded_code() {
let err = WyrdError::PrincipalOrphaned {
message: "card deleted".to_owned(),
details: serde_json::json!({}),
};
let problem = err.as_problem_json();
let code = problem["code"].as_str().expect("code is a string");
let status = problem["status"].as_u64().expect("status is a number");
let code_status: u64 = code
.split('_')
.find(|s| s.chars().all(|c| c.is_ascii_digit()))
.and_then(|s| s.parse().ok())
.expect("code must embed a numeric status");
assert_eq!(code_status, status);
}
}
#[cfg(test)]
mod error_storage_tests {
use std::collections::HashSet;
use crate::error::WyrdError;
use crate::error::storage::WyrdStorageError;
#[test]
fn storage_error_codes_statuses_and_titles_are_stable() {
let cases = storage_errors()
.into_iter()
.map(|error| (error.code(), error.status(), error.title()))
.collect::<Vec<_>>();
assert_eq!(
cases,
vec![
(
"WYRD_STORAGE_400_TENANT_PATH_MISMATCH",
400,
"Tenant path validation rejected upload init",
),
(
"WYRD_STORAGE_400_ARTIFACT_TOO_LARGE",
400,
"Artifact size exceeds backend capacity",
),
(
"WYRD_STORAGE_400_SHA256_INVALID",
400,
"Invalid SHA-256 in upload init",
),
(
"WYRD_STORAGE_400_SIZE_INVALID",
400,
"Invalid expected_size_bytes",
),
(
"WYRD_STORAGE_400_SHA256_MISMATCH",
400,
"Stored object SHA-256 does not match expected_sha256",
),
(
"WYRD_STORAGE_400_SIZE_MISMATCH",
400,
"Stored object byte length does not match expected_size_bytes",
),
(
"WYRD_STORAGE_400_INVALID_UPLOAD_ID",
400,
"Upload id not recognised",
),
(
"WYRD_STORAGE_400_INVALID_URI",
400,
"Source URI failed to parse",
),
(
"WYRD_STORAGE_400_TENANT_PREFIX_INVALID",
400,
"Source URI tenant prefix is not a UUIDv7",
),
(
"WYRD_STORAGE_403_UPLOAD_FOREIGN_TENANT",
403,
"Upload belongs to a different tenant",
),
(
"WYRD_STORAGE_404_OBJECT_NOT_FOUND",
404,
"Object not found in backend",
),
(
"WYRD_STORAGE_404_UPLOAD_NOT_FOUND",
404,
"Upload id not found",
),
(
"WYRD_STORAGE_409_UPLOAD_NOT_PENDING",
409,
"Cannot complete or abort a terminal upload",
),
(
"WYRD_STORAGE_409_ENCRYPTION_MISSING",
409,
"Server-side encryption verification failed",
),
(
"WYRD_STORAGE_412_PRECONDITION",
412,
"Object changed during download",
),
(
"WYRD_STORAGE_416_RANGE_NOT_SATISFIABLE",
416,
"Source changed mid-download",
),
(
"WYRD_STORAGE_500_BACKEND",
500,
"Backend storage operation failed",
),
(
"WYRD_STORAGE_500_CONFIG_INVALID",
500,
"Storage settings invalid",
),
(
"WYRD_STORAGE_500_CREDENTIAL_CHAIN",
500,
"Backend credential chain failed",
),
(
"WYRD_STORAGE_500_LIFECYCLE_MISSING",
500,
"S3 lifecycle missing required rule",
),
(
"WYRD_STORAGE_503_PRESIGN_EXPIRED",
503,
"Presigned URL expired",
),
(
"WYRD_STORAGE_503_BACKEND_UNAVAILABLE",
503,
"Backend transiently unavailable",
),
]
);
let unique = cases
.iter()
.map(|(code, _, _)| *code)
.collect::<HashSet<_>>();
assert_eq!(unique.len(), cases.len(), "duplicate storage error code");
}
#[test]
fn storage_error_lifts_into_problem_json_without_losing_code() {
let error: WyrdError = WyrdStorageError::ObjectNotFound {
storage_path: "tenant/cards/card/model.bin".to_owned(),
}
.into();
let problem = error.as_problem_json();
assert_eq!(error.code(), "WYRD_STORAGE_404_OBJECT_NOT_FOUND");
assert_eq!(error.status(), 404);
assert_eq!(problem["code"], "WYRD_STORAGE_404_OBJECT_NOT_FOUND");
assert_eq!(problem["status"], 404);
assert_eq!(problem["title"], "Object not found in backend");
assert_eq!(
problem["detail"],
"object not found at tenant/cards/card/model.bin"
);
assert_eq!(
problem["details"]["variant"], "object_not_found",
"storage details retain the storage enum discriminant"
);
}
fn storage_errors() -> Vec<WyrdStorageError> {
vec![
WyrdStorageError::TenantPathMismatch { detail: "x".into() },
WyrdStorageError::ArtifactTooLarge {
actual: 2,
limit: 1,
},
WyrdStorageError::Sha256Invalid { detail: "x".into() },
WyrdStorageError::SizeInvalid(0),
WyrdStorageError::Sha256Mismatch {
expected: "a".into(),
actual: "b".into(),
},
WyrdStorageError::SizeMismatch {
expected: 1,
actual: 2,
},
WyrdStorageError::InvalidUploadId { reason: "x".into() },
WyrdStorageError::InvalidUri { detail: "x".into() },
WyrdStorageError::TenantPrefixInvalid { detail: "x".into() },
WyrdStorageError::TenantPathForeign,
WyrdStorageError::ObjectNotFound {
storage_path: "x".into(),
},
WyrdStorageError::UploadNotFound,
WyrdStorageError::UploadNotPending,
WyrdStorageError::EncryptionMissing,
WyrdStorageError::PreconditionFailed,
WyrdStorageError::RangeNotSatisfiable,
WyrdStorageError::Backend { detail: "x".into() },
WyrdStorageError::ConfigInvalid { detail: "x".into() },
WyrdStorageError::CredentialChain {
backend: "s3".into(),
},
WyrdStorageError::LifecycleMissing,
WyrdStorageError::PresignExpired { detail: "x".into() },
WyrdStorageError::BackendUnavailable { status: 503 },
]
}
}
#[cfg(test)]
mod error_vala_tests {
use crate::error::WyrdError;
use crate::vala::error::BifrostError;
#[test]
fn bifrost_error_delegates_code_and_status_into_wyrd_error() {
let cases = vec![
(
BifrostError::FingerprintMismatch {
table: "tenant.events".to_owned(),
},
"WYRD_VALA_409_BIFROST_FINGERPRINT_MISMATCH",
409u16,
),
(
BifrostError::QueryInvalidSql {
detail: "not a SELECT".to_owned(),
},
"WYRD_VALA_400_QUERY_INVALID_SQL",
400,
),
(
BifrostError::QueryResultTooLarge,
"WYRD_VALA_413_QUERY_RESULT_TOO_LARGE",
413,
),
(
BifrostError::QueryMemoryRequestTooLarge,
"WYRD_VALA_422_QUERY_MEMORY_REQUEST_TOO_LARGE",
422,
),
(
BifrostError::QueryTimeout,
"WYRD_VALA_504_QUERY_TIMEOUT",
504,
),
];
for (wire, code, status) in cases {
let wire_code = wire.code();
let wire_status = wire.status();
let wire_title = wire.title();
let error: WyrdError = wire.into();
assert_eq!(error.code(), code);
assert_eq!(error.status(), status);
assert_eq!(error.code(), wire_code, "delegate forwards wire code");
assert_eq!(error.status(), wire_status, "delegate forwards wire status");
assert_eq!(error.title(), wire_title, "delegate forwards wire title");
}
}
#[test]
fn bifrost_error_lifts_into_problem_json_without_losing_code() {
let error: WyrdError = BifrostError::FingerprintMismatch {
table: "tenant.events".to_owned(),
}
.into();
let problem = error.as_problem_json();
assert_eq!(
problem["code"],
"WYRD_VALA_409_BIFROST_FINGERPRINT_MISMATCH"
);
assert_eq!(problem["status"], 409);
assert_eq!(problem["title"], "Schema fingerprint mismatch");
assert_eq!(
problem["details"]["variant"], "fingerprint_mismatch",
"vala details retain the wire enum discriminant"
);
}
}