Capability Grants (Developer Guide)

From an agent's perspective, capability grants are temporary extensions to the agent's capability set granted by a parent agent or operator.

When You Need a Grant

If invoke_tool returns a ToolFailed error containing "access denied" or "lacks capability", the agent needs either:

  1. A capability added to its manifest (permanent, requiring redeployment)
  2. A runtime capability grant (temporary, requiring no restart)

Requesting a Grant

Currently, grant requests are issued by operators via ash or by parent agents via IPC. Agent-initiated grant requests are planned.

The typical flow from the agent's side:

#![allow(unused)]
fn main() {
match agent.invoke_tool("web.search", json!({"query": "..."})).await {
    Err(AgentError::ToolFailed(ref msg)) if msg.contains("access denied") => {
        // Observe the denial
        agent.observe(
            "capability_denied",
            msg,
            vec!["error".to_string(), "capability".to_string()],
        ).await?;

        // Check for an escalation response (parent may grant)
        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
        let escalations = agent.pending_escalations().await?;
        // ... process escalations ...
    }
    Ok(result) => { /* proceed */ }
    Err(e) => return Err(e.into()),
}
}

Handling Escalations from Children

If you are writing a parent agent that manages child agents:

#![allow(unused)]
fn main() {
let escalations = agent.pending_escalations().await?;
for esc in &escalations {
    // Inspect the escalation type
    if esc["type"] == "capability_request" {
        let requested_cap = esc["capability"].as_str().unwrap_or("");
        let child_id = esc["agent_id"].as_str().unwrap_or("");

        // Decide: approve or deny
        // (parent agents issue grants via IPC - direct API planned)
        tracing::info!("Child {} requests capability: {}", child_id, requested_cap);
    }
}
}

Grant Expiry

Grants are temporary. Design agents to handle grant expiry gracefully:

#![allow(unused)]
fn main() {
loop {
    match agent.invoke_tool("web.search", input.clone()).await {
        Ok(result) => { /* success */ break; }
        Err(AgentError::ToolFailed(ref msg)) if msg.contains("access denied") => {
            // Grant may have expired - wait or escalate
            tracing::warn!("Access denied - grant may have expired");
            break;
        }
        Err(e) => return Err(e.into()),
    }
}
}

Best Practice: Manifest Over Grants

Prefer declaring capabilities in the manifest over relying on runtime grants. Grants are intended for:

  • One-off operations the agent didn't anticipate needing
  • Temporary escalation during an unusual workflow
  • Operator-approved access to sensitive resources

Regular, predictable tool usage should be in the manifest.