pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Agent boundary types — library primitives, user policies.
//!
//! Six typed boundary mechanisms that control what an agent can do.
//! All default to fully open. Opt-in, not opt-out.
//! Enforced by the runtime automatically — nodes don't check manually.

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

use crate::agent::AgentId;

// ── Permissions ───────────────────────────────────────────────────────

/// What actions are allowed/denied.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionSet {
    pub allowed: HashSet<Permission>,
    pub denied: HashSet<Permission>,
    pub default_policy: DefaultPolicy,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Permission {
    VaultRead,
    VaultWrite,
    VaultDelete,
    FileRead,
    FileWrite,
    FileDelete,
    ShellExec,
    NetworkAccess,
    MemoryRead,
    MemoryWrite,
    TaskCreate,
    TaskModify,
    CollectiveRead,
    CollectiveWrite,
    Custom(String),
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum DefaultPolicy {
    #[default]
    AllowUnlessExplicitlyDenied,
    DenyUnlessExplicitlyAllowed,
}

impl PermissionSet {
    pub fn fully_open() -> Self {
        Self {
            allowed: HashSet::new(),
            denied: HashSet::new(),
            default_policy: DefaultPolicy::AllowUnlessExplicitlyDenied,
        }
    }

    pub fn fully_locked() -> Self {
        Self {
            allowed: HashSet::new(),
            denied: HashSet::new(),
            default_policy: DefaultPolicy::DenyUnlessExplicitlyAllowed,
        }
    }

    pub fn is_allowed(&self, permission: &Permission) -> bool {
        if self.denied.contains(permission) {
            return false;
        }
        if self.allowed.contains(permission) {
            return true;
        }
        matches!(
            self.default_policy,
            DefaultPolicy::AllowUnlessExplicitlyDenied
        )
    }
}

impl Default for PermissionSet {
    fn default() -> Self {
        Self::fully_open()
    }
}

// ── Tool Policy ───────────────────────────────────────────────────────

/// Which tools, with what limits.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolPolicy {
    pub rules: HashMap<String, ToolConstraint>,
    pub default: ToolDefaultPolicy,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ToolConstraint {
    Allowed {
        max_calls_per_turn: Option<u32>,
        max_calls_per_session: Option<u32>,
    },
    Denied,
    RequiresApproval,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ToolDefaultPolicy {
    #[default]
    AllowAll,
    DenyUnlisted,
}

impl ToolPolicy {
    pub fn open() -> Self {
        Self {
            rules: HashMap::new(),
            default: ToolDefaultPolicy::AllowAll,
        }
    }

    /// Whether a tool is executable without any further approval step.
    pub fn allows_without_approval(&self, tool_name: &str) -> bool {
        match self.rules.get(tool_name) {
            Some(ToolConstraint::Denied) => false,
            Some(ToolConstraint::Allowed { .. }) => true,
            Some(ToolConstraint::RequiresApproval) => false,
            None => matches!(self.default, ToolDefaultPolicy::AllowAll),
        }
    }

    /// Whether this tool requires an explicit approval step before execution.
    pub fn requires_approval(&self, tool_name: &str) -> bool {
        matches!(
            self.rules.get(tool_name),
            Some(ToolConstraint::RequiresApproval)
        )
    }

    /// Access the configured rule for a specific tool, if one exists.
    pub fn constraint(&self, tool_name: &str) -> Option<&ToolConstraint> {
        self.rules.get(tool_name)
    }

    /// Backward-compatible helper retained for existing callers.
    pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
        self.allows_without_approval(tool_name)
    }
}

impl Default for ToolPolicy {
    fn default() -> Self {
        Self::open()
    }
}

// ── Communication Rules ───────────────────────────────────────────────

/// Who can this agent reach.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunicationRules {
    pub can_delegate_to: Option<HashSet<AgentId>>,
    pub can_query: Option<HashSet<AgentId>>,
    pub cannot_contact: HashSet<AgentId>,
    pub default: CommunicationDefault,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum CommunicationDefault {
    #[default]
    AllowAll,
    DelegateOnly,
    DenyAll,
}

impl CommunicationRules {
    pub fn open() -> Self {
        Self {
            can_delegate_to: None,
            can_query: None,
            cannot_contact: HashSet::new(),
            default: CommunicationDefault::AllowAll,
        }
    }

    pub fn can_reach(&self, target: &AgentId) -> bool {
        if self.cannot_contact.contains(target) {
            return false;
        }
        match &self.can_delegate_to {
            Some(allowed) => allowed.contains(target),
            None => !matches!(self.default, CommunicationDefault::DenyAll),
        }
    }

    /// Check whether this agent can query the target agent.
    pub fn can_query(&self, target: &AgentId) -> bool {
        if self.cannot_contact.contains(target) {
            return false;
        }
        match &self.can_query {
            Some(allowed) => allowed.contains(target),
            None => !matches!(self.default, CommunicationDefault::DenyAll),
        }
    }
}

impl Default for CommunicationRules {
    fn default() -> Self {
        Self::open()
    }
}

// ── Context Budget ────────────────────────────────────────────────────

/// Token budgets per context layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextBudget {
    pub self_context_tokens: u32,
    pub collective_tokens: u32,
    pub task_context_tokens: u32,
    pub execution_awareness_tokens: u32,
    pub handoff_context_tokens: u32,
    pub overflow_policy: OverflowPolicy,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum OverflowPolicy {
    Compress,
    Truncate,
    #[default]
    Warn,
}

impl Default for ContextBudget {
    fn default() -> Self {
        Self {
            self_context_tokens: 4096,
            collective_tokens: 2048,
            task_context_tokens: 1024,
            execution_awareness_tokens: 512,
            handoff_context_tokens: 512,
            overflow_policy: OverflowPolicy::Warn,
        }
    }
}

// ── Write Governance ──────────────────────────────────────────────────

/// What can be persisted, where.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteGovernance {
    pub own_memory: WriteAccess,
    pub collective: WriteAccess,
    pub vault: WriteAccess,
    pub task_store: WriteAccess,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WriteAccess {
    #[default]
    Free,
    RequiresGrant,
    ReadOnly,
    Attributed,
}

impl Default for WriteGovernance {
    fn default() -> Self {
        Self {
            own_memory: WriteAccess::Free,
            collective: WriteAccess::Free,
            vault: WriteAccess::Free,
            task_store: WriteAccess::Free,
        }
    }
}

// ── Guardrails ────────────────────────────────────────────────────────

/// Output constraints enforced after execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Guardrail {
    MaxOutputTokens(u32),
    MustCiteSources,
    NoCodeExecution,
    MaxToolCallsPerTurn(u32),
    Custom { name: String, description: String },
}