agentkit-tools-core
Core abstractions for defining, registering, executing, and governing tools in agentkit.
This crate provides:
Tool trait — the interface every tool implements (spec + async invoke)
ToolRegistry — a name-keyed collection of tools
BasicToolExecutor — looks up tools, checks permissions, invokes them
- Permission system — composable policies (
PathPolicy, CommandPolicy, McpServerPolicy, CustomKindPolicy) combined via CompositePermissionChecker
- Interruption types —
ApprovalRequest and AuthRequest for human-in-the-loop flows
- Capability bridge —
ToolCapabilityProvider adapts a registry into the agentkit capability layer
Defining a tool
Implement the Tool trait with a ToolSpec and an async invoke method.
If the tool performs operations that need permission checks (filesystem
access, shell commands), override proposed_requests to declare them.
use agentkit_core::{MetadataMap, ToolOutput, ToolResultPart};
use agentkit_tools_core::{
FileSystemPermissionRequest, PermissionRequest, Tool, ToolContext, ToolError,
ToolName, ToolRequest, ToolResult, ToolSpec,
};
use async_trait::async_trait;
use serde_json::json;
use std::path::PathBuf;
struct ReadFileTool {
spec: ToolSpec,
}
impl ReadFileTool {
fn new() -> Self {
Self {
spec: ToolSpec::new(
"read_file",
"Read a file from the local filesystem",
json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Absolute file path" }
},
"required": ["path"]
}),
),
}
}
}
#[async_trait]
impl Tool for ReadFileTool {
fn spec(&self) -> &ToolSpec {
&self.spec
}
fn proposed_requests(
&self,
request: &ToolRequest,
) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
let path = request.input["path"]
.as_str()
.ok_or_else(|| ToolError::InvalidInput("missing `path`".into()))?;
Ok(vec![Box::new(FileSystemPermissionRequest::Read {
path: PathBuf::from(path),
metadata: MetadataMap::new(),
})])
}
async fn invoke(
&self,
request: ToolRequest,
_ctx: &mut ToolContext<'_>,
) -> Result<ToolResult, ToolError> {
let path = request.input["path"].as_str().unwrap();
let content = std::fs::read_to_string(path)
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
Ok(ToolResult::new(ToolResultPart::success(
request.call_id,
ToolOutput::text(content),
)))
}
}
Registering tools and executing them
Build a ToolRegistry, wrap it in a BasicToolExecutor, and call
execute with a ToolRequest and ToolContext.
use agentkit_capabilities::CapabilityContext;
use agentkit_core::{MetadataMap, SessionId, ToolOutput, ToolResultPart, TurnId};
use agentkit_tools_core::{
BasicToolExecutor, PermissionChecker, PermissionDecision, Tool, ToolContext, ToolError,
ToolExecutionOutcome, ToolExecutor, ToolName, ToolRegistry, ToolRequest, ToolResult, ToolSpec,
};
use async_trait::async_trait;
use serde_json::json;
struct EchoTool { spec: ToolSpec }
#[async_trait]
impl Tool for EchoTool {
fn spec(&self) -> &ToolSpec { &self.spec }
async fn invoke(
&self, request: ToolRequest, _ctx: &mut ToolContext<'_>,
) -> Result<ToolResult, ToolError> {
Ok(ToolResult::new(ToolResultPart::success(
request.call_id,
ToolOutput::structured(json!({ "ok": true })),
)))
}
}
struct AllowAll;
impl PermissionChecker for AllowAll {
fn evaluate(
&self, _request: &dyn agentkit_tools_core::PermissionRequest,
) -> PermissionDecision {
PermissionDecision::Allow
}
}
# #[tokio::main]
# async fn main() {
let registry = ToolRegistry::new().with(EchoTool {
spec: ToolSpec::new("echo", "Return a fixed payload", json!({ "type": "object" })),
});
let executor = BasicToolExecutor::new(registry);
let metadata = MetadataMap::new();
let mut ctx = ToolContext {
capability: CapabilityContext {
session_id: Some(&SessionId::new("s1")),
turn_id: Some(&TurnId::new("t1")),
metadata: &metadata,
},
permissions: &AllowAll,
resources: &(),
cancellation: None,
};
let outcome = executor
.execute(
ToolRequest::new(
"call-1",
"echo",
json!({}),
SessionId::new("s1"),
TurnId::new("t1"),
),
&mut ctx,
)
.await;
assert!(matches!(outcome, ToolExecutionOutcome::Completed(_)));
# }
Setting up permissions
Use CompositePermissionChecker to layer multiple policies. Policies are
evaluated in order; the first Deny short-circuits. Any RequireApproval
is returned unless a later policy denies.
use agentkit_tools_core::{
CommandPolicy, CompositePermissionChecker, PathPolicy,
McpServerPolicy, PermissionDecision,
};
let permissions = CompositePermissionChecker::new(PermissionDecision::Allow)
.with_policy(
PathPolicy::new()
.allow_root("/workspace/project")
.protect_root("/workspace/project/.env"),
)
.with_policy(
CommandPolicy::new()
.allow_executable("git")
.allow_executable("cargo")
.deny_executable("rm")
.require_approval_for_unknown(true),
)
.with_policy(
McpServerPolicy::new()
.trust_server("github-mcp"),
);