1use crate::session::{
4 DeferredToolLoadAuthority, SessionToolVisibilityState, ToolVisibilityWitness,
5 WitnessedToolFilter,
6};
7use crate::tool_catalog::stable_owner_key_for_tool;
8use crate::types::{ToolDef, ToolNameSet};
9use std::collections::BTreeSet;
10use std::collections::HashSet;
11use std::fmt;
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::{Arc, RwLock};
14
15#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum ToolFilter {
18 #[default]
20 All,
21 Allow(ToolNameSet),
23 Deny(ToolNameSet),
25}
26
27impl ToolFilter {
28 fn names(&self) -> Option<&ToolNameSet> {
29 match self {
30 Self::All => None,
31 Self::Allow(names) | Self::Deny(names) => Some(names),
32 }
33 }
34}
35
36pub const EXTERNAL_TOOL_FILTER_METADATA_KEY: &str = "tool_scope_external_filter";
38
39pub const INHERITED_TOOL_FILTER_METADATA_KEY: &str = "tool_scope_inherited_filter";
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48#[repr(transparent)]
49pub struct ToolScopeRevision(pub u64);
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ToolScopeSnapshot {
54 pub known_base_names: Vec<String>,
55 pub visible_names: Vec<String>,
56 pub capability_base_filter: ToolFilter,
57 pub base_filter: ToolFilter,
58 pub active_external_filter: ToolFilter,
59 pub active_turn_allow: Option<Vec<String>>,
60 pub active_turn_deny: Vec<String>,
61 pub active_revision: ToolScopeRevision,
62 pub staged_external_filter: ToolFilter,
63 pub staged_revision: ToolScopeRevision,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum ExternalToolSurfaceGlobalPhase {
69 Operating,
70 Shutdown,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum ExternalToolSurfaceBaseState {
76 Absent,
77 Active,
78 Removing,
79 Removed,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ExternalToolSurfacePendingOp {
85 None,
86 Add,
87 Reload,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ExternalToolSurfaceStagedOp {
93 None,
94 Add,
95 Remove,
96 Reload,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ExternalToolSurfaceDeltaOperation {
102 None,
103 Add,
104 Remove,
105 Reload,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum ExternalToolSurfaceDeltaPhase {
111 None,
112 Pending,
113 Applied,
114 Draining,
115 Failed,
116 Forced,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
125#[serde(rename_all = "snake_case")]
126pub enum ExternalToolSurfaceFailureCause {
127 PendingFailed,
128 SurfaceDraining,
129 SurfaceUnavailable,
130}
131
132impl ExternalToolSurfaceFailureCause {
133 pub fn as_str(self) -> &'static str {
134 match self {
135 Self::PendingFailed => "pending_failed",
136 Self::SurfaceDraining => "surface_draining",
137 Self::SurfaceUnavailable => "surface_unavailable",
138 }
139 }
140}
141
142impl fmt::Display for ExternalToolSurfaceFailureCause {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(self.as_str())
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct ExternalToolSurfaceEntrySnapshot {
154 pub surface_id: String,
155 pub visible: bool,
156 pub base_state: ExternalToolSurfaceBaseState,
157 pub has_removal_timing: bool,
158 pub pending_op: ExternalToolSurfacePendingOp,
159 pub staged_op: ExternalToolSurfaceStagedOp,
160 pub staged_intent_sequence: u64,
161 pub pending_task_sequence: u64,
162 pub pending_lineage_sequence: u64,
163 pub inflight_call_count: u64,
164 pub last_delta_operation: ExternalToolSurfaceDeltaOperation,
165 pub last_delta_phase: ExternalToolSurfaceDeltaPhase,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct ExternalToolSurfaceSnapshot {
175 pub phase: ExternalToolSurfaceGlobalPhase,
176 pub snapshot_epoch: u64,
177 pub snapshot_aligned_epoch: u64,
178 pub entries: Vec<ExternalToolSurfaceEntrySnapshot>,
179}
180
181#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
182pub enum ToolScopeStageError {
183 #[error("Unknown tool(s) in filter: {names:?}")]
184 UnknownTools { names: Vec<String> },
185 #[error("Missing tool visibility witness(es) for deferred tool(s): {names:?}")]
186 MissingWitnesses { names: Vec<String> },
187 #[error("Missing tool visibility witness(es) for filter tool(s): {names:?}")]
188 MissingFilterWitnesses { names: Vec<String> },
189 #[error("Invalid tool visibility witness(es) for deferred tool(s): {names:?}")]
190 InvalidWitnesses { names: Vec<String> },
191 #[error("Invalid tool visibility witness(es) for filter tool(s): {names:?}")]
192 InvalidFilterWitnesses { names: Vec<String> },
193 #[error("Tool scope state lock poisoned")]
194 LockPoisoned,
195 #[error("Tool visibility owner error: {message}")]
196 Owner { message: String },
197}
198
199#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
200pub enum ToolScopeApplyError {
201 #[error("Tool scope state lock poisoned")]
202 LockPoisoned,
203 #[error("Injected boundary failure for testing")]
204 InjectedFailure,
205 #[error("Tool visibility owner error: {message}")]
206 Owner { message: String },
207}
208
209pub trait ToolVisibilityOwner: Send + Sync {
215 fn visibility_state(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError>;
216
217 fn replace_visibility_state(
218 &self,
219 visibility_state: SessionToolVisibilityState,
220 ) -> Result<(), ToolScopeApplyError>;
221
222 fn stage_persistent_filter(
223 &self,
224 filter: ToolFilter,
225 witnesses: std::collections::BTreeMap<String, ToolVisibilityWitness>,
226 ) -> Result<ToolScopeRevision, ToolScopeStageError>;
227
228 fn stage_requested_deferred_names(
229 &self,
230 names: BTreeSet<String>,
231 ) -> Result<ToolScopeRevision, ToolScopeStageError>;
232
233 fn request_deferred_tools(
234 &self,
235 authorities: Vec<DeferredToolLoadAuthority>,
236 ) -> Result<ToolScopeRevision, ToolScopeStageError>;
237
238 fn replace_deferred_tool_authority_catalog(
239 &self,
240 _catalog: std::collections::BTreeMap<String, ToolVisibilityWitness>,
241 ) {
242 }
243
244 fn replace_filter_tool_authority_catalog(
245 &self,
246 _catalog: std::collections::BTreeMap<String, ToolVisibilityWitness>,
247 ) {
248 }
249
250 fn requires_filter_witnesses(&self) -> bool {
251 false
252 }
253
254 fn boundary_applied(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError>;
255}
256
257#[derive(Debug, Clone, Default)]
259pub struct LocalToolVisibilityOwner {
260 state: Arc<RwLock<SessionToolVisibilityState>>,
261 next_revision: Arc<AtomicU64>,
262 deferred_authority_catalog:
263 Arc<RwLock<std::collections::BTreeMap<String, ToolVisibilityWitness>>>,
264}
265
266impl LocalToolVisibilityOwner {
267 pub fn new() -> Self {
268 Self {
269 state: Arc::new(RwLock::new(SessionToolVisibilityState::default())),
270 next_revision: Arc::new(AtomicU64::new(0)),
271 deferred_authority_catalog: Arc::new(RwLock::new(std::collections::BTreeMap::new())),
272 }
273 }
274}
275
276impl ToolVisibilityOwner for LocalToolVisibilityOwner {
277 fn visibility_state(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
278 self.state
279 .read()
280 .map(|state| state.clone())
281 .map_err(|_| ToolScopeApplyError::LockPoisoned)
282 }
283
284 fn replace_visibility_state(
285 &self,
286 mut visibility_state: SessionToolVisibilityState,
287 ) -> Result<(), ToolScopeApplyError> {
288 let deferred_names = deferred_authority_names_for_visibility_state(&visibility_state);
289 if !deferred_names.is_empty() {
290 let canonical_authorities = {
291 let catalog = self
292 .deferred_authority_catalog
293 .read()
294 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
295 canonical_deferred_authorities_for_names(
296 &deferred_names,
297 &visibility_state.requested_witnesses,
298 &catalog,
299 )
300 .map_err(|err| ToolScopeApplyError::Owner {
301 message: format!("invalid deferred visibility authority: {err}"),
302 })?
303 };
304 visibility_state
305 .requested_witnesses
306 .extend(canonical_authorities);
307 }
308 let mut state = self
309 .state
310 .write()
311 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
312 let next_revision = visibility_state
313 .active_revision
314 .max(visibility_state.staged_revision);
315 *state = visibility_state;
316 self.next_revision.store(next_revision, Ordering::SeqCst);
317 Ok(())
318 }
319
320 fn stage_persistent_filter(
321 &self,
322 filter: ToolFilter,
323 witnesses: std::collections::BTreeMap<String, ToolVisibilityWitness>,
324 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
325 let mut state = self
326 .state
327 .write()
328 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
329 let revision = ToolScopeRevision(self.next_revision.fetch_add(1, Ordering::SeqCst) + 1);
330 state.staged_filter = filter;
331 state.filter_witnesses.extend(witnesses);
332 state.staged_revision = revision.0;
333 Ok(revision)
334 }
335
336 fn stage_requested_deferred_names(
337 &self,
338 names: BTreeSet<String>,
339 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
340 if !names.is_empty() {
341 return Err(ToolScopeStageError::MissingWitnesses {
342 names: names.into_iter().collect(),
343 });
344 }
345 let mut state = self
346 .state
347 .write()
348 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
349 let revision = ToolScopeRevision(self.next_revision.fetch_add(1, Ordering::SeqCst) + 1);
350 state.staged_requested_deferred_names = names;
351 state.staged_revision = revision.0;
352 Ok(revision)
353 }
354
355 fn request_deferred_tools(
356 &self,
357 authorities: Vec<DeferredToolLoadAuthority>,
358 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
359 let mut state = self
360 .state
361 .write()
362 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
363 let authorities = deferred_load_authority_map(&authorities)?;
364 let names = authorities.keys().cloned().collect::<BTreeSet<_>>();
365 let extended = state
366 .staged_requested_deferred_names
367 .union(&names)
368 .cloned()
369 .collect();
370 let mut combined_witnesses = state.requested_witnesses.clone();
371 combined_witnesses.extend(authorities);
372 let canonical_authorities = {
373 let catalog = self
374 .deferred_authority_catalog
375 .read()
376 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
377 canonical_deferred_authorities_for_names(&extended, &combined_witnesses, &catalog)?
378 };
379 combined_witnesses.extend(canonical_authorities);
380 let revision = ToolScopeRevision(self.next_revision.fetch_add(1, Ordering::SeqCst) + 1);
381 state.staged_requested_deferred_names = extended;
382 state.requested_witnesses = combined_witnesses;
383 state.staged_revision = revision.0;
384 Ok(revision)
385 }
386
387 fn replace_deferred_tool_authority_catalog(
388 &self,
389 catalog: std::collections::BTreeMap<String, ToolVisibilityWitness>,
390 ) {
391 if let Ok(mut guard) = self.deferred_authority_catalog.write() {
392 *guard = catalog;
393 }
394 }
395
396 fn boundary_applied(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
397 let mut state = self
398 .state
399 .write()
400 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
401 state.active_filter = state.staged_filter.clone();
402 state.active_requested_deferred_names = state.staged_requested_deferred_names.clone();
403 state.active_revision = state.staged_revision;
404 Ok(state.clone())
405 }
406}
407
408#[derive(Debug, Clone)]
409struct ToolScopeState {
410 base_tools: Arc<[Arc<ToolDef>]>,
411 known_base_names: ToolNameSet,
412 control_tool_names: ToolNameSet,
413 deferred_tool_names: ToolNameSet,
414 active_turn_allow: Option<ToolNameSet>,
415 active_turn_deny: ToolNameSet,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq)]
420pub struct ComposedToolFilter {
421 allow: Option<ToolNameSet>,
422 deny: ToolNameSet,
423}
424
425impl ComposedToolFilter {
426 pub fn allows(&self, name: &str) -> bool {
427 let allowed = self.allow.as_ref().is_none_or(|set| set.contains(name));
428 allowed && !self.deny.contains(name)
429 }
430}
431
432#[derive(Clone)]
434pub struct ToolScope {
435 state: Arc<RwLock<ToolScopeState>>,
436 visibility_owner: Arc<dyn ToolVisibilityOwner>,
437 fail_next_boundary_apply: Arc<AtomicBool>,
438}
439
440#[derive(Debug, Clone)]
441pub struct ToolScopeBoundaryResult {
442 pub previous_base_names: HashSet<String>,
443 pub current_base_names: HashSet<String>,
444 pub previous_visible_names: Vec<String>,
445 pub visible_names: Vec<String>,
446 pub previous_active_revision: ToolScopeRevision,
447 pub applied_revision: ToolScopeRevision,
448 pub tools: Arc<[Arc<ToolDef>]>,
449}
450
451impl ToolScopeBoundaryResult {
452 pub fn base_changed(&self) -> bool {
453 self.previous_base_names != self.current_base_names
454 }
455
456 pub fn visible_changed(&self) -> bool {
457 self.previous_visible_names != self.visible_names
458 }
459
460 pub fn changed(&self) -> bool {
461 self.base_changed() || self.visible_changed()
462 }
463}
464
465impl ToolScope {
466 pub fn new(base_tools: Arc<[Arc<ToolDef>]>) -> Self {
468 Self::new_with_projection_names(base_tools, HashSet::new(), HashSet::new())
469 }
470
471 pub fn new_with_control_tool_names(
473 base_tools: Arc<[Arc<ToolDef>]>,
474 control_tool_names: HashSet<String>,
475 ) -> Self {
476 Self::new_with_projection_names(base_tools, control_tool_names, HashSet::new())
477 }
478
479 pub fn new_with_projection_names(
481 base_tools: Arc<[Arc<ToolDef>]>,
482 control_tool_names: HashSet<String>,
483 deferred_tool_names: HashSet<String>,
484 ) -> Self {
485 Self::new_with_visibility_owner(
486 base_tools,
487 control_tool_names,
488 deferred_tool_names,
489 Arc::new(LocalToolVisibilityOwner::new()),
490 )
491 }
492
493 pub fn new_with_visibility_owner(
495 base_tools: Arc<[Arc<ToolDef>]>,
496 control_tool_names: HashSet<String>,
497 deferred_tool_names: HashSet<String>,
498 visibility_owner: Arc<dyn ToolVisibilityOwner>,
499 ) -> Self {
500 let deferred_tool_names: ToolNameSet = deferred_tool_names.into_iter().collect();
501 visibility_owner.replace_deferred_tool_authority_catalog(
502 deferred_authority_catalog_for_base_tools(&base_tools, &deferred_tool_names),
503 );
504 visibility_owner.replace_filter_tool_authority_catalog(
505 filter_authority_catalog_for_base_tools(&base_tools),
506 );
507 let known_base_names: ToolNameSet = base_tools
508 .iter()
509 .map(|tool| tool.name.to_string())
510 .collect();
511
512 Self {
513 state: Arc::new(RwLock::new(ToolScopeState {
514 base_tools,
515 known_base_names,
516 control_tool_names: control_tool_names.into_iter().collect(),
517 deferred_tool_names,
518 active_turn_allow: None,
519 active_turn_deny: ToolNameSet::new(),
520 })),
521 visibility_owner,
522 fail_next_boundary_apply: Arc::new(AtomicBool::new(false)),
523 }
524 }
525
526 pub fn visible_tools(&self) -> Arc<[Arc<ToolDef>]> {
528 match self.visible_tools_result() {
529 Ok(tools) => tools,
530 Err(_) => Vec::<Arc<ToolDef>>::new().into(),
531 }
532 }
533
534 pub fn visible_tools_result(&self) -> Result<Arc<[Arc<ToolDef>]>, ToolScopeApplyError> {
536 let state = self
537 .state
538 .read()
539 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
540 let visibility_state = self.visibility_owner.visibility_state()?;
541 let require_filter_witnesses = self.visibility_owner.requires_filter_witnesses();
542
543 let composed =
544 Self::compose_state_filters(&state, &visibility_state, require_filter_witnesses);
545
546 Ok(state
547 .base_tools
548 .iter()
549 .filter(|tool| {
550 state.control_tool_names.contains(tool.name.as_str())
551 || (Self::is_requested_session_tool_visible(
552 &state,
553 &visibility_state,
554 tool.as_ref(),
555 ) && composed.allows(tool.name.as_str()))
556 })
557 .map(Arc::clone)
558 .collect::<Vec<_>>()
559 .into())
560 }
561
562 pub fn handle(&self) -> ToolScopeHandle {
564 ToolScopeHandle {
565 state: Arc::clone(&self.state),
566 visibility_owner: Arc::clone(&self.visibility_owner),
567 }
568 }
569
570 pub fn snapshot(&self) -> Option<ToolScopeSnapshot> {
572 let state = self.state.read().ok()?;
573 let visibility_state = self.visibility_owner.visibility_state().ok()?;
574 let require_filter_witnesses = self.visibility_owner.requires_filter_witnesses();
575 Some(ToolScopeSnapshot {
576 known_base_names: sorted_names(&state.known_base_names),
577 visible_names: Self::visible_names_for_state(
578 &state,
579 &visibility_state,
580 require_filter_witnesses,
581 ),
582 capability_base_filter: visibility_state.capability_base_filter.clone(),
583 base_filter: visibility_state.inherited_base_filter.clone(),
584 active_external_filter: visibility_state.active_filter.clone(),
585 active_turn_allow: state.active_turn_allow.as_ref().map(sorted_names),
586 active_turn_deny: sorted_names(&state.active_turn_deny),
587 active_revision: ToolScopeRevision(visibility_state.active_revision),
588 staged_external_filter: visibility_state.staged_filter.clone(),
589 staged_revision: ToolScopeRevision(visibility_state.staged_revision),
590 })
591 }
592
593 pub fn apply_staged(
601 &self,
602 new_base_tools: Arc<[Arc<ToolDef>]>,
603 ) -> Result<ToolScopeBoundaryResult, ToolScopeApplyError> {
604 let (control_tool_names, deferred_tool_names) = self
605 .state
606 .read()
607 .map(|state| {
608 (
609 state.control_tool_names.clone(),
610 state.deferred_tool_names.clone(),
611 )
612 })
613 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
614 let previous_visibility_state = self.visibility_owner.visibility_state()?;
615 let visibility_state = self.promote_staged_visibility()?;
616 self.apply_staged_projection_with_previous(
617 new_base_tools,
618 control_tool_names,
619 deferred_tool_names,
620 &previous_visibility_state,
621 &visibility_state,
622 )
623 }
624
625 pub fn promote_staged_visibility(
627 &self,
628 ) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
629 self.visibility_owner.boundary_applied()
630 }
631
632 pub fn apply_staged_projection(
634 &self,
635 new_base_tools: Arc<[Arc<ToolDef>]>,
636 control_tool_names: HashSet<String>,
637 deferred_tool_names: HashSet<String>,
638 visibility_state: &SessionToolVisibilityState,
639 ) -> Result<ToolScopeBoundaryResult, ToolScopeApplyError> {
640 let previous_visibility_state = self.visibility_owner.visibility_state()?;
641 self.apply_staged_projection_with_previous(
642 new_base_tools,
643 control_tool_names.into_iter().collect(),
644 deferred_tool_names.into_iter().collect(),
645 &previous_visibility_state,
646 visibility_state,
647 )
648 }
649
650 pub(crate) fn apply_staged_projection_with_previous(
651 &self,
652 new_base_tools: Arc<[Arc<ToolDef>]>,
653 control_tool_names: ToolNameSet,
654 deferred_tool_names: ToolNameSet,
655 previous_visibility_state: &SessionToolVisibilityState,
656 visibility_state: &SessionToolVisibilityState,
657 ) -> Result<ToolScopeBoundaryResult, ToolScopeApplyError> {
658 if self
659 .fail_next_boundary_apply
660 .swap(false, std::sync::atomic::Ordering::SeqCst)
661 {
662 return Err(ToolScopeApplyError::InjectedFailure);
663 }
664
665 let mut state = self
666 .state
667 .write()
668 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
669 let require_filter_witnesses = self.visibility_owner.requires_filter_witnesses();
670
671 let previous_base_names = state.known_base_names.clone();
672 let previous_visible_names = Self::visible_names_for_state(
673 &state,
674 previous_visibility_state,
675 require_filter_witnesses,
676 );
677 let previous_active_revision = ToolScopeRevision(previous_visibility_state.active_revision);
678
679 state.base_tools = new_base_tools;
680 state.control_tool_names = control_tool_names;
681 state.deferred_tool_names = deferred_tool_names;
682 state.known_base_names = state
683 .base_tools
684 .iter()
685 .map(|tool| tool.name.to_string())
686 .collect::<ToolNameSet>();
687
688 let known_base_names = state.known_base_names.clone();
689 if let Some(allow) = state.active_turn_allow.as_mut() {
690 allow.retain(|name| known_base_names.contains(name.as_str()));
691 }
692 state
693 .active_turn_deny
694 .retain(|name| known_base_names.contains(name.as_str()));
695
696 let tools =
697 Self::visible_tools_for_state(&state, visibility_state, require_filter_witnesses);
698 let visible_names = tools
699 .iter()
700 .map(|tool| tool.name.to_string())
701 .collect::<Vec<_>>();
702 self.visibility_owner
703 .replace_deferred_tool_authority_catalog(deferred_authority_catalog_for_base_tools(
704 &state.base_tools,
705 &state.deferred_tool_names,
706 ));
707 self.visibility_owner.replace_filter_tool_authority_catalog(
708 filter_authority_catalog_for_base_tools(&state.base_tools),
709 );
710
711 Ok(ToolScopeBoundaryResult {
712 previous_base_names: previous_base_names.to_string_set(),
713 current_base_names: state.known_base_names.to_string_set(),
714 previous_visible_names,
715 visible_names,
716 previous_active_revision,
717 applied_revision: ToolScopeRevision(visibility_state.active_revision),
718 tools,
719 })
720 }
721
722 pub fn compose(filters: &[ToolFilter]) -> ComposedToolFilter {
724 let mut allow: Option<ToolNameSet> = None;
725 let mut deny = ToolNameSet::new();
726
727 for filter in filters {
728 match filter {
729 ToolFilter::All => {}
730 ToolFilter::Allow(names) => {
731 allow = Some(match allow {
732 Some(existing) => Self::allow_intersection(&existing, names),
733 None => names.clone(),
734 });
735 }
736 ToolFilter::Deny(names) => {
737 deny = Self::deny_union(&deny, names);
738 }
739 }
740 }
741
742 ComposedToolFilter { allow, deny }
743 }
744
745 fn allow_intersection(left: &ToolNameSet, right: &ToolNameSet) -> ToolNameSet {
747 left.iter()
748 .filter(|name| right.contains(name.as_str()))
749 .cloned()
750 .collect()
751 }
752
753 fn deny_union(left: &ToolNameSet, right: &ToolNameSet) -> ToolNameSet {
755 let mut union = left.clone();
756 union.extend(right.iter().cloned());
757 union
758 }
759
760 fn visible_names_for_state(
761 state: &ToolScopeState,
762 visibility_state: &SessionToolVisibilityState,
763 require_filter_witnesses: bool,
764 ) -> Vec<String> {
765 let tools =
766 Self::visible_tools_for_state(state, visibility_state, require_filter_witnesses);
767 tools.iter().map(|tool| tool.name.to_string()).collect()
768 }
769
770 fn visible_tools_for_state(
771 state: &ToolScopeState,
772 visibility_state: &SessionToolVisibilityState,
773 require_filter_witnesses: bool,
774 ) -> Arc<[Arc<ToolDef>]> {
775 let composed =
776 Self::compose_state_filters(state, visibility_state, require_filter_witnesses);
777
778 state
779 .base_tools
780 .iter()
781 .filter(|tool| {
782 state.control_tool_names.contains(tool.name.as_str())
783 || (Self::is_requested_session_tool_visible(
784 state,
785 visibility_state,
786 tool.as_ref(),
787 ) && composed.allows(tool.name.as_str()))
788 })
789 .map(Arc::clone)
790 .collect::<Vec<_>>()
791 .into()
792 }
793
794 fn compose_state_filters(
795 state: &ToolScopeState,
796 visibility_state: &SessionToolVisibilityState,
797 require_filter_witnesses: bool,
798 ) -> ComposedToolFilter {
799 let mut filters = vec![
800 Self::effective_filter_for_current_projection(
801 state,
802 visibility_state,
803 &visibility_state.capability_base_filter,
804 false,
805 ),
806 Self::effective_inherited_filter_for_current_projection(
807 state,
808 visibility_state,
809 &visibility_state.inherited_base_filter,
810 ),
811 Self::effective_filter_for_current_projection(
812 state,
813 visibility_state,
814 &visibility_state.active_filter,
815 require_filter_witnesses,
816 ),
817 ];
818 if let Some(allow) = &state.active_turn_allow {
819 filters.push(ToolFilter::Allow(allow.clone()));
820 }
821 if !state.active_turn_deny.is_empty() {
822 filters.push(ToolFilter::Deny(state.active_turn_deny.clone()));
823 }
824 Self::compose(&filters)
825 }
826
827 fn current_projection_tool<'a>(state: &'a ToolScopeState, name: &str) -> Option<&'a ToolDef> {
828 state
829 .base_tools
830 .iter()
831 .find(|tool| tool.name == name)
832 .map(Arc::as_ref)
833 }
834
835 fn witness_matches_tool(witness: Option<&ToolVisibilityWitness>, tool: &ToolDef) -> bool {
836 let Some(witness) = witness else {
837 return true;
838 };
839 if let Some(expected_owner) = witness.stable_owner_key.as_deref()
840 && stable_owner_key_for_tool(tool).as_deref() != Some(expected_owner)
841 {
842 return false;
843 }
844 if let Some(expected_provenance) = witness.last_seen_provenance.as_ref()
845 && tool.provenance.as_ref() != Some(expected_provenance)
846 {
847 return false;
848 }
849 true
850 }
851
852 fn requested_witness_matches_tool(
853 witness: Option<&ToolVisibilityWitness>,
854 tool: &ToolDef,
855 ) -> bool {
856 witness.is_some_and(|witness| {
857 witness.has_provenance_identity_witness()
858 && Self::witness_matches_tool(Some(witness), tool)
859 })
860 }
861
862 fn filter_name_applies(
863 state: &ToolScopeState,
864 visibility_state: &SessionToolVisibilityState,
865 name: &str,
866 require_filter_witnesses: bool,
867 ) -> bool {
868 let witness = visibility_state.filter_witnesses.get(name);
869 Self::current_projection_tool(state, name).is_none_or(|tool| match witness {
870 Some(witness) => {
871 witness.has_identity_witness() && Self::witness_matches_tool(Some(witness), tool)
872 }
873 None => !require_filter_witnesses,
874 })
875 }
876
877 fn inherited_filter_name_applies(
878 state: &ToolScopeState,
879 visibility_state: &SessionToolVisibilityState,
880 name: &str,
881 ) -> bool {
882 Self::current_projection_tool(state, name).is_none_or(|tool| {
883 match visibility_state.filter_witnesses.get(name) {
884 Some(witness) => {
885 witness.has_identity_witness()
886 && Self::witness_matches_tool(Some(witness), tool)
887 }
888 None => true,
889 }
890 })
891 }
892
893 fn effective_filter_for_current_projection(
894 state: &ToolScopeState,
895 visibility_state: &SessionToolVisibilityState,
896 filter: &ToolFilter,
897 require_filter_witnesses: bool,
898 ) -> ToolFilter {
899 match filter {
900 ToolFilter::All => ToolFilter::All,
901 ToolFilter::Allow(names) => ToolFilter::Allow(
902 names
903 .iter()
904 .filter(|name| {
905 Self::filter_name_applies(
906 state,
907 visibility_state,
908 name.as_str(),
909 require_filter_witnesses,
910 )
911 })
912 .cloned()
913 .collect(),
914 ),
915 ToolFilter::Deny(names) => ToolFilter::Deny(
916 names
917 .iter()
918 .filter(|name| {
919 Self::filter_name_applies(
920 state,
921 visibility_state,
922 name.as_str(),
923 require_filter_witnesses,
924 )
925 })
926 .cloned()
927 .collect(),
928 ),
929 }
930 }
931
932 fn effective_inherited_filter_for_current_projection(
933 state: &ToolScopeState,
934 visibility_state: &SessionToolVisibilityState,
935 filter: &ToolFilter,
936 ) -> ToolFilter {
937 match filter {
938 ToolFilter::All => ToolFilter::All,
939 ToolFilter::Allow(names) => ToolFilter::Allow(
940 names
941 .iter()
942 .filter(|name| {
943 Self::inherited_filter_name_applies(state, visibility_state, name.as_str())
944 })
945 .cloned()
946 .collect(),
947 ),
948 ToolFilter::Deny(names) => ToolFilter::Deny(
949 names
950 .iter()
951 .filter(|name| {
952 Self::inherited_filter_name_applies(state, visibility_state, name.as_str())
953 })
954 .cloned()
955 .collect(),
956 ),
957 }
958 }
959
960 fn is_requested_session_tool_visible(
961 state: &ToolScopeState,
962 visibility_state: &SessionToolVisibilityState,
963 tool: &ToolDef,
964 ) -> bool {
965 if !state.deferred_tool_names.contains(tool.name.as_str()) {
966 return true;
967 }
968 visibility_state
969 .active_requested_deferred_names
970 .contains(tool.name.as_str())
971 && Self::requested_witness_matches_tool(
972 visibility_state.requested_witnesses.get(tool.name.as_str()),
973 tool,
974 )
975 }
976
977 pub fn set_base_filter(&self, filter: ToolFilter) -> Result<(), ToolScopeApplyError> {
984 let state = self
985 .state
986 .read()
987 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
988 let mut visibility_state = self.visibility_owner.visibility_state()?;
989 extend_filter_witnesses(
990 &state.base_tools,
991 &mut visibility_state.filter_witnesses,
992 &filter,
993 );
994 visibility_state.inherited_base_filter = filter;
995 self.visibility_owner
996 .replace_visibility_state(visibility_state)
997 }
998
999 pub fn set_visibility_state(
1001 &self,
1002 visibility_state: SessionToolVisibilityState,
1003 ) -> Result<(), ToolScopeApplyError> {
1004 self.visibility_owner
1005 .replace_visibility_state(visibility_state)
1006 }
1007
1008 pub fn visibility_state(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
1010 self.visibility_owner.visibility_state()
1011 }
1012
1013 pub fn visible_tool_names(&self) -> Result<BTreeSet<String>, ToolScopeApplyError> {
1015 self.visible_tools_result().map(|tools| {
1016 tools
1017 .iter()
1018 .map(|tool| tool.name.to_string())
1019 .collect::<BTreeSet<_>>()
1020 })
1021 }
1022
1023 pub fn staged_session_filters_allow_name(
1026 &self,
1027 name: &str,
1028 ) -> Result<bool, ToolScopeApplyError> {
1029 let state = self
1030 .state
1031 .read()
1032 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
1033 let visibility_state = self.visibility_owner.visibility_state()?;
1034 let require_filter_witnesses = self.visibility_owner.requires_filter_witnesses();
1035 Ok(Self::compose(&[
1036 Self::effective_filter_for_current_projection(
1037 &state,
1038 &visibility_state,
1039 &visibility_state.capability_base_filter,
1040 false,
1041 ),
1042 Self::effective_inherited_filter_for_current_projection(
1043 &state,
1044 &visibility_state,
1045 &visibility_state.inherited_base_filter,
1046 ),
1047 Self::effective_filter_for_current_projection(
1048 &state,
1049 &visibility_state,
1050 &visibility_state.staged_filter,
1051 require_filter_witnesses,
1052 ),
1053 ])
1054 .allows(name))
1055 }
1056
1057 pub fn base_tools_snapshot(&self) -> Result<Arc<[Arc<ToolDef>]>, ToolScopeApplyError> {
1059 self.state
1060 .read()
1061 .map(|state| Arc::clone(&state.base_tools))
1062 .map_err(|_| ToolScopeApplyError::LockPoisoned)
1063 }
1064
1065 pub fn base_tool_names(&self) -> Result<BTreeSet<String>, ToolScopeApplyError> {
1067 self.state
1068 .read()
1069 .map(|state| {
1070 state
1071 .known_base_names
1072 .iter()
1073 .map(|name| name.as_str().to_string())
1074 .collect::<BTreeSet<_>>()
1075 })
1076 .map_err(|_| ToolScopeApplyError::LockPoisoned)
1077 }
1078
1079 pub fn fail_closed_projection(&self) -> Result<Arc<[Arc<ToolDef>]>, ToolScopeApplyError> {
1085 let mut state = self
1086 .state
1087 .write()
1088 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
1089 state.base_tools = Arc::<[Arc<ToolDef>]>::from([]);
1090 state.known_base_names.clear();
1091 state.control_tool_names.clear();
1092 state.deferred_tool_names.clear();
1093 state.active_turn_allow = Some(ToolNameSet::new());
1094 state.active_turn_deny.clear();
1095 Ok(Arc::<[Arc<ToolDef>]>::from([]))
1096 }
1097
1098 pub fn revisions(&self) -> Result<(ToolScopeRevision, ToolScopeRevision), ToolScopeApplyError> {
1100 let visibility_state = self.visibility_owner.visibility_state()?;
1101 Ok((
1102 ToolScopeRevision(visibility_state.active_revision),
1103 ToolScopeRevision(visibility_state.staged_revision),
1104 ))
1105 }
1106
1107 pub fn missing_requested_names(&self) -> Result<BTreeSet<String>, ToolScopeApplyError> {
1109 let state = self
1110 .state
1111 .read()
1112 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
1113 let visibility_state = self.visibility_owner.visibility_state()?;
1114 Ok(visibility_state
1115 .active_requested_deferred_names
1116 .iter()
1117 .filter(|name| !state.known_base_names.contains(name.as_str()))
1118 .cloned()
1119 .collect::<BTreeSet<_>>())
1120 }
1121
1122 pub fn missing_filter_names(&self) -> Result<BTreeSet<String>, ToolScopeApplyError> {
1124 let state = self
1125 .state
1126 .read()
1127 .map_err(|_| ToolScopeApplyError::LockPoisoned)?;
1128 let visibility_state = self.visibility_owner.visibility_state()?;
1129 Ok(durable_filter_names(&visibility_state)
1130 .into_iter()
1131 .filter(|name| !state.known_base_names.contains(name.as_str()))
1132 .map(crate::types::ToolName::into_string)
1133 .collect::<BTreeSet<_>>())
1134 }
1135
1136 pub fn stage_requested_deferred_names(
1138 &self,
1139 names: BTreeSet<String>,
1140 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1141 self.visibility_owner.stage_requested_deferred_names(names)
1142 }
1143
1144 pub fn add_requested_deferred_authorities(
1146 &self,
1147 authorities: &[DeferredToolLoadAuthority],
1148 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1149 let state = self
1150 .state
1151 .read()
1152 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
1153 let visibility_state =
1154 self.visibility_owner
1155 .visibility_state()
1156 .map_err(|err| ToolScopeStageError::Owner {
1157 message: err.to_string(),
1158 })?;
1159 let authorities_by_name = deferred_load_authority_map(authorities)?;
1160 let names = authorities_by_name.keys().cloned().collect::<BTreeSet<_>>();
1161 let staged_requested_deferred_names = visibility_state.staged_requested_deferred_names;
1162 let mut combined_witnesses = visibility_state.requested_witnesses;
1163 let extended = staged_requested_deferred_names
1164 .union(&names)
1165 .cloned()
1166 .collect::<BTreeSet<_>>();
1167 combined_witnesses.extend(authorities_by_name);
1168 let catalog = deferred_authority_catalog_for_base_tools(
1169 &state.base_tools,
1170 &state.deferred_tool_names,
1171 );
1172 let canonical_authorities =
1173 canonical_deferred_authorities_for_names(&extended, &combined_witnesses, &catalog)?;
1174 let requested_canonical_authorities = canonical_authorities
1175 .into_iter()
1176 .filter(|(name, _)| names.contains(name.as_str()))
1177 .map(|(name, witness)| DeferredToolLoadAuthority::new(name, witness))
1178 .collect();
1179 drop(state);
1180
1181 self.visibility_owner
1182 .request_deferred_tools(requested_canonical_authorities)
1183 }
1184
1185 #[cfg(test)]
1186 pub(crate) fn inject_boundary_failure_once_for_test(&self) {
1187 self.fail_next_boundary_apply.store(true, Ordering::SeqCst);
1188 }
1189}
1190
1191#[derive(Clone)]
1193pub struct ToolScopeHandle {
1194 state: Arc<RwLock<ToolScopeState>>,
1195 visibility_owner: Arc<dyn ToolVisibilityOwner>,
1196}
1197
1198impl std::fmt::Debug for ToolScope {
1199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1200 f.debug_struct("ToolScope")
1201 .field("state", &"<projection>")
1202 .field("visibility_owner", &"<dyn ToolVisibilityOwner>")
1203 .finish()
1204 }
1205}
1206
1207impl std::fmt::Debug for ToolScopeHandle {
1208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1209 f.debug_struct("ToolScopeHandle")
1210 .field("state", &"<projection>")
1211 .field("visibility_owner", &"<dyn ToolVisibilityOwner>")
1212 .finish()
1213 }
1214}
1215
1216impl ToolScopeHandle {
1217 pub fn stage_external_filter(
1219 &self,
1220 filter: ToolFilter,
1221 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1222 let state = self
1223 .state
1224 .read()
1225 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
1226 let visibility_state =
1227 self.visibility_owner
1228 .visibility_state()
1229 .map_err(|err| ToolScopeStageError::Owner {
1230 message: err.to_string(),
1231 })?;
1232
1233 let mut known_names = state.known_base_names.clone();
1234 for control_name in &state.control_tool_names {
1235 known_names.remove(control_name.as_str());
1236 }
1237 known_names.extend(durable_filter_names(&visibility_state));
1238 validate_filter(&filter, &known_names)?;
1239
1240 self.visibility_owner.stage_persistent_filter(
1241 filter.clone(),
1242 filter_witnesses_for_base_tools_or_existing(
1243 &state.base_tools,
1244 &visibility_state.filter_witnesses,
1245 &filter,
1246 ),
1247 )
1248 }
1249
1250 pub(crate) fn staged_revision(&self) -> Result<ToolScopeRevision, ToolScopeStageError> {
1251 self.visibility_owner
1252 .visibility_state()
1253 .map(|state| ToolScopeRevision(state.staged_revision))
1254 .map_err(|err| ToolScopeStageError::Owner {
1255 message: err.to_string(),
1256 })
1257 }
1258
1259 pub fn set_turn_overlay(
1261 &self,
1262 allow: Option<HashSet<String>>,
1263 deny: HashSet<String>,
1264 ) -> Result<(), ToolScopeStageError> {
1265 let allow: Option<ToolNameSet> = allow.map(|names| names.into_iter().collect());
1266 let deny: ToolNameSet = deny.into_iter().collect();
1267 let mut state = self
1268 .state
1269 .write()
1270 .map_err(|_| ToolScopeStageError::LockPoisoned)?;
1271
1272 if let Some(allow_set) = &allow {
1273 validate_filter(
1274 &ToolFilter::Allow(allow_set.clone()),
1275 &state.known_base_names,
1276 )?;
1277 }
1278 if !deny.is_empty() {
1279 validate_filter(&ToolFilter::Deny(deny.clone()), &state.known_base_names)?;
1280 }
1281
1282 state.active_turn_allow = allow;
1283 state.active_turn_deny = deny;
1284 Ok(())
1285 }
1286
1287 pub fn clear_turn_overlay(&self) {
1289 if let Ok(mut state) = self.state.write() {
1290 state.active_turn_allow = None;
1291 state.active_turn_deny.clear();
1292 }
1293 }
1294}
1295
1296fn validate_filter(
1297 filter: &ToolFilter,
1298 known_base_names: &ToolNameSet,
1299) -> Result<(), ToolScopeStageError> {
1300 let Some(names) = filter.names() else {
1301 return Ok(());
1302 };
1303
1304 let mut unknown: Vec<String> = names
1305 .iter()
1306 .filter(|name| !known_base_names.contains(name.as_str()))
1307 .map(|name| name.as_str().to_string())
1308 .collect();
1309
1310 if unknown.is_empty() {
1311 return Ok(());
1312 }
1313
1314 unknown.sort_unstable();
1315 unknown.dedup();
1316 Err(ToolScopeStageError::UnknownTools { names: unknown })
1317}
1318
1319pub fn witnessed_tool_filter_for_defs(
1320 filter: ToolFilter,
1321 tool_defs: &[ToolDef],
1322) -> WitnessedToolFilter {
1323 let witnesses = filter_witnesses_for_tool_defs(tool_defs, &filter);
1324 WitnessedToolFilter::new(filter, witnesses)
1325}
1326
1327pub fn filter_witnesses_for_tool_defs(
1328 tool_defs: &[ToolDef],
1329 filter: &ToolFilter,
1330) -> std::collections::BTreeMap<String, ToolVisibilityWitness> {
1331 let Some(filter_names) = filter.names() else {
1332 return Default::default();
1333 };
1334
1335 let mut witnesses = std::collections::BTreeMap::new();
1336 for name in filter_names {
1337 if let Some(tool) = tool_defs.iter().find(|tool| tool.name == name.as_str()) {
1338 let witness = filter_witness_for_tool(tool);
1339 if witness.has_identity_witness() {
1340 witnesses.insert(name.as_str().to_string(), witness);
1341 }
1342 }
1343 }
1344 witnesses
1345}
1346
1347pub fn validate_inherited_filter_witnesses(
1348 filter: &ToolFilter,
1349 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1350) -> Result<(), ToolScopeStageError> {
1351 let Some(filter_names) = filter.names() else {
1352 return Ok(());
1353 };
1354
1355 let mut missing = filter_names
1356 .iter()
1357 .filter(|name| {
1358 witnesses
1359 .get(name.as_str())
1360 .is_none_or(|witness| !witness.has_identity_witness())
1361 })
1362 .map(|name| name.as_str().to_string())
1363 .collect::<Vec<_>>();
1364
1365 if missing.is_empty() {
1366 return Ok(());
1367 }
1368
1369 missing.sort_unstable();
1370 missing.dedup();
1371 Err(ToolScopeStageError::MissingFilterWitnesses { names: missing })
1372}
1373
1374pub fn validate_filter_witnesses_match_catalog(
1375 filter: &ToolFilter,
1376 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1377 catalog: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1378) -> Result<(), ToolScopeStageError> {
1379 validate_inherited_filter_witnesses(filter, witnesses)?;
1380 let Some(filter_names) = filter.names() else {
1381 return Ok(());
1382 };
1383
1384 let mut invalid = filter_names
1385 .iter()
1386 .filter(|name| {
1387 let Some(expected) = catalog.get(name.as_str()) else {
1388 return false;
1389 };
1390 witnesses
1391 .get(name.as_str())
1392 .is_some_and(|witness| !filter_witness_matches_catalog(witness, expected))
1393 })
1394 .map(|name| name.as_str().to_string())
1395 .collect::<Vec<_>>();
1396
1397 if invalid.is_empty() {
1398 return Ok(());
1399 }
1400
1401 invalid.sort_unstable();
1402 invalid.dedup();
1403 Err(ToolScopeStageError::InvalidFilterWitnesses { names: invalid })
1404}
1405
1406pub fn validate_witnessed_filter_authority(
1407 filter: &ToolFilter,
1408 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1409) -> Result<(), ToolScopeStageError> {
1410 validate_inherited_filter_witnesses(filter, witnesses)?;
1411 let Some(filter_names) = filter.names() else {
1412 if witnesses.is_empty() {
1413 return Ok(());
1414 }
1415 let invalid = witnesses.keys().cloned().collect::<Vec<_>>();
1416 return Err(ToolScopeStageError::InvalidFilterWitnesses { names: invalid });
1417 };
1418
1419 let mut invalid = witnesses
1420 .keys()
1421 .filter(|name| !filter_names.contains(name.as_str()))
1422 .cloned()
1423 .collect::<Vec<_>>();
1424 if invalid.is_empty() {
1425 return Ok(());
1426 }
1427
1428 invalid.sort_unstable();
1429 invalid.dedup();
1430 Err(ToolScopeStageError::InvalidFilterWitnesses { names: invalid })
1431}
1432
1433fn filter_witness_matches_catalog(
1434 witness: &ToolVisibilityWitness,
1435 expected: &ToolVisibilityWitness,
1436) -> bool {
1437 if let Some(owner) = witness.stable_owner_key.as_deref()
1438 && expected.stable_owner_key.as_deref() != Some(owner)
1439 {
1440 return false;
1441 }
1442 if let Some(provenance) = witness.last_seen_provenance.as_ref()
1443 && expected.last_seen_provenance.as_ref() != Some(provenance)
1444 {
1445 return false;
1446 }
1447 witness.has_identity_witness()
1448}
1449
1450fn deferred_authority_catalog_for_base_tools(
1451 base_tools: &[Arc<ToolDef>],
1452 deferred_tool_names: &ToolNameSet,
1453) -> std::collections::BTreeMap<String, ToolVisibilityWitness> {
1454 base_tools
1455 .iter()
1456 .filter(|tool| deferred_tool_names.contains(tool.name.as_str()))
1457 .filter_map(|tool| {
1458 let provenance = tool.provenance.as_ref()?;
1459 Some((
1460 tool.name.to_string(),
1461 ToolVisibilityWitness {
1462 stable_owner_key: stable_owner_key_for_tool(tool),
1463 last_seen_provenance: Some(provenance.clone()),
1464 },
1465 ))
1466 })
1467 .collect()
1468}
1469
1470fn filter_authority_catalog_for_base_tools(
1471 base_tools: &[Arc<ToolDef>],
1472) -> std::collections::BTreeMap<String, ToolVisibilityWitness> {
1473 base_tools
1474 .iter()
1475 .filter_map(|tool| {
1476 let witness = filter_witness_for_tool(tool);
1477 witness
1478 .has_identity_witness()
1479 .then(|| (tool.name.to_string(), witness))
1480 })
1481 .collect()
1482}
1483
1484fn deferred_authority_names_for_visibility_state(
1485 visibility_state: &SessionToolVisibilityState,
1486) -> BTreeSet<String> {
1487 visibility_state
1488 .active_requested_deferred_names
1489 .union(&visibility_state.staged_requested_deferred_names)
1490 .cloned()
1491 .collect()
1492}
1493
1494fn deferred_load_authority_map(
1495 authorities: &[DeferredToolLoadAuthority],
1496) -> Result<std::collections::BTreeMap<String, ToolVisibilityWitness>, ToolScopeStageError> {
1497 let mut by_name = std::collections::BTreeMap::new();
1498 let mut invalid = Vec::new();
1499
1500 for authority in authorities {
1501 match by_name.insert(authority.name.clone(), authority.witness.clone()) {
1502 Some(existing) if existing != authority.witness => invalid.push(authority.name.clone()),
1503 _ => {}
1504 }
1505 }
1506
1507 if invalid.is_empty() {
1508 return Ok(by_name);
1509 }
1510
1511 invalid.sort_unstable();
1512 invalid.dedup();
1513 Err(ToolScopeStageError::InvalidWitnesses { names: invalid })
1514}
1515
1516pub(crate) fn validate_deferred_authorities_for_names(
1517 names: &BTreeSet<String>,
1518 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1519 authority_catalog: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1520) -> Result<(), ToolScopeStageError> {
1521 let missing = missing_visibility_witness_names(names, witnesses);
1522 if !missing.is_empty() {
1523 return Err(ToolScopeStageError::MissingWitnesses { names: missing });
1524 }
1525
1526 let mut invalid = names
1527 .iter()
1528 .filter(|name| {
1529 let witness = witnesses.get(name.as_str());
1530 let expected = authority_catalog.get(name.as_str());
1531 !matches!(
1532 (witness, expected),
1533 (Some(witness), Some(expected))
1534 if witness.stable_owner_key == expected.stable_owner_key
1535 && witness.last_seen_provenance == expected.last_seen_provenance
1536 )
1537 })
1538 .cloned()
1539 .collect::<Vec<_>>();
1540
1541 if invalid.is_empty() {
1542 return Ok(());
1543 }
1544
1545 invalid.sort_unstable();
1546 invalid.dedup();
1547 Err(ToolScopeStageError::InvalidWitnesses { names: invalid })
1548}
1549
1550fn canonical_deferred_authorities_for_names(
1551 names: &BTreeSet<String>,
1552 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1553 authority_catalog: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1554) -> Result<std::collections::BTreeMap<String, ToolVisibilityWitness>, ToolScopeStageError> {
1555 validate_deferred_authorities_for_names(names, witnesses, authority_catalog)?;
1556 let mut authorities = std::collections::BTreeMap::new();
1557 for name in names {
1558 let Some(witness) = authority_catalog.get(name.as_str()) else {
1559 return Err(ToolScopeStageError::InvalidWitnesses {
1560 names: vec![name.clone()],
1561 });
1562 };
1563 authorities.insert(name.clone(), witness.clone());
1564 }
1565 Ok(authorities)
1566}
1567
1568fn durable_filter_names(state: &SessionToolVisibilityState) -> ToolNameSet {
1569 let mut names = ToolNameSet::new();
1570 for filter in [
1571 &state.inherited_base_filter,
1572 &state.active_filter,
1573 &state.staged_filter,
1574 ] {
1575 if let Some(filter_names) = filter.names() {
1576 names.extend(filter_names.iter().cloned());
1577 }
1578 }
1579 names
1580}
1581
1582fn filter_witnesses_for_base_tools(
1583 base_tools: &Arc<[Arc<ToolDef>]>,
1584 filter: &ToolFilter,
1585) -> std::collections::BTreeMap<String, ToolVisibilityWitness> {
1586 let mut witnesses = std::collections::BTreeMap::new();
1587 extend_filter_witnesses(base_tools, &mut witnesses, filter);
1588 witnesses
1589}
1590
1591fn filter_witnesses_for_base_tools_or_existing(
1592 base_tools: &Arc<[Arc<ToolDef>]>,
1593 existing: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1594 filter: &ToolFilter,
1595) -> std::collections::BTreeMap<String, ToolVisibilityWitness> {
1596 let mut witnesses = filter_witnesses_for_base_tools(base_tools, filter);
1597 let Some(filter_names) = filter.names() else {
1598 return witnesses;
1599 };
1600
1601 for name in filter_names {
1602 if witnesses.contains_key(name.as_str()) {
1603 continue;
1604 }
1605 if let Some(witness) = existing
1606 .get(name.as_str())
1607 .filter(|witness| witness.has_identity_witness())
1608 {
1609 witnesses.insert(name.as_str().to_string(), witness.clone());
1610 }
1611 }
1612 witnesses
1613}
1614
1615fn extend_filter_witnesses(
1616 base_tools: &Arc<[Arc<ToolDef>]>,
1617 witnesses: &mut std::collections::BTreeMap<String, ToolVisibilityWitness>,
1618 filter: &ToolFilter,
1619) {
1620 let Some(filter_names) = filter.names() else {
1621 return;
1622 };
1623
1624 for name in filter_names {
1625 if let Some(tool) = base_tools.iter().find(|tool| tool.name == name.as_str()) {
1626 let witness = filter_witness_for_tool(tool);
1627 if witness.has_identity_witness() {
1628 witnesses.insert(name.as_str().to_string(), witness);
1629 }
1630 }
1631 }
1632}
1633
1634fn filter_witness_for_tool(tool: &ToolDef) -> ToolVisibilityWitness {
1635 ToolVisibilityWitness {
1636 stable_owner_key: stable_owner_key_for_tool(tool),
1637 last_seen_provenance: tool.provenance.clone(),
1638 }
1639}
1640
1641fn missing_visibility_witness_names(
1642 names: &BTreeSet<String>,
1643 witnesses: &std::collections::BTreeMap<String, ToolVisibilityWitness>,
1644) -> Vec<String> {
1645 names
1646 .iter()
1647 .filter(|name| {
1648 witnesses
1649 .get(name.as_str())
1650 .is_none_or(|witness| !witness.has_provenance_identity_witness())
1651 })
1652 .cloned()
1653 .collect()
1654}
1655
1656fn sorted_names(names: &ToolNameSet) -> Vec<String> {
1657 let mut values = names
1658 .iter()
1659 .map(|name| name.as_str().to_string())
1660 .collect::<Vec<_>>();
1661 values.sort_unstable();
1662 values
1663}
1664
1665#[cfg(test)]
1666#[allow(clippy::unwrap_used, clippy::expect_used)]
1667mod tests {
1668 use super::ToolScopeRevision;
1669 use super::{
1670 ToolFilter, ToolScope, ToolScopeApplyError, ToolScopeStageError, ToolVisibilityOwner,
1671 };
1672 use crate::session::{
1673 DeferredToolLoadAuthority, SessionToolVisibilityState, ToolVisibilityWitness,
1674 };
1675 use crate::types::{ToolDef, ToolNameSet, ToolProvenance, ToolSourceKind};
1676 use std::collections::{BTreeMap, BTreeSet, HashSet};
1677 use std::sync::Arc;
1678
1679 fn set(names: &[&str]) -> ToolNameSet {
1680 names.iter().map(|name| (*name).to_string()).collect()
1681 }
1682
1683 fn raw_set(names: &[&str]) -> HashSet<String> {
1684 names.iter().map(|name| (*name).to_string()).collect()
1685 }
1686
1687 fn tools(names: &[&str]) -> Arc<[Arc<ToolDef>]> {
1688 names
1689 .iter()
1690 .map(|name| {
1691 Arc::new(ToolDef {
1692 name: (*name).into(),
1693 description: format!("{name} tool"),
1694 input_schema: serde_json::json!({ "type": "object" }),
1695 provenance: None,
1696 })
1697 })
1698 .collect::<Vec<_>>()
1699 .into()
1700 }
1701
1702 fn tool_with_provenance(name: &str, source_id: &str) -> Arc<ToolDef> {
1703 Arc::new(ToolDef {
1704 name: name.into(),
1705 description: format!("{name} tool"),
1706 input_schema: serde_json::json!({ "type": "object" }),
1707 provenance: Some(ToolProvenance {
1708 kind: ToolSourceKind::Callback,
1709 source_id: source_id.into(),
1710 }),
1711 })
1712 }
1713
1714 struct VisibilityReadFailingOwner;
1715
1716 impl ToolVisibilityOwner for VisibilityReadFailingOwner {
1717 fn visibility_state(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
1718 Err(ToolScopeApplyError::Owner {
1719 message: "visibility read fixture failed".to_string(),
1720 })
1721 }
1722
1723 fn replace_visibility_state(
1724 &self,
1725 _visibility_state: SessionToolVisibilityState,
1726 ) -> Result<(), ToolScopeApplyError> {
1727 Ok(())
1728 }
1729
1730 fn stage_persistent_filter(
1731 &self,
1732 _filter: ToolFilter,
1733 _witnesses: BTreeMap<String, ToolVisibilityWitness>,
1734 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1735 Err(ToolScopeStageError::Owner {
1736 message: "visibility read fixture failed".to_string(),
1737 })
1738 }
1739
1740 fn stage_requested_deferred_names(
1741 &self,
1742 _names: BTreeSet<String>,
1743 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1744 Err(ToolScopeStageError::Owner {
1745 message: "visibility read fixture failed".to_string(),
1746 })
1747 }
1748
1749 fn request_deferred_tools(
1750 &self,
1751 _authorities: Vec<DeferredToolLoadAuthority>,
1752 ) -> Result<ToolScopeRevision, ToolScopeStageError> {
1753 Err(ToolScopeStageError::Owner {
1754 message: "visibility read fixture failed".to_string(),
1755 })
1756 }
1757
1758 fn boundary_applied(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
1759 Err(ToolScopeApplyError::Owner {
1760 message: "visibility read fixture failed".to_string(),
1761 })
1762 }
1763 }
1764
1765 #[test]
1766 fn tool_filter_typed_names_keep_legacy_string_wire_shape() -> Result<(), String> {
1767 let filter = ToolFilter::Allow(set(&["read_file", "shell"]));
1768 let value = serde_json::to_value(&filter).unwrap();
1769 let names = value["Allow"]
1770 .as_array()
1771 .expect("tool filter names remain string array-shaped");
1772 assert_eq!(names.len(), 2);
1773 assert!(names.contains(&serde_json::json!("read_file")));
1774 assert!(names.contains(&serde_json::json!("shell")));
1775
1776 let parsed: ToolFilter = serde_json::from_value(value).unwrap();
1777 match parsed {
1778 ToolFilter::Allow(names) => {
1779 assert!(names.contains("read_file"));
1780 assert!(names.contains("shell"));
1781 }
1782 other => return Err(format!("expected allow filter, got {other:?}")),
1783 }
1784 Ok(())
1785 }
1786
1787 #[test]
1788 fn stage_revision_is_monotonic() {
1789 let scope = ToolScope::new(tools(&["a", "b", "c"]));
1790 let handle = scope.handle();
1791
1792 let first = handle
1793 .stage_external_filter(ToolFilter::Deny(set(&["a"])))
1794 .unwrap();
1795 let second = handle
1796 .stage_external_filter(ToolFilter::Allow(set(&["b", "c"])))
1797 .unwrap();
1798
1799 assert!(second > first);
1800 assert_eq!(handle.staged_revision().unwrap(), second);
1801 }
1802
1803 #[test]
1804 fn owner_visibility_read_failure_fails_closed_without_local_defaults() {
1805 let scope = ToolScope::new_with_visibility_owner(
1806 tools(&["visible", "deferred"]),
1807 HashSet::new(),
1808 raw_set(&["deferred"]),
1809 Arc::new(VisibilityReadFailingOwner),
1810 );
1811
1812 let err = scope
1813 .visible_tools_result()
1814 .expect_err("owner read failures must stay explicit");
1815 assert!(
1816 err.to_string().contains("visibility read fixture failed"),
1817 "unexpected visibility error: {err}"
1818 );
1819 assert!(
1820 scope.visible_tools().is_empty(),
1821 "fallible visible_tools facade must close the projected tool set"
1822 );
1823 assert!(
1824 scope.visible_tool_names().is_err(),
1825 "dispatch prechecks must not synthesize names from local defaults"
1826 );
1827 }
1828
1829 #[test]
1830 fn stage_rejects_unknown_tools() {
1831 let scope = ToolScope::new(tools(&["known"]));
1832 let handle = scope.handle();
1833
1834 let err = handle
1835 .stage_external_filter(ToolFilter::Allow(set(&["known", "missing"])))
1836 .unwrap_err();
1837
1838 assert_eq!(
1839 err,
1840 ToolScopeStageError::UnknownTools {
1841 names: vec!["missing".to_string()],
1842 }
1843 );
1844 }
1845
1846 #[test]
1847 fn control_tools_remain_visible_and_unfilterable() {
1848 let scope = ToolScope::new_with_control_tool_names(
1849 tools(&["visible", "tool_catalog_search"]),
1850 raw_set(&["tool_catalog_search"]),
1851 );
1852 let handle = scope.handle();
1853
1854 let err = handle
1855 .stage_external_filter(ToolFilter::Deny(set(&["tool_catalog_search"])))
1856 .unwrap_err();
1857 assert_eq!(
1858 err,
1859 ToolScopeStageError::UnknownTools {
1860 names: vec!["tool_catalog_search".to_string()],
1861 }
1862 );
1863
1864 handle
1865 .stage_external_filter(ToolFilter::Deny(set(&["visible"])))
1866 .unwrap();
1867 let applied = scope
1868 .apply_staged(tools(&["visible", "tool_catalog_search"]))
1869 .unwrap();
1870
1871 assert_eq!(
1872 applied.visible_names,
1873 vec!["tool_catalog_search".to_string()],
1874 "control tools should remain visible even when the session plane is filtered out"
1875 );
1876 }
1877
1878 #[test]
1879 fn deferred_tools_stay_hidden_until_requested_boundary_applies() {
1880 let visible = tools(&["visible"])[0].clone();
1881 let deferred = tool_with_provenance("deferred", "owner-a");
1882 let scope = ToolScope::new_with_projection_names(
1883 vec![Arc::clone(&visible), Arc::clone(&deferred)].into(),
1884 HashSet::new(),
1885 raw_set(&["deferred"]),
1886 );
1887
1888 assert_eq!(
1889 scope.visible_tool_names().unwrap(),
1890 ["visible".to_string()].into_iter().collect(),
1891 "deferred tools should be hidden until requested"
1892 );
1893
1894 scope
1895 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
1896 "deferred",
1897 crate::ToolVisibilityWitness {
1898 stable_owner_key: Some("callback:owner-a".to_string()),
1899 last_seen_provenance: deferred.provenance.clone(),
1900 },
1901 )])
1902 .unwrap();
1903
1904 assert_eq!(
1905 scope.visible_tool_names().unwrap(),
1906 ["visible".to_string()].into_iter().collect(),
1907 "staged requests should not publish deferred tools before the next boundary"
1908 );
1909
1910 let applied = scope
1911 .apply_staged(vec![Arc::clone(&visible), Arc::clone(&deferred)].into())
1912 .unwrap();
1913 assert_eq!(
1914 applied.visible_names,
1915 vec!["visible".to_string(), "deferred".to_string()],
1916 "the next boundary should promote requested deferred tools into the visible set"
1917 );
1918 }
1919
1920 #[test]
1921 fn late_deferred_names_stay_hidden_until_requested_after_projection_refresh() {
1922 let scope = ToolScope::new(tools(&["visible"]));
1923
1924 let late_deferred = tools(&["visible", "late_deferred"]);
1925 let visibility_state = scope.visibility_state().unwrap();
1926 let applied = scope
1927 .apply_staged_projection(
1928 late_deferred,
1929 HashSet::new(),
1930 raw_set(&["late_deferred"]),
1931 &visibility_state,
1932 )
1933 .expect("projection refresh should succeed");
1934
1935 assert_eq!(
1936 applied.visible_names,
1937 vec!["visible".to_string()],
1938 "late deferred additions should stay hidden until explicitly requested"
1939 );
1940 }
1941
1942 #[test]
1943 fn snapshot_reflects_active_and_staged_scope_state() {
1944 let scope = ToolScope::new(tools(&["a", "b", "c"]));
1945 let handle = scope.handle();
1946
1947 handle
1948 .stage_external_filter(ToolFilter::Deny(set(&["a"])))
1949 .unwrap();
1950 handle
1951 .set_turn_overlay(Some(raw_set(&["b", "c"])), raw_set(&["c"]))
1952 .unwrap();
1953
1954 let snapshot = scope.snapshot().expect("snapshot should be available");
1955
1956 assert_eq!(
1957 snapshot.known_base_names,
1958 vec!["a".to_string(), "b".to_string(), "c".to_string()]
1959 );
1960 assert_eq!(snapshot.visible_names, vec!["b".to_string()]);
1961 assert_eq!(snapshot.base_filter, ToolFilter::All);
1962 assert_eq!(snapshot.active_external_filter, ToolFilter::All);
1963 assert_eq!(
1964 snapshot.active_turn_allow,
1965 Some(vec!["b".to_string(), "c".to_string()])
1966 );
1967 assert_eq!(snapshot.active_turn_deny, vec!["c".to_string()]);
1968 assert_eq!(snapshot.active_revision, ToolScopeRevision(0));
1969 assert_eq!(snapshot.staged_revision, ToolScopeRevision(1));
1970 assert_eq!(
1971 snapshot.staged_external_filter,
1972 ToolFilter::Deny(set(&["a"]))
1973 );
1974 }
1975
1976 #[test]
1977 fn filter_algebra_is_most_restrictive() {
1978 let allow_a_b = ToolFilter::Allow(set(&["a", "b"]));
1979 let allow_b_c = ToolFilter::Allow(set(&["b", "c"]));
1980 let deny_c = ToolFilter::Deny(set(&["c"]));
1981 let deny_b = ToolFilter::Deny(set(&["b"]));
1982
1983 let composed_allow = ToolScope::compose(&[allow_a_b.clone(), allow_b_c]);
1985 assert!(composed_allow.allows("b"));
1986 assert!(!composed_allow.allows("a"));
1987 assert!(!composed_allow.allows("c"));
1988
1989 let composed_deny = ToolScope::compose(&[deny_c, deny_b.clone()]);
1991 assert!(!composed_deny.allows("b"));
1992 assert!(!composed_deny.allows("c"));
1993 assert!(composed_deny.allows("a"));
1994
1995 let composed_precedence = ToolScope::compose(&[allow_a_b, deny_b]);
1997 assert!(composed_precedence.allows("a"));
1998 assert!(!composed_precedence.allows("b"));
1999 }
2000
2001 #[test]
2002 fn staged_update_is_boundary_only_until_apply_staged() {
2003 let scope = ToolScope::new(tools(&["visible", "secret"]));
2004 let handle = scope.handle();
2005
2006 assert_eq!(
2007 scope
2008 .visible_tools()
2009 .iter()
2010 .map(|t| t.name.to_string())
2011 .collect::<Vec<_>>(),
2012 vec!["visible".to_string(), "secret".to_string()]
2013 );
2014
2015 handle
2016 .stage_external_filter(ToolFilter::Deny(set(&["secret"])))
2017 .unwrap();
2018
2019 assert_eq!(
2021 scope
2022 .visible_tools()
2023 .iter()
2024 .map(|t| t.name.to_string())
2025 .collect::<Vec<_>>(),
2026 vec!["visible".to_string(), "secret".to_string()]
2027 );
2028
2029 let applied = scope
2030 .apply_staged(tools(&["visible", "secret"]))
2031 .expect("boundary apply should succeed");
2032 assert!(applied.visible_changed());
2033 assert_eq!(applied.visible_names, vec!["visible".to_string()]);
2034 assert_eq!(
2035 scope
2036 .visible_tools()
2037 .iter()
2038 .map(|t| t.name.to_string())
2039 .collect::<Vec<_>>(),
2040 vec!["visible".to_string()]
2041 );
2042 }
2043
2044 #[test]
2045 fn boundary_projection_uses_pre_promotion_visibility_for_change_detection() {
2046 let scope = ToolScope::new(tools(&["visible", "secret"]));
2047 let handle = scope.handle();
2048
2049 handle
2050 .stage_external_filter(ToolFilter::Deny(set(&["secret"])))
2051 .unwrap();
2052
2053 let previous_visibility_state = scope.visibility_state().unwrap();
2054 let promoted_visibility_state = scope.promote_staged_visibility().unwrap();
2055 let applied = scope
2056 .apply_staged_projection_with_previous(
2057 tools(&["visible", "secret"]),
2058 ToolNameSet::new(),
2059 ToolNameSet::new(),
2060 &previous_visibility_state,
2061 &promoted_visibility_state,
2062 )
2063 .expect("projection refresh should detect the promoted visibility change");
2064
2065 assert!(applied.visible_changed());
2066 assert_eq!(applied.previous_visible_names, vec!["visible", "secret"]);
2067 assert_eq!(applied.visible_names, vec!["visible"]);
2068 assert_eq!(applied.previous_active_revision, ToolScopeRevision(0));
2069 assert_eq!(applied.applied_revision, ToolScopeRevision(1));
2070 }
2071
2072 #[test]
2073 fn structural_base_change_preserves_dormant_filter_names() {
2074 let scope = ToolScope::new(tools(&["a", "b", "c"]));
2075 let handle = scope.handle();
2076
2077 handle
2078 .stage_external_filter(ToolFilter::Deny(set(&["c"])))
2079 .unwrap();
2080 scope
2081 .apply_staged(tools(&["a", "b", "c"]))
2082 .expect("initial apply should succeed");
2083 assert_eq!(
2084 scope
2085 .visible_tools()
2086 .iter()
2087 .map(|t| t.name.to_string())
2088 .collect::<Vec<_>>(),
2089 vec!["a".to_string(), "b".to_string()]
2090 );
2091
2092 handle
2095 .stage_external_filter(ToolFilter::Allow(set(&["b", "c"])))
2096 .unwrap();
2097
2098 let applied = scope
2099 .apply_staged(tools(&["a", "b"]))
2100 .expect("boundary apply after structural delta should succeed");
2101
2102 assert!(applied.base_changed());
2103 assert_eq!(applied.visible_names, vec!["b".to_string()]);
2104 assert_eq!(
2105 scope
2106 .visible_tools()
2107 .iter()
2108 .map(|t| t.name.to_string())
2109 .collect::<Vec<_>>(),
2110 vec!["b".to_string()]
2111 );
2112 let visibility_state = scope.visibility_state().expect("visibility state");
2113 assert_eq!(
2114 visibility_state.active_filter,
2115 ToolFilter::Allow(set(&["b", "c"])),
2116 "missing names should remain in durable active filter state"
2117 );
2118 assert_eq!(
2119 visibility_state.staged_filter,
2120 ToolFilter::Allow(set(&["b", "c"])),
2121 "missing names should remain in durable staged filter state"
2122 );
2123 }
2124
2125 #[test]
2126 fn requested_witness_mismatch_prevents_rebinding_a_dormant_deferred_name() {
2127 let requested = tool_with_provenance("deferred", "owner-a");
2128 let rebound = tool_with_provenance("deferred", "owner-b");
2129 let scope = ToolScope::new_with_projection_names(
2130 vec![Arc::clone(&requested)].into(),
2131 HashSet::new(),
2132 raw_set(&["deferred"]),
2133 );
2134
2135 scope
2136 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2137 "deferred",
2138 crate::ToolVisibilityWitness {
2139 stable_owner_key: Some("callback:owner-a".to_string()),
2140 last_seen_provenance: requested.provenance.clone(),
2141 },
2142 )])
2143 .unwrap();
2144 let promoted = scope.promote_staged_visibility().unwrap();
2145 scope
2146 .apply_staged_projection(
2147 vec![Arc::clone(&requested)].into(),
2148 HashSet::new(),
2149 raw_set(&["deferred"]),
2150 &promoted,
2151 )
2152 .unwrap();
2153 assert_eq!(
2154 scope.visible_tool_names().unwrap(),
2155 ["deferred".to_string()].into_iter().collect(),
2156 "the matching owner should remain visible"
2157 );
2158
2159 let current = scope.visibility_state().unwrap();
2160 scope
2161 .apply_staged_projection(
2162 vec![Arc::clone(&rebound)].into(),
2163 HashSet::new(),
2164 raw_set(&["deferred"]),
2165 ¤t,
2166 )
2167 .unwrap();
2168 assert!(
2169 scope.visible_tool_names().unwrap().is_empty(),
2170 "a different owner must not inherit prior deferred visibility intent"
2171 );
2172 }
2173
2174 #[test]
2175 fn local_visibility_replace_rejects_empty_deferred_authority() {
2176 let requested = tool_with_provenance("deferred", "owner-a");
2177 let scope = ToolScope::new_with_projection_names(
2178 vec![Arc::clone(&requested)].into(),
2179 HashSet::new(),
2180 raw_set(&["deferred"]),
2181 );
2182
2183 let err = scope
2184 .set_visibility_state(crate::SessionToolVisibilityState {
2185 active_requested_deferred_names: ["deferred".to_string()].into_iter().collect(),
2186 staged_requested_deferred_names: ["deferred".to_string()].into_iter().collect(),
2187 requested_witnesses: [(
2188 "deferred".to_string(),
2189 crate::ToolVisibilityWitness::default(),
2190 )]
2191 .into_iter()
2192 .collect(),
2193 ..Default::default()
2194 })
2195 .expect_err("local replacement must reject empty deferred-tool authority");
2196
2197 assert!(
2198 err.to_string().contains("deferred"),
2199 "rejection should name the missing deferred authority: {err}"
2200 );
2201 assert!(
2202 scope
2203 .visibility_state()
2204 .unwrap()
2205 .active_requested_deferred_names
2206 .is_empty(),
2207 "failed local replacement must not install active deferred names"
2208 );
2209 }
2210
2211 #[test]
2212 fn local_visibility_replace_rejects_mismatched_deferred_authority() {
2213 let requested = tool_with_provenance("deferred", "owner-a");
2214 let scope = ToolScope::new_with_projection_names(
2215 vec![Arc::clone(&requested)].into(),
2216 HashSet::new(),
2217 raw_set(&["deferred"]),
2218 );
2219
2220 let err = scope
2221 .set_visibility_state(crate::SessionToolVisibilityState {
2222 active_requested_deferred_names: ["deferred".to_string()].into_iter().collect(),
2223 staged_requested_deferred_names: ["deferred".to_string()].into_iter().collect(),
2224 requested_witnesses: [(
2225 "deferred".to_string(),
2226 crate::ToolVisibilityWitness {
2227 stable_owner_key: Some("callback:owner-b".to_string()),
2228 last_seen_provenance: Some(ToolProvenance {
2229 kind: ToolSourceKind::Callback,
2230 source_id: "owner-b".into(),
2231 }),
2232 },
2233 )]
2234 .into_iter()
2235 .collect(),
2236 ..Default::default()
2237 })
2238 .expect_err("local replacement must reject forged deferred-tool authority");
2239
2240 assert!(
2241 err.to_string().contains("deferred"),
2242 "rejection should name the mismatched deferred authority: {err}"
2243 );
2244 assert!(
2245 scope
2246 .visibility_state()
2247 .unwrap()
2248 .staged_requested_deferred_names
2249 .is_empty(),
2250 "failed local replacement must not install staged deferred names"
2251 );
2252 }
2253
2254 #[test]
2255 fn name_only_deferred_staging_rejects_without_witness_authority() {
2256 let requested = tool_with_provenance("deferred", "owner-a");
2257 let scope = ToolScope::new_with_projection_names(
2258 vec![Arc::clone(&requested)].into(),
2259 HashSet::new(),
2260 raw_set(&["deferred"]),
2261 );
2262
2263 let err = scope
2264 .stage_requested_deferred_names(["deferred".to_string()].into_iter().collect())
2265 .expect_err("name-only deferred staging must not become authority");
2266
2267 assert_eq!(
2268 err,
2269 ToolScopeStageError::MissingWitnesses {
2270 names: vec!["deferred".to_string()],
2271 }
2272 );
2273 assert!(
2274 scope
2275 .visibility_state()
2276 .unwrap()
2277 .staged_requested_deferred_names
2278 .is_empty(),
2279 "failed name-only staging must not stage deferred names"
2280 );
2281 }
2282
2283 #[test]
2284 fn requested_deferred_authorities_require_provenance_witnesses() {
2285 let requested = tool_with_provenance("deferred", "owner-a");
2286 let scope = ToolScope::new_with_projection_names(
2287 vec![Arc::clone(&requested)].into(),
2288 HashSet::new(),
2289 raw_set(&["deferred"]),
2290 );
2291
2292 let err = scope
2293 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2294 "deferred",
2295 crate::ToolVisibilityWitness {
2296 stable_owner_key: Some("callback:owner-a".to_string()),
2297 last_seen_provenance: None,
2298 },
2299 )])
2300 .expect_err("requesting a deferred tool without provenance authority must fail");
2301
2302 assert!(
2303 err.to_string().contains("deferred"),
2304 "missing-witness error should name the requested tool: {err}"
2305 );
2306 assert!(
2307 scope
2308 .visibility_state()
2309 .unwrap()
2310 .staged_requested_deferred_names
2311 .is_empty(),
2312 "failed witness validation must not stage deferred names"
2313 );
2314 }
2315
2316 #[test]
2317 fn requested_deferred_names_reject_empty_witnesses() {
2318 let requested = tool_with_provenance("deferred", "owner-a");
2319 let scope = ToolScope::new_with_projection_names(
2320 vec![Arc::clone(&requested)].into(),
2321 HashSet::new(),
2322 raw_set(&["deferred"]),
2323 );
2324
2325 let err = scope
2326 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2327 "deferred",
2328 crate::ToolVisibilityWitness::default(),
2329 )])
2330 .expect_err("empty deferred-tool witnesses should fail");
2331
2332 assert!(
2333 err.to_string().contains("deferred"),
2334 "missing-witness error should name the requested tool: {err}"
2335 );
2336 assert!(
2337 scope
2338 .visibility_state()
2339 .unwrap()
2340 .staged_requested_deferred_names
2341 .is_empty(),
2342 "failed empty-witness validation must not stage deferred names"
2343 );
2344 }
2345
2346 #[test]
2347 fn requested_deferred_authorities_reject_mismatched_visible_catalog() {
2348 let requested = tool_with_provenance("deferred", "owner-a");
2349 let scope = ToolScope::new_with_projection_names(
2350 vec![Arc::clone(&requested)].into(),
2351 HashSet::new(),
2352 raw_set(&["deferred"]),
2353 );
2354
2355 let err = scope
2356 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2357 "deferred",
2358 crate::ToolVisibilityWitness {
2359 stable_owner_key: Some("callback:owner-b".to_string()),
2360 last_seen_provenance: Some(ToolProvenance {
2361 kind: ToolSourceKind::Callback,
2362 source_id: "owner-b".into(),
2363 }),
2364 },
2365 )])
2366 .expect_err("mismatched deferred-tool authority should fail");
2367
2368 assert!(
2369 err.to_string().contains("deferred"),
2370 "mismatch error should name the requested tool: {err}"
2371 );
2372 assert!(
2373 scope
2374 .visibility_state()
2375 .unwrap()
2376 .staged_requested_deferred_names
2377 .is_empty(),
2378 "failed mismatch validation must not stage deferred names"
2379 );
2380 }
2381
2382 #[test]
2383 fn requested_deferred_authorities_reject_conflicting_duplicate_authority_values() {
2384 let requested = tool_with_provenance("deferred", "owner-a");
2385 let scope = ToolScope::new_with_projection_names(
2386 vec![Arc::clone(&requested)].into(),
2387 HashSet::new(),
2388 raw_set(&["deferred"]),
2389 );
2390
2391 let err = scope
2392 .add_requested_deferred_authorities(&[
2393 crate::DeferredToolLoadAuthority::new(
2394 "deferred",
2395 crate::ToolVisibilityWitness {
2396 stable_owner_key: Some("callback:owner-a".to_string()),
2397 last_seen_provenance: requested.provenance.clone(),
2398 },
2399 ),
2400 crate::DeferredToolLoadAuthority::new(
2401 "deferred",
2402 crate::ToolVisibilityWitness {
2403 stable_owner_key: Some("callback:forged".to_string()),
2404 last_seen_provenance: Some(ToolProvenance {
2405 kind: ToolSourceKind::Callback,
2406 source_id: "forged".into(),
2407 }),
2408 },
2409 ),
2410 ])
2411 .expect_err("conflicting authority values for one deferred route must fail");
2412
2413 assert!(
2414 err.to_string().contains("deferred"),
2415 "duplicate authority rejection should name the conflicted tool: {err}"
2416 );
2417 assert!(
2418 scope
2419 .visibility_state()
2420 .unwrap()
2421 .staged_requested_deferred_names
2422 .is_empty(),
2423 "failed duplicate-authority validation must not stage deferred names"
2424 );
2425 }
2426
2427 #[test]
2428 fn requested_deferred_names_reuse_existing_witnesses_for_extended_sets() {
2429 let requested_a = tool_with_provenance("deferred_a", "owner-a");
2430 let requested_b = tool_with_provenance("deferred_b", "owner-b");
2431 let scope = ToolScope::new_with_projection_names(
2432 vec![Arc::clone(&requested_a), Arc::clone(&requested_b)].into(),
2433 HashSet::new(),
2434 raw_set(&["deferred_a", "deferred_b"]),
2435 );
2436
2437 scope
2438 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2439 "deferred_a",
2440 crate::ToolVisibilityWitness {
2441 stable_owner_key: Some("callback:owner-a".to_string()),
2442 last_seen_provenance: requested_a.provenance.clone(),
2443 },
2444 )])
2445 .expect("initial deferred request should stage witness");
2446
2447 scope
2448 .add_requested_deferred_authorities(&[crate::DeferredToolLoadAuthority::new(
2449 "deferred_b",
2450 crate::ToolVisibilityWitness {
2451 stable_owner_key: Some("callback:owner-b".to_string()),
2452 last_seen_provenance: requested_b.provenance.clone(),
2453 },
2454 )])
2455 .expect("extended deferred request should reuse already staged witnesses");
2456
2457 let state = scope.visibility_state().unwrap();
2458 assert_eq!(
2459 state.staged_requested_deferred_names,
2460 ["deferred_a".to_string(), "deferred_b".to_string()]
2461 .into_iter()
2462 .collect()
2463 );
2464 assert!(state.requested_witnesses.contains_key("deferred_a"));
2465 assert!(state.requested_witnesses.contains_key("deferred_b"));
2466 }
2467
2468 #[test]
2469 fn filter_witness_mismatch_prevents_rebinding_a_dormant_filter_name() {
2470 let original = tool_with_provenance("a", "owner-a");
2471 let rebound = tool_with_provenance("a", "owner-b");
2472 let visible = tool_with_provenance("b", "owner-b");
2473 let scope = ToolScope::new(vec![Arc::clone(&original), Arc::clone(&visible)].into());
2474 let handle = scope.handle();
2475
2476 handle
2477 .stage_external_filter(ToolFilter::Deny(set(&["a"])))
2478 .unwrap();
2479 scope
2480 .apply_staged(vec![Arc::clone(&original), Arc::clone(&visible)].into())
2481 .unwrap();
2482 assert_eq!(
2483 scope.visible_tool_names().unwrap(),
2484 ["b".to_string()].into_iter().collect(),
2485 "the original owner should be hidden by the deny filter"
2486 );
2487
2488 scope
2489 .apply_staged(vec![Arc::clone(&rebound), Arc::clone(&visible)].into())
2490 .unwrap();
2491 assert_eq!(
2492 scope.visible_tool_names().unwrap(),
2493 ["a".to_string(), "b".to_string()].into_iter().collect(),
2494 "a different owner must not inherit the dormant filter intent"
2495 );
2496 }
2497
2498 #[test]
2499 fn empty_inherited_witness_does_not_become_authority() {
2500 let original = tool_with_provenance("a", "owner-a");
2501 let rebound = tool_with_provenance("a", "owner-b");
2502 let scope = ToolScope::new(vec![Arc::clone(&original)].into());
2503
2504 scope
2505 .set_visibility_state(crate::SessionToolVisibilityState {
2506 inherited_base_filter: ToolFilter::Allow(set(&["a"])),
2507 filter_witnesses: [("a".to_string(), crate::ToolVisibilityWitness::default())]
2508 .into_iter()
2509 .collect(),
2510 ..Default::default()
2511 })
2512 .unwrap();
2513
2514 assert!(
2515 scope.visible_tool_names().unwrap().is_empty(),
2516 "empty inherited witness must fail closed even while the original name is present"
2517 );
2518
2519 let current = scope.visibility_state().unwrap();
2520 scope
2521 .apply_staged_projection(
2522 vec![Arc::clone(&rebound)].into(),
2523 HashSet::new(),
2524 HashSet::new(),
2525 ¤t,
2526 )
2527 .unwrap();
2528 assert!(
2529 scope.visible_tool_names().unwrap().is_empty(),
2530 "empty inherited witness must not rebind to a same-name replacement"
2531 );
2532 }
2533
2534 #[test]
2535 fn inherited_filter_witness_mismatch_prevents_rebinding_a_dormant_name() {
2536 let original = tool_with_provenance("a", "owner-a");
2537 let rebound = tool_with_provenance("a", "owner-b");
2538 let scope = ToolScope::new(vec![Arc::clone(&original)].into());
2539
2540 scope
2541 .set_visibility_state(crate::SessionToolVisibilityState {
2542 inherited_base_filter: ToolFilter::Allow(set(&["a"])),
2543 filter_witnesses: [(
2544 "a".to_string(),
2545 crate::ToolVisibilityWitness {
2546 stable_owner_key: Some("callback:owner-a".to_string()),
2547 last_seen_provenance: original.provenance.clone(),
2548 },
2549 )]
2550 .into_iter()
2551 .collect(),
2552 ..Default::default()
2553 })
2554 .unwrap();
2555
2556 assert_eq!(
2557 scope.visible_tool_names().unwrap(),
2558 ["a".to_string()].into_iter().collect(),
2559 "matching inherited witness should keep the original tool visible"
2560 );
2561
2562 let current = scope.visibility_state().unwrap();
2563 scope
2564 .apply_staged_projection(
2565 vec![Arc::clone(&rebound)].into(),
2566 HashSet::new(),
2567 HashSet::new(),
2568 ¤t,
2569 )
2570 .unwrap();
2571 assert!(
2572 scope.visible_tool_names().unwrap().is_empty(),
2573 "a different owner must not inherit prior inherited-base visibility intent"
2574 );
2575 }
2576
2577 #[test]
2578 fn turn_overlay_is_ephemeral_and_most_restrictive() {
2579 let scope = ToolScope::new(tools(&["a", "b", "c"]));
2580 let handle = scope.handle();
2581
2582 handle
2583 .stage_external_filter(ToolFilter::Allow(set(&["a", "b"])))
2584 .unwrap();
2585 scope
2586 .apply_staged(tools(&["a", "b", "c"]))
2587 .expect("initial apply should succeed");
2588
2589 assert_eq!(
2590 scope
2591 .visible_tools()
2592 .iter()
2593 .map(|t| t.name.to_string())
2594 .collect::<Vec<_>>(),
2595 vec!["a".to_string(), "b".to_string()]
2596 );
2597
2598 handle
2599 .set_turn_overlay(Some(raw_set(&["b", "c"])), raw_set(&["b"]))
2600 .unwrap();
2601 assert_eq!(
2602 scope
2603 .visible_tools()
2604 .iter()
2605 .map(|t| t.name.to_string())
2606 .collect::<Vec<_>>(),
2607 Vec::<String>::new(),
2608 "external allow(a,b) + turn allow(b,c) + turn deny(b) should be empty"
2609 );
2610
2611 handle.clear_turn_overlay();
2612 assert_eq!(
2613 scope
2614 .visible_tools()
2615 .iter()
2616 .map(|t| t.name.to_string())
2617 .collect::<Vec<_>>(),
2618 vec!["a".to_string(), "b".to_string()]
2619 );
2620 }
2621
2622 #[test]
2623 fn set_base_filter_restricts_visible_tools() {
2624 let scope = ToolScope::new(tools(&["a", "b", "c"]));
2625
2626 assert_eq!(
2628 scope
2629 .visible_tools()
2630 .iter()
2631 .map(|t| t.name.to_string())
2632 .collect::<Vec<_>>(),
2633 vec!["a".to_string(), "b".to_string(), "c".to_string()]
2634 );
2635
2636 scope
2638 .set_base_filter(ToolFilter::Allow(set(&["a", "b"])))
2639 .unwrap();
2640
2641 assert_eq!(
2642 scope
2643 .visible_tools()
2644 .iter()
2645 .map(|t| t.name.to_string())
2646 .collect::<Vec<_>>(),
2647 vec!["a".to_string(), "b".to_string()]
2648 );
2649 }
2650
2651 #[test]
2652 fn set_base_filter_composes_with_external_filter() {
2653 let scope = ToolScope::new(tools(&["a", "b", "c"]));
2654 let handle = scope.handle();
2655
2656 scope
2658 .set_base_filter(ToolFilter::Allow(set(&["a", "b"])))
2659 .unwrap();
2660
2661 handle
2663 .stage_external_filter(ToolFilter::Allow(set(&["b", "c"])))
2664 .unwrap();
2665 scope.apply_staged(tools(&["a", "b", "c"])).unwrap();
2666
2667 assert_eq!(
2669 scope
2670 .visible_tools()
2671 .iter()
2672 .map(|t| t.name.to_string())
2673 .collect::<Vec<_>>(),
2674 vec!["b".to_string()]
2675 );
2676 }
2677
2678 #[test]
2679 fn inherited_metadata_key_is_distinct_from_external() {
2680 assert_ne!(
2681 super::INHERITED_TOOL_FILTER_METADATA_KEY,
2682 super::EXTERNAL_TOOL_FILTER_METADATA_KEY
2683 );
2684 }
2685}