Skip to main content

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