Skip to main content

pe_core/
boundaries.rs

1//! Agent boundary types — library primitives, user policies.
2//!
3//! Six typed boundary mechanisms that control what an agent can do.
4//! All default to fully open. Opt-in, not opt-out.
5//! Enforced by the runtime automatically — nodes don't check manually.
6
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10use crate::agent::AgentId;
11
12// ── Permissions ───────────────────────────────────────────────────────
13
14/// What actions are allowed/denied.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PermissionSet {
17    pub allowed: HashSet<Permission>,
18    pub denied: HashSet<Permission>,
19    pub default_policy: DefaultPolicy,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[non_exhaustive]
24pub enum Permission {
25    VaultRead,
26    VaultWrite,
27    VaultDelete,
28    FileRead,
29    FileWrite,
30    FileDelete,
31    ShellExec,
32    NetworkAccess,
33    MemoryRead,
34    MemoryWrite,
35    TaskCreate,
36    TaskModify,
37    CollectiveRead,
38    CollectiveWrite,
39    Custom(String),
40}
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub enum DefaultPolicy {
44    #[default]
45    AllowUnlessExplicitlyDenied,
46    DenyUnlessExplicitlyAllowed,
47}
48
49impl PermissionSet {
50    pub fn fully_open() -> Self {
51        Self {
52            allowed: HashSet::new(),
53            denied: HashSet::new(),
54            default_policy: DefaultPolicy::AllowUnlessExplicitlyDenied,
55        }
56    }
57
58    pub fn fully_locked() -> Self {
59        Self {
60            allowed: HashSet::new(),
61            denied: HashSet::new(),
62            default_policy: DefaultPolicy::DenyUnlessExplicitlyAllowed,
63        }
64    }
65
66    pub fn is_allowed(&self, permission: &Permission) -> bool {
67        if self.denied.contains(permission) {
68            return false;
69        }
70        if self.allowed.contains(permission) {
71            return true;
72        }
73        matches!(
74            self.default_policy,
75            DefaultPolicy::AllowUnlessExplicitlyDenied
76        )
77    }
78}
79
80impl Default for PermissionSet {
81    fn default() -> Self {
82        Self::fully_open()
83    }
84}
85
86// ── Tool Policy ───────────────────────────────────────────────────────
87
88/// Which tools, with what limits.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ToolPolicy {
91    pub rules: HashMap<String, ToolConstraint>,
92    pub default: ToolDefaultPolicy,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96#[non_exhaustive]
97pub enum ToolConstraint {
98    Allowed {
99        max_calls_per_turn: Option<u32>,
100        max_calls_per_session: Option<u32>,
101    },
102    Denied,
103    RequiresApproval,
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub enum ToolDefaultPolicy {
108    #[default]
109    AllowAll,
110    DenyUnlisted,
111}
112
113impl ToolPolicy {
114    pub fn open() -> Self {
115        Self {
116            rules: HashMap::new(),
117            default: ToolDefaultPolicy::AllowAll,
118        }
119    }
120
121    /// Whether a tool is executable without any further approval step.
122    pub fn allows_without_approval(&self, tool_name: &str) -> bool {
123        match self.rules.get(tool_name) {
124            Some(ToolConstraint::Denied) => false,
125            Some(ToolConstraint::Allowed { .. }) => true,
126            Some(ToolConstraint::RequiresApproval) => false,
127            None => matches!(self.default, ToolDefaultPolicy::AllowAll),
128        }
129    }
130
131    /// Whether this tool requires an explicit approval step before execution.
132    pub fn requires_approval(&self, tool_name: &str) -> bool {
133        matches!(
134            self.rules.get(tool_name),
135            Some(ToolConstraint::RequiresApproval)
136        )
137    }
138
139    /// Access the configured rule for a specific tool, if one exists.
140    pub fn constraint(&self, tool_name: &str) -> Option<&ToolConstraint> {
141        self.rules.get(tool_name)
142    }
143
144    /// Backward-compatible helper retained for existing callers.
145    pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
146        self.allows_without_approval(tool_name)
147    }
148}
149
150impl Default for ToolPolicy {
151    fn default() -> Self {
152        Self::open()
153    }
154}
155
156// ── Communication Rules ───────────────────────────────────────────────
157
158/// Who can this agent reach.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct CommunicationRules {
161    pub can_delegate_to: Option<HashSet<AgentId>>,
162    pub can_query: Option<HashSet<AgentId>>,
163    pub cannot_contact: HashSet<AgentId>,
164    pub default: CommunicationDefault,
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub enum CommunicationDefault {
169    #[default]
170    AllowAll,
171    DelegateOnly,
172    DenyAll,
173}
174
175impl CommunicationRules {
176    pub fn open() -> Self {
177        Self {
178            can_delegate_to: None,
179            can_query: None,
180            cannot_contact: HashSet::new(),
181            default: CommunicationDefault::AllowAll,
182        }
183    }
184
185    pub fn can_reach(&self, target: &AgentId) -> bool {
186        if self.cannot_contact.contains(target) {
187            return false;
188        }
189        match &self.can_delegate_to {
190            Some(allowed) => allowed.contains(target),
191            None => !matches!(self.default, CommunicationDefault::DenyAll),
192        }
193    }
194
195    /// Check whether this agent can query the target agent.
196    pub fn can_query(&self, target: &AgentId) -> bool {
197        if self.cannot_contact.contains(target) {
198            return false;
199        }
200        match &self.can_query {
201            Some(allowed) => allowed.contains(target),
202            None => !matches!(self.default, CommunicationDefault::DenyAll),
203        }
204    }
205}
206
207impl Default for CommunicationRules {
208    fn default() -> Self {
209        Self::open()
210    }
211}
212
213// ── Context Budget ────────────────────────────────────────────────────
214
215/// Token budgets per context layer.
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ContextBudget {
218    pub self_context_tokens: u32,
219    pub collective_tokens: u32,
220    pub task_context_tokens: u32,
221    pub execution_awareness_tokens: u32,
222    pub handoff_context_tokens: u32,
223    pub overflow_policy: OverflowPolicy,
224}
225
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub enum OverflowPolicy {
228    Compress,
229    Truncate,
230    #[default]
231    Warn,
232}
233
234impl Default for ContextBudget {
235    fn default() -> Self {
236        Self {
237            self_context_tokens: 4096,
238            collective_tokens: 2048,
239            task_context_tokens: 1024,
240            execution_awareness_tokens: 512,
241            handoff_context_tokens: 512,
242            overflow_policy: OverflowPolicy::Warn,
243        }
244    }
245}
246
247// ── Write Governance ──────────────────────────────────────────────────
248
249/// What can be persisted, where.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct WriteGovernance {
252    pub own_memory: WriteAccess,
253    pub collective: WriteAccess,
254    pub vault: WriteAccess,
255    pub task_store: WriteAccess,
256}
257
258#[derive(Debug, Clone, Default, Serialize, Deserialize)]
259#[non_exhaustive]
260pub enum WriteAccess {
261    #[default]
262    Free,
263    RequiresGrant,
264    ReadOnly,
265    Attributed,
266}
267
268impl Default for WriteGovernance {
269    fn default() -> Self {
270        Self {
271            own_memory: WriteAccess::Free,
272            collective: WriteAccess::Free,
273            vault: WriteAccess::Free,
274            task_store: WriteAccess::Free,
275        }
276    }
277}
278
279// ── Guardrails ────────────────────────────────────────────────────────
280
281/// Output constraints enforced after execution.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[non_exhaustive]
284pub enum Guardrail {
285    MaxOutputTokens(u32),
286    MustCiteSources,
287    NoCodeExecution,
288    MaxToolCallsPerTurn(u32),
289    Custom { name: String, description: String },
290}