تفويض الأدوات
تحكّم في الأدوات التي يمكن للوكيل تنفيذها ومتى تكون موافقة الإنسان مطلوبة. يوفر ADK-Rust أربع آليات — بدءًا من التأكيد البسيط لكل أداة ووصولًا إلى RBAC الكامل — تعمل عبر CLI وخادم الويب وبروتوكول A2A.
مقارنة سريعة
| الآلية | حالة الاستخدام | مستوى الدقة | وقت التشغيل |
|---|---|---|---|
| سياسة تأكيد الأداة | الموافقة التفاعلية في CLI/الويب | لكل أداة أو جميع الأدوات | يوقف التنفيذ مؤقتًا، ويصدر حدثًا |
| BeforeToolCallback | بوابة برمجية / تدقيق | منطق مخصص لكل استدعاء | قرار متزامن، دون إيقاف مؤقت |
| التحكم في الوصول (RBAC) | أمان مؤسسي قائم على الأدوار | لكل مستخدم ولكل أداة | المنع قبل التنفيذ |
| مقاطعات الرسم البياني | سير عمل موافقة معقد | نقطة تحقق لكل عقدة | يحفظ الحالة ويستأنف لاحقًا |
سياسة تأكيد الأدوات
الآلية المضمّنة للتفاعل البشري أثناء التنفيذ. عند استدعاء أداة تتطلب تأكيدًا، يتوقف الوكيل، ويصدر حدثًا ToolConfirmationRequest، وينتظر قرارًا Approve أو Deny في التشغيل التالي.
الإعداد
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()
آلية العمل
- يقرر LLM استدعاء
delete_fileباستخدام الوسائط{"path": "/data/report.csv"} - يصدر الوكيل
Eventمع:{ "actions": { "toolConfirmation": { "toolName": "delete_file", "functionCallId": "call_abc123", "args": {"path": "/data/report.csv"} } } } - ينتهي تدفق الوكيل — ويُوقَف التنفيذ مؤقتًا
- تعرض واجهة المستخدم لديك للمستخدم: "يريد الوكيل حذف
/data/report.csv. هل تسمح؟" - في
Runner::run()التالي، مرّر القرار المفهرس بمعرّف استدعاء الدالة الوارد في الطلب:
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
إذا رُفض الطلب، يتم تخطي الأداة، ويتلقى LLM رسالة مثل "رفض المستخدم تنفيذ الأداة"، حتى يتمكن من تعديل نهجه.
تسمح القرارات باستدعاء واحد محدد بدقة
ينطبق القرار على الاستدعاء الواحد الذي طُلب من أجله. إن استخدام اسم الأداة كمفتاح سيجعل موافقة واحدة تسمح بكل استدعاءات تلك الأداة، ولذلك فإن الموافقة على
delete_file في مسار مؤقت ستسمح أيضًا باستدعاء يستهدف شيئًا آخر. لذلك، يتطلب استدعاءان للأداة نفسها في دورة واحدة قرارين.
يعني معرّف الاستدعاء غير المعروف "لا يوجد قرار"، مما يُبقي الاستدعاء بانتظار التأكيد. يكون اتجاه الفشل دائمًا نحو طلب التأكيد مرة أخرى بدلًا من التنفيذ.
ربط القرار بوسائطه
عندما ينتقل القرار عبر شيء لا تتحكم فيه — مثل متصفح أو قائمة انتظار أو خدمة موافقة خارجية — فقد يُعاد تشغيل معرّف الاستدعاء باستخدام وسائط مختلفة. اربط القرار بالوسائط التي مُنح من أجلها:
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();
إذا لم يطابق الاستدعاء الوارد بصمة التعريف، فسيتم تجاهل القرار، ويُعامل الاستدعاء على أنه غير مؤكد. يكون tool_call_fingerprint هو الشكل الأساسي بغض النظر عن ترتيب المفاتيح، ولذلك يظل كائن الوسائط الذي أُعيد تسلسله مطابقًا.
بالنسبة إلى القرارات التي ينبغي تطبيقها وفقًا للسياسة بدلًا من كل استدعاء، نفِّذ ToolConfirmationHandler بدلًا من توسيع الخريطة الثابتة.
مثال على CLI
وكيل طرفي يطلب التأكيد قبل تشغيل الأدوات:
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(())
}
مثال على خادم ويب
نقطة نهاية SSE تبث الأحداث إلى الواجهة الأمامية. عند وصول حدث toolConfirmation، تعرض الواجهة الأمامية مربع حوار للموافقة وترسل القرار مرة أخرى:
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
للتفويض البرمجي — تحقّق من الأذونات، أو استدعِ خدمة مصادقة خارجية، أو سجّل لأغراض التدقيق. لا حاجة إلى تفاعل المستخدم.
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()?;
القيم المُعادة:
Ok(None)— السماح بتنفيذ الأداةOk(Some(content))— تخطّي الأداة وإرسال هذا المحتوى إلى LLM بدلًا من ذلكErr(e)— إيقاف تنفيذ الوكيل بالكامل
التحكم في الوصول
لـ RBAC المؤسسية ذات الأذونات المستندة إلى الأدوار. راجع التحكم في الوصول للاطلاع على الوثائق الكاملة.
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));
مقاطعات الرسم البياني
لسير عمل الموافقة المعقدة الذي يحتاج فيه التنفيذ إلى حفظ الحالة واستئنافه لاحقًا. راجع الوكلاء الرسوميين للاطلاع على الوثائق الكاملة.
يدعم وكلاء الرسم البياني المقاطعات المستندة إلى نقاط التحقق، حيث يتوقف التنفيذ عند عقدة، ويحفظ الحالة في مخزن نقاط التحقق، ثم يُستأنف بعد إدخال بشري — حتى عبر عمليات إعادة تشغيل الخادم.
تأكيد الأدوات الأصلي للرسم البياني
يحافظ AgentNode على سياسة تأكيد الأدوات القياسية عند تشغيله داخل CompiledGraph. بدلًا من تسطيح الرسم البياني إلى دفق أحداث Runner، ينشئ الرسم البياني نقطة تحقق لجبهته الخاصة ويصدر حدثًا مخصصًا منظمًا يمكن قراءته باستخدام 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;
تحتفظ الرسم البياني بدورة حياة العقد، والحالة الوسيطة، والرسوم البيانية الفرعية المتداخلة، والجبهة المعلّقة. ويتم وضع نقاط تحقق للعقد التي اكتمل تنفيذها بالتزامن مع طلب التأكيد، ولا يُعاد تشغيلها بعد الموافقة. ويستخدم الوكيل نفسه دلالات قرار RunConfig مثل تشغيل ADK العادي.
دمج الآليات
تتآلف هذه الآليات بصورة طبيعية:
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()?;
ترتيب التقييم:
- فحص RBAC (إذا استُخدم غلاف
ProtectedTool) — يرفض المستخدمين غير المصرّح لهم BeforeToolCallback— بوابة برمجية، يمكنها التخطي أو الإيقافToolConfirmationPolicy— يوقف التنفيذ مؤقتًا للحصول على موافقة بشرية عند الحاجة- تُنفَّذ الأداة
AfterToolCallback/AfterToolCallbackFull— فحص ما بعد التنفيذ
ذات صلة
- التحكم في الوصول — RBAC وSSO وتسجيل التدقيق
- عمليات الاستدعاء — جميع أنواع عمليات الاستدعاء ودورة الحياة
- الوكلاء القائمون على الرسم البياني — المقاطعات القائمة على نقاط التحقق
- الضوابط الوقائية — التحقق من الإدخال والإخراج
السابق: ← التحكم في الوصول | التالي: الضوابط الوقائية →