Skip to main content

pointlock_runner/
error.rs

1//! Typed runner errors and blocked reasons.
2//!
3//! M0 iron rule: the error *shapes* are final API surface — content may be
4//! narrow, but nothing outside the M0 subset is silently accepted or
5//! silently skipped; it is refused with a typed error.
6
7use std::fmt;
8
9use pointlock_ir::{AlignmentReport, Hash, StepId};
10use pointlock_provider_kit::ProviderError;
11use pointlock_store::StoreError;
12
13/// Why a run is blocked awaiting a human decision (spine §6.7-B: the
14/// uncertain reconcile branch defaults to escalation; the `onResumeDrift` /
15/// human pipeline itself is not implemented in M0 — the shape is final).
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum BlockedReason {
18    /// The fate of a pending mutating intent could not be established
19    /// (`logUnavailable` / `startedNoTerminal`) and the step is neither
20    /// `readonly` nor declared `idempotent` — replay is forbidden (I2) and
21    /// adjudication requires a human (`repairWorld`, not in M0).
22    RequiresHuman {
23        /// The unresolved action intent's callId.
24        call_id: String,
25        /// Human-readable explanation of why a human is required.
26        detail: String,
27    },
28    /// A preflight probe found the world drifted (or unverifiable — an
29    /// exhausted probe chain is treated as drift, 07 §4.2 rule 2) and no
30    /// `onResumeDrift` disposition remained: none is declared, or the
31    /// declared ladder ran out (`maxTriggers`) without the world coming
32    /// back. The run records `runSuspended` and blocks for the operator.
33    Drifted {
34        /// The step whose preflight did not hold.
35        step_id: String,
36        /// Which probe failed and why.
37        detail: String,
38    },
39}
40
41impl fmt::Display for BlockedReason {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            BlockedReason::RequiresHuman { call_id, detail } => {
45                write!(
46                    f,
47                    "requires human adjudication (callId {call_id}): {detail}"
48                )
49            }
50            BlockedReason::Drifted { step_id, detail } => {
51                write!(
52                    f,
53                    "drifted: preflight of step '{step_id}' did not hold: {detail} \
54                     (no onResumeDrift disposition remained — declare one, or repair \
55                     the world and resume)"
56                )
57            }
58        }
59    }
60}
61
62/// Runner-level typed error (thiserror). Step-level failures are *not*
63/// errors — they fold into verdicts and the run finishes; this enum covers
64/// refusals (load checks, capability drift, M0 subset, resume gates) and
65/// infrastructure failures (store, provider).
66#[derive(Debug, thiserror::Error)]
67pub enum RunnerError {
68    /// The FlowIR's stored `irHash` does not match recomputation
69    /// (load check, spine §1.2: the runner recomputes and compares).
70    #[error("irHash mismatch: declared {declared}, computed {computed}")]
71    IrHashMismatch {
72        /// The hash stored on the FlowIR.
73        declared: Hash,
74        /// The hash recomputed from the FlowIR content.
75        computed: Hash,
76    },
77
78    /// A step's stored `effectHash`/`judgeHash` does not match
79    /// recomputation (the artifact is self-checkable, 02 §12.2).
80    #[error("step '{step_id}': stored {domain}Hash {declared} != computed {computed}")]
81    StepHashMismatch {
82        /// The offending step.
83        step_id: StepId,
84        /// `"effect"` or `"judge"`.
85        domain: &'static str,
86        /// The hash stored on the step.
87        declared: Hash,
88        /// The hash recomputed from the step content.
89        computed: Hash,
90    },
91
92    /// The IR uses a construct outside the current execution subset —
93    /// fail-closed, never silently skipped. The vocabulary that remains
94    /// outside: the re-invocation dispositions on call/human hosts
95    /// (07 §1's attempt-framed re-call and the fresh-request re-ask),
96    /// which the load gate keeps as typed refusals.
97    #[error("not in the M0 subset: {construct} (step: {step_id:?})")]
98    NotInM0Subset {
99        /// The offending step, when the construct is step-scoped.
100        step_id: Option<StepId>,
101        /// The refused construct, human-readable.
102        construct: String,
103    },
104
105    /// A human step's declared shape is inconsistent (runtime defense line
106    /// against hand-built IR; the compiler check phase refuses these
107    /// first): `confirm` without exactly two decision labels, `judge`
108    /// decisions outside the three-valued vocabulary, or `provideInput`
109    /// without an `outputSchema` (06 §2.2).
110    #[error("human step '{step_id}' is invalid: {reason}")]
111    InvalidHumanStep {
112        /// The offending step.
113        step_id: StepId,
114        /// What exactly is wrong.
115        reason: String,
116    },
117
118    /// The static call closure exceeds `maxCallDepth` (07 §1.3: 8 frames
119    /// including the root; the compiler already refuses this — the load
120    /// check is the runtime defense against hand-built IR).
121    #[error("call depth {depth} exceeds maxCallDepth {max} (07 §1.3)")]
122    CallDepthExceeded {
123        /// The offending static depth (frames, root included).
124        depth: usize,
125        /// The pinned maximum.
126        max: usize,
127    },
128
129    /// The subflow registry handed to the runner does not close over the
130    /// IR's `subflows` pins (a call target is missing, a hash key does not
131    /// self-verify, or a pin disagrees with the flow's own table).
132    #[error("subflow registry: {detail}")]
133    SubflowRegistry {
134        /// What exactly is wrong, human-readable.
135        detail: String,
136    },
137
138    /// The session attestation does not match the IR's `lockfileDigest`
139    /// (spine §4.1/§5 `capability_drift`): refuse to run or resume, never
140    /// silently degrade.
141    #[error(
142        "capability drift: IR lockfileDigest {expected} != session attestation {attested}; \
143         refusing to run"
144    )]
145    CapabilityDrift {
146        /// The digest the IR was bound against.
147        expected: Hash,
148        /// The digest the live session attested.
149        attested: Hash,
150    },
151
152    /// The run params are not usable (not an object, or a required param
153    /// without default is missing).
154    #[error("invalid params: {reason}")]
155    InvalidParams {
156        /// What is wrong with the params.
157        reason: String,
158    },
159
160    /// The supplied old FlowIR is not the IR the run executed.
161    #[error(
162        "old FlowIR mismatch: the run executed irHash {expected}, supplied IR computes {computed}"
163    )]
164    OldIrMismatch {
165        /// The irHash recorded in the checkpoint.
166        expected: Hash,
167        /// The irHash recomputed from the supplied old IR.
168        computed: Hash,
169    },
170
171    /// Re-execution of already-effective mutating steps requires explicit
172    /// human authorization (07 §5.4 unified gate). Fails closed; the
173    /// author releases entries by naming them in
174    /// `ResumeOptions::allow_mutating_reexec` (the CLI's repeatable
175    /// `--allow-mutating-reexec <stepId>`). The report carries whatever
176    /// remains gated.
177    #[error(
178        "resume requires explicit confirmation for {} mutating step(s) (07 §5.4); \
179         authorize each with --allow-mutating-reexec <stepId>",
180        report.requires_confirmation.len()
181    )]
182    RequiresConfirmation {
183        /// The alignment report whose `requiresConfirmation` entries name
184        /// the gated steps.
185        report: Box<AlignmentReport>,
186    },
187
188    /// A combination that is valid in the design but deliberately not
189    /// implemented in M0 (each site documents the pending incorporation).
190    #[error("not supported in M0: {detail}")]
191    M0Unsupported {
192        /// What exactly is unsupported.
193        detail: String,
194    },
195
196    /// Localized evidence bytes do not match the provider-declared sha256
197    /// (04 §4.3: evidence integrity is non-negotiable).
198    #[error(
199        "evidence integrity failure for asset {asset_id}: sha256 {actual} != declared {expected}"
200    )]
201    EvidenceIntegrity {
202        /// The provider asset id.
203        asset_id: String,
204        /// The sha256 the provider declared.
205        expected: String,
206        /// The sha256 of the bytes actually fetched.
207        actual: String,
208    },
209
210    /// Store-layer failure (SQLite / fold / IO).
211    #[error(transparent)]
212    Store(#[from] StoreError),
213
214    /// Provider-layer failure outside an action terminal (e.g. reconcile
215    /// or verdict write-back failed).
216    #[error(transparent)]
217    Provider(#[from] ProviderError),
218}