Autorização de ferramentas
Controle quais ferramentas um agente pode executar e quando a aprovação humana é necessária. ADK-Rust fornece quatro mecanismos — desde a confirmação simples por ferramenta até o RBAC completo — que funcionam em todo o CLI, no servidor web e no protocolo A2A.
Comparação rápida
| Mecanismo | Caso de uso | Granularidade | Tempo de execução |
|---|---|---|---|
| Política de confirmação de ferramentas | Aprovação interativa em CLI/web | Por ferramenta ou todas as ferramentas | Pausa a execução, emite um evento |
| BeforeToolCallback | Gate programático / auditoria | Lógica personalizada por chamada | Decisão síncrona, sem pausa |
| Controle de acesso (RBAC) | Segurança empresarial baseada em funções | Por usuário, por ferramenta | Negar antes da execução |
| Interrupções de grafos | Fluxos de trabalho complexos de aprovação | Ponto de verificação por nó | Mantém o estado e retoma posteriormente |
Política de Confirmação de Ferramentas
O mecanismo integrado de participação humana no processo. Quando uma ferramenta que exige confirmação é chamada, o agente pausa, emite um evento ToolConfirmationRequest e aguarda uma decisão Approve ou Deny na próxima execução.
Configuração
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()
Como Funciona
- O LLM decide chamar
delete_filecom os argumentos{"path": "/data/report.csv"} - O agente emite um
Eventcom:{ "actions": { "toolConfirmation": { "toolName": "delete_file", "functionCallId": "call_abc123", "args": {"path": "/data/report.csv"} } } } - O fluxo do agente termina — a execução é pausada
- Sua interface exibe ao usuário: "O agente deseja excluir
/data/report.csv. Permitir?" - Na próxima
Runner::run(), passe a decisão associada ao ID da chamada da função da solicitação:
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
Se negada, a ferramenta é ignorada e o LLM recebe uma mensagem como "A execução da ferramenta foi negada pelo usuário", para que possa ajustar sua abordagem.
As Decisões Autorizam Uma Única Chamada Exata
Uma decisão se aplica à única chamada para a qual foi solicitada. Associá-la ao nome da ferramenta faria com que uma aprovação autorizasse todas as chamadas dessa ferramenta; assim, uma aprovação para delete_file em um caminho temporário também autorizaria uma chamada direcionada a outra coisa. Portanto, duas chamadas para a mesma ferramenta em uma única rodada precisam de duas decisões.
Um ID de chamada desconhecido significa "nenhuma decisão", o que mantém a chamada aguardando confirmação. Em caso de falha, o comportamento é sempre solicitar novamente em vez de executar.
Vinculando uma Decisão aos Seus Argumentos
Quando uma decisão passa por algo que você não controla — um navegador, uma fila, um serviço externo de aprovação — o ID da chamada pode ser reutilizado com argumentos diferentes. Vincule a decisão aos argumentos para os quais ela foi concedida:
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();
Se a chamada recebida não corresponder à impressão digital, a decisão será ignorada e a chamada será tratada como não confirmada. tool_call_fingerprint é canônico em relação à ordem das chaves, portanto, um objeto de argumentos serializado novamente ainda corresponderá.
Para decisões que devem ser aplicadas por política, em vez de por chamada, implemente um
ToolConfirmationHandler em vez de ampliar o mapa estático.
Exemplo de CLI
Um agente de terminal que solicita confirmação antes de executar ferramentas:
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(())
}
Exemplo de servidor Web
Um endpoint SSE que transmite eventos ao frontend. Quando um evento toolConfirmation chega, o frontend renderiza uma caixa de diálogo de aprovação e envia a decisão de volta:
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 autorização programática — verificar permissões, chamar um serviço externo de autenticação ou registrar para auditoria. Nenhuma interação do usuário é necessária.
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 a execução da ferramentaOk(Some(content))— ignora a ferramenta e envia este conteúdo para LLMErr(e)— aborta toda a execução do agente
Controle de acesso
Para RBAC empresarial com permissões baseadas em funções. Consulte Controle de acesso para obter a documentação 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));
Interrupções do grafo
Para fluxos de trabalho complexos de aprovação, nos quais a execução precisa persistir o estado e ser retomada posteriormente. Consulte Agentes de grafo para obter a documentação completa.
Os agentes de grafo são compatíveis com interrupções baseadas em pontos de verificação, nas quais a execução é pausada em um nó, o estado é persistido em um armazenamento de pontos de verificação e retomado após a entrada humana — mesmo após reinicializações do servidor.
Confirmação de ferramentas nativa do grafo
Um AgentNode preserva a política padrão de confirmação de ferramentas quando é executado em um
CompiledGraph. Em vez de achatar o grafo em um fluxo de eventos Runner,
o grafo cria um ponto de verificação de sua própria fronteira e emite um evento personalizado estruturado
que pode ser lido com 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;
O grafo retém o ciclo de vida dos nós, o estado intermediário, os subgrafos aninhados e a fronteira pendente. Os nós concluídos junto com a solicitação de confirmação são armazenados em checkpoint e não são reproduzidos após a aprovação. O próprio agente usa a mesma semântica de decisão de RunConfig que uma execução normal de ADK.
Combinação de mecanismos
Esses mecanismos são combinados naturalmente:
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()?;
Ordem de avaliação:
- Verificação de RBAC (se o wrapper
ProtectedToolfor usado) — nega acesso a usuários não autorizados BeforeToolCallback— barreira programática que pode ignorar ou abortarToolConfirmationPolicy— pausa para aprovação humana, se necessário- A ferramenta é executada
AfterToolCallback/AfterToolCallbackFull— inspeção pós-execução
Relacionados
- Controle de acesso — RBAC, SSO, registro de auditoria
- Callbacks — Todos os tipos de callback e o ciclo de vida
- Agentes de grafo — Interrupções baseadas em checkpoints
- Barreiras de proteção — Validação de entrada/saída
Anterior: ← Controle de acesso | Próximo: Barreiras de proteção →