Skip to main content

bamboo_domain/session/
tool_authority.rs

1//! Durable Root tool authority, separate from prompt or permission suggestions.
2
3use super::{model_context::ModelContextResetReason, Session, SessionKind};
4
5/// Root product policy, independent of a model call's `ReasoningEffort`.
6/// Ultra delegates planning/execution and keeps verification at the Root.
7/// It is projected from existing durable Root authority, never persisted twice.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum RootThinkingMode {
11    Standard,
12    Ultra,
13}
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16#[error("thinking_mode and root orchestration enabled selection conflict")]
17pub struct RootThinkingModeConflict;
18
19impl RootThinkingMode {
20    /// Optional DTO field: omission is allowed, explicit null is not a mode.
21    pub fn deserialize_selection<'de, D>(deserializer: D) -> Result<Option<Self>, D::Error>
22    where
23        D: serde::Deserializer<'de>,
24    {
25        <Self as serde::Deserialize>::deserialize(deserializer).map(Some)
26    }
27
28    pub fn from_enabled(enabled: bool) -> Self {
29        if enabled {
30            Self::Ultra
31        } else {
32            Self::Standard
33        }
34    }
35
36    pub fn enabled(self) -> bool {
37        self == Self::Ultra
38    }
39
40    /// Normalize canonical and legacy selectors before existing admission.
41    pub fn resolve_selection(
42        mode: Option<Self>,
43        enabled: Option<bool>,
44    ) -> Result<Option<bool>, RootThinkingModeConflict> {
45        if let (Some(mode), Some(enabled)) = (mode, enabled) {
46            if mode.enabled() != enabled {
47                return Err(RootThinkingModeConflict);
48            }
49        }
50        Ok(mode.map(Self::enabled).or(enabled))
51    }
52}
53
54/// Exact execution identities admitted for an orchestration-only Root.
55/// Unknown tools, external providers, and legacy aliases do not enter this
56/// list; callers first resolve a registered execution identity.
57pub const ROOT_ORCHESTRATION_TOOLS: [&str; 18] = [
58    "SubAgent",
59    "spawn_session",
60    "Plan",
61    "Task",
62    "session_history_current",
63    "session_history",
64    "session_message",
65    "Read",
66    "Grep",
67    "Glob",
68    "GetFileInfo",
69    "ViewImage",
70    "work_overview",
71    "work_search",
72    "work_inspect",
73    "work_changes",
74    "work_update",
75    "work_dispatch",
76];
77
78pub fn orchestration_only_allows_execution_name(execution_name: &str) -> bool {
79    ROOT_ORCHESTRATION_TOOLS.contains(&execution_name)
80}
81
82/// Exact Supervisor surface, independent of an ordinary Root's thinking mode.
83/// These are delegation or bounded business views, never raw workspace/file,
84/// process, Skill, Workflow or external-provider execution. Concrete Session
85/// ports still require their live Host instance proof; names grant nothing.
86pub const SUPERVISOR_ORCHESTRATION_TOOLS: [&str; 15] = [
87    "SubAgent",
88    "spawn_session",
89    "Plan",
90    "Task",
91    "session_history_current",
92    "session_history",
93    "session_message",
94    "session_control",
95    "work_overview",
96    "work_search",
97    "work_inspect",
98    "work_changes",
99    "work_update",
100    "work_dispatch",
101    "work_review",
102];
103
104pub fn orchestration_only_allows_execution_name_for_supervisor(
105    execution_name: &str,
106    executing_supervisor: bool,
107) -> bool {
108    if executing_supervisor {
109        SUPERVISOR_ORCHESTRATION_TOOLS.contains(&execution_name)
110    } else {
111        orchestration_only_allows_execution_name(execution_name)
112    }
113}
114
115#[derive(Debug, thiserror::Error, PartialEq, Eq)]
116pub enum RootToolAuthorityError {
117    #[error("only a Root Session may select orchestration-only tool authority")]
118    NotRoot,
119    #[error("Root tool authority revision cannot advance")]
120    RevisionOverflow,
121    #[error("Root tool authority snapshot is stale or inconsistent")]
122    StaleSnapshot,
123    #[error("orchestration-only Root cannot enter while legacy Plan mode is active")]
124    LegacyPlanActive,
125    #[error("orchestration-only Root cannot use a selected Skill or Workflow")]
126    WorkflowSelected,
127}
128
129impl Session {
130    /// A trusted Supervisor's delegation-only business authority is independent
131    /// of thinking mode. Callers still need Storage's canonical identity proof.
132    /// Thinking-mode counters are not business permission epochs. Existing
133    /// positive Work binding revisions remain in their canonical Ticket store;
134    /// the Host verifies their unchanged Supervisor namespace separately.
135    pub fn supervisor_orchestration_revision(&self) -> Option<u64> {
136        self.supervisor_incarnation_id().map(|_| 1)
137    }
138
139    pub fn root_thinking_mode(&self) -> RootThinkingMode {
140        RootThinkingMode::from_enabled(self.root_orchestration_only_enabled())
141    }
142
143    pub fn root_orchestration_only_enabled(&self) -> bool {
144        self.kind == SessionKind::Root
145            && self.parent_session_id.is_none()
146            && self.root_orchestration_only
147    }
148
149    /// Called only by a host-owned selection, never by a model tool.
150    pub fn set_root_orchestration_only(
151        &mut self,
152        enabled: bool,
153    ) -> Result<(), RootToolAuthorityError> {
154        if self.kind != SessionKind::Root || self.parent_session_id.is_some() {
155            return Err(RootToolAuthorityError::NotRoot);
156        }
157        if enabled {
158            self.validate_root_orchestration_compatibility()?;
159        }
160        if self.root_orchestration_only == enabled {
161            return Ok(());
162        }
163        let next = self
164            .root_tool_authority_revision
165            .checked_add(1)
166            .ok_or(RootToolAuthorityError::RevisionOverflow)?;
167        self.root_orchestration_only = enabled;
168        self.root_tool_authority_revision = next;
169        if self.model_context_state.is_some() {
170            self.reset_model_context_epoch(ModelContextResetReason::CacheScopeChanged);
171        }
172        Ok(())
173    }
174
175    /// Keep the narrow tool catalog usable when an old turn or a new request
176    /// carries a mode that requires tools outside the Root allowlist.
177    pub fn validate_root_orchestration_compatibility(&self) -> Result<(), RootToolAuthorityError> {
178        if self
179            .agent_runtime_state
180            .as_ref()
181            .is_some_and(|state| state.plan_mode.is_some())
182        {
183            return Err(RootToolAuthorityError::LegacyPlanActive);
184        }
185        if self.selected_skill_ids().is_some_and(|ids| !ids.is_empty()) {
186            return Err(RootToolAuthorityError::WorkflowSelected);
187        }
188        Ok(())
189    }
190
191    /// Derived model posture, not a persisted selection or permission grant.
192    /// A trusted Supervisor orchestrates in both thinking modes. Children and
193    /// ordinary Roots retain their own independent mode and permissions.
194    pub fn model_orchestration_only_enabled(&self) -> bool {
195        self.root_orchestration_only_enabled() || self.supervisor_incarnation_id().is_some()
196    }
197
198    /// Defense in depth at both catalog projection and final dispatch.
199    pub fn allows_model_tool_execution(&self, execution_name: &str) -> bool {
200        !self.model_orchestration_only_enabled()
201            || orchestration_only_allows_execution_name_for_supervisor(
202                execution_name,
203                self.supervisor_incarnation_id().is_some(),
204            )
205    }
206
207    /// Refresh a running snapshot from the durable control plane before tool
208    /// dispatch. An older snapshot can never loosen a selected Root policy.
209    pub fn adopt_root_tool_authority_from(
210        &mut self,
211        latest: &Session,
212    ) -> Result<(), RootToolAuthorityError> {
213        // A tool policy revision is meaningful only within the same Session
214        // lifetime. A deleted Root ID can be recreated with a fresh birth, so
215        // matching the ID and revision alone would authorize an old SDK run.
216        fn root_id(session: &Session) -> &str {
217            if session.kind == SessionKind::Root && session.root_session_id.is_empty() {
218                session.id.as_str()
219            } else {
220                session.root_session_id.as_str()
221            }
222        }
223        if !super::compatibility::same_session_tool_contract(
224            self.session_version,
225            latest.session_version,
226        ) || self.id != latest.id
227            || self.kind != latest.kind
228            || self.created_at != latest.created_at
229            || self.authority_identity != latest.authority_identity
230            || self.parent_session_id != latest.parent_session_id
231            || root_id(self) != root_id(latest)
232            || self.spawn_depth != latest.spawn_depth
233            || (self.root_orchestration_only && self.root_tool_authority_revision == 0)
234            || (latest.root_orchestration_only && latest.root_tool_authority_revision == 0)
235            || latest.root_tool_authority_revision < self.root_tool_authority_revision
236            || (latest.root_tool_authority_revision == self.root_tool_authority_revision
237                && latest.root_orchestration_only != self.root_orchestration_only)
238            || latest.root_mode_transition_epoch < self.root_mode_transition_epoch
239            || (latest.root_mode_transition_epoch == self.root_mode_transition_epoch
240                && latest.root_mode_operations != self.root_mode_operations)
241        {
242            return Err(RootToolAuthorityError::StaleSnapshot);
243        }
244        let selections = latest.root_tool_authority_revision - self.root_tool_authority_revision;
245        self.root_orchestration_only = latest.root_orchestration_only;
246        self.root_tool_authority_revision = latest.root_tool_authority_revision;
247        self.root_mode_transition_epoch = latest.root_mode_transition_epoch;
248        self.root_mode_operations = latest.root_mode_operations.clone();
249        if selections > 0 {
250            // A running snapshot may miss several selections, including a
251            // round trip to the same mode. Adopt the durable invalidation;
252            // resetting the local ledger once could retain an older epoch.
253            self.model_context_state = latest.model_context_state.clone();
254            self.provider_transcript.invalidate_repeated(
255                super::provider_transcript::ProviderTranscriptResetReason::CacheScopeChanged,
256                selections,
257            );
258        }
259        Ok(())
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::session::{AgentRuntimeState, ModelContextState, PlanModeState, PlanModeStatus};
267
268    #[test]
269    fn supervisor_direct_files_and_processes_require_delegation_in_both_modes() {
270        use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
271        let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
272        supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
273            incarnation_id: uuid::Uuid::new_v4(),
274        };
275        for ultra in [false, true, false] {
276            supervisor.set_root_orchestration_only(ultra).unwrap();
277            let restored: Session =
278                serde_json::from_value(serde_json::to_value(&supervisor).unwrap()).unwrap();
279            for name in [
280                "Read",
281                "Grep",
282                "Glob",
283                "GetFileInfo",
284                "ViewImage",
285                "Write",
286                "Edit",
287                "NotebookEdit",
288                "apply_patch",
289                "Bash",
290                "exec_command",
291                "write_stdin",
292                "load_skill",
293                "workflow_run",
294                "browser",
295                "browser_eval",
296                "mcp__vendor__Read",
297                "default::Read",
298                "vendor::Read",
299            ] {
300                assert!(
301                    !restored.allows_model_tool_execution(name),
302                    "Supervisor mode={ultra} must delegate {name}"
303                );
304            }
305            for name in [
306                "SubAgent",
307                "Plan",
308                "Task",
309                "session_control",
310                "session_history",
311            ] {
312                assert!(restored.allows_model_tool_execution(name), "{name}");
313            }
314            let child = Session::new_child_of("worker", &restored, "model", "worker");
315            assert!(child.authority_identity.is_ordinary());
316            assert!(child.allows_model_tool_execution("Read"));
317            assert!(child.allows_model_tool_execution("Edit"));
318            assert!(child.allows_model_tool_execution("exec_command"));
319        }
320        let mut ordinary = Session::new("ordinary", "model");
321        for ultra in [false, true] {
322            ordinary.set_root_orchestration_only(ultra).unwrap();
323            assert!(ordinary.allows_model_tool_execution("Read"));
324            assert_eq!(ordinary.supervisor_incarnation_id(), None);
325            // Standard's coarse tool policy is not the concrete Host port's
326            // management authorization. Keep its existing surface unchanged.
327            if ultra {
328                assert!(!ordinary.allows_model_tool_execution("session_control"));
329            }
330        }
331    }
332
333    #[test]
334    fn supervisor_delegation_first_does_not_depend_on_ultra_or_leak_to_children() {
335        use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
336        let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
337        supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
338            incarnation_id: uuid::Uuid::new_v4(),
339        };
340        for ultra in [false, true, false] {
341            supervisor.set_root_orchestration_only(ultra).unwrap();
342            let restored: Session =
343                serde_json::from_value(serde_json::to_value(&supervisor).unwrap()).unwrap();
344            for name in SUPERVISOR_ORCHESTRATION_TOOLS {
345                assert!(
346                    restored.allows_model_tool_execution(name),
347                    "{ultra}: {name}"
348                );
349            }
350            for name in [
351                "exec_command",
352                "write_stdin",
353                "Bash",
354                "Edit",
355                "Write",
356                "apply_patch",
357                "load_skill",
358                "workflow_run",
359                "mcp__vendor__read",
360                "default::Read",
361            ] {
362                assert!(
363                    !restored.allows_model_tool_execution(name),
364                    "Supervisor {ultra}: {name}"
365                );
366            }
367            let child = Session::new_child_of("worker", &restored, "model", "worker");
368            assert!(child.authority_identity.is_ordinary());
369            assert!(child.allows_model_tool_execution("Edit"));
370            assert!(child.allows_model_tool_execution("exec_command"));
371        }
372        let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
373        ordinary.metadata.insert("role".into(), "supervisor".into());
374        assert!(ordinary.allows_model_tool_execution("Edit"));
375        ordinary.set_root_orchestration_only(true).unwrap();
376        assert!(!ordinary.allows_model_tool_execution("session_control"));
377        assert!(ordinary.allows_model_tool_execution("session_history"));
378    }
379
380    #[test]
381    fn trusted_supervisor_session_management_survives_thinking_mode_selection() {
382        use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
383        let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
384        supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
385            incarnation_id: uuid::Uuid::new_v4(),
386        };
387        for ultra in [false, true] {
388            supervisor.set_root_orchestration_only(ultra).unwrap();
389            let restored: Session =
390                serde_json::from_value(serde_json::to_value(&supervisor).unwrap()).unwrap();
391            for name in ["session_control", "session_history"] {
392                assert!(
393                    restored.allows_model_tool_execution(name),
394                    "trusted Supervisor lost {name} in {:?}",
395                    restored.root_thinking_mode()
396                );
397            }
398        }
399    }
400
401    #[test]
402    fn ultra_management_exception_requires_canonical_supervisor_and_exact_tool() {
403        use crate::{SessionAuthorityIdentity, DEFAULT_SUPERVISOR_SESSION_ID};
404        let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
405        supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
406            incarnation_id: uuid::Uuid::new_v4(),
407        };
408        supervisor.set_root_orchestration_only(true).unwrap();
409        for mutation in [
410            |s: &mut Session| s.authority_identity = SessionAuthorityIdentity::Ordinary,
411            |s: &mut Session| s.id = "other".into(),
412            |s: &mut Session| s.root_session_id = "other".into(),
413            |s: &mut Session| s.spawn_depth = 1,
414            |s: &mut Session| {
415                s.authority_identity = SessionAuthorityIdentity::Supervisor {
416                    incarnation_id: uuid::Uuid::nil(),
417                }
418            },
419        ] {
420            let mut invalid = supervisor.clone();
421            mutation(&mut invalid);
422            invalid.metadata.insert("role".into(), "supervisor".into());
423            assert!(!invalid.allows_model_tool_execution("session_control"));
424            assert!(invalid.allows_model_tool_execution("session_history"));
425        }
426        for name in [
427            "default::session_control",
428            "vendor::session_control",
429            "exec_command",
430            "Write",
431        ] {
432            assert!(!supervisor.allows_model_tool_execution(name), "{name}");
433        }
434        let child = Session::new_child_of("child", &supervisor, "model", "child");
435        assert_eq!(child.supervisor_incarnation_id(), None);
436        let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
437        ordinary.set_root_orchestration_only(true).unwrap();
438        ordinary.metadata.insert("role".into(), "supervisor".into());
439        assert!(!ordinary.allows_model_tool_execution("session_control"));
440        assert!(ordinary.allows_model_tool_execution("session_history"));
441    }
442
443    #[test]
444    fn thinking_mode_is_a_derived_root_policy_independent_of_effort() {
445        use crate::ReasoningEffort;
446        let mut root = Session::new("ultra-root", "model");
447        root.reasoning_effort = Some(ReasoningEffort::Max);
448        root.set_root_orchestration_prompt_enabled(true);
449        assert_eq!(root.root_thinking_mode(), RootThinkingMode::Standard);
450        root.set_root_orchestration_only(true).unwrap();
451        let persisted = serde_json::to_value(&root).unwrap();
452        assert!(
453            persisted.get("thinking_mode").is_none(),
454            "no second persisted authority"
455        );
456        let reloaded: Session = serde_json::from_value(persisted.clone()).unwrap();
457        assert_eq!(reloaded.root_thinking_mode(), RootThinkingMode::Ultra);
458        assert_eq!(reloaded.reasoning_effort, Some(ReasoningEffort::Max));
459        assert_eq!(
460            serde_json::to_value(&reloaded).unwrap(),
461            persisted,
462            "projection never writes"
463        );
464        let mut child = Session::new_child_of("ultra-child", &reloaded, "model", "child");
465        assert_eq!(child.root_thinking_mode(), RootThinkingMode::Standard);
466        assert_eq!(child.reasoning_effort, None);
467        child.root_orchestration_only = true;
468        assert_eq!(
469            child.root_thinking_mode(),
470            RootThinkingMode::Standard,
471            "raw child bool has no authority"
472        );
473        assert_eq!(
474            child.set_root_orchestration_only(true),
475            Err(RootToolAuthorityError::NotRoot)
476        );
477        assert_eq!(
478            serde_json::to_string(&RootThinkingMode::Ultra).unwrap(),
479            "\"ultra\""
480        );
481        assert_eq!(
482            serde_json::from_str::<RootThinkingMode>("\"standard\"").unwrap(),
483            RootThinkingMode::Standard
484        );
485        assert!(serde_json::from_str::<RootThinkingMode>("\"max\"").is_err());
486        assert!(serde_json::from_str::<ReasoningEffort>("\"ultra\"").is_err());
487    }
488
489    #[test]
490    fn canonical_and_legacy_thinking_selectors_share_one_boolean_request() {
491        assert_eq!(RootThinkingMode::resolve_selection(None, None), Ok(None));
492        for enabled in [false, true] {
493            let mode = RootThinkingMode::from_enabled(enabled);
494            assert_eq!(
495                RootThinkingMode::resolve_selection(Some(mode), None),
496                Ok(Some(enabled))
497            );
498            assert_eq!(
499                RootThinkingMode::resolve_selection(None, Some(enabled)),
500                Ok(Some(enabled))
501            );
502            assert_eq!(
503                RootThinkingMode::resolve_selection(Some(mode), Some(enabled)),
504                Ok(Some(enabled))
505            );
506            assert_eq!(
507                RootThinkingMode::resolve_selection(Some(mode), Some(!enabled)),
508                Err(RootThinkingModeConflict)
509            );
510        }
511    }
512
513    #[test]
514    fn root_selection_is_persistent_and_child_does_not_inherit_it() {
515        let mut root = Session::new("root", "model");
516        root.set_root_orchestration_only(true).unwrap();
517        assert_eq!(root.root_tool_authority_revision, 1);
518        let reloaded: Session =
519            serde_json::from_str(&serde_json::to_string(&root).unwrap()).unwrap();
520        assert!(reloaded.root_orchestration_only_enabled());
521        let mut child = Session::new_child_of("child", &reloaded, "model", "child");
522        assert!(!child.root_orchestration_only_enabled());
523        assert_eq!(child.root_tool_authority_revision, 0);
524        assert_eq!(
525            child.set_root_orchestration_only(false),
526            Err(RootToolAuthorityError::NotRoot)
527        );
528        assert!(child.allows_model_tool_execution("Bash"));
529    }
530
531    #[test]
532    fn root_allowlist_is_exact_and_denies_unknown_tools() {
533        let mut root = Session::new("root", "model");
534        root.set_root_orchestration_only(true).unwrap();
535        for tool in ROOT_ORCHESTRATION_TOOLS {
536            assert!(root.allows_model_tool_execution(tool), "{tool}");
537        }
538        for tool in [
539            "exec_command",
540            "write_stdin",
541            "Bash",
542            "Edit",
543            "Write",
544            "WebFetch",
545            "workflow_run",
546            "mcp__x__read",
547            "read_file",
548            "default::Read",
549        ] {
550            assert!(!root.allows_model_tool_execution(tool), "{tool}");
551        }
552        assert_eq!(root.root_tool_authority_revision, 1);
553        root.set_root_orchestration_only(true).unwrap();
554        assert_eq!(root.root_tool_authority_revision, 1);
555        root.set_root_orchestration_only(false).unwrap();
556        assert_eq!(root.root_tool_authority_revision, 2);
557        assert!(root.allows_model_tool_execution("Bash"));
558    }
559
560    #[test]
561    fn durable_refresh_adopts_tightening_and_rejects_stale_loosen() {
562        let mut running = Session::new("root", "model");
563        let mut durable = running.clone();
564        durable.set_root_orchestration_only(true).unwrap();
565        running.adopt_root_tool_authority_from(&durable).unwrap();
566        assert!(!running.allows_model_tool_execution("Bash"));
567        assert_eq!(
568            durable.adopt_root_tool_authority_from(&Session::new("root", "model")),
569            Err(RootToolAuthorityError::StaleSnapshot)
570        );
571        durable.set_root_orchestration_only(false).unwrap();
572        running.adopt_root_tool_authority_from(&durable).unwrap();
573        assert!(running.allows_model_tool_execution("Bash"));
574    }
575
576    #[test]
577    fn durable_refresh_rejects_a_different_birth_identity_or_lineage() {
578        let running = Session::new("root", "model");
579        let mut other_birth = running.clone();
580        other_birth.created_at += chrono::Duration::microseconds(1);
581        assert_eq!(
582            running.clone().adopt_root_tool_authority_from(&other_birth),
583            Err(RootToolAuthorityError::StaleSnapshot)
584        );
585
586        let mut other_identity = running.clone();
587        other_identity.authority_identity = super::super::SessionAuthorityIdentity::Supervisor {
588            incarnation_id: uuid::Uuid::new_v4(),
589        };
590        assert_eq!(
591            running
592                .clone()
593                .adopt_root_tool_authority_from(&other_identity),
594            Err(RootToolAuthorityError::StaleSnapshot)
595        );
596
597        let mut other_lineage = running.clone();
598        other_lineage.root_session_id = "another-root".into();
599        assert_eq!(
600            running
601                .clone()
602                .adopt_root_tool_authority_from(&other_lineage),
603            Err(RootToolAuthorityError::StaleSnapshot)
604        );
605        other_lineage = running.clone();
606        other_lineage.spawn_depth = 1;
607        assert_eq!(
608            running
609                .clone()
610                .adopt_root_tool_authority_from(&other_lineage),
611            Err(RootToolAuthorityError::StaleSnapshot)
612        );
613
614        // Legacy Roots can omit this derived field without changing lineage.
615        let mut legacy = running.clone();
616        legacy.root_session_id.clear();
617        legacy.adopt_root_tool_authority_from(&running).unwrap();
618    }
619
620    #[test]
621    fn root_selection_rejects_legacy_plan_or_workflow_that_needs_other_tools() {
622        let mut root = Session::new("root", "model");
623        let runtime = AgentRuntimeState {
624            plan_mode: Some(PlanModeState {
625                entered_at: chrono::Utc::now(),
626                pre_permission_mode: "default".to_string(),
627                plan_file_path: None,
628                status: PlanModeStatus::Exploring,
629            }),
630            ..AgentRuntimeState::default()
631        };
632        root.agent_runtime_state = Some(runtime);
633        assert_eq!(
634            root.set_root_orchestration_only(true),
635            Err(RootToolAuthorityError::LegacyPlanActive)
636        );
637        root.agent_runtime_state = None;
638        root.set_selected_skill_ids(vec!["workflow".to_string()]);
639        assert_eq!(
640            root.set_root_orchestration_only(true),
641            Err(RootToolAuthorityError::WorkflowSelected)
642        );
643        root.clear_selected_skill_ids();
644        root.set_root_orchestration_only(true).unwrap();
645        root.set_selected_skill_ids(vec!["workflow".to_string()]);
646        assert_eq!(
647            root.validate_root_orchestration_compatibility(),
648            Err(RootToolAuthorityError::WorkflowSelected)
649        );
650    }
651
652    #[test]
653    fn live_authority_adoption_resets_prior_model_context_epoch() {
654        for enabled in [false, true] {
655            let mut running = Session::new("root", "model");
656            if !enabled {
657                running.set_root_orchestration_only(true).unwrap();
658            }
659            running.model_context_state = Some(ModelContextState {
660                prefix_epoch: 4,
661                cache_scope_sha256: Some("prior-provider-scope".to_string()),
662                ..ModelContextState::default()
663            });
664            let mut durable = running.clone();
665            durable.set_root_orchestration_only(enabled).unwrap();
666            running.adopt_root_tool_authority_from(&durable).unwrap();
667            let context = running.model_context_state.as_ref().unwrap();
668            assert_eq!(context.prefix_epoch, 5);
669            assert!(context.cache_scope_sha256.is_none());
670            assert_eq!(
671                context.last_reset_reason,
672                Some(ModelContextResetReason::CacheScopeChanged)
673            );
674        }
675    }
676
677    #[test]
678    fn live_authority_adoption_preserves_all_missed_mode_resets() {
679        let mut running = Session::new("root", "model");
680        running.model_context_state = Some(ModelContextState {
681            prefix_epoch: 4,
682            cache_scope_sha256: Some("old scope".into()),
683            ..Default::default()
684        });
685        running
686            .activate_provider_transcript_route(
687                super::super::provider_transcript::ProviderFamily::OpenAi,
688                super::super::provider_transcript::ProviderProtocol::OpenAiResponsesV1,
689                &"a".repeat(64),
690            )
691            .unwrap();
692        let mut durable = running.clone();
693        durable.set_root_orchestration_only(true).unwrap();
694        // A prepared round between selections makes the second reset a new
695        // ledger boundary rather than the existing pending-reset coalescing.
696        durable
697            .model_context_state
698            .as_mut()
699            .unwrap()
700            .cache_scope_sha256 = Some("new scope".into());
701        durable.set_root_orchestration_only(false).unwrap();
702        assert_eq!(running.root_thinking_mode(), durable.root_thinking_mode());
703        running.adopt_root_tool_authority_from(&durable).unwrap();
704        assert_eq!(running.model_context_state, durable.model_context_state);
705        assert_eq!(
706            running.model_context_state.as_ref().unwrap().prefix_epoch,
707            6
708        );
709        assert_eq!(running.provider_transcript, durable.provider_transcript);
710        // A recovery-only epoch advance must preserve current owner work.
711        running
712            .model_context_state
713            .as_mut()
714            .unwrap()
715            .cache_scope_sha256 = Some("owner scope".into());
716        let owner_context = running.model_context_state.clone();
717        durable.root_mode_transition_epoch += 1;
718        running.adopt_root_tool_authority_from(&durable).unwrap();
719        assert_eq!(running.model_context_state, owner_context);
720    }
721}