Skip to main content

context_harness/agent_runtime/
delegation.rs

1//! Explicit, sequential delegation with inherited authority and shared budgets.
2use 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    /// Supply an immutable resource catalog from the same resolution context as
92    /// the root. Child names never trigger a second filesystem discovery.
93    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}