Development Guide¶
Project structure recap¶
aide/
├── server/ # All Python code
│ ├── main.py # FastAPI entry point
│ ├── config.py # Settings singleton
│ ├── database.py # Pool, migrations, stores
│ ├── auth.py # Password hashing, sessions, TOTP
│ ├── audit.py # Audit log
│ ├── security.py # Whitelist enforcement, sanitisation
│ ├── context_vars.py # asyncio ContextVars
│ ├── agent/ # Core agent loop
│ ├── agents/ # Scheduled agents (runner + CRUD)
│ ├── tools/ # Tool implementations
│ ├── providers/ # AI provider abstractions
│ ├── web/ # REST API routes + templates
│ ├── brain/ # 2nd Brain (pgvector)
│ ├── inbox/ # Email inbox listeners
│ ├── telegram/ # Telegram bot
│ ├── monitors/ # RSS + page monitors
│ ├── mcp_client/ # MCP server client
│ └── webhooks/ # Inbound webhooks
├── docs/ # This documentation
├── SOUL.md # Agent personality
├── USER.md # Owner context
├── requirements.txt
├── Dockerfile
└── docker-compose.yml.example
Running in development¶
# From the project root
source venv/bin/activate
python -m uvicorn server.main:app --host 0.0.0.0 --port 8080 --reload
--reload restarts the server on any .py file change. Templates and static files are hot-reloaded without restart via the volume mounts.
Code conventions¶
Async everywhere¶
All I/O is async. Never use blocking calls in the agent path:
- httpx.AsyncClient (not requests)
- asyncpg (not psycopg2)
- asyncio.to_thread() for unavoidable blocking calls (e.g. imaplib)
Tool result contract¶
Every BaseTool.execute() must:
- Never raise — catch all exceptions, return ToolResult(success=False, error=...)
- Return ToolResult(success=True, data=...) on success
- Use data: dict for structured results, data: str for plain text
The dispatcher (tool_registry.py) has a final exception safety net, but tools should not rely on it.
No exceptions into the agent loop¶
# Good
async def execute(self, **kwargs) -> ToolResult:
try:
result = await do_thing()
return ToolResult(success=True, data=result)
except Exception as e:
return ToolResult(success=False, error=str(e))
# Bad — may crash the agent loop
async def execute(self, **kwargs) -> ToolResult:
result = await do_thing() # can raise!
return ToolResult(success=True, data=result)
Type hints everywhere¶
from __future__ import annotations
async def my_function(name: str, count: int = 0) -> list[str]:
...
The from __future__ import annotations import enables forward references without quotes.
Dependency imports inside functions¶
Circular imports are a recurring issue due to the dependency graph (agent → tools → database → config, etc.). The pattern:
# In tools/__init__.py — avoid at module level
def build_registry():
from ..agent.tool_registry import ToolRegistry # safe: called after all modules load
...
When you see circular import errors, move the import inside the function body.
get_pool() always imported locally in routes.py¶
Due to the module load order, get_pool is always imported inside route handler functions:
@router.get("/api/something")
async def get_something():
from ..database import get_pool as _gp # never at module level here
pool = await _gp()
...
Adding a new tool¶
- Create
server/tools/my_tool.py:
"""
tools/my_tool.py — One-line description.
Longer explanation of what this tool does, why it was built this way,
and any important security or design decisions.
"""
from __future__ import annotations
from .base import BaseTool, ToolResult
class MyTool(BaseTool):
name = "my_tool"
description = "One sentence for Claude. Operations: op1, op2."
input_schema = {
"type": "object",
"properties": {
"operation": {"type": "string", "enum": ["op1", "op2"]},
},
"required": ["operation"],
}
requires_confirmation = False
allowed_in_scheduled_tasks = True
async def execute(self, operation: str, **kwargs) -> ToolResult:
if operation == "op1":
return await self._op1(**kwargs)
return ToolResult(success=False, error=f"Unknown operation: {operation!r}")
async def _op1(self, **kwargs) -> ToolResult:
try:
# ... do work ...
return ToolResult(success=True, data={"result": "..."})
except Exception as e:
return ToolResult(success=False, error=str(e))
- Register in
server/tools/__init__.py::build_registry():
- Test in the chat UI and verify it appears in the audit log.
Adding a database migration¶
Migrations are in server/database.py::_MIGRATIONS — a list of lists of SQL strings.
_MIGRATIONS: list[list[str]] = [
# ... existing migrations ...
# v29 — add my new table
[
"""CREATE TABLE IF NOT EXISTS my_table (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL
)""",
"CREATE INDEX IF NOT EXISTS idx_my_table_name ON my_table(name)",
],
]
Rules:
- All SQL must be idempotent (IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, ON CONFLICT DO NOTHING)
- Each migration is a list of SQL statements (asyncpg runs one at a time)
- Migrations never modify existing data — add new columns with defaults, never rename or drop
- Update the # Current DB schema: vN comment in CLAUDE.md when you add a migration
Coding style¶
- No line length limit enforced, but keep functions short and focused
- No auto-formatter configured — match the style of surrounding code
- Comments explain why, not what:
# Non-admin users can't use bash — limit blast radiusnot# filter bash - No emojis in code comments or log messages
- Log at
DEBUGfor per-request trace,INFOfor lifecycle events,WARNING/ERRORfor problems
Testing¶
No automated test suite currently. Manual testing via the smoke test scripts:
The EchoTool and ConfirmTool in server/tools/mock.py are available for testing by passing include_mock=True to build_registry().
Commit and deploy workflow¶
# Local
git add <specific files> # never git add -A
git commit -m "Clear description of the change and why"
git push # always push immediately
# On server
git pull
docker compose build # if Python files changed
docker compose up -d
Never sign commits (git commit -S / --gpg-sign).