liminal_server/config/types/lys.rs
1use std::net::SocketAddr;
2
3/// `[auth.lys]`: the Lys issuer whose passes are admitted (ACCESS-001 R2).
4#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct LysConfig {
7 #[serde(default, skip_serializing_if = "Option::is_none")]
8 pub drafts: Option<crate::auth_lys::live::drafts::DraftConfig>,
9 /// The issuer URL exactly as the pass's `iss` names it.
10 pub issuer: String,
11 /// The Lys network liminal's subjects are placed under.
12 pub network_id: String,
13 /// Path to the issuer's published key set as JSON, read once at start.
14 pub keys_path: std::path::PathBuf,
15 /// Ask the issuer live for deliberate decisions (opening a link,
16 /// deploying a service, an administrator's act) with the holder's own
17 /// pass. Off, Lys cannot be asked and every deliberate route refuses by
18 /// that name; it never falls back to the pass.
19 #[serde(default)]
20 pub live: bool,
21 /// How the three grant decisions answer a missing grant (ACCESS-011 R2):
22 /// `enforce` (the default) refuses the act as today; `advise` says the
23 /// missing grant by name at WARN and lets the act through. Neither
24 /// changes who the holder is, the bearer, or the wire pass: an
25 /// unverifiable pass is refused the same under both.
26 #[serde(default)]
27 pub decisions: Decisions,
28 /// This liminal as a machine Lys knows, for links (ACCESS-002 R1).
29 #[serde(default)]
30 pub machine: Option<MachineConfig>,
31 /// Argus's functions as liminal services, bound to Lys and the server's
32 /// publish path (ACCESS-003 R1).
33 #[serde(default)]
34 pub services: Option<LysServicesConfig>,
35 /// Address-only broker and its observed-address UDP echo.
36 #[serde(default)]
37 pub broker: Option<BrokerConfig>,
38 /// Blind forwarding of marked relay streams.
39 #[serde(default)]
40 pub relay: Option<crate::services::relay::RelayConfig>,
41 /// Channel membership from Lys's one shared membership contract
42 /// (ACCESS-006 R1): the explicit product mapping of liminal channels to
43 /// placed Lys resources, and the provider route it is asked through.
44 /// Absent, no channel is membership-protected and every channel keeps
45 /// its prior contract.
46 #[serde(default)]
47 pub membership: Option<MembershipConfig>,
48 /// One bounded reader for this log; required beside membership protection.
49 #[serde(default)]
50 pub grant_stream: Option<super::GrantStreamConfig>,
51 /// Agent station bounds; required when any agent is placed.
52 #[serde(default)]
53 pub agents: Option<crate::agents::config::AgentsConfig>,
54 /// The templates services are created from (SRV-028 R1): the only
55 /// command lines this server will ever run for a seat.
56 #[serde(default)]
57 pub templates: crate::services::lifecycle::templates::TemplateSources,
58 /// Required operator bounds and machine-held builder delegation sources.
59 #[serde(default)]
60 pub construction: Option<crate::services::lifecycle::site::ConstructionConfig>,
61 /// The base of Lys's API routes (`.../api/`) for live asks, placements
62 /// and the service routes, when they are not served at the issuer
63 /// itself; absent, the issuer is the base, as before.
64 #[serde(default)]
65 pub api_url: Option<String>,
66 /// Path to this liminal's Lys app credential, read once at start; a new
67 /// service and its subjects are placed under the network with it
68 /// (SRV-028 R3). Absent, every create is refused by name.
69 #[serde(default)]
70 pub app_credential_path: Option<std::path::PathBuf>,
71}
72
73/// `[[auth.lys.templates]]`: one command a seat may create services from
74/// (SRV-028 R1). Read-only at run time.
75#[derive(Clone, PartialEq, Eq, serde::Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct ServiceTemplateConfig {
78 /// The template's name, one segment.
79 pub name: String,
80 /// The program, an absolute path to an executable file.
81 pub command: std::path::PathBuf,
82 /// Its arguments, passed as written.
83 #[serde(default)]
84 pub args: Vec<String>,
85 /// The subject roots a service of this template serves; each service
86 /// serves `<root>.<service name>`.
87 pub subjects: Vec<String>,
88 /// Names of variables passed through from the server's environment;
89 /// nothing else of it reaches the process.
90 #[serde(default)]
91 pub env: Vec<String>,
92 /// How long a stop waits after SIGTERM before SIGKILL; positive.
93 pub stop_timeout_ms: u64,
94}
95
96/// `[auth.lys.membership]`: channel membership bound to the verified holder
97/// and the placed resource (ACCESS-006 R1).
98///
99/// The mapping is explicit: a channel is membership-protected only when it is
100/// listed here with its workspace and placed resource, and a participant
101/// conversation is placed under a channel only when it is listed here. No
102/// mapping is guessed from a subject string, and a readable parent is never
103/// taken as membership of a child. Every bound is required and positive;
104/// none has a default. Validated whole when the membership gate is built at
105/// start ([`crate::auth_membership::MembershipGate::new`]).
106#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct MembershipConfig {
109 /// The membership contract version this liminal speaks; it must equal
110 /// `lys_pass::membership::CONTRACT_VERSION`.
111 pub contract: u32,
112 /// The identity of the Lys grant log decisions are asked from.
113 pub log_identity: String,
114 /// The reset epoch of that grant log.
115 pub log_epoch: u64,
116 /// The product schema's workspace kind, as Lys places it.
117 pub workspace_kind: String,
118 /// The product schema's channel kind, as Lys places it.
119 pub channel_kind: String,
120 /// Path to this liminal's app credential for the Lys membership routes,
121 /// read once at start. Its bytes never appear in a `Debug`.
122 pub credential_path: std::path::PathBuf,
123 /// Most rows one membership page carries; positive.
124 pub page_rows: u32,
125 /// Most encoded row bytes and HTTP answer bytes; positive. The same cap
126 /// bounds reading the app credential before opening any protected route.
127 pub page_bytes: u32,
128 /// Most decisions the local membership index retains at once; positive.
129 /// A decision past it is refused by name before anything is allocated
130 /// for it, never evicted to make room.
131 pub max_decisions: usize,
132 /// The workspace a Lys-admitted `Connect` must hold a current grant in
133 /// (scoped guest admission). Absent, `Connect` asks no admission and a
134 /// protected channel is still decided per act.
135 #[serde(default)]
136 pub connect_workspace: Option<String>,
137 /// The membership-protected channels.
138 pub channels: Vec<MembershipChannelConfig>,
139 /// Participant conversations placed under a protected channel.
140 #[serde(default)]
141 pub conversations: Vec<MembershipConversationConfig>,
142}
143
144/// One membership-protected channel: the liminal channel name, the
145/// workspace it is placed under and the placed resource id.
146#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
147#[serde(deny_unknown_fields)]
148pub struct MembershipChannelConfig {
149 /// The liminal channel (subject) name, matched exactly.
150 pub channel: String,
151 /// The workspace resource id the channel is placed under.
152 pub workspace: String,
153 /// The placed channel resource id.
154 pub resource: String,
155}
156
157/// One participant conversation placed under a protected channel.
158#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct MembershipConversationConfig {
161 /// The participant conversation id.
162 pub conversation: u64,
163 /// The protected channel it is placed under, as listed in `channels`.
164 pub channel: String,
165}
166
167/// `[auth.lys.services]`: the production bindings of the ACCESS-003
168/// services. They ask Lys as this machine, so `[auth.lys.machine]` is
169/// required beside it.
170#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct LysServicesConfig {
173 /// The machine id Lys records, on whose runner `seats.<name>` starts.
174 pub machine: String,
175 /// The host name service messages are published as (`liminal.<host>`).
176 pub host: String,
177 /// Run the context watch over the `context-watch` channel, reporting
178 /// each reading to Lys.
179 #[serde(default)]
180 pub context_watch: bool,
181}
182
183/// `[auth.lys.broker]`: the address-only broker's UDP echo listener.
184#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct BrokerConfig {
187 /// The socket on which a device asks for its observed address.
188 pub udp_listen: SocketAddr,
189}
190
191/// `[auth.lys.machine]`: this liminal's agent identity and pass.
192///
193/// Its links are keyed from the Ed25519 identity Lys recorded for it,
194/// its own end of every crossing is decided from its own pass, and the
195/// holders that may open links to it are named with their public keys.
196/// The link identity is an agent's own key and pass. The machine section
197/// name is retained so existing file-pass configurations still read.
198#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
199#[serde(deny_unknown_fields)]
200pub struct MachineConfig {
201 /// Carrier order and message-scoped direct deadlines.
202 #[serde(default)]
203 pub carrier_choice: crate::link_open::ChoiceConfig,
204 /// The device this machine is a district of, when configured.
205 #[serde(default)]
206 pub parent: Option<String>,
207 /// Path to the agent's Ed25519 seed, 64 hexadecimal digits, read once
208 /// at start. The link handshake keys from its X25519 derivation.
209 pub identity_path: std::path::PathBuf,
210 /// Path to the agent's own Lys pass, verified at start; absent when
211 /// the signed agent dial is configured.
212 #[serde(default)]
213 pub pass_path: std::path::PathBuf,
214 /// The agent whose capability certificate names this identity's key.
215 /// With it set, the pass is asked of Lys at start and again at any link
216 /// open while none is held or the held one is expired or due for
217 /// renewal; a start whose ask Lys refuses is logged once and asks again
218 /// at the next open, never by a restart (ACCESS-009).
219 #[serde(default)]
220 pub agent_id: String,
221 /// The app the agent asks the issuer for a pass to.
222 #[serde(default)]
223 pub audience: String,
224 /// The public key recorded for each holder that may open a link,
225 /// holder id to Ed25519 public key as 64 hexadecimal digits.
226 #[serde(default)]
227 pub machines: std::collections::BTreeMap<String, String>,
228 /// Outbound links keyed from this machine, each configured by name.
229 #[serde(default)]
230 pub links: Vec<OutboundLinkConfig>,
231}
232
233/// One exporting link; an absent wire address leaves carrier choice to the caller.
234#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
235#[serde(deny_unknown_fields)]
236pub struct OutboundLinkConfig {
237 /// The link id placed under the configured network.
238 pub link_id: String,
239 /// The holder whose Ed25519 key is recorded in this machine's directory mirror.
240 pub peer_holder: String,
241 /// The peer's wire listener as host:port, if this link uses the wire.
242 #[serde(default)]
243 pub peer_address: Option<String>,
244 /// Placed subject prefixes exported over this link.
245 pub exports: Vec<String>,
246}
247
248/// How the three grant decisions (a hot act, a live ask, a site act) answer
249/// a grant Lys does not hold (ACCESS-011 R2).
250#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum Decisions {
253 /// A missing grant refuses the act, as every exchange ran before this.
254 #[default]
255 Enforce,
256 /// A missing grant is said by name at WARN and the act proceeds.
257 Advise,
258}
259
260impl Decisions {
261 /// The word the configuration uses.
262 #[must_use]
263 pub const fn word(self) -> &'static str {
264 match self {
265 Self::Enforce => "enforce",
266 Self::Advise => "advise",
267 }
268 }
269}
270
271impl std::fmt::Display for Decisions {
272 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 formatter.write_str(self.word())
274 }
275}