Skip to main content

agent_engine/
orchestration.rs

1use agent_core::orchestration::{
2    CatalogSnapshot, CompletionGate, DelegationPolicy, WorkerRegistry, WorkerRole, WorkerTerminal,
3    WorkerWritePolicy,
4};
5use agent_core::prompt::QualifiedModelId;
6use serde::Serialize;
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::sync::Mutex;
9
10pub struct DelegationTreeBudget {
11    pub max_depth: u16,
12    pub max_children_per_worker: usize,
13    pub max_total_descendants: usize,
14}
15
16#[derive(Debug, Clone, Eq, PartialEq)]
17pub enum DelegationTreeDenied {
18    DepthLimit,
19    ChildLimit,
20    DescendantLimit,
21    UnknownParent,
22}
23
24#[derive(Debug, Default)]
25struct DelegationTreeState {
26    depth_by_worker: BTreeMap<String, u16>,
27    parent_by_worker: BTreeMap<String, String>,
28    child_count: BTreeMap<String, usize>,
29    descendants: usize,
30}
31
32/// Session-scoped runtime enforcement shared by every subagent tool path.
33pub struct OrchestrationRuntime {
34    inner: Mutex<Inner>,
35    tree_budget: DelegationTreeBudget,
36    tree: Mutex<DelegationTreeState>,
37}
38struct Inner {
39    registry: WorkerRegistry,
40    handles: HashMap<String, agent_core::orchestration::WorkerHandle>,
41}
42
43#[derive(Clone, Debug, Serialize)]
44pub struct AuthorizedWorkerModel {
45    pub model: QualifiedModelId,
46    pub selection_source: SelectionSource,
47    pub network_attempted: bool,
48    pub policy_digest: String,
49    pub catalog_snapshot_id: String,
50    pub catalog_digest: String,
51    pub correlation_id: String,
52    pub cross_provider_grant_id: Option<String>,
53}
54#[derive(Clone, Copy, Debug, Serialize)]
55#[serde(rename_all = "snake_case")]
56pub enum SelectionSource {
57    ForegroundInheritance,
58    ExplicitRequest,
59}
60impl SelectionSource {
61    pub fn as_str(self) -> &'static str {
62        match self {
63            Self::ForegroundInheritance => "foreground_inheritance",
64            Self::ExplicitRequest => "explicit_request",
65        }
66    }
67}
68#[derive(Clone, Debug, Serialize)]
69pub struct DispatchDenial {
70    pub kind: &'static str,
71    pub code: &'static str,
72    pub requested_model: Option<String>,
73    pub foreground_model: String,
74    pub selection_source: SelectionSource,
75    pub network_attempted: bool,
76    pub remediation: &'static str,
77}
78impl std::fmt::Display for DispatchDenial {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(
81            f,
82            "{}",
83            serde_json::to_string(self).unwrap_or_else(|_| self.code.into())
84        )
85    }
86}
87impl std::error::Error for DispatchDenial {}
88
89/// Canonicalize a runtime foreground model string into its exact qualified
90/// identity. Legacy configs persist bare Anthropic IDs (e.g. `claude-fable-5`)
91/// which the runtime router still deliberately routes to `anthropic`; the
92/// canonical identity is derived from that single routing entry point — never
93/// from substring heuristics. Unroutable values fail closed.
94pub fn canonical_foreground_identity(raw: &str) -> Result<QualifiedModelId, String> {
95    if let Ok(model) = QualifiedModelId::parse(raw) {
96        return Ok(model);
97    }
98    let route = crate::runtime::openai::resolve_route(raw)
99        .ok_or_else(|| format!("foreground model is unresolved: {raw}"))?;
100    QualifiedModelId::parse(format!("{}/{}", route.provider, route.model))
101        .map_err(|e| e.to_string())
102}
103
104/// Validate an exact identity against source-controlled runtime routing/catalog
105/// descriptors before allowing a model-driven session grant. This operation is
106/// credential- and network-blind: availability is checked later by normal
107/// provider execution, while invented identities fail before policy mutation.
108pub fn validate_user_authorizable_model(raw: &str) -> Result<QualifiedModelId, String> {
109    let model = QualifiedModelId::parse(raw)
110        .map_err(|_| "authorization denied: invalid qualified model".to_string())?;
111    let known = if model.provider() == "anthropic" {
112        agent_core::models::KNOWN_MODELS
113            .iter()
114            .any(|(id, _)| *id == model.model())
115    } else if model.provider() == "openai-codex" {
116        crate::runtime::openai::catalog::codex_static_catalog_models()
117            .iter()
118            .any(|entry| entry.runtime_id() == model.as_str())
119    } else if model.provider() == "xai-auth" {
120        crate::runtime::openai::catalog::xai_model(model.model()).is_some()
121    } else if model.provider() == "github-copilot" {
122        crate::runtime::openai::catalog::github_copilot_runtime_model(model.model()).is_some()
123    } else if model.provider() == "google-gemini" {
124        crate::runtime::openai::catalog::google_gemini_model(model.model()).is_some()
125    } else {
126        crate::runtime::openai::registry::providers()
127            .iter()
128            .find(|provider| provider.key == model.provider())
129            .is_some_and(|provider| {
130                provider
131                    .models
132                    .iter()
133                    .any(|(id, _, _)| *id == model.model())
134            })
135    };
136    if !known || crate::runtime::openai::resolve_route(model.as_str()).is_none() {
137        return Err(format!(
138            "authorization denied: '{}' is not a known routable model",
139            model.as_str()
140        ));
141    }
142    Ok(model)
143}
144
145/// Exact source-controlled OpenRouter worker stack. These identities are
146/// trusted local descriptors (not live catalog results or historical logs).
147const OPENROUTER_WORKER_MODELS: &[&str] = &[
148    "openrouter/deepseek/deepseek-v4-pro",
149    "openrouter/moonshotai/kimi-k2.7-code",
150    "openrouter/z-ai/glm-5.2",
151];
152
153/// Exact manifestless worker choices for a foreground provider.
154fn manifestless_worker_choices(foreground: &QualifiedModelId) -> Vec<QualifiedModelId> {
155    let source = if foreground.provider() == "openrouter" {
156        OPENROUTER_WORKER_MODELS
157    } else {
158        &[]
159    };
160    let mut choices: Vec<_> = source
161        .iter()
162        .filter_map(|value| QualifiedModelId::parse(*value).ok())
163        .collect();
164    choices.push(foreground.clone());
165    choices.sort();
166    choices.dedup();
167    choices
168}
169
170impl OrchestrationRuntime {
171    /// Build a snapshot exclusively from runtime-owned routing/catalog descriptors.
172    /// `manifest_references` are deliberately ignored here: manifests may narrow the
173    /// snapshot, but can never create catalog membership. The foreground is admitted
174    /// only after the credential-free runtime router resolves its exact identity.
175    pub fn trusted_catalog(
176        foreground: &QualifiedModelId,
177        _manifest_references: impl IntoIterator<Item = QualifiedModelId>,
178    ) -> Result<CatalogSnapshot, &'static str> {
179        use crate::runtime::openai::catalog;
180        use agent_core::orchestration::CatalogEntry;
181
182        let route = crate::runtime::openai::resolve_route(foreground.as_str())
183            .ok_or("foreground model is unresolved")?;
184        if route.provider != foreground.provider() {
185            return Err("foreground provider did not resolve exactly");
186        }
187
188        let mut descriptors = catalog::codex_static_catalog_models();
189        descriptors.extend(catalog::xai_static_catalog_models());
190        descriptors.extend(catalog::copilot_static_catalog_models());
191        descriptors.extend(catalog::google_gemini_static_catalog_models());
192        let mut entries: Vec<_> = descriptors
193            .into_iter()
194            .filter_map(|descriptor| QualifiedModelId::parse(descriptor.runtime_id()).ok())
195            .map(|model| CatalogEntry {
196                model,
197                available: true,
198                worker_eligible: true,
199            })
200            .collect();
201        entries.extend(
202            manifestless_worker_choices(foreground)
203                .into_iter()
204                .map(|model| CatalogEntry {
205                    model,
206                    available: true,
207                    worker_eligible: true,
208                }),
209        );
210        // `resolve_route` is itself a trusted local provider descriptor. This is
211        // independent of manifest content and permits configured generic/Anthropic
212        // foreground routes whose live catalogs are not fetched during startup.
213        entries.push(CatalogEntry {
214            model: foreground.clone(),
215            available: true,
216            worker_eligible: true,
217        });
218        Ok(CatalogSnapshot::from_entries(entries))
219    }
220
221    /// Secure manifestless baseline: the exact foreground identity is the only
222    /// worker choice in a deterministic runtime-controlled catalog.
223    pub fn baseline(
224        foreground: QualifiedModelId,
225        concurrent: usize,
226        total: usize,
227    ) -> Result<Self, &'static str> {
228        let choices = manifestless_worker_choices(&foreground);
229        let catalog = Self::trusted_catalog(&foreground, std::iter::empty())?;
230        Ok(Self::new(DelegationPolicy::provider_baseline(
231            foreground, catalog, choices, concurrent, total,
232        )?))
233    }
234
235    pub fn new(policy: DelegationPolicy) -> Self {
236        let max_total_descendants = policy.max_total_workers;
237        Self {
238            inner: Mutex::new(Inner {
239                registry: WorkerRegistry::new(policy),
240                handles: HashMap::new(),
241            }),
242            tree_budget: DelegationTreeBudget {
243                max_depth: 4,
244                max_children_per_worker: 8,
245                max_total_descendants,
246            },
247            tree: Mutex::new(DelegationTreeState::default()),
248        }
249    }
250
251    pub fn with_tree_budget(mut self, budget: DelegationTreeBudget) -> Result<Self, &'static str> {
252        if budget.max_depth == 0
253            || budget.max_children_per_worker == 0
254            || budget.max_total_descendants == 0
255        {
256            return Err("invalid delegation tree budget");
257        }
258        self.tree_budget = budget;
259        Ok(self)
260    }
261
262    /// Reserve one tree edge before allocating channels/threads/provider
263    /// runtimes. `parent=None` is a foreground-root child. Every denial is
264    /// fail-closed and leaves the counters unchanged.
265    pub fn reserve_delegation(
266        &self,
267        worker_id: &str,
268        parent: Option<&str>,
269    ) -> Result<u16, DelegationTreeDenied> {
270        let mut tree = self.tree.lock().unwrap();
271        if tree.descendants >= self.tree_budget.max_total_descendants {
272            return Err(DelegationTreeDenied::DescendantLimit);
273        }
274        let depth = match parent {
275            None => 1,
276            Some(parent_id) => tree
277                .depth_by_worker
278                .get(parent_id)
279                .copied()
280                .ok_or(DelegationTreeDenied::UnknownParent)?
281                .saturating_add(1),
282        };
283        if depth > self.tree_budget.max_depth {
284            return Err(DelegationTreeDenied::DepthLimit);
285        }
286        let parent_key = parent.unwrap_or("<root>");
287        if tree.child_count.get(parent_key).copied().unwrap_or(0)
288            >= self.tree_budget.max_children_per_worker
289        {
290            return Err(DelegationTreeDenied::ChildLimit);
291        }
292        if tree.depth_by_worker.contains_key(worker_id) {
293            return Err(DelegationTreeDenied::ChildLimit);
294        }
295        tree.depth_by_worker.insert(worker_id.to_string(), depth);
296        tree.parent_by_worker
297            .insert(worker_id.to_string(), parent_key.to_string());
298        *tree.child_count.entry(parent_key.to_string()).or_default() += 1;
299        tree.descendants += 1;
300        Ok(depth)
301    }
302
303    /// Release one reserved tree edge. Descendants are recursively removed
304    /// because a live child cannot outlive its parent lineage. The caller-
305    /// supplied `parent` is intentionally ignored: ownership is taken from the
306    /// reservation recorded under the tree lock, so a stale or hostile caller
307    /// cannot decrement another parent's fanout.
308    pub fn release_delegation(&self, worker_id: &str, _parent: Option<&str>) {
309        let mut tree = self.tree.lock().unwrap();
310        let mut stack = vec![worker_id.to_string()];
311        while let Some(current) = stack.pop() {
312            let children = tree
313                .parent_by_worker
314                .iter()
315                .filter_map(|(child, parent)| (parent == &current).then_some(child.clone()))
316                .collect::<Vec<_>>();
317            stack.extend(children);
318            if tree.depth_by_worker.remove(&current).is_some() {
319                tree.descendants = tree.descendants.saturating_sub(1);
320                if let Some(parent_key) = tree.parent_by_worker.remove(&current) {
321                    if let Some(count) = tree.child_count.get_mut(&parent_key) {
322                        *count = count.saturating_sub(1);
323                    }
324                }
325                tree.child_count.remove(&current);
326            }
327        }
328    }
329
330    pub fn delegation_descendants(&self) -> usize {
331        self.tree.lock().unwrap().descendants
332    }
333    pub fn preflight(&self, model: &str) -> Result<(), String> {
334        let model = QualifiedModelId::parse(model)
335            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
336        self.inner
337            .lock()
338            .unwrap()
339            .registry
340            .validate_dispatch(&model)
341            .map_err(|e| format!("delegation denied: {}", e.code()))
342    }
343
344    /// Honors an explicit mid-session user trust grant for one exact worker
345    /// model. Trusted models were never meant to be pinned at session start:
346    /// the live policy and catalog are extended in place while worker
347    /// lifecycle state and concurrency limits are preserved. The grant takes
348    /// effect for the next dispatch decision.
349    pub fn grant_worker_model(&self, model: &str) -> Result<(), String> {
350        let model = QualifiedModelId::parse(model)
351            .map_err(|_| "grant denied: invalid qualified model".to_string())?;
352        self.inner
353            .lock()
354            .unwrap()
355            .registry
356            .grant_worker_model(model)
357            .map_err(|error| format!("grant denied: {error}"))
358    }
359
360    /// Read-only UltraCode authorization snapshot. The exact foreground identity,
361    /// policy authorization, and configured limits are checked under one lock; no
362    /// worker reservation or lifecycle state is created.
363    pub fn ultracode_readiness(&self, model: &str) -> Result<(usize, usize), String> {
364        let model = QualifiedModelId::parse(model)
365            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
366        let inner = self.inner.lock().unwrap();
367        if inner.registry.foreground_model() != &model {
368            return Err("delegation denied: model is not exact foreground".into());
369        }
370        inner
371            .registry
372            .validate_dispatch(&model)
373            .map_err(|e| format!("delegation denied: {}", e.code()))?;
374        let policy = inner.registry.policy();
375        if policy.max_concurrent_workers == 0 || policy.max_total_workers == 0 {
376            return Err("delegation denied: invalid worker limits".into());
377        }
378        Ok((policy.max_concurrent_workers, policy.max_total_workers))
379    }
380
381    /// Single parse/resolve/catalog/policy/limits decision point used by every
382    /// public spawn path. No credential or provider object is reachable here.
383    pub fn resolve_and_authorize(
384        &self,
385        runtime_handle: &str,
386        requested: Option<&str>,
387    ) -> Result<AuthorizedWorkerModel, DispatchDenial> {
388        let mut inner = self.inner.lock().unwrap();
389        let foreground = inner.registry.foreground_model().clone();
390        let selection_source = if requested.is_some() {
391            SelectionSource::ExplicitRequest
392        } else {
393            SelectionSource::ForegroundInheritance
394        };
395        let model = match requested {
396            Some(value) => QualifiedModelId::parse(value).map_err(|_| DispatchDenial {
397                kind: "dispatch_denied",
398                code: "invalid_qualified_model",
399                requested_model: None,
400                foreground_model: foreground.as_str().into(),
401                selection_source,
402                network_attempted: false,
403                remediation: "Omit model to inherit foreground or select an exact session choice.",
404            })?,
405            None => foreground.clone(),
406        };
407        if inner.handles.contains_key(runtime_handle) {
408            return Err(DispatchDenial {
409                kind: "dispatch_denied",
410                code: "duplicate_runtime_handle",
411                requested_model: Some(model.as_str().into()),
412                foreground_model: foreground.as_str().into(),
413                selection_source,
414                network_attempted: false,
415                remediation: "Use a new worker handle.",
416            });
417        }
418        let policy_digest = inner.registry.policy().digest();
419        let catalog_snapshot_id = inner.registry.policy().catalog_snapshot_id().to_owned();
420        let catalog_digest = inner.registry.policy().catalog_digest().to_owned();
421        let cross_provider_grant_id = inner
422            .registry
423            .policy()
424            .authorize(&model)
425            .ok()
426            .flatten()
427            .map(str::to_owned);
428        let handle = inner
429            .registry
430            .authorize_dispatch(&model, WorkerRole::Implementer, WorkerWritePolicy::ReadOnly)
431            .map_err(|error| DispatchDenial {
432                kind: "dispatch_denied",
433                code: error.code(),
434                requested_model: Some(model.as_str().into()),
435                foreground_model: foreground.as_str().into(),
436                selection_source,
437                network_attempted: false,
438                remediation: "Omit model to inherit foreground, select an exact session choice, or trust the model mid-session (favorite it in the models picker).",
439            })?;
440        inner.handles.insert(runtime_handle.to_owned(), handle);
441        Ok(AuthorizedWorkerModel {
442            model,
443            selection_source,
444            network_attempted: false,
445            policy_digest,
446            catalog_snapshot_id,
447            catalog_digest,
448            correlation_id: runtime_handle.to_owned(),
449            cross_provider_grant_id,
450        })
451    }
452
453    /// Marks the persisted handle Starting immediately before the runtime/thread
454    /// factory is invoked. Starting remains rollback-safe until `mark_running`.
455    pub fn mark_starting(&self, runtime_handle: &str) -> Result<(), String> {
456        let mut inner = self.inner.lock().unwrap();
457        let handle = inner
458            .handles
459            .get(runtime_handle)
460            .cloned()
461            .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
462        inner
463            .registry
464            .mark_starting(&handle)
465            .map_err(|_| "worker lifecycle unavailable".into())
466    }
467    pub fn mark_running(&self, runtime_handle: &str) -> Result<(), String> {
468        let mut inner = self.inner.lock().unwrap();
469        let handle = inner
470            .handles
471            .get(runtime_handle)
472            .cloned()
473            .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
474        inner
475            .registry
476            .mark_running(&handle)
477            .map_err(|_| "worker lifecycle unavailable".into())
478    }
479
480    pub fn rollback(&self, runtime_handle: &str) {
481        let mut inner = self.inner.lock().unwrap();
482        if let Some(handle) = inner.handles.get(runtime_handle).cloned() {
483            let _ = inner.registry.rollback_dispatch(&handle);
484            inner.handles.remove(runtime_handle);
485        }
486    }
487
488    pub fn effective_choices(&self) -> Vec<String> {
489        self.inner
490            .lock()
491            .unwrap()
492            .registry
493            .policy()
494            .effective_choices()
495            .iter()
496            .map(|model| model.as_str().to_owned())
497            .collect()
498    }
499
500    pub fn foreground_model(&self) -> String {
501        self.inner
502            .lock()
503            .unwrap()
504            .registry
505            .foreground_model()
506            .as_str()
507            .to_owned()
508    }
509    pub fn authorize_with_policy(
510        &self,
511        runtime_handle: &str,
512        model: &str,
513        role: WorkerRole,
514        writes: WorkerWritePolicy,
515    ) -> Result<(), String> {
516        let model = QualifiedModelId::parse(model)
517            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
518        let mut inner = self.inner.lock().unwrap();
519        if inner.handles.contains_key(runtime_handle) {
520            return Err("delegation denied: duplicate runtime handle".to_string());
521        }
522        let handle = inner
523            .registry
524            .authorize_dispatch(&model, role, writes)
525            .map_err(|e| format!("delegation denied: {}", e.code()))?;
526        inner.registry.mark_starting(&handle).map_err(|error| {
527            let _ = inner.registry.rollback_dispatch(&handle);
528            error.to_string()
529        })?;
530        if let Err(error) = inner.registry.mark_running(&handle) {
531            let _ = inner.registry.rollback_dispatch(&handle);
532            return Err(error.to_string());
533        }
534        inner.handles.insert(runtime_handle.to_string(), handle);
535        Ok(())
536    }
537    pub fn authorize(&self, runtime_handle: &str, model: &str) -> Result<(), String> {
538        self.authorize_with_policy(
539            runtime_handle,
540            model,
541            WorkerRole::Implementer,
542            WorkerWritePolicy::ReadOnly,
543        )
544    }
545    pub fn poll(&self, id: &str, fingerprint: &str) -> Result<(), String> {
546        let mut inner = self.inner.lock().unwrap();
547        let h = inner
548            .handles
549            .get(id)
550            .cloned()
551            .ok_or_else(|| "unknown worker".to_string())?;
552        inner.registry.poll(&h, fingerprint).map_err(str::to_string)
553    }
554    pub fn steer(&self, id: &str) -> Result<(), String> {
555        let mut inner = self.inner.lock().unwrap();
556        let h = inner
557            .handles
558            .get(id)
559            .cloned()
560            .ok_or_else(|| "unknown worker".to_string())?;
561        inner.registry.steer(&h).map_err(str::to_string)
562    }
563    pub fn terminal_and_collect(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
564        let mut inner = self.inner.lock().unwrap();
565        let h = inner
566            .handles
567            .get(id)
568            .cloned()
569            .ok_or_else(|| "unknown worker".to_string())?;
570        inner
571            .registry
572            .mark_terminal(&h, terminal)
573            .map_err(str::to_string)?;
574        inner.registry.collect(&h).map_err(str::to_string)
575    }
576    pub fn finish_one_shot(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
577        self.terminal_and_collect(id, terminal)?;
578        self.reconcile(id)
579    }
580    pub fn reconcile(&self, id: &str) -> Result<(), String> {
581        let mut inner = self.inner.lock().unwrap();
582        let h = inner
583            .handles
584            .get(id)
585            .cloned()
586            .ok_or_else(|| "unknown worker".to_string())?;
587        inner.registry.reconcile(&h).map_err(str::to_string)
588    }
589    /// Snapshot runtime handle IDs still named by the completion gate. Taking this
590    /// snapshot before locking the subagent registry avoids cross-registry lock order.
591    pub fn unreconciled_runtime_handles(&self) -> HashSet<String> {
592        let inner = self.inner.lock().unwrap();
593        // Use all_unreconciled_ids (includes Running workers) — NOT
594        // completion_gate (which only reports Terminal|Collected). The reaper
595        // must retain handles the orchestration still tracks, even if the
596        // gate lets the foreground turn through.
597        let unreconciled = inner.registry.all_unreconciled_ids();
598        if unreconciled.is_empty() {
599            return HashSet::new();
600        }
601        inner
602            .handles
603            .iter()
604            .filter(|(_, handle)| unreconciled.iter().any(|id| id == handle.id()))
605            .map(|(runtime_id, _)| runtime_id.clone())
606            .collect()
607    }
608
609    /// Whether a runtime handle is still named by the policy completion gate.
610    pub fn is_unreconciled(&self, runtime_handle: &str) -> bool {
611        self.unreconciled_runtime_handles().contains(runtime_handle)
612    }
613
614    pub fn completion_gate(&self) -> CompletionGate {
615        // Core WorkerRegistry reports internal policy IDs (`worker-N`). Map them
616        // back through the runtime handle table so tool-facing remediation can
617        // cite `sa_*` handles accepted by `subagent_collect`.
618        let inner = self.inner.lock().unwrap();
619        match inner.registry.completion_gate() {
620            CompletionGate::Allowed => CompletionGate::Allowed,
621            CompletionGate::Warning { workers } => CompletionGate::Warning {
622                workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
623            },
624            CompletionGate::Blocked { workers } => CompletionGate::Blocked {
625                workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
626            },
627        }
628    }
629
630    /// Map policy `WorkerHandle` IDs (`worker-N`) to runtime handles (`sa_*`).
631    /// Preserves the deterministic order of the policy gate's worker list.
632    fn map_policy_ids_to_runtime(
633        handles: &HashMap<String, agent_core::orchestration::WorkerHandle>,
634        policy_ids: Vec<String>,
635    ) -> Vec<String> {
636        let reverse: HashMap<&str, &str> = handles
637            .iter()
638            .map(|(runtime_handle, policy_handle)| (policy_handle.id(), runtime_handle.as_str()))
639            .collect();
640        policy_ids
641            .into_iter()
642            .map(|policy_id| {
643                reverse
644                    .get(policy_id.as_str())
645                    .map(|runtime| (*runtime).to_owned())
646                    .unwrap_or(policy_id)
647            })
648            .collect()
649    }
650    pub fn check_foreground_write(&self, path: &str) -> agent_core::orchestration::ScopeDecision {
651        self.inner
652            .lock()
653            .unwrap()
654            .registry
655            .check_foreground_write(path)
656    }
657    pub fn telemetry_json(&self) -> String {
658        serde_json::to_string(self.inner.lock().unwrap().registry.telemetry())
659            .unwrap_or_else(|_| "[]".into())
660    }
661    pub fn enforcement_mode(&self) -> agent_core::orchestration::EnforcementMode {
662        self.inner.lock().unwrap().registry.mode()
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use std::sync::Arc;
670    fn model(s: &str) -> QualifiedModelId {
671        QualifiedModelId::parse(s).unwrap()
672    }
673    /// Regression: a legacy config (`model = claude-fable-5`, no provider
674    /// segment) must not brick startup. The router deliberately routes bare
675    /// Claude IDs to `anthropic`, so the baseline foreground identity must be
676    /// the router's canonical `anthropic/<id>` — not a startup Config error.
677    #[test]
678    fn legacy_bare_claude_foreground_canonicalizes_via_router() {
679        let id = canonical_foreground_identity("claude-fable-5").unwrap();
680        assert_eq!(id.as_str(), "anthropic/claude-fable-5");
681        // The canonical identity must satisfy the manifestless baseline.
682        OrchestrationRuntime::baseline(id, 8, 64).unwrap();
683    }
684
685    #[test]
686    fn baseline_openrouter_catalog_authorizes_exact_chinese_worker_stack() {
687        let deepseek = model("openrouter/deepseek/deepseek-v4-pro");
688        let glm = model("openrouter/z-ai/glm-5.2");
689        let kimi = model("openrouter/moonshotai/kimi-k2.7-code");
690        let rt = OrchestrationRuntime::baseline(deepseek.clone(), 3, 8).unwrap();
691
692        assert_eq!(
693            rt.effective_choices(),
694            vec![
695                deepseek.as_str().to_owned(),
696                kimi.as_str().to_owned(),
697                glm.as_str().to_owned(),
698            ]
699        );
700        assert_eq!(
701            rt.resolve_and_authorize("sa_glm", Some(glm.as_str()))
702                .unwrap()
703                .model,
704            glm
705        );
706        assert_eq!(
707            rt.resolve_and_authorize("sa_kimi", Some(kimi.as_str()))
708                .unwrap()
709                .model,
710            kimi
711        );
712    }
713
714    #[test]
715    fn manifestless_non_openrouter_baseline_remains_foreground_only() {
716        let foreground = model("anthropic/claude-fable-5");
717        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
718        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
719        let denied = rt
720            .resolve_and_authorize(
721                "sa_openrouter",
722                Some("openrouter/moonshotai/kimi-k2.7-code"),
723            )
724            .unwrap_err();
725        assert!(matches!(
726            denied.code,
727            "provider_not_allowed" | "catalog_model_unknown"
728        ));
729    }
730
731    /// Regression: an explicit mid-session user trust grant must flip an
732    /// exact-model dispatch from `provider_not_allowed` to authorized without
733    /// restarting the session. Trusted models were never meant to be pinned
734    /// at session start.
735    #[test]
736    fn baseline_honors_mid_session_user_grant() {
737        let foreground = model("anthropic/claude-fable-5");
738        let requested = "openai-codex/gpt-5.6-sol";
739        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
740        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
741        let denied = rt
742            .resolve_and_authorize("sa_denied", Some(requested))
743            .unwrap_err();
744        assert!(matches!(
745            denied.code,
746            "provider_not_allowed" | "catalog_model_unknown"
747        ));
748        assert!(!denied.network_attempted);
749
750        rt.grant_worker_model(requested).unwrap();
751
752        assert!(rt.effective_choices().contains(&requested.to_owned()));
753        let authorized = rt
754            .resolve_and_authorize("sa_granted", Some(requested))
755            .unwrap();
756        assert_eq!(authorized.model.as_str(), requested);
757        assert_eq!(
758            authorized.cross_provider_grant_id.as_deref(),
759            Some("session-user-grant-openai-codex/gpt-5.6-sol")
760        );
761        assert!(!authorized.network_attempted);
762        rt.rollback("sa_granted");
763        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
764    }
765
766    /// A grant for a malformed identity fails closed and changes nothing.
767    #[test]
768    fn mid_session_grant_rejects_invalid_qualified_model() {
769        let foreground = model("anthropic/claude-fable-5");
770        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
771        assert!(rt.grant_worker_model("").is_err());
772        assert!(rt.grant_worker_model("not-qualified").is_err());
773        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
774    }
775
776    #[test]
777    fn qualified_foreground_identity_passes_through_unchanged() {
778        for raw in [
779            "openai-codex/gpt-5.6-sol",
780            "anthropic/claude-fable-5",
781            "openrouter/z-ai/glm-5.1",
782        ] {
783            assert_eq!(canonical_foreground_identity(raw).unwrap().as_str(), raw);
784        }
785    }
786
787    #[test]
788    fn unroutable_foreground_identity_fails_closed() {
789        // Neither a valid qualified ID nor routable: empty model segment
790        // under an unknown provider prefix.
791        assert!(canonical_foreground_identity("foo/").is_err());
792        assert!(canonical_foreground_identity("").is_err());
793    }
794
795    #[test]
796    fn trusted_catalog_rejects_unresolved_foreground_and_ignores_manifest_candidates() {
797        let unresolved = model("invented/foreground");
798        assert!(OrchestrationRuntime::trusted_catalog(
799            &unresolved,
800            [model("anthropic/claude-manifest-injected")]
801        )
802        .is_err());
803
804        let foreground = model("openai-codex/gpt-5.1-codex-mini");
805        let catalog = OrchestrationRuntime::trusted_catalog(
806            &foreground,
807            [model("anthropic/claude-manifest-injected")],
808        )
809        .unwrap();
810        assert!(catalog.contains(&foreground));
811        assert!(!catalog.contains(&model("anthropic/claude-manifest-injected")));
812    }
813
814    #[test]
815    fn authorization_reserves_without_claiming_worker_is_running() {
816        let foreground = model("anthropic/claude-foreground");
817        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
818            foreground.clone(),
819            [foreground.clone()],
820            1,
821            1,
822        ));
823        rt.resolve_and_authorize("sa_reserved", None).unwrap();
824        assert!(rt.poll("sa_reserved", "not-started").is_err());
825        rt.mark_starting("sa_reserved").unwrap();
826        assert!(rt.poll("sa_reserved", "not-running").is_err());
827        rt.rollback("sa_reserved");
828        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
829    }
830
831    #[test]
832    fn one_shot_uses_the_full_lifecycle_before_terminal_collection() {
833        let foreground = model("anthropic/foreground");
834        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
835            foreground.clone(),
836            [foreground],
837            1,
838            1,
839        ));
840        rt.resolve_and_authorize("sa_one_shot", None).unwrap();
841        rt.mark_starting("sa_one_shot").unwrap();
842        rt.mark_running("sa_one_shot").unwrap();
843        rt.finish_one_shot("sa_one_shot", WorkerTerminal::Completed)
844            .unwrap();
845        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
846        let telemetry = rt.telemetry_json();
847        for event in [
848            "worker.dispatch_allowed",
849            "worker.starting",
850            "worker.running",
851            "worker.terminal",
852            "worker.collected",
853            "worker.reconciled",
854        ] {
855            assert!(telemetry.contains(event), "missing {event}: {telemetry}");
856        }
857    }
858
859    #[test]
860    fn authorization_happens_before_a_worker_is_registered() {
861        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
862            model("anthropic/foreground"),
863            [model("anthropic/worker")],
864            1,
865            1,
866        ));
867        assert!(rt.authorize("sa_1", "openrouter/worker").is_err());
868        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
869        rt.authorize("sa_2", "anthropic/worker").unwrap();
870        // Running workers pass through the gate (reactive pattern).
871        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
872        rt.poll("sa_2", "same").unwrap();
873        rt.steer("sa_2").unwrap();
874        rt.terminal_and_collect("sa_2", WorkerTerminal::Completed)
875            .unwrap();
876        // Terminal+Collected: gate blocks until reconciled.
877        assert!(matches!(
878            rt.completion_gate(),
879            CompletionGate::Blocked { .. }
880        ));
881        rt.reconcile("sa_2").unwrap();
882        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
883    }
884
885    /// Completion remediation must cite tool-facing `sa_*` handles, never the
886    /// internal policy IDs (`worker-N`) that `WorkerRegistry` allocates.
887    #[test]
888    fn delegation_tree_depth_children_and_total_are_independently_bounded() {
889        let foreground = model("anthropic/foreground");
890        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
891            foreground.clone(),
892            [foreground],
893            8,
894            16,
895        ))
896        .with_tree_budget(DelegationTreeBudget {
897            max_depth: 2,
898            max_children_per_worker: 2,
899            max_total_descendants: 3,
900        })
901        .unwrap();
902
903        assert_eq!(rt.reserve_delegation("a", None), Ok(1));
904        assert_eq!(
905            rt.reserve_delegation("a", None),
906            Err(DelegationTreeDenied::ChildLimit),
907            "registration/spawn retry cannot double-reserve one handle"
908        );
909        assert_eq!(rt.reserve_delegation("b", Some("a")), Ok(2));
910        assert_eq!(
911            rt.reserve_delegation("too-deep", Some("b")),
912            Err(DelegationTreeDenied::DepthLimit)
913        );
914        assert_eq!(rt.reserve_delegation("c", Some("a")), Ok(2));
915        assert_eq!(
916            rt.reserve_delegation("too-many", Some("a")),
917            Err(DelegationTreeDenied::DescendantLimit)
918        );
919        assert_eq!(rt.delegation_descendants(), 3);
920        rt.release_delegation("c", Some("a"));
921        assert_eq!(rt.delegation_descendants(), 2);
922    }
923
924    /// Active reservation count is exact across reserve/release races.
925    #[test]
926    fn delegation_release_is_idempotent_under_completion_cancel_race() {
927        let foreground = model("anthropic/foreground");
928        let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
929            foreground.clone(),
930            [foreground],
931            4,
932            8,
933        )));
934        rt.reserve_delegation("child", None).unwrap();
935        let a = Arc::clone(&rt);
936        let b = Arc::clone(&rt);
937        let t1 = std::thread::spawn(move || a.release_delegation("child", None));
938        let t2 = std::thread::spawn(move || b.release_delegation("child", None));
939        t1.join().unwrap();
940        t2.join().unwrap();
941        assert_eq!(rt.delegation_descendants(), 0);
942    }
943
944    #[test]
945    fn parent_exit_releases_entire_live_descendant_subtree() {
946        let foreground = model("anthropic/foreground");
947        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
948            foreground.clone(),
949            [foreground],
950            8,
951            16,
952        ));
953        rt.reserve_delegation("parent", None).unwrap();
954        rt.reserve_delegation("child", Some("parent")).unwrap();
955        rt.reserve_delegation("grandchild", Some("child")).unwrap();
956        assert_eq!(rt.delegation_descendants(), 3);
957        rt.release_delegation("parent", None);
958        assert_eq!(rt.delegation_descendants(), 0);
959        assert_eq!(
960            rt.reserve_delegation("orphan", Some("child")),
961            Err(DelegationTreeDenied::UnknownParent)
962        );
963    }
964
965    /// Active reservation count is exact when a live worker completes while
966    /// its parent is cancelled and recursively releases the same subtree.
967    #[test]
968    fn parent_cancel_racing_child_completion_releases_every_lease() {
969        let foreground = model("anthropic/foreground");
970        let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
971            foreground.clone(),
972            [foreground],
973            8,
974            16,
975        )));
976        rt.reserve_delegation("parent", None).unwrap();
977        rt.reserve_delegation("child", Some("parent")).unwrap();
978        rt.reserve_delegation("grandchild", Some("child")).unwrap();
979        let cancel = Arc::clone(&rt);
980        let complete = Arc::clone(&rt);
981        let t1 = std::thread::spawn(move || cancel.release_delegation("parent", None));
982        let t2 = std::thread::spawn(move || complete.release_delegation("child", Some("parent")));
983        t1.join().unwrap();
984        t2.join().unwrap();
985
986        assert_eq!(rt.delegation_descendants(), 0);
987        assert_eq!(
988            rt.reserve_delegation("orphan", Some("grandchild")),
989            Err(DelegationTreeDenied::UnknownParent)
990        );
991        assert_eq!(rt.reserve_delegation("replacement", None), Ok(1));
992        rt.release_delegation("replacement", None);
993        assert_eq!(rt.delegation_descendants(), 0);
994    }
995
996    /// A child receives tree lineage only, never a new policy/grant mutation:
997    /// even when the session already has one explicit cross-provider grant,
998    /// child lineage cannot use that fact to authorize a different identity.
999    #[test]
1000    fn descendant_lineage_cannot_mint_or_broaden_session_model_grants() {
1001        let foreground = model("anthropic/foreground");
1002        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1003            foreground.clone(),
1004            [foreground],
1005            4,
1006            8,
1007        ));
1008        rt.grant_worker_model("openai-codex/gpt-5.6-sol").unwrap();
1009        rt.reserve_delegation("parent", None).unwrap();
1010        rt.reserve_delegation("child", Some("parent")).unwrap();
1011
1012        assert!(rt.preflight("openai-codex/gpt-5.6-sol").is_ok());
1013        assert!(
1014            rt.preflight("openai-codex/gpt-5.6-luna").is_err(),
1015            "a child lineage must not broaden one exact session grant to a sibling model"
1016        );
1017        assert!(
1018            rt.preflight("openrouter/ungranted").is_err(),
1019            "tree lineage must never mint a cross-provider grant"
1020        );
1021        rt.release_delegation("parent", None);
1022        assert_eq!(rt.delegation_descendants(), 0);
1023    }
1024
1025    #[test]
1026    fn completion_gate_reports_runtime_handles_not_policy_ids() {
1027        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1028            model("anthropic/foreground"),
1029            [model("anthropic/worker")],
1030            2,
1031            2,
1032        ));
1033        rt.authorize("sa_alpha", "anthropic/worker").unwrap();
1034        rt.authorize("sa_beta", "anthropic/worker").unwrap();
1035        // Transition both to Terminal so the gate reports them.
1036        rt.terminal_and_collect("sa_alpha", WorkerTerminal::Completed)
1037            .unwrap();
1038        rt.terminal_and_collect("sa_beta", WorkerTerminal::Completed)
1039            .unwrap();
1040
1041        match rt.completion_gate() {
1042            CompletionGate::Blocked { workers } => {
1043                assert_eq!(
1044                    workers,
1045                    vec!["sa_alpha".to_string(), "sa_beta".to_string()],
1046                    "blocked IDs must be runtime handles in deterministic policy order"
1047                );
1048                for id in &workers {
1049                    assert!(
1050                        id.starts_with("sa_"),
1051                        "blocked id must be a subagent_collect handle, got {id}"
1052                    );
1053                    assert!(
1054                        !id.starts_with("worker-"),
1055                        "blocked id must never leak policy WorkerHandle, got {id}"
1056                    );
1057                }
1058            }
1059            other => panic!("expected Blocked with two sa_* handles, got {other:?}"),
1060        }
1061
1062        // After one reconcile, only the remaining runtime handle is reported.
1063        rt.reconcile("sa_alpha").unwrap();
1064        match rt.completion_gate() {
1065            CompletionGate::Blocked { workers } => {
1066                assert_eq!(workers, vec!["sa_beta".to_string()]);
1067            }
1068            other => panic!("expected only sa_beta still blocked, got {other:?}"),
1069        }
1070    }
1071}