Distributed Erlang over iroh P2P + TLS, with a bot powers SDK
1//! Capability flags ("powers") that bots can be granted.
2//!
3//! Powers gate high-level SDK operations so a bot only gets the tools it needs.
4
5use bitflags::bitflags;
6
7bitflags! {
8 /// Set of powers a [`crate::Bot`] may exercise.
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10 pub struct Powers: u32 {
11 /// Call remote MFA via `erpc` / `rex` (`spawn_request`).
12 const RPC = 0b0000_0001;
13 /// Send messages to registered names / pids.
14 const SEND = 0b0000_0010;
15 /// Introspect processes, applications, ETS (via RPC helpers).
16 const INTROSPECT = 0b0000_0100;
17 /// Connect / disconnect peers (iroh, tcp, tls).
18 const CLUSTER = 0b0000_1000;
19 /// Accept inbound distribution connections.
20 const LISTEN = 0b0001_0000;
21 /// Evaluate expressions (`erl_eval` style RPC).
22 const EVAL = 0b0010_0000;
23 /// Start / stop function tracing on peers.
24 const TRACE = 0b0100_0000;
25 }
26}
27
28impl Powers {
29 /// Safe default for a worker bot: rpc + send + introspect + cluster.
30 pub fn worker() -> Self {
31 Self::RPC | Self::SEND | Self::INTROSPECT | Self::CLUSTER
32 }
33
34 /// Read-only observer: introspect + cluster.
35 pub fn observer() -> Self {
36 Self::INTROSPECT | Self::CLUSTER
37 }
38
39 /// Human-readable list of set flags.
40 pub fn describe(self) -> String {
41 let mut names = Vec::new();
42 if self.contains(Self::RPC) {
43 names.push("rpc");
44 }
45 if self.contains(Self::SEND) {
46 names.push("send");
47 }
48 if self.contains(Self::INTROSPECT) {
49 names.push("introspect");
50 }
51 if self.contains(Self::CLUSTER) {
52 names.push("cluster");
53 }
54 if self.contains(Self::LISTEN) {
55 names.push("listen");
56 }
57 if self.contains(Self::EVAL) {
58 names.push("eval");
59 }
60 if self.contains(Self::TRACE) {
61 names.push("trace");
62 }
63 if names.is_empty() {
64 "none".into()
65 } else {
66 names.join("|")
67 }
68 }
69
70 /// Return `Ok(())` if `needed` is granted, else [`crate::Error::PowerDenied`].
71 pub fn require(self, needed: Powers) -> crate::Result<()> {
72 if self.contains(needed) {
73 Ok(())
74 } else {
75 Err(crate::Error::PowerDenied {
76 needed: power_name(needed),
77 have: self.describe(),
78 })
79 }
80 }
81}
82
83impl Default for Powers {
84 fn default() -> Self {
85 Self::worker()
86 }
87}
88
89impl std::fmt::Display for Powers {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "{}", self.describe())
92 }
93}
94
95fn power_name(p: Powers) -> &'static str {
96 // Match single-flag values used at call sites.
97 if p == Powers::RPC {
98 "rpc"
99 } else if p == Powers::SEND {
100 "send"
101 } else if p == Powers::INTROSPECT {
102 "introspect"
103 } else if p == Powers::CLUSTER {
104 "cluster"
105 } else if p == Powers::LISTEN {
106 "listen"
107 } else if p == Powers::EVAL {
108 "eval"
109 } else if p == Powers::TRACE {
110 "trace"
111 } else {
112 "unknown"
113 }
114}