liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
//! Argus's functions as liminal services under Lys grants (ACCESS-003 R1).
//!
//! Six services serve subjects placed under the network: `context-watch`,
//! `seats.<name>`, `budgets.<name>`, `alarms`, `launches` and
//! `sessions.<id>.keys`. Rights on them are Lys grants decided through the
//! ACCESS-001 seams: a hot subject is decided from the pass offline as a
//! `call`; `sessions.<id>.keys` defaults to class deliberate, mode `by_draft`,
//! so only an administrator holds it outright, and it is asked of Lys live,
//! never decided hot and never defaulted. Budgets and stops are read from
//! Lys and never decided here. There is no client registry: a seat is a Lys
//! identity with a pass, and a request without an admitted pass never reaches
//! a service.

use crate::auth_lys::session::LysSession;
use crate::auth_lys::{
    AccessAction, AccessDecisionError, AccessResource, AccessRouteClass, AdmittedLysPass,
    DeliberateDecision, GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, RefusalCause,
    SubjectBinding, SubjectBindingError, decide_deliberate, decide_message,
};
use serde::{Deserialize, Serialize};

pub mod actions;
pub mod broker;
pub mod budgets;
pub mod context_watch;
pub mod keys;
pub mod lifecycle;
pub mod lys_http;
pub mod production;
pub mod relay;
pub mod seats;

/// The six services, each a family of placed subjects.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ServiceKind {
    ContextWatch,
    Seats,
    Budgets,
    Alarms,
    Launches,
    SessionKeys,
}

impl ServiceKind {
    /// Every service, in manifest order.
    pub const ALL: [Self; 6] = [
        Self::ContextWatch,
        Self::Seats,
        Self::Budgets,
        Self::Alarms,
        Self::Launches,
        Self::SessionKeys,
    ];

    /// The service's directory name under `services/`.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::ContextWatch => "context-watch",
            Self::Seats => "seats",
            Self::Budgets => "budgets",
            Self::Alarms => "alarms",
            Self::Launches => "launches",
            Self::SessionKeys => "session-keys",
        }
    }

    /// The subject shape the service serves; `<name>` and `<id>` are one
    /// non-empty segment each.
    #[must_use]
    pub const fn subject_shape(self) -> &'static str {
        match self {
            Self::ContextWatch => "context-watch",
            Self::Seats => "seats.<name>",
            Self::Budgets => "budgets.<name>",
            Self::Alarms => "alarms",
            Self::Launches => "launches",
            Self::SessionKeys => "sessions.<id>.keys",
        }
    }

    /// The class the schema gives the service's subjects by default.
    #[must_use]
    pub const fn default_class(self) -> AccessRouteClass {
        match self {
            Self::SessionKeys => AccessRouteClass::Deliberate {
                mode: GrantMode::ByDraft,
            },
            Self::ContextWatch | Self::Seats | Self::Budgets | Self::Alarms | Self::Launches => {
                AccessRouteClass::Call
            }
        }
    }

    /// The committed manifest of this service.
    #[must_use]
    pub fn manifest(self) -> ServiceManifest {
        ServiceManifest {
            service: self,
            subject: self.subject_shape().to_owned(),
            placed_under: "network".to_owned(),
            class: self.default_class(),
        }
    }
}

/// What `services/<name>/service.json` holds; the gate test proves the file
/// equals [`ServiceKind::manifest`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceManifest {
    /// The service.
    pub service: ServiceKind,
    /// The subject shape it serves.
    pub subject: String,
    /// The kind its subjects are placed under.
    pub placed_under: String,
    /// The route class its subjects carry by default.
    #[serde(flatten)]
    pub class: AccessRouteClass,
}

/// A subject resolved to its service and the segment that names the instance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServiceSubject<'a> {
    /// The service the subject belongs to.
    pub service: ServiceKind,
    /// `<name>` or `<id>` for the shaped services; `None` for the bare ones.
    pub instance: Option<&'a str>,
}

impl ServiceSubject<'_> {
    /// The route class this subject carries.
    #[must_use]
    pub const fn class(&self) -> AccessRouteClass {
        self.service.default_class()
    }
}

fn segment(value: &str) -> Option<&str> {
    (!value.is_empty()
        && !value.contains(['.', '*', '>'])
        && !value.chars().any(char::is_whitespace))
    .then_some(value)
}

