Skip to content

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

  1. 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))
  1. Register in server/tools/__init__.py::build_registry():
from .my_tool import MyTool
registry.register(MyTool())
  1. 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 radius not # filter bash
  • No emojis in code comments or log messages
  • Log at DEBUG for per-request trace, INFO for lifecycle events, WARNING/ERROR for problems

Testing

No automated test suite currently. Manual testing via the smoke test scripts:

python smoke_test.py        # basic API checks
python smoke_test_live.py   # checks against a running server

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).