Autorización de herramientas

Controla qué herramientas puede ejecutar un agente y cuándo se requiere la aprobación humana. ADK-Rust proporciona cuatro mecanismos —desde la confirmación sencilla por herramienta hasta RBAC completo— que funcionan en CLI, el servidor web y el protocolo A2A.

Comparación rápida

MecanismoCaso de usoGranularidadTiempo de ejecución
Política de confirmación de herramientasAprobación interactiva en CLI/webPor herramienta o todas las herramientasPausa la ejecución, emite un evento
BeforeToolCallbackPuerta de control programática / auditoríaLógica personalizada por llamadaDecisión síncrona, sin pausa
Control de acceso (RBAC)Seguridad empresarial basada en rolesPor usuario y herramientaDenegación antes de la ejecución
Interrupciones del grafoFlujos de trabajo complejos de aprobaciónPunto de control por nodoPersiste el estado y reanuda más tarde

Política de confirmación de herramientas

El mecanismo integrado de participación humana. Cuando se llama a una herramienta que requiere confirmación, el agente se pausa, emite un evento ToolConfirmationRequest y espera una decisión Approve o Deny en la siguiente ejecución.

Configuración

use adk_agent::LlmAgentBuilder;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("assistant")
    .model(model)
    .instruction("You are a helpful assistant with file and email tools.")
    .tool(Arc::new(search_tool))
    .tool(Arc::new(delete_file_tool))
    .tool(Arc::new(send_email_tool))
    // Require confirmation for dangerous tools
    .require_tool_confirmation("delete_file")
    .require_tool_confirmation("send_email")
    .build()?;

// Or require confirmation for ALL tool calls:
// .require_tool_confirmation_for_all()

Cómo funciona

  1. LLM decide llamar a delete_file con los argumentos {"path": "/data/report.csv"}
  2. El agente emite un Event con:
    {
      "actions": {
        "toolConfirmation": {
          "toolName": "delete_file",
          "functionCallId": "call_abc123",
          "args": {"path": "/data/report.csv"}
        }
      }
    }
  3. El flujo del agente finaliza; la ejecución queda pausada
  4. Tu interfaz de usuario muestra al usuario: «El agente quiere eliminar /data/report.csv. ¿Permitirlo?»
  5. En la siguiente Runner::run(), pasa la decisión identificada mediante el ID de la llamada a la función de la solicitud:
use adk_core::{RunConfig, ToolConfirmationDecision};
use std::collections::HashMap;

let mut decisions = HashMap::new();
decisions.insert(
    "call_abc123".to_string(), // functionCallId from the request, not the tool name
    ToolConfirmationDecision::Approve, // or Deny
);

// The runner picks up the decision and continues

Si se deniega, la herramienta se omite y LLM recibe un mensaje como «El usuario denegó la ejecución de la herramienta», para que pueda ajustar su estrategia.

Las decisiones autorizan una única llamada exacta

Una decisión se aplica a la única llamada para la que se solicitó. Identificarla por el nombre de la herramienta haría que una aprobación autorizara todas las llamadas de esa herramienta, por lo que una aprobación para delete_file en una ruta temporal también autorizaría una llamada dirigida a otro destino. Por lo tanto, dos llamadas a la misma herramienta en un turno necesitan dos decisiones.

Un ID de llamada desconocido significa «sin decisión», por lo que la llamada queda a la espera de confirmación. Ante un error, siempre se vuelve a solicitar confirmación en lugar de ejecutar la llamada.

Vincular una decisión a sus argumentos

Cuando una decisión pasa por algo que no controlas —un navegador, una cola o un servicio externo de aprobación—, el ID de la llamada podría reutilizarse con argumentos diferentes. Vincula la decisión a los argumentos para los que se concedió:

use adk_core::{RunConfig, ToolConfirmationDecision, tool_call_fingerprint};
use serde_json::json;
use std::collections::HashMap;

let approved_args = json!({ "path": "/data/report.csv" });

let mut decisions = HashMap::new();
decisions.insert("call_abc123".to_string(), ToolConfirmationDecision::Approve);

let mut fingerprints = HashMap::new();
fingerprints.insert(
    "call_abc123".to_string(),
    tool_call_fingerprint("delete_file", &approved_args),
);

let config = RunConfig::builder()
    .tool_confirmation_decisions(decisions)
    .tool_confirmation_fingerprints(fingerprints)
    .build();

Si la llamada recibida no coincide con la huella digital, la decisión se ignora y la llamada se trata como no confirmada. tool_call_fingerprint es canónico con respecto al orden de las claves, por lo que un objeto de argumentos serializado de nuevo sigue coincidiendo.

