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
//! The scope-set contract: which [`Scope`] atoms a phantom marker type stands
//! for.
//!
//! Every marker family in the workspace implements [`ScopeSet`] — the OIDC
//! markers in `ppoppo-token` (`Openid`, `Email`, …), and each SDK's sealed
//! family (`ppoppo-pcs-session`'s tiers, `ppoppo-pcs-external`'s and
//! `ppoppo-pas-plims`'s grants). A crate that performs an authorization leg
//! can then read a marker it has never heard of, and a wire scope string is
//! always *derived* from typed atoms, never spelled by hand.
//!
//! # Why here
//!
//! This is the one node every marker family can reach. `ppoppo-sdk-core`
//! depends on `ppoppo-token`, so a trait declared in sdk-core could never be
//! implemented by token's OIDC markers (`ADR_202610021053`). The trait sits
//! beside the vocabulary its atoms come from; sdk-core keeps the machinery
//! that consumes it (`missing_atoms`, `ScopedTokenSource`) and re-exports it.
//!
//! # Open parent, sealed children
//!
//! These traits are deliberately **not** sealed: a sealed trait could only be
//! implemented here. Each family stays sealed exactly as before — sealing
//! restricts who may implement *that family's* trait, not which other traits
//! its members implement.
//!
//! # It provides coherence, not authorization
//!
//! Implementing [`ScopeSet`] grants nothing. The atoms are what a client *asks
//! for*; the server decides what to give. PAS refuses unknown or
//! un-allowlisted scopes loudly (`invalid_scope`) and never silently
//! intersects them down. The authorization boundary is, and stays,
//! server-side.
use crateScope;
/// The OAuth2 scope atoms a marker type stands for.
///
/// # Atoms, not a joined string
///
/// [`SCOPES`](ScopeSet::SCOPES) is a slice of typed atoms because two things
/// need set semantics, and neither can be had from a pre-joined string:
///
/// 1. **The granted-vs-requested covers-check.** RFC 6749 §5.1 lets the token
/// response echo the granted scope; §3.3 makes order and whitespace
/// insignificant. So the check is `granted ⊇ requested` over atom sets —
/// never string equality, which would reject a reordered echo.
/// 2. **Per-atom correspondence.** The PCS capability map asserts each
/// capability marker's implied scope is present in every family that
/// claims it. Set membership is the assertion; a joined string would have
/// to be re-split by every test.
///
/// **Do not add a parallel joined const.** Two declarations of the same fact
/// is precisely the drift this contract exists to remove — the joined form is
/// derived on demand by [`scope_line`](ScopeSet::scope_line).
///
/// # Membership is not this trait's business
///
/// Which atoms a marker contains is the owning crate's decision and changes on
/// that crate's release cadence alone. Server-first, always: an atom must be
/// live in the PAS catalog ([`Scope`]) and the client's allowlist before a
/// binary requests it, or authorize fails loud — and since an atom is a
/// [`Scope`], a marker cannot name one PAS does not mint.
/// A [`ScopeSet`] a **user can consent to** — a rung on an app's consent
/// ladder, requestable through an authorization-code flow.
///
/// Empty on purpose: it adds no atoms and no behaviour, it says where the
/// atoms may be asked for. `NativeAuthFlow<S>` and `RelyingParty<S>` are
/// bounded on it (`RFC_202609171314` T-31, SDK-G12): before the split, a fixed
/// grant satisfied the same bound, so `NativeAuthFlow::<Agent>` compiled and
/// sent an authorize request PAS answers `invalid_scope`.
/// A [`ScopeSet`] **no user consents to**: PAS mints it for a credential of a
/// kind — an app's or an AI agent's own `client_credentials` grant, a
/// dependent agent's token exchange. Putting one on an authorize wire is a
/// request the server refuses, so the bound refuses it first.
///
/// The counterpart to [`ConsentScopes`], and the reason neither is the base:
/// generic machinery that only needs the *atoms* (the covers-check, a scoped
/// token source) is bounded on [`ScopeSet`] and serves both.