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

MecanismoCaso de usoGranularidadeTempo de execução
Política de confirmação de ferramentasAprovação interativa em CLI/webPor ferramenta ou todas as ferramentasPausa a execução, emite um evento
BeforeToolCallbackGate programático / auditoriaLógica personalizada por chamadaDecisão síncrona, sem pausa
Controle de acesso (RBAC)Segurança empresarial baseada em funçõesPor usuário, por ferramentaNegar antes da execução
Interrupções de grafosFluxos de trabalho complexos de aprovaçãoPonto 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

  1. O LLM decide chamar delete_file com os argumentos {"path": "/data/report.csv"}
  2. O agente emite um Event com:
    {
      "actions": {
        "toolConfirmation": {
          "toolName": "delete_file",
          "functionCallId": "call_abc123",
          "args": {"path": "/data/report.csv"}
        }
      }
    }
  3. O fluxo do agente termina — a execução é pausada
  4. Sua interface exibe ao usuário: "O agente deseja excluir /data/report.csv. Permitir?"
  5. 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 ferramenta
  • Ok(Some(content)) — ignora a ferramenta e envia este conteúdo para LLM
  • Err(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:

  1. Verificação de RBAC (se o wrapper ProtectedTool for usado) — nega acesso a usuários não autorizados
  2. BeforeToolCallback — barreira programática que pode ignorar ou abortar
  3. ToolConfirmationPolicy — pausa para aprovação humana, se necessário
  4. A ferramenta é executada
  5. AfterToolCallback / AfterToolCallbackFull — inspeção pós-execução

Anterior: ← Controle de acesso | Próximo: Barreiras de proteção →