Native tool implementations the agent can call, organized by domain. Importing src.tools
registers every module below as a side effect (see init.py) — nothing else
needs to change to make a new tool visible to the model.
- registry.py -
ToolRegistry: the@registry.register(...)decorator, schema bookkeeping, andGROUP_ORDER/GROUP_TITLES(the canonical list of tool groups and their display headers used by/toolsand the system prompt). No tools of its own. - editing.py -
save_to_file/read_file/list_workspace_files/edit_file, plussafe_workspace_path()— the sandbox boundary every workspace-touching tool (includingcoding.py) resolves paths through. Group:editing. - coding.py -
run_python,run_shell(fixed command allowlist, not a blocklist) — scoped to the workspace directory with a short timeout. Nogittools:agent_workspace/isn't its own git repo, so a git command there would walk up to Tuffy's own project.gitinstead of staying sandboxed. Group:coding. - research.py -
web_search,get_datetime,translate. Group:research. - system.py -
get_system_stats,top_processes,capture_image,view_image. Group:system. - mcp_client.py - bridges the async-only MCP Python SDK into this synchronous
registry: reads
./.tuffy/mcp.json(gitignored — see docs/configure-mcp.md for the config shape) plus each loaded skill'smcp.json, connects over stdio, and registers each remote tool as<server>_<tool>under groupmcp:<server>. A server that fails to connect is skipped with a warning rather than blocking startup.
(src/memory.py's remember tool and src/skills/__init__.py's read_skill tool register
themselves directly rather than living in this package — main.py already imports both modules
for other reasons.)
See docs/configure-tools.md for the full guide. In short: pick the module matching its domain (or add a new one), write a plain function, decorate it:
from src.tools.registry import registry
@registry.register(
name="my_tool",
description="What this does and when the model should call it.",
parameters={"arg": {"type": "string", "description": "..."}},
required=["arg"],
group="research", # or editing/coding/system/docs/memory/mcp:<server>
)
def my_tool(arg: str) -> str:
...That's it — no registration list to update elsewhere. If it's a new module, add
import src.tools.<module> # noqa: F401 to __init__.py.
- All tools are always visible to the model, every turn. There's no runtime mode-switching
or filtering;
groupis metadata only, used for/toolsoutput and system-prompt section headers, not for hiding tools from the model. - The workspace sandbox is a real boundary, not decorative.
safe_workspace_path()inediting.pyrejects path traversal; every file-touching tool (includingcoding.py'srun_python/run_shell) resolves through it, so nothing here can touch files outsideagent_workspace/without the user explicitly widening that scope in code. run_shellis allowlisted, not blocklisted. An unrecognized command name fails safe by default rather than needing to be added to a "don't run this" list.