Para decisiones que deben aplicarse según una política en lugar de por llamada, implementa un ToolConfirmationHandler en lugar de ampliar el mapa estático.

Ejemplo de CLI

Un agente de terminal que solicita confirmación antes de ejecutar herramientas:

use adk_agent::LlmAgentBuilder;
use adk_core::{
    Content, Event, RunConfig, ToolConfirmationDecision,
    SessionId, UserId,
};
use adk_runner::Runner;
use adk_session::InMemorySessionService;
use adk_model::GeminiModel;
use adk_tool::tool;
use futures::StreamExt;
use schemars::JsonSchema;
use serde::Deserialize;
use std::collections::HashMap;
use std::io::{self, Write};
use std::sync::Arc;

#[derive(Deserialize, JsonSchema)]
struct DeleteArgs {
    /// File path to delete
    path: String,
}

/// Delete a file from the filesystem.
#[tool]
async fn delete_file(args: DeleteArgs) -> Result<serde_json::Value, adk_core::AdkError> {
    // In production, actually delete the file
    Ok(serde_json::json!({"deleted": args.path}))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    let agent = LlmAgentBuilder::new("file-manager")
        .model(Arc::new(model))
        .instruction("You help manage files. Use delete_file when asked to remove files.")
        .tool(Arc::new(DeleteFile))
        .require_tool_confirmation("delete_file")
        .build()?;

    let session_service = Arc::new(InMemorySessionService::new());
    let runner = Runner::new(adk_runner::RunnerConfig {
        app_name: "file-manager".to_string(),
        agent: Arc::new(agent),
        session_service: session_service.clone(),
        ..Default::default()
    })?;

    let user_id = UserId::new("user-1")?;
    let session_id = SessionId::new("session-1")?;

    // Create session
    session_service.create(adk_session::CreateRequest {
        app_name: "file-manager".to_string(),
        user_id: "user-1".to_string(),
        session_id: Some("session-1".to_string()),
        state: HashMap::new(),
    }).await?;

    println!("File Manager (type 'quit' to exit)");
    loop {
        print!("> ");
        io::stdout().flush()?;
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        let input = input.trim();
        if input == "quit" { break; }

        let content = Content::new("user").with_text(input);
        let mut stream = runner.run(
            user_id.clone(), session_id.clone(), content,
        ).await?;

        while let Some(result) = stream.next().await {
            let event = result?;

            // Check if the agent is requesting tool confirmation
            if let Some(ref confirmation) = event.actions.tool_confirmation {
                println!(
                    "\n⚠️  The agent wants to run '{}' with args: {}",
                    confirmation.tool_name,
                    serde_json::to_string_pretty(&confirmation.args)?
                );
                print!("Allow? [y/n]: ");
                io::stdout().flush()?;

                let mut answer = String::new();
                io::stdin().read_line(&mut answer)?;

                let decision = if answer.trim().eq_ignore_ascii_case("y") {
                    ToolConfirmationDecision::Approve
                } else {
                    ToolConfirmationDecision::Deny
                };

                // Re-run with the decision
                let mut decisions = HashMap::new();
                // Keyed by the call ID, so the decision authorizes only this call.
                if let Some(call_id) = confirmation.function_call_id.clone() {
                    decisions.insert(call_id, decision);
                }

                let content = Content::new("user").with_text("");
                let mut resume_stream = runner.run(
                    user_id.clone(), session_id.clone(), content,
                ).await?;

                while let Some(result) = resume_stream.next().await {
                    let event = result?;
                    if let Some(ref content) = event.llm_response.content {
                        for part in &content.parts {
                            if let Some(text) = part.text() {
                                print!("{text}");
                            }
                        }
                    }
                }
                println!();
            } else if let Some(ref content) = event.llm_response.content {
                for part in &content.parts {
                    if let Some(text) = part.text() {
                        print!("{text}");
                    }
                }
            }
        }
        println!();
    }
    Ok(())
}

Ejemplo de servidor web

Un endpoint SSE que transmite eventos al frontend. Cuando llega un evento toolConfirmation, el frontend muestra un cuadro de diálogo de aprobación y envía la decisión de vuelta:

use adk_agent::LlmAgentBuilder;
use adk_core::{
    Content, RunConfig, ToolConfirmationDecision, SessionId, UserId,
};
use adk_runner::Runner;
use adk_session::InMemorySessionService;
use axum::{Json, Router, extract::State, response::sse::{Event, Sse}};
use axum::routing::post;
use futures::StreamExt;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    runner: Arc<Runner>,
}

