उपकरण प्राधिकरण

नियंत्रित करें कि कोई एजेंट किन उपकरणों को निष्पादित कर सकता है और मानव अनुमोदन कब आवश्यक है। 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()

यह कैसे काम करता है

  1. LLM delete_file को args {"path": "/data/report.csv"} के साथ कॉल करने का निर्णय लेता है
  2. एजेंट एक Event उत्सर्जित करता है:
    {
      "actions": {
        "toolConfirmation": {
          "toolName": "delete_file",
          "functionCallId": "call_abc123",
          "args": {"path": "/data/report.csv"}
        }
      }
    }
  3. एजेंट स्ट्रीम समाप्त हो जाती है — निष्पादन रोक दिया जाता है
  4. आपका UI उपयोगकर्ता को दिखाता है: "एजेंट /data/report.csv को हटाना चाहता है। अनुमति दें?"
  5. अगले Runner::run() पर, अनुरोध से फ़ंक्शन कॉल ID द्वारा कुंजीबद्ध निर्णय भेजें:
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 के लिए दी गई अनुमति किसी अन्य लक्ष्य वाली कॉल को भी अधिकृत कर देती। इसलिए एक ही टर्न में एक ही टूल की दो कॉल के लिए दो निर्णय आवश्यक हैं।

अज्ञात कॉल ID का अर्थ है "कोई निर्णय नहीं", जिससे कॉल पुष्टि की प्रतीक्षा में रहती है। विफलता की दिशा हमेशा निष्पादन करने के बजाय दोबारा पूछने की होती है।

किसी निर्णय को उसके आर्ग्युमेंट से बाँधना

जब कोई निर्णय ऐसी चीज़ के माध्यम से भेजा जाता है जिसे आप नियंत्रित नहीं करते — जैसे ब्राउज़र, कतार या बाहरी अनुमति सेवा — तो कॉल ID को अलग आर्ग्युमेंट के साथ दोबारा चलाया जा सकता है। निर्णय को उन आर्ग्युमेंट से बाँधें जिनके लिए उसे दिया गया था:

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 कुंजी क्रम के संबंध में canonical है, इसलिए दोबारा क्रमबद्ध किया गया आर्ग्युमेंट ऑब्जेक्ट भी मेल खाता है।

ऐसे निर्णयों के लिए जिन्हें हर कॉल के बजाय नीति के आधार पर लागू किया जाना चाहिए, स्थिर मैप को विस्तृत करने के बजाय एक 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;

ग्राफ़ नोड जीवनचक्र, मध्यवर्ती स्थिति, नेस्टेड सबग्राफ़ और लंबित फ्रंटियर को बनाए रखता है। पुष्टिकरण अनुरोध के साथ पूर्ण हुए नोड्स का चेकपॉइंट बनाया जाता है और स्वीकृति के बाद उन्हें दोबारा चलाया नहीं जाता। एजेंट स्वयं सामान्य ADK रन के समान RunConfig निर्णय शब्दार्थ का उपयोग करता है।

तंत्रों का संयोजन

ये तंत्र स्वाभाविक रूप से संयोजित होते हैं:

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

मूल्यांकन का क्रम:

  1. RBAC जाँच (यदि ProtectedTool रैपर का उपयोग किया गया है) — अनधिकृत उपयोगकर्ताओं को अस्वीकार करती है
  2. BeforeToolCallback — प्रोग्रामेटिक गेट, छोड़ सकता है या निरस्त कर सकता है
  3. ToolConfirmationPolicy — आवश्यक होने पर मानव अनुमोदन के लिए रुकता है
  4. टूल निष्पादित होता है
  5. AfterToolCallback / AfterToolCallbackFull — निष्पादन के बाद निरीक्षण

पिछला: ← पहुँच नियंत्रण | अगला: गार्डरेल →

उपकरण प्राधिकरण - ADK-Rust दस्तावेज़ीकरण | ADK-Rust