도구 권한 부여

에이전트가 실행할 수 있는 도구와 사람의 승인이 필요한 시점을 제어합니다. 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을 인수 {"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이 표준이므로 인수 객체를 다시 직렬화해도 일치합니다.

정책에 따라 호출별이 아니라 적용되어야 하는 결정의 경우, 정적 맵을 확장하는 대신 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));

그래프 인터럽트

실행 상태를 유지하고 나중에 재개해야 하는 복잡한 승인 워크플로를 위한 기능입니다. 전체 문서는 그래프 에이전트를 참조하세요.

그래프 에이전트는 체크포인트 기반 인터럽트를 지원합니다. 실행이 노드에서 일시 중지되고 상태가 체크포인트 저장소에 유지된 후, 사용자 입력이 완료되면 재개됩니다. 서버가 재시작된 이후에도 가능합니다.

그래프 네이티브 도구 확인

AgentNodeCompiledGraph에서 실행될 때 표준 도구 확인 정책을 유지합니다. 그래프를 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 — 실행 후 검사

이전: ← 액세스 제어 | 다음: 가드레일 →