#[derive(Deserialize)]
struct ChatRequest {
    message: String,
    user_id: String,
    session_id: String,
    /// Tool confirmation decisions from the previous turn
    #[serde(default)]
    tool_decisions: HashMap<String, String>, // "tool_name" -> "approve"|"deny"
}

async fn chat_handler(
    State(state): State<AppState>,
    Json(req): Json<ChatRequest>,
) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
    let runner = state.runner.clone();
    let user_id = UserId::new(&req.user_id).unwrap();
    let session_id = SessionId::new(&req.session_id).unwrap();
    let content = Content::new("user").with_text(&req.message);

    let stream = async_stream::stream! {
        let mut event_stream = match runner.run(user_id, session_id, content).await {
            Ok(s) => s,
            Err(e) => {
                yield Ok(Event::default().data(
                    serde_json::json!({"error": e.to_string()}).to_string()
                ));
                return;
            }
        };

        while let Some(result) = event_stream.next().await {
            match result {
                Ok(event) => {
                    // Emit tool confirmation request to frontend
                    if let Some(ref confirmation) = event.actions.tool_confirmation {
                        yield Ok(Event::default()
                            .event("tool_confirmation")
                            .data(serde_json::json!({
                                "toolName": confirmation.tool_name,
                                "args": confirmation.args,
                                "functionCallId": confirmation.function_call_id,
                            }).to_string()));
                    }

                    // Emit text content
                    if let Some(ref content) = event.llm_response.content {
                        for part in &content.parts {
                            if let Some(text) = part.text() {
                                yield Ok(Event::default()
                                    .event("text")
                                    .data(serde_json::json!({"text": text}).to_string()));
                            }
                        }
                    }
                }
                Err(e) => {
                    yield Ok(Event::default().data(
                        serde_json::json!({"error": e.to_string()}).to_string()
                    ));
                }
            }
        }

        yield Ok(Event::default().event("done").data("{}".to_string()));
    };

    Sse::new(stream)
}

// Frontend JavaScript (conceptual):
//
// const source = new EventSource('/api/chat');
// source.addEventListener('tool_confirmation', (e) => {
//   const data = JSON.parse(e.data);
//   showConfirmDialog(data.toolName, data.args, (approved) => {
//     fetch('/api/chat', {
//       method: 'POST',
//       body: JSON.stringify({
//         message: '',
//         tool_decisions: { [data.toolName]: approved ? 'approve' : 'deny' }
//       })
//     });
//   });
// });

BeforeToolCallback

Para la autorización programática: comprobar permisos, llamar a un servicio de autorización externo o registrar información para auditoría. No se necesita interacción del usuario.

use adk_agent::LlmAgentBuilder;
use adk_core::{BeforeToolCallback, CallbackContext, Content};
use std::sync::Arc;

let agent = LlmAgentBuilder::new("assistant")
    .model(model)
    .tool(Arc::new(my_tool))
    .before_tool_callback(Box::new(|ctx: Arc<dyn CallbackContext>| {
        Box::pin(async move {
            let tool_name = ctx.tool_name().unwrap_or("unknown");
            let tool_input = ctx.tool_input();

            // Log for audit
            tracing::info!(tool = tool_name, "tool execution requested");

            // Custom authorization logic
            let user_scopes = ctx.user_scopes();
            if tool_name == "admin_action" && !user_scopes.contains(&"admin".to_string()) {
                // Return Some(Content) to skip the tool
                return Ok(Some(
                    Content::new("tool")
                        .with_text("Permission denied: admin scope required")
                ));
            }

            Ok(None) // Allow execution
        })
    }))
    .build()?;

Valores de retorno:

  • Ok(None) — permite que la herramienta se ejecute
  • Ok(Some(content)) — omite la herramienta y envía este contenido al LLM en su lugar
  • Err(e) — cancela toda la ejecución del agente

Control de acceso

Para RBAC empresarial con permisos basados en roles. Consulta Control de acceso para obtener la documentación completa.

use adk_auth::{AccessControl, Role, Permission, ToolExt};

let ac = AccessControl::builder()
    .role(Role::new("analyst")
        .allow(Permission::Tool("search".into()))
        .allow(Permission::Tool("summarize".into()))
        .deny(Permission::Tool("delete_file".into())))
    .role(Role::new("admin")
        .allow(Permission::AllTools))
    .assign("alice@co.com", "admin")
    .assign("bob@co.com", "analyst")
    .build()?;

