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
32pub 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
89pub 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
104pub 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
145const 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
153fn 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 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 entries.push(CatalogEntry {
214 model: foreground.clone(),
215 available: true,
216 worker_eligible: true,
217 });
218 Ok(CatalogSnapshot::from_entries(entries))
219 }
220
221 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 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 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 == ¤t).then_some(child.clone()))
316 .collect::<Vec<_>>();
317 stack.extend(children);
318 if tree.depth_by_worker.remove(¤t).is_some() {
319 tree.descendants = tree.descendants.saturating_sub(1);
320 if let Some(parent_key) = tree.parent_by_worker.remove(¤t) {
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(¤t);
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 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 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 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 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 pub fn unreconciled_runtime_handles(&self) -> HashSet<String> {
592 let inner = self.inner.lock().unwrap();
593 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 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 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 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 #[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 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 #[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 #[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 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 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 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 #[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 #[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 #[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 #[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 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 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}