@celestia-island/plana-types 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +38 -0
- package/bindings/FileAnchor.ts +3 -0
- package/bindings/engine.ts +277 -0
- package/bindings/enums.ts +31 -0
- package/bindings/httpTypes.ts +197 -0
- package/bindings/index.ts +47 -0
- package/bindings/mcp/aporia.ts +61 -0
- package/bindings/mcp/eleos.ts +18 -0
- package/bindings/mcp/epieikeia.ts +48 -0
- package/bindings/mcp/haplotes.ts +47 -0
- package/bindings/mcp/hubris.ts +41 -0
- package/bindings/mcp/index.ts +13 -0
- package/bindings/mcp/kalos.ts +47 -0
- package/bindings/mcp/neikos.ts +76 -0
- package/bindings/mcp/orexis.ts +64 -0
- package/bindings/mcp/philia.ts +57 -0
- package/bindings/mcp/polemos.ts +55 -0
- package/bindings/mcp/skemma.ts +58 -0
- package/bindings/mcp/skopeo.ts +56 -0
- package/bindings/mcp/webAutomation.ts +31 -0
- package/bindings/model.ts +240 -0
- package/bindings/package.json +16 -0
- package/bindings/region.ts +3 -0
- package/bindings/serde_json/JsonValue.ts +3 -0
- package/bindings/ws/agentLifecycle.ts +41 -0
- package/bindings/ws/auth.ts +15 -0
- package/bindings/ws/baseMessages.ts +7 -0
- package/bindings/ws/bridgeNetwork.ts +71 -0
- package/bindings/ws/core.ts +51 -0
- package/bindings/ws/fileBrowsing.ts +57 -0
- package/bindings/ws/handshake.ts +23 -0
- package/bindings/ws/industrial.ts +72 -0
- package/bindings/ws/knowledgeBase.ts +10 -0
- package/bindings/ws/layer2.ts +25 -0
- package/bindings/ws/llmProvider.ts +74 -0
- package/bindings/ws/logs.ts +11 -0
- package/bindings/ws/malkuth.ts +62 -0
- package/bindings/ws/noa.ts +17 -0
- package/bindings/ws/stateSync.ts +19 -0
- package/bindings/ws/systemUi.ts +5 -0
- package/bindings/ws/tasks.ts +6 -0
- package/bindings/ws/views.ts +163 -0
- package/bindings/ws/workspace.ts +11 -0
- package/bindings/ws/yolo.ts +32 -0
- package/examples/schema_dump.rs +51 -0
- package/package.json +6 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/engine.rs +602 -0
- package/src/enums.rs +334 -0
- package/src/external_mcp.rs +132 -0
- package/src/http.rs +1077 -0
- package/src/identity.rs +160 -0
- package/src/lib.rs +1215 -0
- package/src/malkuth.rs +145 -0
- package/src/mcp/aporia.rs +272 -0
- package/src/mcp/eleos.rs +210 -0
- package/src/mcp/epieikeia.rs +194 -0
- package/src/mcp/haplotes.rs +310 -0
- package/src/mcp/hubris.rs +377 -0
- package/src/mcp/kalos.rs +251 -0
- package/src/mcp/mod.rs +23 -0
- package/src/mcp/neikos.rs +533 -0
- package/src/mcp/orexis.rs +493 -0
- package/src/mcp/philia.rs +267 -0
- package/src/mcp/polemos.rs +241 -0
- package/src/mcp/skemma.rs +442 -0
- package/src/mcp/skopeo.rs +282 -0
- package/src/mcp/web_automation.rs +122 -0
- package/src/model.rs +421 -0
- package/src/protocol/base_messages.rs +125 -0
- package/src/protocol/handshake.rs +364 -0
- package/src/protocol/jsonrpc.rs +888 -0
- package/src/protocol/mod.rs +10 -0
- package/src/rbac.rs +786 -0
- package/src/region.rs +362 -0
- package/src/tracing_helpers.rs +9 -0
- package/src/ws/agent/agent_lifecycle.rs +221 -0
- package/src/ws/agent/layer2.rs +126 -0
- package/src/ws/agent/mod.rs +9 -0
- package/src/ws/agent/state_sync.rs +111 -0
- package/src/ws/agent/tasks.rs +43 -0
- package/src/ws/agent/yolo.rs +161 -0
- package/src/ws/mod.rs +10 -0
- package/src/ws/services/auth.rs +99 -0
- package/src/ws/services/industrial.rs +647 -0
- package/src/ws/services/knowledge_base.rs +59 -0
- package/src/ws/services/llm_provider.rs +371 -0
- package/src/ws/services/mod.rs +7 -0
- package/src/ws/ui/bridge_network.rs +96 -0
- package/src/ws/ui/file_browsing.rs +88 -0
- package/src/ws/ui/logs.rs +55 -0
- package/src/ws/ui/mod.rs +11 -0
- package/src/ws/ui/noa.rs +105 -0
- package/src/ws/ui/system_ui.rs +27 -0
- package/src/ws/ui/views.rs +159 -0
- package/src/ws/ui/workspace.rs +73 -0
package/src/rbac.rs
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
use schemars::JsonSchema;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use ts_rs::TS;
|
|
4
|
+
|
|
5
|
+
/// The three dimensions of access control in arona.
|
|
6
|
+
///
|
|
7
|
+
/// (1) User — individual account
|
|
8
|
+
/// (2) UserGroup — collection of users sharing permissions
|
|
9
|
+
/// (3) ModelGroup — collection of models with access policies (replaces "workspace")
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)]
|
|
11
|
+
#[serde(rename_all = "snake_case")]
|
|
12
|
+
pub enum AccessDimension {
|
|
13
|
+
User,
|
|
14
|
+
UserGroup,
|
|
15
|
+
ModelGroup,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// The scope in which a permission is evaluated.
|
|
19
|
+
/// Some permissions are system-wide, others are tied to a model group,
|
|
20
|
+
/// and yet others are user-personal.
|
|
21
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)]
|
|
22
|
+
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
|
|
23
|
+
pub enum PermissionScope {
|
|
24
|
+
Global,
|
|
25
|
+
ModelGroup(String),
|
|
26
|
+
Personal,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Every discrete operation that can be authorised in the arona platform.
|
|
30
|
+
///
|
|
31
|
+
/// Serialised as a dotted path (`gateway.chat`, `model.deploy`, …).
|
|
32
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)]
|
|
33
|
+
pub enum Permission {
|
|
34
|
+
// ── Gateway & Proxy ──────────────────────────────────────────
|
|
35
|
+
#[serde(rename = "gateway.chat")]
|
|
36
|
+
GatewayChat,
|
|
37
|
+
#[serde(rename = "gateway.models")]
|
|
38
|
+
GatewayListModels,
|
|
39
|
+
#[serde(rename = "gateway.stream")]
|
|
40
|
+
GatewayStream,
|
|
41
|
+
|
|
42
|
+
// ── Conversations ────────────────────────────────────────────
|
|
43
|
+
#[serde(rename = "conversation.create")]
|
|
44
|
+
ConversationCreate,
|
|
45
|
+
#[serde(rename = "conversation.read")]
|
|
46
|
+
ConversationRead,
|
|
47
|
+
#[serde(rename = "conversation.delete")]
|
|
48
|
+
ConversationDelete,
|
|
49
|
+
|
|
50
|
+
// ── API Keys ─────────────────────────────────────────────────
|
|
51
|
+
#[serde(rename = "apikey.create")]
|
|
52
|
+
ApiKeyCreate,
|
|
53
|
+
#[serde(rename = "apikey.read")]
|
|
54
|
+
ApiKeyRead,
|
|
55
|
+
#[serde(rename = "apikey.revoke")]
|
|
56
|
+
ApiKeyRevoke,
|
|
57
|
+
|
|
58
|
+
// ── Model Management ─────────────────────────────────────────
|
|
59
|
+
#[serde(rename = "model.deploy")]
|
|
60
|
+
ModelDeploy,
|
|
61
|
+
#[serde(rename = "model.stop")]
|
|
62
|
+
ModelStop,
|
|
63
|
+
#[serde(rename = "model.read")]
|
|
64
|
+
ModelRead,
|
|
65
|
+
#[serde(rename = "model.download")]
|
|
66
|
+
ModelDownload,
|
|
67
|
+
|
|
68
|
+
// ── Provider Management ──────────────────────────────────────
|
|
69
|
+
#[serde(rename = "provider.create")]
|
|
70
|
+
ProviderCreate,
|
|
71
|
+
#[serde(rename = "provider.read")]
|
|
72
|
+
ProviderRead,
|
|
73
|
+
#[serde(rename = "provider.update")]
|
|
74
|
+
ProviderUpdate,
|
|
75
|
+
#[serde(rename = "provider.delete")]
|
|
76
|
+
ProviderDelete,
|
|
77
|
+
#[serde(rename = "provider.test")]
|
|
78
|
+
ProviderTest,
|
|
79
|
+
|
|
80
|
+
// ── Agent Management ─────────────────────────────────────────
|
|
81
|
+
#[serde(rename = "agent.register")]
|
|
82
|
+
AgentRegister,
|
|
83
|
+
#[serde(rename = "agent.read")]
|
|
84
|
+
AgentRead,
|
|
85
|
+
#[serde(rename = "agent.deregister")]
|
|
86
|
+
AgentDeregister,
|
|
87
|
+
|
|
88
|
+
// ── User & Group Management ──────────────────────────────────
|
|
89
|
+
#[serde(rename = "user.create")]
|
|
90
|
+
UserCreate,
|
|
91
|
+
#[serde(rename = "user.read")]
|
|
92
|
+
UserRead,
|
|
93
|
+
#[serde(rename = "user.update")]
|
|
94
|
+
UserUpdate,
|
|
95
|
+
#[serde(rename = "user.delete")]
|
|
96
|
+
UserDelete,
|
|
97
|
+
#[serde(rename = "group.create")]
|
|
98
|
+
GroupCreate,
|
|
99
|
+
#[serde(rename = "group.read")]
|
|
100
|
+
GroupRead,
|
|
101
|
+
#[serde(rename = "group.manage")]
|
|
102
|
+
GroupManage,
|
|
103
|
+
|
|
104
|
+
// ── System ───────────────────────────────────────────────────
|
|
105
|
+
#[serde(rename = "system.settings")]
|
|
106
|
+
SystemSettings,
|
|
107
|
+
#[serde(rename = "system.metrics")]
|
|
108
|
+
SystemMetrics,
|
|
109
|
+
#[serde(rename = "system.admin")]
|
|
110
|
+
SystemAdmin,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
impl Permission {
|
|
114
|
+
/// Human-readable English description.
|
|
115
|
+
pub fn description(&self) -> &'static str {
|
|
116
|
+
match self {
|
|
117
|
+
Self::GatewayChat => "Use /v1/chat/completions",
|
|
118
|
+
Self::GatewayListModels => "List available models",
|
|
119
|
+
Self::GatewayStream => "Use streaming responses",
|
|
120
|
+
Self::ConversationCreate => "Create conversations",
|
|
121
|
+
Self::ConversationRead => "Read own conversations",
|
|
122
|
+
Self::ConversationDelete => "Delete conversations",
|
|
123
|
+
Self::ApiKeyCreate => "Create API keys",
|
|
124
|
+
Self::ApiKeyRead => "List API keys",
|
|
125
|
+
Self::ApiKeyRevoke => "Revoke API keys",
|
|
126
|
+
Self::ModelDeploy => "Deploy models",
|
|
127
|
+
Self::ModelStop => "Stop models",
|
|
128
|
+
Self::ModelRead => "List/search models",
|
|
129
|
+
Self::ModelDownload => "Download models from registry",
|
|
130
|
+
Self::ProviderCreate => "Add providers",
|
|
131
|
+
Self::ProviderRead => "List providers",
|
|
132
|
+
Self::ProviderUpdate => "Update providers",
|
|
133
|
+
Self::ProviderDelete => "Remove providers",
|
|
134
|
+
Self::ProviderTest => "Test provider connection",
|
|
135
|
+
Self::AgentRegister => "Register agents",
|
|
136
|
+
Self::AgentRead => "List agents",
|
|
137
|
+
Self::AgentDeregister => "Deregister agents",
|
|
138
|
+
Self::UserCreate => "Create users",
|
|
139
|
+
Self::UserRead => "List users",
|
|
140
|
+
Self::UserUpdate => "Update users",
|
|
141
|
+
Self::UserDelete => "Delete users",
|
|
142
|
+
Self::GroupCreate => "Create groups",
|
|
143
|
+
Self::GroupRead => "List groups",
|
|
144
|
+
Self::GroupManage => "Add/remove members, change group permissions",
|
|
145
|
+
Self::SystemSettings => "Read/update system settings",
|
|
146
|
+
Self::SystemMetrics => "View metrics and logs",
|
|
147
|
+
Self::SystemAdmin => "Full admin access (god mode)",
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// i18n key for localisation (e.g. `"permission.gateway.chat"`).
|
|
152
|
+
pub fn i18n_key(&self) -> String {
|
|
153
|
+
format!("permission.{}", self.as_str())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Return the scope in which this permission is evaluated.
|
|
157
|
+
pub fn scope(&self) -> PermissionScope {
|
|
158
|
+
match self {
|
|
159
|
+
Self::GatewayChat
|
|
160
|
+
| Self::GatewayListModels
|
|
161
|
+
| Self::GatewayStream
|
|
162
|
+
| Self::ModelDeploy
|
|
163
|
+
| Self::ModelStop
|
|
164
|
+
| Self::ModelRead
|
|
165
|
+
| Self::ModelDownload
|
|
166
|
+
| Self::ProviderCreate
|
|
167
|
+
| Self::ProviderRead
|
|
168
|
+
| Self::ProviderUpdate
|
|
169
|
+
| Self::ProviderDelete
|
|
170
|
+
| Self::ProviderTest => PermissionScope::ModelGroup(String::new()),
|
|
171
|
+
|
|
172
|
+
Self::ConversationCreate
|
|
173
|
+
| Self::ConversationRead
|
|
174
|
+
| Self::ConversationDelete
|
|
175
|
+
| Self::ApiKeyCreate
|
|
176
|
+
| Self::ApiKeyRead
|
|
177
|
+
| Self::ApiKeyRevoke => PermissionScope::Personal,
|
|
178
|
+
|
|
179
|
+
Self::AgentRegister
|
|
180
|
+
| Self::AgentRead
|
|
181
|
+
| Self::AgentDeregister
|
|
182
|
+
| Self::UserCreate
|
|
183
|
+
| Self::UserRead
|
|
184
|
+
| Self::UserUpdate
|
|
185
|
+
| Self::UserDelete
|
|
186
|
+
| Self::GroupCreate
|
|
187
|
+
| Self::GroupRead
|
|
188
|
+
| Self::GroupManage
|
|
189
|
+
| Self::SystemSettings
|
|
190
|
+
| Self::SystemMetrics
|
|
191
|
+
| Self::SystemAdmin => PermissionScope::Global,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Return the serde wire name (e.g. `"gateway.chat"`).
|
|
196
|
+
pub fn as_str(&self) -> &'static str {
|
|
197
|
+
match self {
|
|
198
|
+
Self::GatewayChat => "gateway.chat",
|
|
199
|
+
Self::GatewayListModels => "gateway.models",
|
|
200
|
+
Self::GatewayStream => "gateway.stream",
|
|
201
|
+
Self::ConversationCreate => "conversation.create",
|
|
202
|
+
Self::ConversationRead => "conversation.read",
|
|
203
|
+
Self::ConversationDelete => "conversation.delete",
|
|
204
|
+
Self::ApiKeyCreate => "apikey.create",
|
|
205
|
+
Self::ApiKeyRead => "apikey.read",
|
|
206
|
+
Self::ApiKeyRevoke => "apikey.revoke",
|
|
207
|
+
Self::ModelDeploy => "model.deploy",
|
|
208
|
+
Self::ModelStop => "model.stop",
|
|
209
|
+
Self::ModelRead => "model.read",
|
|
210
|
+
Self::ModelDownload => "model.download",
|
|
211
|
+
Self::ProviderCreate => "provider.create",
|
|
212
|
+
Self::ProviderRead => "provider.read",
|
|
213
|
+
Self::ProviderUpdate => "provider.update",
|
|
214
|
+
Self::ProviderDelete => "provider.delete",
|
|
215
|
+
Self::ProviderTest => "provider.test",
|
|
216
|
+
Self::AgentRegister => "agent.register",
|
|
217
|
+
Self::AgentRead => "agent.read",
|
|
218
|
+
Self::AgentDeregister => "agent.deregister",
|
|
219
|
+
Self::UserCreate => "user.create",
|
|
220
|
+
Self::UserRead => "user.read",
|
|
221
|
+
Self::UserUpdate => "user.update",
|
|
222
|
+
Self::UserDelete => "user.delete",
|
|
223
|
+
Self::GroupCreate => "group.create",
|
|
224
|
+
Self::GroupRead => "group.read",
|
|
225
|
+
Self::GroupManage => "group.manage",
|
|
226
|
+
Self::SystemSettings => "system.settings",
|
|
227
|
+
Self::SystemMetrics => "system.metrics",
|
|
228
|
+
Self::SystemAdmin => "system.admin",
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Parse a dotted permission path (e.g. `"agent.read"`) back into a variant.
|
|
233
|
+
pub fn from_path(s: &str) -> Option<Self> {
|
|
234
|
+
match s {
|
|
235
|
+
"gateway.chat" => Some(Self::GatewayChat),
|
|
236
|
+
"gateway.models" => Some(Self::GatewayListModels),
|
|
237
|
+
"gateway.stream" => Some(Self::GatewayStream),
|
|
238
|
+
"conversation.create" => Some(Self::ConversationCreate),
|
|
239
|
+
"conversation.read" => Some(Self::ConversationRead),
|
|
240
|
+
"conversation.delete" => Some(Self::ConversationDelete),
|
|
241
|
+
"apikey.create" => Some(Self::ApiKeyCreate),
|
|
242
|
+
"apikey.read" => Some(Self::ApiKeyRead),
|
|
243
|
+
"apikey.revoke" => Some(Self::ApiKeyRevoke),
|
|
244
|
+
"model.deploy" => Some(Self::ModelDeploy),
|
|
245
|
+
"model.stop" => Some(Self::ModelStop),
|
|
246
|
+
"model.read" => Some(Self::ModelRead),
|
|
247
|
+
"model.download" => Some(Self::ModelDownload),
|
|
248
|
+
"provider.create" => Some(Self::ProviderCreate),
|
|
249
|
+
"provider.read" => Some(Self::ProviderRead),
|
|
250
|
+
"provider.update" => Some(Self::ProviderUpdate),
|
|
251
|
+
"provider.delete" => Some(Self::ProviderDelete),
|
|
252
|
+
"provider.test" => Some(Self::ProviderTest),
|
|
253
|
+
"agent.register" => Some(Self::AgentRegister),
|
|
254
|
+
"agent.read" => Some(Self::AgentRead),
|
|
255
|
+
"agent.deregister" => Some(Self::AgentDeregister),
|
|
256
|
+
"user.create" => Some(Self::UserCreate),
|
|
257
|
+
"user.read" => Some(Self::UserRead),
|
|
258
|
+
"user.update" => Some(Self::UserUpdate),
|
|
259
|
+
"user.delete" => Some(Self::UserDelete),
|
|
260
|
+
"group.create" => Some(Self::GroupCreate),
|
|
261
|
+
"group.read" => Some(Self::GroupRead),
|
|
262
|
+
"group.manage" => Some(Self::GroupManage),
|
|
263
|
+
"system.settings" => Some(Self::SystemSettings),
|
|
264
|
+
"system.metrics" => Some(Self::SystemMetrics),
|
|
265
|
+
"system.admin" => Some(Self::SystemAdmin),
|
|
266
|
+
_ => None,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Return all 31 leaf permissions.
|
|
271
|
+
pub fn all() -> Vec<Self> {
|
|
272
|
+
vec![
|
|
273
|
+
Self::GatewayChat,
|
|
274
|
+
Self::GatewayListModels,
|
|
275
|
+
Self::GatewayStream,
|
|
276
|
+
Self::ConversationCreate,
|
|
277
|
+
Self::ConversationRead,
|
|
278
|
+
Self::ConversationDelete,
|
|
279
|
+
Self::ApiKeyCreate,
|
|
280
|
+
Self::ApiKeyRead,
|
|
281
|
+
Self::ApiKeyRevoke,
|
|
282
|
+
Self::ModelDeploy,
|
|
283
|
+
Self::ModelStop,
|
|
284
|
+
Self::ModelRead,
|
|
285
|
+
Self::ModelDownload,
|
|
286
|
+
Self::ProviderCreate,
|
|
287
|
+
Self::ProviderRead,
|
|
288
|
+
Self::ProviderUpdate,
|
|
289
|
+
Self::ProviderDelete,
|
|
290
|
+
Self::ProviderTest,
|
|
291
|
+
Self::AgentRegister,
|
|
292
|
+
Self::AgentRead,
|
|
293
|
+
Self::AgentDeregister,
|
|
294
|
+
Self::UserCreate,
|
|
295
|
+
Self::UserRead,
|
|
296
|
+
Self::UserUpdate,
|
|
297
|
+
Self::UserDelete,
|
|
298
|
+
Self::GroupCreate,
|
|
299
|
+
Self::GroupRead,
|
|
300
|
+
Self::GroupManage,
|
|
301
|
+
Self::SystemSettings,
|
|
302
|
+
Self::SystemMetrics,
|
|
303
|
+
Self::SystemAdmin,
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Return all unique domain prefixes (first segment of the dotted path).
|
|
308
|
+
pub fn all_domains() -> Vec<&'static str> {
|
|
309
|
+
vec![
|
|
310
|
+
"gateway",
|
|
311
|
+
"conversation",
|
|
312
|
+
"apikey",
|
|
313
|
+
"model",
|
|
314
|
+
"provider",
|
|
315
|
+
"agent",
|
|
316
|
+
"user",
|
|
317
|
+
"group",
|
|
318
|
+
"system",
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Expand a domain name into all leaf permissions under it.
|
|
323
|
+
/// `"agent"` → `[AgentRegister, AgentRead, AgentDeregister]`.
|
|
324
|
+
pub fn expand_domain(domain: &str) -> Vec<Self> {
|
|
325
|
+
Self::all()
|
|
326
|
+
.into_iter()
|
|
327
|
+
.filter(|p| p.as_str().starts_with(domain) && p.as_str() != domain)
|
|
328
|
+
.collect()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Group all permissions by category (domain) for organised UI display.
|
|
332
|
+
/// Returns `Vec<(category_label, permissions)>`.
|
|
333
|
+
pub fn categories() -> Vec<(String, Vec<Self>)> {
|
|
334
|
+
vec![
|
|
335
|
+
(
|
|
336
|
+
"gateway".into(),
|
|
337
|
+
vec![
|
|
338
|
+
Self::GatewayChat,
|
|
339
|
+
Self::GatewayListModels,
|
|
340
|
+
Self::GatewayStream,
|
|
341
|
+
],
|
|
342
|
+
),
|
|
343
|
+
(
|
|
344
|
+
"conversation".into(),
|
|
345
|
+
vec![
|
|
346
|
+
Self::ConversationCreate,
|
|
347
|
+
Self::ConversationRead,
|
|
348
|
+
Self::ConversationDelete,
|
|
349
|
+
],
|
|
350
|
+
),
|
|
351
|
+
(
|
|
352
|
+
"apikey".into(),
|
|
353
|
+
vec![Self::ApiKeyCreate, Self::ApiKeyRead, Self::ApiKeyRevoke],
|
|
354
|
+
),
|
|
355
|
+
(
|
|
356
|
+
"model".into(),
|
|
357
|
+
vec![
|
|
358
|
+
Self::ModelDeploy,
|
|
359
|
+
Self::ModelStop,
|
|
360
|
+
Self::ModelRead,
|
|
361
|
+
Self::ModelDownload,
|
|
362
|
+
],
|
|
363
|
+
),
|
|
364
|
+
(
|
|
365
|
+
"provider".into(),
|
|
366
|
+
vec![
|
|
367
|
+
Self::ProviderCreate,
|
|
368
|
+
Self::ProviderRead,
|
|
369
|
+
Self::ProviderUpdate,
|
|
370
|
+
Self::ProviderDelete,
|
|
371
|
+
Self::ProviderTest,
|
|
372
|
+
],
|
|
373
|
+
),
|
|
374
|
+
(
|
|
375
|
+
"agent".into(),
|
|
376
|
+
vec![Self::AgentRegister, Self::AgentRead, Self::AgentDeregister],
|
|
377
|
+
),
|
|
378
|
+
(
|
|
379
|
+
"user".into(),
|
|
380
|
+
vec![
|
|
381
|
+
Self::UserCreate,
|
|
382
|
+
Self::UserRead,
|
|
383
|
+
Self::UserUpdate,
|
|
384
|
+
Self::UserDelete,
|
|
385
|
+
],
|
|
386
|
+
),
|
|
387
|
+
(
|
|
388
|
+
"group".into(),
|
|
389
|
+
vec![Self::GroupCreate, Self::GroupRead, Self::GroupManage],
|
|
390
|
+
),
|
|
391
|
+
(
|
|
392
|
+
"system".into(),
|
|
393
|
+
vec![Self::SystemSettings, Self::SystemMetrics, Self::SystemAdmin],
|
|
394
|
+
),
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ═══════════════════════════════════════════════════════════════
|
|
400
|
+
// Role definitions
|
|
401
|
+
// ═══════════════════════════════════════════════════════════════
|
|
402
|
+
|
|
403
|
+
/// A named collection of permissions that can be assigned to a user.
|
|
404
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)]
|
|
405
|
+
pub struct Role {
|
|
406
|
+
pub name: String,
|
|
407
|
+
pub description: String,
|
|
408
|
+
pub permissions: Vec<Permission>,
|
|
409
|
+
pub is_default: bool,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
impl Role {
|
|
413
|
+
/// Admin — every permission in the system.
|
|
414
|
+
pub fn admin() -> Self {
|
|
415
|
+
Self {
|
|
416
|
+
name: "admin".into(),
|
|
417
|
+
description: "Full system access — all permissions granted.".into(),
|
|
418
|
+
permissions: Permission::all(),
|
|
419
|
+
is_default: false,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Developer — gateway access, model operations, and personal resources.
|
|
424
|
+
pub fn developer() -> Self {
|
|
425
|
+
Self {
|
|
426
|
+
name: "developer".into(),
|
|
427
|
+
description: "Can chat, manage models, and handle own conversations & API keys.".into(),
|
|
428
|
+
permissions: vec![
|
|
429
|
+
Permission::GatewayChat,
|
|
430
|
+
Permission::GatewayListModels,
|
|
431
|
+
Permission::GatewayStream,
|
|
432
|
+
Permission::ConversationCreate,
|
|
433
|
+
Permission::ConversationRead,
|
|
434
|
+
Permission::ConversationDelete,
|
|
435
|
+
Permission::ApiKeyCreate,
|
|
436
|
+
Permission::ApiKeyRead,
|
|
437
|
+
Permission::ApiKeyRevoke,
|
|
438
|
+
Permission::ModelRead,
|
|
439
|
+
Permission::ProviderRead,
|
|
440
|
+
Permission::AgentRead,
|
|
441
|
+
],
|
|
442
|
+
is_default: true,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Viewer — read-only access across the platform.
|
|
447
|
+
pub fn viewer() -> Self {
|
|
448
|
+
Self {
|
|
449
|
+
name: "viewer".into(),
|
|
450
|
+
description: "Read-only access to models, providers, agents, and own resources.".into(),
|
|
451
|
+
permissions: vec![
|
|
452
|
+
Permission::GatewayListModels,
|
|
453
|
+
Permission::ConversationRead,
|
|
454
|
+
Permission::ApiKeyRead,
|
|
455
|
+
Permission::ModelRead,
|
|
456
|
+
Permission::ProviderRead,
|
|
457
|
+
Permission::AgentRead,
|
|
458
|
+
],
|
|
459
|
+
is_default: false,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// All three built-in roles.
|
|
464
|
+
pub fn builtin_roles() -> Vec<Self> {
|
|
465
|
+
vec![Self::admin(), Self::developer(), Self::viewer()]
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ═══════════════════════════════════════════════════════════════
|
|
470
|
+
// i18n key constants
|
|
471
|
+
// ═══════════════════════════════════════════════════════════════
|
|
472
|
+
|
|
473
|
+
pub mod i18n {
|
|
474
|
+
use super::Permission;
|
|
475
|
+
|
|
476
|
+
pub const PERMISSION_PREFIX: &str = "permission.";
|
|
477
|
+
pub const ROLE_PREFIX: &str = "role.";
|
|
478
|
+
|
|
479
|
+
pub const ROLE_ADMIN: &str = "role.admin";
|
|
480
|
+
pub const ROLE_DEVELOPER: &str = "role.developer";
|
|
481
|
+
pub const ROLE_VIEWER: &str = "role.viewer";
|
|
482
|
+
|
|
483
|
+
pub const DIM_USER: &str = "dimension.user";
|
|
484
|
+
pub const DIM_USER_GROUP: &str = "dimension.user_group";
|
|
485
|
+
pub const DIM_MODEL_GROUP: &str = "dimension.model_group";
|
|
486
|
+
|
|
487
|
+
pub const SCOPE_GLOBAL: &str = "scope.global";
|
|
488
|
+
pub const SCOPE_PERSONAL: &str = "scope.personal";
|
|
489
|
+
pub const SCOPE_MODEL_GROUP: &str = "scope.model_group";
|
|
490
|
+
|
|
491
|
+
/// Build an i18n key from a permission, e.g. `"permission.gateway.chat"`.
|
|
492
|
+
pub fn permission_key(p: &Permission) -> String {
|
|
493
|
+
format!("{}{}", PERMISSION_PREFIX, p.as_str())
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════
|
|
498
|
+
// Convenience free functions (mirror the old rbac module's API)
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════
|
|
500
|
+
|
|
501
|
+
/// Parse a dotted permission path string into a `Permission` variant.
|
|
502
|
+
pub fn parse_permission(s: &str) -> Option<Permission> {
|
|
503
|
+
Permission::from_path(s)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/// Validate a list of grant strings. Returns `Ok(())` if all are valid
|
|
507
|
+
/// leaf permissions or domain prefixes; returns `Err(invalid)` otherwise.
|
|
508
|
+
pub fn validate_grant_permissions(grants: &[String]) -> Result<(), Vec<String>> {
|
|
509
|
+
let invalid: Vec<String> = grants
|
|
510
|
+
.iter()
|
|
511
|
+
.filter(|g| Permission::from_path(g).is_none() && Permission::expand_domain(g).is_empty())
|
|
512
|
+
.cloned()
|
|
513
|
+
.collect();
|
|
514
|
+
if invalid.is_empty() {
|
|
515
|
+
Ok(())
|
|
516
|
+
} else {
|
|
517
|
+
Err(invalid)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/// Return the serde names of all leaf permissions.
|
|
522
|
+
pub fn list_all_permission_names() -> Vec<String> {
|
|
523
|
+
Permission::all()
|
|
524
|
+
.iter()
|
|
525
|
+
.map(|p| p.as_str().to_string())
|
|
526
|
+
.collect()
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/// Return all domain prefix strings.
|
|
530
|
+
pub fn list_all_domain_names() -> Vec<&'static str> {
|
|
531
|
+
Permission::all_domains()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// Expand a domain name into the names of all leaf permissions under it.
|
|
535
|
+
pub fn expand_domain(domain: &str) -> Vec<String> {
|
|
536
|
+
Permission::expand_domain(domain)
|
|
537
|
+
.iter()
|
|
538
|
+
.map(|p| p.as_str().to_string())
|
|
539
|
+
.collect()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ═══════════════════════════════════════════════════════════════
|
|
543
|
+
// Tests
|
|
544
|
+
// ═══════════════════════════════════════════════════════════════
|
|
545
|
+
|
|
546
|
+
#[cfg(test)]
|
|
547
|
+
mod tests {
|
|
548
|
+
use super::*;
|
|
549
|
+
|
|
550
|
+
// ── Permission::from_path / parse_permission ────────────────
|
|
551
|
+
|
|
552
|
+
#[test]
|
|
553
|
+
fn parse_valid_leaf() {
|
|
554
|
+
assert!(parse_permission("agent.read").is_some());
|
|
555
|
+
assert!(parse_permission("model.deploy").is_some());
|
|
556
|
+
assert!(parse_permission("provider.test").is_some());
|
|
557
|
+
assert!(parse_permission("system.admin").is_some());
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
#[test]
|
|
561
|
+
fn parse_invalid() {
|
|
562
|
+
assert!(parse_permission("nonexistent.action").is_none());
|
|
563
|
+
assert!(parse_permission("").is_none());
|
|
564
|
+
assert!(parse_permission("agent.invalid").is_none());
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── validate_grant_permissions ──────────────────────────────
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn validate_grants_ok() {
|
|
571
|
+
let grants = vec!["agent.read".into(), "model.deploy".into()];
|
|
572
|
+
assert!(validate_grant_permissions(&grants).is_ok());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn validate_grants_rejects_invalid() {
|
|
577
|
+
let grants = vec!["agent.read".into(), "invalid.permission".into()];
|
|
578
|
+
assert!(validate_grant_permissions(&grants).is_err());
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#[test]
|
|
582
|
+
fn validate_grants_allows_domain_wildcards() {
|
|
583
|
+
let grants = vec!["agent".into()];
|
|
584
|
+
assert!(validate_grant_permissions(&grants).is_ok());
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── counts ──────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
#[test]
|
|
590
|
+
fn all_permissions_count() {
|
|
591
|
+
let names = list_all_permission_names();
|
|
592
|
+
assert_eq!(names.len(), 31, "expected 31 leaf permissions");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#[test]
|
|
596
|
+
fn all_domains_count() {
|
|
597
|
+
let domains = list_all_domain_names();
|
|
598
|
+
assert_eq!(domains.len(), 9);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── Permission::expand_domain ───────────────────────────────
|
|
602
|
+
|
|
603
|
+
#[test]
|
|
604
|
+
fn expand_agent_domain() {
|
|
605
|
+
let perms = Permission::expand_domain("agent");
|
|
606
|
+
assert_eq!(perms.len(), 3);
|
|
607
|
+
assert!(perms.contains(&Permission::AgentRegister));
|
|
608
|
+
assert!(perms.contains(&Permission::AgentRead));
|
|
609
|
+
assert!(perms.contains(&Permission::AgentDeregister));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#[test]
|
|
613
|
+
fn expand_nonexistent_domain() {
|
|
614
|
+
let perms = Permission::expand_domain("nope");
|
|
615
|
+
assert!(perms.is_empty());
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── Permission::scope ───────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn system_admin_is_global() {
|
|
622
|
+
assert_eq!(Permission::SystemAdmin.scope(), PermissionScope::Global);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn model_deploy_is_model_group_scoped() {
|
|
627
|
+
assert_eq!(
|
|
628
|
+
Permission::ModelDeploy.scope(),
|
|
629
|
+
PermissionScope::ModelGroup(String::new())
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#[test]
|
|
634
|
+
fn conversation_create_is_personal() {
|
|
635
|
+
assert_eq!(
|
|
636
|
+
Permission::ConversationCreate.scope(),
|
|
637
|
+
PermissionScope::Personal
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── Permission serde round-trip ─────────────────────────────
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
fn permission_serde_as_string() {
|
|
645
|
+
let json = serde_json::to_string(&Permission::GatewayChat).unwrap();
|
|
646
|
+
assert_eq!(json, r#""gateway.chat""#);
|
|
647
|
+
let back: Permission = serde_json::from_str(&json).unwrap();
|
|
648
|
+
assert_eq!(back, Permission::GatewayChat);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#[test]
|
|
652
|
+
fn all_permissions_round_trip() {
|
|
653
|
+
for p in Permission::all() {
|
|
654
|
+
let s = serde_json::to_string(&p).unwrap();
|
|
655
|
+
let back: Permission = serde_json::from_str(&s).unwrap();
|
|
656
|
+
assert_eq!(back, p, "round-trip failed for {p:?}");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── Role definitions ────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
fn admin_has_all_permissions() {
|
|
664
|
+
let admin = Role::admin();
|
|
665
|
+
assert_eq!(admin.permissions.len(), Permission::all().len());
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
#[test]
|
|
669
|
+
fn developer_has_subset() {
|
|
670
|
+
let dev = Role::developer();
|
|
671
|
+
assert!(dev.permissions.len() < Permission::all().len());
|
|
672
|
+
assert!(dev.is_default);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#[test]
|
|
676
|
+
fn viewer_is_read_only() {
|
|
677
|
+
let viewer = Role::viewer();
|
|
678
|
+
// viewer shouldn't have any create/delete/deploy/stop permissions
|
|
679
|
+
let write_perms: Vec<_> = viewer
|
|
680
|
+
.permissions
|
|
681
|
+
.iter()
|
|
682
|
+
.filter(|p| {
|
|
683
|
+
p.as_str().contains(".create")
|
|
684
|
+
|| p.as_str().contains(".delete")
|
|
685
|
+
|| p.as_str().contains(".deploy")
|
|
686
|
+
|| p.as_str().contains(".stop")
|
|
687
|
+
|| p.as_str().contains(".revoke")
|
|
688
|
+
|| p.as_str().contains(".register")
|
|
689
|
+
|| p.as_str().contains(".deregister")
|
|
690
|
+
|| p.as_str().contains(".update")
|
|
691
|
+
})
|
|
692
|
+
.collect();
|
|
693
|
+
assert!(
|
|
694
|
+
write_perms.is_empty(),
|
|
695
|
+
"viewer should have no write permissions, got: {write_perms:?}"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#[test]
|
|
700
|
+
fn three_builtin_roles() {
|
|
701
|
+
assert_eq!(Role::builtin_roles().len(), 3);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── i18n keys ───────────────────────────────────────────────
|
|
705
|
+
|
|
706
|
+
#[test]
|
|
707
|
+
fn i18n_permission_key() {
|
|
708
|
+
assert_eq!(
|
|
709
|
+
i18n::permission_key(&Permission::GatewayChat),
|
|
710
|
+
"permission.gateway.chat"
|
|
711
|
+
);
|
|
712
|
+
assert_eq!(
|
|
713
|
+
i18n::permission_key(&Permission::SystemAdmin),
|
|
714
|
+
"permission.system.admin"
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── AccessDimension serde ───────────────────────────────────
|
|
719
|
+
|
|
720
|
+
#[test]
|
|
721
|
+
fn access_dimension_serde_snake_case() {
|
|
722
|
+
let json = serde_json::to_string(&AccessDimension::UserGroup).unwrap();
|
|
723
|
+
assert_eq!(json, r#""user_group""#);
|
|
724
|
+
let back: AccessDimension = serde_json::from_str(&json).unwrap();
|
|
725
|
+
assert_eq!(back, AccessDimension::UserGroup);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ── PermissionScope serde ───────────────────────────────────
|
|
729
|
+
|
|
730
|
+
#[test]
|
|
731
|
+
fn permission_scope_global_serde() {
|
|
732
|
+
let json = serde_json::to_string(&PermissionScope::Global).unwrap();
|
|
733
|
+
assert_eq!(json, r#"{"type":"global"}"#);
|
|
734
|
+
let back: PermissionScope = serde_json::from_str(&json).unwrap();
|
|
735
|
+
assert_eq!(back, PermissionScope::Global);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
#[test]
|
|
739
|
+
fn permission_scope_model_group_serde() {
|
|
740
|
+
let scope = PermissionScope::ModelGroup("mg-42".into());
|
|
741
|
+
let json = serde_json::to_string(&scope).unwrap();
|
|
742
|
+
assert_eq!(json, r#"{"type":"model_group","value":"mg-42"}"#);
|
|
743
|
+
let back: PermissionScope = serde_json::from_str(&json).unwrap();
|
|
744
|
+
assert_eq!(back, scope);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#[test]
|
|
748
|
+
fn permission_scope_personal_serde() {
|
|
749
|
+
let json = serde_json::to_string(&PermissionScope::Personal).unwrap();
|
|
750
|
+
assert_eq!(json, r#"{"type":"personal"}"#);
|
|
751
|
+
let back: PermissionScope = serde_json::from_str(&json).unwrap();
|
|
752
|
+
assert_eq!(back, PermissionScope::Personal);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ── categories ──────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
#[test]
|
|
758
|
+
fn categories_count() {
|
|
759
|
+
let cats = Permission::categories();
|
|
760
|
+
assert_eq!(cats.len(), 9);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
#[test]
|
|
764
|
+
fn all_permissions_in_categories() {
|
|
765
|
+
let mut seen = std::collections::HashSet::new();
|
|
766
|
+
for (_, perms) in Permission::categories() {
|
|
767
|
+
for p in perms {
|
|
768
|
+
seen.insert(p);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
assert_eq!(seen.len(), Permission::all().len());
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ── description / as_str exhaustiveness ─────────────────────
|
|
775
|
+
|
|
776
|
+
#[test]
|
|
777
|
+
fn every_permission_has_description_and_str() {
|
|
778
|
+
for p in Permission::all() {
|
|
779
|
+
let desc = p.description();
|
|
780
|
+
assert!(!desc.is_empty(), "missing description for {p:?}");
|
|
781
|
+
let s = p.as_str();
|
|
782
|
+
assert!(!s.is_empty(), "missing as_str for {p:?}");
|
|
783
|
+
assert!(s.contains('.'), "as_str must be dotted: {s}");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|