// Wrap tools with automatic permission checking
let protected_tool = my_tool.with_access_control(Arc::new(ac));

Interrupciones del grafo

Para flujos de trabajo de aprobación complejos en los que la ejecución debe conservar el estado y reanudarse posteriormente. Consulta Agentes de grafo para obtener la documentación completa.

Los agentes de grafo admiten interrupciones basadas en puntos de control, en las que la ejecución se pausa en un nodo, conserva el estado en un almacén de puntos de control y se reanuda después de recibir la entrada humana, incluso tras reinicios del servidor.

Confirmación de herramientas nativa del grafo

Un AgentNode conserva la política estándar de confirmación de herramientas cuando se ejecuta en un CompiledGraph. En lugar de aplanar el grafo en un flujo de eventos Runner, el grafo establece un punto de control de su propia frontera y emite un evento personalizado estructurado que puede leerse con GraphToolConfirmationPause::from_stream_event.

use adk_agent::LlmAgentBuilder;
use adk_core::{RunConfig, ToolConfirmationDecision};
use adk_graph::{
    checkpoint::MemoryCheckpointer,
    edge::{END, START},
    graph::StateGraph,
    node::{AgentNode, ExecutionConfig},
    state::State,
    interrupt::GraphToolConfirmationPause,
    stream::StreamMode,
};
use futures::StreamExt;
use std::{collections::HashMap, sync::Arc};

let agent = LlmAgentBuilder::new("file_manager")
    .model(model)
    .tool(delete_file_tool)
    .require_tool_confirmation("delete_file")
    .build()?;

let graph = StateGraph::with_channels(&["messages"])
    .add_node(AgentNode::new(Arc::new(agent)))
    .add_edge(START, "file_manager")
    .add_edge("file_manager", END)
    .compile()?
    .with_checkpointer(MemoryCheckpointer::new());

let mut events = Box::pin(graph.stream(
    State::new(),
    ExecutionConfig::new("delete-report"),
    StreamMode::Debug,
));

let pause = loop {
    match events.next().await.transpose()? {
        Some(event) => {
            if let Some(pause) = GraphToolConfirmationPause::from_stream_event(&event) {
                break pause;
            }
        }
        None => unreachable!("the graph must pause before the tool runs"),
    }
};

// Present `pause.request.tool_name` and `pause.request.args` to the approver. A decision
// is scoped to this exact function call ID; bind its arguments as well when it
// crosses an untrusted boundary.
let call_id = pause.request.function_call_id.expect("LLM tool calls have an ID");
let decisions = HashMap::from([(call_id, ToolConfirmationDecision::Approve)]);

// The checkpoint is selected automatically by thread ID. `pause.checkpoint_id` is
// available for audit records or an explicit `with_resume_from` call.
drop(events);
let final_events = graph.stream_with_run_config(
    State::new(),
    ExecutionConfig::new("delete-report"),
    StreamMode::Debug,
    RunConfig::builder().tool_confirmation_decisions(decisions).build(),
);
# let _ = pause;
# let _ = final_events;

El grafo conserva el ciclo de vida de los nodos, el estado intermedio, los subgrafos anidados y la frontera pendiente. Los nodos que se completaron junto con la solicitud de confirmación se guardan en un punto de control y no se reproducen después de la aprobación. El agente utiliza la misma semántica de decisión de RunConfig que una ejecución normal de ADK.

Combinación de mecanismos

Estos mecanismos se combinan de forma natural:

let agent = LlmAgentBuilder::new("secure-assistant")
    .model(model)
    // RBAC: deny unauthorized users entirely
    .tool(Arc::new(search_tool.with_access_control(Arc::new(ac))))
    // Callback: audit all tool calls
    .before_tool_callback(audit_callback())
    // Confirmation: require human approval for destructive ops
    .require_tool_confirmation("delete_file")
    .require_tool_confirmation("send_email")
    .build()?;

Orden de evaluación:

  1. Comprobación de RBAC (si se utiliza el envoltorio ProtectedTool): deniega el acceso a usuarios no autorizados
  2. BeforeToolCallback: barrera programática que puede omitir o cancelar
  3. ToolConfirmationPolicy: pausa para solicitar aprobación humana si es necesario
  4. La herramienta se ejecuta
  5. AfterToolCallback / AfterToolCallbackFull: inspección posterior a la ejecución

Anterior: ← Control de acceso | Siguiente: Guardarraíles →

Autorización de herramientas - Documentación ADK-Rust | ADK-Rust