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