Plugin tools use the same format as functions/*.py — they're registered with the function manager and the AI calls them like any built-in tool.
For simple tool creation without a full plugin, see TOOLMAKER.md.
ENABLED = True
EMOJI = '🔧'
AVAILABLE_FUNCTIONS = ['my_tool_do_thing']
TOOLS = [
{
"type": "function",
"is_local": True,
"function": {
"name": "my_tool_do_thing",
"description": "Does the thing",
"parameters": {
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "What to do it to"
}
},
"required": ["target"]
}
}
}
]
def execute(function_name, arguments, config):
"""Called by function manager.
Args:
function_name: Which function was called
arguments: Dict of parameters
config: System config
Returns:
(message: str, success: bool) tuple
"""
if function_name == "my_tool_do_thing":
target = arguments.get("target", "")
return f"Did the thing to {target}", True
return "Unknown function", False| Export | Type | Description |
|---|---|---|
ENABLED |
bool | Whether tool is active |
EMOJI |
str | Display icon |
AVAILABLE_FUNCTIONS |
list | Function names this file provides |
TOOLS |
list | OpenAI-compatible function schemas |
execute() |
function | Dispatcher — returns (message, success) |
"capabilities": {
"tools": ["tools/my_tool.py"]
}Inside each tool's schema dict:
| Flag | Type | Default | Description |
|---|---|---|---|
is_local |
bool/str | True |
True = runs locally, "endpoint" = calls external API, False = network required |
network |
bool | false |
Mark as network-dependent (tracked by function manager) |
TOOLS = [{
"type": "function",
"is_local": "endpoint", # calls Home Assistant API
"network": True, # needs network access
"function": { ... }
}]Tools that support multiple accounts (email, bitcoin, etc.) can read the active scope:
from core.chat.function_manager import scope_email
def execute(function_name, arguments, config):
account = scope_email.get() # returns active account name (ContextVar)
creds = load_credentials(account)
# ... use account-specific credentialsAvailable scope ContextVars: scope_email, scope_bitcoin, scope_knowledge, scope_memory, scope_people, scope_rag, scope_goal.
Tools can load their own plugin's settings:
import json
from pathlib import Path
def _load_settings():
path = Path("user/webui/plugins/my-plugin.json")
if path.exists():
return json.loads(path.read_text())
return {}Or via the plugin loader (merges with manifest defaults):
from pathlib import Path
import json
DEFAULTS = {"timeout": 30, "max_results": 10}
def _load_settings():
path = Path(__file__).parent.parent.parent.parent / "user" / "webui" / "plugins" / "my-plugin.json"
settings = DEFAULTS.copy()
if path.exists():
try:
user = json.loads(path.read_text())
settings.update(user)
except Exception:
pass
return settingsEach plugin gets a persistent JSON key-value store at user/plugin_state/{name}.json:
from core.plugin_loader import plugin_loader
state = plugin_loader.get_plugin_state("my-plugin")
state.get("counter", 0) # read
state.save("counter", 42) # write (auto-persists)
state.delete("counter") # remove key
state.all() # entire dict
state.clear() # wipe everythingFor heavier storage, plugins can create their own SQLite database.
Never expose raw credentials (emails, keys, addresses) to the AI. Resolve at execution time:
# BAD — AI sees raw email addresses
def execute(function_name, arguments, config):
return f"Contacts: alice@example.com, bob@example.com", True
# GOOD — AI only sees names and IDs
def execute(function_name, arguments, config):
contacts = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
return json.dumps(contacts), TrueFor tools that execute commands (SSH, shell):
BLACKLIST = ["rm -rf /", "mkfs", "dd if=/dev", ":(){ :|:& };:"]
def _check_blacklist(command):
for pattern in BLACKLIST:
try:
if re.search(pattern, command):
return f"Blocked: matches '{pattern}'"
except re.error:
if pattern in command:
return f"Blocked: contains '{pattern}'"
return NoneFor tools that fetch external data, cache per-scope with TTL:
_cache = {}
CACHE_TTL = 60
def _get_cached(scope):
entry = _cache.get(scope)
if entry and time.time() - entry["timestamp"] < CACHE_TTL:
return entry["data"]
return None
def _invalidate(scope):
_cache.pop(scope, None)Tools are added to toolsets and the AI calls them contextually. See TOOLS.md for the user-facing tools guide.