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/// The `server.describe` capability a daemon advertises when it admits routes
19/// under scopes. A carrier that needs a scoped route and does not see it fails
20/// the call (`scope_unsupported`) instead of opening an unscoped route.
21pub const CAP_SCOPES_V1: &str = "scopes/v1";
22
23/// Module-to-subc op that registers an owner's full scope set.
24pub const SCOPE_SYNC_OP: &str = "scope.sync";
25/// Module-to-subc op that reads one scope's current state.
26pub const SCOPE_DESCRIBE_OP: &str = "scope.describe";
27
28/// Most live scopes one owner may hold. A sync naming more is refused whole.
29pub const MAX_LIVE_SCOPES_PER_OWNER: usize = 10_000;
30/// Most bytes one scope's `attributes` may take, measured as compact JSON. A
31/// sync carrying a larger record is refused whole.
32pub const MAX_SCOPE_ATTRIBUTE_BYTES: usize = 4 * 1024;
33/// Most ended scopes the daemon remembers per owner. The oldest is forgotten
34/// first, and reaching the bound never refuses a sync.
35pub const MAX_SCOPE_TOMBSTONES_PER_OWNER: usize = 1_000;
36/// Most modules one targeted carrier entry may list.
37pub const MAX_CARRIER_TARGETS: usize = 16;
38
39/// What a scope stands for. Closed, and fixed for the life of one
40/// `scope_epoch`: a different kind needs a new epoch, which ends the old scope.
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
42#[serde(rename_all = "snake_case")]
43pub enum ScopeKind {
44 Head,
45 Worker,
46 Ephemeral,
47}
48
49/// A link from a scope to another scope, pinned to that scope's session by its
50/// epoch.
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(deny_unknown_fields)]
53pub struct ScopeParent {
54 pub owner: Principal,
55 #[serde(rename = "ref")]
56 pub scope_ref: String,
57 pub scope_epoch: u64,
58}
59
60/// Who, besides the owner, may open routes under a scope.
61///
62/// `targets` absent means the carrier may open to any module. Present, it
63/// names the only module ids the carrier may open to, and must hold between 1
64/// and [`MAX_CARRIER_TARGETS`] entries: an empty list is refused rather than
65/// read as either "none" or "all".
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(deny_unknown_fields)]
68pub struct ScopeCarrier {
69 pub principal: Principal,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub targets: Option<Vec<String>>,
72}
73
74/// The attributes the daemon stamps without interpreting. Both grant authority,
75/// so only an owner listed in the daemon's `scope_authority_owners` may set
76/// either.
77#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(deny_unknown_fields)]
79pub struct ScopeAttributes {
80 /// The agent the scope's session belongs to. Identity, never permission.
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub agent_id: Option<String>,
83 /// Whether a provider may act as `agent_id`. Refused without `agent_id`.
84 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
85 pub delegates: bool,
86}
87
88impl ScopeAttributes {
89 pub fn is_empty(&self) -> bool {
90 self.agent_id.is_none() && !self.delegates
91 }
92}
93
94/// One scope as its owner registers it in `scope.sync`.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(deny_unknown_fields)]
97pub struct ScopeRecord {
98 #[serde(rename = "ref")]
99 pub scope_ref: String,
100 /// Owner-supplied session number. The owner keeps it with its own record of
101 /// the session, re-sends the same value for the same session after any
102 /// restart, and uses a higher one when it reuses the ref for a new session.
103 pub scope_epoch: u64,
104 pub kind: ScopeKind,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub parent: Option<ScopeParent>,
107 /// Principals, other than the owner, allowed to register child scopes under
108 /// this one. Listing a principal here grants it nothing else.
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub child_owners: Vec<Principal>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub carriers: Vec<ScopeCarrier>,
113 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
114 pub attributes: ScopeAttributes,
115}
116
117/// The scope a `route.open` asks to be admitted under.
118///
119/// `scope_epoch` is optional on the wire only so that leaving it out is
120/// refused by name (`scope_epoch_required`) rather than as a malformed body:
121/// every opener must name it, the owner included.
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(deny_unknown_fields)]
124pub struct ScopeSelector {
125 pub owner: Principal,
126 #[serde(rename = "ref")]
127 pub scope_ref: String,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub scope_epoch: Option<u64>,
130}
131
132/// The state of a scope's parent link.
133#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
134#[serde(rename_all = "snake_case")]
135pub enum ParentState {
136 /// The parent is live at the named epoch and the link was permitted.
137 Linked,
138 /// The parent's owner has not synced in this daemon incarnation, so the
139 /// link is unverified and grants nothing yet.
140 Pending,
141 /// The parent is gone or live at another epoch, or the link was refused
142 /// when the parent's owner synced. Final for this link.
143 Ended,
144}
145
146/// What a `scope.sync` did with one record.
147#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(rename_all = "snake_case")]
149pub enum ScopeRecordOutcome {
150 /// The ref was not live before; the scope was created.
151 Created,
152 /// The ref was live at a lower epoch; that scope ended and this one began.
153 Replaced,
154 /// The ref was live at this epoch and its content changed.
155 Updated,
156 /// The ref was live at this epoch with identical content; its `version`
157 /// did not move.
158 Unchanged,
159 /// The record was refused on its own merits (`code` says why) and the ref
160 /// keeps whatever state it had before this sync.
161 Refused,
162}
163
164/// The per-record result in a `scope.sync` reply, in request order.
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166pub struct ScopeRecordResult {
167 #[serde(rename = "ref")]
168 pub scope_ref: String,
169 /// The epoch the record named, which for a refusal may differ from the
170 /// epoch still held.
171 pub scope_epoch: u64,
172 pub outcome: ScopeRecordOutcome,
173 /// The refusal code when `outcome` is `refused`.
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub code: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub message: Option<String>,
178 /// The daemon's content counter for the scope held under this ref after
179 /// the sync; absent when no scope is live under it.
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub version: Option<u64>,
182 /// The parent link's state after the sync, for a scope with a parent.
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub parent_state: Option<ParentState>,
185}
186
187/// A scope of the syncing owner that the sync ended, by removal or by a
188/// higher epoch for the same ref.
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
190pub struct ScopeEnded {
191 #[serde(rename = "ref")]
192 pub scope_ref: String,
193 pub scope_epoch: u64,
194}
195
196/// `scope.describe`'s answer about one `(owner, ref)`.
197#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
198#[serde(rename_all = "snake_case")]
199pub enum ScopeStatus {
200 Live,
201 /// Ended in this daemon incarnation, and still remembered.
202 Ended,
203 /// Neither live nor remembered as ended. With `owner_synced` true the scope
204 /// is gone: an owner's first sync of an incarnation is its full set. With
205 /// `owner_synced` false and `owner_configured` true the owner has not
206 /// re-synced since a daemon restart, so a reader waits. With
207 /// `owner_configured` false the owner will never sync.
208 NotLive,
209}
210
211/// The fields the daemon stamps for a live scope.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213pub struct ScopeStamp {
214 pub owner: Principal,
215 #[serde(rename = "ref")]
216 pub scope_ref: String,
217 pub scope_epoch: u64,
218 pub kind: ScopeKind,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub parent: Option<ScopeParent>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub parent_state: Option<ParentState>,
223 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
224 pub attributes: ScopeAttributes,
225 /// Whether the owner is listed in the daemon's `scope_authority_owners`.
226 /// Providers decide on this flag and keep no copy of the list.
227 pub owner_authorized: bool,
228}