Skip to main content

repl_core/
runtime_bridge.rs

1use std::sync::{Arc, Mutex};
2use symbi_runtime::communication::policy_gate::CommunicationPolicyGate;
3use symbi_runtime::communication::{
4    CommunicationBus, CommunicationConfig, DefaultCommunicationBus,
5};
6use symbi_runtime::context::manager::{ContextManagerConfig, StandardContextManager};
7use symbi_runtime::integrations::policy_engine::engine::{
8    OpaPolicyEngine, PolicyDecision, PolicyEngine,
9};
10use symbi_runtime::lifecycle::{DefaultLifecycleController, LifecycleConfig, LifecycleController};
11use symbi_runtime::reasoning::agent_registry::AgentRegistry;
12use symbi_runtime::reasoning::inference::InferenceProvider;
13use symbi_runtime::types::agent::AgentConfig;
14use symbi_runtime::types::security::Capability;
15use symbi_runtime::types::AgentId;
16
17/// The RuntimeBridge manages a simulated, in-process Symbiont runtime environment.
18pub struct RuntimeBridge {
19    lifecycle_controller: Arc<Mutex<Option<Arc<DefaultLifecycleController>>>>,
20    context_manager: Arc<Mutex<Option<Arc<StandardContextManager>>>>,
21    policy_engine: Arc<Mutex<OpaPolicyEngine>>,
22    /// Inference provider for reasoning builtins.
23    inference_provider: Arc<Mutex<Option<Arc<dyn InferenceProvider>>>>,
24    /// Agent registry for multi-agent composition.
25    agent_registry: Arc<AgentRegistry>,
26    /// Communication bus for agent-to-agent messaging (set in initialize()).
27    comm_bus: Arc<Mutex<Option<Arc<dyn CommunicationBus + Send + Sync>>>>,
28    /// Communication policy gate (deny-by-default; replaced via set_comm_policy).
29    comm_policy: Arc<Mutex<Arc<CommunicationPolicyGate>>>,
30}
31
32impl Default for RuntimeBridge {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl RuntimeBridge {
39    /// Construct a RuntimeBridge with a **deny-by-default** communication policy.
40    ///
41    /// Production callers should immediately follow with [`Self::set_comm_policy`]
42    /// to install a policy containing the rules required for their agent topology.
43    /// Inter-agent messaging will fail with `PolicyDenied` until rules are configured.
44    ///
45    /// For tests or trusted single-tenant dev environments, use
46    /// [`Self::new_permissive_for_dev`] to opt into allow-all semantics.
47    pub fn new() -> Self {
48        Self::with_policy(Arc::new(CommunicationPolicyGate::new(Vec::new())))
49    }
50
51    /// Construct a RuntimeBridge with a permissive (allow-all) communication policy.
52    ///
53    /// This is ONLY safe for local development, tests, or single-tenant
54    /// trusted environments. Never use in multi-tenant or production deployments:
55    /// permissive mode allows any agent to message any other agent with no gating.
56    pub fn new_permissive_for_dev() -> Self {
57        Self::with_policy(Arc::new(CommunicationPolicyGate::permissive()))
58    }
59
60    /// Construct a RuntimeBridge with an explicit communication policy gate.
61    pub fn with_policy(comm_policy_gate: Arc<CommunicationPolicyGate>) -> Self {
62        let lifecycle_controller = Arc::new(Mutex::new(None));
63        let context_manager = Arc::new(Mutex::new(None));
64        let policy_engine = Arc::new(Mutex::new(OpaPolicyEngine::new()));
65        let inference_provider = Arc::new(Mutex::new(None));
66        let agent_registry = Arc::new(AgentRegistry::new());
67        let comm_bus = Arc::new(Mutex::new(None));
68        let comm_policy = Arc::new(Mutex::new(comm_policy_gate));
69
70        Self {
71            lifecycle_controller,
72            context_manager,
73            policy_engine,
74            inference_provider,
75            agent_registry,
76            comm_bus,
77            comm_policy,
78        }
79    }
80
81    /// Set the inference provider for reasoning builtins.
82    pub fn set_inference_provider(&self, provider: Arc<dyn InferenceProvider>) {
83        *self.inference_provider.lock().unwrap() = Some(provider);
84    }
85
86    /// Get the agent registry.
87    pub fn agent_registry(&self) -> Arc<AgentRegistry> {
88        Arc::clone(&self.agent_registry)
89    }
90
91    /// Get the communication bus (if initialized).
92    pub fn comm_bus(&self) -> Option<Arc<dyn CommunicationBus + Send + Sync>> {
93        self.comm_bus.lock().unwrap().clone()
94    }
95
96    /// Replace the communication policy gate.
97    pub fn set_comm_policy(&self, policy: Arc<CommunicationPolicyGate>) {
98        *self.comm_policy.lock().unwrap() = policy;
99    }
100
101    /// Get the reasoning context for async builtins.
102    ///
103    /// Includes the communication bus and policy gate if they've been initialized
104    /// via [`initialize`]. The bus is used by `ask`, `send_to`, `parallel`, and
105    /// `race` builtins for policy-gated, audit-logged agent-to-agent messaging.
106    pub fn reasoning_context(&self) -> crate::dsl::reasoning_builtins::ReasoningBuiltinContext {
107        let provider = self.inference_provider.lock().unwrap().clone();
108        let comm_bus = self.comm_bus.lock().unwrap().clone();
109        let comm_policy = Some(self.comm_policy.lock().unwrap().clone());
110        crate::dsl::reasoning_builtins::ReasoningBuiltinContext {
111            provider,
112            agent_registry: Some(Arc::clone(&self.agent_registry)),
113            sender_agent_id: None,
114            comm_bus,
115            comm_policy,
116            // RuntimeBridge today does not own a ReasoningPolicyGate; the
117            // reasoning builtin will fall back to DefaultPolicyGate::new()
118            // (production-default). Callers embedding the runtime should
119            // install their concrete gate directly via the SDK.
120            reasoning_policy_gate: None,
121        }
122    }
123
124    /// Initialize the runtime bridge components asynchronously.
125    ///
126    /// Sets up the lifecycle controller, context manager, and communication bus.
127    /// After this returns, `reasoning_context()` produces a context with a live
128    /// bus and policy gate, so DSL builtins like `ask` and `send_to` will route
129    /// messages through the audited communication path.
130    pub async fn initialize(&self) -> Result<(), String> {
131        // Initialize lifecycle controller
132        let lifecycle_config = LifecycleConfig::default();
133        let lifecycle_controller = Arc::new(
134            DefaultLifecycleController::new(lifecycle_config)
135                .await
136                .map_err(|e| format!("Failed to create lifecycle controller: {}", e))?,
137        );
138
139        // Initialize context manager
140        let context_config = ContextManagerConfig::default();
141        let context_manager = Arc::new(
142            StandardContextManager::new(context_config, "runtime_bridge")
143                .await
144                .map_err(|e| format!("Failed to create context manager: {}", e))?,
145        );
146
147        // Initialize the context manager
148        context_manager
149            .initialize()
150            .await
151            .map_err(|e| format!("Failed to initialize context manager: {}", e))?;
152
153        // Initialize the communication bus
154        let bus_config = CommunicationConfig::default();
155        let bus = Arc::new(
156            DefaultCommunicationBus::new(bus_config)
157                .await
158                .map_err(|e| format!("Failed to create communication bus: {}", e))?,
159        ) as Arc<dyn CommunicationBus + Send + Sync>;
160
161        // Store the initialized components
162        *self.lifecycle_controller.lock().unwrap() = Some(lifecycle_controller);
163        *self.context_manager.lock().unwrap() = Some(context_manager);
164        *self.comm_bus.lock().unwrap() = Some(bus);
165
166        Ok(())
167    }
168
169    pub async fn initialize_agent(&self, config: AgentConfig) -> Result<AgentId, String> {
170        let controller = {
171            let controller_guard = self.lifecycle_controller.lock().unwrap();
172            controller_guard.clone()
173        };
174
175        if let Some(controller) = controller {
176            controller
177                .initialize_agent(config)
178                .await
179                .map_err(|e| e.to_string())
180        } else {
181            Err("Lifecycle controller not initialized".to_string())
182        }
183    }
184
185    /// Checks if a given capability is allowed for an agent.
186    pub async fn check_capability(
187        &self,
188        agent_id: &str,
189        capability: &Capability,
190    ) -> Result<PolicyDecision, String> {
191        // Clone the engine to avoid holding the lock across the await
192        let engine = {
193            let engine_guard = self.policy_engine.lock().unwrap();
194            engine_guard.clone()
195        };
196        engine
197            .check_capability(agent_id, capability)
198            .await
199            .map_err(|e| e.to_string())
200    }
201
202    /// Register an event handler for an agent (stub implementation)
203    pub async fn register_event_handler(
204        &self,
205        agent_id: &str,
206        event_name: &str,
207        _event_type: &str,
208    ) -> Result<(), String> {
209        tracing::info!(
210            "Registered event handler '{}' for agent {}",
211            event_name,
212            agent_id
213        );
214        Ok(())
215    }
216
217    /// Emit an event from an agent (stub implementation)
218    pub async fn emit_event(
219        &self,
220        agent_id: &str,
221        event_name: &str,
222        _data: &serde_json::Value,
223    ) -> Result<(), String> {
224        tracing::info!("Agent {} emitted event: {}", agent_id, event_name);
225        Ok(())
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[tokio::test]
234    async fn test_reasoning_context_before_init_has_no_bus() {
235        let bridge = RuntimeBridge::new();
236        let ctx = bridge.reasoning_context();
237        // Before initialize, bus is None but the policy gate is always Some
238        // (deny-by-default by construction).
239        assert!(ctx.comm_bus.is_none());
240        assert!(ctx.comm_policy.is_some());
241    }
242
243    #[tokio::test]
244    async fn test_new_default_policy_denies() {
245        use symbi_runtime::types::MessageType;
246        let bridge = RuntimeBridge::new();
247        let ctx = bridge.reasoning_context();
248        let policy = ctx.comm_policy.expect("policy present");
249        let recipient = AgentId::new();
250        let request = symbi_runtime::communication::policy_gate::CommunicationRequest {
251            sender: AgentId::new(),
252            recipient,
253            message_type: MessageType::Direct(recipient),
254            topic: None,
255        };
256        assert!(
257            policy.evaluate(&request).is_err(),
258            "default policy must be deny-by-default"
259        );
260    }
261
262    #[tokio::test]
263    async fn test_permissive_for_dev_allows() {
264        use symbi_runtime::types::MessageType;
265        let bridge = RuntimeBridge::new_permissive_for_dev();
266        let ctx = bridge.reasoning_context();
267        let policy = ctx.comm_policy.expect("policy present");
268        let recipient = AgentId::new();
269        let request = symbi_runtime::communication::policy_gate::CommunicationRequest {
270            sender: AgentId::new(),
271            recipient,
272            message_type: MessageType::Direct(recipient),
273            topic: None,
274        };
275        assert!(policy.evaluate(&request).is_ok());
276    }
277
278    #[tokio::test]
279    async fn test_reasoning_context_after_init_has_bus() {
280        let bridge = RuntimeBridge::new_permissive_for_dev();
281        bridge
282            .initialize()
283            .await
284            .expect("initialize should succeed");
285        let ctx = bridge.reasoning_context();
286        assert!(
287            ctx.comm_bus.is_some(),
288            "Communication bus should be populated after initialize()"
289        );
290        assert!(ctx.comm_policy.is_some(), "Policy gate is always present");
291    }
292
293    #[tokio::test]
294    async fn test_comm_bus_accessor() {
295        let bridge = RuntimeBridge::new_permissive_for_dev();
296        assert!(bridge.comm_bus().is_none());
297        bridge
298            .initialize()
299            .await
300            .expect("initialize should succeed");
301        assert!(bridge.comm_bus().is_some());
302    }
303
304    #[tokio::test]
305    async fn test_set_comm_policy_replaces_default() {
306        let bridge = RuntimeBridge::new();
307        let new_policy = Arc::new(CommunicationPolicyGate::permissive());
308        bridge.set_comm_policy(Arc::clone(&new_policy));
309        let ctx = bridge.reasoning_context();
310        let retrieved = ctx.comm_policy.expect("policy should be set");
311        assert!(Arc::ptr_eq(&retrieved, &new_policy));
312    }
313}