1use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10use crate::{
11 capability::{CapabilityId, CapabilityKind, CapabilitySpec, PackageSidecarRef},
12 domain::{AgentError, AgentId, ContentRef as ContentRefId, PolicyKind, PolicyRef},
13 package::{
14 AgentSnapshot, PackageSidecarSnapshot, ProviderRouteSnapshot, RuntimePackage,
15 RuntimePackageFingerprint, RuntimePackageId,
16 },
17};
18
19#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum ContextHandoffPolicy {
24 #[default]
26 None,
27 SummaryOnly {
29 summary_ref: ContentRefId,
32 max_tokens: u32,
35 policy_ref: PolicyRef,
38 },
39 SelectedRefs {
41 refs: Vec<ContentRefId>,
45 policy_ref: PolicyRef,
48 },
49 FullHistoryWithPolicy {
51 policy_ref: PolicyRef,
54 projection_audit_required: bool,
58 },
59}
60
61impl ContextHandoffPolicy {
62 pub fn validate(&self) -> Result<(), AgentError> {
66 match self {
67 Self::None => Ok(()),
68 Self::SummaryOnly {
69 max_tokens,
70 policy_ref,
71 ..
72 } => {
73 if *max_tokens == 0 {
74 return Err(AgentError::contract_violation(
75 "summary handoff requires a positive token budget",
76 ));
77 }
78 validate_non_host_policy(policy_ref, "summary handoff")
79 }
80 Self::SelectedRefs { refs, policy_ref } => {
81 if refs.is_empty() {
82 return Err(AgentError::contract_violation(
83 "selected refs handoff requires at least one content ref",
84 ));
85 }
86 validate_non_host_policy(policy_ref, "selected refs handoff")
87 }
88 Self::FullHistoryWithPolicy {
89 policy_ref,
90 projection_audit_required,
91 } => {
92 if !projection_audit_required {
93 return Err(AgentError::contract_violation(
94 "full history handoff requires a projection audit",
95 ));
96 }
97 validate_non_host_policy(policy_ref, "full history handoff")
98 }
99 }
100 }
101
102 pub fn policy_refs(&self) -> Vec<PolicyRef> {
106 match self {
107 Self::None => Vec::new(),
108 Self::SummaryOnly { policy_ref, .. }
109 | Self::SelectedRefs { policy_ref, .. }
110 | Self::FullHistoryWithPolicy { policy_ref, .. } => vec![policy_ref.clone()],
111 }
112 }
113
114 pub fn selected_content_refs(&self) -> Vec<ContentRefId> {
118 match self {
119 Self::None | Self::FullHistoryWithPolicy { .. } => Vec::new(),
120 Self::SummaryOnly { summary_ref, .. } => vec![summary_ref.clone()],
121 Self::SelectedRefs { refs, .. } => refs.clone(),
122 }
123 }
124
125 pub fn variant_name(&self) -> &'static str {
129 match self {
130 Self::None => "none",
131 Self::SummaryOnly { .. } => "summary_only",
132 Self::SelectedRefs { .. } => "selected_refs",
133 Self::FullHistoryWithPolicy { .. } => "full_history_with_policy",
134 }
135 }
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
139#[serde(rename_all = "snake_case")]
140pub enum RouteInheritanceMode {
143 InheritParent,
145 ExplicitOverrideOnly,
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150#[serde(tag = "type", rename_all = "snake_case")]
151pub enum SubagentRoutePolicy {
154 InheritParent,
156 UseAllowedOverride {
158 route_id: String,
160 model_id: String,
162 },
163}
164
165impl SubagentRoutePolicy {
166 pub fn selected_route(
170 &self,
171 parent: &ProviderRouteSnapshot,
172 child_policy: &ChildRuntimePackagePolicy,
173 ) -> Result<ProviderRouteSnapshot, AgentError> {
174 match self {
175 Self::InheritParent => Ok(parent.clone()),
176 Self::UseAllowedOverride { route_id, model_id } => {
177 if !child_policy.allowed_route_overrides.contains(route_id) {
178 return Err(AgentError::contract_violation(
179 "child provider route override is not allowed by package policy",
180 ));
181 }
182 if route_id.is_empty() || model_id.is_empty() {
183 return Err(AgentError::missing_required_field(
184 "subagent.route_override.route_id_or_model_id",
185 ));
186 }
187 Ok(ProviderRouteSnapshot::new(route_id, model_id))
188 }
189 }
190 }
191}
192
193#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
194pub struct ChildRuntimePackagePolicy {
197 pub source_parent_package: RuntimePackageFingerprint,
199 pub inherit_provider_route: RouteInheritanceMode,
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
202 pub allowed_route_overrides: Vec<String>,
205 pub strip_recursive_subagents: bool,
208 pub strip_disallowed_tools: bool,
211 pub child_lifecycle_bounds: PolicyRef,
213 pub redaction_policy_ref: PolicyRef,
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub parent_control_tool_ids: Vec<CapabilityId>,
220}
221
222impl ChildRuntimePackagePolicy {
223 pub fn strip_recursive_defaults(source_parent_package: RuntimePackageFingerprint) -> Self {
227 Self {
228 source_parent_package,
229 inherit_provider_route: RouteInheritanceMode::InheritParent,
230 allowed_route_overrides: Vec::new(),
231 strip_recursive_subagents: true,
232 strip_disallowed_tools: true,
233 child_lifecycle_bounds: PolicyRef::with_kind(
234 PolicyKind::RuntimePackage,
235 "policy.child.parent_owned",
236 ),
237 redaction_policy_ref: PolicyRef::with_kind(
238 PolicyKind::Redaction,
239 "policy.redaction.subagent.default",
240 ),
241 parent_control_tool_ids: default_parent_control_tool_ids(),
242 }
243 }
244}
245
246#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
247#[serde(tag = "type", rename_all = "snake_case")]
248pub enum SubagentToolPolicy {
251 InheritAllowlist,
253 #[default]
255 ReadOnly,
256 NoTools,
258 CustomAllowlist {
260 capability_ids: Vec<CapabilityId>,
262 },
263}
264
265impl SubagentToolPolicy {
266 fn retains(&self, capability: &CapabilitySpec) -> bool {
267 match self {
268 Self::InheritAllowlist | Self::ReadOnly => true,
269 Self::NoTools => capability.kind != CapabilityKind::Tool,
270 Self::CustomAllowlist { capability_ids } => {
271 capability.kind != CapabilityKind::Tool
272 || capability_ids.contains(&capability.capability_id)
273 }
274 }
275 }
276}
277
278#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
279pub struct DepthBudget {
282 pub current_depth: u32,
284 pub max_depth: u32,
287 pub max_children: u32,
290}
291
292impl DepthBudget {
293 pub fn max_depth(max_depth: u32) -> Self {
297 Self {
298 current_depth: 0,
299 max_depth,
300 max_children: 1,
301 }
302 }
303
304 pub fn validate_child_start(&self) -> Result<(), AgentError> {
308 if self.max_depth == 0 || self.current_depth >= self.max_depth {
309 return Err(AgentError::contract_violation(
310 "subagent depth budget exhausted before child start",
311 ));
312 }
313 if self.max_children == 0 {
314 return Err(AgentError::contract_violation(
315 "subagent child count budget exhausted before child start",
316 ));
317 }
318 Ok(())
319 }
320
321 pub fn child_budget(&self) -> Self {
325 Self {
326 current_depth: self.current_depth + 1,
327 max_depth: self.max_depth,
328 max_children: self.max_children,
329 }
330 }
331}
332
333#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
334pub struct ChildRuntimePackage {
337 pub package: RuntimePackage,
339 pub fingerprint: RuntimePackageFingerprint,
342 pub strip_manifest: ChildPackageStripManifest,
344}
345
346#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
347pub struct ChildPackageStripManifest {
350 pub parent_package_fingerprint: RuntimePackageFingerprint,
353 pub child_agent_id: AgentId,
355 pub selected_provider_route_id: String,
358 pub handoff_policy_variant: String,
360 pub tool_policy: SubagentToolPolicy,
362 pub recursive_subagent_strip: bool,
365 #[serde(default, skip_serializing_if = "Vec::is_empty")]
366 pub stripped_capability_ids: Vec<CapabilityId>,
369 #[serde(default, skip_serializing_if = "Vec::is_empty")]
370 pub retained_capability_ids: Vec<CapabilityId>,
373 pub lifecycle_policy_ref: PolicyRef,
376 pub redaction_policy_ref: PolicyRef,
379}
380
381impl ChildPackageStripManifest {
382 pub fn content_hash(&self) -> Result<String, AgentError> {
386 let bytes = serde_json::to_vec(self).map_err(|error| {
387 AgentError::contract_violation(format!(
388 "child package strip manifest serialization failed: {error}"
389 ))
390 })?;
391 Ok(format!("sha256:{:x}", Sha256::digest(bytes)))
392 }
393}
394
395pub fn build_child_runtime_package(
398 parent: &RuntimePackage,
399 child_agent_id: AgentId,
400 route_policy: &SubagentRoutePolicy,
401 handoff_policy: &ContextHandoffPolicy,
402 child_policy: &ChildRuntimePackagePolicy,
403 tool_policy: &SubagentToolPolicy,
404) -> Result<ChildRuntimePackage, AgentError> {
405 parent.validate()?;
406 handoff_policy.validate()?;
407
408 if !child_policy.strip_recursive_subagents {
409 return Err(AgentError::contract_violation(
410 "recursive subagent tools are denied by the core SDK contract",
411 ));
412 }
413
414 let parent_fingerprint = parent.fingerprint()?;
415 if parent_fingerprint != child_policy.source_parent_package {
416 return Err(AgentError::contract_violation(
417 "child package policy source parent fingerprint does not match parent package",
418 ));
419 }
420
421 let selected_route = route_policy.selected_route(&parent.provider_route, child_policy)?;
422 let mut child = parent.clone();
423 child.package_id = child_package_id(&child_agent_id, &parent_fingerprint);
424 child.agent = AgentSnapshot {
425 agent_id: child_agent_id.clone(),
426 name: child_agent_id.as_str().to_string(),
427 default_behavior_refs: parent.agent.default_behavior_refs.clone(),
428 };
429 child.provider_route = selected_route.clone();
430 child.child_lifecycle.policy_ref = child_policy.child_lifecycle_bounds.clone();
431 child.child_lifecycle.detach_policy_ref = child_policy.child_lifecycle_bounds.clone();
432
433 let mut stripped = Vec::new();
434 let mut retained = Vec::new();
435 child.capabilities.retain(|capability| {
436 let strip = is_recursive_subagent_capability(capability, child_policy)
437 || (child_policy.strip_disallowed_tools && !tool_policy.retains(capability));
438 if strip {
439 stripped.push(capability.capability_id.clone());
440 false
441 } else {
442 retained.push(capability.capability_id.clone());
443 true
444 }
445 });
446
447 stripped.sort();
448 retained.sort();
449 let manifest = ChildPackageStripManifest {
450 parent_package_fingerprint: parent_fingerprint,
451 child_agent_id,
452 selected_provider_route_id: selected_route.route_id.clone(),
453 handoff_policy_variant: handoff_policy.variant_name().to_string(),
454 tool_policy: tool_policy.clone(),
455 recursive_subagent_strip: true,
456 stripped_capability_ids: stripped,
457 retained_capability_ids: retained,
458 lifecycle_policy_ref: child_policy.child_lifecycle_bounds.clone(),
459 redaction_policy_ref: child_policy.redaction_policy_ref.clone(),
460 };
461
462 child.sidecars.push(PackageSidecarSnapshot {
463 sidecar_id: "sidecar.subagent.child_package_strip_manifest".to_string(),
464 kind: "subagent_child_package_strip_manifest".to_string(),
465 version: "v1".to_string(),
466 refs: vec![PackageSidecarRef::new(
467 "sidecar.subagent.child_package_strip_manifest",
468 "subagent_child_package_strip_manifest",
469 "v1",
470 )],
471 policy_refs: vec![
472 child_policy.child_lifecycle_bounds.clone(),
473 child_policy.redaction_policy_ref.clone(),
474 ],
475 content_hash: manifest.content_hash()?,
476 });
477 child
478 .policies
479 .policy_refs
480 .push(child_policy.redaction_policy_ref.clone());
481 child.fingerprint_manifest = child.computed_fingerprint_manifest();
482 child.validate()?;
483 let fingerprint = child.fingerprint()?;
484
485 Ok(ChildRuntimePackage {
486 package: child,
487 fingerprint,
488 strip_manifest: manifest,
489 })
490}
491
492fn is_recursive_subagent_capability(
493 capability: &CapabilitySpec,
494 policy: &ChildRuntimePackagePolicy,
495) -> bool {
496 capability.kind == CapabilityKind::AgentAsTool
497 || policy
498 .parent_control_tool_ids
499 .contains(&capability.capability_id)
500}
501
502fn default_parent_control_tool_ids() -> Vec<CapabilityId> {
503 [
504 "tool.subagent_send_message",
505 "tool.subagent_reply_to_clarification",
506 "tool.subagent_ask_parent",
507 "tool.subagent_read_parent_messages",
508 "tool.subagent_monitor",
509 ]
510 .into_iter()
511 .map(CapabilityId::new)
512 .collect()
513}
514
515fn child_package_id(
516 child_agent_id: &AgentId,
517 parent_fingerprint: &RuntimePackageFingerprint,
518) -> RuntimePackageId {
519 let digest = Sha256::digest(parent_fingerprint.as_str().as_bytes());
520 let suffix = format!("{:x}", digest);
521 RuntimePackageId::new(format!(
522 "package.subagent.{}.{}",
523 child_agent_id.as_str(),
524 &suffix[..16]
525 ))
526}
527
528fn validate_non_host_policy(policy_ref: &PolicyRef, label: &str) -> Result<(), AgentError> {
529 if policy_ref.kind == PolicyKind::Host {
530 Err(AgentError::contract_violation(format!(
531 "{label} requires an explicit SDK policy ref"
532 )))
533 } else {
534 Ok(())
535 }
536}