use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use zeph_llm::any::AnyProvider;
use zeph_memory::semantic::SemanticMemory;
use zeph_skills::matcher::SkillMatcherBackend;
use zeph_skills::registry::SkillRegistry;
use zeph_tools::ErasedToolExecutor;
#[derive(Clone)]
pub(crate) struct ServeAgentDeps {
pub(crate) provider: AnyProvider,
pub(crate) embedding_provider: AnyProvider,
pub(crate) registry: Arc<RwLock<SkillRegistry>>,
pub(crate) matcher: Option<SkillMatcherBackend>,
pub(crate) max_active_skills: usize,
pub(crate) skill_disambiguation_threshold: f32,
pub(crate) skill_two_stage_matching: bool,
pub(crate) skill_confusability_threshold: f32,
pub(crate) skill_generation_provider: String,
pub(crate) skill_disambiguate_provider: String,
pub(crate) semantic_scan: bool,
pub(crate) semantic_scan_provider: String,
pub(crate) tool_executor: Arc<dyn ErasedToolExecutor>,
pub(crate) memory: Arc<SemanticMemory>,
pub(crate) history_limit: u32,
pub(crate) recall_limit: usize,
pub(crate) summarization_threshold: usize,
pub(crate) session_config: zeph_core::AgentSessionConfig,
pub(crate) session_persistence_config: zeph_config::SessionConfig,
pub(crate) resume_condenser: Arc<zeph_session::LlmCondenser>,
pub(crate) resume_token_counter: Arc<zeph_agent_context::memory_backend::TokenCounterAdapter>,
pub(crate) provider_pool: Vec<zeph_core::config::ProviderEntry>,
pub(crate) provider_config_snapshot: zeph_core::ProviderConfigSnapshot,
}
pub(crate) async fn build_serve_deps(
config_path: Option<&std::path::Path>,
vault_backend: Option<&str>,
vault_key: Option<&std::path::Path>,
vault_path: Option<&std::path::Path>,
) -> anyhow::Result<(ServeAgentDeps, Option<String>)> {
use crate::bootstrap::AppBuilder;
let app = AppBuilder::new(config_path, vault_backend, vault_key, vault_path).await?;
let auth_token = resolve_auth_token(&app).await;
let cancel = tokio_util::sync::CancellationToken::new();
let supervisor = zeph_common::task_supervisor::TaskSupervisor::new(cancel);
let core = crate::acp::build_shared_core(&app, &supervisor).await?;
let deps = assemble_serve_deps(&app, &core, &supervisor).await?;
Ok((deps, auth_token))
}
pub(crate) async fn assemble_serve_deps(
app: &crate::bootstrap::AppBuilder,
core: &crate::acp::SharedCore,
supervisor: &zeph_common::task_supervisor::TaskSupervisor,
) -> anyhow::Result<ServeAgentDeps> {
let config = app.config();
let tool_executor = build_tool_executor(config, supervisor).await?;
let session_config = zeph_core::AgentSessionConfig::from_config(config, core.budget_tokens);
let max_active_skills = config.skills.max_active_skills.get();
let history_limit = config.memory.history_limit;
let recall_limit = config.memory.semantic.recall_limit;
let summarization_threshold = config.memory.summarization_threshold;
let session_persistence_config = config.session.clone();
let (resume_condenser, resume_token_counter) =
zeph_core::provider_factory::build_resume_condenser(config, &core.provider);
let provider_config_snapshot = zeph_core::ProviderConfigSnapshot {
claude_api_key: config
.secrets
.claude_api_key
.as_ref()
.map(|s| s.expose().to_owned()),
openai_api_key: config
.secrets
.openai_api_key
.as_ref()
.map(|s| s.expose().to_owned()),
gemini_api_key: config
.secrets
.gemini_api_key
.as_ref()
.map(|s| s.expose().to_owned()),
compatible_api_keys: config
.secrets
.compatible_api_keys
.iter()
.map(|(k, v)| (k.clone(), v.expose().to_owned()))
.collect(),
llm_request_timeout_secs: config.timeouts.llm_request_timeout_secs,
embedding_model: config.llm.embedding_model.clone(),
gonka_private_key: config
.secrets
.gonka_private_key
.as_ref()
.map(|s| zeroize::Zeroizing::new(s.expose().to_owned())),
gonka_address: config
.secrets
.gonka_address
.as_ref()
.map(|s| s.expose().to_owned()),
cocoon_access_hash: config
.secrets
.cocoon_access_hash
.as_ref()
.map(|s| s.expose().to_owned()),
};
Ok(ServeAgentDeps {
provider: core.provider.clone(),
embedding_provider: core.embedding_provider.clone(),
registry: Arc::clone(&core.registry),
matcher: core.matcher.clone(),
max_active_skills,
skill_disambiguation_threshold: config.skills.disambiguation_threshold,
skill_two_stage_matching: config.skills.two_stage_matching,
skill_confusability_threshold: config.skills.confusability_threshold,
skill_generation_provider: config.skills.generation_provider.as_str().to_owned(),
skill_disambiguate_provider: config.skills.disambiguate_provider.as_str().to_owned(),
semantic_scan: config.skills.semantic_scan,
semantic_scan_provider: config.skills.semantic_scan_provider.as_str().to_owned(),
tool_executor,
memory: Arc::clone(&core.memory),
history_limit,
recall_limit,
summarization_threshold,
session_config,
session_persistence_config,
resume_condenser: Arc::new(resume_condenser),
resume_token_counter,
provider_pool: config.llm.providers.clone(),
provider_config_snapshot,
})
}
pub(crate) async fn resolve_auth_token(app: &crate::bootstrap::AppBuilder) -> Option<String> {
let key = &app.config().serve.auth_token_vault_key;
if key.is_empty() {
return None;
}
app.vault().get_secret(key).await.unwrap_or_else(|e| {
tracing::warn!(
error = %e,
key = %key,
"serve-sessions: failed to resolve auth token from vault"
);
None
})
}
async fn build_tool_executor(
config: &zeph_core::config::Config,
supervisor: &zeph_common::task_supervisor::TaskSupervisor,
) -> anyhow::Result<Arc<dyn ErasedToolExecutor>> {
let filter_registry = if config.tools.filters.enabled {
zeph_tools::OutputFilterRegistry::default_filters(&config.tools.filters)
} else {
zeph_tools::OutputFilterRegistry::new(false)
};
let mut shell_executor = zeph_tools::ShellExecutor::new(&config.tools.shell)
.with_permissions(zeph_tools::build_permission_policy(
&config.tools,
config.security.autonomy_level,
))
.with_output_filters(filter_registry)
.with_task_supervisor(supervisor.clone());
if config.tools.sandbox.enabled {
let denied_present = !config.tools.sandbox.denied_domains.is_empty();
match zeph_tools::sandbox::build_sandbox_with_policy(
config.tools.sandbox.strict,
config.tools.sandbox.fail_if_unavailable,
denied_present,
) {
Ok(backend) => {
let name = backend.name();
let policy = crate::agent_setup::sandbox_policy_from_config(&config.tools.sandbox);
shell_executor = shell_executor.with_sandbox(Arc::from(backend), policy);
tracing::info!(backend = name, "OS sandbox enabled (serve-sessions)");
}
Err(e) if config.tools.sandbox.strict || config.tools.sandbox.fail_if_unavailable => {
anyhow::bail!("sandbox initialization failed: {e}");
}
Err(e) => {
tracing::warn!("OS sandbox unavailable, running without isolation: {e}");
}
}
}
let mut scrape_executor = zeph_tools::WebScrapeExecutor::new(&config.tools.scrape)
.with_egress_config(config.tools.egress.clone());
if config.tools.audit.enabled
&& let Ok(logger) = zeph_tools::AuditLogger::from_config(&config.tools.audit, false).await
{
let logger = Arc::new(logger);
shell_executor = shell_executor.with_audit(Arc::clone(&logger));
scrape_executor = scrape_executor.with_audit(logger);
}
let file_executor = zeph_tools::FileExecutor::new(
config
.tools
.shell
.allowed_paths
.iter()
.map(PathBuf::from)
.collect(),
);
let cwd_executor = zeph_tools::SetCwdExecutor;
Ok(Arc::new(zeph_tools::CompositeExecutor::new(
file_executor,
zeph_tools::CompositeExecutor::new(
shell_executor,
zeph_tools::CompositeExecutor::new(scrape_executor, cwd_executor),
),
)))
}