Tool System
Architecture, traits, and design of Aleph's extensible tool framework
Overview
Aleph's tool system is a type-safe, extensible framework that gives the agent the ability to act on the world — executing shell commands, reading files, searching the web, controlling a browser, and more. Every tool is defined as a Rust trait implementation with automatic JSON Schema generation, giving compile-time correctness and runtime flexibility.
Three principles:
- Type safety first — Tool arguments and outputs are strongly-typed Rust
structs. The JSON Schema sent to the LLM is auto-generated from these types
via
schemars, eliminating schema drift. - One chokepoint — Every tool call funnels through
ScopedToolService(src/tools/scoped/), which merges the per-channel tool-permission layer with the exec tier and the sandbox command-policy floor. - Capability-gated surfaces — Capability-bearing builtin tools (MCP resource / prompt readers, the OAuth login helper) are only present in the registry while at least one connected server actually advertises that capability.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Harness │
│ │
│ Arc<dyn ToolService> (per-request override) │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ScopedToolService (src/tools/scoped/) │
│ │
│ • tool_permissions merge (global → agent → channel, │
│ most-restrictive wins) │
│ • exec tier (Ask / Auto / Full) │
│ • sandbox::command_policy floor │
│ • action-aware approval gate (HITL seam) │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LoopToolRegistry │
│ │
│ Builtin handlers · MCP handlers (McpHandler per server) · │
│ Capability-gated builtin tools (mcp_read_resource, │
│ mcp_list_resources, mcp_list_resource_templates, mcp_get_prompt, │
│ mcp_list_prompts, mcp_login) │
└─────────────────────────────────────────────────────────────────┘Source locations:
- Production chokepoint:
src/tools/scoped/(dispatch.rs,builder.rs,traits.rs,ledger.rs) - Tool registry:
src/tools/registry/,src/tools/handlers/ - Builtin tools:
src/builtin_tools/ - MCP client / manager:
src/mcp/ - Browser runtime + per-action tools:
src/browser/,src/builtin_tools/browser_tools/
The production chokepoint — ToolService
Harness consumers depend on Arc<dyn ToolService> exclusively:
pub trait ToolService: Send + Sync + 'static {
async fn execute(&self, name: &str, input: Value) -> Result<ToolOutput, ToolError>;
async fn list(&self) -> Vec<ToolDefinition>;
async fn describe(&self, name: &str) -> Option<ToolDefinition>;
fn dispatcher_schema(&self) -> Arc<[crate::dispatcher::ToolDefinition]>;
}The production impl is ScopedToolService (src/tools/scoped/). The
Gateway builds one per request via
gateway::execution_engine::tool_service_builder::build_request_tool_service
and supplies it as FlowRequest::tool_service, so each turn sees an
allow-listed view over the shared LoopToolRegistry.
ScopedToolService carries the HITL seams natively:
with_confirmation(confirm_tools, requester)— gatesrequires_confirmationtools throughApprovalRequester(wired at boot toChannelApprovalBridgeAdapter).with_turn_context(TurnContext)— scopes theTURN_CONTEXTtask-local for every tool call so HITL tools (ask_user, sandbox escalations, channel approval) can route back to the originating channel.
Tool authors implement AlephTool (typed) or LoopTool (untyped via
RegistryToolAdapter). The harness fallback AgentHarnessRunner.tool_service
is NullToolService (src/tools/null.rs) — production never reaches it
because Gateway always supplies the per-request override; a NotFound from
that service signals an upstream wiring regression.
The pre-ScopedToolService Phase 2 decorator chain (facade.rs /
dispatch.rs / registry.rs / middleware/ / handlers/, ~2,700 lines)
was deleted in 2026-05-20; it was unreachable because every Gateway request
overrode it.
The AlephTool trait
AlephTool is the primary interface for typed tools. Static-dispatch, with
the JSON Schema auto-derived from Args:
pub trait AlephTool: Clone + Send + Sync + 'static {
const NAME: &'static str;
const DESCRIPTION: &'static str;
type Args: Serialize + DeserializeOwned + JsonSchema + Send;
type Output: Serialize + Send;
async fn call(&self, args: Self::Args) -> Result<Self::Output>;
async fn call_json(&self, args: Value) -> Result<Value> { /* blanket */ }
fn definition(&self) -> ToolDefinition { /* blanket, schemars */ }
}LoopTool is the untyped, JSON-Value shape used by McpHandler and the
capability-gated builtin tools. RegistryToolAdapter bridges the two.
Schema generation via schemars
Tool argument types derive JsonSchema, which generates the JSON Schema sent
to the LLM. Field-level documentation comments become parameter
descriptions:
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchArgs {
/// Search query (natural language)
pub query: String,
/// Maximum results to return (default: 10)
#[serde(default)]
pub max_results: Option<u32>,
}The required array is auto-computed: only fields without #[serde(default)]
are required.
Tool categories
| Source | Dispatch | Examples |
|---|---|---|
Built-in (src/builtin_tools/) | Static (AlephTool) | bash_exec, code_exec, file_ops, web_fetch, search, memory_*, browser_* |
| MCP (external process) | Dynamic (LoopTool via McpHandler) | <server>__<tool>, capability-gated mcp_read_resource / mcp_list_resources / mcp_list_resource_templates / mcp_get_prompt / mcp_list_prompts / mcp_login |
Browser per-action (src/builtin_tools/browser_tools/) | Static | browser_open, browser_click, browser_navigate, browser_snapshot, browser_screenshot, browser_evaluate, browser_fill_form, … |
A capability-gated builtin appears in the registry only while at least one
connected MCP server advertises that capability — the model is never offered
a tool that every call would reject. The bridge reconciles every server's
capability set on every manager event (src/mcp/tool_bridge.rs).
Built-in tool registration
Builtin tools are registered by the executor's builtin registry
(src/executor/builtin_registry/), not by a hand-written builder. Each tool
holds the shared dependencies it needs (Arc<Sandbox>, Arc<ProfileManager>,
Arc<McpManagerHandle>, …) and the registry owns wiring them up at boot.
A tool constructed without a required dependency returns a structured "sandbox not configured" / "manager not configured" error rather than falling back to unscoped execution — the default is safe, not permissive.
Per-channel tool permissions
Limiting what an agent may do (as opposed to who may connect) is the job
of the per-channel tool-permission layer, ScopedToolService. It merges a
ToolPermissionsConfig across three tiers — global → agent → channel,
most-restrictive wins — and does not read IdentityContext. This is
orthogonal to connection trust.
Three compositions happen at the same chokepoint, in this order:
[policies.tool_permissions]— per-toolallow/ask/deny, exact name or glob, most-restrictive-wins across the three tiers. An explicit entry beats the tier.- The exec tier (
Ask/Auto/Full) — consultsToolFacts{idempotent, requires_approval}, never the tool's name. Default is fail-closed: a tool Aleph has not heard of is non-idempotent, henceAskunder any tier. [sandbox.command_policy]— the hardline command floor, which no tier and no permission entry can lower.
Deny hides the tool from the model and refuses the call; Ask routes to
the approval gate.
Tool result
#[derive(Debug, Serialize, Deserialize)]
pub struct ToolResult {
pub success: bool,
pub output: Value,
pub error: Option<String>,
pub duration_ms: u64,
}Integration with the agent loop
When the LLM responds with a tool call:
- Resolve the call against the per-request
ScopedToolService. - Filter through the merged
tool_permissions(allow / ask / deny). - Consult the exec tier for tools nobody named (Ask / Auto / Full).
- Gate confirm-gated tools through the approval requester (HITL).
- Execute — exec-class tools route through
WorkspaceSandbox. - Record the decision to the session event log; signed tools also append to the per-agent signed ledger.
Next steps
- Built-in Tools — the catalog of shipped tools
- MCP Integration — connecting external MCP servers
- Creating Tools — how to add a new tool
- Browser Automation — per-action browser tools