# Oduflow

> AI-first Odoo development and CI tool powered by reusable database templates. Provisions isolated, ephemeral Odoo environments on Docker — one per git branch — and exposes them to AI coding agents via MCP.

## Installation

### System Requirements

- Docker (Docker Engine or Docker Desktop)
- Python 3.10+
- Git
- fuse-overlayfs (for filestore overlay mounting; not needed on macOS)

### Install

Recommended — install via [uv](https://docs.astral.sh/uv/):

```bash
uv tool install oduflow
```

Alternative — install via pip:

```bash
pip install oduflow
```

### Configure

All settings are configured via `oduflow.toml` (searched in `/etc/oduflow/` then `~/.oduflow/`, or set `ODUFLOW_TOML`).

Minimal configuration:

```toml
[team.1]
hostname = "localhost"
```

Full configuration reference:

```toml
[server]
host = "0.0.0.0"                     # HTTP bind address
port = 8000                           # HTTP port

[routing]
mode = "port"                         # "port" | "traefik" (auto-HTTPS)
# acme_email = "admin@example.com"    # required for traefik mode

[database]
user = "odoo"
password = "odoo"
image = "postgres:15"

[storage]
# data_dir = "/srv/oduflow"           # default: /srv/oduflow or ~/.oduflow/data
overlay_threshold_mb = 50             # filestore size threshold for overlay vs copy

[team.1]
hostname = "localhost"
auth_token = ""                       # MCP bearer token (empty = auth disabled)
ui_password = ""                      # Web UI password (empty = UI open)
port_range = [50000, 50100]           # port range for Odoo containers
```

### Initialize

```bash
oduflow init              # Create shared Docker network, PostgreSQL, team directories
```

### Set up a template

```bash
# From scratch
oduflow init-template --odoo-image odoo:17.0 --template-name default

# From production dump
# Place dump.sql and filestore/ into {data_dir}/team_1/templates/default/ then:
oduflow reload-template default
```

### Start the MCP server

```bash
oduflow
```

The server starts on `http://0.0.0.0:8000`. MCP endpoint: `http://<host>:8000/mcp`.

## MCP Client Configuration

### Cursor / Windsurf

`.cursor/mcp.json` or `.windsurf/mcp.json`:

```json
{
  "mcpServers": {
    "oduflow": {
      "type": "http",
      "url": "https://<your-oduflow-host>/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}
```

### Claude Desktop / Amp

Same JSON format in `claude_desktop_config.json` or `.amp/settings.json`.

## Core MCP Tools

- `create_environment` — provision a new Odoo environment for a branch
- `delete_environment` — tear down an environment
- `start_environment` / `stop_environment` / `restart_environment` — lifecycle control
- `rebuild_environment` — re-create the container from the same image, preserving DB and filestore
- `install_odoo_modules` — install Odoo modules
- `upgrade_odoo_modules` — upgrade Odoo modules
- `run_odoo_tests` — run Odoo tests for specific modules
- `pull_and_apply` — pull latest code and auto-install/upgrade/restart as needed
- `get_environment_logs` — retrieve container logs
- `run_odoo_command` — execute shell commands inside the Odoo container
- `run_odoo_shell` — execute Python code in the Odoo shell with full ORM access
- `read_file_in_odoo` — read a text file or list a directory inside the container (supports line ranges)
- `write_file_in_odoo` — write a text file inside the container (CSV imports, scripts, configs)
- `search_in_odoo` — search for a pattern (fixed-string grep) in files inside the container
- `http_request_to_odoo` — make an HTTP request to the running Odoo instance (controllers, JSON-RPC, REST)
- `list_installed_modules` — list Odoo modules and their states with name/state filtering
- `run_db_query` — execute SQL queries against the environment's PostgreSQL database
- `reset_admin_password` — reset the admin user password (default: "test")
- `read_output` — read from a cached tool output by ID (paginate, grep, errors, tail)
- `list_environments` / `get_environment_info` — inspect environments
- `create_service` / `delete_service` / `update_service` / `list_services` / `get_service_logs` — manage auxiliary services
- `list_service_presets` / `restore_service` / `delete_service_preset` — manage service presets
- `save_as_template` / `delete_template` / `list_templates` — template management
- `import_template_from_odoo` — import a template from a running Odoo instance
- `setup_repo_auth` — cache git credentials for private repositories
- `add_extra_repo` / `list_extra_repos` / `update_extra_repo` / `delete_extra_repo` — manage extra addons repositories
- `get_agent_instructions` — get AI agent instructions for using Oduflow
- `get_odoo_development_guide` — get Odoo development standards guide for a specific version (15–19)

## Typical Agent Workflow

1. Call `list_environments` to check if an environment for the branch exists
2. If not, call `create_environment` with `branch_name`, `template_name`, `repo_url`, and `odoo_image`
3. Write code, `git push`, then call `pull_and_apply` (auto-detects what to do)
4. Use `install_odoo_modules` / `run_odoo_tests` / `get_environment_logs` to verify
5. Call `delete_environment` when the task is done

## Links

- Repository: <https://github.com/oduist/oduflow>
- Documentation: <https://docs.oduflow.dev>
- License: Polyform Noncommercial 1.0.0
- Website: <https://oduflow.dev>

---

# Oduflow

[TOC]

[![Oduflow Dashboard](img/envs.png)](img/envs.png)

An **AI-first** Odoo development and CI tool, powered by **reusable database templates**. Oduflow provisions isolated, ephemeral Odoo environments on Docker — one per git branch — and exposes them to AI coding agents via [MCP](https://modelcontextprotocol.io/), creating a **closed feedback loop** that enables fully autonomous Odoo development.

## Beyond Vibe Coding: Spec-Driven Development

**Vibe coding** — chatting with an AI and eyeballing the output — was the first wave. It works for prototypes, but breaks down on real ERP systems where a module must install cleanly, pass tests, and work against production data.

**Spec-Driven Development (SDD)** is the next step: you write a precise specification of *what* the module should do, and the AI agent autonomously implements *how* — because it has a **closed feedback loop** with the running system:

```
┌─────────────────────────────────────────────────────┐
│                    AI Agent                          │
│          (Cursor, Cline, Amp, Claude, …)             │
└──────┬──────────────────────────────▲────────────────┘
       │ 1. Read spec                 │ 5. Read errors,
       │ 2. Write code                │    fix code,
       │ 3. Install module via MCP    │    retry
       │ 4. Click-test UI via         │
       │    Playwright MCP            │
┌──────▼──────────────────────────────┴────────────────┐
│               Oduflow (MCP Server)                    │
│  • install_odoo_modules → traceback or success        │
│  • run_odoo_tests → test pass/fail with details     │
│  • get_environment_logs → runtime errors              │
│  • upgrade_odoo_modules → upgrade output              │
├──────────────────────────────────────────────────────┤
│            + Playwright MCP / other tools              │
│  • Navigate Odoo UI, click buttons, fill forms        │
│  • Verify business logic end-to-end                   │
│  • Validate acceptance criteria from the spec         │
└──────────────────────────────────────────────────────┘
```

The agent writes code, installs the module, reads the traceback, fixes the error, retries — and when it installs cleanly, it can open the browser via [Playwright MCP](https://github.com/anthropics/mcp-playwright) to click through the UI, verify business flows, and validate acceptance criteria — **all without human intervention**.

| | Vibe Coding | Spec-Driven Development |
|---|---|---|
| **Input** | Conversational prompts | Formal specification with acceptance criteria |
| **Feedback** | Human eyeballs the code | System returns errors, test results, and UI state automatically |
| **Iteration** | Human copy-pastes errors back | Agent retries autonomously via MCP |
| **Scope** | Single files, prototypes | Full modules against real databases |
| **Verification** | "Looks right" | Module installs, tests pass, UI works on production data |

## Key Features

### Core
- **One command to provision** a fully working Odoo instance for any git branch
- **Instant environment creation** from large production databases via PostgreSQL templates and overlayfs
- **Minimal disk footprint** — environments share the template DB and filestore; only per-branch changes consume additional space
- **Template-free mode** — create environments from scratch (`template_name="none"`) when no production dump is available
- **Auto branch creation** — if a branch doesn't exist on the remote, Oduflow clones the default branch and creates the new branch automatically
- **Extra addons repositories** — mount shared addon repos (e.g. Odoo Enterprise) into environments via git worktrees; `addons_path` is auto-merged into `odoo.conf`
- **Environment protection** — protect environments from accidental deletion via a toggle in the dashboard or REST API

### Smart Automation
- **Smart pull** — `pull_and_apply` analyzes changed files (manifest, Python fields, security XML, JS) and automatically decides whether to install, upgrade, restart, or do nothing
- **Auto-install dependencies** — `requirements.txt` (pip) and `apt_packages.txt` (apt) in the repository root are automatically installed when creating an environment
- **Custom odoo.conf** — if the repository contains an `odoo.conf` at its root, it is used instead of the default template
- **Field change detection** — Python files are analyzed for `fields.*` definition changes, triggering module upgrades only when necessary

### Infrastructure
- **Auxiliary services** — managed sidecar containers for Redis, Meilisearch, Elasticsearch, or any other service your Odoo setup needs
- **Traefik auto-HTTPS** — optional reverse proxy with Let's Encrypt certificates for production-like access
- **Stable port registry** — port assignments are persisted in `ports.json` and survive container restarts
- **Resource monitoring** — per-container CPU and RAM stats, plus system-level metrics (memory, load average)

### Integration
- **AI-agent friendly** — the server exposes tools via [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), so LLM-based coding agents (Cursor, Cline, Amp, etc.) can provision and manage Odoo environments programmatically
- **Web dashboard** — a built-in HTML dashboard for managing environments from a browser
- **REST API** — full JSON API for programmatic control from any HTTP client
- **CLI tools** — every MCP tool can be called directly from the command line via `oduflow call`
- **Dual transport** — supports both HTTP (Streamable HTTP) and stdio MCP transports

---

# Quick Start

[TOC]

## 1. Configure

Create `oduflow.toml` (searched in `/etc/oduflow/` then `~/.oduflow/`, or set `ODUFLOW_TOML`):

```toml
[team.1]
hostname = "localhost"
```

## 2. Initialize the system

Create the shared Docker network, PostgreSQL container, and all team directories:

```bash
oduflow init
```

To set up a template database, use `oduflow init-template` (see [Template Management](templates.md)).

## 3. Start the MCP server

```bash
oduflow
```

The server starts on `http://0.0.0.0:8000` by default (configurable via `[server]` section in `oduflow.toml`).

## 4. Connect an MCP client

Point your MCP client (Cursor, Cline, Amp, etc.) to `http://<host>:8000/mcp`.

---

# Installation

[TOC]

## System Requirements

- **Docker** (Docker Engine or Docker Desktop)
- **Python 3.10+**
- **Git**
- **fuse-overlayfs** (for filestore overlay mounting)

!!! note "macOS support"
    On macOS, Docker Desktop runs containers inside a Linux VM and projects
    files via VirtioFS. **fuse-overlayfs is not needed** — filestore overlays
    are skipped and a plain directory is used instead.
    File ownership (`chown`) is handled automatically: Oduflow detects the
    `PermissionError` that VirtioFS raises and falls back to running `chown`
    inside a throwaway container. No extra configuration is required.

### Install fuse-overlayfs

```bash
sudo apt install fuse-overlayfs
```

The `/dev/fuse` device must be available (present by default on Ubuntu).

In `/etc/fuse.conf`, uncomment `user_allow_other` so the Docker daemon (root) can access FUSE mountpoints created by the user:

```bash
sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
```

## Install Oduflow

Recommended — install via [uv](https://docs.astral.sh/uv/) (manages an isolated environment automatically):

```bash
uv tool install oduflow
```

Alternative — install via pip:

```bash
pip install oduflow
```

For local development:

```bash
git clone https://github.com/oduist/oduflow.git
cd oduflow
uv sync          # or: python -m venv .venv && pip install -e .
```

### Upgrade

```bash
uv tool upgrade oduflow
```

## Configuration Reference

All settings are configured via a TOML file. Oduflow searches for `oduflow.toml` in the following order:

1. `ODUFLOW_TOML` environment variable (explicit path)
2. `/etc/oduflow/oduflow.toml`
3. `~/.oduflow/oduflow.toml`

If no config file exists when running `oduflow init`, the bundled default is automatically copied to the appropriate location.

### Minimal configuration

```toml
[team.1]
hostname = "localhost"
```

### Full configuration reference

```toml
# ── Server ────────────────────────────────────────────
[server]
host = "0.0.0.0"           # HTTP server bind address
port = 8000                 # HTTP server port
# trace = false             # verbose tracing for git analysis & env ops

# ── Routing ───────────────────────────────────────────
[routing]
mode = "port"               # "port" (direct host port) | "traefik" (reverse proxy with auto-HTTPS)
# acme_email = "admin@example.com"  # required when mode = "traefik"

# ── Database ──────────────────────────────────────────
[database]
user = "odoo"               # PostgreSQL user for the shared database container
password = "odoo"           # PostgreSQL password
image = "postgres:15"       # PostgreSQL Docker image

# ── Storage ───────────────────────────────────────────
[storage]
# data_dir = "/srv/oduflow"         # base directory for all data (default: /srv/oduflow or ~/.oduflow/data)
overlay_threshold_mb = 50            # template filestore size threshold (MB) — larger uses fuse-overlayfs, smaller uses copy

# ── Teams ─────────────────────────────────────────────
# Each team gets isolated workspaces, templates, credentials, and services.
# At least one [team.*] section is required.

[team.1]
hostname = "localhost"               # port mode: http://{hostname}:{port}, traefik mode: https://{slug}.{hostname}
auth_token = ""                      # MCP bearer token (empty = MCP auth disabled)
ui_password = ""                     # Web UI password (empty = UI auth disabled)
port_range = [50000, 50100]          # port range for Odoo containers [start, end)
```

### Server settings

| Key | Default | Description |
|---|---|---|
| `[server].host` | `0.0.0.0` | HTTP server bind address |
| `[server].port` | `8000` | HTTP server port |
| `[server].trace` | `false` | Enable detailed trace logging for git analysis and environment operations |

### Routing settings

| Key | Default | Description |
|---|---|---|
| `[routing].mode` | `port` | `port` — direct host port mapping; `traefik` — reverse proxy with auto-HTTPS |
| `[routing].acme_email` | *(empty)* | Let's Encrypt email for TLS certificates. Required when `mode = "traefik"` |

### Database settings

| Key | Default | Description |
|---|---|---|
| `[database].user` | `odoo` | PostgreSQL user for the shared database container |
| `[database].password` | `odoo` | PostgreSQL password |
| `[database].image` | `postgres:15` | PostgreSQL Docker image |

### Storage settings

| Key | Default | Description |
|---|---|---|
| `[storage].data_dir` | `/srv/oduflow` or `~/.oduflow/data` | Base directory for all data. Team data directories are `team_{ID}` subdirectories inside |
| `[storage].overlay_threshold_mb` | `50` | Template filestore size threshold (MB). Templates smaller than this use a simple copy per environment; larger templates use fuse-overlayfs. The decision is stored in `metadata.json` at template creation time |

### Per-team settings

Each `[team.*]` section defines an isolated team with its own workspaces, templates, credentials, and services. At least one team is required.

| Key | Default | Description |
|---|---|---|
| `hostname` | `localhost` | Team hostname. In port mode: `http://{hostname}:{port}`. In traefik mode: `https://{slug}.{hostname}` |
| `auth_token` | *(empty)* | Bearer token for MCP HTTP auth. Empty = MCP auth disabled for this team |
| `ui_password` | *(empty)* | Password for Web UI Basic auth (user: `admin`). Separate from MCP auth token. Empty = UI auth disabled |
| `port_range` | `[50000, 50100]` | Port range for Odoo containers `[start, end)` — supports up to 100 concurrent environments |

Team data is stored at `{data_dir}/team_{ID}/`:

```
team_{ID}/
├── workspaces/           # Per-branch environments
├── templates/            # Reusable database snapshots
├── shared_repos/         # Extra addon repositories (bare clones)
├── ports.json            # Port registry
├── .git-credentials      # Git credentials for this team
└── agent_guides/         # AI agent guides (markdown)
```

### Configuration file overrides

When `oduflow init` runs, it copies the bundled `postgresql.conf` and `odoo.conf` to the config directory. These files take **priority** over the bundled defaults — edit them to customize PostgreSQL tuning or Odoo settings globally:

```
/etc/oduflow/             (or ~/.oduflow/conf/)
  oduflow.toml            ← main configuration file
  postgresql.conf         ← custom PostgreSQL tuning (used by oduflow-db)
  odoo.conf               ← custom Odoo defaults (used by new environments)
  license.key             ← license file (optional)
  traefik/                ← Traefik dynamic configuration (auto-generated)
```

If a repository contains an `odoo.conf` at its root, it takes priority over both the bundled and system-level versions for that specific environment.

## Auto-start with systemd

On Linux servers, Oduflow can be registered as a systemd service so it starts automatically on boot.

### Prerequisites

```bash
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install oduflow as a tool (as root)
uv tool install oduflow

# Create the configuration file
# (oduflow init will auto-create a default oduflow.toml if none exists)

# Initialize shared infrastructure and all team directories
oduflow init
```

### Install the service

```bash
oduflow systemd-install
```

This will:

1. Generate a systemd unit file at `/etc/systemd/system/oduflow.service`
2. Run `systemctl daemon-reload`
3. Enable the service for auto-start on boot

### Manage the service

```bash
# Start
systemctl start oduflow

# Status
systemctl status oduflow

# Logs (follow)
journalctl -u oduflow -f

# Restart after config changes
systemctl restart oduflow
```

### Remove the service

```bash
oduflow systemd-uninstall
```

This stops, disables, and removes the unit file.

---

# Use Cases & Workflows

[TOC]

## 🚀 Feature Branch Development

The most common workflow — test your changes against real production data:

```bash
# Create an environment for your feature branch
oduflow call create_environment feature-login default https://github.com/company/odoo-addons.git odoo:17.0

# Make changes, push to remote, then pull into the environment
oduflow call pull_and_apply feature-login
# Oduflow automatically installs/upgrades/restarts as needed

# When done, tear it down
oduflow call delete_environment feature-login
```

## 🐛 Bug Reproduction

Reproduce a production bug with real data:

```bash
# Spin up an environment with production data
oduflow call create_environment bug-12345 default https://github.com/company/odoo-addons.git odoo:17.0

# Debug inside the container
oduflow call run_odoo_command bug-12345 "python3 -c 'import odoo; ...'"

# Check the database directly
oduflow call run_odoo_command bug-12345 "psql -h oduflow-db -U odoo -d oduflow_bug-12345 -c 'SELECT * FROM sale_order WHERE id=42;'"
```

## 🧪 Module Testing

Run Odoo tests in an isolated environment:

```bash
oduflow call create_environment test-suite default https://github.com/company/odoo-addons.git odoo:17.0
oduflow call run_odoo_tests test-suite sale_custom,invoice_custom
oduflow call delete_environment test-suite
```

## 🌱 Greenfield Project (No Production Database)

Start a new Odoo project from scratch:

```bash
# Generate a clean template with common modules
oduflow init-template --odoo-image odoo:17.0 --template-name default --modules base,web,contacts,sale,purchase,stock

# Customize the template interactively
oduflow template-up --odoo-image odoo:17.0 --template-name default
# → Install additional modules, configure settings, create demo users in the browser
oduflow template-down --template-name default

# Now create environments that start with your customized setup
oduflow call create_environment dev default https://github.com/company/new-project.git odoo:17.0
```

## 🔄 Multiple Odoo Versions

Manage environments across different Odoo versions using named templates:

```bash
# Set up templates for different versions
oduflow init-template --odoo-image odoo:15.0 --template-name v15
oduflow init-template --odoo-image odoo:17.0 --template-name v17

# Create environments targeting specific versions
oduflow call create_environment legacy-fix v15 https://github.com/company/v15-addons.git odoo:15.0
oduflow call create_environment new-feature v17 https://github.com/company/v17-addons.git odoo:17.0
```

## 🤖 AI-Assisted Development

Let your AI coding agent manage Odoo environments. Configure your MCP client (Cursor, Cline, Amp) to connect to `http://<host>:8000/mcp`, then:

> *"Create an Odoo 17 environment for the `feature-payment-gateway` branch from our repo. Install the `sale` and `payment` modules, then run the tests."*

The agent will call the appropriate MCP tools in sequence:

1. `create_environment` → provision the environment
2. `install_odoo_modules` → install the requested modules
3. `run_odoo_tests` → run the test suite
4. Report results back

### Connecting Your Agent to Oduflow MCP

Add the Oduflow MCP server to your agent's configuration. The exact format depends on the client:

```json
{
  "mcpServers": {
    "oduflow": {
      "type": "http",
      "url": "https://<your-oduflow-host>/mcp",
      "headers": {
        "Authorization": "Bearer test"
      }
    }
  }
}
```

Replace `<your-oduflow-host>` with your Oduflow server address (e.g. `localhost:8000` or `oduflow.example.com`). The Bearer token must match the `auth_token` configured for your team in `oduflow.toml`.

### Recommended Agent Rule (Cursor / Windsurf / Amp)

You can add the following rule to your AI coding agent to automate environment lifecycle management:

```
---
description: "Manage Odoo dev environments via the Oduflow MCP server"
alwaysApply: true
---
```

**Initialization**

1. **Check**: Call `list_environments`. If an environment matching the current branch already exists, use it.
2. **Create**: If not, use `create_environment`:
   - `branch_name`: `<current branch>`
   - `repo_url`: `<repository URL>` (HTTPS)
   - `odoo_image`: `odoo19_prod` (IMPORTANT: always use this image)
3. **Auth**: On a 401/403 error, suggest `setup_repo_auth`.
4. When creating or finding an existing environment, add the environment URL to `{@artifacts_path}/report.md`.

**Sync & Work Cycle**

1. **Push**: Run `git push` when the task is complete.
2. **Pull**: After every `push` (yours or user-requested), ALWAYS call `pull_and_apply`.
3. **Automation**: The Flow server decides whether a restart or module upgrade is needed. You do NOT need to call `restart_environment` or `upgrade_odoo_modules`.

**Teardown**

- Only delete the environment via `delete_environment` if the task status is **Done** or **Canceled**.
- Do not recreate the environment to fix errors without the user's consent.

**Important**

- One task = one branch = one environment.
- Always display the environment URL to the user when creating an environment.

## 📊 Environment with Auxiliary Services

Set up a full-stack development environment:

```bash
# Create the Odoo environment
oduflow call create_environment dev default https://github.com/company/odoo-addons.git odoo:17.0

# Add Redis for caching
oduflow call create_service redis redis:7 6379

# Add Meilisearch for full-text search
oduflow call create_service meilisearch getmeili/meilisearch:v1.6 7700 "" "MEILI_MASTER_KEY=devkey123"
```

All services share the `oduflow-net` Docker network and can communicate using container names as hostnames (e.g. `oduflow-svc-redis:6379`).

## 🔧 CI/CD Pipeline Integration

Use `oduflow call` in your CI pipeline:

```yaml
# .github/workflows/test.yml
steps:
  - name: Create test environment
    run: oduflow call create_environment ci-${{ github.sha }} default ${{ github.repository }} odoo:17.0

  - name: Install and test
    run: |
      oduflow call install_odoo_modules ci-${{ github.sha }} my_module
      oduflow call run_odoo_tests ci-${{ github.sha }} my_module

  - name: Cleanup
    if: always()
    run: oduflow call delete_environment ci-${{ github.sha }}
```

## 📦 Importing a Template from Odoo or Another Workspace

You can create a template from a running Odoo instance, from a manual database backup, or by copying a template directory from another Oduflow instance.

**Directly from a running Odoo instance (recommended):**

The easiest way — Oduflow downloads the backup, extracts it, auto-detects the Odoo version, and loads the template in one command:

```bash
oduflow import-template https://my-odoo.example.com master_password --template-name default
```

Options:

- `--db-name <db>` — specify the database name (auto-detected if only one DB exists)
- `--template-name <name>` — template profile name (default: `default`)

This is also available as an MCP tool (`import_template_from_odoo`) for AI agents.

**From Odoo Database Manager (manual):**

1. Go to `/web/database/manager` in your Odoo instance
2. Download a backup — **make sure to include the filestore** (the checkbox must be enabled, otherwise the template will be missing all attachments, images, and assets)
3. Extract the archive — it contains a `dump.sql` file and a `filestore/` directory
4. Place them into the template directory:

```bash
mkdir -p {data_dir}/team_{ID}/templates/myproject
# Copy or move the extracted files
cp dump.sql {data_dir}/team_{ID}/templates/myproject/
cp -r filestore {data_dir}/team_{ID}/templates/myproject/
```

5. Load the template into PostgreSQL:

```bash
oduflow reload-template myproject
```

**From another Oduflow workspace:**

Simply copy the entire template directory and reload:

```bash
cp -r /other/oduflow/templates/myproject {data_dir}/team_{ID}/templates/myproject
oduflow reload-template myproject
```

!!! warning
    The SQL dump is loaded into the shared PostgreSQL instance by `reload-template`. Without this step, the template will appear in the list but show **DB NOT LOADED** and cannot be used to create environments.

## 🏗️ Template Evolution

Evolve your template as the project grows:

```bash
# 1. Create an environment for template changes
oduflow call create_environment template-update default https://github.com/company/odoo-addons.git odoo:17.0

# 2. Install new modules
oduflow call install_odoo_modules template-update accounting,hr,project

# 3. Verify everything works
oduflow call run_odoo_tests template-update accounting,hr,project

# 4. Save as the new template
oduflow call save_as_template template-update default

# 5. All future environments will include these modules pre-installed
```

---

# Template Management

[TOC]

[![Templates Dashboard](img/templates.png)](img/templates.png)

Templates are the foundation of Oduflow's instant environment creation. A template consists of a PostgreSQL dump file and an optional filestore directory.

Create templates from production dumps, staging snapshots, or from scratch. Maintain **multiple named templates** side-by-side (e.g. per Odoo version, per client, per project phase) and spin up any combination of branch + database in seconds.

## Starting from Scratch (No Production Dump)

If you don't have a production database dump — for example, you're starting a new Odoo project or just want to try Oduflow — you can generate a clean template automatically.

### Generate a clean template

```bash
oduflow init-template --odoo-image odoo:17.0 --template-name default
```

If a `dump.sql` or filestore already exists, the command will refuse to run. Use `--force` to overwrite:

```bash
oduflow init-template --odoo-image odoo:17.0 --template-name default --force
```

This will:

1. Start a PostgreSQL container (if not already running)
2. Run a temporary Odoo container that initializes a fresh database with the `base` module
3. Dump the database to `{data_dir}/team_{ID}/templates/{name}/dump.pgdump`
4. Extract the filestore to `{data_dir}/team_{ID}/templates/{name}/filestore/`
5. Load the dump into the template database automatically

### Install additional modules during generation

```bash
oduflow init-template --odoo-image odoo:17.0 --template-name default --modules base,web,contacts,sale
```

### Named templates for different projects

```bash
oduflow init-template --odoo-image odoo:17.0 --template-name myproject-v17
oduflow init-template --odoo-image odoo:15.0 --template-name legacy-v15
```

## From a Production Dump

Place your dump file at `{data_dir}/team_{ID}/templates/default/dump.sql` (plain SQL) or `dump.pgdump` (PostgreSQL custom format) and optionally copy the filestore:

```bash
mkdir -p /srv/oduflow/team_1/templates/default/
cp /path/to/production.sql /srv/oduflow/team_1/templates/default/dump.sql
cp -r /path/to/filestore/ /srv/oduflow/team_1/templates/default/filestore/
oduflow reload-template default
```

## Editing the Template Database

Once you have a template, you can modify it interactively — install modules, configure settings, create demo data — and save the result back as the new template.

**Start the template editor:**

```bash
oduflow template-up --odoo-image odoo:17.0 --template-name default
```

This starts an Odoo container that works **directly** with the template database and filestore (no overlays, no copies). Open the printed URL in your browser, log in, and make any changes you need.

**Save and stop:**

```bash
oduflow template-down --template-name default
```

This stops the container, dumps the updated database, and restores the PostgreSQL template flag. The filestore is already updated in place since it was mounted directly.

All environments created after this will be based on the updated template.

## Saving a Branch as Template

When you've made significant changes in a branch environment (installed modules, created configurations), you can save it as the new template:

```bash
oduflow template-from-env my-branch --template-name default
oduflow template-from-env my-branch --template-name myproject  # save to a named template
```

This operation:

1. Dumps the branch database to a new template dump file
2. Reloads the template database from the new dump
3. Snapshots the branch's merged filestore
4. Unmounts all overlay filesystems across active environments
5. Replaces the template filestore with the snapshot
6. Remounts overlays for all active environments (clearing their upper dirs)
7. Restarts all active containers

!!! warning
    **Destructive operation**: All other environments lose their filestore deltas and are reset to the new baseline.

## Reloading a Template

Update the template from a newer production dump without touching the filestore:

```bash
oduflow reload-template default --dump-path /path/to/new.dump
oduflow reload-template myproject --dump-path /path/to/new.dump
```

## Listing and Dropping Templates

```bash
# List all template profiles with their status
oduflow list-templates

# Delete a template profile (removes DB + files from disk)
oduflow delete-template myproject
```

## Template Metadata

Each template profile can contain a `metadata.json` file that stores defaults and configuration:

```json
{
  "odoo_image": "odoo:17.0",
  "repo_url": "https://github.com/company/addons.git",
  "extra_addons": {"enterprise": "17.0"},
  "use_overlay": true,
  "source_url": "https://my-odoo.example.com",
  "source_db": "production",
  "odoo_version": "17.0+e",
  "pg_version": "15.0"
}
```

When `create_environment` is called with a template name, `repo_url`, `odoo_image`, and `extra_addons` are automatically loaded from metadata if not explicitly provided. This means after importing or configuring a template, you can create environments with just `branch_name` and `template_name` — all other parameters are inherited.

The `use_overlay` flag determines whether new environments use fuse-overlayfs (for large filestores) or a simple copy (for small ones). It is set automatically based on `overlay_threshold_mb` (in `[storage]`) when the template is created.

## Template Decision Matrix

| Scenario | Command |
|---|---|
| New project, no existing database | `oduflow init-template --odoo-image odoo:17.0 --template-name default` |
| Regenerate template from scratch | `oduflow init-template --odoo-image odoo:17.0 --template-name default --force` |
| Named template for a specific project | `oduflow init-template --odoo-image odoo:17.0 --template-name myproject` |
| Have a production dump file | Place dump at `{data_dir}/team_{ID}/templates/default/dump.sql` and run `oduflow reload-template default` |
| Need to install modules or configure the template | `oduflow template-up --odoo-image odoo:17.0 --template-name default` / `oduflow template-down --template-name default` |
| Update the template from a newer production dump | `oduflow reload-template default --dump-path /path/to/new.dump` |
| Save a branch environment as template | `oduflow template-from-env my-branch --template-name default` |
| List all templates | `oduflow list-templates` |
| Delete a template | `oduflow delete-template myproject` |

---

# Environment Management

[TOC]

[![Environments Dashboard](img/envs.png)](img/envs.png)

## Creating Environments

```bash
# Create with a named template (template_name, repo_url, odoo_image)
oduflow call create_environment feature-login myproject https://github.com/owner/repo.git odoo:17.0

# Create without a template (fresh Odoo with -i base)
oduflow call create_environment feature-login none https://github.com/owner/repo.git odoo:17.0

# Create with JSON arguments (more explicit)
oduflow call create_environment '{"branch_name":"feature-login","template_name":"myproject","repo_url":"https://github.com/owner/repo.git","odoo_image":"odoo:17.0"}'
```

When creating an environment, Oduflow:

1. **Clones the repository** — shallow clone (`--depth 1`) for speed
2. **Creates the database** — `CREATE DATABASE ... TEMPLATE oduflow_template_{team_id}_{name}` for instant copy, or empty DB when `template=none`
3. **Mounts the filestore overlay** — fuse-overlayfs with the template as lower layer
4. **Detects UID/GID** — runs `id` in the Odoo image to set correct file ownership
5. **Installs dependencies** — auto-installs from `apt_packages.txt` and `requirements.txt` if present in the repo
6. **Configures Odoo** — uses repo's `odoo.conf` if available, otherwise the default template
7. **Starts the container** — with `--dev=xml` for hot-reloading XML/QWeb changes
8. **Initializes base** — when `template=none`, runs `odoo -i base --stop-after-init`

### Private repository authentication

For private repos, configure credentials first:

```bash
oduflow call setup_repo_auth https://user:PAT@github.com/owner/private-repo.git
```

Credentials are stored in the git credential store. Subsequent `create_environment` calls can use the clean URL without credentials.

### Auto-dependency installation

Place these files in your repository root for automatic installation during environment creation:

**`requirements.txt`** — Python packages installed via pip:

```
phonenumbers==8.13.0
python-barcode==0.15.1
xlsxwriter>=3.0
```

**`apt_packages.txt`** — System packages installed via apt:

```
# Dependencies for wkhtmltopdf
libfontconfig1
libxrender1
xfonts-75dpi
```

## Database Sanitization

When an environment is created from a template, Oduflow **automatically sanitizes** the database to prevent the test instance from sending real emails or polling mailboxes. This is enabled by default (`sanitize=True`).

Sanitization uses a **two-tier** approach:

1. **Team-level scripts** from `{team_data_dir}/odoo_sanitize/` — managed by the administrator, shared across all environments in the team
2. **Per-project scripts** from `.odoo_sanitize/` in the repository root — managed by the developer, specific to the project

Both folders support `.sql` and `.py` files, executed in alphabetical order (first all `.sql`, then all `.py`). Team-level scripts run first, then per-project scripts.

### Team-level sanitization

During `oduflow init`, the folder `{team_data_dir}/odoo_sanitize/` is created and seeded with a default script:

**`01_disable_mail.sql`** — disables incoming and outgoing mail servers:

```sql
-- Disable incoming mail servers (fetchmail)
UPDATE fetchmail_server SET active = false WHERE active = true;

-- Disable outgoing mail servers
UPDATE ir_mail_server SET active = false WHERE active = true;
```

The team administrator can add, modify, or remove scripts in this folder to control sanitization for all environments in the team.

!!! tip
    To disable additional cron jobs team-wide, create `{team_data_dir}/odoo_sanitize/02_disable_crons.sql`:
    ```sql
    UPDATE ir_cron SET active = false;
    ```

### Per-project sanitization

You can add project-specific sanitization by placing scripts in a `.odoo_sanitize/` folder in your repository root:

| File type | Execution method |
|-----------|-----------------|
| `*.sql`   | Executed directly against the environment database via `psql` |
| `*.py`    | Executed inside the Odoo container via `python3 -c` |

**Example SQL script** (`.odoo_sanitize/01_clean_partners.sql`):

```sql
UPDATE res_partner SET email = 'test@example.com' WHERE email IS NOT NULL;
```

**Example Python script** (`.odoo_sanitize/02_reset_passwords.py`):

```python
import os, psycopg2
conn = psycopg2.connect(
    host=os.environ["DB_HOST"],
    dbname=os.environ["ODOO_DB"],
    user=os.environ["DB_USER"],
    password=os.environ["DB_PASSWORD"],
)
with conn.cursor() as cr:
    cr.execute("UPDATE res_partner SET email = 'test@example.com' WHERE email IS NOT NULL")
    conn.commit()
conn.close()
```

Python scripts receive the following environment variables: `ODOO_DB`, `DB_HOST`, `DB_USER`, `DB_PASSWORD`.

### Disabling sanitization

Pass `sanitize=false` when creating an environment to skip all sanitization (both team-level and per-project):

```bash
oduflow call create_environment '{"branch_name":"my-branch","template_name":"mytemplate","repo_url":"https://...","odoo_image":"odoo:17.0","sanitize":false}'
```

!!! note
    Sanitization only runs when creating from a template. Environments created without a template (`template=none`) are not sanitized since they start with a clean database.

## Lifecycle Management

```bash
# List all environments with status, URL, image, and repo info
oduflow call list_environments

# Check detailed environment info (DB, URL, repo, image, CPU/RAM stats)
oduflow call get_environment_info feature-login

# Stop an environment (preserves data)
oduflow call stop_environment feature-login

# Start a stopped environment
oduflow call start_environment feature-login

# Restart the Odoo container
oduflow call restart_environment feature-login

# Rebuild the container from scratch (keeps database and filestore)
oduflow call rebuild_environment feature-login

# Tear down everything (container, database, filestore, workspace)
oduflow call delete_environment feature-login
```

### Recreating Environments

The **Recreate** action (available via the Web Dashboard and REST API) deletes an environment and immediately creates a fresh one using the same parameters (repo URL, Odoo image, template, extra addons). This is useful when you want a clean slate without manually re-entering all environment settings.

```bash
# Via REST API
curl -X POST http://localhost:8000/api/environments/feature-login/recreate
```

Recreate reads the original configuration from the container's Docker labels, so all parameters (repo URL, image, template, extra addons, git user) are preserved automatically.

## Viewing Logs

```bash
# Last 100 lines (default)
oduflow call get_environment_logs feature-login

# Last 500 lines
oduflow call get_environment_logs feature-login 500
```

## Installing and Upgrading Modules

```bash
# Install modules (odoo -i)
oduflow call install_odoo_modules feature-login sale,crm,website

# Upgrade modules (odoo -u)
oduflow call upgrade_odoo_modules feature-login sale,crm
```

## Running Tests

```bash
oduflow call run_odoo_tests feature-login sale,crm
```

This runs `odoo --test-enable --stop-after-init -i <modules>` inside the container.

## Smart Pull — Intelligent Change Detection

The `pull_and_apply` tool is one of Oduflow's most powerful features. It pulls the latest changes from the remote repository and **automatically determines the minimal action required**:

```bash
oduflow call pull_and_apply feature-login
```

### How it works

After `git pull --rebase`, Oduflow compares `HEAD` before and after, then classifies every changed file:

| Changed File | Analysis | Action |
|---|---|---|
| `__manifest__.py` (new module) | No previous manifest exists | **Install** the module |
| `__manifest__.py` (version changed) | `version` key differs | **Upgrade** the module |
| `__manifest__.py` (data/assets/demo/qweb changed) | File lists in manifest changed | **Upgrade** the module |
| `*.py` with `fields.*` changes | Field definitions added/removed/modified | **Upgrade** the module |
| `*.py` (no field changes) | Business logic change | **Restart** the container |
| `security/*.xml` | Access control or record rules | **Upgrade** the module |
| `*.xml` (not in security/) | Views, actions, data | **Refresh** (hot-reloaded via `--dev=xml`) |
| `*.js` | Frontend assets | **Refresh** (hot-reloaded via `--dev=xml`) |

### Action priority

`install` > `upgrade` > `restart` > `refresh`

If any module needs installation, all pending upgrades are also executed. If only Python files changed (without field modifications), a container restart is sufficient. If only XML/JS changed, no server-side action is needed — just refresh the browser.

!!! note
    `pull_and_apply` updates only the **main project repository**. Extra addons repositories are pinned to the commit they were deployed with and are not affected. See [Extra Addons — Updating](extra-addons.md#updating-extra-repos) for details.

### Module detection

Oduflow walks up from each changed file to find the nearest `__manifest__.py`, correctly identifying which Odoo module a file belongs to, even in nested directory structures.

## Reading Files Inside Environments

Use `read_file_in_odoo` to inspect files and directories inside the Odoo container without constructing shell commands:

```bash
# Read Odoo source code
oduflow call read_file_in_odoo feature-login /usr/lib/python3/dist-packages/odoo/addons/sale/models/sale_order.py

# Read a specific line range (lines 100–200)
oduflow call read_file_in_odoo feature-login /usr/lib/python3/dist-packages/odoo/addons/sale/models/sale_order.py "100:200"

# List a directory
oduflow call read_file_in_odoo feature-login /mnt/extra-addons/

# Check the generated Odoo config
oduflow call read_file_in_odoo feature-login /etc/odoo/odoo.conf

# Verify file presence after pull_and_apply
oduflow call read_file_in_odoo feature-login /mnt/extra-addons/my_module/__manifest__.py
```

- If the path is a **directory**, returns a listing (like `ls -la`).
- If the path is a **text file**, returns its contents (up to 100KB by default).
- **Binary files** are not supported — use `run_odoo_command` for binary operations.
- The optional `read_range` parameter accepts a `"START:END"` format (e.g. `"1:50"`, `"100:200"`) to read only specific lines.

!!! tip
    Prefer `read_file_in_odoo` over `run_odoo_command` with `cat` or `ls` commands — it handles file type detection, size limits, and binary file rejection automatically.

## Executing Commands Inside Environments

Run arbitrary shell commands inside the Odoo container:

```bash
# List addon files
oduflow call run_odoo_command feature-login "ls /mnt/extra-addons"

# Check Python version
oduflow call run_odoo_command feature-login "python3 --version"

# Run a Python script
oduflow call run_odoo_command feature-login "python3 -c 'import odoo; print(odoo.release.version)'"

# Install a package as root
oduflow call run_odoo_command feature-login "pip3 install phonenumbers" root

# Debug database
oduflow call run_odoo_command feature-login "psql -h oduflow-db -U odoo -d oduflow_feature-login -c 'SELECT count(*) FROM res_partner;'"
```

The `user` parameter defaults to `odoo`. Use `root` for privileged operations (installing packages, modifying system files).

## Interactive Terminal

The Web Dashboard provides an **interactive Odoo Python shell** directly in the browser via WebSocket. It launches `odoo shell` connected to the environment's database, allowing you to inspect and manipulate Odoo models in real time.

The terminal is accessible from the environment card in the Web Dashboard. It supports:

- Full interactive Python REPL with Odoo ORM access (`self.env['res.partner'].search([])`)
- Terminal resizing (adapts to browser window)
- Standard TTY features (colors, line editing)

The WebSocket endpoint is `ws://<host>:<port>/api/environments/{branch}/terminal`.

!!! note
    The terminal requires the environment container to be running. If the container is stopped, the terminal will display an error message.

## Environment Protection

Environments can be **protected** from accidental deletion. A protected environment cannot be deleted until protection is removed.

Protection state is stored as a `.protected` marker file in the environment's workspace directory, so it survives container rebuilds and restarts.

When an environment is protected:

- **Delete** is blocked with a `ProtectedError`
- **Stop** is also blocked with a `ProtectedError`
- Other operations (restart, sync, install/upgrade modules) are unaffected

### Via REST API

```bash
# Protect an environment
curl -X POST http://localhost:8000/api/environments/feature-login/protect

# Unprotect an environment
curl -X POST http://localhost:8000/api/environments/feature-login/unprotect
```

### Via Web Dashboard

Click the **🔓 Protect** button on any environment card to toggle protection. When protected:

- The button shows **🔒 Protected** (highlighted)
- The **Delete** button is disabled
- Attempting to delete via MCP or API returns a `ProtectedError`

---

# Auxiliary Services

[TOC]

[![Services Dashboard](img/services.png)](img/services.png)

Oduflow can manage sidecar containers for auxiliary services your Odoo instance depends on — Redis, Meilisearch, Elasticsearch, RabbitMQ, or any other Docker-based service.

## Creating a Service

```bash
# Redis
oduflow call create_service redis redis:7 6379

# Meilisearch with environment variables
oduflow call create_service meilisearch getmeili/meilisearch:v1.6 7700 "" "MEILI_MASTER_KEY=abc123,MEILI_ENV=production"

# Elasticsearch
oduflow call create_service elasticsearch docker.elastic.co/elasticsearch/elasticsearch:8.11.0 9200 "" "discovery.type=single-node,ES_JAVA_OPTS=-Xms512m -Xmx512m"
```

Services are:

- Attached to the shared `oduflow-net` network (accessible by all Odoo containers)
- Given an `unless-stopped` restart policy
- Automatically routed through Traefik with HTTPS when in traefik mode
- Labeled for management (`oduflow.managed=true`, `oduflow.service=<name>`)

## Managing Services

```bash
# List all services with status, ports, URLs, and env vars
oduflow call list_services

# View service logs
oduflow call get_service_logs redis 200

# Update a service (pull latest image, recreate container with same settings)
oduflow call update_service meilisearch

# Delete a service
oduflow call delete_service redis
```

## Service Update Flow

The `update_service` operation:

1. Captures the current image name, environment variables, port, and hostname
2. Pulls the latest version of the image
3. Compares image digests — if unchanged, reports "already up-to-date"
4. If the image changed: stops the old container, removes it, and creates a new one with identical settings

## Service Presets

Every time a service is created, its configuration (image, port, hostname, environment variables) is automatically saved as a **preset** in `{team_data_dir}/service_presets.json`. This allows you to restore a service after deletion without re-entering its configuration.

```bash
# List saved presets
oduflow call list_service_presets

# Restore a previously deleted service
oduflow call restore_service redis

# Remove a saved preset
oduflow call delete_service_preset redis
```

---

# Extra Addons Repositories

[TOC]

[![Extra Addons Dashboard](img/extra_addons.png)](img/extra_addons.png)

Oduflow supports mounting **extra addon repositories** (e.g. Odoo Enterprise, third-party themes) into environments. Extra repos are cloned once at the instance level and shared across environments via git worktrees.

## Architecture

```
{data_dir}/team_{ID}/
  shared_repos/
    enterprise/          ← bare git clone (shared)
    custom-themes/       ← bare git clone (shared)
  workspaces/
    feature-x/
      repo/              ← main project repo (existing)
      extra/
        enterprise/      ← git worktree (branch 17.0)
        custom-themes/   ← git worktree (branch main)
```

## Setting Up Extra Repos

Clone an extra repository once (it will be available for all environments):

```bash
# Via CLI
oduflow call add_extra_repo enterprise https://github.com/odoo/enterprise.git

# Private repos — configure auth first
oduflow call setup_repo_auth https://user:PAT@github.com/odoo/enterprise.git
oduflow call add_extra_repo enterprise https://github.com/odoo/enterprise.git
```

## Using Extra Addons in Environments

When creating an environment, specify which extra repos to mount:

```bash
# Mount enterprise addons on branch 17.0
oduflow call create_environment feature-x https://github.com/company/addons.git odoo:17.0 default enterprise 17.0

# Mount multiple extra repos
oduflow call create_environment feature-x https://github.com/company/addons.git odoo:17.0 default "enterprise,custom-themes" 17.0
```

Oduflow automatically:

1. Creates a git worktree for each extra repo at the specified branch
2. Mounts the worktree **read-only** into the container as `/mnt/extra-addons-{name}`
3. Generates a merged `odoo.conf` with all extra paths added to `addons_path`

## Managing Extra Repos

```bash
# List all cloned extra repos with available branches
oduflow call list_extra_repos

# Delete an extra repo (fails if any environment references it)
oduflow call delete_extra_repo enterprise
```

Extra repos can also be managed from the **Web Dashboard** under the "Extra Addons" tab.

## Protecting Extra Repos

Extra addon repositories can be **protected** from accidental deletion, similar to [environment protection](environments.md#environment-protection). A protected repo cannot be deleted until protection is removed.

Protection state is stored as a `.protected` marker file in the bare repository directory.

### Via REST API

```bash
# Protect an extra repo
curl -X POST http://localhost:8000/api/extra-repos/enterprise/protect

# Unprotect an extra repo
curl -X POST http://localhost:8000/api/extra-repos/enterprise/unprotect
```

### Via Web Dashboard

Extra repo protection can be toggled from the **Extra Addons** tab in the Web Dashboard. When protected:

- The **Delete** button is disabled
- Attempting to delete via API returns a `ProtectedError`

## Updating Extra Repos

Use `update_extra_repo` to fetch the latest changes from the remote:

```bash
oduflow call update_extra_repo enterprise
```

This runs `git fetch --all --prune` on the **shared bare repository** only. It does **not** affect any running environments.

### Why environments are not updated automatically

Each environment gets a **detached git worktree** pinned to a specific commit at creation time. This is by design:

- **Stability** — the environment keeps working with the exact version of extra addons it was deployed with, regardless of upstream changes.
- **Isolation** — updating one environment's dependencies cannot break another.
- **Predictability** — `pull_and_apply` handles only the main project repository; extra addons remain unchanged.

### How to update extra addons in an environment

Delete and recreate the environment. The new environment will get a fresh worktree pointing to the latest commit on the specified branch:

```bash
oduflow call delete_environment feature-x
oduflow call create_environment feature-x ... "enterprise:17.0"
```

!!! tip
    Run `update_extra_repo` **before** recreating the environment to ensure the bare repo has the latest commits.

---

# Web Dashboard & REST API

[TOC]

## Web Dashboard

[![Web Dashboard — Agent Guides](img/agent_guides.png)](img/agent_guides.png)

When running in HTTP mode, a web dashboard is available at the server root (`http://<host>:<port>/`). It provides:

- **Environment list** with status indicators (running / stopped / partial)
- **Environment actions**: Start / Stop / Restart / Recreate / Protect / Delete
- **Environment creation** form (branch, repo URL, Odoo image, template, extra addons)
- **Environment protection** — toggle to prevent accidental deletion
- **Live log viewer** for each environment
- **Interactive terminal** — WebSocket-based Odoo Python shell directly in the browser
- **Container and system resource stats** (CPU, RAM, load average)
- **Service management** — create, update, delete, and view logs for auxiliary services
- **Extra addons management** — clone, pull, protect, and delete extra addon repositories
- **Git credential management** — list, add, delete, and validate stored git credentials
- **Template listing** — view available template profiles with their status
- **License management** — view current license and activate license keys

## REST API Endpoints

All endpoints return JSON with an `ok` field. Authentication via HTTP Basic auth when `ui_password` is set in `oduflow.toml` (user: `admin`, password: the configured value). This is separate from the MCP Bearer token auth (`auth_token`).

### Environments

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/environments` | List all environments |
| `POST` | `/api/environments/create` | Create a new environment (JSON body: `branch_name`, `repo_url`, `odoo_image`, `template_name`) |
| `POST` | `/api/environments/{branch}/start` | Start an environment |
| `POST` | `/api/environments/{branch}/stop` | Stop an environment |
| `POST` | `/api/environments/{branch}/restart` | Restart an environment |
| `POST` | `/api/environments/{branch}/sync` | Pull latest code and auto-install/upgrade/restart |
| `POST` | `/api/environments/{branch}/recreate` | Recreate an environment (delete + create with the same parameters) |
| `POST` | `/api/environments/{branch}/delete` | Delete an environment |
| `GET` | `/api/environments/{branch}/logs?n=200` | Get environment logs |
| `POST` | `/api/environments/{branch}/protect` | Protect environment from deletion |
| `POST` | `/api/environments/{branch}/unprotect` | Remove protection from environment |
| `WebSocket` | `/api/environments/{branch}/terminal` | Interactive Odoo Python shell via WebSocket (used by the Web Dashboard terminal) |

### Services

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/services` | List all managed services |
| `POST` | `/api/services/create` | Create a service (JSON body: `name`, `image`, `port`, `hostname`, `env_vars`) |
| `POST` | `/api/services/{name}/update` | Update (pull latest image & recreate) |
| `POST` | `/api/services/{name}/delete` | Delete a service |
| `GET` | `/api/services/{name}/logs?n=200` | Get service logs |

### Service Presets

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/service-presets` | List saved service presets |
| `POST` | `/api/service-presets/restore` | Restore a service from a saved preset (JSON body: `name`, `image`, `port`, `hostname`, `env_vars`) |
| `POST` | `/api/service-presets/{name}/delete` | Delete a saved service preset |

### System

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/stats` | Container CPU/RAM stats + system metrics |
| `GET` | `/api/templates` | List available template profiles |

### Extra Addons

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/extra-repos` | List all cloned extra addons repositories |
| `POST` | `/api/extra-repos/add` | Clone an extra addons repo (JSON body: `name`, `repo_url`, `git_user`) |
| `POST` | `/api/extra-repos/{name}/pull` | Fetch latest changes from the remote for an extra repo |
| `POST` | `/api/extra-repos/{name}/protect` | Protect an extra repo from deletion |
| `POST` | `/api/extra-repos/{name}/unprotect` | Remove protection from an extra repo |
| `POST` | `/api/extra-repos/{name}/delete` | Delete a cloned extra addons repository |

### Credentials

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/credentials` | List all stored git credentials |
| `POST` | `/api/credentials/add` | Store git credentials for a repository (JSON body: `repo_url`) |
| `POST` | `/api/credentials/delete` | Delete a stored credential (JSON body: `host`, `username`) |
| `POST` | `/api/credentials/validate` | Validate a stored credential (JSON body: `host`, `username`) |

### Licensing

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/license` | Get current license information |
| `POST` | `/api/license/activate` | Activate a license key (JSON body: `key`) |

### Agent Guides

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/agent-guides` | List all available agent guides |
| `GET` | `/api/agent-guides/{filename}` | Get content of a specific agent guide |

---

# MCP Tools Reference

[TOC]

[![Agent Instructions](img/agent_instructions.png)](img/agent_instructions.png)

All tools are accessible via MCP clients (Cursor, Cline, Amp, etc.) and the CLI (`oduflow call`). A subset is also available via the [REST API](web-api.md).

| Tool | Lock | Description |
|---|:---:|---|
| **Environment Management** | | |
| `create_environment` | ✓ | Provision an Odoo environment for a branch (clone, DB, container, filestore) |
| `delete_environment` | ✓ | Tear down all resources for a branch |
| `list_environments` | | List all managed environments with status and URLs |
| `get_environment_info` | | Full environment details: DB name, URL, repo, image, template, extra addons, workspace, container status, CPU/RAM stats |
| `start_environment` | | Start a stopped environment |
| `stop_environment` | | Stop a running environment |
| `restart_environment` | | Restart the Odoo container |
| `rebuild_environment` | ✓ | Re-create the container from the same image, preserving DB and filestore |
| **Odoo Operations** | | |
| `pull_and_apply` | ✓ | Git pull + smart analysis → auto install/upgrade/restart |
| `install_odoo_modules` | ✓ | Install Odoo modules (`-i`) |
| `upgrade_odoo_modules` | ✓ | Upgrade Odoo modules (`-u`) |
| `run_odoo_tests` | ✓ | Run Odoo tests for specific modules |
| `get_environment_logs` | | Retrieve recent container logs |
| `run_odoo_command` | ✓ | Execute an arbitrary shell command inside the Odoo container |
| `run_odoo_shell` | ✓ | Execute Python code in the Odoo shell context with full ORM access |
| `read_file_in_odoo` | | Read a text file or list a directory inside the Odoo container. Supports line ranges (e.g. `"1:50"`) |
| `write_file_in_odoo` | ✓ | Write a text file inside the container (CSV imports, scripts, configs) |
| `search_in_odoo` | | Search for a pattern (fixed-string grep) in files inside the Odoo container |
| `http_request_to_odoo` | | Make an HTTP request to the running Odoo instance (test controllers, JSON-RPC, REST) |
| `list_installed_modules` | | List Odoo modules and their states with name/state filtering |
| `run_db_query` | ✓ | Execute a SQL query against the environment's PostgreSQL database |
| `reset_admin_password` | ✓ | Reset the admin user password in the Odoo database (default: "test") |
| `read_output` | | Read from a cached tool output by ID (paginate, grep, errors, tail) |
| **Template Management** | | |
| `save_as_template` | ✓ | ⚠️ Save a branch DB + filestore as a new template |
| `list_templates` | | List available template profiles |
| `delete_template` | ✓ | ⚠️ Delete a template profile (DB + files) |
| `import_template_from_odoo` | ✓ | Import a template from a running Odoo instance via database manager API |
| **Auxiliary Services** | | |
| `create_service` | ✓ | Create a managed service container (e.g. Redis, Meilisearch) |
| `delete_service` | ✓ | Stop and remove a service container |
| `update_service` | ✓ | Pull latest image and recreate the service |
| `list_services` | | List all managed service containers |
| `get_service_logs` | | Retrieve service container logs |
| **Service Presets** | | |
| `list_service_presets` | | List saved service presets (configurations that can be restored) |
| `restore_service` | ✓ | Restore a service from a saved preset |
| `delete_service_preset` | ✓ | Remove a saved service preset |
| **Repository Auth** | | |
| `setup_repo_auth` | ✓ | Cache git credentials for a private repository |
| **Extra Addons** | | |
| `add_extra_repo` | ✓ | Clone an extra addons repository (e.g. Odoo Enterprise) for use with environments |
| `list_extra_repos` | | List all cloned extra addons repositories |
| `update_extra_repo` | ✓ | Fetch latest changes from the remote for an extra addons repository |
| `delete_extra_repo` | ✓ | Delete a cloned extra addons repository |
| **Agent Instructions** | | |
| `get_agent_instructions` | | Get AI agent instructions for using Oduflow MCP tools |
| `get_odoo_development_guide` | | Get Odoo development standards guide for a specific version (15–19) |

!!! info "Locking"
    Tools marked with ✓ acquire a per-branch or per-team lock. Operations on different branches run in parallel. If another operation on the **same branch** (or team, for team-level tools) is already in progress, the call is rejected with `BusyError`.

---

# CLI Reference

[TOC]

## Global Options

```bash
# Show version
oduflow --version
```

## Running the Server

```bash
# Start the MCP server (reads oduflow.toml automatically)
oduflow
```

By default, the server starts on `http://0.0.0.0:8000`. Configuration is loaded from `oduflow.toml` (see [Installation](installation.md#configuration-reference)).

## System Commands

```bash
# Initialize shared infrastructure (network, DB, Traefik) and all team directories
oduflow init

# Initialize and install a license in one step
oduflow init --license /path/to/license.key

# Destroy all shared infrastructure (requires no active environments)
oduflow destroy
```

## Template Commands

All template commands accept `--team` to specify the team ID (default: `1`).

```bash
# Generate a clean template from a Docker image
oduflow init-template --odoo-image odoo:17.0 --template-name myproject [--modules base,web,sale] [--force] [--team 1]

# Start interactive template editor
oduflow template-up --odoo-image odoo:17.0 --template-name myproject [--team 1]

# Stop template editor and save changes
oduflow template-down --template-name myproject [--team 1]

# Save a branch environment as the new template
oduflow template-from-env <branch> --template-name myproject [--team 1]

# Reload template DB from a dump file
oduflow reload-template <template_name> [--dump-path /path/to/new.dump] [--team 1]

# List all template profiles
oduflow list-templates [--team 1]

# Delete a template profile
oduflow delete-template <template_name> [--team 1]

# Import a template from a running Odoo instance
oduflow import-template <odoo_url> <master_pwd> --template-name myproject [--db-name <db>] [--team 1]
```

## Service Commands

```bash
# List all managed services
oduflow list-services [--team 1]
```

## Maintenance Commands

```bash
# Show orphaned databases, workspaces, and port entries (dry-run by default)
oduflow cleanup [--team 1]

# Same as above — only show what would be removed
oduflow cleanup --dry-run [--team 1]

# Actually remove orphaned resources
oduflow cleanup --force [--team 1]
```

The `cleanup` command detects and removes resources that no longer have a corresponding running or stopped container:

- **Orphan databases** — PostgreSQL databases with the `oduflow_` prefix that have no matching environment container
- **Orphan workspaces** — workspace directories on disk that have no matching environment container
- **Orphan port entries** — entries in `ports.json` that have no matching environment container

By default, `cleanup` runs in **dry-run mode** and only reports what would be removed. Use `--force` to actually delete the orphaned resources.

## Systemd Service

```bash
# Install and enable systemd service
oduflow systemd-install

# Remove the systemd service
oduflow systemd-uninstall
```

The `systemd-install` command generates a unit file at `/etc/systemd/system/oduflow.service`, runs `daemon-reload`, and enables the service.

See [Auto-start with systemd](installation.md#auto-start-with-systemd) for the full setup guide.

## Tool Introspection

```bash
# List all registered MCP tools with parameters
oduflow list [--verbose]
```

## Direct Tool Invocation

You can invoke any registered MCP tool directly from the terminal using `oduflow call`, without running the server or connecting an MCP client. This is useful for scripting, debugging, and manual operations.

```bash
# List all available tools with their parameters
oduflow call

# Call a tool with positional arguments (mapped to parameters in order)
oduflow call create_environment dev "" https://github.com/owner/repo.git odoo:17.0
oduflow call delete_environment dev
oduflow call list_environments
oduflow call get_environment_logs main 50
oduflow call run_odoo_command dev "ls /mnt/extra-addons"
oduflow call create_service redis redis:7 6379

# Call a tool with JSON-encoded arguments
oduflow call create_environment '{"branch_name":"dev","repo_url":"https://github.com/owner/repo.git","odoo_image":"odoo:17.0","template_name":"myproject"}'

# Type coercion is automatic: int, bool, and float parameters are cast from strings
oduflow call get_environment_logs dev 500
```

---

# Traefik Routing (Auto-HTTPS)

[TOC]

By default Oduflow uses **port mode**: each environment gets a dedicated host port (e.g. `http://server:50001`). This is simple and works well for local or single-developer setups.

For production-like access with HTTPS, Oduflow can deploy a **Traefik** reverse proxy that gives every environment its own subdomain with an automatically issued Let's Encrypt certificate.

## Setup

1. **Configure a wildcard DNS record.** Point `*.dev.example.com` to your server's IP address:

   ```
   *.dev.example.com  →  A  →  203.0.113.10
   ```

   Every environment will get a subdomain: `feature-login.dev.example.com`, `fix-invoice.dev.example.com`, etc.

2. **Set the configuration** in `oduflow.toml`:

   ```toml
   [routing]
   mode = "traefik"
   acme_email = "admin@example.com"

   [team.1]
   hostname = "dev.example.com"
   ```

3. **Run `oduflow init`** (or restart the server). Oduflow will create a Traefik v3 container that:
   - Listens on ports 80 and 443
   - Automatically redirects HTTP to HTTPS
   - Obtains a separate TLS certificate from Let's Encrypt for each environment subdomain via HTTP-01 challenge
   - Routes requests to the correct Odoo container based on the subdomain
   - Also routes the Oduflow server itself via the team `hostname`

## How certificates work

Traefik requests a **per-subdomain certificate** from Let's Encrypt each time a new environment is created. This works out of the box with any DNS provider since it uses HTTP-01 validation (Traefik responds to the ACME challenge on port 80).

Wildcard certificates (`*.dev.example.com`) via DNS-01 validation are also possible but require additional Traefik configuration with a provider-specific plugin.

## Service routing with Traefik

Auxiliary services also get Traefik routing. A service named `meilisearch` with base domain `dev.example.com` becomes accessible at `https://meilisearch.dev.example.com`. Custom hostnames are also supported.

---

# Multi-Team Support

[TOC]

Oduflow supports running **multiple isolated teams** within a single server instance. Each team has its own environments, templates, services, credentials, and port registry, while sharing the Docker network and PostgreSQL container.

## Configuration

Define teams in `oduflow.toml` using `[team.*]` sections:

```toml
[team.1]
hostname = "team-a.example.com"
auth_token = "token-team-a"
ui_password = "pass-a"
port_range = [50000, 50050]

[team.2]
hostname = "team-b.example.com"
auth_token = "token-team-b"
ui_password = "pass-b"
port_range = [50050, 50100]
```

Each team gets a dedicated data directory under the base `data_dir`:

```
/srv/oduflow/
├── team_1/
│   ├── workspaces/
│   ├── templates/
│   ├── shared_repos/
│   ├── ports.json
│   ├── .git-credentials
│   └── agent_guides/
├── team_2/
│   ├── workspaces/
│   ├── templates/
│   ├── shared_repos/
│   ├── ports.json
│   ├── .git-credentials
│   └── agent_guides/
```

## Team Resolution

When an MCP tool is called, Oduflow resolves the team using the following priority:

1. **Auth token** — matches the Bearer token against `auth_token` values in team configs
2. **Host header** — matches the HTTP `Host` header against team `hostname` values
3. **Single team** — if only one team is configured, uses it automatically
4. **Default** — falls back to team `"1"`

## Shared vs. Per-Team Resources

| Resource | Scope |
|---|---|
| Docker network (`oduflow-net`) | Shared |
| PostgreSQL container (`oduflow-db`) | Shared |
| Traefik container (`oduflow-traefik`) | Shared |
| Environments (workspaces, containers) | Per-team |
| Templates (DB snapshots, filestores) | Per-team |
| Extra addon repositories | Per-team |
| Auxiliary services | Per-team |
| Port assignments | Per-team |
| Git credentials | Per-team |

## Database Naming

Databases are namespaced by team ID:

- Environment DB: `oduflow_{team_id}_{slugified_branch}` (e.g. `oduflow_1_feature-login`)
- Template DB: `oduflow_template_{team_id}_{template_name}` (e.g. `oduflow_template_1_default`)

Containers are labeled with `oduflow.team={team_id}` for filtering.

## CLI Team Selection

CLI template and service commands accept a `--team` flag:

```bash
oduflow init-template --odoo-image odoo:17.0 --template-name myproject --team 2
oduflow list-templates --team 2
oduflow cleanup --team 2
```

The default is `--team 1`.

---

# Authentication & Security

[TOC]

## MCP HTTP Auth

When `auth_token` is set for a team in `oduflow.toml`, the MCP endpoint (`/mcp`) requires a Bearer token:

```
Authorization: Bearer <your-token>
```

Each team can have its own auth token:

```toml
[team.1]
auth_token = "secret-token-team-1"

[team.2]
auth_token = "secret-token-team-2"
```

The token is used to both authenticate and identify the team. This is implemented via FastMCP's `StaticTokenVerifier`.

## Web Dashboard Auth

The web dashboard and REST API use HTTP Basic authentication with a **separate** password:

- **Username**: `admin`
- **Password**: value of `ui_password` from `oduflow.toml`

This is independent from the MCP Bearer token (`auth_token`). Credentials are compared using `hmac.compare_digest` to prevent timing attacks.

## When auth is disabled

MCP auth and Web UI auth are configured independently per team:

- If `auth_token` is empty, the MCP endpoint runs without authentication
- If `ui_password` is empty, the web dashboard runs without authentication

Warnings are logged on startup for each team:

```
INFO  [team.1] http://localhost:8000/ (MCP token OFF, UI auth OFF)
```

## Git Credentials

[![Credentials Management](img/credentials.png)](img/credentials.png)

Private repository credentials are stored in the git credential store at `{team_data_dir}/.git-credentials` (per-team) via the `setup_repo_auth` tool. The clean URL (without credentials) is always used in Docker labels and logs — credentials are never exposed.

### Managing credentials via MCP

```bash
# Store credentials for a private repository
oduflow call setup_repo_auth https://user:PAT@github.com/owner/private-repo.git
```

The tool parses the URL, stores the credentials, and verifies access by running `git ls-remote`.

### Managing credentials via REST API and Web Dashboard

The Web Dashboard and REST API provide full credential lifecycle management:

| Action | REST API |
|---|---|
| **List** all stored credentials | `GET /api/credentials` |
| **Add** credentials for a repository | `POST /api/credentials/add` (body: `repo_url`) |
| **Delete** a stored credential | `POST /api/credentials/delete` (body: `host`, `username`) |
| **Validate** a credential against the provider | `POST /api/credentials/validate` (body: `host`, `username`) |

Validation checks the credential against the provider's API (GitHub, GitLab, Bitbucket). For other hosts, it reports `"valid"` if the credential exists. Tokens are always masked in API responses (e.g. `ghp_****`).

## iptables rule

During `oduflow init`, an `iptables ACCEPT` rule is automatically added for the `oduflow-net` Docker bridge interface. This ensures that containers on the shared network can communicate with the host (required for Traefik `host.docker.internal` routing and PostgreSQL access). If `iptables` is not available, the rule is skipped with a warning.

## Odoo security defaults

The bundled `odoo.conf` template includes these security settings:

- `admin_passwd` set to a random value (prevents database manager access)
- `list_db = False` (hides database selector)
- `without_demo = all` (no demo data)
- `max_cron_threads = 0` (disables cron in dev environments)

---

# Running Oduflow in Docker

[TOC]

Oduflow can run as a Docker container. Since it manages other Docker containers (Odoo environments, PostgreSQL, etc.), it uses the **Docker-out-of-Docker** pattern — the host's Docker socket is mounted into the container.

## Build

```bash
docker build -t oduflow .
```

## Run

### Minimal example

```bash
docker run -d \
  --name oduflow \
  -p 8000:8000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v oduflow_data:/srv/oduflow \
  oduflow
```

### Full example with all typical options

```bash
docker run -d \
  --name oduflow \
  --restart unless-stopped \
  -p 8000:8000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v oduflow_data:/srv/oduflow \
  -v /etc/oduflow:/etc/oduflow \
  oduflow
```

## Volume Mounts

| Mount | Purpose |
|---|---|
| `/var/run/docker.sock` | **Required.** Gives Oduflow access to the host Docker daemon to manage Odoo containers, PostgreSQL, Traefik, etc. |
| `/srv/oduflow` | Oduflow data directory. Contains team directories with workspaces, templates, port registry. Use a named volume or a host path to persist data across container restarts. |
| `/etc/oduflow` | System configuration directory. Contains `oduflow.toml`, license key, `postgresql.conf`, default `odoo.conf`, and other configuration files. Mount to persist configuration across container restarts. |

## Networking

The Oduflow container must be on the same Docker network as the containers it creates. The simplest approach is to connect it to `oduflow-net` after initialization:

```bash
# 1. Start Oduflow
docker run -d --name oduflow -p 8000:8000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v oduflow_data:/srv/oduflow \
  -v /etc/oduflow:/etc/oduflow \
  oduflow

# 2. Initialize shared infrastructure (creates oduflow-net, PostgreSQL, etc.)
docker exec oduflow oduflow init

# 3. Connect Oduflow to the shared network
docker network connect oduflow-net oduflow
```

Alternatively, start with `--network oduflow-net` if the network already exists.

## Initialization

On first run, you need to initialize the shared infrastructure:

```bash
# Create shared Docker network, PostgreSQL container, and team directories
docker exec oduflow oduflow init

# Connect to the shared network
docker network connect oduflow-net oduflow
```

To set up a template database:

```bash
# From scratch (clean Odoo with specified modules)
docker exec oduflow oduflow init-template --odoo-image odoo:17.0 --template-name default --modules base,web,contacts

# Or import from a running Odoo instance
docker exec oduflow oduflow import-template https://my-odoo.example.com master_password --template-name default
```

## Configuration

Oduflow reads its configuration from `oduflow.toml`. When running in Docker, mount the config directory:

```bash
-v /etc/oduflow:/etc/oduflow
```

Key configuration settings in `oduflow.toml`:

```toml
[server]
host = "0.0.0.0"
port = 8000

[team.1]
hostname = "localhost"
auth_token = "your-secret-token"   # MCP auth (empty = disabled)
ui_password = "your-ui-password"   # Web UI auth (empty = disabled)
```

See [Installation — Configuration Reference](installation.md#configuration-reference) for all options.

## Docker Compose

```yaml
services:
  oduflow:
    image: oduist/oduflow
    ports:
      - "8000:8000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - oduflow_data:/srv/oduflow
      - oduflow_etc:/etc/oduflow
    restart: unless-stopped
    networks:
      - oduflow-net

volumes:
  oduflow_data:
  oduflow_etc:

networks:
  oduflow-net:
    name: oduflow-net
```

After `docker compose up -d`, run initialization:

```bash
docker compose exec oduflow oduflow init
```

## Security Notes

- Mounting the Docker socket gives the container **full control** over the host Docker daemon. This is equivalent to root access on the host. Only run Oduflow in trusted environments.
- Set `auth_token` in `[team.*]` to protect the MCP endpoint.
- Set `ui_password` in `[team.*]` to protect the Web UI.

## Privileged Mode and fuse-overlayfs

Oduflow uses `fuse-overlayfs` for efficient filestore sharing when templates exceed `overlay_threshold_mb` (default: 50 MB). This requires the `/dev/fuse` device inside the container.

If your templates are small (under the threshold), Oduflow falls back to simple file copy and no special privileges are needed.

For large templates, run with the fuse device:

```bash
docker run -d \
  --name oduflow \
  --device /dev/fuse \
  --cap-add SYS_ADMIN \
  -p 8000:8000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v oduflow_data:/srv/oduflow \
  oduflow
```

Alternatively, set a high threshold in `oduflow.toml` to avoid overlayfs entirely:

```toml
[storage]
overlay_threshold_mb = 999999
```

---

# Internals

[TOC]

## Architecture

```
┌──────────────────────────────────────────────────┐
│                   MCP Clients                    │
│         (Cursor, Cline, Amp, Claude, …)          │
└────────────────────┬─────────────────────────────┘
                     │  MCP (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 /
                        # template_up / template_down / 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`):

1. **Try host-side `os.chown`** — fast, works on Linux.
2. **On `PermissionError`** — fall back to `chown -R` inside 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

---

# Licensing

[TOC]

Oduflow is source-available under the [Polyform Noncommercial License 1.0.0](https://github.com/oduist/oduflow/blob/main/LICENSE). Commercial use requires a license.

## License Types

| Type | Label | Description |
|---|---|---|
| `unlicensed` | UNLICENSED — NON-COMMERCIAL USE ONLY | Default when no license key is installed |
| `individual` | Licensed to individual | Personal commercial license |
| `business` | Licensed to company (internal use only) | Company license for internal use |
| `integrator` | Licensed to Odoo integrator | License for Odoo integrators and consultancies |

## Installing a License

**Via CLI (during init):**

```bash
oduflow init --license /path/to/license.key
```

**Via CLI (standalone):**

The license file is stored at `/etc/oduflow/license.key`.

**Via Web Dashboard:**

Navigate to the dashboard and use the license activation form. The license key text can be pasted directly.

**Via REST API:**

```bash
curl -X POST http://localhost:8000/api/license/activate \
  -H "Content-Type: application/json" \
  -d '{"key": "<license-key-text>"}'
```

## Checking License Status

```bash
# Via REST API
curl http://localhost:8000/api/license

# Via Web Dashboard — license info is displayed in the dashboard header
```

License keys are RSA-signed and verified against a built-in public key. Invalid or tampered keys are rejected.

---

For business use or integrator licenses, visit [oduflow.dev](https://oduflow.dev).

---

