Skip to main content

meerkat_core/
tool_scope.rs

1//! Tool visibility scope and external filter staging.
2
3use 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/// Visibility filter for tools.
16#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum ToolFilter {
18    /// All tools are visible.
19    #[default]
20    All,
21    /// Only listed tools are visible.
22    Allow(ToolNameSet),
23    /// Listed tools are hidden.
24    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
36/// Session metadata key storing the persisted external tool filter.
37pub const EXTERNAL_TOOL_FILTER_METADATA_KEY: &str = "tool_scope_external_filter";
38
39/// Session metadata key storing the inherited tool filter from a parent agent.
40///
41/// When a child session is created from a parent mob, the parent's visible tool
42/// set is captured as a base filter. On session rebuild, this key is recovered
43/// and applied as the `ToolScope` base filter.
44pub const INHERITED_TOOL_FILTER_METADATA_KEY: &str = "tool_scope_inherited_filter";
45
46/// Monotonic revision for staged external visibility updates.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48#[repr(transparent)]
49pub struct ToolScopeRevision(pub u64);
50
51/// Diagnostic snapshot of the current live tool-scope state.
52#[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/// Global phase of the live external tool-surface machine.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum ExternalToolSurfaceGlobalPhase {
69    Operating,
70    Shutdown,
71}
72
73/// Base lifecycle state for one external tool surface.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum ExternalToolSurfaceBaseState {
76    Absent,
77    Active,
78    Removing,
79    Removed,
80}
81
82/// Pending async operation for one external tool surface.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ExternalToolSurfacePendingOp {
85    None,
86    Add,
87    Reload,
88}
89
90/// Staged-but-not-yet-applied operation for one external tool surface.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ExternalToolSurfaceStagedOp {
93    None,
94    Add,
95    Remove,
96    Reload,
97}
98
99/// Last emitted lifecycle delta operation for one external tool surface.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ExternalToolSurfaceDeltaOperation {
102    None,
103    Add,
104    Remove,
105    Reload,
106}
107
108/// Last emitted lifecycle delta phase for one external tool surface.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum ExternalToolSurfaceDeltaPhase {
111    None,
112    Pending,
113    Applied,
114    Draining,
115    Failed,
116    Forced,
117}
118
119/// Closed cause set for external tool surface failures and call rejections.
120///
121/// These codes are the stable external projection for MCP/router callers. Keep
122/// `as_str` and serde in snake_case because older consumers already observe
123/// these string codes at the surface boundary.
124#[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/// Diagnostic snapshot of one external tool surface entry.
149///
150/// This is an observational surface over the canonical external-tool authority.
151/// It does not create a second semantic owner.
152#[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/// Diagnostic snapshot of the live external tool-surface machine state.
169///
170/// This keeps external tool mutation lineage and publication alignment visible
171/// for MeerkatMachine mapping work without changing who owns the underlying
172/// lifecycle truth.
173#[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
209/// Canonical owner for durable tool-visibility state.
210///
211/// `ToolScope` reads committed/staged visibility from this trait and routes all
212/// durable visibility mutations through it, so the projection bridge does not
213/// become a competing owner.
214pub 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/// Local in-process owner used when no runtime-backed MeerkatMachine owner is supplied.
258#[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/// Composed filter representation using most-restrictive semantics.
419#[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/// Runtime tool scope used to determine provider-visible tools.
433#[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    /// Build a scope with no base restriction.
467    pub fn new(base_tools: Arc<[Arc<ToolDef>]>) -> Self {
468        Self::new_with_projection_names(base_tools, HashSet::new(), HashSet::new())
469    }
470
471    /// Build a scope with an explicit set of control-plane tool names.
472    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    /// Build a scope with explicit control-plane and deferred-session names.
480    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    /// Build a scope with an explicit durable visibility owner.
494    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    /// Returns the currently visible tools using base + active external filter composition.
527    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    /// Returns current visible tools, or an explicit error for boundary fail-safe handling.
535    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    /// Return a handle for thread-safe staged external updates.
563    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    /// Snapshot the current live scope state for diagnostics.
571    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    /// Atomically apply staged state at CallingLlm boundary.
594    ///
595    /// Sequence:
596    /// 1) Refresh base from dispatcher snapshot.
597    /// 2) Prune active/pending filters against base deltas.
598    /// 3) Apply staged external filter revision.
599    /// 4) Compute visible tools.
600    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    /// Promote the durable staged visibility state to the active boundary state.
626    pub fn promote_staged_visibility(
627        &self,
628    ) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
629        self.visibility_owner.boundary_applied()
630    }
631
632    /// Atomically apply refreshed projection inputs against a supplied boundary state.
633    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    /// Compose filters with most-restrictive semantics.
723    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    /// Helper: intersection for allow-list composition.
746    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    /// Helper: union for deny-list composition.
754    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    /// Set the base filter for this scope.
978    ///
979    /// The base filter is the most fundamental restriction layer — it is
980    /// composed with external and turn-level filters using most-restrictive
981    /// semantics. This is used for inherited tool visibility from a parent
982    /// agent's scope.
983    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    /// Replace the durable tool visibility state carried by this projection bridge.
1000    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    /// Snapshot the current durable visibility state.
1009    pub fn visibility_state(&self) -> Result<SessionToolVisibilityState, ToolScopeApplyError> {
1010        self.visibility_owner.visibility_state()
1011    }
1012
1013    /// Return the names currently visible to the session plane.
1014    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    /// Return whether the staged durable session filters would allow a session-plane tool name
1024    /// to become visible after the next boundary.
1025    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    /// Return the current base tool snapshot.
1058    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    /// Return the current base tool names.
1066    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    /// Force the live projection closed after a boundary apply failure.
1080    ///
1081    /// This updates the projection state itself, not only the caller's local
1082    /// provider tool list, so later dispatch prechecks cannot accidentally
1083    /// observe the previous full tool set before the next healthy boundary.
1084    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    /// Return the currently configured active and staged revisions.
1099    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    /// Return any requested deferred names that are not currently present in the base snapshot.
1108    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    /// Return any durable filter names that are not currently present in the base snapshot.
1123    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    /// Record durable requested deferred names for the next boundary.
1137    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    /// Add durable requested deferred authorities for the next boundary.
1145    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/// Thread-safe handle for staging external scope updates.
1192#[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    /// Stage an external filter update and return its monotonic revision.
1218    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    /// Set or clear an ephemeral per-turn overlay.
1260    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    /// Clear ephemeral per-turn overlay.
1288    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        // Allow lists intersect.
1984        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        // Deny lists union.
1990        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        // Deny wins after allow.
1996        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        // Still unchanged until boundary apply.
2020        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        // Pending filter still references `c`, and the durable state should
2093        // preserve that dormant intent even after the base snapshot shrinks.
2094        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                &current,
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                &current,
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                &current,
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        // All visible initially
2627        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        // Set base filter to allow only a and b
2637        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        // Base restricts to a and b
2657        scope
2658            .set_base_filter(ToolFilter::Allow(set(&["a", "b"])))
2659            .unwrap();
2660
2661        // External further restricts to b and c
2662        handle
2663            .stage_external_filter(ToolFilter::Allow(set(&["b", "c"])))
2664            .unwrap();
2665        scope.apply_staged(tools(&["a", "b", "c"])).unwrap();
2666
2667        // Most-restrictive: intersection = b only
2668        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}