1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ActionCategory {
36 Dml,
38 Ddl,
40 Schema,
42 Function,
44 Mgmt,
46 Policy,
48 Admin,
50 Config,
52 Vault,
54 Wildcard,
56 Ai,
58 Notification,
61 Stream,
65 Queue,
69 Graph,
74 Ops,
82 Vector,
88 Other,
91}
92
93impl ActionCategory {
94 pub fn as_str(&self) -> &'static str {
98 match self {
99 ActionCategory::Dml => "dml",
100 ActionCategory::Ddl => "ddl",
101 ActionCategory::Schema => "schema",
102 ActionCategory::Function => "function",
103 ActionCategory::Mgmt => "mgmt",
104 ActionCategory::Policy => "policy",
105 ActionCategory::Admin => "admin",
106 ActionCategory::Config => "config",
107 ActionCategory::Vault => "vault",
108 ActionCategory::Wildcard => "wildcard",
109 ActionCategory::Ai => "ai",
110 ActionCategory::Notification => "notification",
111 ActionCategory::Stream => "stream",
112 ActionCategory::Queue => "queue",
113 ActionCategory::Graph => "graph",
114 ActionCategory::Ops => "ops",
115 ActionCategory::Vector => "vector",
116 ActionCategory::Other => "other",
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum LifecycleState {
124 Active,
126 Deprecated {
128 replacement: Option<&'static str>,
130 since_version: &'static str,
132 },
133 Removed,
137}
138
139#[derive(Debug, Clone)]
141pub struct ActionEntry {
142 pub name: &'static str,
143 pub category: ActionCategory,
144 pub lifecycle_state: LifecycleState,
145 pub gates_description: &'static str,
146}
147
148pub const ACTIONS: &[ActionEntry] = &[
157 ActionEntry {
159 name: "select",
160 category: ActionCategory::Dml,
161 lifecycle_state: LifecycleState::Active,
162 gates_description: "read rows from a collection",
163 },
164 ActionEntry {
165 name: "write",
166 category: ActionCategory::Dml,
167 lifecycle_state: LifecycleState::Active,
168 gates_description: "any mutating DML (insert/update/delete)",
169 },
170 ActionEntry {
171 name: "insert",
172 category: ActionCategory::Dml,
173 lifecycle_state: LifecycleState::Active,
174 gates_description: "insert rows into a collection",
175 },
176 ActionEntry {
177 name: "update",
178 category: ActionCategory::Dml,
179 lifecycle_state: LifecycleState::Active,
180 gates_description: "update rows in a collection",
181 },
182 ActionEntry {
183 name: "delete",
184 category: ActionCategory::Dml,
185 lifecycle_state: LifecycleState::Active,
186 gates_description: "delete rows from a collection",
187 },
188 ActionEntry {
189 name: "truncate",
190 category: ActionCategory::Dml,
191 lifecycle_state: LifecycleState::Active,
192 gates_description: "truncate a collection",
193 },
194 ActionEntry {
195 name: "references",
196 category: ActionCategory::Schema,
197 lifecycle_state: LifecycleState::Active,
198 gates_description: "declare a foreign key referencing a table",
199 },
200 ActionEntry {
201 name: "execute",
202 category: ActionCategory::Function,
203 lifecycle_state: LifecycleState::Active,
204 gates_description: "execute a stored function",
205 },
206 ActionEntry {
207 name: "usage",
208 category: ActionCategory::Schema,
209 lifecycle_state: LifecycleState::Active,
210 gates_description: "use a schema namespace",
211 },
212 ActionEntry {
213 name: "grant",
214 category: ActionCategory::Mgmt,
215 lifecycle_state: LifecycleState::Active,
216 gates_description: "grant privileges to another principal",
217 },
218 ActionEntry {
219 name: "revoke",
220 category: ActionCategory::Mgmt,
221 lifecycle_state: LifecycleState::Active,
222 gates_description: "revoke privileges from another principal",
223 },
224 ActionEntry {
225 name: "create",
226 category: ActionCategory::Ddl,
227 lifecycle_state: LifecycleState::Active,
228 gates_description: "create a database object",
229 },
230 ActionEntry {
231 name: "drop",
232 category: ActionCategory::Ddl,
233 lifecycle_state: LifecycleState::Active,
234 gates_description: "drop a database object",
235 },
236 ActionEntry {
237 name: "alter",
238 category: ActionCategory::Ddl,
239 lifecycle_state: LifecycleState::Active,
240 gates_description: "alter a database object",
241 },
242 ActionEntry {
251 name: "schema:write",
252 category: ActionCategory::Schema,
253 lifecycle_state: LifecycleState::Active,
254 gates_description: "grouped DDL on the current schema namespace (foreign table, migration)",
255 },
256 ActionEntry {
257 name: "schema:admin",
258 category: ActionCategory::Admin,
259 lifecycle_state: LifecycleState::Active,
260 gates_description: "namespace-level DDL (CREATE SCHEMA, CREATE SERVER)",
261 },
262 ActionEntry {
264 name: "policy:put",
265 category: ActionCategory::Policy,
266 lifecycle_state: LifecycleState::Active,
267 gates_description: "create or update a managed policy document",
268 },
269 ActionEntry {
270 name: "policy:drop",
271 category: ActionCategory::Policy,
272 lifecycle_state: LifecycleState::Active,
273 gates_description: "delete a managed policy document",
274 },
275 ActionEntry {
276 name: "policy:attach",
277 category: ActionCategory::Policy,
278 lifecycle_state: LifecycleState::Active,
279 gates_description: "attach a policy to a principal",
280 },
281 ActionEntry {
282 name: "policy:detach",
283 category: ActionCategory::Policy,
284 lifecycle_state: LifecycleState::Active,
285 gates_description: "detach a policy from a principal",
286 },
287 ActionEntry {
288 name: "policy:simulate",
289 category: ActionCategory::Policy,
290 lifecycle_state: LifecycleState::Active,
291 gates_description: "run the policy simulator",
292 },
293 ActionEntry {
295 name: "kv:invalidate",
296 category: ActionCategory::Other,
297 lifecycle_state: LifecycleState::Active,
298 gates_description: "invalidate cached KV entries",
299 },
300 ActionEntry {
302 name: "admin:bootstrap",
303 category: ActionCategory::Admin,
304 lifecycle_state: LifecycleState::Active,
305 gates_description: "execute the bootstrap workflow",
306 },
307 ActionEntry {
308 name: "admin:audit-read",
309 category: ActionCategory::Admin,
310 lifecycle_state: LifecycleState::Active,
311 gates_description: "read the platform audit log",
312 },
313 ActionEntry {
314 name: "admin:reload",
315 category: ActionCategory::Admin,
316 lifecycle_state: LifecycleState::Active,
317 gates_description: "reload runtime configuration",
318 },
319 ActionEntry {
320 name: "admin:lease-promote",
321 category: ActionCategory::Admin,
322 lifecycle_state: LifecycleState::Active,
323 gates_description: "promote a standby instance via lease handoff",
324 },
325 ActionEntry {
327 name: "config:read",
328 category: ActionCategory::Config,
329 lifecycle_state: LifecycleState::Active,
330 gates_description: "read runtime configuration values",
331 },
332 ActionEntry {
333 name: "config:write",
334 category: ActionCategory::Config,
335 lifecycle_state: LifecycleState::Active,
336 gates_description: "mutate runtime configuration values",
337 },
338 ActionEntry {
339 name: "config:*",
340 category: ActionCategory::Wildcard,
341 lifecycle_state: LifecycleState::Active,
342 gates_description: "any runtime configuration verb",
343 },
344 ActionEntry {
346 name: "vault:read_metadata",
347 category: ActionCategory::Vault,
348 lifecycle_state: LifecycleState::Active,
349 gates_description: "read vault entry metadata (no plaintext)",
350 },
351 ActionEntry {
352 name: "vault:read",
353 category: ActionCategory::Vault,
354 lifecycle_state: LifecycleState::Active,
355 gates_description: "reveal vault entry plaintext",
356 },
357 ActionEntry {
358 name: "vault:write",
359 category: ActionCategory::Vault,
360 lifecycle_state: LifecycleState::Active,
361 gates_description: "write or rotate vault entries",
362 },
363 ActionEntry {
364 name: "vault:unseal",
365 category: ActionCategory::Vault,
366 lifecycle_state: LifecycleState::Active,
367 gates_description: "unseal the vault master key for this session",
368 },
369 ActionEntry {
375 name: "vault:unseal_history",
376 category: ActionCategory::Vault,
377 lifecycle_state: LifecycleState::Deprecated {
378 replacement: Some("vault:read_metadata"),
379 since_version: "0.5.0",
380 },
381 gates_description: "read the vault unseal-event audit trail",
382 },
383 ActionEntry {
384 name: "vault:purge",
385 category: ActionCategory::Vault,
386 lifecycle_state: LifecycleState::Active,
387 gates_description: "purge (destructively remove) vault entries",
388 },
389 ActionEntry {
391 name: "evidence:export",
392 category: ActionCategory::Other,
393 lifecycle_state: LifecycleState::Active,
394 gates_description: "export evidence bundles",
395 },
396 ActionEntry {
397 name: "evidence:*",
398 category: ActionCategory::Wildcard,
399 lifecycle_state: LifecycleState::Active,
400 gates_description: "any evidence-pipeline verb",
401 },
402 ActionEntry {
404 name: "red.registry:register",
405 category: ActionCategory::Other,
406 lifecycle_state: LifecycleState::Active,
407 gates_description: "register a new managed-config schema",
408 },
409 ActionEntry {
410 name: "red.registry:supersede",
411 category: ActionCategory::Other,
412 lifecycle_state: LifecycleState::Active,
413 gates_description: "supersede an existing managed-config schema",
414 },
415 ActionEntry {
416 name: "red.registry:*",
417 category: ActionCategory::Wildcard,
418 lifecycle_state: LifecycleState::Active,
419 gates_description: "any registry verb",
420 },
421 ActionEntry {
428 name: "ai:provider:openai",
429 category: ActionCategory::Ai,
430 lifecycle_state: LifecycleState::Active,
431 gates_description: "use the OpenAI provider for ASK / AUTO EMBED / SEARCH SIMILAR",
432 },
433 ActionEntry {
434 name: "ai:provider:anthropic",
435 category: ActionCategory::Ai,
436 lifecycle_state: LifecycleState::Active,
437 gates_description: "use the Anthropic provider for ASK / AUTO EMBED / SEARCH SIMILAR",
438 },
439 ActionEntry {
440 name: "ai:provider:groq",
441 category: ActionCategory::Ai,
442 lifecycle_state: LifecycleState::Active,
443 gates_description: "use the Groq provider for ASK / AUTO EMBED / SEARCH SIMILAR",
444 },
445 ActionEntry {
446 name: "ai:provider:openrouter",
447 category: ActionCategory::Ai,
448 lifecycle_state: LifecycleState::Active,
449 gates_description: "use the OpenRouter provider for ASK / AUTO EMBED / SEARCH SIMILAR",
450 },
451 ActionEntry {
452 name: "ai:provider:together",
453 category: ActionCategory::Ai,
454 lifecycle_state: LifecycleState::Active,
455 gates_description: "use the Together provider for ASK / AUTO EMBED / SEARCH SIMILAR",
456 },
457 ActionEntry {
458 name: "ai:provider:venice",
459 category: ActionCategory::Ai,
460 lifecycle_state: LifecycleState::Active,
461 gates_description: "use the Venice provider for ASK / AUTO EMBED / SEARCH SIMILAR",
462 },
463 ActionEntry {
464 name: "ai:provider:ollama",
465 category: ActionCategory::Ai,
466 lifecycle_state: LifecycleState::Active,
467 gates_description: "use the Ollama provider for ASK / AUTO EMBED / SEARCH SIMILAR",
468 },
469 ActionEntry {
470 name: "ai:provider:deepseek",
471 category: ActionCategory::Ai,
472 lifecycle_state: LifecycleState::Active,
473 gates_description: "use the DeepSeek provider for ASK / AUTO EMBED / SEARCH SIMILAR",
474 },
475 ActionEntry {
476 name: "ai:provider:huggingface",
477 category: ActionCategory::Ai,
478 lifecycle_state: LifecycleState::Active,
479 gates_description: "use the HuggingFace provider for ASK / AUTO EMBED / SEARCH SIMILAR",
480 },
481 ActionEntry {
482 name: "ai:provider:local",
483 category: ActionCategory::Ai,
484 lifecycle_state: LifecycleState::Active,
485 gates_description: "use the local (in-process) embedding provider",
486 },
487 ActionEntry {
488 name: "ai:provider:*",
489 category: ActionCategory::Wildcard,
490 lifecycle_state: LifecycleState::Active,
491 gates_description: "use any AI provider (provider-gate wildcard)",
492 },
493 ActionEntry {
494 name: "ai:*",
495 category: ActionCategory::Wildcard,
496 lifecycle_state: LifecycleState::Active,
497 gates_description: "any AI-namespace verb",
498 },
499 ActionEntry {
505 name: "notify",
506 category: ActionCategory::Notification,
507 lifecycle_state: LifecycleState::Active,
508 gates_description:
509 "publish to / subscribe to ephemeral notification channels in the principal's own tenant",
510 },
511 ActionEntry {
512 name: "notify:cross-tenant",
513 category: ActionCategory::Notification,
514 lifecycle_state: LifecycleState::Active,
515 gates_description:
516 "address ephemeral notification channels in another tenant or the global namespace",
517 },
518 ActionEntry {
519 name: "notify:*",
520 category: ActionCategory::Wildcard,
521 lifecycle_state: LifecycleState::Active,
522 gates_description: "any ephemeral notification verb",
523 },
524 ActionEntry {
531 name: "stream",
532 category: ActionCategory::Stream,
533 lifecycle_state: LifecycleState::Active,
534 gates_description:
535 "append, read, and offset-save on durable streams in the principal's own tenant",
536 },
537 ActionEntry {
538 name: "stream:cross-tenant",
539 category: ActionCategory::Stream,
540 lifecycle_state: LifecycleState::Active,
541 gates_description:
542 "address durable streams in another tenant or the global namespace",
543 },
544 ActionEntry {
545 name: "stream:*",
546 category: ActionCategory::Wildcard,
547 lifecycle_state: LifecycleState::Active,
548 gates_description: "any durable stream verb",
549 },
550 ActionEntry {
558 name: "queue:enqueue",
559 category: ActionCategory::Queue,
560 lifecycle_state: LifecycleState::Active,
561 gates_description: "push / produce a message onto a queue",
562 },
563 ActionEntry {
564 name: "queue:read",
565 category: ActionCategory::Queue,
566 lifecycle_state: LifecycleState::Active,
567 gates_description: "destructive read: pop, group-read, claim",
568 },
569 ActionEntry {
570 name: "queue:peek",
571 category: ActionCategory::Queue,
572 lifecycle_state: LifecycleState::Active,
573 gates_description: "non-destructive read: peek, len, pending, select",
574 },
575 ActionEntry {
576 name: "queue:ack",
577 category: ActionCategory::Queue,
578 lifecycle_state: LifecycleState::Active,
579 gates_description: "acknowledge a delivered queue message",
580 },
581 ActionEntry {
582 name: "queue:nack",
583 category: ActionCategory::Queue,
584 lifecycle_state: LifecycleState::Active,
585 gates_description: "negative-acknowledge / requeue a delivered queue message",
586 },
587 ActionEntry {
588 name: "queue:retry",
589 category: ActionCategory::Queue,
590 lifecycle_state: LifecycleState::Active,
591 gates_description: "override retry policy (e.g. per-failure NACK delay)",
592 },
593 ActionEntry {
594 name: "queue:dlq:move",
595 category: ActionCategory::Queue,
596 lifecycle_state: LifecycleState::Active,
597 gates_description: "move / replay messages between a queue and its DLQ",
598 },
599 ActionEntry {
600 name: "queue:purge",
601 category: ActionCategory::Queue,
602 lifecycle_state: LifecycleState::Active,
603 gates_description: "destructively purge all messages from a queue",
604 },
605 ActionEntry {
606 name: "queue:presence:read",
607 category: ActionCategory::Queue,
608 lifecycle_state: LifecycleState::Active,
609 gates_description: "read consumer presence / heartbeat snapshots",
610 },
611 ActionEntry {
612 name: "queue:*",
613 category: ActionCategory::Wildcard,
614 lifecycle_state: LifecycleState::Active,
615 gates_description: "any queue verb",
616 },
617 ActionEntry {
628 name: "graph:read",
629 category: ActionCategory::Graph,
630 lifecycle_state: LifecycleState::Active,
631 gates_description: "read graph node/edge metadata and graph-wide properties",
632 },
633 ActionEntry {
634 name: "graph:traverse",
635 category: ActionCategory::Graph,
636 lifecycle_state: LifecycleState::Active,
637 gates_description: "execute pattern match / neighborhood / path traversal queries",
638 },
639 ActionEntry {
640 name: "graph:algorithm:run",
641 category: ActionCategory::Graph,
642 lifecycle_state: LifecycleState::Active,
643 gates_description: "run a graph analytics algorithm (centrality, community, components, ...)",
644 },
645 ActionEntry {
646 name: "graph:*",
647 category: ActionCategory::Wildcard,
648 lifecycle_state: LifecycleState::Active,
649 gates_description: "any graph verb",
650 },
651 ActionEntry {
666 name: "ops:read:self",
667 category: ActionCategory::Ops,
668 lifecycle_state: LifecycleState::Active,
669 gates_description: "read single-instance health / lifecycle state",
670 },
671 ActionEntry {
672 name: "ops:read:tenant",
673 category: ActionCategory::Ops,
674 lifecycle_state: LifecycleState::Active,
675 gates_description: "read tenant-scoped operational metrics / aggregates",
676 },
677 ActionEntry {
678 name: "ops:read:cluster",
679 category: ActionCategory::Ops,
680 lifecycle_state: LifecycleState::Active,
681 gates_description:
682 "read cluster topology / replication / backup / full metrics exposition",
683 },
684 ActionEntry {
685 name: "ops:admin",
686 category: ActionCategory::Ops,
687 lifecycle_state: LifecycleState::Active,
688 gates_description:
689 "read security-sensitive operational state (audit log, vault posture)",
690 },
691 ActionEntry {
692 name: "ops:*",
693 category: ActionCategory::Wildcard,
694 lifecycle_state: LifecycleState::Active,
695 gates_description: "any operational read verb",
696 },
697 ActionEntry {
702 name: "cluster:replication:stream",
703 category: ActionCategory::Other,
704 lifecycle_state: LifecycleState::Active,
705 gates_description: "stream primary WAL records and replication snapshots to a replica",
706 },
707 ActionEntry {
708 name: "cluster:replication:ack",
709 category: ActionCategory::Other,
710 lifecycle_state: LifecycleState::Active,
711 gates_description: "acknowledge replica LSN progress to the primary",
712 },
713 ActionEntry {
714 name: "cluster:*",
715 category: ActionCategory::Wildcard,
716 lifecycle_state: LifecycleState::Active,
717 gates_description: "any cluster-scoped capability",
718 },
719 ActionEntry {
731 name: "vector:read",
732 category: ActionCategory::Vector,
733 lifecycle_state: LifecycleState::Active,
734 gates_description: "read vector metadata / data (non-search reads on a vector collection)",
735 },
736 ActionEntry {
737 name: "vector:search",
738 category: ActionCategory::Vector,
739 lifecycle_state: LifecycleState::Active,
740 gates_description: "similarity / text / hybrid search against a vector collection",
741 },
742 ActionEntry {
743 name: "vector:artifact:read",
744 category: ActionCategory::Vector,
745 lifecycle_state: LifecycleState::Active,
746 gates_description: "introspect operational vector index artifacts (pages, status)",
747 },
748 ActionEntry {
749 name: "vector:artifact:rebuild",
750 category: ActionCategory::Vector,
751 lifecycle_state: LifecycleState::Active,
752 gates_description: "rebuild / warmup vector index artifacts",
753 },
754 ActionEntry {
755 name: "vector:admin",
756 category: ActionCategory::Vector,
757 lifecycle_state: LifecycleState::Active,
758 gates_description: "admin operations on a vector collection (clustering, maintenance)",
759 },
760 ActionEntry {
761 name: "vector:*",
762 category: ActionCategory::Wildcard,
763 lifecycle_state: LifecycleState::Active,
764 gates_description: "any vector verb",
765 },
766 ActionEntry {
768 name: "*",
769 category: ActionCategory::Wildcard,
770 lifecycle_state: LifecycleState::Active,
771 gates_description: "any action (escape hatch — audit usage carefully)",
772 },
773 ActionEntry {
774 name: "admin:*",
775 category: ActionCategory::Wildcard,
776 lifecycle_state: LifecycleState::Active,
777 gates_description: "any admin verb",
778 },
779 ActionEntry {
780 name: "vault:*",
781 category: ActionCategory::Wildcard,
782 lifecycle_state: LifecycleState::Active,
783 gates_description: "any vault verb",
784 },
785 ActionEntry {
786 name: "kv:*",
787 category: ActionCategory::Wildcard,
788 lifecycle_state: LifecycleState::Active,
789 gates_description: "any KV verb",
790 },
791 ActionEntry {
792 name: "policy:*",
793 category: ActionCategory::Wildcard,
794 lifecycle_state: LifecycleState::Active,
795 gates_description: "any policy lifecycle verb",
796 },
797];
798
799pub fn is_valid_action(name: &str) -> bool {
803 ACTIONS
804 .iter()
805 .any(|e| e.name == name && !matches!(e.lifecycle_state, LifecycleState::Removed))
806}
807
808pub fn lookup(name: &str) -> Option<&'static ActionEntry> {
810 ACTIONS.iter().find(|e| e.name == name)
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use std::collections::HashSet;
817
818 const HISTORICAL_ALLOWLIST: &[&str] = &[
823 "select",
824 "write",
825 "insert",
826 "update",
827 "delete",
828 "truncate",
829 "references",
830 "execute",
831 "usage",
832 "grant",
833 "revoke",
834 "create",
835 "drop",
836 "alter",
837 "policy:put",
838 "policy:drop",
839 "policy:attach",
840 "policy:detach",
841 "policy:simulate",
842 "kv:invalidate",
843 "admin:bootstrap",
844 "admin:audit-read",
845 "admin:reload",
846 "admin:lease-promote",
847 "config:read",
848 "config:write",
849 "config:*",
850 "vault:read_metadata",
851 "vault:read",
852 "vault:write",
853 "vault:unseal",
854 "vault:unseal_history",
855 "vault:purge",
856 "evidence:export",
857 "evidence:*",
858 "red.registry:register",
859 "red.registry:supersede",
860 "red.registry:*",
861 "*",
862 "admin:*",
863 "vault:*",
864 "kv:*",
865 "policy:*",
866 ];
867
868 #[test]
869 fn no_duplicate_names() {
870 let mut seen = HashSet::new();
871 for entry in ACTIONS {
872 assert!(
873 seen.insert(entry.name),
874 "duplicate action name in catalog: {}",
875 entry.name
876 );
877 }
878 }
879
880 #[test]
881 fn covers_historical_allowlist() {
882 let names: HashSet<&'static str> = ACTIONS.iter().map(|e| e.name).collect();
883 for action in HISTORICAL_ALLOWLIST {
884 assert!(
885 names.contains(action),
886 "catalog missing historically-accepted action: {action}",
887 );
888 }
889 }
890
891 #[test]
892 fn historical_allowlist_still_validates() {
893 for action in HISTORICAL_ALLOWLIST {
894 assert!(
895 is_valid_action(action),
896 "action {action} was accepted before the catalog and must still validate",
897 );
898 }
899 }
900
901 #[test]
902 fn has_at_least_one_deprecated_entry() {
903 let count = ACTIONS
904 .iter()
905 .filter(|e| matches!(e.lifecycle_state, LifecycleState::Deprecated { .. }))
906 .count();
907 assert!(
908 count >= 1,
909 "catalog must demonstrate the Deprecated lifecycle state with at least one entry",
910 );
911 }
912
913 #[test]
914 fn removed_entries_are_rejected() {
915 for entry in ACTIONS {
918 if matches!(entry.lifecycle_state, LifecycleState::Removed) {
919 assert!(
920 !is_valid_action(entry.name),
921 "Removed entry {} must not validate",
922 entry.name,
923 );
924 }
925 }
926 }
927
928 #[test]
929 fn lookup_finds_known_entries() {
930 assert!(lookup("policy:put").is_some());
931 assert!(lookup("definitely-not-an-action").is_none());
932 }
933}