context_harness/agent_runtime/
delegation.rs1use super::*;
3use crate::{
4 agent_resource::{Capability, Permissions},
5 tool_binding::{self, ToolTrustClass},
6 traits::{Tool, ToolRuntimeDispatch},
7};
8use async_trait::async_trait;
9use serde::Deserialize;
10use serde_json::Value;
11use std::collections::BTreeMap;
12
13#[derive(Clone)]
14pub(super) struct ExecutionContext {
15 pub deadline: i64,
16 pub ancestry: Vec<String>,
17}
18#[derive(Deserialize)]
19#[serde(deny_unknown_fields)]
20struct InvokeArgs {
21 agent: String,
22 input: String,
23}
24pub(super) fn validate(value: &Value) -> Result<()> {
25 let args: InvokeArgs = serde_json::from_value(value.clone())?;
26 ensure!(
27 !args.agent.is_empty() && !args.input.trim().is_empty() && args.input.len() <= 64 * 1024,
28 "invalid delegation arguments"
29 );
30 Ok(())
31}
32pub(super) struct InvokeTool;
33#[async_trait]
34impl Tool for InvokeTool {
35 fn name(&self) -> &str {
36 "agent.invoke"
37 }
38 fn description(&self) -> &str {
39 "Invoke one allowed agent with inherited permissions and a shared execution budget"
40 }
41 fn parameters_schema(&self) -> Value {
42 json!({"type":"object","properties":{"agent":{"type":"string"},"input":{"type":"string","minLength":1,"maxLength":65536}},"required":["agent","input"],"additionalProperties":false})
43 }
44 fn capabilities(&self) -> Option<Vec<Capability>> {
45 Some(vec![Capability::AgentDelegate])
46 }
47 fn binding_metadata(&self) -> Option<Value> {
48 Some(tool_binding::compatibility_binding_metadata(
49 self.name(),
50 "builtin.agent.invoke",
51 self.parameters_schema(),
52 self.capabilities().unwrap(),
53 ToolTrustClass::Builtin,
54 ))
55 }
56 fn runtime_dispatch(&self) -> ToolRuntimeDispatch {
57 ToolRuntimeDispatch::AgentDelegation
58 }
59 fn validate_arguments(&self, arguments: &Value) -> Result<()> {
60 validate(arguments)
61 }
62 async fn execute(&self, _: Value, _: &ToolContext) -> Result<Value> {
63 anyhow::bail!("delegation requires runtime context")
64 }
65}
66impl RuntimePolicy {
67 pub(super) fn inherited(&self, parent: &Permissions) -> Self {
68 let mut allow = Vec::new();
69 let mut require_approval = Vec::new();
70 for capability in [
71 Capability::ReadOnly,
72 Capability::WorkspaceWrite,
73 Capability::ProcessExecute,
74 Capability::Network,
75 Capability::ExternalSideEffect,
76 Capability::AgentDelegate,
77 ] {
78 match self.authorize(parent, &[capability]) {
79 Authorization::Allowed => allow.push(capability),
80 Authorization::Approval(_) => require_approval.push(capability),
81 Authorization::Denied => {}
82 }
83 }
84 Self {
85 allow,
86 require_approval,
87 }
88 }
89}
90impl AgentRuntime {
91 pub fn with_resources(mut self, resources: BTreeMap<String, LoadedAgentResource>) -> Self {
94 self.resources = Arc::new(resources);
95 self
96 }
97 pub(super) fn validate_delegation(
98 &self,
99 parent: &LoadedAgentResource,
100 arguments: &Value,
101 ) -> Result<()> {
102 validate(arguments)?;
103 let args: InvokeArgs = serde_json::from_value(arguments.clone())?;
104 ensure!(
105 parent
106 .definition
107 .agent
108 .delegation
109 .allow
110 .contains(&args.agent),
111 "delegation target is not allowed"
112 );
113 ensure!(
114 self.resources.contains_key(&args.agent),
115 "delegation target unavailable"
116 );
117 let context = self
118 .execution
119 .as_ref()
120 .context("delegation execution context unavailable")?;
121 ensure!(
122 context.ancestry.len() <= 4,
123 "maximum delegation depth reached"
124 );
125 ensure!(
126 !context.ancestry.contains(&args.agent),
127 "delegation cycle rejected"
128 );
129 Ok(())
130 }
131
132 pub(super) async fn invoke(
133 &self,
134 id: &str,
135 call_id: &str,
136 parent: &LoadedAgentResource,
137 arguments: Value,
138 ) -> Result<Value> {
139 self.validate_delegation(parent, &arguments)?;
140 let args: InvokeArgs = serde_json::from_value(arguments)?;
141 ensure!(
142 parent
143 .definition
144 .agent
145 .delegation
146 .allow
147 .contains(&args.agent),
148 "delegation target is not allowed"
149 );
150 let resource = self
151 .resources
152 .get(&args.agent)
153 .context("delegation target unavailable")?;
154 let context = self
155 .execution
156 .as_ref()
157 .context("delegation execution context unavailable")?;
158 ensure!(
159 context.ancestry.len() <= 4,
160 "maximum delegation depth reached"
161 );
162 ensure!(
163 !context.ancestry.contains(&args.agent),
164 "delegation cycle rejected"
165 );
166 let child = self
167 .store
168 .create_child_run_with_budgets(
169 id,
170 call_id,
171 &resource.definition.agent.name,
172 &resource.version,
173 &resource.definition.agent.model,
174 &args.input,
175 run_budgets(resource),
176 )
177 .await?;
178 let mut runtime = self.clone();
179 runtime.policy = self.policy.inherited(&parent.definition.agent.permissions);
180 let mut context = context.clone();
181 context.ancestry.push(args.agent);
182 context.deadline = context.deadline.min(
183 child.created_at.saturating_add(
184 i64::try_from(resource.definition.agent.execution.timeout_seconds)
185 .unwrap_or(i64::MAX)
186 .saturating_mul(1000),
187 ),
188 );
189 runtime.execution = Some(context);
190 let result = async {
191 let files = files::acquire(&self.root, &child.id)?;
192 let (_sender, cancel) = watch::channel(false);
193 Box::pin(runtime.drive(&child, resource, None, 0, cancel, &files)).await
194 }
195 .await;
196 let child = match result {
197 Ok(child) => child,
198 Err(_) => {
199 if self
200 .store
201 .get_run(&child.id)
202 .await?
203 .is_some_and(|run| run.status == "running")
204 {
205 self.store
206 .finish_run(
207 &child.id,
208 RunOutcome::Failed("delegated execution failed".into()),
209 )
210 .await?;
211 }
212 anyhow::bail!("delegated execution failed")
213 }
214 };
215 ensure!(
216 child.status == "completed",
217 "delegated execution did not complete"
218 );
219 Ok(
220 json!({"run_id":child.id,"agent":child.agent_name,"status":child.status,"output":child.output}),
221 )
222 }
223}