1use super::init::{
2 build_connect_manager, build_provider_handles, build_schedule_manager, build_spawn_scheduler,
3 init_mcp_manager, init_metrics_service, init_schedule_store, init_skill_manager, init_storage,
4 load_permission_checker, spawn_session_map_cleanup_task,
5};
6use super::tools::{build_base_tools, build_root_tools};
7use super::*;
8use crate::tools::OptionalSubagentModelResolver;
9use bamboo_agent_core::storage::Storage;
10
11impl AppState {
12 pub async fn new(bamboo_home_dir: PathBuf) -> Result<Self, AppError> {
54 bamboo_config::paths::init_bamboo_dir(bamboo_home_dir.clone());
57
58 let (config, config_facade) = match bamboo_config::ConfigFacade::open_or_migrate(
65 &bamboo_home_dir,
66 ) {
67 Ok(facade) => {
68 let facade = Arc::new(facade);
69 let config =
70 super::config_runtime::load_facade_effective_config(&facade, &bamboo_home_dir);
71 (config, Some(facade))
72 }
73 Err(error) => {
74 if bamboo_config::section_layout_is_active(&bamboo_home_dir).unwrap_or(false) {
75 return Err(AppError::InternalError(anyhow::anyhow!(
76 "modular configuration authority is unavailable: {error}"
77 )));
78 }
79 tracing::warn!(
80 error = %error,
81 "modular configuration facade is unavailable; retaining recovered legacy authority"
82 );
83 (
84 Config::from_data_dir_without_publish(Some(bamboo_home_dir.clone())),
85 None,
86 )
87 }
88 };
89 config.publish_env_vars();
90
91 if config.plugin_trust.enforcement_is_off() {
103 super::config_runtime::warn_plugin_trust_enforcement_off();
104 }
105
106 let provider_registry =
107 match bamboo_llm::ProviderRegistry::from_config(&config, bamboo_home_dir.clone()).await
108 {
109 Ok(registry) => Arc::new(registry),
110 Err(e) => {
111 tracing::error!("Failed to create provider registry: {}", e);
112 Arc::new(
113 bamboo_llm::ProviderRegistry::from_config(
114 &Config::default(),
115 bamboo_home_dir.clone(),
116 )
117 .await
118 .expect("Cannot create even an empty provider registry"),
119 )
120 }
121 };
122
123 let provider = provider_registry.get_default().unwrap_or_else(|| {
124 let default_provider_name = provider_registry.default_provider_name();
125 let message = if config.has_provider_instances() {
126 format!(
127 "Default provider instance '{}' is not available or failed to initialize",
128 default_provider_name
129 )
130 } else {
131 format!(
132 "Provider '{}' is not available or failed to initialize",
133 config.provider
134 )
135 };
136 Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
137 });
138
139 Self::new_with_provider_and_facade(bamboo_home_dir, config, provider, config_facade).await
140 }
141
142 pub async fn new_with_provider(
157 bamboo_home_dir: PathBuf,
158 config: Config,
159 provider: Arc<dyn LLMProvider>,
160 ) -> Result<Self, AppError> {
161 Self::new_with_provider_and_facade(bamboo_home_dir, config, provider, None).await
162 }
163
164 async fn new_with_provider_and_facade(
165 bamboo_home_dir: PathBuf,
166 config: Config,
167 provider: Arc<dyn LLMProvider>,
168 config_facade: Option<Arc<bamboo_config::ConfigFacade>>,
169 ) -> Result<Self, AppError> {
170 let data_dir = bamboo_home_dir.clone();
172 let (session_store, storage) = init_storage(&data_dir).await?;
173 let persistence = Arc::new(LockedSessionStore::new(storage.clone()));
174
175 let sessions: bamboo_engine::SessionCache = Arc::new(dashmap::DashMap::new());
177
178 let mut config = config;
185 let embedded_broker = maybe_embed_broker(&mut config, &data_dir).await;
186
187 let config = Arc::new(RwLock::new(config));
188
189 {
198 let config_for_workspace = config.clone();
199 let last_known: Arc<std::sync::Mutex<Option<PathBuf>>> =
200 Arc::new(std::sync::Mutex::new(None));
201 bamboo_agent_core::workspace_state::set_default_workspace_provider(Box::new(
202 move || match config_for_workspace.try_read() {
203 Ok(cfg) => {
204 let path = cfg.get_default_work_area_path();
205 if let Ok(mut cache) = last_known.lock() {
206 *cache = path.clone();
207 }
208 path
209 }
210 Err(_) => last_known.lock().ok().and_then(|c| c.clone()),
211 },
212 ));
213 }
214
215 bamboo_agent_core::workspace_state::set_workspace_root_provider(Box::new(|| {
226 bamboo_agent_core::workspace_state::WorkspaceRootConfig {
227 root: bamboo_config::paths::resolve_workspace_root(),
228 confine: bamboo_config::paths::workspace_confinement_enforced(),
229 }
230 }));
231
232 let (permission_checker, permission_section) =
233 load_permission_checker(&bamboo_home_dir).await?;
234 let permission_io_lock = Arc::new(tokio::sync::Mutex::new(()));
235 let notification_service = Arc::new(bamboo_notification::NotificationService::new(
236 bamboo_home_dir.join("notification_preferences.json"),
237 ));
238 let session_watchers = super::watchers::SessionWatchers::new();
239 let (mcp_manager, _legacy_mcp_bootstrap) =
240 init_mcp_manager(config.clone(), &bamboo_home_dir);
241 let skill_manager = init_skill_manager(&data_dir).await;
242 let metrics_service = init_metrics_service(&data_dir).await?;
243
244 let startup_sessions = {
245 let entries = session_store.list_index_entries().await;
246 let mut sessions = Vec::new();
247 for entry in entries {
248 if let Some(session) = session_store
249 .load_session(&entry.id)
250 .await
251 .map_err(AppError::StorageError)?
252 {
253 sessions.push(session);
254 }
255 }
256 sessions
257 };
258 metrics_service
259 .reconcile_startup_sessions(startup_sessions, &[])
260 .await
261 .map_err(|error| {
262 AppError::InternalError(anyhow::anyhow!(
263 "Failed to reconcile stale metrics state on startup: {error}"
264 ))
265 })?;
266
267 let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
268 Arc::new(RwLock::new(HashMap::new()));
269 let process_registry = Arc::new(ProcessRegistry::new());
274 let (provider_lock, provider_handle) = build_provider_handles(provider);
275
276 let config_snapshot = config.read().await;
278 let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
279 &config_snapshot,
280 bamboo_home_dir.clone(),
281 )
282 .await
283 {
284 Ok(registry) => Arc::new(registry),
285 Err(e) => {
286 tracing::error!("Failed to create provider registry: {}", e);
287 Arc::new(
288 bamboo_llm::ProviderRegistry::from_config(
289 &Config::default(),
290 bamboo_home_dir.clone(),
291 )
292 .await
293 .expect("Cannot create even an empty provider registry"),
294 )
295 }
296 };
297 drop(config_snapshot);
298
299 let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
300 provider_registry.clone(),
301 ));
302 let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
303 provider_registry.clone(),
304 ));
305
306 let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
311 Arc::new(RwLock::new(HashMap::new()));
312
313 let notification_relay_deps = crate::app_state::session_events::NotificationRelayDeps {
319 notification_service: notification_service.clone(),
320 session_event_senders: session_event_senders.clone(),
321 session_watchers: session_watchers.clone(),
322 config: config.clone(),
323 };
324
325 let ledger_schedule_bridge =
328 Arc::new(crate::schedule_app::LateBoundLedgerBridge::default());
329
330 let session_repo = bamboo_engine::SessionRepository::new(
334 sessions.clone(),
335 storage.clone(),
336 persistence.clone(),
337 );
338
339 let base_tools = build_base_tools(
340 config.clone(),
341 permission_checker.clone(),
342 mcp_manager.clone(),
343 skill_manager.clone(),
344 session_repo.clone(),
345 bamboo_home_dir.clone(),
346 notification_service.clone(),
347 session_event_senders.clone(),
348 session_watchers.clone(),
349 ledger_schedule_bridge.clone(),
350 );
351
352 let workflow_runs = crate::workflow::WorkflowRunAccess::new(
356 &data_dir,
357 base_tools.clone(),
358 skill_manager.clone(),
359 session_repo.clone(),
360 )
361 .await
362 .map_err(|error| AppError::InternalError(anyhow::anyhow!(error)))?;
363
364 spawn_session_map_cleanup_task(agent_runners.clone(), session_event_senders.clone(), None);
368
369 let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
372 .map_err(|e| {
373 AppError::InternalError(anyhow::anyhow!(
374 "failed to initialize account change-feed journal: {e}"
375 ))
376 })?;
377 {
380 let mut workflow_events = skill_manager.store().subscribe_workflow_catalog();
381 let account_sink = account_sink.clone();
382 tokio::spawn(async move {
383 loop {
384 let event = match workflow_events.recv().await {
385 Ok(event) => event,
386 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
387 tracing::warn!("Workflow catalog event bridge lagged by {skipped}");
388 continue;
389 }
390 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
391 };
392 let event = match event.kind {
393 bamboo_skills::WorkflowCatalogEventKind::Changed => {
394 AgentEvent::WorkflowChanged {
395 workflow_id: event.workflow_id,
396 revision: event.revision,
397 scope: event.scope,
398 }
399 }
400 bamboo_skills::WorkflowCatalogEventKind::Invalid => {
401 AgentEvent::WorkflowInvalid {
402 workflow_id: event.workflow_id,
403 revision: event.revision,
404 scope: event.scope,
405 }
406 }
407 bamboo_skills::WorkflowCatalogEventKind::Recovered => {
408 AgentEvent::WorkflowRecovered {
409 workflow_id: event.workflow_id,
410 revision: event.revision,
411 scope: event.scope,
412 }
413 }
414 };
415 account_sink.record(None, &event);
416 }
417 });
418 }
419 let (approval_registry, restart_approval_events) =
420 bamboo_engine::external_agents::live::initialize_durable_approvals(
421 data_dir.join("approvals/child-approvals-v1.json"),
422 )
423 .map_err(|error| {
424 AppError::InternalError(anyhow::anyhow!(
425 "failed to initialize durable child approvals: {error}"
426 ))
427 })?;
428 for event in restart_approval_events {
429 account_sink.record(event.session_id(), &event);
430 }
431
432 let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
435
436 let agent = Arc::new(
441 bamboo_engine::Agent::builder()
442 .storage(storage.clone())
443 .persistence(Arc::new(session_repo.clone()))
444 .attachment_reader(session_store.clone())
445 .skill_manager(skill_manager.clone())
446 .metrics_collector(metrics_service.collector())
447 .config(config.clone())
448 .provider(provider_handle.clone())
449 .default_tools(base_tools.clone())
450 .build()
451 .expect("agent runtime should be fully configured"),
452 );
453
454 let child_completion_coordinator =
455 Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
456 storage.clone(),
457 persistence.clone(),
458 sessions.clone(),
459 agent_runners.clone(),
460 session_event_senders.clone(),
461 agent.clone(),
462 config.clone(),
463 provider_registry.clone(),
464 provider_router.clone(),
465 data_dir.clone(),
466 Some(account_sink.inbox()),
467 ));
468
469 let config_snapshot = config.read().await.clone();
471
472 let mcp_proxy_shutdown = tokio_util::sync::CancellationToken::new();
479 if let Some(broker) = config_snapshot.subagents().broker.clone() {
480 if !broker.endpoint.trim().is_empty() {
481 let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
482 std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
483 mcp_manager.clone(),
484 mcp_manager.tool_index(),
485 ));
486 let shutdown = mcp_proxy_shutdown.clone();
487
488 let role_entries: Vec<(String, Vec<String>)> = config_snapshot
497 .subagents()
498 .mcp_role_allowlist
499 .iter()
500 .map(|e| (e.role.clone(), e.tools.clone()))
501 .collect();
502 if role_entries.is_empty() {
503 tracing::info!(
509 "mcp proxy: no subagents.mcp_role_allowlist configured — every worker \
510 role sees/can call the full host-bound MCP tool set (opt in a role \
511 policy in config.json to scope tools per role; see issue #54)"
512 );
513 }
514 let known_tools: std::collections::HashSet<String> = backend
522 .list_tools()
523 .into_iter()
524 .map(|t| t.function.name)
525 .collect();
526 if !role_entries.is_empty() && known_tools.is_empty() {
527 tracing::warn!(
528 "mcp role allowlist: the orchestrator's MCP tool set was empty at \
529 policy-load time (servers may still be connecting in the background) — \
530 skipped tool-name typo validation for subagents.mcp_role_allowlist"
531 );
532 }
533 let allowlist = std::sync::Arc::new(bamboo_broker::RoleToolAllowlist::from_config(
534 role_entries,
535 &known_tools,
536 ));
537 tokio::spawn(async move {
538 let me = bamboo_broker::AgentRef {
539 session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
540 role: Some("orchestrator".into()),
541 };
542 bamboo_broker::serve_mcp_proxy_supervised(
543 &broker.endpoint,
544 me,
545 &broker.token,
546 backend,
547 allowlist,
548 shutdown,
549 )
550 .await;
551 });
552 }
553 }
554 let parent_approval_reviewer = Arc::new(
555 crate::app_state::parent_approval_reviewer::ParentAgentApprovalReviewer::new(
556 session_repo.clone(),
557 provider_router.clone(),
558 ),
559 );
560 let codex_run_tokens = Arc::new(crate::codex_run_tokens::CodexRunTokenRegistry::default());
561 let external_runner =
562 bamboo_engine::external_agents::runtime::build_external_child_runner_with_codex_tokens(
563 &config_snapshot,
564 Some(approval_registry.clone()),
565 Some(parent_approval_reviewer),
566 permission_checker.permission_config(),
567 Some(codex_run_tokens.clone()),
568 );
569 let spawn_scheduler = build_spawn_scheduler(
570 agent.clone(),
571 child_tools,
572 sessions.clone(),
573 agent_runners.clone(),
574 session_event_senders.clone(),
575 external_runner,
576 Some(provider_router.clone()),
577 Some(child_completion_coordinator.clone()),
578 Some(data_dir.clone()),
579 Some(account_sink.inbox()),
580 );
581
582 let tools_with_task = base_tools.clone();
583
584 let schedule_store = init_schedule_store(&data_dir).await?;
585
586 ledger_schedule_bridge
588 .bind(Arc::new(crate::schedule_app::ScheduleLedgerBridge::new(
589 schedule_store.clone(),
590 )))
591 .await;
592
593 let schedule_manager = build_schedule_manager(
594 schedule_store.clone(),
595 agent.clone(),
596 tools_with_task.clone(),
597 permission_checker.permission_config(),
598 sessions.clone(),
599 agent_runners.clone(),
600 session_event_senders.clone(),
601 persistence.clone(),
602 config.clone(),
603 provider_registry.clone(),
604 Some(data_dir.clone()),
605 Some(account_sink.inbox()),
606 notification_relay_deps.clone(),
607 );
608
609 bamboo_engine::auto_dream::spawn_auto_dream_task(
610 bamboo_engine::auto_dream::AutoDreamContext {
611 session_store: session_store.clone(),
612 storage: storage.clone(),
613 provider: provider_handle.clone(),
614 config: config.clone(),
615 provider_registry: provider_registry.clone(),
616 },
617 );
618
619 bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
623 session_store: session_store.clone(),
624 storage: storage.clone(),
625 provider: provider_handle.clone(),
626 config: config.clone(),
627 provider_registry: provider_registry.clone(),
628 });
629
630 bamboo_engine::ledger_gardener::spawn_ledger_gardener_task(
634 bamboo_engine::ledger_gardener::LedgerGardenerContext {
635 dream: bamboo_engine::auto_dream::AutoDreamContext {
636 session_store: session_store.clone(),
637 storage: storage.clone(),
638 provider: provider_handle.clone(),
639 config: config.clone(),
640 provider_registry: provider_registry.clone(),
641 },
642 schedule_bridge: Some(ledger_schedule_bridge.clone()),
643 },
644 );
645
646 let config_for_resolver = config.clone();
647 let subagent_model_resolver: OptionalSubagentModelResolver = {
648 let registry = provider_registry.clone();
649 Some(Arc::new(
650 move |subagent_type: String| -> futures::future::BoxFuture<
651 'static,
652 Option<bamboo_domain::ProviderModelRef>,
653 > {
654 let config_for_resolver = config_for_resolver.clone();
655 let registry = registry.clone();
656 Box::pin(async move {
657 let config_snap = config_for_resolver.read().await.clone();
658 bamboo_engine::model_config_helper::resolve_subagent_model_ref(
659 &config_snap,
660 &config_snap.provider,
661 ®istry,
662 &subagent_type,
663 )
664 })
665 },
666 ))
667 };
668
669 let config_io_lock = Arc::new(tokio::sync::Mutex::new(()));
673 let fabric_registry: crate::tools::DeployedRegistry =
674 Arc::new(tokio::sync::Mutex::new(HashMap::new()));
675 let fabric_bamboo_bin =
676 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("bamboo"));
677 let fabric_deployer = Arc::new(bamboo_server_tools::FabricDeployer::new(
678 config.clone(),
679 config_io_lock.clone(),
680 bamboo_home_dir.clone(),
681 fabric_registry,
682 fabric_bamboo_bin,
683 ));
684 let health_monitor = fabric_deployer
689 .clone()
690 .spawn_health_monitor()
691 .await
692 .map(HealthMonitor);
693
694 let tools = build_root_tools(
695 tools_with_task.clone(),
696 schedule_store.clone(),
697 schedule_manager.clone(),
698 session_store.clone(),
699 storage.clone(),
700 persistence.clone(),
701 spawn_scheduler.clone(),
702 sessions.clone(),
703 agent_runners.clone(),
704 session_event_senders.clone(),
705 subagent_model_resolver,
706 config.clone(),
707 provider_registry.clone(),
708 config_snapshot.subagents().broker.clone(),
709 fabric_deployer.clone(),
710 notification_relay_deps.clone(),
711 );
712 let workflow_run_tool =
713 Arc::new(crate::workflow::WorkflowRunTool::new(workflow_runs.clone()));
714 let tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = Arc::new(
715 crate::tools::OverlayToolExecutor::new(tools, workflow_run_tool),
716 );
717
718 child_completion_coordinator
719 .set_root_tools(tools.clone())
720 .await;
721
722 let tool_factory =
723 crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
724
725 let session_repo = bamboo_engine::SessionRepository::new(
726 sessions.clone(),
727 storage.clone(),
728 persistence.clone(),
729 );
730
731 let connect_manager = Arc::new(
736 build_connect_manager(
737 agent.clone(),
738 tool_factory.get(crate::tools::ToolSurface::Root),
739 session_repo.clone(),
740 agent_runners.clone(),
741 session_event_senders.clone(),
742 Some(account_sink.inbox()),
743 Some(data_dir.clone()),
744 config.clone(),
745 provider_registry.clone(),
746 permission_checker.clone(),
747 )
748 .await,
749 );
750
751 let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
758 session_store: session_store.clone(),
759 storage: storage.clone(),
760 persistence: persistence.clone(),
761 scheduler: spawn_scheduler.clone(),
762 sessions_cache: sessions.clone(),
763 agent_runners: agent_runners.clone(),
764 session_event_senders: session_event_senders.clone(),
765 subagent_model_resolver: None,
766 config: config.clone(),
767 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
768 notification_relay: Some(notification_relay_deps.clone()),
769 });
770 let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
771 child_completion_coordinator
774 .set_guardian_spawner(guardian_spawner.clone())
775 .await;
776
777 let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
781 child_completion_coordinator.clone();
782
783 child_completion_coordinator.spawn_child_wait_watchdog();
789
790 reconcile_fabric_on_boot(&config, &bamboo_home_dir).await;
795
796 let service_manager = Arc::new(crate::service_manager::ServiceManager::new());
802 let boot_reconcile_services_handle = {
814 let service_manager = service_manager.clone();
815 let app_data_dir = bamboo_home_dir.clone();
816 tokio::spawn(async move {
817 crate::plugin_installer::boot_reconcile_services(&app_data_dir, &service_manager)
818 .await;
819 })
820 };
821
822 let (config_watcher, config_live_health, mcp_config_live_health) =
823 super::config_runtime::ConfigWatcherRuntime::start(
824 bamboo_home_dir.clone(),
825 config.clone(),
826 config_facade.clone(),
827 config_io_lock.clone(),
828 provider_registry.clone(),
829 provider_lock.clone(),
830 mcp_manager.clone(),
831 account_sink.clone(),
832 );
833 let credential_store = Arc::new(bamboo_config::CredentialStore::open(&bamboo_home_dir));
834
835 Ok(Self {
836 app_data_dir: bamboo_home_dir,
837 config,
838 config_facade,
839 config_io_lock,
840 config_live_health,
841 mcp_config_live_health,
842 config_watcher,
843 credential_store,
844 fabric_deployer,
845 embedded_broker,
846 health_monitor,
847 provider: provider_lock,
848 provider_handle,
849 sessions,
850 storage,
851 session_store,
852 session_repo,
853 persistence,
854 spawn_scheduler,
855 child_completion_coordinator,
856 guardian_spawner,
857 bash_resume_hook,
858 schedule_store,
859 schedule_manager,
860 connect_manager,
861 tool_factory,
862 permission_checker,
863 permission_section,
864 permission_io_lock,
865 approval_registry,
866 notification_service,
867 session_watchers,
868 cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
869 mcp_proxy_shutdown,
870 skill_manager,
871 workflow_runs,
872 mcp_manager,
873 service_manager,
874 boot_reconcile_services_handle: tokio::sync::Mutex::new(Some(
875 boot_reconcile_services_handle,
876 )),
877 metrics_service,
878 agent_runners,
879 execute_startups: Arc::new(std::sync::Mutex::new(HashMap::new())),
880 session_event_senders,
881 account_sink,
882 process_registry,
883 metrics_bus: None, agent,
885 provider_registry,
886 provider_router,
887 model_catalog,
888 title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
889 pairing_codes: Arc::new(dashmap::DashMap::new()),
890 pairing_code_guard: Arc::new(crate::handlers::settings::PairingCodeGuard::default()),
891 root_password_guard: Arc::new(crate::handlers::settings::RootPasswordGuard::default()),
892 codex_run_tokens,
893 })
895 }
896}
897
898pub struct EmbeddedBroker {
901 task: tokio::task::JoinHandle<()>,
902 gc_task: tokio::task::JoinHandle<()>,
903}
904
905impl Drop for EmbeddedBroker {
906 fn drop(&mut self) {
907 self.task.abort();
908 self.gc_task.abort();
909 }
910}
911
912pub struct HealthMonitor(tokio::task::JoinHandle<()>);
916
917impl Drop for HealthMonitor {
918 fn drop(&mut self) {
919 self.0.abort();
920 }
921}
922
923async fn maybe_embed_broker(
935 config: &mut bamboo_llm::Config,
936 data_dir: &std::path::Path,
937) -> Option<EmbeddedBroker> {
938 if let Some(external) = load_external_broker(data_dir) {
943 let endpoint = external.endpoint.trim().to_string();
944 if broker_endpoint_reachable(&endpoint).await {
945 tracing::info!(%endpoint, "using external broker from broker.json");
946 config.subagents_mut().broker = Some(external);
947 return None;
948 }
949 tracing::warn!(
950 %endpoint,
951 "broker.json endpoint is unreachable — embedding a fresh in-process broker instead"
952 );
953 }
954
955 let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
956 Ok(l) => l,
957 Err(e) => {
958 tracing::warn!("embedded broker: bind failed, sub-agent dispatch disabled: {e}");
959 return None;
960 }
961 };
962 let port = match listener.local_addr() {
963 Ok(a) => a.port(),
964 Err(e) => {
965 tracing::warn!("embedded broker: local_addr failed: {e}");
966 return None;
967 }
968 };
969 let token = uuid::Uuid::new_v4().simple().to_string();
970 let root = data_dir.join("broker");
971 let core = Arc::new(bamboo_broker::BrokerCore::new(root));
972 let gc_task = core
975 .clone()
976 .spawn_mailbox_gc(std::time::Duration::from_secs(300));
977 let server = Arc::new(bamboo_broker::BrokerServer::new(core, token.clone()));
978
979 let task = tokio::spawn(async move {
980 if let Err(e) = server.serve(listener).await {
981 tracing::error!("embedded broker serve loop ended: {e}");
982 }
983 });
984
985 config.subagents_mut().broker = Some(bamboo_config::BrokerClientConfig {
988 endpoint: format!("ws://127.0.0.1:{port}"),
989 token,
990 token_encrypted: None,
991 credential_ref: None,
992 configured: false,
993 });
994 tracing::info!(port, "embedded mailbox bus (broker) started in-process");
995 Some(EmbeddedBroker { task, gc_task })
996}
997
998fn load_external_broker(data_dir: &std::path::Path) -> Option<bamboo_config::BrokerClientConfig> {
1007 let path = data_dir.join("broker.json");
1008 if let Err(error) = bamboo_config::migrate_external_broker_credentials(data_dir)
1009 .and_then(|_| bamboo_config::ensure_provider_mcp_migration_ready(data_dir))
1010 {
1011 tracing::warn!(error = %error, "external broker credential migration unavailable");
1012 return None;
1013 }
1014 let bytes = std::fs::read(&path).ok()?;
1015 match parse_external_broker_snapshot(&bytes, data_dir) {
1016 Ok(cfg) => Some(cfg),
1017 Err(error) => {
1018 tracing::warn!(?path, %error, "broker.json is unavailable");
1019 None
1020 }
1021 }
1022}
1023
1024fn parse_external_broker_snapshot(
1025 bytes: &[u8],
1026 data_dir: &std::path::Path,
1027) -> bamboo_config::ConfigStoreResult<bamboo_config::BrokerClientConfig> {
1028 let mut cfg: bamboo_config::BrokerClientConfig = serde_json::from_slice(bytes)?;
1029 if !cfg.token.trim().is_empty()
1030 || cfg
1031 .token_encrypted
1032 .as_deref()
1033 .is_some_and(|value| !value.trim().is_empty())
1034 {
1035 return Err(bamboo_config::ConfigStoreError::Validation(
1036 "legacy broker credential appeared after migration".to_string(),
1037 ));
1038 }
1039 if cfg.endpoint.trim().is_empty() {
1040 return Err(bamboo_config::ConfigStoreError::Validation(
1041 "broker endpoint is empty".to_string(),
1042 ));
1043 }
1044 cfg.hydrate_credential_from_store(data_dir)?;
1045 Ok(cfg)
1046}
1047
1048async fn broker_endpoint_reachable(endpoint: &str) -> bool {
1053 let host_port = endpoint
1054 .trim()
1055 .trim_start_matches("wss://")
1056 .trim_start_matches("ws://")
1057 .split('/')
1058 .next()
1059 .unwrap_or("");
1060 if host_port.is_empty() {
1061 return false;
1062 }
1063 matches!(
1064 tokio::time::timeout(
1065 std::time::Duration::from_millis(500),
1066 tokio::net::TcpStream::connect(host_port),
1067 )
1068 .await,
1069 Ok(Ok(_))
1070 )
1071}
1072
1073async fn reconcile_fabric_on_boot(
1078 config: &Arc<RwLock<bamboo_llm::Config>>,
1079 data_dir: &std::path::Path,
1080) {
1081 use bamboo_config::cluster_fabric::NodeStatus;
1082
1083 let snapshot = {
1084 let mut cfg = config.write().await;
1085 let mut changed = 0usize;
1086 for node in &mut cfg.cluster_fabric.nodes {
1087 if let Some(state) = node.state.as_mut() {
1088 if matches!(state.status, NodeStatus::Running | NodeStatus::Deploying) {
1089 state.status = NodeStatus::Unreachable;
1090 state.last_error =
1091 Some("orchestrator restarted; worker no longer tracked".to_string());
1092 changed += 1;
1093 }
1094 }
1095 }
1096 if changed == 0 {
1097 return;
1098 }
1099 tracing::info!(
1100 reconciled = changed,
1101 "cluster-fabric: marked stale Running nodes Unreachable on boot"
1102 );
1103 cfg.clone()
1104 };
1105
1106 if let Err(e) = snapshot.save_to_dir(data_dir.to_path_buf()) {
1107 tracing::warn!("cluster-fabric boot reconcile: failed to persist: {e}");
1108 }
1109}
1110
1111#[cfg(test)]
1112mod broker_embed_tests {
1113 use super::{broker_endpoint_reachable, load_external_broker, parse_external_broker_snapshot};
1114
1115 #[test]
1116 fn load_external_broker_reads_broker_json_not_config() {
1117 let _key = bamboo_config::encryption::set_test_encryption_key([0x57; 32]);
1118 let dir = tempfile::tempdir().unwrap();
1119 assert!(load_external_broker(dir.path()).is_none());
1121
1122 std::fs::write(
1124 dir.path().join("broker.json"),
1125 r#"{ "endpoint": "wss://broker.example:9600", "token": "t" }"#,
1126 )
1127 .unwrap();
1128 let got = load_external_broker(dir.path()).expect("parsed");
1129 assert_eq!(got.endpoint, "wss://broker.example:9600");
1130 assert_eq!(got.token, "t");
1131 assert_eq!(
1132 got.credential_ref.as_ref().unwrap().as_str(),
1133 "broker.external.bearer_token"
1134 );
1135 assert!(got.configured);
1136 let durable = std::fs::read_to_string(dir.path().join("broker.json")).unwrap();
1137 assert!(!durable.contains("\"token\""));
1138 assert!(!durable.contains("token_encrypted"));
1139 assert!(!durable.contains("\"t\""));
1140
1141 std::fs::write(dir.path().join("broker.json"), r#"{ "endpoint": " " }"#).unwrap();
1143 assert!(load_external_broker(dir.path()).is_none());
1144
1145 std::fs::write(dir.path().join("broker.json"), "not json").unwrap();
1147 assert!(load_external_broker(dir.path()).is_none());
1148 }
1149
1150 #[test]
1151 fn external_broker_reference_fails_closed_and_tracks_generic_cas_updates() {
1152 let _key = bamboo_config::encryption::set_test_encryption_key([0x58; 32]);
1153 let dir = tempfile::tempdir().unwrap();
1154 let reference =
1155 bamboo_config::credential_ref("broker", "external", "bearer_token").unwrap();
1156 std::fs::write(
1157 dir.path().join("broker.json"),
1158 serde_json::to_vec_pretty(&serde_json::json!({
1159 "endpoint": "wss://broker.example:9600",
1160 "credential_ref": reference,
1161 "configured": true,
1162 }))
1163 .unwrap(),
1164 )
1165 .unwrap();
1166
1167 assert!(load_external_broker(dir.path()).is_none());
1168 std::fs::write(
1169 dir.path().join("broker.json"),
1170 serde_json::to_vec_pretty(&serde_json::json!({
1171 "endpoint": "wss://broker.example:9600",
1172 "credential_ref": reference,
1173 "configured": false,
1174 }))
1175 .unwrap(),
1176 )
1177 .unwrap();
1178 assert!(load_external_broker(dir.path()).is_none());
1179 let store = bamboo_config::CredentialStore::open(dir.path());
1180 store
1181 .replace(
1182 reference.clone(),
1183 "replacement-token",
1184 bamboo_config::CredentialSource::User,
1185 0,
1186 )
1187 .unwrap();
1188 let hydrated = load_external_broker(dir.path()).unwrap();
1189 assert_eq!(hydrated.token, "replacement-token");
1190 store.clear(&reference, 1).unwrap();
1191 assert!(load_external_broker(dir.path()).is_none());
1192 }
1193
1194 #[test]
1195 fn broker_snapshot_with_late_legacy_secret_fails_closed() {
1196 let _key = bamboo_config::encryption::set_test_encryption_key([0x59; 32]);
1197 let dir = tempfile::tempdir().unwrap();
1198 let error = parse_external_broker_snapshot(
1199 br#"{
1200 "endpoint": "wss://broker.example:9600",
1201 "token": "late-legacy-token"
1202 }"#,
1203 dir.path(),
1204 )
1205 .unwrap_err();
1206 assert!(error.to_string().contains("appeared after migration"));
1207 }
1208
1209 #[tokio::test]
1210 async fn reachability_probe_distinguishes_live_from_dead() {
1211 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1214 let dead = l.local_addr().unwrap();
1215 drop(l);
1216 assert!(!broker_endpoint_reachable(&format!("ws://{dead}")).await);
1217
1218 assert!(!broker_endpoint_reachable("").await);
1220 assert!(!broker_endpoint_reachable("ws://").await);
1221
1222 let live = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1224 let addr = live.local_addr().unwrap();
1225 assert!(broker_endpoint_reachable(&format!("ws://{addr}/stream")).await);
1226 }
1227}