Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 111 additions & 19 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,25 @@ fn gethostname() -> Option<String> {
}
}

/// Check if a tool name matches a pattern, supporting exact matches and prefix wildcards (ending with *).
/// Examples:
/// "exact_tool" matches "exact_tool"
/// "prefix_*" matches "prefix_anything"
/// "*" matches everything
pub(crate) fn tool_matches_pattern(pattern: &str, tool_name: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern == tool_name {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
let prefix = prefix.strip_suffix('_').unwrap_or(prefix);
return tool_name.starts_with(prefix);
}
false
}

impl OpenFangKernel {
/// Boot the kernel with configuration from the given path.
pub fn boot(config_path: Option<&Path>) -> KernelResult<Self> {
Expand Down Expand Up @@ -5097,12 +5116,12 @@ impl OpenFangKernel {
caps.iter().any(|c| matches!(c, Capability::ToolAll))
});

let mut all_tools: Vec<ToolDefinition> = if !tools_unrestricted {
// Agent declares specific tools — only include matching builtins
all_builtins
.into_iter()
.filter(|t| declared_tools.iter().any(|d| d == &t.name))
.collect()
let mut all_tools: Vec<ToolDefinition> = if !tools_unrestricted {
// Agent declares specific tools — only include matching builtins
all_builtins
.into_iter()
.filter(|t| declared_tools.iter().any(|d| tool_matches_pattern(d, &t.name)))
.collect()
} else {
// No specific tools declared — fall back to profile or all builtins
match &tool_profile {
Expand All @@ -5112,7 +5131,7 @@ impl OpenFangKernel {
let allowed = profile.tools();
all_builtins
.into_iter()
.filter(|t| allowed.iter().any(|a| a == "*" || a == &t.name))
.filter(|t| allowed.iter().any(|a| a == "*" || tool_matches_pattern(a, &t.name)))
.collect()
}
_ if has_tool_all => all_builtins,
Expand Down Expand Up @@ -5141,11 +5160,11 @@ impl OpenFangKernel {
registry.tool_definitions_for_skills(&skill_allowlist)
}
};
for skill_tool in skill_tools {
// If agent declares specific tools, only include matching skill tools
if !tools_unrestricted && !declared_tools.iter().any(|d| d == &skill_tool.name) {
continue;
}
for skill_tool in skill_tools {
// If agent declares specific tools, only include matching skill tools
if !tools_unrestricted && !declared_tools.iter().any(|d| tool_matches_pattern(d, &skill_tool.name)) {
continue;
}
all_tools.push(ToolDefinition {
name: skill_tool.name.clone(),
description: skill_tool.description.clone(),
Expand Down Expand Up @@ -5173,13 +5192,13 @@ impl OpenFangKernel {
.cloned()
.collect()
};
for t in mcp_candidates {
// If agent declares specific tools, only include matching MCP tools
if !tools_unrestricted && !declared_tools.iter().any(|d| d == &t.name) {
continue;
}
all_tools.push(t);
}
for t in mcp_candidates {
// If agent declares specific tools, only include matching MCP tools
if !tools_unrestricted && !declared_tools.iter().any(|d| tool_matches_pattern(d, &t.name)) {
continue;
}
all_tools.push(t);
}
}

// Step 4: Apply per-agent tool_allowlist/tool_blocklist overrides.
Expand Down Expand Up @@ -6844,4 +6863,77 @@ mod tests {

kernel.shutdown();
}

#[test]
fn test_tool_matches_pattern_exact() {
assert!(tool_matches_pattern("exact_tool", "exact_tool"));
assert!(!tool_matches_pattern("exact_tool", "different_tool"));
}

#[test]
fn test_tool_matches_pattern_prefix_wildcard() {
assert!(tool_matches_pattern("prefix_*", "prefix_anything"));
assert!(tool_matches_pattern("prefix_*", "prefix_"));
assert!(tool_matches_pattern("prefix_*", "prefix"));
assert!(!tool_matches_pattern("prefix_*", "different_prefix_"));
assert!(!tool_matches_pattern("prefix_*", "suffix_prefix"));
}

#[test]
fn test_tool_matches_pattern_star() {
assert!(tool_matches_pattern("*", "anything"));
assert!(tool_matches_pattern("*", ""));
assert!(tool_matches_pattern("*", "exact_tool"));
assert!(tool_matches_pattern("*", "prefix_*"));
}

#[test]
fn test_tool_matches_pattern_edge_cases() {
assert!(!tool_matches_pattern("", "nonempty"));
assert!(tool_matches_pattern("", ""));
assert!(!tool_matches_pattern("nomatch*", "prefix"));
}

#[test]
fn test_tool_matches_pattern_with_mcp_tools() {
let mcp_tools = vec![
ToolDefinition {
name: "mcp_filesystem_list_allowed_directories".to_string(),
description: "List allowed directories".to_string(),
input_schema: serde_json::json!({}),
},
ToolDefinition {
name: "mcp_filesystem_read_file".to_string(),
description: "Read a file".to_string(),
input_schema: serde_json::json!({}),
},
ToolDefinition {
name: "mcp_github_create_issue".to_string(),
description: "Create a GitHub issue".to_string(),
input_schema: serde_json::json!({}),
},
];

#[allow(clippy::useless_vec)]
let caps = vec![Capability::ToolInvoke("mcp_filesystem_*".to_string())];

let available: Vec<ToolDefinition> = mcp_tools
.into_iter()
.filter(|t| {
caps.iter().any(|cap| {
if let Capability::ToolInvoke(pattern) = cap {
tool_matches_pattern(pattern, &t.name)
} else {
false
}
})
})
.collect();

let tool_names: Vec<_> = available.iter().map(|t| t.name.as_str()).collect();

assert!(tool_names.contains(&"mcp_filesystem_list_allowed_directories"));
assert!(tool_names.contains(&"mcp_filesystem_read_file"));
assert!(!tool_names.contains(&"mcp_github_create_issue"));
}
}
1 change: 1 addition & 0 deletions docs/agent-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ max_concurrent_tools = 5 # Max parallel tool executions
tools = ["file_read", "file_write", "file_list", "shell_exec",
"memory_store", "memory_recall", "web_fetch",
"agent_send", "agent_list", "agent_spawn", "agent_kill"]
# Or use wildcards: "mcp_filesystem_*", "mcp_*" for MCP tools
network = ["*"] # Network access patterns
memory_read = ["*"] # Memory namespaces agent can read
memory_write = ["self.*"] # Memory namespaces agent can write
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ provider = "groq"
model = "llama-3.3-70b-versatile"

[capabilities]
tools = ["file_read", "file_list", "web_fetch"]
tools = ["file_read", "file_list", "web_fetch"] # Or use wildcards: "mcp_*", "mcp_filesystem_*"
memory_read = ["*"]
memory_write = ["self.*"]
```
Expand Down
16 changes: 16 additions & 0 deletions docs/mcp-a2a.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ Helper functions (exported from `openfang_runtime::mcp`):
- `is_mcp_tool(name)` -- checks if a tool name starts with `mcp_`
- `extract_mcp_server(tool_name)` -- extracts the server name from a namespaced tool

#### Granting MCP Tool Access

MCP tools are namespaced as `mcp_{server}_{tool}` and require explicit capability grants in the agent manifest. Use wildcards to grant access to all tools from a server:

```toml
[capabilities]
# Grant all filesystem MCP tools
tools = ["mcp_filesystem_*"]

# Grant all MCP tools from any server
tools = ["mcp_*"]

# Grant specific MCP tools
tools = ["mcp_github_create_issue", "mcp_github_search_repos"]
```

#### Auto-Connection on Kernel Boot

When the kernel starts (`start_background_agents()`), it checks `config.mcp_servers`. If any are configured, it spawns a background task that calls `connect_mcp_servers()`. This method:
Expand Down
8 changes: 8 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,16 @@ Auto-compaction is enabled by default when the session reaches the threshold (co
tools = ["file_read", "web_fetch", "shell_exec"] # Must list each tool
# OR
# tools = ["*"] # Grant all tools (use with caution)
# OR use wildcards to match multiple tools:
# tools = ["mcp_filesystem_*"] # Grants all filesystem MCP tools
# tools = ["mcp_*"] # Grants all MCP tools (any server)
```

**Note on MCP tools**: MCP tools are namespaced as `mcp_{server}_{tool}` (e.g., `mcp_filesystem_read_file`). Use wildcards to grant access without listing each tool:
- `mcp_filesystem_*` - all filesystem server tools
- `mcp_github_*` - all GitHub server tools
- `mcp_*` - all MCP tools from any server

### "Permission denied" errors in agent responses

**Cause**: The agent is trying to use a tool or access a resource not in its capabilities.
Expand Down