Discovery
Unified component discovery across multiple directories with upward traversal and on-demand MCP / tool discovery.
The discovery module provides a unified directory-scanning system that finds configuration files, skills, commands, agents, and plugins across multiple roots. It also owns the on-demand discovery contract — the model no longer relies on a pre-built index but reaches for mcp_list_* and tool_search at call time.
Design Philosophy
- Multi-source resolution — components can live in multiple locations; later entries override earlier ones
- Upward traversal — project-level config files are found by walking from the working directory up to the git root
- Claude Code compatibility — reads
.claude/for interop with existing workflows; when both.aleph/and.claude/exist at the project level, the native.aleph/wins on a name clash (so a project's own skill isn't shadowed by a same-named.claude/entry) - On-demand MCP / tool discovery — no eager indexing of MCP resources or prompts, no full schemas in the initial tool list; the model discovers on demand via
mcp_list_*andtool_search
Directory Strategy
Read Paths (by priority, later overrides earlier)
| Priority | Path | Notes |
|---|---|---|
| 0 | ~/.claude/ | Claude Code global (read-only) |
| 10 | ~/.aleph/ | Aleph global |
| 20+ | <project>/.claude/ | Project-level Claude Code (read-only) |
| 40+ | <project>/.aleph/ | Project-level Aleph native (outranks the project .claude/ on clash) |
Each global / project root's ~/.aleph/{skills,commands,agents,plugins} is scanned. On a same-ID clash between .aleph/ and .claude/, the native .aleph/ entry wins.
Write Paths
- Writes always go to
~/.aleph/;.claude/is never written
Core Components
DiscoveryManager
pub struct DiscoveryConfig {
pub working_dir: PathBuf,
pub scan_claude_dirs: bool,
pub scan_project_dirs: bool,
pub max_upward_depth: usize, // default 10
}
pub struct DiscoveryManager {
config: DiscoveryConfig,
scanner: DirectoryScanner,
}Key entry points:
| Method | Description |
|---|---|
with_defaults() | Build with the current directory + default depth |
find_config_files(filename) | Walk upward for <filename> (e.g. aleph.jsonc); returns [global, …project] — project entries come last and win |
discover_skill_dirs() / discover_command_dirs() / discover_agent_dirs() | Enumerate child directories under each scan root's <root>/<component>/ |
discover_plugins() | Scan ~/.aleph/plugins/ only (with monorepo one-level descent) |
discover_plugins_with_extra(parents) | Also scan each project-level <project>/.aleph/plugins/ |
aleph_home() / working_dir() / git_root() | Key path accessors |
get_scan_directories() | All scan roots in current priority order |
DirectoryScanner
The actual filesystem scan lives in src/discovery/scanner.rs:
- Each child directory must be a real directory (
symlink_metadata); symlinks pointing outside the expected tree are rejected - Hidden directories (
.git,.node_modules, …) are skipped - An
agents/subdirectory without anagent.mdmarker is not a valid extension agent (it could be an Aleph identity directory) - Plugin recognition accepts multiple manifest formats:
aleph.plugin.toml,aleph.plugin.json,.claude-plugin/plugin.json,.codex-plugin/plugin.json,.cursor-plugin/plugin.json,.cursorrules,.mcp.json, or the presence of any ofskills//commands//agents//hooks/directories, or.cursor/rules/
DiscoveredPath / DiscoverySource
pub struct DiscoveredPath {
pub path: PathBuf,
pub source: DiscoverySource,
pub name: String,
pub priority: u32,
}
pub enum DiscoverySource {
AlephGlobal, // ~/.aleph/
ClaudeGlobal, // ~/.claude/
Project, // <project>/.claude or .aleph
Plugin, // provided by a loaded plugin
}is_read_only() returns true for ClaudeGlobal and Project.
Upward Traversal
let manager = DiscoveryManager::with_defaults()?;
let configs = manager.find_config_files("aleph.jsonc")?;
// Walks from working_dir up to the git root (or max_upward_depth),
// returning [global, …project].Edge case: canonicalize() may succeed for the stop directory but fail for the current directory (or vice versa), causing the stop comparison to never match. find_upward tracks whether the stop was canonicalized and compares against both the raw and canonicalized form; max_upward_depth is the hard cap. find_upward also calls validate_path_component, which rejects empty strings, / / \, .. segments, and \0 — even when scan_project_dirs = false so the global branch cannot be exploited via path traversal.
On-Demand MCP / Tool Discovery
MCP: the old McpResourceIndexLayer (eager index layer) has been retracted. The model now discovers MCP resources and prompts through three capability-gated builtin tools:
| Tool | Description |
|---|---|
mcp_list_resources | List concrete resource URIs exposed by current MCP servers |
mcp_list_resource_templates | List URI templates (covers servers whose resource_count == 0 and only expose templates) |
mcp_list_prompts | List prompt names available for mcp_get_prompt |
Each mcp_list_* ships alongside its reader (mcp_read_resource / mcp_get_prompt) and is only registered in the tool registry while at least one connected server advertises that capability — the model never sees a tool that every call would reject. mcp_tool_bridge is the single channel that translates MCP manager events into registry mutations.
Tools / tool_search: non-core tool schemas are collapsed — the model sees only name + description, and pulls the full schema back on demand via get_tool_schema. tool_search is a self-contained BM25 meta-tool (no new dependencies) covering every deferred tool: the model queries by capability, gets the top matches with full schemas, and calls them directly. [tools] defer_mcp_tools pushes MCP tools into the deferred tier as well. MCP-qualified names ({server}__{tool}) are exempt from the builtin collapse tables.
Plugin Directory Recognition
Each child of ~/.aleph/plugins/ is treated as a plugin root (direct manifest) or a monorepo (one more level down — scan subdirectories for a manifest). The presence of path.join("skills") / commands / agents / hooks is sufficient on its own, and the directory coexists with Claude Code's .claude-plugin/plugin.json.
Plugin Marketplace
plugin.marketplace.list/plugin.marketplace.addmanage marketplace sources- The unified
plugin.installentry classifies by source (marketplace / zip / path) - Trust tiers: Official / Verified / Community / Unverified
- Pre-install disclosure: command+args, secrets, network / filesystem reach, pinned version + SHA256, trust tier
See Extensions Store.
Safety Properties
- No SQL injection — no database queries (filesystem only)
- No locks — no Mutex / RwLock usage
- Path-traversal safe —
validate_path_componentrejects empty strings,//\,..segments,\0;symlink_metadatarejects symlinks pointing outside the expected tree;is_hidden()skips.git/.node_modules - No UTF-8 slicing traps — all operations stay on char boundaries
- Git root cannot be hijacked via symlink —
find_git_rootdelegates tocrate::utils::paths::find_git_root; the start path is canonicalized before.gitdetection, so a.gitsymlink cannot mis-report an arbitrary ancestor as the repo root
Code Locations
src/discovery/mod.rs— module entry +DiscoveryManagersrc/discovery/scanner.rs— directory scan, upward traversal, plugin manifest recognitionsrc/discovery/paths.rs— path constants +find_upward+validate_path_componentsrc/discovery/types.rs—DiscoveredPath/DiscoverySource/ScanDirectorysrc/mcp/tool_bridge.rs— MCP ↔ tool registry bridge + capability gatessrc/builtin_tools/mcp_resource.rs—mcp_list_resources/mcp_list_resource_templates/mcp_read_resourcesrc/builtin_tools/mcp_prompt.rs—mcp_list_prompts/mcp_get_promptsrc/tools/tool_search.rs—tool_searchBM25 retriever +ToolSearchToolsrc/tools/scoped/deferred.rs— deferred tier +tool_searchpromotion
Related Pages
- Skills — How discovered skills are used
- Extensions — Plugin architecture
- MCP Integration — MCP resources / prompts / tools end to end
- Tool Infrastructure — Tool registry and tiering
- Extensions Store — Trust tiers and disclosure