1use std::path::Path;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use harn_serve::{AcpProfileConfig, AcpRuntimeConfigurator, AcpServerConfig, AuthPolicy};
6use tokio::sync::mpsc;
7
8struct CliAcpRuntimeConfigurator;
9
10#[async_trait(?Send)]
11impl AcpRuntimeConfigurator for CliAcpRuntimeConfigurator {
12 async fn configure(
13 &self,
14 vm: &mut harn_vm::Vm,
15 source_path: Option<&Path>,
16 ) -> Result<(), String> {
17 #[cfg(feature = "hostlib")]
23 {
24 let _ = harn_hostlib::install_default(vm);
25 }
26
27 #[cfg(feature = "guard-neural")]
32 {
33 let base_dir = source_path
34 .and_then(std::path::Path::parent)
35 .unwrap_or_else(|| std::path::Path::new("."))
36 .to_path_buf();
37 harn_vm::security::set_injection_classifier_loader(Box::new(move |selector| {
38 harn_guard::load_classifier(&base_dir, selector)
39 }));
40 }
41
42 let Some(path) = source_path else {
43 return Ok(());
44 };
45
46 crate::compiler_context::enable_trusted_host_dispatch_for_source(vm, path)
50 .map_err(|error| format!("failed to enable trusted host dispatch: {error}"))?;
51
52 let extensions = crate::package::load_runtime_extensions(path);
53 crate::package::install_runtime_extensions(&extensions);
54 crate::package::install_manifest_triggers(vm, &extensions)
55 .await
56 .map_err(|error| format!("failed to install manifest triggers: {error}"))?;
57 crate::package::install_manifest_hooks(vm, &extensions)
58 .await
59 .map_err(|error| format!("failed to install manifest hooks: {error}"))?;
60 Ok(())
61 }
62}
63
64pub(crate) fn server_config(pipeline: Option<String>, auth_policy: AuthPolicy) -> AcpServerConfig {
65 let extensions = pipeline
66 .as_deref()
67 .map(Path::new)
68 .map(crate::package::load_runtime_extensions)
69 .unwrap_or_default();
70 AcpServerConfig::new(pipeline)
71 .with_auth_policy(auth_policy)
72 .with_runtime_configurator(Arc::new(CliAcpRuntimeConfigurator))
73 .with_llm_overrides(extensions.llm, extensions.capabilities)
74}
75
76pub(crate) fn ensure_acp_event_log(pipeline: Option<&str>) {
77 if harn_vm::event_log::active_event_log().is_none() {
78 let base_dir = pipeline
79 .map(Path::new)
80 .and_then(Path::parent)
81 .unwrap_or_else(|| Path::new("."));
82 if let Err(error) = harn_vm::event_log::install_default_for_base_dir(base_dir) {
83 eprintln!(
84 "[harn] ACP session replay disabled: failed to initialize EventLog for {}: {error}",
85 base_dir.display()
86 );
87 }
88 }
89}
90
91pub(crate) async fn run_acp_server(
92 pipeline: Option<&str>,
93 auth_policy: AuthPolicy,
94 trace: bool,
95 profile: AcpProfileConfig,
96) {
97 ensure_acp_event_log(pipeline);
98 if trace {
99 harn_vm::llm::enable_tracing();
100 }
101 harn_serve::run_acp_server(
102 server_config(pipeline.map(str::to_string), auth_policy).with_profile(profile),
103 )
104 .await;
105 if trace {
106 eprint!("{}", crate::commands::run::render_trace_summary());
107 }
108}
109
110pub(crate) async fn run_acp_channel_server(
111 pipeline: Option<String>,
112 request_rx: mpsc::UnboundedReceiver<serde_json::Value>,
113 response_tx: mpsc::UnboundedSender<String>,
114) {
115 harn_serve::run_acp_channel_server(
116 server_config(pipeline, AuthPolicy::allow_all()),
117 request_rx,
118 response_tx,
119 )
120 .await;
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 async fn configure_and_resolve_fixture(declared: bool) -> Result<(), String> {
128 harn_vm::reset_thread_local_state();
129 crate::compiler_context::ensure_builtin_signatures_installed();
130 let project = tempfile::tempdir().expect("temp project");
131 let script =
132 crate::tests::common::host_dispatch_project::write_host_dispatch_trigger_project(
133 project.path(),
134 declared,
135 r#"
136pub fn on_tick(_event) -> nil {
137 const _ = host_call("runtime.pipeline_input", {})
138 return nil
139}
140"#,
141 );
142 let mut vm = harn_vm::Vm::new();
143 harn_vm::register_vm_stdlib(&mut vm);
144 let result = async {
145 CliAcpRuntimeConfigurator
146 .configure(&mut vm, Some(&script))
147 .await?;
148 let extensions = crate::package::load_runtime_extensions(&script);
149 let collected = crate::package::collect_manifest_triggers(&mut vm, &extensions)
150 .await
151 .map_err(|error| error.to_string())?;
152 let crate::package::CollectedTriggerHandler::Local { callable, .. } =
153 &collected[0].handler
154 else {
155 return Err("fixture trigger must use a local handler".to_string());
156 };
157 vm.resolve_callable(callable)
158 .await
159 .map(|_| ())
160 .map_err(|error| error.to_string())
161 }
162 .await;
163 harn_vm::reset_thread_local_state();
164 result
165 }
166
167 #[tokio::test]
168 async fn acp_honors_manifest_trusted_host_dispatch_before_installing_triggers() {
169 configure_and_resolve_fixture(true)
170 .await
171 .expect("declared ACP project resolves its privileged trigger on dispatch");
172
173 let error = configure_and_resolve_fixture(false)
174 .await
175 .expect_err("undeclared ACP project remains unprivileged on dispatch");
176 assert!(
177 error.contains("host_call") && error.contains("not callable source API"),
178 "unexpected refusal: {error}"
179 );
180 }
181}