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
| Mecanismo | Caso de uso | Granularidad | Tiempo de ejecución |
|---|---|---|---|
| Política de confirmación de herramientas | Aprobación interactiva en CLI/web | Por herramienta o todas las herramientas | Pausa la ejecución, emite un evento |
| BeforeToolCallback | Puerta de control programática / auditoría | Lógica personalizada por llamada | Decisión síncrona, sin pausa |
| Control de acceso (RBAC) | Seguridad empresarial basada en roles | Por usuario y herramienta | Denegación antes de la ejecución |
| Interrupciones del grafo | Flujos de trabajo complejos de aprobación | Punto de control por nodo | Persiste 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
- LLM decide llamar a
delete_filecon los argumentos{"path": "/data/report.csv"} - El agente emite un
Eventcon:{ "actions": { "toolConfirmation": { "toolName": "delete_file", "functionCallId": "call_abc123", "args": {"path": "/data/report.csv"} } } } - El flujo del agente finaliza; la ejecución queda pausada
- Tu interfaz de usuario muestra al usuario: «El agente quiere eliminar
/data/report.csv. ¿Permitirlo?» - 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 ejecuteOk(Some(content))— omite la herramienta y envía este contenido al LLM en su lugarErr(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:
- Comprobación de RBAC (si se utiliza el envoltorio
ProtectedTool): deniega el acceso a usuarios no autorizados BeforeToolCallback: barrera programática que puede omitir o cancelarToolConfirmationPolicy: pausa para solicitar aprobación humana si es necesario- La herramienta se ejecuta
AfterToolCallback/AfterToolCallbackFull: inspección posterior a la ejecución
Relacionado
- Control de acceso: RBAC, SSO y registro de auditoría
- Callbacks: todos los tipos de callbacks y el ciclo de vida
- Agentes de grafo: interrupciones basadas en puntos de control
- Guardarraíles: validación de entrada y salida
Anterior: ← Control de acceso | Siguiente: Guardarraíles →