subc_protocol/scope.rs
1//! Scope records: owned identity records the daemon holds for sessions.
2//!
3//! A scope is identified by `(owner, ref)`. The owner is the module whose own
4//! registered connection synced it, never a value in the request, and the
5//! `ref` is an opaque string unique within that owner only. The design is
6//! `docs/designs/daemon-scopes.md`; the wire shapes here are the owner-facing
7//! half of it (`scope.sync`, `scope.describe`).
8//!
9//! Every record type refuses unknown fields. A field this daemon does not know
10//! may be one that narrows authority in a later version (a carrier's target
11//! list, say), and silently dropping it would widen what the scope grants, so
12//! an owner sending one is told its body is malformed instead.
13
14use serde::{Deserialize, Serialize};
15
16use crate::Principal;
17
18// Scope principals are authority-bearing input: an unrecognized constraint must
19// not silently widen a grant. Principal elsewhere is a forward-compatible caller
20// fact, so keep its general decoder lenient and enforce this only on scope input.
21#[derive(Deserialize)]
22#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
23enum ScopePrincipal {
24 Reserved { module_id: String },
25 Direct {},
26 Unverified {},
27}
28
29impl From<ScopePrincipal> for Principal {
30 fn from(value: ScopePrincipal) -> Self {
31 match value {
32 ScopePrincipal::Reserved { module_id } => Self::Reserved { module_id },
33 ScopePrincipal::Direct {} => Self::Direct,
34 ScopePrincipal::Unverified {} => Self::Unverified,
35 }
36 }
37}
38
39fn deserialize_scope_principal<'de, D: serde::Deserializer<'de>>(
40 deserializer: D,
41) -> Result<Principal, D::Error> {
42 ScopePrincipal::deserialize(deserializer).map(Into::into)
43}
44
45fn deserialize_scope_principals<'de, D: serde::Deserializer<'de>>(
46 deserializer: D,
47) -> Result<Vec<Principal>, D::Error> {
48 Vec::<ScopePrincipal>::deserialize(deserializer)
49 .map(|principals| principals.into_iter().map(Into::into).collect())
50}
51
52/// The `server.describe` capability a daemon advertises when it admits routes
53/// under scopes. A carrier that needs a scoped route and does not see it fails
54/// the call (`scope_unsupported`) instead of opening an unscoped route.
55pub const CAP_SCOPES_V1: &str = "scopes/v1";
56
57/// The `server.describe` capability a daemon advertises when it checks a
58/// `route.open`'s `role_versions` and forwards them on the module's bind. A
59/// daemon without it drops the field silently, so a consumer that relies on
60/// the provider seeing its role versions checks for this first.
61pub const CAP_ROUTE_ROLE_VERSIONS_V1: &str = "route-role-versions/v1";
62
63/// Module-to-subc op that registers an owner's full scope set.
64pub const SCOPE_SYNC_OP: &str = "scope.sync";
65/// Module-to-subc op that reads one scope's current state.
66pub const SCOPE_DESCRIBE_OP: &str = "scope.describe";
67
68/// Most live scopes one owner may hold. A sync naming more is refused whole.
69pub const MAX_LIVE_SCOPES_PER_OWNER: usize = 10_000;
70/// Most bytes one scope's `attributes` may take, measured as compact JSON. A
71/// sync carrying a larger record is refused whole.
72pub const MAX_SCOPE_ATTRIBUTE_BYTES: usize = 4 * 1024;
73/// Most ended scopes the daemon remembers per owner. The oldest is forgotten
74/// first, and reaching the bound never refuses a sync.
75pub const MAX_SCOPE_TOMBSTONES_PER_OWNER: usize = 1_000;
76/// Most modules one targeted carrier entry may list.
77pub const MAX_CARRIER_TARGETS: usize = 16;
78
79/// What a scope stands for. Closed, and fixed for the life of one
80/// `scope_epoch`: a different kind needs a new epoch, which ends the old scope.
81#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
82#[serde(rename_all = "snake_case")]
83pub enum ScopeKind {
84 Head,
85 Worker,
86 Ephemeral,
87}
88
89/// A link from a scope to another scope, pinned to that scope's session by its
90/// epoch.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(deny_unknown_fields)]
93pub struct ScopeParent {
94 #[serde(deserialize_with = "deserialize_scope_principal")]
95 pub owner: Principal,
96 #[serde(rename = "ref")]
97 pub scope_ref: String,
98 pub scope_epoch: u64,
99}
100
101/// Who, besides the owner, may open routes under a scope.
102///
103/// `targets` absent means the carrier may open to any module. Present, it
104/// names the only module ids the carrier may open to, and must hold between 1
105/// and [`MAX_CARRIER_TARGETS`] entries: an empty list is refused rather than
106/// read as either "none" or "all".
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(deny_unknown_fields)]
109pub struct ScopeCarrier {
110 #[serde(deserialize_with = "deserialize_scope_principal")]
111 pub principal: Principal,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub targets: Option<Vec<String>>,
114}
115
116/// The attributes the daemon stamps without interpreting. Both grant authority,
117/// so only an owner listed in the daemon's `scope_authority_owners` may set
118/// either.
119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
120#[serde(deny_unknown_fields)]
121pub struct ScopeAttributes {
122 /// The agent the scope's session belongs to. Identity, never permission.
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub agent_id: Option<String>,
125 /// Whether a provider may act as `agent_id`. Refused without `agent_id`.
126 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
127 pub delegates: bool,
128}
129
130impl ScopeAttributes {
131 pub fn is_empty(&self) -> bool {
132 self.agent_id.is_none() && !self.delegates
133 }
134}
135
136/// One scope as its owner registers it in `scope.sync`.
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
138#[serde(deny_unknown_fields)]
139pub struct ScopeRecord {
140 #[serde(rename = "ref")]
141 pub scope_ref: String,
142 /// Owner-supplied session number. The owner keeps it with its own record of
143 /// the session, re-sends the same value for the same session after any
144 /// restart, and uses a higher one when it reuses the ref for a new session.
145 pub scope_epoch: u64,
146 pub kind: ScopeKind,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub parent: Option<ScopeParent>,
149 /// Principals, other than the owner, allowed to register child scopes under
150 /// this one. Listing a principal here grants it nothing else.
151 #[serde(
152 default,
153 skip_serializing_if = "Vec::is_empty",
154 deserialize_with = "deserialize_scope_principals"
155 )]
156 pub child_owners: Vec<Principal>,
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub carriers: Vec<ScopeCarrier>,
159 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
160 pub attributes: ScopeAttributes,
161}
162
163/// The scope a `route.open` asks to be admitted under.
164///
165/// `scope_epoch` is optional on the wire only so that leaving it out is
166/// refused by name (`scope_epoch_required`) rather than as a malformed body:
167/// every opener must name it, the owner included.
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169#[serde(deny_unknown_fields)]
170pub struct ScopeSelector {
171 #[serde(deserialize_with = "deserialize_scope_principal")]
172 pub owner: Principal,
173 #[serde(rename = "ref")]
174 pub scope_ref: String,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub scope_epoch: Option<u64>,
177}
178
179/// The state of a scope's parent link.
180#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
181#[serde(rename_all = "snake_case")]
182pub enum ParentState {
183 /// The parent is live at the named epoch and the link was permitted.
184 Linked,
185 /// The parent's owner has not synced in this daemon incarnation, so the
186 /// link is unverified and grants nothing yet.
187 Pending,
188 /// The parent is gone or live at another epoch, or the link was refused
189 /// when the parent's owner synced. Final for this link.
190 Ended,
191}
192
193/// What a `scope.sync` did with one record.
194#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
195#[serde(rename_all = "snake_case")]
196pub enum ScopeRecordOutcome {
197 /// The ref was not live before; the scope was created.
198 Created,
199 /// The ref was live at a lower epoch; that scope ended and this one began.
200 Replaced,
201 /// The ref was live at this epoch and its content changed.
202 Updated,
203 /// The ref was live at this epoch with identical content; its `version`
204 /// did not move.
205 Unchanged,
206 /// The record was refused on its own merits (`code` says why) and the ref
207 /// keeps whatever state it had before this sync.
208 Refused,
209}
210
211/// The per-record result in a `scope.sync` reply, in request order.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213pub struct ScopeRecordResult {
214 #[serde(rename = "ref")]
215 pub scope_ref: String,
216 /// The epoch the record named, which for a refusal may differ from the
217 /// epoch still held.
218 pub scope_epoch: u64,
219 pub outcome: ScopeRecordOutcome,
220 /// The refusal code when `outcome` is `refused`.
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub code: Option<String>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub message: Option<String>,
225 /// The daemon's content counter for the scope held under this ref after
226 /// the sync; absent when no scope is live under it.
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub version: Option<u64>,
229 /// The parent link's state after the sync, for a scope with a parent.
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub parent_state: Option<ParentState>,
232}
233
234/// A scope of the syncing owner that the sync ended, by removal or by a
235/// higher epoch for the same ref.
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
237pub struct ScopeEnded {
238 #[serde(rename = "ref")]
239 pub scope_ref: String,
240 pub scope_epoch: u64,
241}
242
243/// `scope.describe`'s answer about one `(owner, ref)`.
244#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
245#[serde(rename_all = "snake_case")]
246pub enum ScopeStatus {
247 Live,
248 /// Ended in this daemon incarnation, and still remembered.
249 Ended,
250 /// Neither live nor remembered as ended. With `owner_synced` true the scope
251 /// is gone: an owner's first sync of an incarnation is its full set. With
252 /// `owner_synced` false and `owner_configured` true the owner has not
253 /// re-synced since a daemon restart, so a reader waits. With
254 /// `owner_configured` false the owner will never sync.
255 NotLive,
256}
257
258/// The fields the daemon stamps for a live scope.
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
260pub struct ScopeStamp {
261 pub owner: Principal,
262 #[serde(rename = "ref")]
263 pub scope_ref: String,
264 pub scope_epoch: u64,
265 pub kind: ScopeKind,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub parent: Option<ScopeParent>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub parent_state: Option<ParentState>,
270 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
271 pub attributes: ScopeAttributes,
272 /// Whether the owner is listed in the daemon's `scope_authority_owners`.
273 /// Providers decide on this flag and keep no copy of the list.
274 pub owner_authorized: bool,
275}