/// Resolves a subject to the service that serves it, or `None` when no
/// service does.
#[must_use]
pub fn service_of(subject: &str) -> Option<ServiceSubject<'_>> {
    match subject {
        "context-watch" => Some(ServiceSubject {
            service: ServiceKind::ContextWatch,
            instance: None,
        }),
        "alarms" => Some(ServiceSubject {
            service: ServiceKind::Alarms,
            instance: None,
        }),
        "launches" => Some(ServiceSubject {
            service: ServiceKind::Launches,
            instance: None,
        }),
        _ => {
            if let Some(name) = subject.strip_prefix("seats.") {
                return segment(name).map(|name| ServiceSubject {
                    service: ServiceKind::Seats,
                    instance: Some(name),
                });
            }
            if let Some(name) = subject.strip_prefix("budgets.") {
                return segment(name).map(|name| ServiceSubject {
                    service: ServiceKind::Budgets,
                    instance: Some(name),
                });
            }
            let id = subject.strip_prefix("sessions.")?.strip_suffix(".keys")?;
            segment(id).map(|id| ServiceSubject {
                service: ServiceKind::SessionKeys,
                instance: Some(id),
            })
        }
    }
}

/// The route class of a service subject; `None` when no service serves it.
/// `sessions.<id>.keys` is deliberate `by_draft`; a `call` on the wire never
/// overrides it.
#[must_use]
pub fn classify_subject(subject: &str) -> Option<AccessRouteClass> {
    service_of(subject).map(|resolved| resolved.class())
}

/// Why a service effect did not happen.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServiceEffectError(pub String);

impl std::fmt::Display for ServiceEffectError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "service effect failed: {}", self.0)
    }
}

impl std::error::Error for ServiceEffectError {}

/// Budget and stop policy as Lys holds it; liminal carries it, never decides
/// it.
///
/// Both members are Lys's own answers, unchanged: no product-side
/// replacement shape is computed from them (the `LysPolicySource` contract).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetPolicy {
    /// The budget holder as Lys names it (`agent`, `team` or `person`, and
    /// its id).
    pub holder: String,
    /// Lys's `GET /budgets/{kind}/{id}` answer, whole.
    pub budgets: serde_json::Value,
    /// Lys's `GET /agents/{id}/stops` answer, whole; `Null` for a holder
    /// that is not an agent, which Lys keeps no stops for.
    pub stops: serde_json::Value,
}

/// What one service request carries beyond its subject.
///
/// The caller's stable
/// operation id, which every effect is made under so a request sent again
/// does nothing twice, and the request's payload bytes (the keys to send, the
/// alarm or launch words), carried unchanged.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ServiceRequest<'a> {
    /// The caller's operation id; never empty.
    pub operation: &'a str,
    /// The request's payload bytes.
    pub payload: &'a [u8],
}

/// Starts a named seat; the seat is a Lys identity with a pass, so the
/// starter receives only the name and never consults a client registry.
pub trait SeatStarter {
    /// Starts `seat` under `operation`.
    ///
    /// # Errors
    /// The seat could not be started.
    fn start(&self, seat: &str, operation: &str) -> Result<(), ServiceEffectError>;
}

/// Sends a session's keys, once per outright decision.
pub trait KeySender {
    /// Sends `keys` to `session` under `operation`.
    ///
    /// # Errors
    /// The keys could not be sent.
    fn send(&self, session: &str, keys: &[u8], operation: &str) -> Result<(), ServiceEffectError>;
}

/// Reads a holder's budget and stop policy from Lys. `Ok(None)` means Lys
/// holds no policy, which is a refusal, never a local default.
pub trait BudgetSource {
    /// # Errors
    /// Lys could not be read.
    fn read(&self, name: &str) -> Result<Option<BudgetPolicy>, ServiceEffectError>;
}

/// Acts for the bare services: alarms, launches and the context watch.
pub trait ServiceAction {
    /// Takes `service`'s act with the request's `payload` under `operation`.
    ///
    /// # Errors
    /// The action could not be taken.
    fn act(
        &self,
        service: ServiceKind,
        payload: &[u8],
        operation: &str,
    ) -> Result<(), ServiceEffectError>;
}

/// The effect seams the services act through.
pub struct Services<'a> {
    /// Starts seats.
    pub seats: &'a dyn SeatStarter,
    /// Sends session keys.
    pub keys: &'a dyn KeySender,
    /// Reads budgets from Lys.
    pub budgets: &'a dyn BudgetSource,
    /// Acts for alarms, launches and the context watch.
    pub actions: &'a dyn ServiceAction,
}

impl std::fmt::Debug for Services<'_> {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.debug_struct("Services").finish_non_exhaustive()
    }
}

