use serde::{Deserialize, Serialize};
use crate::core::Spend;
use crate::core::{EffectKey, Sensitivity, Seq};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeError {
#[error("policy denied: {0}")]
PolicyDenied(#[from] PolicyError),
#[error("plan contract violation: {0}")]
PlanContract(String),
#[error("no skill provides capability '{0}'")]
NoProvider(String),
#[error(
"non-determinism at seq {seq}: journal has {expected}, replay recomputed {actual} \
— the deterministic zone is not deterministic; run quarantined"
)]
NonDeterminism {
seq: Seq,
expected: EffectKey,
actual: EffectKey,
},
#[error("journal integrity broken at seq {seq}: {detail}")]
ChainBroken { seq: Seq, detail: String },
#[error("fenced at run {run}: held epoch {held}, store is at {current}")]
Fenced {
run: String,
held: u64,
current: u64,
},
#[error("run {run} is leased by '{owner}' for another {remaining_secs}s")]
LeaseHeld {
run: String,
owner: String,
remaining_secs: u64,
},
#[error(transparent)]
Store(#[from] StoreError),
#[error(transparent)]
Encoding(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Disposition {
DidNotHappen,
InDoubt,
Landed,
}
impl Disposition {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::DidNotHappen => "did_not_happen",
Self::InDoubt => "in_doubt",
Self::Landed => "landed",
}
}
#[must_use]
pub fn is_definitely_safe_to_repeat(self) -> bool {
matches!(self, Self::DidNotHappen)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum EffectError {
#[error("driver '{driver}' unavailable: {detail}")]
Unavailable { driver: String, detail: String },
#[error("effect rejected: {0}")]
Rejected(String),
#[error("driver '{driver}' did not answer within {waited_ms}ms")]
Timeout { driver: String, waited_ms: u64 },
#[error("driver '{driver}' interrupted: {detail}")]
Interrupted { driver: String, detail: String },
#[error("effect consumed resources and failed: {detail}")]
Metered {
detail: String,
spend: Spend,
disposition: Disposition,
},
#[error("effect performed and failed: {0}")]
Performed(String),
#[error("effect output did not match its declared type: {0}")]
OutputShape(#[from] serde_json::Error),
#[error("{0}")]
Other(String),
}
impl EffectError {
#[must_use]
pub fn spend(&self) -> Spend {
match self {
Self::Metered { spend, .. } => *spend,
_ => Spend::default(),
}
}
#[must_use]
pub fn disposition(&self) -> Disposition {
match self {
Self::Metered { disposition, .. } => *disposition,
Self::Unavailable { .. } | Self::Rejected(_) => Disposition::DidNotHappen,
Self::OutputShape(_) | Self::Performed(_) => Disposition::Landed,
Self::Timeout { .. } | Self::Interrupted { .. } | Self::Other(_) => {
Disposition::InDoubt
}
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SkillError {
#[error("input did not match the declared schema: {0}")]
Input(String),
#[error(transparent)]
Step(#[from] StepError),
#[error("{0}")]
Other(String),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StepError {
#[error(transparent)]
Effect(#[from] EffectError),
#[error(transparent)]
Policy(#[from] PolicyError),
#[error(transparent)]
Store(#[from] StoreError),
#[error("{0}")]
Encoding(#[from] serde_json::Error),
#[error(
"effect {key} is undecidable ({detail}); recovery mode {recovery:?} forbids \
guessing — run quarantined"
)]
Undecidable {
key: EffectKey,
recovery: crate::core::Recovery,
detail: String,
},
#[error("non-determinism at seq {seq}: expected {expected}, recomputed {actual}")]
NonDeterminism {
seq: Seq,
expected: EffectKey,
actual: EffectKey,
},
#[error(transparent)]
Budget(#[from] crate::core::BudgetExceeded),
#[error("suspended: {0}")]
Suspended(crate::core::SuspendReason),
#[error("policy denied '{action}' on '{resource}': {reason}")]
Denied {
action: String,
resource: String,
reason: String,
},
#[error(
"replay overrun: journal is exhausted but the run requested {actual} — \
this build performs more effects than the recorded one"
)]
ReplayOverrun { actual: EffectKey },
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PolicyError {
#[error("principal '{principal}' may not '{action}' on '{resource}'")]
Denied {
principal: String,
action: String,
resource: String,
},
#[error("untrusted data may not reach mutating sink '{sink}' without declassification")]
TaintGate { sink: String },
#[error("sensitivity {actual:?} exceeds sink '{sink}' ceiling {ceiling:?}")]
EgressCeiling {
sink: String,
actual: Sensitivity,
ceiling: Sensitivity,
},
}
pub const REFUSED: &str = "this action was not permitted";
impl PolicyError {
#[must_use]
pub const fn for_model(&self) -> &'static str {
REFUSED
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StoreError {
#[error("backend: {0}")]
Backend(String),
#[error("not found: {0}")]
NotFound(String),
#[error(
"record of {bytes} bytes exceeds the {limit}-byte journal limit — \
journal a digest and keep the bytes outside the chain"
)]
RecordTooLarge { bytes: usize, limit: usize },
#[error("effect {0} already started in this run")]
DuplicateEffect(EffectKey),
#[error("case {case} has moved to {current}; the write was made against {expected}")]
CaseConflict {
case: String,
expected: u64,
current: u64,
},
#[error("fenced: run {run} is owned at epoch {current}, writer held {held}")]
Fenced {
run: String,
held: u64,
current: u64,
},
#[error("run {run} is leased by '{owner}' at epoch {epoch} for another {remaining_secs}s")]
LeaseHeld {
run: String,
owner: String,
epoch: u64,
remaining_secs: u64,
},
#[error("corrupt record at seq {seq}: {detail}")]
Corrupt { seq: Seq, detail: String },
#[error(transparent)]
Encoding(#[from] serde_json::Error),
}
impl RuntimeError {
#[must_use]
pub fn from_store(e: StoreError) -> Self {
match e {
StoreError::Fenced { run, held, current } => Self::Fenced { run, held, current },
StoreError::LeaseHeld {
run,
owner,
remaining_secs,
..
} => Self::LeaseHeld {
run,
owner,
remaining_secs,
},
StoreError::Corrupt { seq, detail } => Self::ChainBroken { seq, detail },
other => Self::Store(other),
}
}
#[must_use]
pub fn is_terminal_for_owner(&self) -> bool {
matches!(
self,
Self::Fenced { .. } | Self::NonDeterminism { .. } | Self::ChainBroken { .. }
)
}
}