@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.
Files changed (96) hide show
  1. package/Cargo.toml +38 -0
  2. package/bindings/FileAnchor.ts +3 -0
  3. package/bindings/engine.ts +277 -0
  4. package/bindings/enums.ts +31 -0
  5. package/bindings/httpTypes.ts +197 -0
  6. package/bindings/index.ts +47 -0
  7. package/bindings/mcp/aporia.ts +61 -0
  8. package/bindings/mcp/eleos.ts +18 -0
  9. package/bindings/mcp/epieikeia.ts +48 -0
  10. package/bindings/mcp/haplotes.ts +47 -0
  11. package/bindings/mcp/hubris.ts +41 -0
  12. package/bindings/mcp/index.ts +13 -0
  13. package/bindings/mcp/kalos.ts +47 -0
  14. package/bindings/mcp/neikos.ts +76 -0
  15. package/bindings/mcp/orexis.ts +64 -0
  16. package/bindings/mcp/philia.ts +57 -0
  17. package/bindings/mcp/polemos.ts +55 -0
  18. package/bindings/mcp/skemma.ts +58 -0
  19. package/bindings/mcp/skopeo.ts +56 -0
  20. package/bindings/mcp/webAutomation.ts +31 -0
  21. package/bindings/model.ts +240 -0
  22. package/bindings/package.json +16 -0
  23. package/bindings/region.ts +3 -0
  24. package/bindings/serde_json/JsonValue.ts +3 -0
  25. package/bindings/ws/agentLifecycle.ts +41 -0
  26. package/bindings/ws/auth.ts +15 -0
  27. package/bindings/ws/baseMessages.ts +7 -0
  28. package/bindings/ws/bridgeNetwork.ts +71 -0
  29. package/bindings/ws/core.ts +51 -0
  30. package/bindings/ws/fileBrowsing.ts +57 -0
  31. package/bindings/ws/handshake.ts +23 -0
  32. package/bindings/ws/industrial.ts +72 -0
  33. package/bindings/ws/knowledgeBase.ts +10 -0
  34. package/bindings/ws/layer2.ts +25 -0
  35. package/bindings/ws/llmProvider.ts +74 -0
  36. package/bindings/ws/logs.ts +11 -0
  37. package/bindings/ws/malkuth.ts +62 -0
  38. package/bindings/ws/noa.ts +17 -0
  39. package/bindings/ws/stateSync.ts +19 -0
  40. package/bindings/ws/systemUi.ts +5 -0
  41. package/bindings/ws/tasks.ts +6 -0
  42. package/bindings/ws/views.ts +163 -0
  43. package/bindings/ws/workspace.ts +11 -0
  44. package/bindings/ws/yolo.ts +32 -0
  45. package/examples/schema_dump.rs +51 -0
  46. package/package.json +6 -0
  47. package/pnpm-workspace.yaml +2 -0
  48. package/src/engine.rs +602 -0
  49. package/src/enums.rs +334 -0
  50. package/src/external_mcp.rs +132 -0
  51. package/src/http.rs +1077 -0
  52. package/src/identity.rs +160 -0
  53. package/src/lib.rs +1215 -0
  54. package/src/malkuth.rs +145 -0
  55. package/src/mcp/aporia.rs +272 -0
  56. package/src/mcp/eleos.rs +210 -0
  57. package/src/mcp/epieikeia.rs +194 -0
  58. package/src/mcp/haplotes.rs +310 -0
  59. package/src/mcp/hubris.rs +377 -0
  60. package/src/mcp/kalos.rs +251 -0
  61. package/src/mcp/mod.rs +23 -0
  62. package/src/mcp/neikos.rs +533 -0
  63. package/src/mcp/orexis.rs +493 -0
  64. package/src/mcp/philia.rs +267 -0
  65. package/src/mcp/polemos.rs +241 -0
  66. package/src/mcp/skemma.rs +442 -0
  67. package/src/mcp/skopeo.rs +282 -0
  68. package/src/mcp/web_automation.rs +122 -0
  69. package/src/model.rs +421 -0
  70. package/src/protocol/base_messages.rs +125 -0
  71. package/src/protocol/handshake.rs +364 -0
  72. package/src/protocol/jsonrpc.rs +888 -0
  73. package/src/protocol/mod.rs +10 -0
  74. package/src/rbac.rs +786 -0
  75. package/src/region.rs +362 -0
  76. package/src/tracing_helpers.rs +9 -0
  77. package/src/ws/agent/agent_lifecycle.rs +221 -0
  78. package/src/ws/agent/layer2.rs +126 -0
  79. package/src/ws/agent/mod.rs +9 -0
  80. package/src/ws/agent/state_sync.rs +111 -0
  81. package/src/ws/agent/tasks.rs +43 -0
  82. package/src/ws/agent/yolo.rs +161 -0
  83. package/src/ws/mod.rs +10 -0
  84. package/src/ws/services/auth.rs +99 -0
  85. package/src/ws/services/industrial.rs +647 -0
  86. package/src/ws/services/knowledge_base.rs +59 -0
  87. package/src/ws/services/llm_provider.rs +371 -0
  88. package/src/ws/services/mod.rs +7 -0
  89. package/src/ws/ui/bridge_network.rs +96 -0
  90. package/src/ws/ui/file_browsing.rs +88 -0
  91. package/src/ws/ui/logs.rs +55 -0
  92. package/src/ws/ui/mod.rs +11 -0
  93. package/src/ws/ui/noa.rs +105 -0
  94. package/src/ws/ui/system_ui.rs +27 -0
  95. package/src/ws/ui/views.rs +159 -0
  96. 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
+ }