/// What serving a subject came to.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServiceOutcome {
    /// The grant allowed it and the effect happened once.
    Acted {
        /// The grant it was decided on.
        receipt: GrantReceipt,
    },
    /// Lys holds the act under a draft; nothing happened.
    Held {
        /// The mode Lys answered with.
        mode: GrantMode,
        /// The grant the draft belongs to.
        grant: String,
        /// The draft Lys opened.
        draft: String,
    },
    /// The policy Lys holds for a budget subject, carried unchanged.
    Policy(BudgetPolicy),
}

/// Why a subject was not served, by name.
#[derive(Debug, PartialEq, Eq)]
pub enum ServiceRefusal {
    /// No service serves the subject.
    NotAService,
    /// The subject could not be bound.
    Binding(SubjectBindingError),
    /// The decision refused it: a missing grant, an expired pass, or the
    /// deliberate seam being absent or offline (`deliberate_unavailable`).
    Decision(AccessDecisionError),
    /// Lys holds no budget or stop policy for the holder; liminal sets none.
    PolicyUnavailable,
    /// The request named no operation id, so its effect could not be made
    /// once; nothing was decided or done.
    NoOperation,
    /// The effect failed after the grant allowed it.
    Effect(ServiceEffectError),
}

impl std::fmt::Display for ServiceRefusal {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotAService => formatter.write_str("no service serves this subject"),
            Self::Binding(SubjectBindingError(member)) => {
                write!(formatter, "subject binding refused: {member}")
            }
            Self::Decision(error) => write!(formatter, "{error}"),
            Self::PolicyUnavailable => {
                formatter.write_str("budget policy unavailable from Lys; liminal sets none")
            }
            Self::Effect(error) => write!(formatter, "{error}"),
            Self::NoOperation => {
                formatter.write_str("service request names no operation id; nothing was done")
            }
        }
    }
}

impl std::error::Error for ServiceRefusal {}

/// Serves `subject` for the holder of `pass`, its effect made under
/// `request`'s operation with its payload.
///
/// `consumer` is the live Lys seam; `None` means no seam is installed, which
/// refuses every deliberate subject as `deliberate_unavailable` and, having
/// no pass consumer either, every hot one too. Nothing here falls back from
/// deliberate to hot.
///
/// # Errors
/// The refusal, by name.
pub fn serve<C: LysDeliberateConsumer>(
    services: &Services<'_>,
    consumer: Option<&C>,
    pass: &AdmittedLysPass<C::Pass>,
    network_id: &str,
    subject: &str,
    request: &ServiceRequest<'_>,
    now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
    let decider = consumer.map(|consumer| ConsumerDecider { consumer, pass });
    serve_with(
        services,
        decider
            .as_ref()
            .map(|decider| -> &dyn ServiceDecider { decider }),
        network_id,
        subject,
        request,
        now,
    )
}

/// Serves `request` on `subject` for a connection's admitted Lys session
/// (ACCESS-003 R1, the wire route).
///
/// `None` is a connection admitted without
/// a Lys pass, refused `deliberate_unavailable` by name before any effect.
///
/// # Errors
/// As [`serve`].
pub fn serve_session(
    services: &Services<'_>,
    session: Option<&dyn LysSession>,
    network_id: &str,
    subject: &str,
    request: &ServiceRequest<'_>,
    now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
    let decider = session.map(SessionDecider);
    serve_with(
        services,
        decider
            .as_ref()
            .map(|decider| -> &dyn ServiceDecider { decider }),
        network_id,
        subject,
        request,
        now,
    )
}

/// Who decides a service request: the admitted pass through its consumer,
/// or a connection's admitted [`LysSession`].
///
/// Both reach the same two seams,
/// the hot decision offline and the deliberate one live, so a request is
/// decided once, the same way, from either door.
pub trait ServiceDecider {
    /// Decides one hot action on the bound subject, offline.
    ///
    /// # Errors
    /// Refused, by cause.
    fn hot(
        &self,
        subject: &SubjectBinding,
        action: HotAction,
        now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError>;

    /// Asks Lys live for `action` on `resource`.
    ///
    /// # Errors
    /// Refused, by cause; `DeliberateUnavailable` without a live seam.
    fn live(
        &self,
        resource: &AccessResource,
        action: AccessAction,
        now: u64,
    ) -> Result<DeliberateDecision, AccessDecisionError>;
}

struct ConsumerDecider<'a, C: LysDeliberateConsumer> {
    consumer: &'a C,
    pass: &'a AdmittedLysPass<C::Pass>,
}

