1use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts, WitLevel};
9use wasmtime::component::{Component, Linker, ResourceTable};
10use wasmtime::{Engine, Store};
11use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
12
13use crate::bindings::process_driver::ProcessDriver as ProcessDriverBindings;
14use crate::errors::RuntimeError;
15use crate::limits::{
16 classify_guest_failure, CallDeadlines, ComponentRole, EpochTicker, ExecutionLimiter,
17 GuestFailure, WallDeadline,
18};
19use crate::ExecutionLimits;
20
21pub use crate::bindings::process_driver::exports::murmur::driver::process::{
22 Bridge, Description, DriverFile, Event, ExitStatus, FailureKind, InterruptMethod, LaunchPlan,
23 LaunchRequest, QuotaState, QuotaWindow, RetryInfo, Session, SessionInfo, SessionMode,
24 ToolCallInfo, ToolCallProgress, ToolCallStart, ToolResultInfo, TurnFailure, Usage,
25};
26
27impl QuotaState {
28 pub fn wire_name(self) -> &'static str {
30 match self {
31 QuotaState::Ok => "ok",
32 QuotaState::Warning => "warning",
33 QuotaState::Exhausted => "exhausted",
34 }
35 }
36}
37
38pub const PROCESS_DRIVER_IFACE: &str = "murmur:driver/process@0.4.0";
40
41const PROCESS_DRIVER_IFACE_PREFIX: &str = "murmur:driver/process@";
44
45pub fn check_driver_interface(
58 transport: &str,
59 name: &str,
60 version: &str,
61 contracts: Option<&WitContracts>,
62 level: &WitLevel,
63) -> Result<(), RuntimeError> {
64 let process_exports: Vec<String> = contracts
65 .map(|c| {
66 c.exports
67 .iter()
68 .filter(|export| export.starts_with(PROCESS_DRIVER_IFACE_PREFIX))
69 .cloned()
70 .collect()
71 })
72 .unwrap_or_default();
73 let expected = level_interface(level, "murmur:driver", "process")
74 .unwrap_or_else(|| PROCESS_DRIVER_IFACE.to_string());
75 match transport {
76 "process" if process_exports.contains(&expected) => Ok(()),
77 "process" if process_exports.is_empty() => {
78 Err(RuntimeError::ProcessDriverInterfaceMissing {
79 name: name.to_string(),
80 version: version.to_string(),
81 expected,
82 })
83 }
84 "process" => Err(
87 level_mismatch(name, version, level, process_exports, Vec::new()).unwrap_or_else(
88 || RuntimeError::ProcessDriverInterfaceMissing {
89 name: name.to_string(),
90 version: version.to_string(),
91 expected,
92 },
93 ),
94 ),
95 "http" if !process_exports.is_empty() => {
96 Err(RuntimeError::HttpDriverExportsProcessInterface {
97 name: name.to_string(),
98 version: version.to_string(),
99 exported: process_exports,
100 })
101 }
102 _ => Ok(()),
103 }
104}
105
106const RETIRED_TEXT_IFACE_PREFIX: &str = "murmur:text/";
109
110const STREAM_IFACE_PREFIX: &str = "murmur:stream/";
112
113pub fn check_stream_interface(
128 name: &str,
129 version: &str,
130 contracts: Option<&WitContracts>,
131 level: &WitLevel,
132) -> Result<(), RuntimeError> {
133 let expected = level_interface(level, "murmur:stream", "events")
134 .unwrap_or_else(|| crate::runtime::WIT_STREAM_EVENTS_IFACE.to_string());
135 let imported: Vec<String> = contracts
136 .map(|c| {
137 c.imports
138 .iter()
139 .filter(|import| {
140 import.starts_with(RETIRED_TEXT_IFACE_PREFIX)
141 || (import.starts_with(STREAM_IFACE_PREFIX) && **import != expected)
142 })
143 .cloned()
144 .collect()
145 })
146 .unwrap_or_default();
147 if imported.is_empty() {
148 return Ok(());
149 }
150 if let Some(mismatch) = level_mismatch(name, version, level, Vec::new(), imported.clone()) {
151 return Err(mismatch);
152 }
153 Err(RuntimeError::DriverStreamInterfaceNotServed {
154 name: name.to_string(),
155 version: version.to_string(),
156 imported,
157 expected,
158 })
159}
160
161pub(crate) fn driver_refusal(
165 stream: Result<(), RuntimeError>,
166 driver: Result<(), RuntimeError>,
167) -> Result<(), RuntimeError> {
168 match (stream, driver) {
169 (
170 Err(RuntimeError::WitLevelMismatch {
171 mismatched: imports,
172 ..
173 }),
174 Err(RuntimeError::WitLevelMismatch {
175 name,
176 version,
177 level,
178 mismatched: mut exports,
179 }),
180 ) => {
181 exports.extend(imports);
182 Err(RuntimeError::WitLevelMismatch {
183 name,
184 version,
185 level,
186 mismatched: exports,
187 })
188 }
189 (Err(_), Err(driver @ RuntimeError::WitLevelMismatch { .. })) => Err(driver),
190 (Err(stream), _) => Err(stream),
191 (Ok(()), driver) => driver,
192 }
193}
194
195fn level_interface(level: &WitLevel, package: &str, interface: &str) -> Option<String> {
197 level
198 .version_of(package)
199 .map(|version| format!("{package}/{interface}@{version}"))
200}
201
202pub(crate) fn check_component_level(
206 name: &str,
207 version: &str,
208 contracts: WitContracts,
209 level: &WitLevel,
210) -> Result<(), RuntimeError> {
211 match level_mismatch(name, version, level, contracts.exports, contracts.imports) {
212 Some(mismatch) => Err(mismatch),
213 None => Ok(()),
214 }
215}
216
217fn level_mismatch(
220 name: &str,
221 version: &str,
222 level: &WitLevel,
223 exports: Vec<String>,
224 imports: Vec<String>,
225) -> Option<RuntimeError> {
226 let subject = FitSubject::component(WitContracts { exports, imports });
227 match version_fit(&subject, level) {
228 VersionFit::Unserved(mismatched) => Some(RuntimeError::WitLevelMismatch {
229 name: name.to_string(),
230 version: version.to_string(),
231 level: level.level,
232 mismatched,
233 }),
234 VersionFit::Fits => None,
235 }
236}
237
238pub fn check_required_env(
243 name: &str,
244 version: &str,
245 description: &Description,
246 env_allow: &[String],
247) -> Result<(), RuntimeError> {
248 let mut missing: Vec<String> = Vec::new();
249 for var in &description.required_env {
250 if !env_allow.contains(var) && !missing.contains(var) {
251 missing.push(var.clone());
252 }
253 }
254 if missing.is_empty() {
255 Ok(())
256 } else {
257 Err(RuntimeError::ProcessDriverRequiredEnvNotAllowed {
258 name: name.to_string(),
259 version: version.to_string(),
260 missing,
261 })
262 }
263}
264
265struct ProcessDriverStoreState {
267 wasi: WasiCtx,
268 table: ResourceTable,
269 limits: ExecutionLimiter,
270}
271
272impl WasiView for ProcessDriverStoreState {
273 fn ctx(&mut self) -> WasiCtxView<'_> {
274 WasiCtxView {
275 ctx: &mut self.wasi,
276 table: &mut self.table,
277 }
278 }
279}
280
281pub struct ProcessDriver {
284 name: String,
285 version: String,
286 store: Store<ProcessDriverStoreState>,
287 bindings: ProcessDriverBindings,
288 call_deadlines: CallDeadlines,
290 cut: Option<(&'static str, u64)>,
293}
294
295impl ProcessDriver {
296 pub(crate) async fn instantiate(
306 engine: &Engine,
307 component: &Component,
308 name: &str,
309 version: &str,
310 call_deadlines: CallDeadlines,
311 ) -> Result<Self, RuntimeError> {
312 let load_error = |message: String| RuntimeError::ProcessDriverLoad {
313 name: name.to_string(),
314 version: version.to_string(),
315 message,
316 };
317 let limits = ExecutionLimits::default().for_hook_calls(None);
318
319 let mut linker: Linker<ProcessDriverStoreState> = Linker::new(engine);
320 wasmtime_wasi::p2::add_to_linker_async(&mut linker)
321 .map_err(|e| load_error(e.to_string()))?;
322
323 let state = ProcessDriverStoreState {
324 wasi: WasiCtxBuilder::new().build(),
327 table: ResourceTable::new(),
328 limits: limits.limiter(),
329 };
330 let mut store = Store::new(engine, state);
331 store.limiter(|state| &mut state.limits);
332 store.set_epoch_deadline(limits.deadline_ticks());
333
334 let bindings = call_deadlines
335 .bound(
336 name,
337 ComponentRole::ProcessDriver,
338 "instantiate",
339 &limits,
340 ProcessDriverBindings::instantiate_async(&mut store, component, &linker),
341 )
342 .await
343 .map_err(|_| load_error(format!("instantiation {}", wall_deadline_text(&limits))))?
344 .map_err(|e| load_error(format!("{e:#}")))?;
345 Ok(Self {
346 name: name.to_string(),
347 version: version.to_string(),
348 store,
349 bindings,
350 call_deadlines,
351 cut: None,
352 })
353 }
354
355 pub async fn describe(&mut self) -> Result<Description, RuntimeError> {
360 self.enter("describe")
361 .map_err(|message| self.load_error(message))?;
362 let limits = self.reset_deadline();
363 let called = self
364 .call_deadlines
365 .bound(
366 &self.name,
367 ComponentRole::ProcessDriver,
368 "describe",
369 &limits,
370 self.bindings
371 .murmur_driver_process()
372 .call_describe(&mut self.store),
373 )
374 .await;
375 let description = self
376 .settle("describe", &limits, called)
377 .map_err(|message| self.load_error(format!("describe() {message}")))?
378 .map_err(|e| {
379 let message = match classify_guest_failure(&e, &self.store.data().limits) {
380 GuestFailure::DeadlineExceeded { seconds } => {
381 format!("describe() exceeded its {seconds}s deadline and was interrupted")
382 }
383 GuestFailure::ResourceLimit { message } => {
384 format!("describe() exceeded its resource limits: {message}")
385 }
386 GuestFailure::Other => format!("describe() trapped: {e:#}"),
387 };
388 self.load_error(message)
389 })?;
390 if let Some(bad) = unusable_env_name(&description) {
391 return Err(self.load_error(format!(
392 "describe() names {bad:?} in required-env, which is not a usable variable name"
393 )));
394 }
395 Ok(description)
396 }
397
398 pub async fn launch(
401 &mut self,
402 request: LaunchRequest,
403 ) -> Result<Result<LaunchPlan, String>, RuntimeError> {
404 self.enter("launch")
405 .map_err(|message| self.failed_in("launch", message))?;
406 let limits = self.reset_deadline();
407 let called = self
408 .call_deadlines
409 .bound(
410 &self.name,
411 ComponentRole::ProcessDriver,
412 "launch",
413 &limits,
414 self.bindings
415 .murmur_driver_process()
416 .call_launch(&mut self.store, &request),
417 )
418 .await;
419 self.settle("launch", &limits, called)
420 .map_err(|message| self.failed_in("launch", message))?
421 .map_err(|e| self.call_error("launch", &e))
422 }
423
424 pub async fn parse(&mut self, lines: Vec<String>) -> Result<Vec<Event>, RuntimeError> {
427 self.enter("parse")
428 .map_err(|message| self.failed_in("parse", message))?;
429 let limits = self.reset_deadline();
430 let called = self
431 .call_deadlines
432 .bound(
433 &self.name,
434 ComponentRole::ProcessDriver,
435 "parse",
436 &limits,
437 self.bindings
438 .murmur_driver_process()
439 .call_parse(&mut self.store, &lines),
440 )
441 .await;
442 self.settle("parse", &limits, called)
443 .map_err(|message| self.failed_in("parse", message))?
444 .map_err(|e| self.call_error("parse", &e))
445 }
446
447 pub async fn classify_exit(&mut self, exit: ExitStatus) -> Result<Event, RuntimeError> {
449 self.enter("classify-exit")
450 .map_err(|message| self.failed_in("classify-exit", message))?;
451 let limits = self.reset_deadline();
452 let called = self
453 .call_deadlines
454 .bound(
455 &self.name,
456 ComponentRole::ProcessDriver,
457 "classify-exit",
458 &limits,
459 self.bindings
460 .murmur_driver_process()
461 .call_classify_exit(&mut self.store, &exit),
462 )
463 .await;
464 self.settle("classify-exit", &limits, called)
465 .map_err(|message| self.failed_in("classify-exit", message))?
466 .map_err(|e| self.call_error("classify-exit", &e))
467 }
468
469 fn reset_deadline(&mut self) -> ExecutionLimits {
473 let limits = self.store.data().limits.limits();
474 self.store.set_epoch_deadline(limits.deadline_ticks());
475 limits
476 }
477
478 fn enter(&self, call: &str) -> Result<(), String> {
480 match self.cut {
481 None => Ok(()),
482 Some((earlier, seconds)) => Err(format!(
483 "{call} was not called: the earlier {earlier} call was stopped at its {seconds}s \
484 deadline, and a stopped driver is not entered again"
485 )),
486 }
487 }
488
489 fn settle<T>(
492 &mut self,
493 call: &'static str,
494 limits: &ExecutionLimits,
495 called: Result<T, WallDeadline>,
496 ) -> Result<T, String> {
497 called.map_err(|_| {
498 self.cut = Some((call, limits.deadline_seconds));
499 wall_deadline_text(limits)
500 })
501 }
502
503 fn failed_in(&self, call: &str, message: String) -> RuntimeError {
504 RuntimeError::ProcessDriverCallFailed {
505 name: self.name.clone(),
506 version: self.version.clone(),
507 call: call.to_string(),
508 message,
509 }
510 }
511
512 fn call_error(&self, call: &str, error: &wasmtime::Error) -> RuntimeError {
513 let message = match classify_guest_failure(error, &self.store.data().limits) {
514 GuestFailure::DeadlineExceeded { seconds } => {
515 format!("exceeded its {seconds}s deadline and was interrupted")
516 }
517 GuestFailure::ResourceLimit { message } => {
518 format!("exceeded its resource limits: {message}")
519 }
520 GuestFailure::Other => format!("trapped: {error:#}"),
521 };
522 self.failed_in(call, message)
523 }
524
525 fn load_error(&self, message: String) -> RuntimeError {
526 RuntimeError::ProcessDriverLoad {
527 name: self.name.clone(),
528 version: self.version.clone(),
529 message,
530 }
531 }
532}
533
534fn wall_deadline_text(limits: &ExecutionLimits) -> String {
538 format!(
539 "waited on the host past its {}s deadline and was stopped",
540 limits.deadline_seconds
541 )
542}
543
544pub(crate) fn is_usable_env_name(name: &str) -> bool {
551 !name.is_empty() && !name.contains('=') && !name.contains('\0')
552}
553
554fn unusable_env_name(description: &Description) -> Option<&str> {
557 description
558 .required_env
559 .iter()
560 .find(|var| !is_usable_env_name(var))
561 .map(String::as_str)
562}
563
564const STANDALONE_NAME: &str = "<wasm>";
567const STANDALONE_VERSION: &str = "";
568
569struct StandaloneDriver {
572 engine: Engine,
573 component: Component,
574 rt: tokio::runtime::Runtime,
575 _ticker: EpochTicker,
576}
577
578impl StandaloneDriver {
579 fn compile(wasm: &[u8]) -> Result<Self, RuntimeError> {
580 let engine = crate::runtime::build_engine()?;
581 let ticker = EpochTicker::spawn(&engine);
582 let component =
583 Component::new(&engine, wasm).map_err(|e| RuntimeError::ToolComponentCompile {
584 name: STANDALONE_NAME.to_string(),
585 version: STANDALONE_VERSION.to_string(),
586 message: e.to_string(),
587 })?;
588 let rt = tokio::runtime::Builder::new_current_thread()
589 .enable_all()
590 .build()
591 .map_err(|e| {
592 RuntimeError::Runtime(format!("failed to build process driver runtime: {e}"))
593 })?;
594 Ok(StandaloneDriver {
595 engine,
596 component,
597 rt,
598 _ticker: ticker,
599 })
600 }
601
602 async fn instantiate(&self) -> Result<ProcessDriver, RuntimeError> {
603 ProcessDriver::instantiate(
604 &self.engine,
605 &self.component,
606 STANDALONE_NAME,
607 STANDALONE_VERSION,
608 CallDeadlines::default(),
610 )
611 .await
612 }
613}
614
615pub fn describe_process_driver_wasm(wasm: &[u8]) -> Result<Description, RuntimeError> {
622 let driver = StandaloneDriver::compile(wasm)?;
623 driver.rt.block_on(async {
624 let mut instance = driver.instantiate().await?;
625 instance.describe().await
626 })
627}
628
629pub fn launch_and_parse_process_driver_wasm(
636 wasm: &[u8],
637 request: LaunchRequest,
638 lines: Vec<String>,
639) -> Result<(Result<LaunchPlan, String>, Vec<Event>), RuntimeError> {
640 let driver = StandaloneDriver::compile(wasm)?;
641 driver.rt.block_on(async {
642 let mut instance = driver.instantiate().await?;
643 let plan = instance.launch(request).await?;
644 let events = instance.parse(lines).await?;
645 Ok((plan, events))
646 })
647}
648
649#[cfg(test)]
650mod tests {
651 use murmur_artifact::ContractDirection;
652
653 use super::*;
654
655 fn served() -> &'static WitLevel {
656 crate::served_wit_level()
657 }
658
659 fn contracts(exports: &[&str]) -> WitContracts {
660 WitContracts {
661 exports: exports.iter().map(|e| e.to_string()).collect(),
662 imports: Vec::new(),
663 }
664 }
665
666 #[test]
667 fn process_driver_interface_process_transport_accepts_the_process_export() {
668 let c = contracts(&[PROCESS_DRIVER_IFACE]);
669 assert!(check_driver_interface("process", "d", "1.0.0", Some(&c), served()).is_ok());
670 }
671
672 #[test]
673 fn process_driver_interface_process_transport_refuses_a_tool_export() {
674 let c = contracts(&["murmur:tool/run@0.2.0"]);
675 let err = check_driver_interface("process", "d", "1.0.0", Some(&c), served()).unwrap_err();
676 match &err {
677 RuntimeError::ProcessDriverInterfaceMissing { expected, .. } => {
678 assert_eq!(expected, PROCESS_DRIVER_IFACE);
679 }
680 other => panic!("unexpected error: {other:?}"),
681 }
682 let message = err.to_string();
683 assert!(message.contains("'d@1.0.0'"), "{message}");
684 assert!(message.contains(PROCESS_DRIVER_IFACE), "{message}");
685 }
686
687 #[test]
688 fn process_driver_interface_process_transport_refuses_another_version_as_a_level_mismatch() {
689 let c = contracts(&["murmur:driver/process@0.0.1"]);
690 let err = check_driver_interface("process", "d", "1.0.0", Some(&c), served()).unwrap_err();
691 match &err {
692 RuntimeError::WitLevelMismatch {
693 level, mismatched, ..
694 } => {
695 assert_eq!(*level, served().level);
696 assert_eq!(mismatched.len(), 1);
697 assert_eq!(mismatched[0].interface, "murmur:driver/process@0.0.1");
698 assert_eq!(mismatched[0].direction, ContractDirection::Export);
699 }
700 other => panic!("unexpected error: {other:?}"),
701 }
702 assert_eq!(
703 err.to_string(),
704 format!(
705 "artifact 'd@1.0.0' does not fit WIT level {0}, the level this mur serves: it \
706 exports murmur:driver/process@0.0.1, and level {0} has murmur:driver@{1}",
707 served().level,
708 served().version_of("murmur:driver").unwrap()
709 )
710 );
711 }
712
713 fn level_1() -> &'static WitLevel {
716 &crate::wit_versions::wit_levels()[0]
717 }
718
719 #[test]
720 fn process_driver_interface_follows_the_level_it_is_given() {
721 let c = contracts(&["murmur:driver/process@0.4.0"]);
722 assert!(check_driver_interface("process", "d", "1.0.0", Some(&c), served()).is_ok());
723 match check_driver_interface("process", "d", "1.0.0", Some(&c), level_1()).unwrap_err() {
724 RuntimeError::WitLevelMismatch {
725 level, mismatched, ..
726 } => {
727 assert_eq!(level, 1);
728 assert_eq!(mismatched[0].direction, ContractDirection::Export);
729 assert_eq!(mismatched[0].package, "murmur:driver");
730 assert_eq!(mismatched[0].served_version.as_deref(), Some("0.3.0"));
731 }
732 other => panic!("unexpected error: {other:?}"),
733 }
734 let old = contracts(&["murmur:driver/process@0.3.0"]);
735 assert!(check_driver_interface("process", "d", "1.0.0", Some(&old), level_1()).is_ok());
736 let stream = importing(&["murmur:stream/events@0.1.0"]);
737 assert!(check_stream_interface("d", "1.0.0", Some(&stream), level_1()).is_ok());
738 }
739
740 #[test]
741 fn process_driver_interface_process_transport_refuses_no_contracts() {
742 let err = check_driver_interface("process", "d", "1.0.0", None, served()).unwrap_err();
743 assert!(matches!(
744 err,
745 RuntimeError::ProcessDriverInterfaceMissing { .. }
746 ));
747 }
748
749 #[test]
750 fn process_driver_interface_http_transport_refuses_the_process_export() {
751 let c = contracts(&[PROCESS_DRIVER_IFACE]);
752 let err = check_driver_interface("http", "d", "1.0.0", Some(&c), served()).unwrap_err();
753 match &err {
754 RuntimeError::HttpDriverExportsProcessInterface { name, exported, .. } => {
755 assert_eq!(name, "d");
756 assert_eq!(exported, &vec![PROCESS_DRIVER_IFACE.to_string()]);
757 }
758 other => panic!("unexpected error: {other:?}"),
759 }
760 }
761
762 #[test]
763 fn process_driver_interface_http_transport_accepts_a_tool_export() {
764 let c = contracts(&["murmur:tool/run@0.2.0"]);
765 assert!(check_driver_interface("http", "d", "1.0.0", Some(&c), served()).is_ok());
766 }
767
768 fn importing(imports: &[&str]) -> WitContracts {
769 WitContracts {
770 exports: vec!["murmur:tool/run@0.2.0".to_string()],
771 imports: imports.iter().map(|i| i.to_string()).collect(),
772 }
773 }
774
775 #[test]
776 fn stream_interface_refuses_and_lists_a_murmur_text_import() {
777 let c = importing(&[
778 "wasi:cli/environment@0.2.12",
779 "murmur:text/chunks@0.1.0",
780 "murmur:task/task@0.1.0",
781 ]);
782 let err = check_stream_interface("d", "1.0.0", Some(&c), served()).unwrap_err();
783 match &err {
784 RuntimeError::WitLevelMismatch {
785 name,
786 version,
787 mismatched,
788 ..
789 } => {
790 assert_eq!((name.as_str(), version.as_str()), ("d", "1.0.0"));
791 assert_eq!(mismatched.len(), 1);
792 assert_eq!(mismatched[0].interface, "murmur:text/chunks@0.1.0");
793 assert_eq!(mismatched[0].served_version, None);
794 }
795 other => panic!("unexpected error: {other:?}"),
796 }
797 let message = err.to_string();
798 assert!(message.contains("'d@1.0.0'"), "{message}");
799 assert!(
800 message.contains(
801 "it imports murmur:text/chunks@0.1.0, and level 4 has no version of murmur:text"
802 ),
803 "{message}"
804 );
805 }
806
807 #[test]
808 fn stream_interface_refuses_another_version_of_murmur_stream() {
809 let c = importing(&["murmur:stream/events@0.2.0"]);
810 let err = check_stream_interface("d", "1.0.0", Some(&c), served()).unwrap_err();
811 assert!(matches!(
812 err,
813 RuntimeError::WitLevelMismatch { ref mismatched, .. }
814 if mismatched.len() == 1 && mismatched[0].interface == "murmur:stream/events@0.2.0"
815 ));
816 }
817
818 #[test]
821 fn stream_interface_refuses_an_unknown_interface_at_the_served_version_as_not_served() {
822 let c = importing(&["murmur:stream/other@0.1.0"]);
823 let err = check_stream_interface("d", "1.0.0", Some(&c), served()).unwrap_err();
824 match &err {
825 RuntimeError::DriverStreamInterfaceNotServed {
826 imported, expected, ..
827 } => {
828 assert_eq!(imported, &vec!["murmur:stream/other@0.1.0".to_string()]);
829 assert_eq!(expected, crate::runtime::WIT_STREAM_EVENTS_IFACE);
830 }
831 other => panic!("unexpected error: {other:?}"),
832 }
833 let both = importing(&["murmur:stream/other@0.1.0", "murmur:text/chunks@0.1.0"]);
835 assert!(matches!(
836 check_stream_interface("d", "1.0.0", Some(&both), served()).unwrap_err(),
837 RuntimeError::WitLevelMismatch { ref mismatched, .. }
838 if mismatched.len() == 1 && mismatched[0].interface == "murmur:text/chunks@0.1.0"
839 ));
840 }
841
842 #[test]
843 fn driver_refusal_reports_a_level_mismatch_over_any_other() {
844 let not_served = || {
845 check_stream_interface(
846 "d",
847 "1.0.0",
848 Some(&importing(&["murmur:stream/other@0.1.0"])),
849 served(),
850 )
851 };
852 let old_export = || {
853 check_driver_interface(
854 "process",
855 "d",
856 "1.0.0",
857 Some(&contracts(&["murmur:driver/process@0.2.0"])),
858 served(),
859 )
860 };
861 let text_import = || {
862 check_stream_interface(
863 "d",
864 "1.0.0",
865 Some(&importing(&["murmur:text/chunks@0.1.0"])),
866 served(),
867 )
868 };
869 let no_export = || check_driver_interface("process", "d", "1.0.0", None, served());
870
871 assert!(matches!(
872 driver_refusal(not_served(), old_export()),
873 Err(RuntimeError::WitLevelMismatch { .. })
874 ));
875 assert!(matches!(
876 driver_refusal(text_import(), no_export()),
877 Err(RuntimeError::WitLevelMismatch { .. })
878 ));
879 assert!(matches!(
880 driver_refusal(not_served(), no_export()),
881 Err(RuntimeError::DriverStreamInterfaceNotServed { .. })
882 ));
883 match driver_refusal(text_import(), old_export()) {
884 Err(RuntimeError::WitLevelMismatch { mismatched, .. }) => {
885 let names: Vec<&str> = mismatched.iter().map(|m| m.interface.as_str()).collect();
886 assert_eq!(
887 names,
888 ["murmur:driver/process@0.2.0", "murmur:text/chunks@0.1.0"]
889 );
890 }
891 other => panic!("unexpected result: {other:?}"),
892 }
893 assert!(driver_refusal(Ok(()), Ok(())).is_ok());
894 }
895
896 #[test]
897 fn stream_interface_accepts_exactly_the_served_instance() {
898 let c = importing(&["murmur:stream/events@0.1.0", "wasi:http/types@0.2.12"]);
899 assert!(check_stream_interface("d", "1.0.0", Some(&c), served()).is_ok());
900 }
901
902 #[test]
903 fn stream_interface_accepts_a_component_with_no_murmur_import() {
904 let c = importing(&["wasi:cli/environment@0.2.12"]);
905 assert!(check_stream_interface("d", "1.0.0", Some(&c), served()).is_ok());
906 assert!(check_stream_interface("d", "1.0.0", Some(&importing(&[])), served()).is_ok());
907 }
908
909 #[test]
910 fn stream_interface_accepts_no_contracts() {
911 assert!(check_stream_interface("d", "1.0.0", None, served()).is_ok());
912 }
913
914 fn description(required_env: &[&str]) -> Description {
915 Description {
916 harness: "h".to_string(),
917 binary: "b".to_string(),
918 version_args: Vec::new(),
919 tested_versions: Vec::new(),
920 interrupt: InterruptMethod::Unsupported,
921 required_env: required_env.iter().map(|v| v.to_string()).collect(),
922 streams_text: false,
923 reports_usage: false,
924 }
925 }
926
927 #[test]
928 fn required_env_all_declared_passes() {
929 let allow = vec!["A".to_string(), "B".to_string(), "C".to_string()];
930 assert!(check_required_env("d", "1.0.0", &description(&["B", "A"]), &allow).is_ok());
931 }
932
933 #[test]
934 fn required_env_reports_missing_names_in_order_once_each() {
935 let allow = vec!["B".to_string()];
936 let err = check_required_env("d", "1.0.0", &description(&["Z", "B", "A", "Z"]), &allow)
937 .unwrap_err();
938 match &err {
939 RuntimeError::ProcessDriverRequiredEnvNotAllowed { missing, .. } => {
940 assert_eq!(missing, &vec!["Z".to_string(), "A".to_string()]);
941 }
942 other => panic!("unexpected error: {other:?}"),
943 }
944 let message = err.to_string();
945 assert!(message.contains("'d@1.0.0'"), "{message}");
946 assert!(message.contains("Z, A"), "{message}");
947 }
948
949 #[test]
950 fn required_env_usable_names_pass() {
951 assert_eq!(
952 unusable_env_name(&description(&["HOME", "PROFILE_2"])),
953 None
954 );
955 assert_eq!(unusable_env_name(&description(&[])), None);
956 }
957
958 #[test]
959 fn required_env_refuses_a_name_that_is_not_a_variable() {
960 for bad in ["", "A=B", "A\0B"] {
961 assert_eq!(
962 unusable_env_name(&description(&["HOME", bad, "TAIL"])),
963 Some(bad),
964 "{bad:?} should be refused"
965 );
966 }
967 }
968
969 #[test]
975 #[ignore = "a measurement, not an assertion; run with --release --ignored --nocapture"]
976 fn process_driver_parse_cost() {
977 use std::time::Instant;
978
979 const WASM: &[u8] = include_bytes!(concat!(
980 env!("CARGO_MANIFEST_DIR"),
981 "/../murmur-cli/tests/fixtures/process-driver/tool/process-driver.wasm"
982 ));
983 let engine = crate::runtime::build_engine().expect("engine");
984 let _ticker = EpochTicker::spawn(&engine);
985 let component = Component::new(&engine, WASM).expect("fixture compiles");
986 let rt = tokio::runtime::Builder::new_current_thread()
987 .enable_all()
988 .build()
989 .expect("runtime");
990
991 let instantiations = 50;
992 let start = Instant::now();
993 for _ in 0..instantiations {
994 rt.block_on(ProcessDriver::instantiate(
995 &engine,
996 &component,
997 "d",
998 "0.1.0",
999 CallDeadlines::default(),
1000 ))
1001 .expect("instantiates");
1002 }
1003 let per_instantiation = start.elapsed() / instantiations;
1004
1005 let mut driver = rt
1006 .block_on(ProcessDriver::instantiate(
1007 &engine,
1008 &component,
1009 "d",
1010 "0.1.0",
1011 CallDeadlines::default(),
1012 ))
1013 .expect("instantiates");
1014 for lines in [1usize, 64] {
1015 let batch: Vec<String> = (0..lines).map(|i| format!("text line {i}")).collect();
1016 let runs = 2_000;
1017 let mut samples = Vec::with_capacity(runs);
1018 for _ in 0..runs {
1019 let start = Instant::now();
1020 let events = rt.block_on(driver.parse(batch.clone())).expect("parses");
1021 samples.push(start.elapsed());
1022 assert_eq!(events.len(), lines);
1023 }
1024 samples.sort_unstable();
1025 let median = samples[samples.len() / 2];
1026 let p95 = samples[samples.len() * 95 / 100];
1027 crate::diagnostic::raw_to_stdout(&format!(
1028 "parse {lines}-line batch on one reused instance: median {median:?}, p95 {p95:?} \
1029 ({runs} calls)\n"
1030 ));
1031 }
1032 crate::diagnostic::raw_to_stdout(&format!(
1033 "instantiate one driver: {per_instantiation:?} (mean of {instantiations})\n"
1034 ));
1035 }
1036
1037 #[test]
1038 fn process_driver_granted_nothing() {
1039 let wat = r#"(component
1040 (import "murmur:stream/events@0.1.0" (instance
1041 (export "emit-chunk" (func (param "chunk" string)))
1042 ))
1043 )"#;
1044 let bytes = wat::parse_str(wat).expect("component WAT parses");
1045 let engine = crate::runtime::build_engine().expect("engine");
1046 let _ticker = EpochTicker::spawn(&engine);
1047 let component = Component::new(&engine, &bytes).expect("component compiles");
1048 let rt = tokio::runtime::Builder::new_current_thread()
1049 .enable_all()
1050 .build()
1051 .expect("runtime");
1052 let result = rt.block_on(ProcessDriver::instantiate(
1053 &engine,
1054 &component,
1055 "d",
1056 "1.0.0",
1057 CallDeadlines::default(),
1058 ));
1059 match result {
1060 Err(RuntimeError::ProcessDriverLoad { name, message, .. }) => {
1061 assert_eq!(name, "d");
1062 assert!(message.contains("murmur:stream/events@0.1.0"), "{message}");
1063 }
1064 Err(other) => panic!("unexpected error: {other:?}"),
1065 Ok(_) => panic!("a driver importing a Murmur interface instantiated"),
1066 }
1067 }
1068
1069 #[test]
1070 fn launch_and_parse_run_on_one_instance() {
1071 const WASM: &[u8] = include_bytes!(concat!(
1072 env!("CARGO_MANIFEST_DIR"),
1073 "/../murmur-cli/tests/fixtures/process-driver/tool/process-driver.wasm"
1074 ));
1075 let request = LaunchRequest {
1076 model: None,
1077 system_prompt: String::new(),
1078 config: None,
1079 bridge: Some(Bridge {
1080 server_name: "murmur".to_string(),
1081 url: "http://127.0.0.1:1/mcp".to_string(),
1082 bearer_token: "token".to_string(),
1083 tool_names: vec!["python3".to_string()],
1084 }),
1085 session: Session {
1086 id: "s1".to_string(),
1087 mode: SessionMode::New,
1088 },
1089 harness_version: None,
1090 task: "probe".to_string(),
1091 };
1092 let lines = vec![
1093 r#"tool c1 murmur__python3 {"command":"x"}"#.to_string(),
1094 "end done".to_string(),
1095 ];
1096 let (plan, events) =
1097 launch_and_parse_process_driver_wasm(WASM, request, lines).expect("driver runs");
1098 let plan = plan.expect("the fixture accepts a non-empty task");
1099 assert!(plan
1100 .env_set
1101 .iter()
1102 .any(|(name, value)| name == "FIXTURE_BRIDGE_TOKEN" && value == "token"));
1103 match &events[..] {
1106 [Event::ToolCall(call), Event::TurnEnd(summary)] => {
1107 assert_eq!(call.name, "python3");
1108 assert_eq!(call.input, r#"{"command":"x"}"#);
1109 assert_eq!(summary, "done");
1110 }
1111 other => panic!("unexpected events: {other:?}"),
1112 }
1113 }
1114}