Skip to main content

capsule_runtime/
origin.rs

1//! Why a task woke the capsule, and how far its content is trusted.
2//!
3//! An origin is set by whoever enqueued the task. A trust class is *derived* from that origin
4//! and, for the two origins that cross a capsule boundary, from the trust of the sending
5//! capsule's own current task. No sender declares its trust class: [`TaskProvenance`] has no
6//! constructor that takes one on its own, so "derived, never declared" is a property of the type
7//! rather than a convention callers are asked to keep.
8//!
9//! Nothing in the runtime branches on trust: the value is recorded on `task_start` and left on
10//! the store state for the task's duration, and no task is refused, delayed or reordered for it.
11//! The origin half is not inert — it picks the queue lane a task waits in, so two tasks with the
12//! same trust can still run in a different order. See [`crate::lanes`].
13
14/// Header carrying the sending runtime's origin claim on an inter-capsule request.
15///
16/// Stamped by the sending *runtime*, never by a guest component: `murmur:message/send` has no
17/// field a capsule author could put an origin in.
18pub const PEER_ORIGIN_HEADER: &str = "x-murmur-task-origin";
19
20/// Header carrying the trust class of the sending capsule's own current task.
21pub const PEER_TRUST_HEADER: &str = "x-murmur-task-trust";
22
23/// Why the runtime woke the capsule.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TaskOrigin {
26    /// A person handed the capsule an instruction — a local `mur run`, a `task.md`.
27    User,
28    /// A message from another capsule, carrying that capsule's own trust class.
29    Peer,
30    /// A timer fired.
31    Schedule,
32    /// A webhook, chat message or PR comment — third-party text, so never trusted.
33    Event,
34    /// A sub-capsule or detached shell reporting that its work finished. Inherits like
35    /// [`TaskOrigin::Peer`]: it is the same boundary, seen from the other end.
36    Completion,
37    /// The runtime enqueued this for itself with no person in the loop — a retry, a sweep.
38    System,
39}
40
41impl TaskOrigin {
42    /// The lowercase wire spelling, as it appears in a header value and in a trace record.
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            Self::User => "user",
46            Self::Peer => "peer",
47            Self::Schedule => "schedule",
48            Self::Event => "event",
49            Self::Completion => "completion",
50            Self::System => "system",
51        }
52    }
53
54    /// Exact match against [`Self::as_str`]. No aliases and no case folding — an inbound header
55    /// arrives already lowercased, and every other caller is in-crate.
56    pub fn parse(value: &str) -> Option<Self> {
57        match value {
58            "user" => Some(Self::User),
59            "peer" => Some(Self::Peer),
60            "schedule" => Some(Self::Schedule),
61            "event" => Some(Self::Event),
62            "completion" => Some(Self::Completion),
63            "system" => Some(Self::System),
64            _ => None,
65        }
66    }
67}
68
69/// How far the content of a task is trusted.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum TrustClass {
72    Trusted,
73    Untrusted,
74}
75
76impl TrustClass {
77    /// The lowercase wire spelling, as it appears in a header value and in a trace record.
78    pub fn as_str(&self) -> &'static str {
79        match self {
80            Self::Trusted => "trusted",
81            Self::Untrusted => "untrusted",
82        }
83    }
84
85    /// Exact match against [`Self::as_str`], on the same terms as [`TaskOrigin::parse`].
86    pub fn parse(value: &str) -> Option<Self> {
87        match value {
88            "trusted" => Some(Self::Trusted),
89            "untrusted" => Some(Self::Untrusted),
90            _ => None,
91        }
92    }
93}
94
95/// An origin paired with the trust class derived from it.
96///
97/// Both fields are private and the only constructor is [`Self::derive`], so there is no way to
98/// pair an origin with a trust class that does not follow from it.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct TaskProvenance {
101    origin: TaskOrigin,
102    trust: TrustClass,
103}
104
105impl TaskProvenance {
106    /// Derive the trust class for `origin`.
107    ///
108    /// `inherited` is the trust of the sending capsule's own current task, and is consulted only
109    /// for [`TaskOrigin::Peer`] and [`TaskOrigin::Completion`] — the two origins that cross a
110    /// capsule boundary. Passing `None` for either means the sender made no claim, which
111    /// resolves to [`TrustClass::Untrusted`]: an absent claim is not a trusted one, so untrust
112    /// cannot launder itself by dropping the header at the first hop.
113    pub fn derive(origin: TaskOrigin, inherited: Option<TrustClass>) -> Self {
114        let trust = match origin {
115            TaskOrigin::User | TaskOrigin::Schedule | TaskOrigin::System => TrustClass::Trusted,
116            TaskOrigin::Event => TrustClass::Untrusted,
117            TaskOrigin::Peer | TaskOrigin::Completion => inherited.unwrap_or(TrustClass::Untrusted),
118        };
119        Self { origin, trust }
120    }
121
122    pub fn origin(&self) -> TaskOrigin {
123        self.origin
124    }
125
126    pub fn trust(&self) -> TrustClass {
127        self.trust
128    }
129}
130
131/// Classify a task arriving over the peer door from the two request headers.
132///
133/// Only `peer` and `completion` are accepted from the wire. `user`, `schedule`, `event` and
134/// `system` are enqueued locally by the CLI or by the runtime and never legitimately arrive over
135/// HTTP, so a caller naming one — or naming anything unrecognised, or sending no origin header
136/// at all — is classified `event` / `untrusted` and its trust header is not read. No HTTP caller
137/// can talk itself into a trusted class it was not given by a murmur runtime.
138///
139/// Provenance is the caller's claim, whether or not the door authenticates. A door declaring
140/// `network.authentication` decides *who may call*; it does not bind a class to a credential, so
141/// any caller it lets in that claims `peer` + `trusted` gets what a genuine trusted peer gets, and
142/// on a public door that is any caller at all. The boundary closed here is untrust laundering
143/// across an honest chain.
144pub fn from_wire(origin_header: Option<&str>, trust_header: Option<&str>) -> TaskProvenance {
145    let origin = origin_header.and_then(TaskOrigin::parse);
146    match origin {
147        Some(origin @ (TaskOrigin::Peer | TaskOrigin::Completion)) => {
148            TaskProvenance::derive(origin, trust_header.and_then(TrustClass::parse))
149        }
150        _ => TaskProvenance::derive(TaskOrigin::Event, None),
151    }
152}
153
154/// The provenance a runtime stamps on an outbound peer message, given its own current task.
155///
156/// The origin is always [`TaskOrigin::Peer`]; a completion is stamped by
157/// [`stamp_for_completion`] instead, which is the other origin the inbound rule accepts. The
158/// trust class is the sending task's own, so untrust survives the hop. `None`, meaning no task is
159/// in scope, stamps [`TrustClass::Untrusted`].
160pub fn stamp_for_peer(sender_task: Option<TaskProvenance>) -> TaskProvenance {
161    TaskProvenance::derive(TaskOrigin::Peer, sender_task.map(|task| task.trust()))
162}
163
164/// The provenance a runtime stamps on a completion it posts to the capsule that delegated to it.
165///
166/// `inherited` is the trust class of the parent task that made the delegation, carried to the
167/// child in its [`crate::delegation::SpawnerHandle`]. It is not a second decision about trust:
168/// the class was derived when the delegating task arrived, and this hands the same class back so
169/// the completion inherits it rather than being reclassified as fresh. `None` stamps
170/// [`TrustClass::Untrusted`], the safe class.
171pub fn stamp_for_completion(inherited: Option<TrustClass>) -> TaskProvenance {
172    TaskProvenance::derive(TaskOrigin::Completion, inherited)
173}
174
175/// How far an installed artifact's text is trusted, from who pinned it in `murmur.lock`.
176///
177/// A pin the operator wrote is [`TrustClass::Trusted`]; a pin a running capsule fetched through
178/// `manage.pull()` is [`TrustClass::Untrusted`] whatever session it names, an empty one
179/// included. The lock origin is the only input, so no runtime pin can come out trusted.
180///
181/// Like a task's trust class this is a marker, not a control: it decides whether the artifact's
182/// tool-array entry carries the runtime-origin marker and whether its skill guidance is fenced,
183/// and nothing is refused, delayed or regranted for it.
184pub fn artifact_trust(origin: &murmur_artifact::LockOrigin) -> TrustClass {
185    match origin {
186        murmur_artifact::LockOrigin::Operator => TrustClass::Trusted,
187        murmur_artifact::LockOrigin::Runtime { .. } => TrustClass::Untrusted,
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    const ALL_ORIGINS: [TaskOrigin; 6] = [
196        TaskOrigin::User,
197        TaskOrigin::Peer,
198        TaskOrigin::Schedule,
199        TaskOrigin::Event,
200        TaskOrigin::Completion,
201        TaskOrigin::System,
202    ];
203
204    #[test]
205    fn origin_round_trips_through_its_wire_spelling() {
206        for origin in ALL_ORIGINS {
207            assert_eq!(TaskOrigin::parse(origin.as_str()), Some(origin));
208        }
209        assert_eq!(TaskOrigin::parse("User"), None);
210        assert_eq!(TaskOrigin::parse("not-an-origin"), None);
211        assert_eq!(TaskOrigin::parse(""), None);
212    }
213
214    #[test]
215    fn trust_round_trips_through_its_wire_spelling() {
216        for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
217            assert_eq!(TrustClass::parse(trust.as_str()), Some(trust));
218        }
219        assert_eq!(TrustClass::parse("Trusted"), None);
220        assert_eq!(TrustClass::parse("maybe"), None);
221    }
222
223    /// The full derivation table: six origins crossed with every inherited value.
224    #[test]
225    fn derive_covers_every_origin_and_inherited_pair() {
226        let cases: [(TaskOrigin, Option<TrustClass>, TrustClass); 18] = [
227            (TaskOrigin::User, None, TrustClass::Trusted),
228            (
229                TaskOrigin::User,
230                Some(TrustClass::Trusted),
231                TrustClass::Trusted,
232            ),
233            (
234                TaskOrigin::User,
235                Some(TrustClass::Untrusted),
236                TrustClass::Trusted,
237            ),
238            (TaskOrigin::Schedule, None, TrustClass::Trusted),
239            (
240                TaskOrigin::Schedule,
241                Some(TrustClass::Trusted),
242                TrustClass::Trusted,
243            ),
244            (
245                TaskOrigin::Schedule,
246                Some(TrustClass::Untrusted),
247                TrustClass::Trusted,
248            ),
249            (TaskOrigin::System, None, TrustClass::Trusted),
250            (
251                TaskOrigin::System,
252                Some(TrustClass::Trusted),
253                TrustClass::Trusted,
254            ),
255            (
256                TaskOrigin::System,
257                Some(TrustClass::Untrusted),
258                TrustClass::Trusted,
259            ),
260            (TaskOrigin::Event, None, TrustClass::Untrusted),
261            (
262                TaskOrigin::Event,
263                Some(TrustClass::Trusted),
264                TrustClass::Untrusted,
265            ),
266            (
267                TaskOrigin::Event,
268                Some(TrustClass::Untrusted),
269                TrustClass::Untrusted,
270            ),
271            (TaskOrigin::Peer, None, TrustClass::Untrusted),
272            (
273                TaskOrigin::Peer,
274                Some(TrustClass::Trusted),
275                TrustClass::Trusted,
276            ),
277            (
278                TaskOrigin::Peer,
279                Some(TrustClass::Untrusted),
280                TrustClass::Untrusted,
281            ),
282            (TaskOrigin::Completion, None, TrustClass::Untrusted),
283            (
284                TaskOrigin::Completion,
285                Some(TrustClass::Trusted),
286                TrustClass::Trusted,
287            ),
288            (
289                TaskOrigin::Completion,
290                Some(TrustClass::Untrusted),
291                TrustClass::Untrusted,
292            ),
293        ];
294        for (origin, inherited, expected) in cases {
295            let provenance = TaskProvenance::derive(origin, inherited);
296            assert_eq!(
297                provenance.trust(),
298                expected,
299                "{origin:?} with {inherited:?} should be {expected:?}"
300            );
301            assert_eq!(provenance.origin(), origin);
302        }
303    }
304
305    #[test]
306    fn wire_accepts_only_peer_and_completion() {
307        for accepted in ["peer", "completion"] {
308            let provenance = from_wire(Some(accepted), Some("trusted"));
309            assert_eq!(provenance.origin(), TaskOrigin::parse(accepted).unwrap());
310            assert_eq!(provenance.trust(), TrustClass::Trusted);
311        }
312    }
313
314    #[test]
315    fn wire_refuses_locally_only_and_unrecognised_origins() {
316        for refused in ["user", "schedule", "event", "system", "not-an-origin", ""] {
317            let provenance = from_wire(Some(refused), Some("trusted"));
318            assert_eq!(
319                provenance.origin(),
320                TaskOrigin::Event,
321                "{refused:?} must not be accepted from the wire"
322            );
323            assert_eq!(provenance.trust(), TrustClass::Untrusted);
324        }
325    }
326
327    #[test]
328    fn wire_without_an_origin_header_is_an_untrusted_event() {
329        let provenance = from_wire(None, Some("trusted"));
330        assert_eq!(provenance.origin(), TaskOrigin::Event);
331        assert_eq!(provenance.trust(), TrustClass::Untrusted);
332    }
333
334    #[test]
335    fn wire_trust_defaults_to_untrusted_when_absent_or_unparseable() {
336        for trust_header in [None, Some("Trusted"), Some("yes"), Some("")] {
337            let provenance = from_wire(Some("peer"), trust_header);
338            assert_eq!(provenance.origin(), TaskOrigin::Peer);
339            assert_eq!(
340                provenance.trust(),
341                TrustClass::Untrusted,
342                "trust header {trust_header:?} must not resolve to trusted"
343            );
344        }
345    }
346
347    #[test]
348    fn stamp_for_peer_carries_the_senders_class_and_defaults_untrusted() {
349        assert_eq!(stamp_for_peer(None).origin(), TaskOrigin::Peer);
350        assert_eq!(stamp_for_peer(None).trust(), TrustClass::Untrusted);
351        for (sender, expected) in [
352            (TaskOrigin::User, TrustClass::Trusted),
353            (TaskOrigin::Event, TrustClass::Untrusted),
354        ] {
355            let stamped = stamp_for_peer(Some(TaskProvenance::derive(sender, None)));
356            assert_eq!(stamped.origin(), TaskOrigin::Peer);
357            assert_eq!(stamped.trust(), expected);
358        }
359    }
360
361    /// A completion inherits the delegating task's class and decides nothing of its own.
362    #[test]
363    fn stamp_for_completion_carries_the_delegating_tasks_class() {
364        assert_eq!(stamp_for_completion(None).origin(), TaskOrigin::Completion);
365        assert_eq!(stamp_for_completion(None).trust(), TrustClass::Untrusted);
366        for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
367            let stamped = stamp_for_completion(Some(trust));
368            assert_eq!(stamped.origin(), TaskOrigin::Completion);
369            assert_eq!(stamped.trust(), trust);
370        }
371    }
372
373    #[test]
374    fn artifact_trust_of_an_operator_pin_is_trusted() {
375        assert_eq!(
376            artifact_trust(&murmur_artifact::LockOrigin::Operator),
377            TrustClass::Trusted
378        );
379    }
380
381    #[test]
382    fn artifact_trust_of_a_runtime_pin_is_untrusted_for_any_session() {
383        for session in ["ses_puller", "", "operator", "trusted"] {
384            let origin = murmur_artifact::LockOrigin::Runtime {
385                session: session.to_string(),
386            };
387            assert_eq!(
388                artifact_trust(&origin),
389                TrustClass::Untrusted,
390                "session {session:?} must not make a runtime pin trusted"
391            );
392        }
393    }
394
395    #[test]
396    fn wire_untrusted_peer_stays_untrusted() {
397        let provenance = from_wire(Some("peer"), Some("untrusted"));
398        assert_eq!(provenance.origin(), TaskOrigin::Peer);
399        assert_eq!(provenance.trust(), TrustClass::Untrusted);
400    }
401}