Autorisation des outils
Contrôlez les outils qu’un agent peut exécuter et déterminez quand l’approbation humaine est requise. ADK-Rust fournit quatre mécanismes — de la simple confirmation par outil à RBAC complète — qui fonctionnent avec CLI, le serveur web et le protocole A2A.
Comparaison rapide
| Mécanisme | Cas d’utilisation | Granularité | Exécution |
|---|---|---|---|
| Politique de confirmation des outils | Approbation interactive dans CLI/le web | Par outil ou pour tous les outils | Suspend l’exécution et émet un événement |
| BeforeToolCallback | Point de contrôle programmatique / audit | Logique personnalisée par appel | Décision synchrone, sans suspension |
| Contrôle d’accès (RBAC) | Sécurité d’entreprise basée sur les rôles | Par utilisateur, par outil | Refus avant exécution |
| Interruptions du graphe | Flux de travail d’approbation complexes | Point de contrôle par nœud | Persiste l’état, reprend ultérieurement |
Politique de confirmation des outils
Le mécanisme intégré de supervision humaine. Lorsqu’un outil nécessitant une confirmation est appelé, l’agent se met en pause, émet un événement ToolConfirmationRequest et attend une décision Approve ou Deny lors de l’exécution suivante.
Configuration
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()
Fonctionnement
- Le LLM décide d’appeler
delete_fileavec les arguments{"path": "/data/report.csv"} - L’agent émet un
Eventavec :{ "actions": { "toolConfirmation": { "toolName": "delete_file", "functionCallId": "call_abc123", "args": {"path": "/data/report.csv"} } } } - Le flux de l’agent se termine — l’exécution est mise en pause
- Votre interface affiche à l’utilisateur : « L’agent souhaite supprimer
/data/report.csv. Autoriser ? » - Lors de l’
Runner::run()suivante, transmettez la décision associée à l’identifiant de l’appel de fonction provenant de la requête :
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
En cas de refus, l’outil est ignoré et le LLM reçoit un message tel que « L’exécution de l’outil a été refusée par l’utilisateur », afin de pouvoir adapter son approche.
Les décisions autorisent un seul appel précis
Une décision s’applique au seul appel pour lequel elle a été demandée. L’associer au nom de l’outil ferait qu’une seule approbation autoriserait tous les appels de cet outil ; ainsi, une approbation pour delete_file sur un chemin temporaire autoriserait également un appel ciblant autre chose. Deux appels au même outil lors d’un même tour nécessitent donc deux décisions.
Un identifiant d’appel inconnu signifie « aucune décision », ce qui laisse l’appel en attente de confirmation. En cas d’échec, le comportement consiste toujours à demander à nouveau plutôt qu’à exécuter.
Lier une décision à ses arguments
Lorsqu’une décision transite par quelque chose que vous ne contrôlez pas — un navigateur, une file d’attente ou un service d’approbation externe — l’identifiant de l’appel pourrait être réutilisé avec des arguments différents. Liez la décision aux arguments pour lesquels elle a été accordée :
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 l’appel reçu ne correspond pas à l’empreinte, la décision est ignorée et l’appel est considéré comme non confirmé. tool_call_fingerprint est canonique quel que soit l’ordre des clés ; un objet d’arguments sérialisé à nouveau correspond donc toujours.
Pour les décisions qui doivent s’appliquer selon une politique plutôt qu’à chaque appel, implémentez un
ToolConfirmationHandler au lieu d’élargir la map statique.
Exemple de CLI
Un agent terminal qui demande une confirmation avant d’exécuter des outils :
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(())
}
Exemple de serveur web
Un endpoint SSE qui diffuse les événements vers l’interface frontend. Lorsqu’un événement toolConfirmation arrive, l’interface frontend affiche une boîte de dialogue d’approbation et renvoie la décision :
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
Pour l’autorisation programmatique — vérifier les permissions, appeler un service d’authentification externe ou journaliser à des fins d’audit. Aucune interaction utilisateur n’est nécessaire.
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()?;
Valeurs de retour :
Ok(None)— autoriser l’exécution de l’outilOk(Some(content))— ignorer l’outil et envoyer ce contenu à LLM à la placeErr(e)— interrompre toute l’exécution de l’agent
Contrôle d’accès
Pour les RBAC d’entreprise avec des permissions basées sur les rôles. Consultez Contrôle d’accès pour la documentation complète.
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));
Interruptions de graphe
Pour les workflows d’approbation complexes où l’exécution doit conserver son état et reprendre ultérieurement. Consultez Agents de graphe pour la documentation complète.
Les agents de graphe prennent en charge les interruptions basées sur des points de contrôle : l’exécution se met en pause au niveau d’un nœud, conserve son état dans un magasin de points de contrôle, puis reprend après la saisie humaine — même après le redémarrage des serveurs.
Confirmation d’outil native au graphe
Un AgentNode préserve la politique standard de confirmation d’outil lorsqu’il s’exécute dans un
CompiledGraph. Au lieu d’aplatir le graphe en un flux d’événements Runner,
le graphe enregistre son propre front dans un point de contrôle et émet un événement personnalisé structuré
qui peut être lu avec 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;
Le graphe conserve le cycle de vie des nœuds, l’état intermédiaire, les sous-graphes imbriqués et la frontière en attente. Les nœuds terminés en même temps que la demande de confirmation sont sauvegardés par point de contrôle et ne sont pas rejoués après l’approbation. L’agent lui-même utilise la même sémantique de décision RunConfig qu’une exécution ADK normale.
Combinaison des mécanismes
Ces mécanismes se combinent naturellement :
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()?;
Ordre d’évaluation :
- Vérification RBAC (si l’enveloppe
ProtectedToolest utilisée) — refuse l’accès aux utilisateurs non autorisés BeforeToolCallback— barrière programmatique, peut ignorer ou interrompre l’exécutionToolConfirmationPolicy— met en pause pour demander une approbation humaine si nécessaire- L’outil s’exécute
AfterToolCallback/AfterToolCallbackFull— inspection après exécution
Voir aussi
- Contrôle d’accès — RBAC, SSO, journalisation d’audit
- Rappels — Tous les types de rappels et leur cycle de vie
- Agents de graphe — Interruptions basées sur des points de contrôle
- Garde-fous — Validation des entrées et des sorties
Précédent : ← Contrôle d’accès | Suivant : Garde-fous →