1#[cfg(test)]
19use async_trait::async_trait;
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22use wm_core::{Args, Context, CoreError, Output, Result, Tool};
23
24use crate::circuit_breaker::CircuitBreakerRegistry;
25use crate::rate_limiter::RateLimiter;
26use wm_governance::{
27 ActionVerdict, DharmaGate, FirebreakOutcome, KarmaLedger, ResourceRules, ResourceVerdict,
28};
29
30pub const DEFAULT_DISPATCH_TIMEOUT: Duration = Duration::from_secs(300);
36
37fn hash_args(args: &Args) -> u64 {
40 use std::hash::Hasher;
41 let bytes = serde_json::to_vec(args).unwrap_or_default();
42 let mut hasher = ahash::AHasher::default();
43 hasher.write(&bytes);
44 hasher.finish()
45}
46
47fn first_str(v: &serde_json::Value, keys: &[&str]) -> Option<String> {
49 keys.iter().find_map(|k| {
50 v.get(*k)
51 .and_then(serde_json::Value::as_str)
52 .map(str::to_string)
53 })
54}
55
56#[allow(clippy::too_many_arguments)]
65fn record_write_audit(
66 journal: &wm_governance::WriteAuditJournal,
67 store_write_baseline: u64,
68 tool: &str,
69 actor: wm_governance::ActorIdentity,
70 declared_writes: bool,
71 args_memory_id: Option<&str>,
72 args_content_hash: Option<&str>,
73 output: &serde_json::Value,
74 success: bool,
75 confirm_gated: Option<bool>,
76) {
77 let reported_writes = output
78 .get("writes")
79 .and_then(|w| w.as_array())
80 .map_or(0, |a| a.len() as u32);
81 let memory_id = first_str(output, &["id", "memory_id", "memory"])
82 .or_else(|| args_memory_id.map(str::to_string));
83 let content_hash = first_str(output, &["content_hash", "hash", "sha256"])
84 .or_else(|| args_content_hash.map(str::to_string));
85 let result = match confirm_gated {
86 Some(confirmed) => journal.record_since_confirmed(
87 store_write_baseline,
88 tool,
89 actor,
90 memory_id.as_deref(),
91 content_hash.as_deref(),
92 declared_writes,
93 reported_writes,
94 success,
95 confirmed,
96 ),
97 None => journal.record_since(
98 store_write_baseline,
99 tool,
100 actor,
101 memory_id.as_deref(),
102 content_hash.as_deref(),
103 declared_writes,
104 reported_writes,
105 success,
106 ),
107 };
108 if let Err(e) = result {
109 tracing::warn!(error = %e, "Write-audit journal record failed");
110 }
111}
112
113pub struct DispatchPipeline {
117 rate_limiter: Arc<RateLimiter>,
118 circuit_breakers: Arc<CircuitBreakerRegistry>,
119 dharma_gate: Arc<DharmaGate>,
120 karma_ledger: Option<Arc<KarmaLedger>>,
121 resource_rules: Option<Arc<ResourceRules>>,
124 write_gate: Option<Arc<crate::write_gate::WriteGate>>,
127 write_audit: Option<Arc<wm_governance::WriteAuditJournal>>,
130 firebreak: Option<Arc<wm_governance::Firebreak>>,
134 gana_registry: Option<Arc<std::sync::Mutex<wm_core::GanaRegistry>>>,
136 dispatch_timeout: Option<Duration>,
140}
141
142impl DispatchPipeline {
143 pub fn new(
148 rate_limiter: Arc<RateLimiter>,
149 circuit_breakers: Arc<CircuitBreakerRegistry>,
150 dharma_gate: Arc<DharmaGate>,
151 karma_ledger: Option<Arc<KarmaLedger>>,
152 ) -> Self {
153 Self {
154 rate_limiter,
155 circuit_breakers,
156 dharma_gate,
157 karma_ledger,
158 resource_rules: None,
159 write_gate: None,
160 write_audit: None,
161 firebreak: Some(Arc::new(wm_governance::Firebreak::promoted())),
167 gana_registry: None,
168 dispatch_timeout: None,
169 }
170 }
171
172 #[must_use]
177 pub fn timeout_from_env() -> Option<Duration> {
178 match std::env::var("WM_DISPATCH_TIMEOUT_MS") {
179 Ok(v) => match v.trim().parse::<u64>() {
180 Ok(0) => None,
181 Ok(ms) => Some(Duration::from_millis(ms)),
182 Err(_) => {
183 tracing::warn!(
184 value = %v,
185 "WM_DISPATCH_TIMEOUT_MS is not a valid millisecond count — using default"
186 );
187 Some(DEFAULT_DISPATCH_TIMEOUT)
188 }
189 },
190 Err(_) => Some(DEFAULT_DISPATCH_TIMEOUT),
191 }
192 }
193
194 #[must_use]
196 pub const fn with_dispatch_timeout(mut self, timeout: Option<Duration>) -> Self {
197 self.dispatch_timeout = timeout;
198 self
199 }
200
201 #[must_use]
203 pub fn with_defaults() -> Self {
204 Self::new(
205 Arc::new(RateLimiter::default()),
206 Arc::new(CircuitBreakerRegistry::default()),
207 Arc::new(DharmaGate::default()),
208 None,
209 )
210 }
211
212 #[must_use]
214 pub fn with_gana_registry(
215 mut self,
216 registry: Arc<std::sync::Mutex<wm_core::GanaRegistry>>,
217 ) -> Self {
218 self.gana_registry = Some(registry);
219 self
220 }
221
222 #[must_use]
224 pub fn with_resource_rules(mut self, rules: Arc<ResourceRules>) -> Self {
225 self.resource_rules = Some(rules);
226 self
227 }
228
229 #[must_use]
233 pub fn with_write_gate(mut self, gate: Arc<crate::write_gate::WriteGate>) -> Self {
234 self.write_gate = Some(gate);
235 self
236 }
237
238 #[must_use]
241 pub fn with_write_audit(mut self, journal: Arc<wm_governance::WriteAuditJournal>) -> Self {
242 self.write_audit = Some(journal);
243 self
244 }
245
246 #[must_use]
249 pub fn with_firebreak(mut self, firebreak: Arc<wm_governance::Firebreak>) -> Self {
250 self.firebreak = Some(firebreak);
251 self
252 }
253
254 #[must_use]
259 pub fn with_firebreak_option(
260 mut self,
261 firebreak: Option<Arc<wm_governance::Firebreak>>,
262 ) -> Self {
263 self.firebreak = firebreak;
264 self
265 }
266
267 #[must_use]
269 pub fn firebreak(&self) -> Option<&wm_governance::Firebreak> {
270 self.firebreak.as_deref()
271 }
272
273 #[must_use]
276 pub fn with_write_audit_option(
277 mut self,
278 journal: Option<Arc<wm_governance::WriteAuditJournal>>,
279 ) -> Self {
280 self.write_audit = journal;
281 self
282 }
283
284 #[must_use]
286 pub fn resource_rules(&self) -> Option<&ResourceRules> {
287 self.resource_rules.as_deref()
288 }
289
290 #[must_use]
292 pub fn write_audit(&self) -> Option<&wm_governance::WriteAuditJournal> {
293 self.write_audit.as_deref()
294 }
295
296 pub async fn dispatch(&self, tool: &dyn Tool, ctx: &mut Context, args: Args) -> Result<Output> {
298 let start = Instant::now();
299
300 if !tool.effects().is_available_in(ctx.brain_wave) {
302 return Err(CoreError::Governance(format!(
303 "tool '{}' not available in {:?} brain-wave state",
304 tool.name(),
305 ctx.brain_wave
306 )));
307 }
308
309 const COHERENCE_THRESHOLD: f32 = 0.3;
311 if !tool.effects().writes.is_empty() && ctx.citta_coherence < COHERENCE_THRESHOLD {
312 return Err(CoreError::Governance(format!(
313 "tool '{}' requires write access but citta coherence is {:.2} (minimum {:.2})",
314 tool.name(),
315 ctx.citta_coherence,
316 COHERENCE_THRESHOLD
317 )));
318 }
319
320 if ctx.readonly && !tool.effects().writes.is_empty() {
324 return Err(CoreError::Governance(format!(
325 "server is read-only: tool '{}' requires write access",
326 tool.name()
327 )));
328 }
329
330 const CONFIDENCE_THRESHOLD: f32 = 0.5;
332 if ctx.self_model_confidence < CONFIDENCE_THRESHOLD {
333 tracing::warn!(
334 tool = tool.name(),
335 confidence = ctx.self_model_confidence,
336 "low self-model confidence — conservative dispatch mode"
337 );
338 if !tool.effects().writes.is_empty() {
340 return Err(CoreError::Governance(format!(
341 "tool '{}' requires write access but self-model confidence is {:.2} (minimum {:.2}) — conservative dispatch blocks writes",
342 tool.name(),
343 ctx.self_model_confidence,
344 CONFIDENCE_THRESHOLD
345 )));
346 }
347 }
348
349 const DRIVE_CAUTION_THRESHOLD: f32 = 0.85;
351 if !tool.effects().writes.is_empty() && ctx.drive_caution > DRIVE_CAUTION_THRESHOLD {
352 tracing::warn!(
353 tool = tool.name(),
354 drive_caution = ctx.drive_caution,
355 "high drive caution — write operation flagged for review"
356 );
357 }
358
359 const DRIVE_ENERGY_THRESHOLD: f32 = 0.15;
361 if !tool.effects().writes.is_empty() && ctx.drive_energy < DRIVE_ENERGY_THRESHOLD {
362 tracing::warn!(
363 tool = tool.name(),
364 drive_energy = ctx.drive_energy,
365 "low drive energy — write operation may be resource-constrained"
366 );
367 }
368
369 let verdict = self.dharma_gate.evaluate(tool.effects(), ctx);
371 match verdict {
372 ActionVerdict::Panic(reason) => {
373 tracing::error!(tool = tool.name(), reason = %reason, "Dharma PANIC");
374 return Err(CoreError::Governance(reason));
375 }
376 ActionVerdict::Intervene(reason) => {
377 tracing::warn!(tool = tool.name(), reason = %reason, "Dharma INTERVENE");
378 return Err(CoreError::Governance(reason));
379 }
380 ActionVerdict::Correct(reason) => {
381 tracing::info!(tool = tool.name(), reason = %reason, "Dharma CORRECT — proceeding with restrictions");
382 }
383 ActionVerdict::Advise(reason) => {
384 tracing::debug!(tool = tool.name(), reason = %reason, "Dharma ADVISE");
385 }
386 ActionVerdict::Observe => {}
387 }
388
389 let mut novelty_flag: Option<String> = None;
395 if let Some(ref rules) = self.resource_rules {
396 let effects = tool.effects();
397 let is_write = !effects.writes.is_empty();
398 let is_spawn = effects.spawns
399 || effects
400 .writes
401 .iter()
402 .chain(effects.reads.iter())
403 .any(|r| matches!(r, wm_core::Resource::Process));
404 let is_network = effects
405 .writes
406 .iter()
407 .chain(effects.reads.iter())
408 .any(|r| matches!(r, wm_core::Resource::Network));
409 let has_purpose = [args.get("purpose"), ctx.meta.get("purpose")]
410 .into_iter()
411 .flatten()
412 .filter_map(serde_json::Value::as_str)
413 .any(|p| !p.trim().is_empty());
414 let homeostasis = self.dharma_gate.homeostasis();
415 let verdict = rules.evaluate(
416 tool.name(),
417 hash_args(&args),
418 is_write,
419 is_spawn,
420 is_network,
421 has_purpose,
422 &homeostasis,
423 ctx.brain_wave,
424 );
425 match verdict {
426 ResourceVerdict::Allow => {}
427 ResourceVerdict::NotNovel { .. } => {
428 novelty_flag = Some(verdict.reason());
429 tracing::warn!(
430 tool = tool.name(),
431 reason = %verdict.reason(),
432 "resource rules: novelty flag on response"
433 );
434 }
435 ResourceVerdict::BudgetExceeded { .. }
436 | ResourceVerdict::RequiresHumanReview { .. }
437 | ResourceVerdict::NoPurpose { .. } => {
438 tracing::warn!(
439 tool = tool.name(),
440 reason = %verdict.reason(),
441 "resource rules: dispatch blocked"
442 );
443 return Err(CoreError::Governance(format!(
444 "resource rules: {}",
445 verdict.reason()
446 )));
447 }
448 }
449 }
450
451 let mut args = args;
457 let gate_disclosure: Option<serde_json::Value> = if let Some(ref gate) = self.write_gate {
458 let outcome = gate.enforce(tool.name(), &mut args)?;
459 if let Some(sc) = outcome.short_circuit {
460 return Ok(sc);
461 }
462 outcome.disclosure
463 } else {
464 None
465 };
466
467 if let Err(retry_after_ms) = self.rate_limiter.try_acquire(tool.name()) {
469 return Err(CoreError::RateLimited(format!(
470 "{}: retry after {}ms",
471 tool.name(),
472 retry_after_ms
473 )));
474 }
475
476 if self.circuit_breakers.is_open(tool.name()) {
478 return Err(CoreError::CircuitBreaker(tool.name().to_string()));
479 }
480
481 let confirmed = args
483 .get("confirm")
484 .and_then(serde_json::Value::as_bool)
485 .unwrap_or(false);
486 let confirm_gated = if tool.effects().destructive {
487 if !confirmed {
488 return Err(CoreError::Governance(format!(
489 "tool '{}' is destructive — pass `\"confirm\": true` in args to proceed",
490 tool.name()
491 )));
492 }
493 Some(true)
496 } else {
497 None
498 };
499
500 let mut firebreak_advisories: Vec<String> = Vec::new();
508 if let Some(ref firebreak) = self.firebreak {
509 match firebreak.enforce(tool.name(), tool.effects(), &args) {
510 FirebreakOutcome::Blocked(reason) => {
511 tracing::warn!(tool = tool.name(), reason = %reason, "firebreak VETO");
512 return Err(CoreError::Governance(reason));
513 }
514 FirebreakOutcome::Proceed { advisories } if !advisories.is_empty() => {
515 tracing::info!(tool = tool.name(), advisories = ?advisories, "firebreak advisories");
516 firebreak_advisories = advisories;
517 }
518 FirebreakOutcome::Proceed { .. } => {}
519 }
520 }
521
522 let has_runtime_galaxy = args
536 .get("galaxy")
537 .and_then(serde_json::Value::as_str)
538 .is_some_and(|g| !g.is_empty());
539 let mut checked_galaxies: Vec<wm_core::Galaxy> = Vec::new();
540
541 if !has_runtime_galaxy {
542 for resource in &tool.effects().reads {
543 if let wm_core::Resource::Galaxy(name) = resource {
544 if let Some(galaxy) = wm_core::Galaxy::from_db_name(name) {
545 if !ctx.can_access_galaxy(galaxy) {
546 return Err(CoreError::Governance(format!(
547 "compartment '{}' cannot read galaxy '{}' (tool '{}')",
548 ctx.compartment.as_deref().unwrap_or("none"),
549 name,
550 tool.name()
551 )));
552 }
553 checked_galaxies.push(galaxy);
554 }
555 }
556 }
557 for resource in &tool.effects().writes {
558 if let wm_core::Resource::Galaxy(name) = resource {
559 if let Some(galaxy) = wm_core::Galaxy::from_db_name(name) {
560 if !ctx.can_write_galaxy(galaxy) {
561 return Err(CoreError::Governance(format!(
562 "compartment '{}' cannot write to galaxy '{}' (tool '{}')",
563 ctx.compartment.as_deref().unwrap_or("none"),
564 name,
565 tool.name()
566 )));
567 }
568 checked_galaxies.push(galaxy);
569 }
570 }
571 }
572 }
573
574 if let Some(galaxy_str) = args.get("galaxy").and_then(serde_json::Value::as_str) {
576 if !galaxy_str.is_empty() {
577 if let Some(runtime_galaxy) = wm_core::Galaxy::from_db_name(galaxy_str) {
578 if !checked_galaxies.contains(&runtime_galaxy) {
579 let has_writes = !tool.effects().writes.is_empty();
581 if has_writes {
582 if !ctx.can_write_galaxy(runtime_galaxy) {
583 return Err(CoreError::Governance(format!(
584 "compartment '{}' cannot write to galaxy '{}' (tool '{}' runtime arg)",
585 ctx.compartment.as_deref().unwrap_or("none"),
586 galaxy_str,
587 tool.name()
588 )));
589 }
590 } else if !ctx.can_access_galaxy(runtime_galaxy) {
591 return Err(CoreError::Governance(format!(
592 "compartment '{}' cannot read galaxy '{}' (tool '{}' runtime arg)",
593 ctx.compartment.as_deref().unwrap_or("none"),
594 galaxy_str,
595 tool.name()
596 )));
597 }
598 }
599 }
600 }
601 }
602
603 if !tool.effects().writes.is_empty()
609 && let Some(galaxy_str) = args.get("galaxy").and_then(serde_json::Value::as_str)
610 && galaxy_str == "citta"
611 && !tool
612 .effects()
613 .reads
614 .iter()
615 .any(|r| matches!(r, wm_core::Resource::Galaxy(g) if g == "citta"))
616 {
617 return Err(CoreError::Governance(
618 "VIOLATION_SATYA: writing to citta (runtime galaxy) without reading — memory fabrication is forbidden"
619 .to_string(),
620 ));
621 }
622
623 let args_memory_id = first_str(&args, &["id", "memory_id", "memory"]);
631 let args_content_hash = first_str(&args, &["content_hash", "hash", "sha256"]);
632 let write_audit_baseline = self
633 .write_audit
634 .as_ref()
635 .map_or(0, |j| j.dispatch_baseline());
636 let result = if let Some(timeout) = self.dispatch_timeout {
637 if let Ok(res) = tokio::time::timeout(timeout, tool.call(ctx, args)).await {
638 res
639 } else {
640 tracing::error!(
641 tool = tool.name(),
642 timeout_ms = timeout.as_millis(),
643 "tool dispatch timed out"
644 );
645 self.circuit_breakers.record_failure(tool.name());
646 return Err(CoreError::Tool(format!(
647 "tool '{}' timed out after {}ms",
648 tool.name(),
649 timeout.as_millis()
650 )));
651 }
652 } else {
653 tool.call(ctx, args).await
654 };
655 let elapsed = start.elapsed();
656
657 let result = match (result, novelty_flag) {
659 (Ok(mut output), Some(flag)) => {
660 if let serde_json::Value::Object(ref mut map) = output {
661 match map.get_mut("resource_flags") {
662 Some(serde_json::Value::Array(arr)) => {
663 arr.push(serde_json::Value::String(flag));
664 }
665 Some(_) => {}
666 None => {
667 map.insert(
668 "resource_flags".to_string(),
669 serde_json::Value::Array(vec![serde_json::Value::String(flag)]),
670 );
671 }
672 }
673 }
674 Ok(output)
675 }
676 (result, _) => result,
677 };
678
679 let result = match (result, gate_disclosure) {
682 (Ok(mut output), Some(disclosure)) => {
683 if let serde_json::Value::Object(ref mut map) = output {
684 map.insert("write_gate".to_string(), disclosure);
685 }
686 Ok(output)
687 }
688 (result, _) => result,
689 };
690
691 let result = match (result, firebreak_advisories) {
695 (Ok(mut output), advisories) if !advisories.is_empty() => {
696 if let serde_json::Value::Object(ref mut map) = output {
697 map.insert(
698 "firebreak".to_string(),
699 serde_json::json!({ "advisories": advisories }),
700 );
701 }
702 Ok(output)
703 }
704 (result, _) => result,
705 };
706
707 if let Ok(output) = &result {
709 tool.stats().record_success(elapsed, elapsed);
710 self.circuit_breakers.record_success(tool.name());
711
712 if let Some(ref ledger) = self.karma_ledger {
713 let declared_writes = !tool.effects().writes.is_empty();
714 let actual_writes = output
715 .get("writes")
716 .and_then(|w| w.as_array())
717 .map_or(0, |a| a.len() as u32);
718 if let Err(e) = ledger.record(tool.name(), declared_writes, actual_writes, true) {
719 tracing::warn!(error = %e, "Karma ledger record failed");
720 }
721 ctx.karma_debt = ledger.total_debt();
722 }
723
724 if let Some(ref journal) = self.write_audit {
725 let declared_writes = !tool.effects().writes.is_empty();
726 record_write_audit(
727 journal,
728 write_audit_baseline,
729 tool.name(),
730 wm_governance::ActorIdentity::from_context(ctx),
731 declared_writes,
732 args_memory_id.as_deref(),
733 args_content_hash.as_deref(),
734 output,
735 true,
736 confirm_gated,
737 );
738 }
739 } else {
740 tool.stats().record_failure(elapsed);
741 self.circuit_breakers.record_failure(tool.name());
742
743 if let Some(ref ledger) = self.karma_ledger {
744 let declared_writes = !tool.effects().writes.is_empty();
745 if let Err(ke) = ledger.record(tool.name(), declared_writes, 0, false) {
746 tracing::warn!(error = %ke, "Karma ledger record failed");
747 }
748 ctx.karma_debt = ledger.total_debt();
749 }
750
751 if let Some(ref journal) = self.write_audit {
752 let declared_writes = !tool.effects().writes.is_empty();
753 record_write_audit(
754 journal,
755 write_audit_baseline,
756 tool.name(),
757 wm_governance::ActorIdentity::from_context(ctx),
758 declared_writes,
759 args_memory_id.as_deref(),
760 args_content_hash.as_deref(),
761 &serde_json::Value::Null,
762 false,
763 confirm_gated,
764 );
765 }
766 }
767
768 if let Some(ref registry) = self.gana_registry {
770 if let Ok(mut reg) = registry.lock() {
771 let gana = tool.gana();
772 reg.record_usage(gana, result.is_ok());
773 if let Some(prev) = ctx.last_gana {
775 reg.record_co_usage(prev, gana);
776 }
777 ctx.last_gana = Some(gana);
778 }
779 }
780
781 result
782 }
783
784 pub async fn dispatch_by_name(
789 &self,
790 registry: &crate::ToolRegistry,
791 name: &str,
792 ctx: &mut Context,
793 args: Args,
794 ) -> Result<Output> {
795 let tool = registry
796 .get(name)
797 .ok_or_else(|| CoreError::NotFound(format!("tool '{name}' not registered")))?;
798 self.dispatch(tool.as_ref(), ctx, args).await
799 }
800
801 #[must_use]
803 pub fn rate_limiter(&self) -> &RateLimiter {
804 &self.rate_limiter
805 }
806
807 #[must_use]
809 pub fn circuit_breakers(&self) -> &CircuitBreakerRegistry {
810 &self.circuit_breakers
811 }
812
813 #[must_use]
815 pub fn dharma_gate(&self) -> &DharmaGate {
816 &self.dharma_gate
817 }
818
819 #[must_use]
821 pub fn karma_ledger(&self) -> Option<&KarmaLedger> {
822 self.karma_ledger.as_deref()
823 }
824}
825
826impl Default for DispatchPipeline {
827 fn default() -> Self {
828 Self::with_defaults()
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use wm_core::{BrainWave, EffectRow, Gana, ToolStats};
836 use wm_governance::{ResourceRulesConfig, WriteAuditJournal};
837
838 struct TestTool {
839 name: String,
840 effects: EffectRow,
841 stats: ToolStats,
842 should_fail: bool,
843 output: Option<Output>,
844 store: Option<Arc<wm_memory::MemoryStore>>,
847 }
848
849 impl TestTool {
850 fn new(name: &str, effects: EffectRow) -> Self {
851 Self {
852 name: name.to_string(),
853 effects,
854 stats: ToolStats::default(),
855 should_fail: false,
856 output: None,
857 store: None,
858 }
859 }
860
861 fn with_output(mut self, output: Output) -> Self {
862 self.output = Some(output);
863 self
864 }
865
866 fn with_store(mut self, store: Arc<wm_memory::MemoryStore>) -> Self {
867 self.store = Some(store);
868 self
869 }
870
871 fn failing(name: &str) -> Self {
872 Self {
873 name: name.to_string(),
874 effects: EffectRow::pure(),
875 stats: ToolStats::default(),
876 should_fail: true,
877 output: None,
878 store: None,
879 }
880 }
881 }
882
883 #[async_trait]
884 impl Tool for TestTool {
885 fn name(&self) -> &str {
886 &self.name
887 }
888 fn gana(&self) -> Gana {
889 Gana::Heart
890 }
891 fn effects(&self) -> &EffectRow {
892 &self.effects
893 }
894 async fn call(&self, _ctx: &mut Context, _args: Args) -> Result<Output> {
895 if let Some(store) = &self.store {
896 let mem = wm_memory::Memory::new(
897 wm_core::Galaxy::Codex,
898 format!("misdeclared write from {}", self.name),
899 );
900 store.put(wm_core::Galaxy::Codex, &mem).ok();
901 }
902 if self.should_fail {
903 Err(CoreError::Tool(self.name.clone()))
904 } else {
905 Ok(self
906 .output
907 .clone()
908 .unwrap_or_else(|| serde_json::json!("ok")))
909 }
910 }
911 fn stats(&self) -> &ToolStats {
912 &self.stats
913 }
914 }
915
916 #[tokio::test]
917 async fn pipeline_dispatch_success() {
918 let pipeline = DispatchPipeline::with_defaults();
919 let mut ctx = Context::new(BrainWave::Gamma);
920 let tool = TestTool::new("test_tool", EffectRow::pure());
921
922 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
923 assert!(result.is_ok());
924 }
925
926 struct HangingTool {
927 effects: EffectRow,
928 stats: ToolStats,
929 }
930
931 impl HangingTool {
932 fn new() -> Self {
933 Self {
934 effects: EffectRow::pure(),
935 stats: ToolStats::default(),
936 }
937 }
938 }
939
940 #[async_trait]
941 impl Tool for HangingTool {
942 fn name(&self) -> &str {
943 "hanging_tool"
944 }
945 fn gana(&self) -> Gana {
946 Gana::Heart
947 }
948 fn effects(&self) -> &EffectRow {
949 &self.effects
950 }
951 async fn call(&self, _ctx: &mut Context, _args: Args) -> Result<Output> {
952 tokio::time::sleep(Duration::from_secs(30)).await;
953 Ok(serde_json::json!("never reached"))
954 }
955 fn stats(&self) -> &ToolStats {
956 &self.stats
957 }
958 }
959
960 #[tokio::test]
961 async fn pipeline_dispatch_timeout_bounds_hung_tool() {
962 let pipeline = DispatchPipeline::with_defaults()
963 .with_dispatch_timeout(Some(Duration::from_millis(50)));
964 let mut ctx = Context::new(BrainWave::Gamma);
965 let tool = HangingTool::new();
966
967 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
968 assert!(result.is_err());
969 let msg = result.err().unwrap().to_string();
970 assert!(
971 msg.contains("timed out"),
972 "expected timeout error, got: {msg}"
973 );
974 }
975
976 #[tokio::test]
977 async fn pipeline_dispatch_with_timeout_allows_fast_tool() {
978 let pipeline = DispatchPipeline::with_defaults()
979 .with_dispatch_timeout(Some(Duration::from_millis(500)));
980 let mut ctx = Context::new(BrainWave::Gamma);
981 let tool = TestTool::new("fast_tool", EffectRow::pure());
982
983 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
984 assert!(result.is_ok());
985 }
986
987 #[tokio::test]
988 async fn pipeline_dispatch_failure_records_stats() {
989 let pipeline = DispatchPipeline::with_defaults();
990 let mut ctx = Context::new(BrainWave::Gamma);
991 let tool = TestTool::failing("failing_tool");
992
993 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
994 assert!(result.is_err());
995 assert_eq!(
996 tool.stats()
997 .call_count
998 .load(std::sync::atomic::Ordering::Relaxed),
999 1
1000 );
1001 }
1002
1003 #[tokio::test]
1004 async fn pipeline_blocks_incompatible_brain_wave() {
1005 let pipeline = DispatchPipeline::with_defaults();
1006 let mut ctx = Context::new(BrainWave::Delta);
1007 let tool = TestTool::new("test_tool", EffectRow::pure());
1008
1009 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1010 assert!(result.is_err());
1011 match result {
1012 Err(CoreError::Governance(_)) => {}
1013 other => panic!("Expected Governance error, got {other:?}"),
1014 }
1015 }
1016
1017 #[tokio::test]
1018 async fn pipeline_dharma_blocks_destructive_in_strict_mode() {
1019 let pipeline = DispatchPipeline::with_defaults();
1020 let mut ctx = Context::new(BrainWave::Theta);
1021 let tool = TestTool::new(
1022 "destructive_tool",
1023 EffectRow {
1024 writes: vec![wm_core::Resource::Filesystem],
1025 ..Default::default()
1026 },
1027 );
1028
1029 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1030 assert!(result.is_err());
1031 match result {
1032 Err(CoreError::Governance(_)) => {}
1033 other => panic!("Expected Governance error, got {other:?}"),
1034 }
1035 }
1036
1037 #[tokio::test]
1038 async fn pipeline_rate_limit_blocks_excess() {
1039 let rate_limiter = Arc::new(RateLimiter::new(1000, 2, 0));
1040 let pipeline = DispatchPipeline::new(
1041 rate_limiter,
1042 Arc::new(CircuitBreakerRegistry::default()),
1043 Arc::new(DharmaGate::default()),
1044 None,
1045 );
1046
1047 let mut ctx = Context::new(BrainWave::Gamma);
1048 let tool = TestTool::new("limited_tool", EffectRow::pure());
1049
1050 assert!(
1051 pipeline
1052 .dispatch(&tool, &mut ctx, Args::default())
1053 .await
1054 .is_ok()
1055 );
1056 assert!(
1057 pipeline
1058 .dispatch(&tool, &mut ctx, Args::default())
1059 .await
1060 .is_ok()
1061 );
1062 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1063 assert!(result.is_err());
1064 match result {
1065 Err(CoreError::RateLimited(_)) => {}
1066 other => panic!("Expected RateLimited error, got {other:?}"),
1067 }
1068 }
1069
1070 #[tokio::test]
1071 async fn pipeline_circuit_breaker_opens_on_repeated_failures() {
1072 let breakers = Arc::new(CircuitBreakerRegistry::new(
1073 crate::circuit_breaker::BreakerConfig {
1074 failure_threshold: 3,
1075 window: std::time::Duration::from_secs(10),
1076 cooldown: std::time::Duration::from_secs(30),
1077 },
1078 ));
1079 let pipeline = DispatchPipeline::new(
1080 Arc::new(RateLimiter::new(10000, 100, 100)),
1081 breakers.clone(),
1082 Arc::new(DharmaGate::default()),
1083 None,
1084 );
1085
1086 let mut ctx = Context::new(BrainWave::Gamma);
1087 let tool = TestTool::failing("flaky_tool");
1088
1089 for _ in 0..3 {
1090 let _ = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1091 }
1092
1093 assert_eq!(
1094 breakers.state("flaky_tool"),
1095 crate::circuit_breaker::BreakerState::Open
1096 );
1097
1098 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1099 assert!(result.is_err());
1100 match result {
1101 Err(CoreError::CircuitBreaker(_)) => {}
1102 other => panic!("Expected CircuitBreaker error, got {other:?}"),
1103 }
1104 }
1105
1106 #[tokio::test]
1107 async fn pipeline_karma_ledger_records() {
1108 let tmp = tempfile::tempdir().unwrap();
1109 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1110 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
1111
1112 let pipeline = DispatchPipeline::new(
1113 Arc::new(RateLimiter::default()),
1114 Arc::new(CircuitBreakerRegistry::default()),
1115 Arc::new(DharmaGate::default()),
1116 Some(ledger.clone()),
1117 );
1118
1119 let mut ctx = Context::new(BrainWave::Gamma);
1120 let tool = TestTool::new("karma_test_tool", EffectRow::pure());
1121
1122 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1123 assert!(result.is_ok());
1124 assert_eq!(ledger.next_id(), 1);
1125 assert_eq!(ctx.karma_debt, 0.0);
1126 }
1127
1128 #[tokio::test]
1129 async fn pipeline_karma_debt_updates_context() {
1130 let tmp = tempfile::tempdir().unwrap();
1131 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1132 let ledger = Arc::new(KarmaLedger::new(store).unwrap());
1133
1134 let pipeline = DispatchPipeline::new(
1135 Arc::new(RateLimiter::default()),
1136 Arc::new(CircuitBreakerRegistry::default()),
1137 Arc::new(DharmaGate::default()),
1138 Some(ledger),
1139 );
1140
1141 let mut ctx = Context::new(BrainWave::Gamma);
1142 let tool = TestTool::new(
1143 "wasteful_tool",
1144 EffectRow {
1145 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1146 ..Default::default()
1147 },
1148 );
1149
1150 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1151 assert!(result.is_ok());
1152 assert!(
1153 (ctx.karma_debt - 0.2).abs() < 0.001,
1154 "Context karma_debt should be 0.2, got {}",
1155 ctx.karma_debt
1156 );
1157 }
1158
1159 #[tokio::test]
1160 async fn pipeline_karma_batched_e2e() {
1161 let tmp = tempfile::tempdir().unwrap();
1164 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1165 let ledger = Arc::new(KarmaLedger::with_flush_threshold(store.clone(), 100).unwrap());
1166
1167 let pipeline = DispatchPipeline::new(
1168 Arc::new(RateLimiter::default()),
1169 Arc::new(CircuitBreakerRegistry::default()),
1170 Arc::new(DharmaGate::default()),
1171 Some(ledger.clone()),
1172 );
1173
1174 let mut ctx = Context::new(BrainWave::Gamma);
1175
1176 let honest_tool = TestTool::new("honest_tool", EffectRow::pure());
1178 let wasteful_tool = TestTool::new(
1179 "wasteful_tool",
1180 EffectRow {
1181 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1182 ..Default::default()
1183 },
1184 );
1185
1186 for _ in 0..10 {
1187 let result = pipeline
1188 .dispatch(&honest_tool, &mut ctx, Args::default())
1189 .await;
1190 assert!(result.is_ok());
1191 }
1192 for _ in 0..10 {
1193 let result = pipeline
1194 .dispatch(&wasteful_tool, &mut ctx, Args::default())
1195 .await;
1196 assert!(result.is_ok());
1197 }
1198
1199 assert_eq!(ledger.next_id(), 20);
1201 assert_eq!(
1202 ledger.pending_count(),
1203 20,
1204 "All 20 entries should be pending before flush"
1205 );
1206
1207 let debt = ledger.total_debt();
1209 assert!(
1210 (debt - 2.0).abs() < 0.001,
1211 "Total debt should be 2.0 (10 x 0.2), got {debt}"
1212 );
1213
1214 ledger.flush().unwrap();
1216 assert_eq!(ledger.pending_count(), 0);
1217
1218 let result = ledger.verify_integrity().unwrap();
1220 assert!(
1221 result.valid,
1222 "Chain should be valid after batched flush: {:?}",
1223 result.violation
1224 );
1225 assert_eq!(result.entries_verified, 20);
1226
1227 let ledger2 = KarmaLedger::new(store).unwrap();
1229 assert_eq!(
1230 ledger2.next_id(),
1231 20,
1232 "Next ID should persist across instances"
1233 );
1234 let entries = ledger2.scan_entries().unwrap();
1235 assert_eq!(
1236 entries.len(),
1237 20,
1238 "All 20 entries should be persisted in LMDB"
1239 );
1240
1241 let debt2 = ledger2.total_debt();
1243 assert!(
1244 (debt2 - 2.0).abs() < 0.001,
1245 "Total debt should persist as 2.0, got {debt2}"
1246 );
1247
1248 let result2 = ledger2.verify_integrity().unwrap();
1250 assert!(result2.valid, "Chain should be valid on reloaded ledger");
1251 assert_eq!(result2.entries_verified, 20);
1252 }
1253
1254 #[tokio::test]
1255 async fn pipeline_coherence_gate_blocks_writes() {
1256 let pipeline = DispatchPipeline::with_defaults();
1257 let mut ctx = Context::new(BrainWave::Gamma);
1258 ctx.citta_coherence = 0.1; let tool = TestTool::new(
1260 "write_tool",
1261 EffectRow {
1262 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1263 ..Default::default()
1264 },
1265 );
1266
1267 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1268 assert!(result.is_err());
1269 match result {
1270 Err(CoreError::Governance(msg)) => {
1271 assert!(msg.contains("coherence"));
1272 }
1273 other => panic!("Expected Governance error, got {other:?}"),
1274 }
1275 }
1276
1277 #[tokio::test]
1278 async fn pipeline_coherence_gate_allows_reads() {
1279 let pipeline = DispatchPipeline::with_defaults();
1280 let mut ctx = Context::new(BrainWave::Gamma);
1281 ctx.citta_coherence = 0.1; let tool = TestTool::new("read_tool", EffectRow::pure());
1283
1284 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1285 assert!(result.is_ok());
1286 }
1287
1288 #[tokio::test]
1289 async fn pipeline_coherence_gate_allows_writes_when_coherent() {
1290 let pipeline = DispatchPipeline::with_defaults();
1291 let mut ctx = Context::new(BrainWave::Gamma);
1292 ctx.citta_coherence = 0.5; let tool = TestTool::new(
1294 "write_tool",
1295 EffectRow {
1296 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1297 ..Default::default()
1298 },
1299 );
1300
1301 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1302 assert!(result.is_ok());
1303 }
1304
1305 #[tokio::test]
1306 async fn pipeline_low_confidence_blocks_writes() {
1307 let pipeline = DispatchPipeline::with_defaults();
1308 let mut ctx = Context::new(BrainWave::Gamma);
1309 ctx.self_model_confidence = 0.3; let tool = TestTool::new(
1311 "write_tool",
1312 EffectRow {
1313 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1314 ..Default::default()
1315 },
1316 );
1317
1318 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1319 assert!(result.is_err());
1320 match result {
1321 Err(CoreError::Governance(msg)) => {
1322 assert!(msg.contains("confidence"));
1323 assert!(msg.contains("conservative"));
1324 }
1325 other => panic!("Expected Governance error, got {other:?}"),
1326 }
1327 }
1328
1329 #[tokio::test]
1330 async fn pipeline_low_confidence_allows_reads() {
1331 let pipeline = DispatchPipeline::with_defaults();
1332 let mut ctx = Context::new(BrainWave::Gamma);
1333 ctx.self_model_confidence = 0.3; let tool = TestTool::new("read_tool", EffectRow::pure());
1335
1336 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1337 assert!(result.is_ok());
1338 }
1339
1340 #[tokio::test]
1341 async fn pipeline_high_confidence_allows_writes() {
1342 let pipeline = DispatchPipeline::with_defaults();
1343 let mut ctx = Context::new(BrainWave::Gamma);
1344 ctx.self_model_confidence = 0.8; let tool = TestTool::new(
1346 "write_tool",
1347 EffectRow {
1348 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1349 ..Default::default()
1350 },
1351 );
1352
1353 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1354 assert!(result.is_ok());
1355 }
1356
1357 #[tokio::test]
1358 async fn pipeline_high_caution_warns_on_writes() {
1359 let pipeline = DispatchPipeline::with_defaults();
1360 let mut ctx = Context::new(BrainWave::Gamma);
1361 ctx.drive_caution = 0.9; let tool = TestTool::new(
1363 "write_tool",
1364 EffectRow {
1365 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1366 ..Default::default()
1367 },
1368 );
1369
1370 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1372 assert!(result.is_ok());
1373 }
1374
1375 #[tokio::test]
1376 async fn pipeline_low_energy_warns_on_writes() {
1377 let pipeline = DispatchPipeline::with_defaults();
1378 let mut ctx = Context::new(BrainWave::Gamma);
1379 ctx.drive_energy = 0.1; let tool = TestTool::new(
1381 "write_tool",
1382 EffectRow {
1383 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1384 ..Default::default()
1385 },
1386 );
1387
1388 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1390 assert!(result.is_ok());
1391 }
1392
1393 #[tokio::test]
1394 async fn pipeline_drive_gates_dont_affect_reads() {
1395 let pipeline = DispatchPipeline::with_defaults();
1396 let mut ctx = Context::new(BrainWave::Gamma);
1397 ctx.drive_caution = 0.95;
1398 ctx.drive_energy = 0.05;
1399 let tool = TestTool::new("read_tool", EffectRow::pure());
1400
1401 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1402 assert!(result.is_ok());
1403 }
1404
1405 #[tokio::test]
1406 async fn pipeline_destructive_blocked_without_confirm() {
1407 let pipeline = DispatchPipeline::with_defaults();
1408 let mut ctx = Context::new(BrainWave::Gamma);
1409 let tool = TestTool::new(
1410 "destructive_tool",
1411 EffectRow {
1412 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1413 destructive: true,
1414 ..Default::default()
1415 },
1416 );
1417
1418 let result = pipeline
1419 .dispatch(&tool, &mut ctx, serde_json::json!({}))
1420 .await;
1421 assert!(result.is_err());
1422 match result {
1423 Err(CoreError::Governance(msg)) => {
1424 assert!(msg.contains("destructive"));
1425 assert!(msg.contains("confirm"));
1426 }
1427 other => panic!("Expected Governance error, got {other:?}"),
1428 }
1429 }
1430
1431 #[tokio::test]
1432 async fn pipeline_destructive_allowed_with_confirm() {
1433 let pipeline = DispatchPipeline::with_defaults();
1434 let mut ctx = Context::new(BrainWave::Gamma);
1435 let tool = TestTool::new(
1436 "destructive_tool",
1437 EffectRow {
1438 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1439 destructive: true,
1440 ..Default::default()
1441 },
1442 );
1443
1444 let result = pipeline
1445 .dispatch(&tool, &mut ctx, serde_json::json!({"confirm": true}))
1446 .await;
1447 assert!(result.is_ok());
1448 }
1449
1450 #[tokio::test]
1451 async fn pipeline_destructive_blocked_with_false_confirm() {
1452 let pipeline = DispatchPipeline::with_defaults();
1453 let mut ctx = Context::new(BrainWave::Gamma);
1454 let tool = TestTool::new(
1455 "destructive_tool",
1456 EffectRow {
1457 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1458 destructive: true,
1459 ..Default::default()
1460 },
1461 );
1462
1463 let result = pipeline
1464 .dispatch(&tool, &mut ctx, serde_json::json!({"confirm": false}))
1465 .await;
1466 assert!(result.is_err());
1467 }
1468
1469 #[tokio::test]
1470 async fn pipeline_compartment_no_restriction_allows_all() {
1471 let pipeline = DispatchPipeline::with_defaults();
1472 let mut ctx = Context::new(BrainWave::Gamma);
1473 let tool = TestTool::new(
1475 "write_tool",
1476 EffectRow {
1477 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1478 ..Default::default()
1479 },
1480 );
1481
1482 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1483 assert!(result.is_ok());
1484 }
1485
1486 #[tokio::test]
1487 async fn pipeline_compartment_sandbox_blocks_write_to_codex() {
1488 let pipeline = DispatchPipeline::with_defaults();
1489 let mut ctx = Context::new(BrainWave::Gamma);
1490 ctx.compartment = Some("sandbox".into());
1491 let tool = TestTool::new(
1492 "write_tool",
1493 EffectRow {
1494 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1495 ..Default::default()
1496 },
1497 );
1498
1499 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1500 assert!(result.is_err());
1501 match result {
1502 Err(CoreError::Governance(msg)) => {
1503 assert!(msg.contains("sandbox"));
1504 assert!(msg.contains("codex"));
1505 }
1506 other => panic!("Expected Governance error, got {other:?}"),
1507 }
1508 }
1509
1510 #[tokio::test]
1511 async fn pipeline_compartment_sandbox_blocks_read_from_karma() {
1512 let pipeline = DispatchPipeline::with_defaults();
1513 let mut ctx = Context::new(BrainWave::Gamma);
1514 ctx.compartment = Some("sandbox".into());
1515 let tool = TestTool::new(
1516 "read_tool",
1517 EffectRow::read_only(vec![wm_core::Resource::Galaxy("karma".into())]),
1518 );
1519
1520 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1521 assert!(result.is_err());
1522 match result {
1523 Err(CoreError::Governance(msg)) => {
1524 assert!(msg.contains("sandbox"));
1525 assert!(msg.contains("karma"));
1526 }
1527 other => panic!("Expected Governance error, got {other:?}"),
1528 }
1529 }
1530
1531 #[tokio::test]
1532 async fn pipeline_compartment_sandbox_allows_write_to_tutorial() {
1533 let pipeline = DispatchPipeline::with_defaults();
1534 let mut ctx = Context::new(BrainWave::Gamma);
1535 ctx.compartment = Some("sandbox".into());
1536 let tool = TestTool::new(
1537 "write_tool",
1538 EffectRow {
1539 writes: vec![wm_core::Resource::Galaxy("tutorial".into())],
1540 ..Default::default()
1541 },
1542 );
1543
1544 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1545 assert!(result.is_ok());
1546 }
1547
1548 #[tokio::test]
1549 async fn pipeline_compartment_sandbox_allows_read_from_research() {
1550 let pipeline = DispatchPipeline::with_defaults();
1551 let mut ctx = Context::new(BrainWave::Gamma);
1552 ctx.compartment = Some("sandbox".into());
1553 let tool = TestTool::new(
1554 "read_tool",
1555 EffectRow::read_only(vec![wm_core::Resource::Galaxy("research".into())]),
1556 );
1557
1558 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1559 assert!(result.is_ok());
1560 }
1561
1562 #[tokio::test]
1563 async fn pipeline_compartment_production_blocks_read_from_karma() {
1564 let pipeline = DispatchPipeline::with_defaults();
1565 let mut ctx = Context::new(BrainWave::Gamma);
1566 ctx.compartment = Some("production".into());
1567 let tool = TestTool::new(
1568 "read_tool",
1569 EffectRow::read_only(vec![wm_core::Resource::Galaxy("karma".into())]),
1570 );
1571
1572 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1573 assert!(result.is_err());
1574 match result {
1575 Err(CoreError::Governance(msg)) => {
1576 assert!(msg.contains("production"));
1577 assert!(msg.contains("karma"));
1578 }
1579 other => panic!("Expected Governance error, got {other:?}"),
1580 }
1581 }
1582
1583 #[tokio::test]
1584 async fn pipeline_compartment_production_allows_write_to_codex() {
1585 let pipeline = DispatchPipeline::with_defaults();
1586 let mut ctx = Context::new(BrainWave::Gamma);
1587 ctx.compartment = Some("production".into());
1588 let tool = TestTool::new(
1589 "write_tool",
1590 EffectRow {
1591 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1592 ..Default::default()
1593 },
1594 );
1595
1596 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1597 assert!(result.is_ok());
1598 }
1599
1600 #[tokio::test]
1601 async fn pipeline_compartment_secure_allows_write_to_codex() {
1602 let pipeline = DispatchPipeline::with_defaults();
1603 let mut ctx = Context::new(BrainWave::Gamma);
1604 ctx.compartment = Some("secure".into());
1605 let tool = TestTool::new(
1606 "write_tool",
1607 EffectRow {
1608 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1609 ..Default::default()
1610 },
1611 );
1612
1613 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1614 assert!(result.is_ok());
1615 }
1616
1617 #[tokio::test]
1618 async fn pipeline_compartment_secure_blocks_read_from_karma() {
1619 let pipeline = DispatchPipeline::with_defaults();
1620 let mut ctx = Context::new(BrainWave::Gamma);
1621 ctx.compartment = Some("secure".into());
1622 let tool = TestTool::new(
1623 "read_tool",
1624 EffectRow::read_only(vec![wm_core::Resource::Galaxy("karma".into())]),
1625 );
1626
1627 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1628 assert!(result.is_err());
1629 match result {
1630 Err(CoreError::Governance(msg)) => {
1631 assert!(msg.contains("secure"));
1632 assert!(msg.contains("karma"));
1633 }
1634 other => panic!("Expected Governance error, got {other:?}"),
1635 }
1636 }
1637
1638 fn rules_with(max_writes: u32, max_repeats: u32) -> Arc<ResourceRules> {
1641 Arc::new(ResourceRules::new(ResourceRulesConfig {
1642 max_writes_per_minute: max_writes,
1643 max_spawns_per_minute: 100,
1644 max_network_per_minute: 100,
1645 novelty_window: 50,
1646 max_repeats,
1647 require_human_review: false,
1648 }))
1649 }
1650
1651 #[tokio::test]
1652 async fn pipeline_resource_rules_budget_exceeding_write_refused() {
1653 let pipeline = DispatchPipeline::with_defaults().with_resource_rules(rules_with(2, 1000));
1654 let mut ctx = Context::new(BrainWave::Gamma);
1655 let tool = TestTool::new(
1656 "write_tool",
1657 EffectRow {
1658 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1659 ..Default::default()
1660 },
1661 );
1662
1663 assert!(
1664 pipeline
1665 .dispatch(&tool, &mut ctx, Args::default())
1666 .await
1667 .is_ok(),
1668 "first write within budget"
1669 );
1670 assert!(
1671 pipeline
1672 .dispatch(&tool, &mut ctx, Args::default())
1673 .await
1674 .is_ok(),
1675 "second write within budget"
1676 );
1677 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1678 assert!(result.is_err(), "third write must exceed the budget");
1679 match result {
1680 Err(CoreError::Governance(msg)) => {
1681 assert!(msg.contains("resource rules"), "got: {msg}");
1682 assert!(msg.contains("writes"), "got: {msg}");
1683 }
1684 other => panic!("Expected Governance error, got {other:?}"),
1685 }
1686 }
1687
1688 #[tokio::test]
1689 async fn pipeline_resource_rules_novelty_flag_reaches_response() {
1690 let pipeline = DispatchPipeline::with_defaults().with_resource_rules(rules_with(1000, 1));
1691 let mut ctx = Context::new(BrainWave::Gamma);
1692 let tool = TestTool::new("read_tool", EffectRow::pure())
1693 .with_output(serde_json::json!({"status": "ok"}));
1694
1695 let first = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1696 assert!(first.is_ok());
1697 assert!(
1698 first.unwrap().get("resource_flags").is_none(),
1699 "first call is novel — no flag"
1700 );
1701
1702 let second = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1703 let output = second.expect("repeated call must still succeed (flag, not block)");
1704 let flags = output
1705 .get("resource_flags")
1706 .and_then(|f| f.as_array())
1707 .expect("novelty flag must reach the response");
1708 assert_eq!(flags.len(), 1);
1709 assert!(flags[0].as_str().unwrap().contains("not novel"));
1710 }
1711
1712 #[tokio::test]
1713 async fn pipeline_resource_rules_blocks_unapproved_autonomous() {
1714 let rules = Arc::new(ResourceRules::default());
1715 rules.set_user_initiated(false);
1716 let pipeline = DispatchPipeline::with_defaults().with_resource_rules(rules);
1717 let mut ctx = Context::new(BrainWave::Gamma);
1718 let tool = TestTool::new(
1719 "memory.consolidate",
1720 EffectRow {
1721 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1722 ..Default::default()
1723 },
1724 );
1725
1726 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1727 assert!(result.is_err());
1728 match result {
1729 Err(CoreError::Governance(msg)) => {
1730 assert!(msg.contains("human review"), "got: {msg}");
1731 }
1732 other => panic!("Expected Governance error, got {other:?}"),
1733 }
1734 }
1735
1736 #[tokio::test]
1737 async fn pipeline_resource_rules_allows_approved_autonomous() {
1738 let rules = Arc::new(ResourceRules::default());
1739 rules.set_user_initiated(false);
1740 rules.set_human_approved(true);
1741 let pipeline = DispatchPipeline::with_defaults().with_resource_rules(rules);
1742 let mut ctx = Context::new(BrainWave::Gamma);
1743 let tool = TestTool::new(
1744 "memory.consolidate",
1745 EffectRow {
1746 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1747 ..Default::default()
1748 },
1749 );
1750
1751 let result = pipeline
1752 .dispatch(
1753 &tool,
1754 &mut ctx,
1755 serde_json::json!({"purpose": "consolidate codex"}),
1756 )
1757 .await;
1758 assert!(result.is_ok());
1759 }
1760
1761 #[tokio::test]
1762 async fn pipeline_resource_rules_user_initiated_writes_allowed_by_default() {
1763 let pipeline = DispatchPipeline::with_defaults()
1765 .with_resource_rules(Arc::new(ResourceRules::default()));
1766 let mut ctx = Context::new(BrainWave::Gamma);
1767 let tool = TestTool::new(
1768 "write_tool",
1769 EffectRow {
1770 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1771 ..Default::default()
1772 },
1773 );
1774
1775 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1776 assert!(result.is_ok());
1777 }
1778
1779 #[tokio::test]
1782 async fn pipeline_runtime_satya_blocks_citta_write_without_read() {
1783 let pipeline = DispatchPipeline::with_defaults();
1784 let mut ctx = Context::new(BrainWave::Gamma);
1785 let tool = TestTool::new(
1786 "memory.create",
1787 EffectRow {
1788 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1789 ..Default::default()
1790 },
1791 );
1792
1793 let result = pipeline
1794 .dispatch(&tool, &mut ctx, serde_json::json!({"galaxy": "citta"}))
1795 .await;
1796 assert!(result.is_err());
1797 match result {
1798 Err(CoreError::Governance(msg)) => {
1799 assert!(msg.contains("VIOLATION_SATYA"), "got: {msg}");
1800 }
1801 other => panic!("Expected Governance error, got {other:?}"),
1802 }
1803 }
1804
1805 #[tokio::test]
1806 async fn pipeline_runtime_satya_allows_citta_write_with_read_evidence() {
1807 let pipeline = DispatchPipeline::with_defaults();
1808 let mut ctx = Context::new(BrainWave::Gamma);
1809 let tool = TestTool::new(
1810 "consolidate_tool",
1811 EffectRow {
1812 reads: vec![wm_core::Resource::Galaxy("citta".into())],
1813 writes: vec![wm_core::Resource::Galaxy("citta".into())],
1814 ..Default::default()
1815 },
1816 );
1817
1818 let result = pipeline
1819 .dispatch(&tool, &mut ctx, serde_json::json!({"galaxy": "citta"}))
1820 .await;
1821 assert!(result.is_ok());
1822 }
1823
1824 #[tokio::test]
1825 async fn pipeline_runtime_satya_allows_non_citta_runtime_galaxy() {
1826 let pipeline = DispatchPipeline::with_defaults();
1827 let mut ctx = Context::new(BrainWave::Gamma);
1828 let tool = TestTool::new(
1829 "memory.create",
1830 EffectRow {
1831 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1832 ..Default::default()
1833 },
1834 );
1835
1836 let result = pipeline
1837 .dispatch(&tool, &mut ctx, serde_json::json!({"galaxy": "research"}))
1838 .await;
1839 assert!(result.is_ok());
1840 }
1841
1842 #[tokio::test]
1845 async fn pipeline_write_audit_detects_misdeclaring_tool() {
1846 let tmp = tempfile::tempdir().unwrap();
1847 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1848 let journal = Arc::new(WriteAuditJournal::with_flush_threshold(store.clone(), 0).unwrap());
1849 let pipeline = DispatchPipeline::with_defaults().with_write_audit(journal.clone());
1850 let mut ctx = Context::new(BrainWave::Gamma);
1851
1852 let tool = TestTool::new("sneaky_tool", EffectRow::pure()).with_store(store);
1854
1855 let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
1856 assert!(result.is_ok());
1857
1858 let mis = journal.misdeclarations().unwrap();
1859 assert!(!mis.is_empty(), "misdeclaring tool must be detected");
1860 assert_eq!(mis.last().unwrap().tool, "sneaky_tool");
1861 assert!(mis.last().unwrap().undeclared_mutation());
1862 }
1863
1864 #[tokio::test]
1865 async fn pipeline_write_audit_records_declared_writes_with_identity() {
1866 let tmp = tempfile::tempdir().unwrap();
1867 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1868 let journal = Arc::new(WriteAuditJournal::with_flush_threshold(store.clone(), 0).unwrap());
1869 let pipeline = DispatchPipeline::with_defaults().with_write_audit(journal.clone());
1870 let mut ctx = Context::new(BrainWave::Gamma);
1871
1872 let tool = TestTool::new(
1873 "honest_tool",
1874 EffectRow {
1875 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1876 ..Default::default()
1877 },
1878 )
1879 .with_store(store);
1880
1881 let args = serde_json::json!({"id": "abc-123", "content_hash": "hash-xyz"});
1882 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
1883 assert!(result.is_ok());
1884
1885 let entries = journal.scan_entries().unwrap();
1886 assert_eq!(entries.len(), 1);
1887 let entry = &entries[0];
1888 assert!(entry.declared_writes);
1889 assert!(entry.store_write_delta >= 1);
1890 assert_eq!(entry.memory_id.as_deref(), Some("abc-123"));
1891 assert_eq!(entry.content_hash.as_deref(), Some("hash-xyz"));
1892 assert!(journal.misdeclarations().unwrap().is_empty());
1893 }
1894
1895 #[tokio::test]
1896 async fn pipeline_write_audit_captures_actor_identity() {
1897 let tmp = tempfile::tempdir().unwrap();
1900 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1901 let journal = Arc::new(WriteAuditJournal::with_flush_threshold(store.clone(), 0).unwrap());
1902 let pipeline = DispatchPipeline::with_defaults().with_write_audit(journal.clone());
1903 let mut ctx = Context::new(BrainWave::Gamma);
1904 ctx.session_id = Some(uuid::Uuid::nil());
1905 ctx.user_id = Some("agent-b".to_string());
1906 ctx.compartment = Some("production".to_string());
1907
1908 let tool = TestTool::new(
1909 "honest_tool",
1910 EffectRow {
1911 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1912 ..Default::default()
1913 },
1914 )
1915 .with_store(store);
1916
1917 let result = pipeline
1918 .dispatch(&tool, &mut ctx, serde_json::json!({"id": "abc-123"}))
1919 .await;
1920 assert!(result.is_ok());
1921
1922 let entries = journal.scan_entries().unwrap();
1923 assert_eq!(entries.len(), 1);
1924 let entry = &entries[0];
1925 assert_eq!(
1926 entry.actor_session.as_deref(),
1927 Some(uuid::Uuid::nil().to_string().as_str())
1928 );
1929 assert_eq!(entry.actor_user.as_deref(), Some("agent-b"));
1930 assert_eq!(entry.actor_compartment.as_deref(), Some("production"));
1931 }
1932
1933 #[tokio::test]
1934 async fn pipeline_write_audit_read_dispatch_not_flagged_after_external_writes() {
1935 let tmp = tempfile::tempdir().unwrap();
1940 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
1941 let journal = Arc::new(WriteAuditJournal::with_flush_threshold(store.clone(), 0).unwrap());
1942 let pipeline = DispatchPipeline::with_defaults().with_write_audit(journal.clone());
1943 let mut ctx = Context::new(BrainWave::Gamma);
1944
1945 for i in 0..3 {
1947 let mem = wm_memory::Memory::new(wm_core::Galaxy::Codex, format!("other session {i}"));
1948 store.put(wm_core::Galaxy::Codex, &mem).unwrap();
1949 }
1950
1951 let read_tool = TestTool::new("memory.search", EffectRow::pure());
1952 let result = pipeline
1953 .dispatch(&read_tool, &mut ctx, Args::default())
1954 .await;
1955 assert!(result.is_ok());
1956
1957 let mis = journal.misdeclarations().unwrap();
1958 assert!(
1959 mis.is_empty(),
1960 "read-only dispatch must not inherit the other session's writes: {mis:?}"
1961 );
1962 let entries = journal.scan_entries().unwrap();
1963 assert_eq!(entries.last().unwrap().store_write_delta, 0);
1964 }
1965
1966 #[tokio::test]
1969 async fn pipeline_firebreak_forbidden_blocks_even_with_confirm() {
1970 let pipeline = DispatchPipeline::with_defaults();
1971 let mut ctx = Context::new(BrainWave::Gamma);
1972 let tool = TestTool::new(
1973 "destructive_tool",
1974 EffectRow {
1975 writes: vec![wm_core::Resource::Galaxy("codex".into())],
1976 destructive: true,
1977 ..Default::default()
1978 },
1979 );
1980
1981 let result = pipeline
1982 .dispatch(
1983 &tool,
1984 &mut ctx,
1985 serde_json::json!({"confirm": true, "cmd": "rm -rf /"}),
1986 )
1987 .await;
1988 match result {
1989 Err(CoreError::Governance(msg)) => {
1990 assert!(msg.contains("FORBIDDEN"), "got: {msg}");
1991 assert!(msg.contains("never allowed"), "got: {msg}");
1992 }
1993 other => panic!("Expected Governance error, got {other:?}"),
1994 }
1995 }
1996
1997 #[tokio::test]
1998 async fn pipeline_firebreak_scope_law_blocks_unscoped_destructive() {
1999 let pipeline = DispatchPipeline::with_defaults();
2000 let mut ctx = Context::new(BrainWave::Gamma);
2001 let tool = TestTool::new(
2003 "memory.delete",
2004 EffectRow {
2005 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2006 destructive: true,
2007 ..Default::default()
2008 },
2009 );
2010
2011 let result = pipeline
2012 .dispatch(&tool, &mut ctx, serde_json::json!({"confirm": true}))
2013 .await;
2014 match result {
2015 Err(CoreError::Governance(msg)) => {
2016 assert!(msg.contains("no explicit scope"), "got: {msg}");
2017 assert!(msg.contains("id"), "names the scope field: {msg}");
2018 }
2019 other => panic!("Expected Governance error, got {other:?}"),
2020 }
2021 }
2022
2023 #[tokio::test]
2024 async fn pipeline_firebreak_scope_law_allows_scoped_destructive() {
2025 let pipeline = DispatchPipeline::with_defaults();
2026 let mut ctx = Context::new(BrainWave::Gamma);
2027 let tool = TestTool::new(
2028 "memory.delete",
2029 EffectRow {
2030 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2031 destructive: true,
2032 ..Default::default()
2033 },
2034 );
2035
2036 let result = pipeline
2037 .dispatch(
2038 &tool,
2039 &mut ctx,
2040 serde_json::json!({"confirm": true, "id": "0f0e0d0c-0000-0000-0000-000000000000"}),
2041 )
2042 .await;
2043 assert!(result.is_ok());
2044 }
2045
2046 #[tokio::test]
2047 async fn pipeline_firebreak_caution_disclosed_in_response() {
2048 let pipeline = DispatchPipeline::with_defaults();
2049 let mut ctx = Context::new(BrainWave::Gamma);
2050 let tool = TestTool::new(
2051 "galaxy.transfer",
2052 EffectRow {
2053 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2054 destructive: true,
2055 ..Default::default()
2056 },
2057 )
2058 .with_output(serde_json::json!({"status": "success"}));
2059
2060 let result = pipeline
2061 .dispatch(
2062 &tool,
2063 &mut ctx,
2064 serde_json::json!({"confirm": true, "from_galaxy": "codex", "note": "mv old new"}),
2065 )
2066 .await;
2067 let output = result.expect("caution must not block");
2068 let advisories = output
2069 .get("firebreak")
2070 .and_then(|f| f.get("advisories"))
2071 .and_then(|a| a.as_array())
2072 .expect("advisories must reach the response");
2073 assert_eq!(advisories.len(), 1);
2074 }
2075
2076 #[tokio::test]
2077 async fn pipeline_firebreak_dangerous_escalates_off_confirm_gate() {
2078 let pipeline = DispatchPipeline::with_defaults();
2082 let mut ctx = Context::new(BrainWave::Gamma);
2083 let tool = TestTool::new(
2084 "spawn_tool",
2085 EffectRow {
2086 spawns: true,
2087 ..Default::default()
2088 },
2089 );
2090
2091 let blocked = pipeline
2092 .dispatch(
2093 &tool,
2094 &mut ctx,
2095 serde_json::json!({"cmd": "sudo rm -r /tmp/build"}),
2096 )
2097 .await;
2098 match blocked {
2099 Err(CoreError::Governance(msg)) => {
2100 assert!(msg.contains("dangerous"), "got: {msg}");
2101 assert!(msg.contains("confirm"), "got: {msg}");
2102 }
2103 other => panic!("Expected Governance error, got {other:?}"),
2104 }
2105
2106 let allowed = pipeline
2107 .dispatch(
2108 &tool,
2109 &mut ctx,
2110 serde_json::json!({"cmd": "sudo rm -r /tmp/build", "confirm": true}),
2111 )
2112 .await;
2113 assert!(allowed.is_ok());
2114 }
2115
2116 #[tokio::test]
2117 async fn pipeline_firebreak_never_scans_prose() {
2118 let pipeline = DispatchPipeline::with_defaults();
2121 let mut ctx = Context::new(BrainWave::Gamma);
2122 let tool = TestTool::new(
2123 "memory.create",
2124 EffectRow {
2125 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2126 ..Default::default()
2127 },
2128 );
2129
2130 let result = pipeline
2131 .dispatch(
2132 &tool,
2133 &mut ctx,
2134 serde_json::json!({"content": "incident: operator ran rm -rf / on the store"}),
2135 )
2136 .await;
2137 assert!(result.is_ok(), "prose is never vetoed");
2138 }
2139
2140 #[tokio::test]
2141 async fn pipeline_firebreak_disarmable_per_pipeline() {
2142 let pipeline = DispatchPipeline::with_defaults()
2143 .with_firebreak_option(None::<Arc<wm_governance::Firebreak>>);
2144 let mut ctx = Context::new(BrainWave::Gamma);
2145 let tool = TestTool::new(
2146 "destructive_tool",
2147 EffectRow {
2148 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2149 destructive: true,
2150 ..Default::default()
2151 },
2152 );
2153
2154 let result = pipeline
2156 .dispatch(&tool, &mut ctx, serde_json::json!({}))
2157 .await;
2158 assert!(result.is_err());
2159
2160 let result = pipeline
2162 .dispatch(
2163 &tool,
2164 &mut ctx,
2165 serde_json::json!({"confirm": true, "cmd": "rm -rf /"}),
2166 )
2167 .await;
2168 assert!(result.is_ok(), "disarmed pipeline must not veto");
2169 }
2170
2171 #[tokio::test]
2172 async fn pipeline_write_audit_records_destructive_confirm() {
2173 let tmp = tempfile::tempdir().unwrap();
2176 let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
2177 let journal = Arc::new(WriteAuditJournal::with_flush_threshold(store.clone(), 0).unwrap());
2178 let pipeline = DispatchPipeline::with_defaults().with_write_audit(journal.clone());
2179 let mut ctx = Context::new(BrainWave::Gamma);
2180
2181 let tool = TestTool::new(
2182 "memory.delete",
2183 EffectRow {
2184 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2185 destructive: true,
2186 ..Default::default()
2187 },
2188 )
2189 .with_store(store);
2190
2191 let result = pipeline
2192 .dispatch(
2193 &tool,
2194 &mut ctx,
2195 serde_json::json!({"confirm": true, "id": "abc-123"}),
2196 )
2197 .await;
2198 assert!(result.is_ok());
2199
2200 let entries = journal.scan_entries().unwrap();
2201 assert_eq!(entries.len(), 1);
2202 assert_eq!(
2203 entries[0].confirmed,
2204 Some(true),
2205 "destructive entry must record the confirm"
2206 );
2207 }
2208
2209 #[tokio::test]
2212 async fn pipeline_compartment_production_blocks_runtime_galaxy_write_bypass() {
2213 let pipeline = DispatchPipeline::with_defaults();
2216 let mut ctx = Context::new(BrainWave::Gamma);
2217 ctx.compartment = Some("production".into());
2218 let tool = TestTool::new(
2219 "memory_update",
2220 EffectRow {
2221 writes: vec![wm_core::Resource::Galaxy("codex".into())],
2222 ..Default::default()
2223 },
2224 );
2225
2226 let args = serde_json::json!({"galaxy": "karma"});
2227 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2228 assert!(result.is_err());
2229 match result {
2230 Err(CoreError::Governance(msg)) => {
2231 assert!(msg.contains("production"));
2232 assert!(msg.contains("karma"));
2233 assert!(msg.contains("runtime"));
2234 }
2235 other => panic!("Expected Governance error, got {other:?}"),
2236 }
2237 }
2238
2239 #[tokio::test]
2240 async fn pipeline_compartment_production_blocks_runtime_galaxy_read_bypass() {
2241 let pipeline = DispatchPipeline::with_defaults();
2244 let mut ctx = Context::new(BrainWave::Gamma);
2245 ctx.compartment = Some("production".into());
2246 let tool = TestTool::new(
2247 "memory_read",
2248 EffectRow::read_only(vec![wm_core::Resource::Galaxy("codex".into())]),
2249 );
2250
2251 let args = serde_json::json!({"galaxy": "karma"});
2252 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2253 assert!(result.is_err());
2254 match result {
2255 Err(CoreError::Governance(msg)) => {
2256 assert!(msg.contains("production"));
2257 assert!(msg.contains("karma"));
2258 assert!(msg.contains("runtime"));
2259 }
2260 other => panic!("Expected Governance error, got {other:?}"),
2261 }
2262 }
2263
2264 #[tokio::test]
2265 async fn pipeline_compartment_production_allows_runtime_galaxy_same_as_declared() {
2266 let pipeline = DispatchPipeline::with_defaults();
2269 let mut ctx = Context::new(BrainWave::Gamma);
2270 ctx.compartment = Some("production".into());
2271 let tool = TestTool::new(
2272 "memory_read",
2273 EffectRow::read_only(vec![wm_core::Resource::Galaxy("codex".into())]),
2274 );
2275
2276 let args = serde_json::json!({"galaxy": "codex"});
2277 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2278 assert!(result.is_ok());
2279 }
2280
2281 #[tokio::test]
2282 async fn pipeline_compartment_no_restriction_allows_runtime_galaxy() {
2283 let pipeline = DispatchPipeline::with_defaults();
2285 let mut ctx = Context::new(BrainWave::Gamma);
2286 let tool = TestTool::new(
2287 "memory_read",
2288 EffectRow::read_only(vec![wm_core::Resource::Galaxy("codex".into())]),
2289 );
2290
2291 let args = serde_json::json!({"galaxy": "karma"});
2292 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2293 assert!(result.is_ok());
2294 }
2295
2296 #[tokio::test]
2297 async fn pipeline_compartment_production_allows_runtime_memory_galaxy() {
2298 let pipeline = DispatchPipeline::with_defaults();
2301 let mut ctx = Context::new(BrainWave::Gamma);
2302 ctx.compartment = Some("production".into());
2303 let tool = TestTool::new(
2304 "memory_read",
2305 EffectRow::read_only(vec![wm_core::Resource::Galaxy("codex".into())]),
2306 );
2307
2308 let args = serde_json::json!({"galaxy": "research"});
2309 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2310 assert!(result.is_ok());
2311 }
2312
2313 #[tokio::test]
2314 async fn pipeline_compartment_production_blocks_runtime_system_galaxy() {
2315 let pipeline = DispatchPipeline::with_defaults();
2318 let mut ctx = Context::new(BrainWave::Gamma);
2319 ctx.compartment = Some("production".into());
2320 let tool = TestTool::new(
2321 "memory_read",
2322 EffectRow::read_only(vec![wm_core::Resource::Galaxy("codex".into())]),
2323 );
2324
2325 let args = serde_json::json!({"galaxy": "karma"});
2326 let result = pipeline.dispatch(&tool, &mut ctx, args).await;
2327 assert!(result.is_err());
2328 match result {
2329 Err(CoreError::Governance(msg)) => {
2330 assert!(msg.contains("production"));
2331 assert!(msg.contains("karma"));
2332 assert!(msg.contains("runtime"));
2333 }
2334 other => panic!("Expected Governance error, got {other:?}"),
2335 }
2336 }
2337
2338 #[tokio::test]
2339 async fn benchmark_pipeline_overhead() {
2340 let pipeline = DispatchPipeline::with_defaults();
2341 let tool = TestTool::new("bench_tool", EffectRow::pure());
2342 let args = Args::default();
2343
2344 for _ in 0..100 {
2346 let mut ctx = Context::new(BrainWave::Gamma);
2347 let _ = pipeline.dispatch(&tool, &mut ctx, args.clone()).await;
2348 }
2349
2350 let n = 10_000;
2352 let start = std::time::Instant::now();
2353 for _ in 0..n {
2354 let mut ctx = Context::new(BrainWave::Gamma);
2355 let _ = pipeline.dispatch(&tool, &mut ctx, args.clone()).await;
2356 }
2357 let pipeline_ns = start.elapsed().as_nanos() / n;
2358
2359 let start = std::time::Instant::now();
2361 for _ in 0..n {
2362 let mut ctx = Context::new(BrainWave::Gamma);
2363 let _ = tool.call(&mut ctx, args.clone()).await;
2364 }
2365 let direct_ns = start.elapsed().as_nanos() / n;
2366
2367 let overhead_ns = pipeline_ns.saturating_sub(direct_ns);
2368 println!(
2369 "\n Pipeline: {pipeline_ns} ns/call | Direct: {direct_ns} ns/call | Overhead: {overhead_ns} ns/call"
2370 );
2371
2372 #[cfg(not(debug_assertions))]
2376 assert!(
2377 overhead_ns < 5_000,
2378 "Pipeline overhead {overhead_ns} ns/call exceeds 5µs budget"
2379 );
2380 }
2381}