impl<C: LysDeliberateConsumer> ServiceDecider for ConsumerDecider<'_, C> {
    fn hot(
        &self,
        subject: &SubjectBinding,
        action: HotAction,
        now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError> {
        decide_message(self.consumer, self.pass, subject, action, now)
    }

    fn live(
        &self,
        resource: &AccessResource,
        action: AccessAction,
        now: u64,
    ) -> Result<DeliberateDecision, AccessDecisionError> {
        decide_deliberate(self.consumer, self.pass, resource, action, now)
    }
}

struct SessionDecider<'a>(&'a dyn LysSession);

impl ServiceDecider for SessionDecider<'_> {
    fn hot(
        &self,
        subject: &SubjectBinding,
        action: HotAction,
        now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError> {
        self.0.decide(subject, action, now)
    }

    fn live(
        &self,
        resource: &AccessResource,
        action: AccessAction,
        now: u64,
    ) -> Result<DeliberateDecision, AccessDecisionError> {
        self.0.decide_live(resource, action, now)
    }
}

fn serve_with(
    services: &Services<'_>,
    decider: Option<&dyn ServiceDecider>,
    network_id: &str,
    subject: &str,
    request: &ServiceRequest<'_>,
    now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
    let resolved = service_of(subject).ok_or(ServiceRefusal::NotAService)?;
    if request.operation.trim().is_empty() {
        return Err(ServiceRefusal::NoOperation);
    }
    let binding = SubjectBinding::new(
        network_id.to_owned(),
        subject.to_owned(),
        subject.to_owned(),
    )
    .map_err(ServiceRefusal::Binding)?;
    let Some(decider) = decider else {
        return Err(ServiceRefusal::Decision(unavailable(&binding, &resolved)));
    };
    match resolved.class() {
        AccessRouteClass::Deliberate { .. } => {
            let decision = decider
                .live(binding.resource(), HotAction::Call.into(), now)
                .map_err(ServiceRefusal::Decision)?;
            match decision {
                DeliberateDecision::Outright(receipt) => {
                    effect(services, &resolved, request)?;
                    Ok(ServiceOutcome::Acted { receipt })
                }
                DeliberateDecision::ByDraft { grant, draft } => Ok(ServiceOutcome::Held {
                    mode: GrantMode::ByDraft,
                    grant,
                    draft,
                }),
                DeliberateDecision::ByTwo { grant, draft } => Ok(ServiceOutcome::Held {
                    mode: GrantMode::ByTwo,
                    grant,
                    draft,
                }),
            }
        }
        AccessRouteClass::Call
        | AccessRouteClass::Publish
        | AccessRouteClass::Subscribe
        | AccessRouteClass::Serve => {
            let receipt = decider
                .hot(&binding, HotAction::Call, now)
                .map_err(ServiceRefusal::Decision)?;
            if resolved.service == ServiceKind::Budgets {
                let name = resolved.instance.unwrap_or_default();
                let policy = services
                    .budgets
                    .read(name)
                    .map_err(ServiceRefusal::Effect)?;
                return policy.map_or(Err(ServiceRefusal::PolicyUnavailable), |policy| {
                    Ok(ServiceOutcome::Policy(policy))
                });
            }
            effect(services, &resolved, request)?;
            Ok(ServiceOutcome::Acted { receipt })
        }
    }
}

fn effect(
    services: &Services<'_>,
    resolved: &ServiceSubject<'_>,
    request: &ServiceRequest<'_>,
) -> Result<(), ServiceRefusal> {
    let instance = resolved.instance.unwrap_or_default();
    match resolved.service {
        ServiceKind::Seats => services.seats.start(instance, request.operation),
        ServiceKind::SessionKeys => {
            services
                .keys
                .send(instance, request.payload, request.operation)
        }
        ServiceKind::Budgets => Ok(()),
        ServiceKind::ContextWatch | ServiceKind::Alarms | ServiceKind::Launches => services
            .actions
            .act(resolved.service, request.payload, request.operation),
    }
    .map_err(ServiceRefusal::Effect)
}

fn unavailable(binding: &SubjectBinding, resolved: &ServiceSubject<'_>) -> AccessDecisionError {
    let action = match resolved.class() {
        AccessRouteClass::Deliberate { .. } => crate::auth_lys::AccessAction::Admin,
        _ => HotAction::Call.into(),
    };
    AccessDecisionError {
        cause: RefusalCause::DeliberateUnavailable,
        refusal: crate::auth_lys::AccessRefusal::new(
            binding.resource().clone(),
            action,
            format!("lys://ask/{}", binding.subject()),
        ),
    }
}