Werkzeugautorisierung

Steuern Sie, welche Tools ein Agent ausführen darf und wann eine menschliche Genehmigung erforderlich ist. ADK-Rust bietet vier Mechanismen – von der einfachen Bestätigung pro Tool bis hin zu vollständigem RBAC –, die über CLI, den Webserver und das A2A-Protokoll hinweg funktionieren.

Schneller Vergleich

MechanismusAnwendungsfallGranularitätLaufzeit
Richtlinie zur Tool-BestätigungInteraktive Genehmigung in CLI/webPro Tool oder alle ToolsUnterbricht die Ausführung, gibt ein Ereignis aus
BeforeToolCallbackProgrammatische Kontrolle / AuditBenutzerdefinierte Logik pro AufrufSynchrone Entscheidung, keine Unterbrechung
Zugriffskontrolle (RBAC)Rollenbasierte UnternehmenssicherheitPro Benutzer, pro ToolVerweigert vor der Ausführung
Graph-UnterbrechungenKomplexe GenehmigungsworkflowsPrüfpunkt pro KnotenSpeichert den Zustand und setzt später fort

Richtlinie zur Tool-Bestätigung

Der integrierte Mechanismus für den Menschen-in-der-Schleife. Wenn ein Tool aufgerufen wird, das eine Bestätigung erfordert, pausiert der Agent, gibt ein ToolConfirmationRequest-Ereignis aus und wartet beim nächsten Lauf auf eine Approve- oder Deny-Entscheidung.

Einrichtung

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()

Funktionsweise

  1. Der LLM entscheidet sich, delete_file mit den Argumenten {"path": "/data/report.csv"} aufzurufen.
  2. Der Agent gibt ein Event aus mit:
    {
      "actions": {
        "toolConfirmation": {
          "toolName": "delete_file",
          "functionCallId": "call_abc123",
          "args": {"path": "/data/report.csv"}
        }
      }
    }
  3. Der Agenten-Stream endet — die Ausführung wird pausiert.
  4. Ihre Benutzeroberfläche zeigt dem Benutzer: „Der Agent möchte /data/report.csv löschen. Zulassen?“
  5. Übergeben Sie beim nächsten Runner::run() die Entscheidung, mit der ID des Funktionsaufrufs als Schlüssel, aus der Anfrage:
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

Wenn die Anfrage abgelehnt wird, wird das Tool übersprungen und der LLM erhält eine Nachricht wie „Die Tool-Ausführung wurde vom Benutzer abgelehnt“, damit er seine Vorgehensweise anpassen kann.

Entscheidungen autorisieren genau einen Aufruf

Eine Entscheidung gilt für den einzelnen Aufruf, für den sie angefordert wurde. Eine Zuordnung nach Toolnamen würde dazu führen, dass eine Genehmigung jeden Aufruf dieses Tools autorisiert. Eine Genehmigung für delete_file in einem temporären Pfad würde dann auch einen Aufruf autorisieren, der auf etwas anderes abzielt. Zwei Aufrufe desselben Tools in einem Durchlauf benötigen daher zwei Entscheidungen.

Eine unbekannte Aufruf-ID bedeutet „keine Entscheidung“ und lässt den Aufruf auf die Bestätigung warten. Bei einem Fehler wird immer erneut nachgefragt, statt die Ausführung zu starten.

Binden einer Entscheidung an ihre Argumente

Wenn eine Entscheidung durch etwas übertragen wird, das Sie nicht kontrollieren — einen Browser, eine Warteschlange oder einen externen Genehmigungsdienst —, könnte die Aufruf-ID mit anderen Argumenten erneut verwendet werden. Binden Sie die Entscheidung an die Argumente, für die sie erteilt wurde:

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();

Wenn der eingehende Aufruf nicht mit dem Fingerabdruck übereinstimmt, wird die Entscheidung ignoriert und der Aufruf als unbestätigt behandelt. tool_call_fingerprint ist unabhängig von der Schlüsselreihenfolge kanonisch, sodass ein erneut serialisiertes Argumentobjekt weiterhin übereinstimmt.

Für Entscheidungen, die per Richtlinie statt pro Aufruf gelten sollen, implementieren Sie eine ToolConfirmationHandler, anstatt die statische Map zu erweitern.

Beispiel für CLI

Ein Terminal-Agent, der vor der Ausführung von Tools eine Bestätigung anfordert:

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(())
}

Beispiel für einen Webserver

Ein SSE-Endpunkt, der Ereignisse an das Frontend streamt. Wenn ein toolConfirmation-Ereignis eintrifft, rendert das Frontend einen Genehmigungsdialog und sendet die Entscheidung zurück:

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

Für programmatische Autorisierung — Berechtigungen prüfen, einen externen Authentifizierungsdienst aufrufen oder zur Prüfung protokollieren. Es ist keine Benutzerinteraktion erforderlich.

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()?;

Rückgabewerte:

  • Ok(None) — die Ausführung des Tools erlauben
  • Ok(Some(content)) — das Tool überspringen und diesen Inhalt stattdessen an LLM senden
  • Err(e) — die gesamte Agentenausführung abbrechen

Zugriffskontrolle

Für Enterprise-RBAC mit rollenbasierten Berechtigungen. Eine vollständige Dokumentation finden Sie unter Zugriffskontrolle.

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));

Graph-Unterbrechungen

Für komplexe Genehmigungs-Workflows, bei denen die Ausführung den Zustand speichern und später fortgesetzt werden muss. Eine vollständige Dokumentation finden Sie unter Graph-Agenten.

Graph-Agenten unterstützen checkpointbasierte Unterbrechungen, bei denen die Ausführung an einem Knoten pausiert, der Zustand in einem Checkpoint-Speicher gespeichert wird und die Ausführung nach menschlicher Eingabe fortgesetzt wird — auch nach Neustarts des Servers.

Tool-Bestätigung nativ im Graphen

Ein AgentNode bewahrt die Standardrichtlinie für Tool-Bestätigungen, wenn es in einem CompiledGraph ausgeführt wird. Anstatt den Graphen in einen Runner-Ereignisstrom zu verflachen, setzt der Graph seinen eigenen Ausführungsvorlauf als Checkpoint und gibt ein strukturiertes benutzerdefiniertes Ereignis aus, das mit GraphToolConfirmationPause::from_stream_event gelesen werden kann.

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;

Der Graph behält den Lebenszyklus der Knoten, den Zwischenstatus, verschachtelte Teilgraphen und die ausstehende Frontier. Knoten, die zusammen mit der Bestätigungsanfrage abgeschlossen wurden, werden als Checkpoint gespeichert und nach der Genehmigung nicht erneut ausgeführt. Der Agent selbst verwendet dieselbe RunConfig-Entscheidungssemantik wie ein normaler ADK-Lauf.

Mechanismen kombinieren

Diese Mechanismen lassen sich problemlos kombinieren:

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()?;

Reihenfolge der Auswertung:

  1. RBAC-Prüfung (falls der Wrapper ProtectedTool verwendet wird) — verweigert nicht autorisierten Benutzern den Zugriff
  2. BeforeToolCallback — programmatisches Gate, kann überspringen oder abbrechen
  3. ToolConfirmationPolicy — pausiert bei Bedarf zur Genehmigung durch einen Menschen
  4. Das Tool wird ausgeführt
  5. AfterToolCallback / AfterToolCallbackFull — Prüfung nach der Ausführung

Zurück: ← Zugriffskontrolle | Weiter: Leitplanken →