Internals¶
Architecture¶
┌──────────────────────────────────────────────────┐
│ MCP Clients │
│ (Cursor, Cline, Amp, Claude, …) │
└────────────────────┬─────────────────────────────┘
│ MCP (stdio or Streamable HTTP)
┌────────────────────▼─────────────────────────────┐
│ server.py — FastMCP transport layer │
│ • MCP tool definitions (42 tools) │
│ • Per-branch / per-team / system locking │
│ • Unified error handler (FlowError → ToolError) │
│ • Web UI mount (Starlette) │
│ • Bearer token auth (MCP) / Basic auth (Web UI) │
│ • Team resolution (token → Host → default) │
└────────────────────┬─────────────────────────────┘
│
┌───────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
system_ops env_ops service_ops
(init/destroy/ (create/delete/ (create/delete/
template mgmt) start/stop/ update/list/
restart/list/ logs)
pull/exec)
│ │ │
│ ▼ │
│ odoo_ops │
│ (install/upgrade/ │
│ test/logs/exec) │
│ │ │
└───────────────┼───────────────────┘
│
Docker SDK (docker-py)
│
┌───────────────┼────────────────────┐
▼ ▼ ▼
oduflow-net oduflow-db oduflow-{branch}-odoo
(network) (PostgreSQL) (Odoo containers)
oduflow-svc-{name}
(Service containers)
Key Architectural Decisions¶
| Decision | Rationale |
|---|---|
| Single process, single uvicorn worker | Designed for a single developer or small team; no shared-state problems |
Granular LockManager (per-branch, per-team, system) |
Operations on different branches run in parallel; same-branch operations are serialised with BusyError |
| Docker SDK only (no subprocess for Docker) | Consistent error handling; put_archive replaces docker cp |
| fuse-overlayfs for filestore | Copy-on-write sharing of a large template filestore across all environments |
Stable port registry (ports.json) |
Port assignments survive container restarts; eliminates TOCTOU race conditions |
| Typed error hierarchy | FlowError base with NotFoundError, BusyError, ConflictError, PrerequisiteNotMetError, ExternalCommandError, ProtectedError — clients can distinguish error types |
| Traefik routing mode (optional) | Automatic HTTPS with Let's Encrypt for production-like setups |
| Dual dump format support | Accepts both plain SQL (.sql) and PostgreSQL custom format (.pgdump) dumps |
| Auto-detection of UID/GID | Resolves Odoo container's UID:GID from the image to set correct file permissions |
| TOML-based multi-team config | Per-team isolation with shared infrastructure; settings loaded from oduflow.toml |
Project Structure¶
src/oduflow/
server.py # MCP transport: tool definitions, error handler, locking, CLI
settings.py # @dataclass Settings, loads from oduflow.toml (TOML)
errors.py # FlowError hierarchy (7 error classes)
models.py # EnvironmentRef dataclass
naming.py # Pure functions: slugify, db name, resource name, paths, URL sanitization
locking.py # LockManager with per-branch, per-team, and system locks
git_ops.py # Git clone, pull, credential management, manifest parsing
git_analysis.py # Classify changed files → install / upgrade / restart / refresh
port_registry.py # Stable port allocation with JSON persistence
web_ui.py # Starlette-based dashboard, REST API, Basic auth middleware
extra_addons.py # Extra addon repo management (clone, worktree, odoo.conf generation)
env_credentials.py # Per-environment PostgreSQL credentials
sanitizer.py # DB sanitization (SQL/Python scripts)
licensing.py # License verification and installation (RSA signatures)
systemd.py # Systemd service install/uninstall
docker_ops/
client.py # docker.from_env() wrapper + UID/GID auto-detection
system_ops.py # init_system / destroy_system / reload_template / init_template /
# save_env_as_template / delete_template / list_templates
env_ops.py # create / delete / start / stop / restart / rebuild / list / status / pull /
# apt/pip auto-install / filestore overlay mount
odoo_ops.py # install / upgrade / test / logs / shell / search / run_command
service_ops.py # create / delete / update / list / logs for auxiliary services
service_presets.py # Save / restore / list / delete service preset configurations
stats.py # Container and system CPU/RAM stats (parallel collection)
templates/
oduflow.toml # Default TOML configuration (copied on first `oduflow init`)
odoo.conf # Odoo configuration template (addons path, limits, security)
postgresql.conf # PostgreSQL tuning (shared_buffers, WAL, autovacuum, etc.)
dashboard.html # Web dashboard UI (single-page application)
favicon.ico # Dashboard favicon
agent_guides/ # AI agent guides (copied to team data dirs on init)
agent_guide.md # Main agent instructions for Oduflow MCP tools
odoo_15_guide.md # Odoo 15 development standards
odoo_16_guide.md # Odoo 16 development standards
odoo_17_guide.md # Odoo 17 development standards
odoo_18_guide.md # Odoo 18 development standards
odoo_19_guide.md # Odoo 19 development standards
tests/ # Unit and integration tests (pytest)
Environment Workspace Structure¶
Each branch gets an isolated workspace:
{data_dir}/team_{ID}/workspaces/{branch}/
repo/ ← shallow git clone (--depth 1)
filestore_upper/ ← overlay upper layer (branch-specific changes)
filestore_work/ ← overlay work directory (required by overlayfs)
filestore/ ← merged overlay mount (bound into the container)
sessions/ ← Odoo session storage
When template_name="none" (no template), the filestore is a plain directory (no overlay).
You can verify active overlay mounts with df -h — each environment with a template gets its own fuse-overlayfs mount:
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv 97G 74G 19G 81% /
fuse-overlayfs 97G 74G 19G 81% /srv/oduflow/team_1/workspaces/manuf-plan/filestore
fuse-overlayfs 97G 74G 19G 81% /srv/oduflow/team_1/workspaces/fixing-landing/filestore
File Ownership (macOS vs Linux)¶
Odoo containers run as uid=101 gid=101. Oduflow must set this ownership on
workspace files so the container can read/write them. The behaviour differs
between platforms:
| Linux | macOS (Docker Desktop) | |
|---|---|---|
| Docker runtime | Native — UID/GID are shared between host and container | Runs inside a Linux VM; files are projected via VirtioFS |
| Host file ownership | Matches container UID (e.g. 101:101) |
Always shown as the macOS user regardless of in-container owner |
os.chown from host |
Works (when running as root) | Raises PermissionError — VirtioFS ignores host-side chown |
To handle both platforms transparently, Oduflow uses chown_recursive()
(docker_ops/client.py):
- Try host-side
os.chown— fast, works on Linux. - On
PermissionError— fall back tochown -Rinside a throwaway container with the target path bind-mounted. The chown happens inside the VM where it takes effect normally.
This means no manual ownership fixups are ever needed on either platform.
Docker Resources¶
| Resource | Name | Description |
|---|---|---|
| Network | oduflow-net |
Shared bridge network for all containers |
| DB container | oduflow-db |
PostgreSQL 15, shared across all environments |
| DB volume | oduflow-db-data |
Persistent database storage |
| Template DB | oduflow_template_{team_id}_{name} |
Created from the dump file, used as PostgreSQL template |
| Environment DB | oduflow_{team_id}_{branch} |
Created from template DB via CREATE DATABASE ... TEMPLATE |
| Odoo containers | oduflow-{branch}-odoo |
One per environment |
| Service containers | oduflow-svc-{name} |
One per auxiliary service |
| Traefik (optional) | oduflow-traefik |
Reverse proxy with auto-HTTPS |
| Traefik volume (optional) | oduflow-traefik-acme |
Let's Encrypt certificate storage |
All containers are labeled with oduflow.managed=true and oduflow.team={team_id} for discovery and management.
Concurrency & Locking¶
Oduflow uses a granular LockManager (locking.py) with per-branch and per-team locks:
| Lock Level | Scope | Example Operations |
|---|---|---|
| Per-branch | One operation per branch at a time | create_environment, delete_environment, install_odoo_modules, pull_and_apply |
| Per-team | One team-level operation at a time | add_extra_repo, setup_repo_auth, create_service |
| System | One system operation at a time | init, destroy |
Operations on different branches run in parallel. If a lock cannot be acquired, the tool immediately returns BusyError (no queuing).
Error Handling¶
Oduflow uses a typed error hierarchy for clear error reporting:
| Error | Description |
|---|---|
FlowError |
Base error for all operations |
BusyError |
Another operation is in progress (lock not available) |
NotFoundError |
Environment, service, or resource not found |
ConflictError |
Resource already exists (e.g. environment already running) |
PrerequisiteNotMetError |
System not initialized, Docker not running, or dependency missing |
ExternalCommandError |
Git, psql, or Docker command failed (includes command, exit code, output) |
ProtectedError |
Environment or extra repo is protected and cannot be deleted |
MCP clients receive errors as ToolError with a descriptive message. REST API clients receive JSON with {"ok": false, "error": "..."}.
PostgreSQL Tuning¶
The bundled postgresql.conf is optimized for a 2 vCPU / 4 GB RAM development server:
- 1 GB shared buffers (25% of RAM)
- 16 MB work_mem per query
- 256 MB maintenance_work_mem for VACUUM and CREATE INDEX
- WAL tuning: 512 MB–2 GB WAL size, 15-minute checkpoint timeout
- Aggressive autovacuum: 30s naptime, 5% scale factor
- Slow query logging: queries over 1 second
- HDD-optimized: random_page_cost=4.0, effective_io_concurrency=2