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