Changelog¶
v1.89.1¶
Bug Fixes¶
- Large database imports no longer fill the host's
/tmp— importing a database used to build a full-size tar archive on the host before uploading it to Docker, which could exhaust a small/tmp. The archive is now streamed through a pipe. Where possible, restore inputs are exposed to the container as readable hardlinks inpg_exchange, so nothing is copied at all. Older containers, inputs on another filesystem, and unreadable links fall back to streaming the copy. Both paths remove their staged inputs on exit. (#346) - Abandoned staging files are reclaimed at startup — leftover import
staging files, including incomplete production dumps ending in
.pgdump.part, are cleaned up when the server starts. The cleanup runs before XFS quota setup refreshes inodectime, so old files are still recognised as abandoned. (#346)
Dashboard¶
- Environment actions grouped in a two-level menu — the environment's long, flat More menu now has a root with Info, five groups (Consoles, Modules, Access, Management, Data) and a separate Delete. Each submenu has a Back control, and Recreate is set apart and marked destructive. Confirmations, shared-view restrictions and the stopped/protected/busy availability rules are unchanged. The panel fits the viewport, opens above its trigger when there is no room below, and scrolls in short windows. It supports arrow keys, Home/End, Escape, Tab and focus restoration. Service and production menus are unchanged. (#344)
v1.89.0¶
Security¶
- Environments are isolated inside the team's coding-agent container —
every environment's Agent CLI/Chat sessions, checkout, attachments and HOME
now belong to that environment's own Linux user (
oduflow-coder:0.4.0). Previously all sessions shared one user, so an agent in one environment could read another environment's scoped MCP token from its process environment and see or change other environments' checkouts and conversations. Existing checkouts, attachments and Claude/Codex conversations are handed over to their environment on the first start; OpenCode conversations from before the upgrade are not carried over. Interactive logins (Claude/login,codex login,opencode auth login) are now per environment; credentials in[team.X.agent_env]keep working for every environment. Agents read other branches through git instead of neighbouring checkouts. An[agent].imageexplicitly pinned to an older coder image is refused; use 0.4.0 or later. See decision record 0079. (#327) - Cluster WAL control and PITR are operator-only —
restore_cluster_pitrandcontrol_production_walacted on every team's productions while being reachable with any single team's credential, gated only by a typed confirmation string — protection against accidents, not against the wrong principal. Both are removed from the MCP surface and from the team dashboard; read-only cluster WAL status stays on both team surfaces. The Server settings console gains a Cluster DR section: WAL monitor state, the WAL-G base-backup inventory, pause/resume/retry/recover/release, and whole-cluster PITR, each behind its own typed confirmation. The owner CLI gainsoduflow wal <action>andoduflow restore-cluster, keeping the headless path that resurrects the cluster on a fresh install from S3 alone. See decision record 0082. (#341) - Odoo's database manager is locked behind a real master password — the
generated production
odoo.confnever setadmin_passwd, so Odoo's compiled-in default — the literaladmin— applied; onlylist_db = Falsekept that from mattering, andlist_dbwas a freely settable override with a silent trap (0,no,offand evenFALSEstay truthy strings and enable database management).list_dbandadmin_passwdare now validated as a pair, on the resulting override set rather than on one call's keys, so the rule holds in whichever order they arrive; non-booleanlist_dbvalues are refused; andadmin_passwdmust be at least 12 characters and notadmin._build_prod_odoo_conffails closed, so a hand-editedproductions.jsoncannot serve the manager unlocked.admin_passwdandsmtp_passwordcome back masked fromget_production_infoandset_production_odoo_conf, and the generated conf is written0600on the host. (#328) - Tenant and operator boundaries hardened —
POST /revokecompared the stored raw team id with the token's public client id, so OAuth access-token revocation was a silent no-op. Dev-surface tools (get_environment_logs,get_environment_info,list_installed_modules, theodoo_*RPC family, the image-build tools) acceptedenv_name="prod-<name>"and acted on the byte-identical production container — including waking a deliberately stopped production outside the prod lock; a shared guard now rejects production names at every dev entry point and points at the*_productiontools. Dev lock keys are canonicalised throughslugify_branchat the LockManager boundary, sofeature/xandfeature-xcan no longer map to the same resources under different locks and run two concurrentodoo -upasses on one database. (#341)
Features¶
- Cron-owned SSH template sync — manage source/schedule/logs from Templates; manual requests are queued for host cron. Optional environment data refresh prepares and sanitizes new data before switching, retains old filestore generations for environments that stay behind, and rolls back caught switch failures. CLI/UI/MCP mutations share process-safe file locks. (#339)
- Template Refresh in the dashboard — reload a template from its own dump files and report overlay remount errors. (#339)
- S3 templates: remembered credentials, bucket browser and backup archives
— a private bucket is re-read on every
--overwritere-sync, so re-supplying the key made a nightly "pull last night's backup into dev" job impossible to automate without an S3 secret in a crontab. The secret key now lives in the team secret store and the template records only asecret:<name>reference; a remembered secret is reused only while the endpoint and access key are unchanged, so it can never sign a different target. Templates → Import from S3 walks the bucket one level at a time, newest first, and marks the level that holds a dump the engine would actually pick up — the one import step nothing used to verify before it ran; the route is read-only, is refused to share-link sessions and passes the same SSRF gate as an import. A key ending in.zip(Odoo's own backup, an Odoo.sh download) is accepted as a fourth source shape with its costs stated: fetched whole on every change, filestore replaced wholesale, no resume. A template imported from a bucket is tagged S3, shows its source and gains a Sync button. (#319) - Environment identity survives recreation — per-environment MCP tokens
move from the frozen container label into a per-team
mcp_tokens.jsonregistry keyed by environment name, so recreating a container no longer rotates the credential already configured in MCP clients; a startup migration adopts existing label tokens, and an unknown token is still matched against the legacy label as a throttled last resort. The newrecreate_environmenttool and dashboard action tear an environment down and reprovision it from its recorded configuration while preserving its identity — share link and scoped MCP token. A recreate whose create fails after the preserved delete revokes the orphaned credentials, so a future unrelated environment of the same name is never reachable with the old ones. (#335) - Extra repo branches are filled in from the Odoo image — the Create
Environment dialog and a template's Settings fill each extra repo's
branch from the Odoo image (
odoo:19.0→19.0) wherever the repo carries that branch, so per-version repos such as the OCA sets or enterprise no longer have to be typed out. An online-release image (odoo:saas~19.1) only gets its ownsaas-19.1. Branches from a template, a production or your own typing are never replaced, a field you cleared stays empty, and each selected repo with a filled-in branch is marked guessed and counted under the list. The image field now also follows a template switch when a template filled it, so the new template's database no longer runs on the previous template's Odoo. (#337) - Explicit hostnames on bare team domains —
create_environment(hostname="qa")on a team whose hostname is a bare domain (example.com) now nests under it (qa.example.com) instead of failing. The bundled Public Suffix List decides what counts as bare, soqaon teamexample.co.ukis never routed toqa.co.uk— a domain the team does not own — and a malformed team hostname fails fast instead of landing in a TraefikHost()rule. The greenfield-i baseinit now runs in an isolated container reading the same generatedodoo.confas the serving container, and a conf-copy, start or non-zero init failure rolls the partial environment back instead of orphaning the database, role, port and workspace and serving an uninitialized database. A new--log-levelCLI flag quiets progress output; uvicorn follows it. (#330) - Codex with a ChatGPT subscription in Agent Chat — Agent Chat no longer
injects
agent_browserinto Codex ACP sessions: the coder image's own Codex config already defines it (forwardingAGENT_BROWSER_SESSION, so each environment keeps its own browser profile), and the duplicate showed a redmcp__agent_browser__startupfailure at the start of every new chat. Codex starts withcheck_for_update_on_startup=false, since an in-container npm update would not survive container recreation. When Codex is not signed in, theAuthentication requiredanswer now carries how to recover — Agent CLI → Sign in with Device Code, orOPENAI_API_KEYin[team.X.agent_env], and it says so when a key is already configured. Codex and its ACP adapter are pinned to one version, so Agent CLI and Agent Chat run one Codex over one~/.codex. Coder imageoduflow-coder:0.3.5. (#320) - Managed
odumcp: client takeover and explicit refresh — Oduflow shipsodumcpas a managed component; a client takes it over by committing the module into their own repository and deploying, after which Oduflow does not touch it. That policy only half-held: once the client repository shippedodumcp, the managed checkout stayed on disk and stayed mounted, soaddons_pathcarried two copies and the repository one won only through ordering. The mount is now decided by repository state, so any recreate drops it the moment the module is client code; a successful deploy that brings the module in completes the takeover itself, recreating the container and retiring the checkout, recorded asodumcp_takeoverin the deploy entry. The newsync_production_mcp(name, refresh=True)re-clones the connector branch and forces the upgrade even when the Odoo major is unchanged — it requires an explicit name, so one call can never re-clone and recreate every production of the team. Acreate_productionwhose OduMCP provisioning failed a prerequisite now quotes the cause instead of a generic warning. (#334)
Bug Fixes¶
- Branch switches restore dependencies after container recreation —
switch_branchreinstalls declared APT/PIP dependencies from the target code and all mounted extra-addons repositories, even with unchanged requirements or an empty diff. Same-branch renames restore dependencies too. Strict guardrail refusals still restore the currently mounted code's dependencies; installation failures are reported and stop subsequent module actions. Failures retain the pending module plan and an explicit two-step retry plan (restore dependencies, then replay the module actions) after the Git diff has been consumed. Completed renames keep the agent checkout and MCP configuration synchronized and report the resulting identity even when dependency setup fails. A completed switch whose module apply failed is reported honestly as a partial result instead of success prose. The dashboard shows full failure output. Same-branch renames combine extra-addons mount changes in one recreation; checking repositories without dependency files adds no restart. (#314) - Environment creation on development-only hosts with limited disk space — the disk check now reserves max(1 GiB, min(1% of the disk, 2 GiB)) on devices that hold no production data, derived from the disk size rather than from current free space so repeated creations face a stable threshold. A copy estimated to write 5.2 GiB can therefore proceed with 8.6 GiB free. Devices shared with the production database volume retain the full 5–10 GiB reserve (all devices, when that volume's device cannot be determined); a host with production on a separate disk keeps the relaxed policy on its dev disk. The shared PostgreSQL data volume additionally budgets 1 GiB of WAL/catalog writes per creation; write estimates still include the 20% safety margin. (#340)
- Failed production deploys explain themselves — a failed
update_productionused to record onlyDeploy failed (exit_code=255). The Odoo output that held the cause reached only the response, which a client that timed out on a long upgrade never received. The deploy record inproduction_deploysnow names the step that failed (dependencies,install,upgrade,healthorexception), keeps the reinstall's own exit code, and stores the Odoo error record with its traceback (failure_excerpt) and the last 200 lines of the step's output (output_tail, returned withinclude_output=true). For a failed health check, that is the server log. The deploy's error line ends with the cause, for exampleCause: psycopg2.errors.SerializationFailure: …, and the server log gets the excerpt. The production docs now describe the environment the upgrade runs in and what the exit codes mean. (#331) - Oversized command output keeps its end — an output over the 10 MB output cache limit kept only its start, which can drop the traceback that Odoo prints last. The cache now keeps the first 1 MB and the end. (#331)
- Production health waits stop on a crash loop — a container that Docker
restarts twice while Oduflow waits for health now fails the wait at once.
Before,
reconfigure_productionand deploy verification kept the production's lock for the full 180 s, so a broken setting could not be reverted during that time. (#331) - Auto deploys skip uninstalled modules — an extra-addons repo bundles
many modules of which only a few are installed, and a fast-forward that
touched the others failed the whole
update_production(orpull_environment) with "not installed and cannot be upgraded". Auto mode now drops the changes of uninstalled modules with a warning and classifies the rest again, so a JS or view change in an installed module still reloads and a changed.py, including one outside any module, still restarts. A module the database already has is upgraded even when it looks new (moved between directories or repos), and a production never auto-installs new modules. Explicitinstall=/upgrade=requests keep the strict error. (#332) - Configurable production health wait — the new
[production] health_timeout(default 180 s) sets how long every production health wait lasts: deploy verification, the rollback restart (was 120 s), reconfigure, restore and Stack apply. Workers-mode productions with many modules can take minutes to start serving. (#332) - Starting a production clears a stale unhealthy flag —
start_productionandrestart_productionon a production flagged unhealthy now wait for its health check and clear the flag once Odoo answers. When it still does not, the MCP reply and the dashboard say so instead of reporting a plain success. (#332) - Production environment variables that break the container are refused
—
create_productionand a changedreconfigure_production(env_vars=…)refuseHOME,PYTHONHOME,PYTHONUSERBASE,PYTHONNOUSERSITEand aPYTHONPATHthat lists asite-packagesdirectory. These variables hid the pip packages installed for the repositories (ModuleNotFoundError) or let adataclassesbackport shadow the standard library, which took the server down. Existing productions that store such a variable still recreate. (#331) - Backup and PITR failure paths — six fixes. Scheduled base backups and
retention persisted their attempt counter before acquiring their locks, so
three contended ticks (a PITR holding the cluster lock, say) silently
skipped the day's base backup or the week's prune; the lock is taken first
now, and pure contention leaves the slot due on the next tick.
_describe_keyalso no longer crashes on the cluster-wide key, which made every contended scheduled base backup raise instead of reportingBusyError. An unreadablepg_dumpexit code is a failure, rather than a possibly-truncated archive recorded as a healthy snapshot. Cluster PITR aborts before touching PGDATA when PostgreSQL cannot be stopped, leaving the data directory untouched. One corrupt revision no longer wedges every future prune for the whole team store. A filestore file deleted between the scan and the read is skipped with a warning instead of aborting the snapshot, and a file that grew is recorded at the bytes actually chunked, so its restore can pass the size check. The store config is created first-writer-wins, so two concurrent first-ever backups on a fresh store can no longer leave the loser's revision permanently unrestorable. (#333) - Reconfigure treats a stale branch as drift to repair — a
reconfigure_production(branch=…)that updated the registry and then died mid-way left the production deploying the old branch forever behind a "No settings changed" no-op. The branch the production actually runs — its container label and repo checkout — is now compared with the record and repaired, and a repair that moved the checkout is recorded in the deploy history with rollback targets, exactly like an explicit branch change. When only the container label drifted, only the container is recreated — no fetch and checkout that would silently deploy the origin tip — and a repo directory that is not a Git working tree is repaired via the staged re-clone and swap path instead of crashing with a raw git error. (#336) - Agent image pulls are logged — the first server start after an upgrade
that bumps the coder tag pulls the new image (~1 GB compressed)
synchronously in
init_system, before the HTTP listener binds. The pull was silent, so startup looked hung and the 900 s startup watchdog could kill the server mid-pull on a slow link. The pull now streams progress, logged at most every 15 s with milestones and the digest immediately, and an error the daemon reports inside the stream raises._ensure_agent_containerlogs why it pulls (container missing, or config changed) and the duration on success and on failure, so a config-only change — token, SSH key, brand — no longer looks like an image download. (#326) - Share links open from email — a share link opened from another site, such
as a button in a webmail message, landed on "This link is missing its key":
the browser kept the other site as initiator across the redirect and withheld
the SameSite=Strict share cookie. The link now answers with a small landing
page that navigates to
/env/<name>itself, so the cookie stays Strict and the key still leaves the address bar. (#318) - Production setup is clear from the start — when the team's
production_tokenis not set, the Production tab and the Create Production dialog now say so up front, and the dialog cannot be submitted, instead of failing after the whole form was filled. While a production is being created, the dialog shows the elapsed time and disables its submit button, so a multi-minute image pull and database initialization can no longer be submitted twice. Errors appear inside the dialog. (#323) - OduMCP for Odoo 16–20 — automatic OduMCP provisioning was hard-wired to
Odoo 19. A production on another version, for example
odoo:20.0, was created with "OduMCP installation or key setup failed" and had no Odoo MCP tools. The connector branch now follows each production's Odoo version: the image tag, orodoo --versionfor an unversioned custom image. The[production].odumcp_refsetting is retired; a leftover value is ignored with a warning.reconfigure_productionswitches the checkout when the image moves to another Odoo version, andsync_production_mcpreplaces a checkout made for another version and recreates the container whenever it does not see the current checkout. This repairs existing productions, including after a failed recreation. Provisioning refuses an addon built for another Odoo version. Odoo 15 and older get a clear explanation and an MCP N/A badge instead of a retry hint that cannot help. (#323) - OduMCP setup failures are visible — a production whose OduMCP addon installation or key setup failed now shows an MCP FAILED badge on its card. The creation warning is also shown in the dashboard, not only in the server log. (#323)
- SSH repository URLs for productions — the Create Production dialog and the
create_productiontool description now say that SSH URLs (git@github.com:owner/repo.git) work with the team deploy key, which private repositories need. (#323) - Quiet default sanitize script — the seeded team script
odoo_sanitize/01_disable_mail.sqlfailed withrelation "fetchmail_server" does not existon every environment created from a template without themailmodule. Because the file runs as one transaction, the failure also rolled back itsir_mail_serverupdate. The script is now a commented-out example: on Odoo 16+, the native neutralization already deactivates mail servers. Runoduflow upgradeto update an existing, unmodified copy. (#323) - Production database left by a failed create — when a failed
create_productioncould not drop the database it had created, the rollback ignored the error. The retry then failed with "already exists in the production cluster" and suggesteddelete_production, which cannot find a production with no registry record. The rollback now tombstones such an attempt, sooduflow cleanup --purge-deleted-productionsreclaims it. The error now names the remedy that applies: purge for tombstoned leftovers, a manualDROP DATABASEfor an unmarked database. An existing production gets a plain "already exists" error. (#325) - Re-creating a deleted production over its kept workspace — after
delete_productionwithoutdrop_databaseand a manual drop of the database, re-creating the name failed atgit cloneon the old repository, and the rollback removed the workspace's purge marker.create_productionnow refuses up front while the workspace is still on disk, and names the purge that frees the name. (#325) - Database-only import from Odoo.sh — the Odoo.sh import always uploaded
the whole filestore, which can run to hundreds of GB from a limited shell
session, and stopped with exit code 3 when the backup had no filestore
directory. The Import from Odoo.sh dialog now has a Database only
checkbox, like the HTTP, S3 and local-path imports; it adds
--without-filestoreto the command, which uploads the manifest, the SQL dump and any selected addons but no filestore. An existing template keeps its current filestore, and chunks left by an interrupted full upload are not promoted as a partial filestore. The client's summary says whether the filestore was imported, kept or not included. Without the flag, a missing filestore still stops the import, now with a hint to re-run with--without-filestore. (#329)
Documentation¶
- WAL panel fields —
docs/production.mdexplains why unarchived WAL fills the production disk and documents the PostgreSQL WAL panel field by field: badge values (including a fetch failure and a cluster that is not provisioned), the queue and its limits, normalpg_walsize (min_wal_size…max_wal_size), free space on the WAL filesystem, disk consumption and time to reserve, thepg_stat_archivercounters, the Last upload failed line, the storage probe, and how the buttons map to actions. In the dashboard, Archived / Failed attempts / PG restarts now show0instead of blank, a missing value renders as—instead ofundefined, and Last archive is shown in the browser's local time like Sample. (#324) - Integration tests are opt-in —
pytestnow collects only the unit suite by default;-m integrationand-m heavyweightselect the suites that create and destroy real Docker resources, so a plain test run no longer touches the contributor's own live Oduflow server. (#321, #322)
v1.88.0¶
Features¶
- Import OCA repositories in bulk — the Extra Addons tab has a new
Import OCA dialog. It lists the public OCA catalogue with search, sorting
by stars or name and an archived filter, reads each selected repository's
branches, and adds the selection as
oca-<name>extra repos. As with Add Repo, nothing is downloaded by default: the Common versions you enter (for example18.0, 19.0) are downloaded right away, and a version a repository lacks must be unchecked before importing. The import runs on the server, up to three repositories at a time, so closing the dialog, reloading the page or a proxy timeout neither stops it nor loses its progress. Failures are reported per repository, and Retry failed resends only those. Repositories the team already has are recognised by origin in any URL form, and colliding import names get a numeric suffix. The catalogue is cached for 15 minutes and falls back to the last good copy when GitHub is unavailable; no token is needed. (#317) - GitHub CLI in the coder image — the coding-agent image ships a current
ghfrom GitHub's apt repository, so agents can work with pull requests and issues out of the box. Coder imageoduflow-coder:0.3.4. (#227)
Bug Fixes¶
- Odoo 20 environments are reachable — Odoo 20 listens on
127.0.0.1whenhttp_interfaceis unset, which neither Traefik nor a published port can reach. Every generatedodoo.conf(dev and production) now pinshttp_interface = 0.0.0.0and drops the legacyxmlrpc_interfacealias that would override it. A startup migration rewrites the conf in existing containers, which pick it up on their next restart, start orpull_and_apply.set_production_odoo_confand stack manifests refuse the listen settings instead of storing a value that is silently replaced. (#315) - Changed CSV data files upgrade their module — a commit that only touched
a module's CSV data (for example
security/ir.model.access.csv) was classified as a plain refresh, so the environment silently kept stale access rules. Odoo loads CSV data only on install or upgrade, so such a change now upgrades the owning module; a CSV outside any module still only refreshes. (#316) without_demois honoured on Odoo 19+ — Odoo 19 readswith_demoinstead. Generated confs translatewithout_demointo the matchingwith_demovalue the way Odoo 19 itself does (only an explicit false enables demo data; an explicitwith_demowins), and template initialisation passes--without-demo=Trueon 19+. (#227)- White label: two remaining gaps — the agent guides viewer rebrands the
file-name title it shows for a guide without a heading, and
get_agent_instructionsdocumentssubmit_agent_feedbackonly when the tool was published at startup, so a license that changes while the server runs no longer points agents at a missing tool. (#311)
Security¶
- The repository can no longer tune the production server — the repo's
.oduflow/odoo.prod.confis no longer part of the production conf chain (teamodoo.prod.conf> bundled), because anyone who can push to the deployed branch must not change server settings. A repo file is logged as ignored and no longer triggers a restart on deploy. (#315)
v1.87.0¶
Features¶
- White-label branding with a custom license — operators can install a
signed custom license that carries a brand name and the domains it is valid
for. While it is in effect (valid, at most 30 days past expiry, and every
team hostname inside its domains), the dashboard, sign-in page, server
settings console and MCP server show the brand instead of Oduflow. Vendor
surfaces are removed, and the logo and icon come from
<config>/branding/. The version stays visible as plain text. Otherwise the product looks like stock Oduflow, and nothing is ever disabled. New CLI:oduflow license status|install|refresh|subscribe. During the grace period a paid renewal is installed automatically once a day. Technical names (oduflow.toml, containers, databases, URL paths, cookies) keep sayingoduflowby design. Seedocs/licensing.mdand decision record 0078. (#295) - No product name left on white-label installs — the remaining texts are
rebranded or made neutral. This covers agent instructions (the feedback
section is omitted), "System not initialized" errors, image build log
markers (
[build]), the OpenCode provider warning, the agent guides viewer, dashboard error responses, the served/import-odoo.sh, the settings console (it says "Restart server" and hides the Docs link) and outbound User-Agents./api/versionand/api/feedback/linkanswer 404 under white label. (#310) - Coding agent's MCP server renamed to
development— the agent's scoped MCP server now has the same name on every install, and replaces the legacyoduflowentry in existing checkouts. Under white label, the agent's commit identity follows the brand. Coder imageoduflow-coder:0.3.3. (#295) - Deploy progress and queued deploys are visible — long operations now
report their current step (deploy phases, snapshot file progress) under
their lock.
BusyErrorquotes that step and names the production instead of a raw lock key. The dashboard shows the step next to the status, andlist_productions/get_production_inforeturn it. A deploy waiting for the production's lock is shown as queued, with the waiting time and the current holder. The Update toast says whether the deploy started or was queued, and a second click while one is queued is refused. (#312, #313)
Bug Fixes¶
- Deploys snapshot only when there is something to apply —
update_productionused to take a full DB + filestore snapshot before pulling, even when no new commits had arrived. The production stayed locked the whole time. The snapshot is now taken only after the code is synced and the classifier found work. Before the snapshot, the checkouts are rewound to the pre-deploy commits, so the snapshot matches the schema its DB ran against. Markdown-only pushes no longer cost a snapshot. (#312) - Faster first filestore backups — the chunkstore chunker re-hashed its whole buffer on every write. On a first backup this cost about 3 s per file, whatever its size. The rolling hash now resumes where it stopped, so every byte is hashed once. Chunk boundaries are byte-identical, so existing stores keep deduplicating. (#312)
- Deploys that give up are recorded — a queued dashboard deploy (waits up
to 5 min) or webhook deploy (up to 30 min) that times out on the lock used
to vanish with only a log line. It now writes a
skippedentry with the reason to the deploy history. That entry is never a rollback target. (#313)
v1.86.0¶
Features¶
- Server settings console — operators can now edit the whole
oduflow.tomlin the browser at/admin, without SSH. Every key is labelled with how a change takes effect: live (applies at once), restart, recreate (restart and recreate existing containers) or locked (read-only). Each edit is checked with the same parser the server boots with, so a typo is caught before a restart instead of after it. Comments and layout are kept, and every write keeps a diff in history with one-click revert. The console has its own password, session and TOTP, separate from team logins, and secrets stay masked unless you reveal them (reveals are logged). Restart is a graceful in-place re-exec and is refused while any lock is held. New CLI:oduflow admin enable|disableandoduflow ui-2fa setup --admin. Seedocs/admin.mdand decision record 0077. (#303) - PostgreSQL major upgrades on operator request — this replaces the
one-time PG15→PG16 replacement from v1.85.0. A cluster stays on its major
(15–17) until
[database].imageis raised explicitly. A new default never upgrades anything, and downgrades are refused. Production databases (including soft-deleted ones) and service databases are dumped with every role and password hash, then restored into the new cluster. Templates are restored from their on-disk dumps. Development environments must be deleted first. The upgrade can be cancelled until an old cluster is removed. Odoo 20 on a PG15 cluster is refused up front. Every server now holds a shared lock on its data directory, and the upgrade runs only while it can take that lock exclusively. This stops one stdio/HTTP server from upgrading clusters out from under another. See decision record 0076. (#301, #302) - Production server mode with a working live bus — productions sent every
request to the prefork workers on 8069, which refuse
/websocket, so Discuss and notifications never updated live. Each production now has a server mode.workers(default) routes/websocketand/longpollingto the gevent port 8072.geventserves everything from 8072. The mode is available viacreate_production,reconfigure_production, the dashboard and Stack manifests (serverMode). The health probe checks both ports, so a dead gevent process rolls the deploy back. Existing productions get the new routing at their next deploy, restart or reconfigure rather than all at once at startup. (#290) - Rotated secrets reach productions —
reconfigure_productionnow findssecret:<name>values that changed since the container was created and recreates it with the new values. Previously it reported "No settings changed". The result lists only the rotated key names, never values. The newrecreateflag (MCP and REST) forces a recreate on request. (#300) - OpenCode through an OpenAI-compatible gateway — a new optional
[agent.opencode_provider]section declares a custom OpenCode provider that uses Chat Completions. It suits gateways such as LiteLLM, which do not accept the Responses API used by OpenCode's built-inopenaiprovider. Select its models as<id>/<model>(for exampleopencode_model = "litellm/glm-5.3"). The provider applies to both Agent CLI and Agent Chat. The config names only the agent-environment variables that hold the base URL and key (defaultOPENCODE_PROVIDER_BASE_URLandOPENCODE_PROVIDER_API_KEY, kept apart from theOPENAI_*variables that Codex reads), so no secret is embedded. The section is validated at startup and in the settings console, where it is editable and applies live. Whenopencode_modelselects the provider and a team lacks the variables, Agent Chat warns and Agent CLI waits for Enter before starting OpenCode. Without the section, behaviour is unchanged. (#308) - Mail servers are archived during sanitization — the bundled
01_disable_mail.sqlnow sets incoming and outgoing mail servers to inactive instead of deleting them, so you can switch one back on in a dev environment. Unmodified deployed copies pick this up automatically. (#291) --no-telemetryflag — a new global CLI flag turns telemetry off for the current process only, without editingoduflow.toml. (#287)- JSON secret keys by dot path — partial JSON secret updates now accept
dot notation (
environment.OPENROUTER_API_KEY) as well as JSON Pointer. A missing final object key is created, and the result says whether the key wascreatedorupdated. (#288)
Dashboard¶
- Cleanup button — opens a preview of the team's orphaned databases, workspaces, port entries and PostgreSQL roles, plus the server's unused Docker images. Confirming removes only what you reviewed and what is still orphaned. Image removal is opt-in, and images used by any container, any build or a recent operation are kept. (#299)
- Production environment variables — the production Create and Settings
forms have an Environment variables field.
get_production_inforeportsenv_vars, andsecret:references stay references. (#298) - Edit one key of a JSON secret — the secret editor has separate replace the whole value and update a single key sections, so changing one key no longer means re-sending a write-only document. (#288)
- Extra-addons branches dialog — each remote extra-repo card shows the
downloaded branches, who uses each one, and the remote list. Download
branches ahead of time, or remove an unused one to free disk.
update_extra_repo(add_branch=...)also accepts a list. (#285) - Update all fetches every extra-addons repo one after another and shows a summary of what changed, was up to date or failed. (#294)
- Repo filter and bulk branch picker in the create-environment modal and template settings. Narrow a long repo list by name, then apply a branch to every repo that has it. Rows hidden by the filter stay selected and are counted. (#293)
- Extra-repo list on environment cards folds to two lines. A long list gets a filter (by name or branch) and a Show all (N) toggle. (#297)
- Create from production is pre-filled with the production's repository,
Odoo image, Git credential and extra addons. A new Base branch field
creates a branch that doesn't exist on origin yet: it starts from the base
branch's tip and is pushed without
--force. (#305) - Agent Chat sits next to Connect As — Agent Chat moves out of the More menu to sit right of Connect As on every environment card. (#308)
Bug Fixes¶
- Template modules upgraded at create —
create_environmentnow runsodoo -ufor installed modules whose checkout manifest is newer than the version recorded in the template. Before, such environments failed withcolumn ... does not existand were rolled back after the readiness timeout. Extra-addons repos are covered. (#304) - Helper containers work on any Docker log driver — on daemons whose
default log driver is not
json-file/journald(for examplelocal),read_file_in_volume,search_in_volume,delete_file_in_volume, UID/GID detection and the PostgreSQL version probe read no output. Visible effects included Stack applies rewriting unchanged files and fake search matches. (#289) - Legacy secrets typed by value — secrets saved before
value_typeexisted are now treated asjsonwhen they hold an object or array, so their keys can be updated. A startup migration records the type. Note:POST /api/secrets/{name}/setwithoutvalue_typeon such a secret now requires valid JSON. (#307) get_environment_inforeports missing environments with aNotFoundErrorinstead of a plausible-looking stub. (#292)- Config errors are reported in one line for wrong-typed TOML values. A
missing
ODUFLOW_TOMLpath is reported instead of a new default config being created there silently. (#294) - Ctrl+C exits quietly with status 130 instead of printing a
KeyboardInterrupttraceback. (#306)
v1.85.0¶
Breaking Changes¶
- PostgreSQL 16 for Odoo 20 — Odoo 20 requires PostgreSQL 16+, so the
development and production clusters both run the official image from
[database].image(defaultpostgres:16, supported majors 16–17). The separate production PostgreSQL image (oduist/oduflow-postgres) and[production].postgres_imageare removed. A one-time startup migration replaces an existing PG15 cluster: it refuses to start while any environment, production or service database still lives there — listing every blocker at once — andoduflow upgraderuns the same read-only check with the new code, so a self-update never restarts into a server that cannot start. Once clear, the old clusters are deleted together with the PG15 WAL-G archive (PG15 base backups cannot restore into PG16), new clusters are created, and template databases are restored from their on-disk dumps behind a journal that resumes after a failure. Only official PG15 clusters are reset; clusters carrying custom extensions need operator handling. See the upgrade procedure in the installation, CLI and production guides and decision record 0074. (#283)
Features¶
- On-demand extra-addons branches — adding an extra-addons repository no longer downloads code: Oduflow checks access and reads the branch list, and each branch is downloaded (latest commit only) the first time an environment uses it. Large multi-version repositories are added in seconds instead of timing out. Downloaded branches update incrementally; branches deleted on the remote are untracked. Repositories added by earlier versions keep fetching all branches. (#282)
- Update single elements of JSON secrets —
POST /api/secrets/{name}/update-jsonreplaces one nested key or array element by JSON Pointer instead of rewriting the whole document. Invalid paths or values are rejected without touching storage, responses never expose secret values, and secret writes no longer block dashboard requests while they hit disk. (#278)
Dashboard¶
- Tool-result images in Agent Chat — screenshot-style tool results are shown
as inline pictures instead of a wall of base64 JSON, with the surrounding text
and metadata kept alongside them.
image/svg+xmlstays outside the MIME allowlist and payloads are charset-validated, so a tool result cannot smuggle a script-bearing data URL into the page. (#280) - Shared links recover instead of dead-ending — a visitor who navigates out
of a shared
/env/<name>view is redirected back to it instead of seeing a raw 403 (API and state-changing requests keep the 403, WebSockets still close), the landing page names both routes back — reopening the share link and signing in — and Sign out is a real sign-out for an operator holding a revoked or rotated share cookie. (#279)
Bug Fixes¶
- SSH in the coder image —
oduflow-coder:0.3.2includes the OpenSSH client so agent Git operations can use team SSH deploy keys. (#281)
v1.84.0¶
Licensing¶
- PolyForm Noncommercial source license - new releases use the unmodified PolyForm Noncommercial License 1.0.0, with a separate free internal evaluation permission and alternative Solo, Business, Integrator and Enterprise commercial agreements. Earlier releases and perpetual commercial purchases retain their original terms. All plans keep the same code, features and public updates.
- License terms in the dashboard - the license details dialog now links to the Oduflow Commercial License Agreement and the public source license alongside the holder, validity period and renewal actions.
v1.83.0¶
Features¶
- Manual commercial periods and agreed annual migration - existing customers can retrieve an agreed annual term through Update license status. A stable license identity allows a manually granted period before a Paddle subscription exists. After expiry, Subscribe annually opens checkout for the same holder and plan; completed payment links the subscription and enables paid renewal. Other perpetual licenses remain valid, and expiration never restricts features.
v1.82.0¶
Features¶
- Annual commercial license details and renewal - click the license banner to see the named holder, plan and paid-through date. Expired licenses show a red status and an Update license status action. A manual check retrieves a signed extension after a completed Paddle payment. All product features remain available, and existing perpetual keys remain valid. Checkout, key delivery and renewal are hosted by the license server.
- Staging builds as services - staging image builds run through the service lifecycle, with image retention that preserves images still used by services or staging environments. (#273)
Fixes¶
- Validate JSON-typed team secrets before storing them, with clear errors for malformed values. (#271)
v1.81.0¶
Breaking Changes¶
import_template_from_odoois nowimport_template— the MCP tool is renamed (first argumentodoo_url→source), and thereload-templateCLI command is removed. Its jobs are covered byimport-template: bare reload →--refresh,--dump-path→ a dump-file source with--overwrite,--source s3://…|/dir→ the same source with--overwrite. The old--sourceimplementation shelled out toaws s3 sync/rsyncinto the live template directory (environments stayed unmounted for the whole download and metadata went stale); the unified path needs no AWS CLI on the host and stages before swapping. The REST endpoint/api/templates/import-from-odookeeps its path and acceptsodoo_urlas a legacy alias forsource. (#257)
Features¶
-
One unified template import —
import_template(MCP),import-template(CLI) and the dashboard API are now the single door for template data, dispatched on the shape of the source: an http(s) Odoo URL (database manager API,master_pwdrequired, as before), ans3://bucket/prefix, a local directory with the same raw layout, a single local dump file (database-only), orrefresh=truewith no source to reload the template from files already placed in its directory (an external rsync/scp drop). Every variant refreshesmetadata.json— Odoo version and modules read from the restored database, sizes, overlay mode and data age. Decision record:specs/0069-unified-template-import.md. (#257) -
Raw-layout import from S3 or a local path — a
dump.pgdump/dump.sql[.gz](hand-madedb.dump/db.dump.gznames are accepted too and installed under the canonical name; canonical wins when both are present) plus an as-isfilestore/copy (e.g. uploaded withaws s3 sync) is imported without a master password or archiving. S3 downloads run in parallel and resume after interruption; local files are hardlinked, which is near-instant on the same filesystem. Nothing touches the live template until the staged copy is promoted under the overlay remount guard, so live environments keep their own changes.overwrite=truere-syncs an existing template incrementally — only changed filestore files are fetched, files deleted at the source are removed, and an unchanged dump (same S3 ETag, or local size+mtime) skips the database reload, which makes a nightly "pull last night's backup into dev" job cheap. S3 credentials come from explicits3_access_key/s3_secret_key(+s3_endpointfor MinIO and similar), the[backup]settings when the bucket matches, or anonymous access for public buckets. (#257) -
Branch picker for extra addon repos — the dashboard's add-repo dialog can list a remote's branches before cloning (
POST /api/extra-repos/ls-remote,git ls-remote --heads) and clone only a chosen subset; all branches remain the default.add_extra_repotakes optional comma-separatedbranches, andupdate_extra_repo(add_branch=...)starts tracking one more branch in a subset clone later. Branch names are validated withgit check-ref-format, and git authentication failures now say what to fix: SSH remotes point at the team deploy key (get_ssh_public_key), HTTPS remotes atsetup_repo_auth. (#257)
Bug Fixes¶
- Missing branch on environment create is reported as such — creating an
environment for a branch that was never pushed failed with a
git cloneexit 128, which the dashboard masks behind the generic "Operation failed" banner. The clone now raisesNotFoundErrorwith the same actionable messageswitch_branchalready uses ("Branch 'X' does not exist on origin. Push it first…"), so the dashboard (404) and MCP callers see the real cause. Production clones share the same path. (#269)
v1.80.0¶
Features¶
-
oduflow self-update— upgrade an installed Oduflow through its own installer, reconcile bundled files in a fresh process on the same interpreter, and restart the systemd service when running as root. pip and uv tool installations are detected; containers, source/editable installs, ephemeral uvx environments and uv tool directories that do not match the running installation are refused rather than half-upgraded. The installed version is verified after the installer exits, so an unavailable release, a retained version pin or an unreadable version stops the command before reconciliation or restart.--forceforwards conflict handling tooduflow upgrade --forceand completes reconciliation even when the package is already current (the recovery path after an interrupted upgrade);--no-restartleaves the restart to the operator. (#268) -
Separate production MCP access — production tools now live behind their own
/productionendpoint with per-team production credentials; development credentials no longer authorize them. New Odoo 19 productions get OduMCP installed automatically and the configured key synchronized for the Odoo administrator, so production records are reached through OduMCP policies, approval plans and audit without standing up a separate MCP server. An addon setup failure leaves production running with a warning and the Odoo tools reporting unavailable; existing productions, retries and key rotations usesync_production_mcp. (#260) -
Release check from the dashboard header — clicking the version next to Oduflow opens a dialog with the installed version, the latest published release with its title and date, a link to its notes, and the upgrade commands when a newer one exists. The lookup is strictly on demand: one unauthenticated request to the GitHub Releases API per click, nothing cached between clicks, no background polling, and no fact about the installation sent. Offline hosts, rate limits and unreadable replies are shown as answers in the dialog, and a source checkout reports
devas not comparable rather than guessing. (#261) -
Select all in the module and extra-addon pickers — the dashboard's Upgrade modules picker gains Select all and Clear, as do the Extra addons repos checkbox lists in the create-environment modal and in template settings (each ticked repo still needs its branch). Selecting every installed module sends Odoo's own
odoo -u allinstead of a command line listing each module, andupgrade_odoo_modules/pull_and_apply(upgrade=...)acceptallon its own for the same run. (#262) -
Template locks narrowed to the template being changed — template mutations used to take the team lock, stopping every environment operation in the team.
import_template_from_odooand the dashboard's metadata editor now take a template-scoped key instead, andattach_filestoreenters the team lock only for the remount-and-swap window, so staging a multi-gigabyte filestore runs outside it.delete_templateandrename_templatekeep the team lock, which their dependent-environment scan genuinely needs. The import's download path is unique per call with cleanup that cannot strand an orphan. (#264)
Security¶
- Generated secrets stay out of the log —
[database].password,auth_tokenandui_passwordwere printed to the startup log, which is shipped off-host and retained far longer than the secrets stay valid. They now live inoduflow.tomlonly and the log points at the file. The config is created0600, and an existing config that gains an auto-provisionedui_passwordon upgrade has a group/world-readable mode narrowed — never widened. Config bootstrap opens the destinationO_EXCL, so a missingODUFLOW_TOMLpath can no longer truncate a live/etc/oduflow/oduflow.tomland destroy its database password, teams and tokens. A badoduflow.tomlnow surfaces as one readableConfigErrorline instead of a traceback. (#266)
Bug Fixes¶
-
One shared Odoo version parser — the test runner (
--longpolling-portvs--gevent-port), the translation exporter (--i18n-*vs the 19-onlyodoo i18nsubcommand) and the sanitizer (doesodoo neutralizeexist?) each had their own regex over the image reference, they disagreed, and guessing wrong makes Odoo abort witherror: no such option.odoo_version.pynow owns the parsing: official tags, custom repositories with a version tag, versioned repository names, and a liveodoo --versionprobe for images that carry no version, anchored on theOdoo Server N.Mbanner. Every source is plausibility-checked, so an image that versions itself on its own scheme falls through to the probe instead of selecting a removed CLI option.create_environment's version-guide reminder uses the same parser. (#265) -
Coder image source label — the image's
org.opencontainers.image.sourcelabel still named the oldoduistorg, so a pulledoduist/oduflow-coderpointed back at an address the project no longer uses. Corrected and republished asoduflow-coder:0.3.1; the Docker Hub namespace is unchanged. (#263)
Documentation¶
-
Architecture page — a new
docs/architecture.mdwith two Mermaid diagrams: a system overview (MCP agents, browser and GitHub webhooks → the single-process server → dev environments, production, auxiliary services, Traefik and the S3 bucket) and a backup-and-recovery diagram contrasting logical snapshots with the WAL-G stream. Mermaid rendering is enabled in MkDocs Material. (#267) -
Indexed per-tool MCP tools reference —
docs/mcp-tools.mdwas one 102-row table whose first column squeezed tool names mid-identifier. It is now a clickable index of all 102 tools in 17 categories plus one section per tool, each with its parameters as a definition list, "use it when" bullets, stated lock scope and, where the invocation isn't obvious, anoduflow callexample. (#259)
v1.79.0¶
Features¶
-
Dashboard two-factor authentication; HTTP Basic auth removed — a team can enrol an authenticator app as a second factor for full dashboard login.
oduflow ui-2fa setup --team Nrenders the QR code locally (no external chart service) and stores the secret only after a confirming code;oduflow ui-2fa reset --team Nis the server-side recovery path. Both bump the team's MFA generation, revoking outstanding full-UI cookies on the next request without a server restart. The last consumed step and per-team attempt counters persist in.ui_totp.json, so replay protection and throttling survive a restart, and candidate steps are derived from an explicit UTC datetime so a DST transition cannot shift the window. Shared scoped links keep their own cookies and stay reachable without a code; MCP client authentication is untouched. HTTP Basic auth is gone from the web UI entirely — REST and WebSocket access now rely solely on the session cookie, and thescripts/create_env.py/scripts/sync_env.pyBasic-auth helpers are replaced byoduflow client. On upgrade, existing full-UI cookies require a fresh login, and sessions expire seven days after login instead of being renewed by dashboard loads. Decision record:specs/0067-dashboard-totp.md. (#258) -
Production WAL disk protection — the shared production cluster is now guarded against WAL-driven disk exhaustion and against silently losing archive continuity. A local WAL monitor samples the real WAL filesystem (through a read-only volume helper while PostgreSQL is down) and derives archive progress and disk risk without touching Docker or S3 on the read path. On danger a persistent circuit breaker latches: it disables Docker restart policies, stops applications before PostgreSQL, and survives an Oduflow or Docker restart; recovery runs PostgreSQL alone behind an HBA fence and requires confirmed new archiving plus extra headroom before release. Production start and deploy now pass an admission gate — an in-container WAL-G storage read/write preflight plus proof that a freshly generated WAL segment reached storage — while an already admitted, running production accepts a current healthy sample instead, so a hung production stays restartable during a brief storage outage. Ships with a dedicated production PostgreSQL image on a pinned base digest, a dashboard WAL panel,
/healthzchecks and an MCP/REST surface. Decision record:specs/0066-production-wal-disk-protection.md. (#254) -
HTTPS without Let's Encrypt —
[routing] tls = {}puts Traefik on :443 with the :80 → :443 redirect and no certificate resolver, for local and test deployments that want TLS on the wire without ACME, a publicly resolvable domain or anacme_email. Supported values stay a closed set:true(ACME, the default),{}(HTTPS without ACME) andfalse(HTTP only); non-empty tables are rejected. Whether the resolver is declared in Traefik and whether managed routes use it are now separate concerns, sotls = {}with anacme_emailset declares the resolver for an operator's own drop-in routes while Oduflow's own hostnames stay on Traefik's default certificate. An existing certificate store is never deleted, so issued certificates and the Let's Encrypt account key survive a later re-enable. In this mode only, Oduflow's probes of the URLs it hands out (http_request_to_odoo, the environment readiness check) skip certificate verification, since the default self-signed certificate has no trust anchor to verify against. (#251, #252) -
Parallel filestore chunk uploads — a first full filestore snapshot paid two serial object-store round-trips per chunk (observed: 70 chunks / 210 MB in ~21 minutes). Chunk HEAD, compress and PUT now run on a bounded thread pool, with the revision's chunk sequence and dedup set still producer-ordered and the revision file still written only after every upload succeeds. The new
[backup].upload_threadssetting defaults to 16 (1restores the previous sequential behaviour) and sizes the boto3 connection pool to match. Memory is bounded on both axes: in-flight plaintext passes a byte budget rather than a weak per-chunk count, and pending futures are reaped past a threshold instead of accumulating one live future per chunk. Snapshot manifests and log lines now record scan, upload and elapsed durations. (#255) -
Extra-addons dependencies, prod-cluster service databases, service protection — extra-addons repos contribute their own apt/pip dependency descriptors, installed per repo, so a
requirements.txtin an extra checkout is honoured the same way the main repo's is. A service database can be created on the production PostgreSQL cluster, taking part in the cluster-wide WAL-G backups instead of the development disk quota. Services and service databases can be marked protected, which blocks delete, update and restore on both the MCP and REST paths. (#250)
Dashboard¶
-
Full service editor — the Update dialog prefills every setting (image, command, exposure and routes, hostname, env vars, host mode, volumes, capabilities) from the new
GET /api/services/{name}/configand submits only the fields the user actually edited, so an open dialog cannot revert a setting changed concurrently over MCP. Deleting a service now offers a "Save as preset" choice, and the result reports what is actually on disk afterwards rather than echoing the request flag. Migration0008-backfill-service-presetswrites presets once at server start for services created before presets existed, skipping services whose image or port cannot be reconstructed and filtering out env vars the image itself sets. (#253) -
Production logs moved into the shared logs modal. (#250)
Bug Fixes¶
- Templates with a hand-placed
db.dump—get_template_sql_pathtried only four canonical names and then reported the missingdump.pgdumpfallback, never looking at thedb.dumpsitting next tofilestore/andmetadata.json.db.dumpanddb.dump.gzare now accepted, ordered last so a dump Oduflow itself persisted keeps priority over a leftover dropped into the template directory. (#256)
v1.78.0¶
Features¶
-
Per-team SSH deploy keys — each team gets an Oduflow-managed ed25519 deploy key as an alternative to HTTPS tokens, generated automatically at server start and copyable/regenerable from the dashboard Credentials tab,
GET /api/ssh-key/POST /api/ssh-key/generate, or theget_ssh_public_keyMCP tool. SSH repository URLs (git@host:path,ssh://…) are accepted everywhere a repo URL is taken — environments, extra addon repos, productions, webhooks — under the same SSRF host checks as HTTPS. Every managed git subprocess runs withGIT_SSH_COMMANDpinned to the team key (BatchMode=yes,IdentitiesOnly=yes, per-teamknown_hosts), so a keyless team fails fast instead of borrowing the host operator's identities, and the key is provisioned into the team's coding-agent container. Decision record:specs/0065-team-ssh-deploy-keys.md. (#247) -
Team base domain and multi-domain productions — a team's DNS zone is now an explicit
base_domainsetting (Traefik mode). With it set, environments and services are named directly under the zone (feature.demo.example.com) as siblings of the dashboard instead of nesting two levels deep, the dashboard hostname defaults tooduflow.<base_domain>, and production primary domains must be the apex or a subdomain of it. Productions also gainextra_domains: arbitrary client-owned FQDNs routed to the same container through one multi-Host()rule. One wildcard record plus the apex now covers the dashboard, every environment, every service and default production names. Leavingbase_domainunset preserves the legacy nested layout exactly, and existing environments keep their hostname until their nextupdate_environment. A newdomains.pyis the single authority for the global TraefikHost()namespace. Decision record:specs/0064-team-base-domain.md. (#246) -
Production targets in declarative Stacks —
spec.productionis a mutually exclusive alternative tospec.environment, so a platform promoted to production can be reconciled without reviving its retired dev container. Stacks create productions or explicitly adopt matching existing ones without copying data or restarting their containers; ownership is stored in the production registry and survives container replacement. Domain, image, environment variables, policies and Odoo configuration are reconciled with secret references preserved, effective runtime drift detected, and an incomplete-apply marker so a retry repairs a failed replacement. Supporting services can resolveproductionField: url|containerName|database. Dev manifests remain compatible. (#244) -
Mutable production configuration and dev-to-production promotion —
reconfigure_production(MCP,POST /api/productions/{name}/reconfigure, and dashboard More → Settings) changes domain, Odoo image, branch, repository URL, git user, extra addon repos and environment variables on a live production; the container is recreated while database and filestore are preserved, and a mid-way failure is resumable rather than reported as "no settings changed".set_production_odoo_confstores per-production odoo.conf overrides in the registry, surviving deploys and retunes. Andcreate_production(from_environment=...)promotes a development environment: its database and filestore are copied as a consistent pair (Odoo briefly stopped) and repo, branch, image, git user, extra addons and env vars — includingsecret:<name>references — are inherited from its container labels. Decision records:specs/0061,specs/0062. (#243)
Security¶
-
Sanitize scripts no longer run as the cluster superuser — repo-controlled
.sql/.pysanitize scripts executed as the shared development cluster's PostgreSQL superuser, which madeCOPY … TO PROGRAMhost RCE and allowed cross-tenant SQL. They now run as the environment's scoped role; an environment without scoped credentials skips sanitization instead of escalating. (#237) -
Service hostnames can no longer hijack another team's routes — a tenant-supplied service hostname was written verbatim into the Traefik
Host(...)rule, so a crafted value could claim another team's hostname. The resolved hostname now passes the same FQDN validation as production domains. (#237) -
Agent Chat markdown is sanitized with DOMPurify — the hand-rolled sanitizer was bypassable (
java	script:scheme, SVGxlink:href), giving clickable XSS in the dashboard origin. Marked's output now goes through vendored DOMPurify 3.2.4, shipped in/staticlike every other asset — no CDN. (#237)
Bug Fixes¶
-
oduflow cleanup --forcedestroyed live productions — production containers deliberately carry no branch label, so orphan classification saw a running production's database, workspace and PG role as reclaimable. The reservedprod-namespace is excluded from every orphan category. (#237) -
CLI boolean and integer arguments were ignored — PEP 563 string annotations made positional-argument coercion dead code, so
oduflow call delete_production erp erp falsedropped the database despite the explicitfalse. Types are resolved viaget_type_hintsnow. (#237) -
Failed
create_productionno longer deletes preserved data — the rollback rmtree'd a workspace kept by an earlierdelete_production(drop_database=False), destroying its filestore and deploy history. It now removes only what the attempt itself created. (#237) -
Backup prune kept manifests but dropped revisions — the weekly prune collapsed a deleted-but-kept production's backups to the newest revision while its manifests survived, so restoring any older snapshot failed. Untracked snapshot ids are kept whole. (#237)
-
Cluster PITR to a past timestamp — recovery always fetched the
LATESTbase backup, so any pasttarget_timeFATAL'd and left the cluster down with PGDATA displaced; the newest base at or before the target is now selected before any destructive step.restore_cluster_pitralso validates[backup]prerequisites before stopping every team's productions, and restarts them best-effort if the restore fails. (#237) -
destroy_systemignored the production tier — it proceeded with live productions, never removed the production PostgreSQL container and volume, and crashed half-way through teardown on the shared-networkAPIError. (#237) -
Stale overlay filestores on start —
start_environmentremounts a missing or stale fuse-overlayfs filestore before starting the container, complementing the startup-widereconcile_overlay_mounts. (#237) -
Published templates lost their metadata —
publish_env_as_templatelooked the source container up by a non-team-scoped name and silently droppedodoo_image,repo_urland friends from every published template. (#237) -
search_in_volumealways failed — it used GNU-onlygrep --includeinside the busybox helper image, exiting 2 on every real search. (#237) -
Apply
odoo.confand dependencies with one gated restart — the serving container boots before Oduflow can install anything into it, and the restart that picked up the repository's config and pip packages was skipped whenever pip had nothing to install, so a repository shipping.oduflow/odoo.confwithout arequirements.txtkept serving on the image's stockaddons_pathand worker settings. The restart is now unconditional and gated on the Odoo registry coming back:create_environmentpreviously returned as soon as the restart was issued, so database neutralization could exec a second Odoo process into a reloading container — a collision that was only logged, handing back an environment with live mail servers, crons and payment providers. The returned URL is live instead of answering 502 for the first minute, and an update that changed nothing inside a fresh container skips a pointless reload. (#249) -
Production database routing during updates — production module checks ran against the shared development PostgreSQL container with the production role, so
update_production(upgrade=...)failed with a missing-role error even though the role existed in the production cluster. The shared apply engine now gets a scoped settings copy pointing at the production database, leaving development settings intact for concurrent requests, and exceptions raised after source synchronization go through the existing code rollback path with the failed commit and exit status recorded in deploy history. (#245) -
without_demo = Truein the bundled configs — Odoo 19 parses the option as a boolean and loggedinvalid boolean value: 'all'on every start; the two spellings are equivalent on all supported versions. (#249)
Documentation¶
- Connecting Claude Desktop to a remote Oduflow server — Claude Desktop can
only launch MCP servers as local processes, so
docs/quick-start.mdnow documents themcp-remotestdio bridge: config file locations, Windows and macOS/Linux snippets, where the team'sauth_tokengoes (inenv.AUTH_HEADER, not inline inargs), why--transport http-onlyis required, and the scoped/mcp/<env>variant.docs/security.mdpoints there from the Claude.ai OAuth section. (#248)
v1.77.0¶
Fixes¶
- Preserve user environment variables and
secret:<name>references when promoting a development environment to production and recreating its container. Validate secrets before changing the source or production.
Features¶
-
Publish productions as dev templates and create environments from production —
save_production_as_templatepublishes a production's database and filestore as a dev template, andcreate_environment(from_production=...)builds an environment from a copy of real production data; the dashboard gets the same two actions on each production card. The copy goes through one managed template per production (prod-<name>), published on first use and reused afterwards, so the second environment from the same production is an instant clone plus an overlay mount. The production keeps serving throughout: a consistentpg_dumpis streamed straight from the production cluster into the dev cluster's exchange dir, and a publish that fails halfway is rolled back. (#240) -
Purge deleted-production leftovers via tombstones — a soft
delete_production(withoutdrop_database) keeps the database and workspace by design, but the leftovers had no lifecycle and accumulated forever. Deletion now writes adeleted.jsontombstone, and tombstoned leftovers can be reclaimed either automatically — new opt-in[lifecycle] prod_purge_hours = N(default0= keep forever) lets the reaper purge the database, PG role and workspace N hours after deletion — or immediately viaoduflow cleanup --purge-deleted-productions(dry-run by default,--forceto apply). Only tombstoned leftovers are ever purged. (#242) -
Per-team
public_scheme—[team.X] public_schemeoverrides the global[routing]value, so onetls = falsedeployment can serve a plain-HTTP LAN team and a team behind a TLS-terminating upstream (e.g. a Cloudflare tunnel) withhttps://links at the same time. Every URL Oduflow hands out (dashboard links, MCP endpoints, environment/service/production URLs) uses the owning team's resolved scheme, and Traefik trusts inboundX-Forwarded-*on:80whenever any team resolves tohttps. The same wire-reality validation applies per team:httpsis rejected in port mode andhttpis rejected whiletls = true. (#241) -
Change an environment's hostname —
update_environmentand the dashboard's Update dialog accept ahostname: for a team atdev.example.com, settinghostname="qa"moves the environment toqa.example.comwhile preserving its database and filestore. The requested hostname is validated and atomically reserved before the container is stopped; conflicts and non-Traefik deployments are rejected up front. (#239) -
Paste the token, not a
user:PAT@repoURL — the dashboard's Add Git Credential dialog now takes the access token, the git host (defaultgithub.com), an optional username and an optional repository URL to verify against. Git matches stored credentials by host, not by repository, so a single token covers every repository on that host; the repository path in the old URL form was only ever used for thegit ls-remotecheck. Without a repository URL the token is verified against the provider API (GitHub, GitLab, Bitbucket).setup_repo_authgained matchingtoken,usernameandhostarguments andPOST /api/credentials/addaccepts the same body; the legacyhttps://user:PAT@host/owner/repo.gitform still works. (#238)
Bug Fixes¶
- Platform Stack image convergence and PostgreSQL startup permissions — a
fresh host initialized under systemd
UMask=0077produced a root-only PostgreSQL config and unsearchable mount parents, so PostgreSQL failed to start and team tablespaces could not be created. Non-secret PostgreSQL configs are now written with explicit0644permissions (including retune and fallback paths), thepg_tablespaces/pg_exchangemount parents get0755, and the service image reference is preserved from the container'sConfig.Image, stopping repeated service image drift. Secrets and team data directories keep their restrictive permissions. (#236)
Dashboard¶
- Copy button in the Info dialog — environment and service Info dialogs
can copy the displayed environment variables as newline-separated
KEY=VALUEentries; the button is disabled when no variables are available. (#235)
v1.76.0¶
Features¶
-
Write-only team secrets — store passwords and API keys in the dashboard's Credentials tab and reference them in environment variables as
secret:<name>. Presets, templates and MCP/REST/dashboard configuration reads retain the reference; Oduflow resolves the value when creating the container. Agents can list secret names without retrieving stored values. Missing references fail before replacing a running container. To rotate a secret, replace its value and recreate its consumers. Resolved values remain accessible inside the consuming container. (#231) -
Service lifecycle controls survive replacement — auxiliary services accept a validated
runtimemapping for tmpfs mounts, private cgroup namespaces, stop signals and shutdown timeouts through MCP, REST and Stack manifests. Updates, presets and restores preserve these settings, and stop, restart and replacement honor the existing container's shutdown timeout. (#229) -
One team hostname for LAN and tunneled OAuth access — self-hosted OAuth derives its issuer from the team's hostname in both port and Traefik modes. Port-mode deployments can expose MCP through Cloudflare Tunnel while hosted agents continue using the local Docker host gateway. Configuration now uses
[server].bind; legacy[server].hostis accepted with a deprecation warning. Upgrade configuration: every team must declare a uniquehostname; the old[routing].hostnamefallback is ignored, and[oauth]/oauth_base_urlare removed. Host-header parsing also rejects malformed authorities and origin injection. (#234)
Bug Fixes¶
-
Branch switching no longer blocks on installed-module preflight —
switch_branchproceeds without the database module-state check, allowing branch changes that the preflight previously refused. Normal apply checks and actual Odoo failures still report problems;strictremains scoped to incomplete install, upgrade or restart actions. (#233) -
Cleanup preserves renamed environments' database roles — orphan detection checks persisted PostgreSQL credentials as well as roles derived from current environment names, so a role still used after a rename is not treated as orphaned. (4c72e68b)
-
Saving a template preserves environment metadata — source-container lookup uses the current team-scoped name, restoring capture of environment variables, repository URL, Odoo image, Git user and extra addons. (#231)
Security¶
- Service presets use owner-only file permissions — new writes use mode 0600, and a startup migration applies the same permissions to existing service preset files, which may contain credentials. (#231)
Dashboard¶
-
More readable dashboard typography — Source Sans 3 and Source Code Pro replace Outfit and Geist Mono across the dashboard and login page, including code, logs and the terminal. Fonts and their licenses ship with the package and require no external CDN. (#232)
-
Inspect environment variables from the environment card — the More menu gains an Info action that displays configured variables, including secret references. The header also stops rendering an empty shared-environment badge in regular team sessions. (#230, #228)
v1.75.0¶
Features¶
- Plain-HTTP deployments via
[routing] public_scheme— a new setting fixes the scheme (httporhttps) of every URL Oduflow hands out: environment, service and production URLs, dashboard share links, MCP endpoints and artifact links. Unset, it derives the historical default —httpsin traefik mode,httpin port mode — so existing deployments, includingtls = falsebehind a Cloudflare tunnel, are untouched. Declaringpublic_scheme = "http"(plain HTTP end to end, nothing terminating TLS) also switches off trust in client-suppliedX-Forwarded-Protoon Traefik'swebentrypoint — with no trusted terminator in front, that header must not be believed — and the Traefik drift check recreates the container when the trust flag no longer matches. Contradictory combinations are rejected at validation:public_scheme = "http"with traefiktls = true(the :80→:443 redirect would bounce every link and leak tokens on the plaintext first hop), andpublic_scheme = "https"in port mode (published ports serve plain HTTP). See the new "Plain HTTP, with nothing terminating TLS" section in the Traefik docs. (#226)
v1.74.0¶
Features¶
-
Share a single environment by link — an operator can now mint a revocable share link from the environment card and hand out
https://<team-host>/env/<name>?key=<secret>without giving away the team password. The link exchanges the key for a signed host-only cookie and lands on the same dashboard scoped to that one environment: the client gets logs, consoles, module install/upgrade, Connect As, the/mcp/<env>credentials and Agent Chat, but no templates, services, volumes, extra repos, productions, host statistics or any other environment. The boundary is a server-side default-deny allowlist that runs in the auth middleware for every HTTP and WebSocket request a scoped session makes — hiding buttons is presentation, this is policy. Agent CLI is deliberately denied: its PTY lives in the per-team agent container, whose workspace holds a checkout of every environment. Share secrets live in the team'sshares.jsonrather than a container label, so environments that already exist are shareable too; they survive recreation and are revoked on deletion. Seedocs/security.mdand specs/0055. (#223) -
Rename an environment in place —
update_environment(and the dashboard's Update environment dialog /POST /api/environments/{branch}/update) accepts anew_nameand renames the environment on the same container recreate. The database, filestore, workspace, port, credentials, activity, Agent Chat history and the coding agent's checkout (moved, never re-cloned) all follow the name, and the scoped MCP endpoint moves to/mcp/<new name>. The target name is locked for the duration, so a concurrentcreate_environmentcannot claim it mid-rename, and productions are refused outright so a rename cannot move a production out from underproductions.json. A hostname derived from the environment name follows the new name; pooled and explicit hostnames stay as they are. (#225)
Bug Fixes¶
- Auxiliary service start failures now say what went wrong — when the
container behind
create_service,update_serviceorrestart_servicefails to start, the MCP response used to be a bare "Error calling tool" with the daemon's reason buried in the server log. A host port that is already taken is now reported as a conflict that names the port and the tool that can apply a new one (create_serviceafter a failed create,update_serviceafter a failed restart); any other start failure carries the Docker daemon's explanation minus the SDK's HTTP wrapper, container IDs and absolute paths. The container Docker leaves behind after a failed start is removed, so the retry no longer collides with its own name, andcreate_serviceagainst a stopped service of the same name now explains itself instead of surfacing Docker's 409. Docker errors outside service start stay masked as before. (#224)
Breaking Changes¶
- Registry credentials are configured directly — the
[team.X.image_registry]section introduced in v1.73.0 took the push token through atoken_envindirection naming an environment variable. It now takes the token itself astoken, alongsideusername;token_envis gone. The value stays request-scoped for each push and is hidden from the settings repr. Updateoduflow.tomlif you enabled agent image builds on v1.73.0. (#222)
v1.73.0¶
Features¶
-
Agents can build and publish container images — environment-scoped agents get four new MCP tools (
start_image_build,get_image_build,publish_image_build,cancel_image_build) that build a Dockerfile from the environment's managed checkout and push the exact built image to the team's registry namespace, so shipping an auxiliary-service image no longer needs a human with Docker and registry access. Oduflow keeps the socket: the agent never receives Docker access or registry credentials. The build source is pinned to the checkout'sHEADcommit, exported under the environment lock into a sealed per-job context, which closes the build/pull race. A successful build is staged in the local daemon asoduflow-build/team-<id>:<build-id>, and publishing is a tag and push of that same image — nothing is rebuilt on the way out. The capability is gated on a[team.X.image_registry]TOML section whoserepository_prefixis the authorization boundary; registry credentials are read directly fromusername+tokenin that config and passed request-scoped for each push, without persisting them in build jobs. Concurrency, context size, log size, and wall-clock limits are enforced per team, and older staging images are pruned automatically (the newestkeep_imagesare kept). (#221) -
Custom start command for auxiliary services —
create_serviceandupdate_serviceaccept an optionalcommandthat replaces the imageCMD, so images whose entrypoint expects arguments (minio/minio server /data) no longer need a repackaged or legacy variant. The shell-quoted string is split into argv once and stored that way in the service preset; the imageENTRYPOINTis untouched.get_service_inforeports the effective command (and the image default when it is not overridden), the dashboard exposes it in the Create/Restore service forms and on the service card, and declarative Stacks gained acommandfield that participates in drift detection. On update the parameter is tri-state: omitted keeps the current command, a string replaces it, and an empty string drops the override. (#219)
Dashboard¶
- Service environment variables are editable from the card — Update on a
service now opens a dialog prefilled with the service's saved environment
variables; editing them applies the change and pulls the latest image in one
step, leaving the field untouched keeps the previous pull-and-recreate
behaviour, and clearing it removes every variable.
update_servicehas always accepted an env override — only the dashboard had no way to send one, so changing a variable meant dropping to MCP or the CLI. The prefill comes from a newGET /api/services/{name}/env-varsthat returns the saved preset (falling back to container inspection for services created before presets existed) rather than the container's whole environment, so image defaults are never offered for editing and cannot leak into the preset on first save. The container name on a service card now copies on click, like the one on an environment card. (#220)
v1.72.1¶
Bug Fixes¶
- Invalid environment hostnames now explain themselves — creating an
environment from the dashboard with a hostname that is not a DNS label (for
example
old_staging) failed with a bare "Internal server error." while the one useful sentence stayed in the server log. The hostname is now validated up front, next to the environment name and before the environment lock is taken, and aValueErrorraised by the create call itself is surfaced as a 400. This is the policy the MCP tools already apply: messages authored for the user are shown, every other exception stays masked. The create dialog now says what to type instead of reporting an internal error. (#218)
v1.72.0¶
Features¶
-
Managed PostgreSQL databases for auxiliary services — teams can create, inspect, rotate, and explicitly delete persistent databases that are independent of Odoo environments and sidecar container lifecycles. Each gets a dedicated non-superuser owner, lives in the team's existing tablespace, counts toward database/disk quotas, and returns ready-to-use
DATABASE_URLandPG*values only on explicit credential surfaces. Declarative Stacks can declare databases and resolve individual connection fields into auxiliary service environment variables without persisting generated passwords in the manifest or Stack state. (#217) -
Built-in remote MCP CLI —
oduflow client <tool>now calls a running Oduflow server through FastMCP usingODUFLOW_MCP_URLandODUFLOW_MCP_TOKEN. It discovers the live tool schemas, accepts named flags or a JSON argument object, derives a required environment target from an explicit override or the current Git branch (and thecreate_environmentbranch from the current checkout), supports full team and scoped/mcp/<env>endpoints, emits machine-readable JSON for automation, and preserves server-side tool errors and output-cache summaries. The existingoduflow call <tool>remains the local in-process path. (#214) -
Token-safe summaries for apply and test calls —
pull_and_applyandrun_odoo_testsnow acceptsummary_only=True, keeping verbose Odoo command logs server-side instead of injecting them into the calling agent's context. Test runs return the finalN failed, M error(s) of K testsresult; applies return one line with the action, changed-file count and exit status. Both include anoutput_idwhen detailed output is available, so failures can be inspected selectively throughread_output. The existing verbose responses remain the default for backward compatibility. (#210) -
Templates carry environment variables — a template's
metadata.jsoncan now hold anenv_varsobject, and every environment created from that template gets those variables injected into its Odoo container. Saving a template from a live environment records that environment's variables automatically, so a tuned configuration (WORKERS,LIMIT_TIME_CPU, service credentials) survives into every environment built from the snapshot instead of being retyped at eachcreate_environmentcall. Values passed at creation time are merged per key over the template's, so one variable can be overridden without restating the rest. Names are validated as shell identifiers when a template is saved; a hand-edited file with an invalid entry is ignored with a warning rather than blocking provisioning. (#207) -
create_environmentreturns an environment that already exists — instead of raising a conflict it answers with the existing environment's URL, database, image and template, starting it when it was stopped and provisioning nothing. Agents no longer need a defensivelist_environmentsround-trip before every create. The lookup runs under the environment lock ahead of template and repository validation, so the fast path stays fast. A branch mismatch is still refused — that database and URL are in use, and moving them stays an explicitswitch_branchdecision. (#212)
Dashboard¶
-
Install and upgrade modules from the environment card — each running development environment gains Install modules and Upgrade modules actions, backed by a fixed internal SQL query for the installed-module list with a legacy-credential fallback. Input is validated, normalized production aliases are rejected, and failed attempts are reported truthfully rather than as success. A successful install or upgrade restarts the environment, and a restart failure is still surfaced as a partial success. Stale upgrade-picker responses can no longer replace the active environment's module list. (#216)
-
Service database controls — the new Databases tab shows live database size, connections, and drift status, with guarded create, credential reveal, password rotation, copy-environment, and permanent-delete actions. (#217)
-
Environment variables in the template Settings dialog — a new field edits a template's variables as one
KEY=VALUEper line, the template card shows how many are set (names and values stay hidden — they routinely carry secrets), and the create-environment form prefills them from the selected template. Multiline values stay inherited server-side instead of being put through the line-based create field. In template Settings, such values are shown read-only and pointed at the raw JSON editor. (#207)
Fixes¶
-
Auto-installed modules are now part of atomic environment provisioning — creation no longer reports success when the serving Odoo registry misses its 120-second readiness deadline or the requested module install fails. The readiness probe now preserves its last error and includes a bounded Odoo log tail, required setup failures stop before sanitization and roll back the new environment, and a final database-bound registry check runs after the post-sanitization restart. This prevents an environment from surviving with requested modules missing or with their neutralization SQL skipped. Module names are validated up front, so a typo is rejected before anything is provisioned instead of after the rollback discards it. (#211)
-
Environment limits no longer rewrite existing Traefik hostnames —
environment_slotsis now only a concurrency-safe team capacity limit and applies in both port and Traefik modes. The new explicitenvironment_hostname_mode = "slots"opt-in enables reusabledev1,dev2, ... routes; the defaultbranchmode preserves established addresses such asfeature.dev.example.com, including acrossupdate_environment. Explicit custom hostnames remain stable. Environments that were automatically moved to a numbered hostname by v1.69-v1.71 return to their branch-derived route on their next update in branch mode, while their capacity reservation remains. (#213) -
Stale filestore overlays are recovered safely — stale and unexpectedly absent fuse-overlayfs mounts are now detected at startup and recovered fail-closed, preserving each environment's upper layer rather than risking its writes. Overlay health is exposed through
/healthzand the dashboard with path-safe reporting. (#208) -
A Markdown-only pull no longer restarts a production container — a pull carrying documentation alone used to be classified as
refresh, which the production deploy engine promotes to a container restart, so a docs commit bounced Odoo for nothing.classify_changesandshallow_classifynow returnaction="none"when every changed file is Markdown, while still reporting what was pulled. (#212) -
A failed install is no longer masked by a later successful upgrade — when a single
pull_and_applyboth installed and upgraded modules, the reported exit code was the last command's, so a broken install followed by a clean upgrade came back as a success. The apply now keeps the first non-zero exit code, which is what the compactsummary_onlystatus line and the live-mount snapshot both key off. (#210) -
Environment listings now carry the evidence needed for safe slot reuse — the MCP
list_environmentsoutput previously discarded lifecycle metadata that the backend already tracked and the dashboard already showed. It now reports the current git branch, creation and last-activity timestamps, stopped time and source, protection, Stack ownership and operator note; legacy records explicitly sayLast Activity: unknown. The same lifecycle metadata is available fromget_environment_info, and the agent guide no longer treats an empty GitHub PR result as proof that a slot is reusable. (#209) -
Translation instructions start with Odoo's exporter — agents are now told to run
export_module_translationsbefore creating a.po, never invent#./#:metadata by hand, and verify the loaded result withtranslation_statusafter the module upgrade. The guide also warns that re-exporting over a catalogue that has not been imported overwrites it with the database's contents. This makes the existing silent-zero-import detector preventive instead of merely diagnostic. (#209)
Security¶
- Dashboard errors no longer leak external command output —
ExternalCommandErrorcarries the failed process's full output (Docker internals, absolute paths, Python tracebacks), and the dashboard returned it verbatim. It now answersOperation failed. Check server logs for details.and keeps the detail server-side, matching the existing HTTP-500 handlers; the status code moves from 400 to 500, since a failed external command is a server-side failure. The Connect As bridge shares the same rule and no longer interpolates raw exceptions into its plain-text response. Both Connect As endpoints now take the environment lock — after reading the request body, so a stalled client cannot hold the branch lock open — so minting a session cannot race a delete or recreate of the same branch. MCP tool responses stay verbose and are unaffected. (#215)
v1.71.0¶
Features¶
- Custom environment names, decoupled from the git branch — an environment
can now carry a name of its own instead of inheriting the branch name, so one
branch can back several isolated environments. The dashboard's create form
gained an optional "Environment name" field (still defaulting to the branch),
and
/api/environments/createtakesbranchandenv_nameseparately. Recreate reads the storedoduflow.git_branchlabel, so an environment named apart from its branch comes back on the right one. Dashboards still sending onlyenv_namekeep working — it is read as the branch. (#205)
Dashboard¶
- Edit environment variables from the Update dialog — the Update
environment dialog used to be a bare confirmation; it now prefills the
environment's user-supplied container variables and lets them be edited (one
KEY=VALUEper line) before the container is recreated. A newGET /api/environments/{branch}/env-varsendpoint serves the prefill, andPOST /api/environments/{branch}/updatedistinguishes replacement (anenv_varskey in the body), clearing (an empty string) and keeping the current set (no key at all), so an untouched field — or a failed prefill — never wipes the existing variables. Body-less update calls stay compatible and the MCP tools are unchanged. (#204)
Fixes¶
-
PostgreSQL access follows the real Docker networks — on every startup, Oduflow now reads the actual IPAM subnets of its shared and per-team networks and reconciles a marked block in the active
pg_hba.conffor both development and production clusters. This repairs reused or partially initialized data volumes that lack a Docker host rule, avoids hard-coded172.xassumptions, preserves every standard/operator rule outside the managed block, validates the reloaded file and rolls back an invalid candidate. The rules usemd5while any role still holds a pre-PostgreSQL-14 md5 verifier andscram-sha-256once every role has migrated, so reconciling an old data volume never locks its environments out. The update travels through the Docker API, so it also works when Oduflow itself runs in a container with named config volumes. (#206) -
URL imports may target private-network hosts —
import_template_from_odooand the dashboard's remote-addon import rejected every RFC1918 target as an SSRF risk, which blocked the ordinary case of an operator-managed Odoo instance or an internal git server on the LAN — and was stricter thanvalidate_repo_url, which the same dashboard endpoint already hands off to. Both call sites now allow private ranges. The parts of the guard that carry the real risk are untouched: loopback, link-local (including the cloud metadata endpoint), unspecified, multicast and reserved addresses are still refused, and the library defaults are unchanged. (#203)
Security¶
- Team resolution only trusts the verified credential — the MCP team lookup
read
client_idfromContext, which carries caller-controlled request metadata rather than the credential established by auth, so a client could name a team other than its own. It now readsclient_idfrom the verified access token instead. Host-header and single-team resolution are unchanged. (#206)
v1.70.0¶
Features¶
-
Environment reuse by branch switch —
switch_branchmoves an existing environment onto another branch instead of forcing a delete-and-create cycle. Everything except the code stays in place: the name, database, filestore, hostname and URL, ports, PostgreSQL credentials and the scoped MCP endpoint and token, so a finished branch can hand its slot to the next one without reissuing certificates or reconfiguring an agent. The switch fetches and checks out the target branch, then reuses the regular pull pipeline to classify the diff and install, upgrade or restart as needed. A preflight warns when the target branch does not carry a module that is installed in the database (strict=Truerefuses instead), and productions and live-mounted environments are rejected. Available as an MCP tool, a REST endpoint, and the branch chip on each dashboard card. Seespecs/0049. (#198) -
Rename an environment in place —
switch_branchaccepts an optionalnew_name, so a reused slot whose name still echoes a finished branch can be relabelled during the same container recreate. Everything the name keys moves with it: the workspace directory, the database, port and hostname registry entries, the activity record, agent chat sessions and the coding agent's checkout (moved rather than re-cloned, so uncommitted work survives). The URL, credentials and scoped token stay; only the/mcp/<name>endpoint path follows the new name, and the tool response says so. A taken or invalid target is refused before anything mutates. (#200) -
Granular resource locks — the coarse per-team lock is replaced by narrow keyed locks, so operations that do not share data now run in parallel: a minutes-long
create_environmentno longer blocksdelete_service,create_volume,add_extra_repoorlist_templatesfor the whole team. Services and volumes get their own keys behind a narrow registry mutex that also closes an existing delete/update race and a TOCTOU in the volume in-use check;restart_serviceandrun_service_commandgain the lock they never had; credential setup, production backups and cluster PITR get dedicated keyspaces; and the XML-RPCodoo_*tools drop their environment lock, since Odoo is transactional. Template mutations keep the team lock, because they remount live overlay filestores. The dashboard's REST routes mirror the same keys, so no new REST/MCP races appear. Seespecs/0050. (#199) -
oduflow upgrade --forcenow overwrites unresolved bundled files —--forceused to only skip the confirmation prompt: conflicts, legacy files with no stored baseline and failed merges still kept the live file, wrote a sidecar and exited non-zero, so an "unattended" upgrade still needed a human. A forced run now resolves those cases in favour of the new bundle — the live file is backed up under<team-data>/.bundled_upgrade/backups/, the destination is overwritten, the baseline advances, stale sidecars and the pending marker are removed, and the run exits 0 with nothing left to review. Clean three-way merges still merge, local-only changes are still left alone, and a first-line# KEEPremains an unconditional opt-out. Behaviour without--forceis unchanged. (#201)
Dashboard¶
- Import a template from a running Odoo instance — the dashboard gained a UI-authenticated flow (with a matching REST endpoint) for importing a template straight from a live Odoo server, including optional database autodetection and database-only backups. HTTP sources are allowed but warn that the master password travels without transport encryption; the outbound host safety checks still apply. (#202)
v1.69.0¶
Features¶
-
Reusable environment hostname slots — Traefik teams can set
environment_slots = Nto decouple public routing from branch names and number the team hostname prefix:dev.example.combecomes the fixed pooldev1.example.comthroughdevN.example.com. Assignments are concurrency-safe, persist across stops and container updates, and return to the pool on deletion, bounding normal Let's Encrypt certificate issuance to the configured pool.create_environment(hostname="qa")requestsqa.example.comwhile still consuming one team slot. The default is 20 environment slots. Teams also receive a separateservice_slots = 10cap for managed auxiliary services; deleting an unused service immediately frees its slot, and either limit can be disabled with0. Seespecs/0048. (#197) -
OpenCode hosted agent — OpenCode joins Claude Code and Codex as a full Agent CLI and Agent Chat runtime. The new immutable coder image includes the MIT-licensed
opencodeCLI and native ACP server; OpenCode gets generic provider authentication, an optionalprovider/modeloverride, approval-free execution inside the existing container boundary, per-environment Agent Browser, scoped Oduflow MCP, modern ACP model selection, and isolated conversation history. (#181) -
Declarative Oduflow Stacks — a versioned
oduflow.yamlcan now describe one development environment together with its Git source, database template, extra-addons repositories, required modules, auxiliary services, named volumes, and text files copied into those volumes.oduflow stack validate/plan/apply/statusprovide strict schema validation, read-only drift previews, team-locked idempotent reconciliation, and machine-readable status;oduflow --stack ...applies the same manifest before starting the MCP server. Stack resources carry ownership/spec-hash labels, secret values can be resolved from the launching process or generated environment outputs without entering state files or plans, and V1 deliberately refuses replacement or deletion rather than risking persisted data. Seedocs/stacks.mdandspecs/0046. (#183) -
Disk space admission control — creating an environment now refuses up front instead of failing halfway through a full disk. Oduflow estimates what creation will actually write (the exact
pg_database_size()of the template database, filestore copy versus overlay headroom from template metadata, and a remote-clone budget), groups the target directories by device — tablespace directory, workspaces directory, and the PGDATA volume that holds WAL and catalogs — and rejects the request with an actionablePrerequisiteNotMetErrorunless a safety margin survives on every device. Database quota checks became predictive as well:used + estimated_new_dbis evaluated beforeCREATE DATABASE, and the dashboard's recreate flow runs the check before deleting anything. Only creation is gated, so deletion and cleanup always remain available to recover a full disk, and a failed measurement never blocks. Built-in constants, no new configuration. (#196) -
Unified host resource planning — dev PostgreSQL, production PostgreSQL and production Odoo workers used to size themselves independently against the whole host, so a combined deployment could overcommit. One deterministic host-wide CPU/RAM plan now drives all three. Plans carry a fingerprint, so Oduflow detects configs that went stale after a CPU/RAM change or a production-mode switch (and recognises legacy managed configs while leaving hand-written ones alone), and
retune-postgresgained an explicit preview/apply flow that backs up the previous config and stages Odoo worker settings without restarting anything on its own. Seespecs/0044. (#171) -
Template and code lineage — templates now record the branch, commit and snapshot time they were made from, and
create_environmentwarns when the checked-out code and the template database come from different points in history, with separate install and upgrade remediation. Lineage checks work with depth-one managed clones and linked worktrees.BusyErrornow names the lock holder and how long it has been held, so an agent can tell active work from a stale lock. Odoo and service commands run through a shell by default with an exact-argv opt-out, and Odoo test runs accept targeted tags and an optional upgrade-free rerun. Seespecs/0043. (#170) -
Incremental template snapshots — publishing a template used to copy large database dumps and filestores several times. Dumps now stream straight to their destination (custom archives restore in parallel), publish dumps stage through a quota-aware PostgreSQL exchange mount when one is available, and filestore snapshots use rsync hardlinks so only the environment's delta consumes new disk. Staged publication stays atomic, interrupted staging no longer leaves quota-consuming leftovers, and hardlinked storage is counted correctly. (#180)
-
Module installs and upgrades are verified — Odoo exits
0even when-i/-utargets a module that is unknown, uninstallable, or never actually installed, and Oduflow reported that no-op as success. Installs are now verified againstir_module_modulebefore reporting success, andupgrade_odoo_modules/pull_and_apply(upgrade=…)reject unknown or uninstalled modules instead of returning a false positive. (#195, #186) -
Two feedback channels —
report_issueand a dashboard Feedback modal build a prefilled GitHub issue form (bug, feature request, or general feedback) with a short non-identifying environment summary, so reports stay attributed to the user's own GitHub account and Oduflow never holds GitHub credentials. Separately, an operator-enabled, anonymous channel lets coding agents report friction with the MCP surface itself; it scrubs hosts, addresses, paths, tokens and instance-specific names, and restricts the tool-name field to registered Oduflow tools. Seespecs/0045. (#175, #173) -
Three-way bundled-file upgrades —
oduflow upgradeno longer compares only file sizes and overwrites every differing deployed copy. Each team'sodoo.conf, agent guides, and sanitize script now keep a pristine bundled baseline and merge local and upstream changes throughgit merge-file, with an atomic pre-update backup. Conflicts never put markers into a live config: they create*.oduflow-merge, preserve the old baseline, and exit non-zero. Existing customized installations without a baseline receive a conservative one-time*.oduflow-newsidecar.--forceremains automation-friendly but skips only confirmation,# KEEPremains a complete opt-out, and generatedpostgresql.confis now exclusively managed byretune-postgres. Seespecs/0047. (#194) -
Structured Odoo ORM tools — six new MCP tools give agents the semantics of standard Odoo XML-RPC
execute_kwwithout writing Python:odoo_search_read,odoo_create,odoo_write,odoo_unlink,odoo_call(public model methods other than those policy-visible CRUD mutations) andodoo_schema(paged model list +fields_get). They call/web/dataset/call_kwover HTTP from inside the environment's Odoo container, so they are independent of the routing mode and need no DNS, TLS or published port. Every tool takesas_user(a login or user id, empty = the environment's admin) and runs inside a real session for that user — access rights and record rules apply exactly as they do in the web client, whichrun_odoo_shellcannot do without hand-rollingenv(user=…)and XML-RPC cannot do without that user's password. The session is minted passwordlessly through the existing "Connect as user" mechanism and cached per environment and user. JSON arguments also accept Python literals, and Odoo-side failures (AccessError,ValidationError, …) come back as text with the server traceback instead of a masked tool error.run_odoo_shellremains the escape hatch for a fresh registry,sudo(), private methods, dry runs and multi-step transactions. Seespecs/0041. (#166) -
Module translation tooling — two new MCP tools cover the i18n loop.
export_module_translationsruns Odoo's own exporter with the environment's real addons path (so_()and_lt()messages are picked up along with database terms), writes the.pot/.pointo the module'si18n/directory under Odoo's own filename rule, and returns a per-type summary rather than the file.translation_statuslines up the module's terms, the translations actually stored in the database, and the committed.pofiles — reporting the two ways Odoo can fail silently: entries without a#:reference import as zero translations without a warning, and entries without a#. module:comment abort the import. When a sibling<module>.potexists, status models Odoo's automatic metadata merge before judging either defect. Both tools accept Odoo locale modifiers such assr@latin, handle catalogues up to their documented 5 MB limit, reject partial failed exports, and work across Odoo 15 through 19 — including 19's replacement of the--i18n-*options with theodoo i18nsubcommand — with no dependency on their.translationmodel removed in 16. (#169) -
One-time artifact download links — files an MCP tool generates inside an environment can now leave it without passing through the agent's context window. Over HTTP the tool returns a single-use
/oduflow-artifactURL (10-minute TTL) using the configured public hostname; under stdio it returns the checkout path or materializes a private process-lifetime temporary file when the source is a read-only/core module. (#169) -
translation_statusreturns a verdict, not three catalogues — the tool used to print what it measured: template, database and file counts plus a capped list of missing or stale msgids, which on a module whose Russian catalogue covered 3 of 442 terms meant roughly 450 lines of view bodies to restate what the first two numbers already said. The server holds all three sources and the inference is mechanical, so it now makes the call itself: each language is classified asOK,PARTIAL,NOT LOADED,NOT TRANSLATED,IMPORT SILENTLY DROPPED,IMPORT ABORTS,NO FILEorNOT ACTIVATED, ordered so failures that mask each other are reported in the order they must be fixed, alongside coverage against the module's own term count and the single call that moves that language forward. (#189) -
Agent guidance sharpened —
get_agent_instructionsreturned a reference manual whose workflow steps were buried under material an agent can discover from the tools themselves; the bundled guide is now trimmed to the workflow essentials and is loaded once per session rather than before every call. Agents inrepo_urlmode are told to publish the branch withgit push -u origin HEADbefore the firstcreate_environment, since Oduflow clones from the remote and cannot see a branch that only exists in the agent's local workspace (local_pathmode stays exempt). Teardown now states plainly that deleting a finished environment is the expected end of a task rather than an exception, so environments stop piling up until the idle reaper collects them. Andpsycopg2.pool.PoolError: The Connection Pool Is Fullfinally has a documented remedy — raisedb_maxconnonce (8 → 16, ceiling 32) in the container'sodoo.confand restart — with the boundaries spelled out: a recurring pool error means leaking cursors,FATAL: sorry, too many clients alreadyis the shared PostgreSQL limit where raising it makes things worse, and the container-local edit is lost on recreation, so permanent changes belong in.oduflow/odoo.conf. (#191, #192, #172, #178)
Dashboard¶
-
Template settings editor — a Template Settings modal inspects and edits a template's complete
metadata.jsonwithout filesystem access, through structured controls for the common fields while preserving unknown metadata and keeping raw JSON editing available for advanced repairs. Invalid metadata stays visible and repairable rather than hidden, writes are atomic with optimistic revisions, and template listing is serialized against metadata updates so a derived size refresh cannot overwrite an edit. Repository names containing quotes, backslashes, apostrophes or JavaScript prototype keys are handled safely. (#168, #186) -
The dashboard stays responsive during long operations — several blocking Docker, Git, backup and filesystem calls ran directly inside async handlers, so one slow request stalled the event loop for every other one. That work moved to the thread pool, empty and non-JSON gateway responses (a proxy's HTML timeout page, for instance) are parsed safely instead of crashing the UI, save-as-template shows elapsed progress, and Odoo.sh import staging — previously protected only by the blocked loop — is now explicitly serialized. (#174)
Bug Fixes¶
-
Declarative Stack plans now converge on the runtime they create — apply rejects unavailable database templates before creating anything and treats a changed sanitization policy as replacement-only drift. Service image defaults no longer look like undeclared environment variables, text files up to the documented 1 MB limit compare cleanly, route paths are canonicalized before duplicate checks and hashing, and
stack validateverifies every referenced local file. Environments created by the first Stack implementation carry a spec hash but no explicit sanitization label; an ambiguous mismatch there is now treated as replacement-only drift, so an unsanitized database can never be reported as converged. (#185, #187) -
OAuth clients see the tools again — in OAuth mode (Traefik routing or an explicit
oauth_base_url) every authenticated request was denied: the OAuth provider handed out the MCP SDK'sAccessToken, and FastMCP'sget_access_token()rejects that type, so the scoped-access middleware — which calls it to read a token's environment scope, and fails closed — returned an empty tool list ontools/listand refused everytools/call, on both/mcpand/mcp/<env>. The provider now issues FastMCP'sAccessTokenon every path. Deployments using static Bearer tokens were unaffected. (#184) -
A wedged Docker call can no longer take the server down silently — startup runs migrations,
init_systemand quotas before the HTTP listener binds, and docker-py disables the socket timeout while reading exec output, so a Docker daemon restarting underneath Oduflow (typicallyunattended-upgradeslettingneedrestartrestart Oduflow in the same batch ascontainerd) could block the start forever: anactive (running)unit serving nothing, for hours. Four changes close that hole. A startup watchdog aborts the process — after dumping every thread's stack to the journal — when startup emits no log line for 15 minutes, so systemd restarts instead of waiting; the PostgreSQL readiness probe now runs detached and pollsexec_inspect, making its 30-second budget real; startup first waits up to a minute for the daemon to answer; andoduflow systemd-installnow generates a unit ordered aftercontainerd.servicewithRestart=alwaysand no start-rate limit, plus an/etc/needrestart/conf.d/oduflow.confoverride that keeps Oduflow out of needrestart's automatic restart batches. Existing installs get the unit and override by re-runningoduflow systemd-install. (#177) -
http_request_to_odoonow requests the path you pass it — the base URL was taken fromget_environment_info()["url"], which ends in/web?debug=1, so appending the path buried it in the query string and every request silently hit/web. Both this tool and the environment readiness probe now build on the path-freeget_env_base_url, makingwait_for_odoo_readycheck the real/web/healthinstead of passing because the login page rendered. (#166) -
Deleting an environment no longer breaks the environment list — the Docker SDK expands a container listing by inspecting each result, so a container removed inside that window raised
NotFoundand took down environment listing and the container-statistics endpoint. Both paths now ignore containers that disappear mid-listing; the Docker SDK requirement moves to 3.3.0 or newer, whereignore_removedexists. (#193) -
Auto-install waits for the Odoo registry — Odoo's HTTP listener can answer
/web/healthbefore registry preloading finishes, and starting a second Odoo process at that moment races the serving one, which cloned Odoo 15 databases hit while recreating registry signaling sequences. Readiness is now a cookie-aware/web/login?db=…probe against the target database rather than a database-independent health check. (#176) -
Translation export creates a missing
i18n/directory — exporting a module's first catalogue wrote throughwrite_file_in_environment, whosemkdir -pran as the unprivileged Odoo user against a root-owned checkout and failed silently; the following archive upload then surfaced as a raw Docker -
Only the missing directory tree is now created as root and handed to the requesting user, and archive/ownership failures come back as ordinary
ExternalCommandErrorresponses. (#190) -
A truncated state file no longer crashes a tool —
activity._loadandport_registry._load_registrycalled.items()on whateverjson.loadreturned while catching only decode/OS errors, so a file holding valid JSON of the wrong shape (a truncated write leavingnull) raisedAttributeErrorstraight through the handler — abortingstop_environmentover a corrupt best-effort tracking file. Both loaders now check the parsed shape. (#165) -
Odoo.sh imports are resumable, and scoped access fails closed — a sweep of the actionable review findings from earlier pull requests: import retries no longer lose staged artifacts, template finalization is retryable with explicit addon wiring under a strict default and an opt-in best-effort policy, an incomplete addon template is no longer reported ready, scoped MCP/token handling fails closed instead of open, duplicate token scans and inactive dependency files triggering restarts are gone, and WebSocket validation, accessibility and CI concurrency are hardened alongside. (#182)
Documentation & Testing¶
- Documentation re-audited against the public code surface — the CLI, MCP,
REST/WebSocket, TOML, production, ORM, authentication, and PostgreSQL-tuning
references now match the implementation. The separate
oduflow upgradestep is documented, and invalid named-option examples foroduflow callwere corrected.llms-full.txtis now generated from every current manual page, with tests preventing undocumented commands, tools, routes, settings, or stale LLM output.oduflow upgrade --forcemakes bundled-file refreshes unattended while keeping warnings and# KEEPbehaviour, and Ruff is pinned to a stable lint profile enforced in CI. (#167) - Record the translation tooling decision — specs/0042 documents why the tools build on Odoo's own exporter instead of a hand-rolled term extractor, why one export primitive answers both "what is translatable" and "what actually loaded" across Odoo majors, and why retrieving a generated file needed a new one-time-token route. (#169)
- Mutation testing across the pure-logic core — a scoped
[tool.mutmut]configuration now covers 39 modules, with the tests needed to kill the mutants it surfaced: killed mutants go from 1553 to 6409 and uncovered mutants from 585 to 10, while the unit suite grows from 1249 to 1723 tests and still runs in about eight seconds. Almost every finding was a missing assertion rather than a bug — the one real defect it caught is the malformed-JSON crash above. A flaky chunkstore incremental-backup test that asserted on non-deterministic CDC chunk counts was rewritten around seed-independent invariants. (#165, #188, #179)
v1.68.1¶
Bug Fixes¶
- Translation changes now reach the database — a pull that only touched
i18n/*.powas classified as a plain refresh (or restart), so the new terms were never loaded: translation catalogs only enter the database through a module install or upgrade.classify_changes/shallow_classifynow classify a changed.poas an upgrade of the module that owns it, reported in the newdetails["i18n_changed"]list and merged across the main repository and the extra-addon worktrees..pottemplates stay ignored (Odoo never loads them), and a.poinside a module that is new in the same push keeps the stronger install action. The guardrail hint and the live-mount agent instructions — where the agent picks the action itself — now mentioni18n/*.poas well. (#164)
v1.68.0¶
Features¶
- Unified host resource planning — dev PostgreSQL, production PostgreSQL, and production Odoo workers now consume one host-wide CPU/RAM plan that accounts for whether production hosting is enabled. Generated PostgreSQL configs carry a fingerprint so resource or mode drift is reported without silently rewriting a live database;
oduflow retune-postgrespreviews every managed config diff, while--applybacks up and writes PostgreSQL configs, stages updated worker settings in existing Odoo containers, and leaves all restarts under operator control. Custom PostgreSQL configs require an explicit--force. (specs/0044) - Production hosting — Oduflow now runs long-lived Odoo productions alongside ephemeral dev environments. A production is a namespaced environment (
prod-<name>) with its own metadata plane: a per-teamproductions.jsonregistry, a dedicated production PostgreSQL cluster with auto-tuned settings and Odoo worker tuning, a custom-domain Traefik route, a full git clone, and a productionodoo.confchain (no--dev=xml, no sanitization). Deploys reuse the shared pull → classify → apply engine with production semantics and verify health in-container; a failed deploy automatically rolls the checkout and extra-addon worktrees back to their pre-update commits, androllback_productiontargets any commit in the recorded history. Backups are first-class:pg_dumpstreamed straight to S3, filestore snapshots through a new content-defined-chunking backup engine (chunkstore/), WAL-G WAL archiving with base backups and cluster PITR, plus a scheduler daemon for daily snapshots and weekly pruning. GitHub push webhooks (HMAC-verified, coalesced) auto-deploy matching productions, and a public/healthzendpoint reports dev/prod PG, Traefik, S3 and disk state. Seedocs/production.mdand specs/0031. (#116, #125) - Production hosting is opt-in — the production tier stays entirely dormant until it is enabled, so dev-only installs never grow a second PostgreSQL container. Re-enabling starts all managed productions, while an ordinary server restart no longer resurrects productions that were deliberately stopped. (#147)
- External Traefik routes and operator drop-in config — declarative
[route.<name>]TOML sections forward arbitrary domains to upstreams Oduflow does not manage, reusing the team TLS/ACME logic. The Traefik dynamic configuration is now a watched directory, so operators can drop in their own*.ymlfiles that Oduflow never overwrites; drift control recreates a Traefik container that is still on the old single-file provider. (#132, specs/0034) run_odoo_shellcommits by default —odoo shellrolls back its cursor when the piped script ends, so ORM writes made throughrun_odoo_shellwere silently discarded despite the docstring promising a commit. A newauto_commitparameter (defaultTrue) commits through a private cursor handle after the script succeeds; passauto_commit=Falsefor a read-only dry run. (#133)- Services report their reachable internal hostname — service tools now return the exact internal hostname and distinguish it from the external routed URL, including host-mode services. Odoo containers also receive a host-gateway mapping so
host.docker.internalresolves on Linux. (#163) - Richer coder shell toolbox — the coder image now ships
curl,less,ripgrep,fd,tree, andpython3+ruff, so a hosted agent can make ad-hoc HTTP calls and lint Odoo Python locally before pushing. (#157) - Versioned coder runtime contract — hosted agents now use the immutable
oduist/oduflow-coder:0.2.3image instead of a rolling:latesttag. The publish workflow emits only versioned multi-architecture tags, legacy official:latestconfiguration resolves to the release's pinned image, and Oduflow pulls a changed image before replacing a working container. Container recreation is derived from the actual Docker run specification rather than a manually bumped runtime epoch. - Shared immutable extra-addons checkouts — development environments using the same extra-addons commit now mount one persistent team-level checkout instead of materialising identical per-environment worktrees. Branch updates create a new SHA-keyed checkout and
pull_and_applyswitches only the requested environment, preserving independent database state; production keeps private worktrees for rollback. Existing environments migrate lazily on their next sync, and cached revisions remain until the extra repository is deleted. - Improved hosted agent runtime and MCP support — the hosted Agent CLI and Agent Chat runtime gained better MCP wiring, a fixed Codex Agent Browser environment, and a short
-talias for--transport. (#142)
Dashboard¶
- Production tab — productions are managed from the dashboard: status badge, custom-domain link,
branch@sha, deploy history, snapshots and restore, logs, the webhook box, and a create modal, plus health chips in the system bar. (#116) - Create templates from the dashboard — new templates can be created directly in the UI, with template saves serialized against environment operations. (#130)
- Agent Chat polish — activity details collapse, scroll position is preserved across updates, and file attachments (including cancelled uploads) are handled properly. (#150)
- Connect As rework — the user picker is split into dedicated Internal and Portal selects (portal membership resolved by xml_id rather than the imprecise
shareflag), and a single Connect button opens the environment already logged in. Login now works across environment subdomains in Traefik mode through a one-time host-bound token and a high-priority/oduflow-connectroute, andupdate_environmentrecomputes Traefik routing labels so a routing-mode, TLS, or hostname switch can be repaired without destroying the database. (#137, specs/0036) - Agent Chat conversation history — each environment and agent now keeps up to 20 recent conversations in MRU order, titled from the first user prompt. The new History menu resumes a selected conversation through ACP
session/load, preserves legacy sessions without a migration, and recovers safely when an adapter cannot load an older session.
Bug Fixes¶
- Live-mount project sanitization uses the active checkout — project scripts
now live under
.oduflow/odoo_sanitizeand resolve against the actuallocal_pathcheckout instead of the empty managed-clone location. Existing repositories keep running with an explicit migration warning, andget_environment_infoplus the dashboard now show the live-mount source path. - Agent Chat image uploads keep their filesystem path — small images still
reach multimodal agents as inline visual input, but now also retain the
persisted
/workspace/.oduflow-uploads/...resource link. Claude and Codex can therefore use the exact uploaded file with shell and Odoo tools instead of guessing temporary upload locations. - Hosted agents use the reachable scoped MCP endpoint — Agent CLI and Agent Chat now use the team's public HTTPS
/mcp/<environment>URL in Traefik mode (and an explicitoauth_base_urlin port mode), matching the dashboard's MCP Access dialog. Local port-mode deployments retain thehost.docker.internalfallback. - Claude Agent Chat explains rejected credentials — provider credentials copied into
[team.*.agent_env]now have surrounding whitespace removed before they reach the coder container. When Claude ACP returns a recognizable authentication failure, Agent Chat preserves the provider error and adds recovery specific to the active setup-token, API-key, or interactive-login mode. Oduflow still fails closed instead of silently switching accounts or billing methods. - claude.ai OAuth connector reaches the authorization endpoint — a claude.ai custom connector derives its OAuth endpoints path-relative to the MCP URL (requesting
https://<host>/mcp/authorizeand/mcp/token) instead of following the root endpoints advertised by discovery. The outer scoped-access shim treatedauthorize/tokenas an environment name and rewrote the request onto the auth-protected/mcpendpoint, so the flow failed with401 invalid_tokenbefore it could start. Oduflow now routes the reserved OAuth/discovery sub-paths requested under/mcp/(/mcp/authorize,/mcp/token,/mcp/register,/mcp/.well-known/*) to the real root routes. Scoped/mcp/<env>connectors remain Bearer-only. - Claude Agent CLI onboarding is pre-seeded — Claude Code onboarding state is written when an OAuth token or API key is configured, preserving valid settings and normalizing API-key approval so the CLI no longer stops on an interactive prompt. The resolved authentication mode (never the secret) is now logged on agent container creation, matching the documented troubleshooting flow. (#144, #146)
- OAuth refresh tokens survive access-token expiry — expired access tokens were pruned together with their non-expiring refresh partners, so refreshing failed after the access TTL or a server restart. Cleanup now removes only the access record while preserving pair revocation and rotation semantics. (#154)
- Concurrent Connect As requests no longer collide — every invocation gets a random temporary script basename and cleans up in a
finally, so simultaneous requests against one Odoo container can no longer overwrite or delete each other's script. (#153) - Server shutdown no longer hangs on idle terminals — the shell and SQL terminal websockets leaked a non-daemon executor thread parked in
recv(), blocking interpreter shutdown and leaving the exec'dodoo shell/psqlprocess running. Teardown now mirrors the agent console pattern: return on first completion, shut the Docker socket down to wake the blocked thread, then cancel and reap. (#151) - Template dumps are cleaned up and concurrency-safe — every dump copied into the shared database container's
/tmpis now tracked and removed in afinally, and staged under a unique per-restore name, so parallel imports cannot clobber each other and theoduflow-dbwritable layer stops growing without bound. (#139, #140) - Extra addon branches are validated up front — a blank or missing branch is rejected with an actionable prerequisite error naming the repository, before
git worktreeis invoked. (#141) - Service errors are actionable — a missing image or other Docker pull failure now surfaces as a safe, specific error across REST and MCP while preserving the existing container, and service names Docker cannot use are rejected with a
400at the shared container-name boundary instead of a raw Internal Server Error. (#158, #161) - Quiet benign MCP disconnect logs — a client disconnecting before a long-running tool finishes no longer produces an ERROR "Stateless session crashed" traceback; genuine crashes still propagate. (#138)
- Agent Chat assets are cache-busted correctly —
chat.jsandacp-client.jsnow share one cache version held in the dashboard template, so a single bump busts the interdependent pair and users can no longer end up on a freshchat.jspaired with a staleacp-client.js. (#148, #149)
Security¶
- Live-mount startup warning — every server start with
allow_local_path=truenow warns that environment creators can bind host directories read/write and that hosted, remote, and multi-user deployments should disable the trusted local-development mode. - OAuth
client_idis no longer the secret — a team's OAuthclient_idis now the non-secret identifierteam_<id>(e.g.team_1); the team'sauth_tokenis theclient_secretand remains the issued access token. Previouslyclient_id == client_secret == auth_token, so the secret appeared in the/authorizequery string (and thus in server logs, browser history, and theReferer). The secret now travels only in the POST/tokenbody. Breaking: re-enter any existing claude.ai connector asClient ID = team_<id>,Client Secret = auth_token. - OAuth issues independent, expiring, revocable tokens — completing the previous item, the OAuth Authorization Code / refresh exchange now mints an independent, opaque
access_token(with a rotating refresh token) instead of handing back the team'sauth_token. A minted access token expires (~1h) and can be revoked via the now-enabled/revokeendpoint; using a refresh token rotates the pair so a leaked refresh token is single-use. The OAuth client (claude.ai/IDE) therefore never stores the team's long-lived master secret. Minted tokens are persisted (oauth_token_store.py) so live connections survive an Oduflow restart. Theauth_tokenstill works as a non-expiring direct Bearer credential for curl/CLI, and per-environment tokens remain Bearer-only. (#83, #135) - Hardened git credentials, import tokens, CSRF and Web UI auth — inline URL credentials are redacted from git stderr and are no longer written into the world-readable
oduflow.repoDocker label (they move into the team.git-credentialsstore, created0700/0600); import-from-Odoo endpoints accept the bearer token only via theAuthorizationheader and require HTTPS, since the request carries the Odoo master password;BasicAuthMiddlewarerejects cross-site state-changing requests and every WebSocket handshake via an Origin/Referer check; and the Web UI now default-denies with401when auth is enabled but no team resolves, instead of silently acting as team 1. Also fixes an output-cache id collision and a naive-timestamp timezone bug, and rejects environment names that slugify to an empty string. (#136) - Secrets kept off the process list and out of error pages — the per-environment database password is passed to
run_odoo_tests/run_odoo_shellviaPGPASSWORDinstead of-w <password>on the Odoo CLI (visible viapsinside the container), and the dashboard's HTTP 500 handlers return a generic message instead ofstr(e), which could leak absolute paths and Docker internals. Details are still logged server-side; MCP responses,FlowErrormessages, 400 validation errors, and interactive terminals stay verbose. (#84, #134)
Documentation & Testing¶
- mypy strict is green and gated in CI —
strict = truehad been configured from the first commit but never ran in CI and emitted 391 errors. All findings are fixed (including several dishonest return annotations and a namespace-package shadowing of thedockerSDK by the repo's owndocker/directory), the type-checker version is pinned via adevextra, and amypyjob now runs on push and PR. (#131) - Production hosting documentation — specs/0031 records the decision,
docs/production.mdcovers configuration, deploys and rollback, webhooks, snapshots and restore, the cluster PITR runbook,/healthz, and the tool table; the bundledoduflow.tomlgains commented[production]and[backup]sections. (#116) - First-run credentials and config paths — the legacy
~/.oduflow/oduflow.tomlfallback is removed and the remainingODUFLOW_TOML→/etc/oduflow/oduflow.toml→~/.oduflow/conf/oduflow.tomllookup order is documented, so first-run users know where the generatedauth_tokenandui_passwordlive. (#152) - Record the versioned coder-image contract — specs/0040 documents immutable image publication, server/image version coupling, safe replacement ordering, and removal of the rolling
:latestchannel. - Document the short transport flag — HTTP server startup examples now show
oduflow -t http/uvx oduflow -t httpas the short form of--transport http. - Record the shared extra-addons cache decision — specs/0039 documents SHA-keyed checkout sharing, isolation from moving branches, persistent cache lifecycle, and why production remains on private worktrees.
- Document the agent UI live-mount limitation — the dashboard agent UI cannot be used with live-mounted (
local_path) environments; this is now stated explicitly in the docs.
v1.67.0¶
Features¶
- Restricted HTTP path routing for services — auxiliary services can now expose a curated set of
routesalongside (and mutually exclusive with) the existing catch-allportmodel. Restricted routes use segment-safe Traefik rules, work for both bridge and host networking, can optionally strip prefixes, and leave every undeclared path at Traefik's 404 so a service no longer has to publish its whole HTTP surface. The route configuration is preserved through presets, live inspection, updates, MCP, REST, and the dashboard. (#126)
Bug Fixes¶
- Dependency changes are applied on Sync — a changed
requirements.txt,.oduflow/requirements.txt, or.oduflow/apt_packages.txtduringpull_and_apply/Sync now reinstalls the apt/pip dependencies into the running container and restarts it, instead of being misreported as an XML/JS-only browser refresh. The classifier previously ignored these files, so a dependency-only change silently did nothing. Reinstall runs before any module install/upgrade so new libraries are importable; packages removed from the file are not uninstalled until the container is rebuilt viaupdate_environment. (#127) - Real client IPs behind Traefik — access logs and the login rate-limiter recorded the Traefik container IP for every request instead of the real client, because uvicorn only trusts
X-Forwarded-Forfrom127.0.0.1by default. Oduflow now passesproxy_headers=Truewith aforwarded_allow_ipstrust list — the stable Traefik Docker-network CIDRs intraefikmode, and nothing inportmode so a direct client cannot spoof its IP. The login rate-limiter now throttles per real client. (#123, #124) - Bounded MCP HTTP graceful shutdown — HTTP transport now applies a 10s Uvicorn graceful-shutdown timeout so long-lived MCP StreamableHTTP streams no longer keep server restarts waiting indefinitely. (#122)
Security¶
- Hardened proxy-header trust and credential redaction — forwarded headers are trusted only from loopback plus the stable Traefik Docker-network CIDRs, failing closed for empty team maps and when Docker-network trust cannot be established; embedded repository credentials are now redacted from non-auth git clone failures; and a staged template filestore replacement that fails to install preserves the previous filestore. (#124)
Documentation & Testing¶
- Restricted service-routing decision record — specs/0033 documents the restricted HTTP path-routing model for services, its security boundary, and the trade-offs versus catch-all port exposure. (#126)
- Superseded PR test runs are cancelled —
tests.ymlnow uses a per-refconcurrencygroup withcancel-in-progress, so pushing a new commit to an open PR cancels the still-running test workflow for that ref while keepingmainprotected by pre-merge CI. (#128)
v1.66.0¶
Features¶
- Traefik certificates available to auxiliary services — in Traefik TLS mode every created or recreated service receives the exact
oduflow-traefik-acmevolume at/etc/traefikread-only, with no per-service TLS flags or wildcard allowance for other system volumes. Existing services gain the mount on their next update. - Database-only Odoo template import —
oduflow import-templateandimport_template_from_odoonow accept--without-filestore/without_filestore=True, requesting a PostgreSQL custom-format dump without filestore files and deriving template metadata after restore. - Attach separate template filestores — new
oduflow attach-filestoreCLI andattach_filestoreMCP tool attach or replace a template filestore after a database-only import. Sources can be local directories, zip/tar archives,rsync://URLs, or SSH rsync paths; wrapper directories such as the database name are auto-detected and live overlay env changes are preserved by default.
Bug Fixes¶
- Invalid service volume updates are non-destructive —
update_servicenow resolves the complete candidate volume configuration before stopping the running container, so a missing or reserved volume no longer deletes the service before returning an error. - Generated PostgreSQL passwords are safe for the Odoo entrypoint — per-environment passwords that randomly began with
-were parsed as another CLI option, leaving--db_passwordwithout a value and putting the Odoo container into a restart loop. Password generation now retries that rare token shape. - Odoo template import now refuses existing template names —
import-templatefails before DB listing or backup download if the target template directory or template database already exists, avoiding accidental overwrites. - Database-only template import can restore newer custom dumps — when the shared PostgreSQL container's
pg_restorecannot read a custom dump archive version, Oduflow converts it to plain SQL with a temporarypostgres:17helper container and restores that SQL through the shared database container.
Documentation & Testing¶
- Implicit service certificate-store decision record — specs/0032 records the deliberate shared-read, read-only ACME mount and its trust-boundary consequences.
v1.65.0¶
Features¶
- Per-team OAuth issuer with host-relative discovery — in traefik mode, each team's OAuth Authorization Server derives its issuer from the incoming Host, so each team runs OAuth on its own already-certificated hostname with no need for a central
oauth_base_url.OduflowOAuthProvidermakes discovery metadata host-relative,HostRelativeAuthChallengerewrites 401 challenge origins, and[routing].hostnameis now validated per-mode (shared default deleted in traefik). (#112)
Security¶
- Hardened web-UI and MCP surfaces — fresh installs and upgrades now bootstrap a
ui_passwordso the dashboard is never served unauthenticated; the interactive shell, SQL, agent, and service-creation surfaces are fail-closed. Closed two SSRF vectors (http_request_to_odoopath rewrite andsetup_repo_authloopback/link-local validation), removed the shared-PostgreSQL-superuser fallback so tenants cannot cross databases on legacy envs, added validation guards and escaped output across the surface. (#110) - psql WebSocket branch validation —
ws_sql_terminalnow validates the branch path param before lookup to close an opaque resolution path in scoped-credentials. (#114)
Bug Fixes¶
- Overlay filestore no longer duplicated into each environment —
_ensure_user_site_packageswas recursively chowning.local/share, causing fuse-overlayfs to copy the entire template filestore (~10 GB) into each env's upper layer, defeating overlay space savings. Now chowns only the pip dirs non-recursively, keeping filestore overlay-bound. (#113) - Overlay unmount now works on Ubuntu 24.04 with enforced fusermount AppArmor profile — switched mount cleanup to try clean
umountfirst (not mediated by the fusermount3 profile on Ubuntu 24.04+), falling back tofusermount/fusermount3helpers and lazyumount -lonly as a last resort. (#113) - Multi-team UI password provisioning for newly-added passwordless teams —
_ensure_web_ui_passwordwas usingany()so a multi-team config with one team already set would skip provisioning for others, locking them out. Now usesall()to provision every team and enforce that all are set at startup. (#114) - Template clone now reassigns objects owned by any non-env role — when creating an environment from a template imported via
import_template_from_odoo, objects owned by roles other than the template env's own role were not reassigned, leaving the new env unable to touch them. Now reassigns all non-env-role-owned objects unconditionally. (#111) - Native Odoo neutralize skipped for Odoo 15 — the official
odoo:15.0image does not include theodoo neutralizeCLI, so Oduflow now skips it for Odoo 15 and tries only for Odoo 16+, keeping custom.odoo_sanitizescripts as the baseline. (#109)
Documentation & Testing¶
- Per-team OAuth decision record — specs/0020 captures the evolution from a static
oauth_base_urlto host-relative issuer discovery. (#112)
v1.64.0¶
Features¶
- Odoo.sh addons-path import — extend push-based import to carry over the addons-path Odoo.sh ran with, supporting Enterprise, Themes, and extra repos. The client detects the live addons-path, classifies each entry, tars private repos and announces reachable extras to the server. Also adds
rename_templateMCP tool, REST endpoint, and dashboard button to rename template directories and their PostgreSQL backing databases. (#108) - Chunked Odoo.sh import uploads — split large SQL dumps and addon tarballs into resumable chunks to pass proxy body-size limits (e.g., Cloudflare's 100 MB cap). Resume state is derived from what is already staged on disk. (#108)
- Local extra-addons repos —
extra_addons.create_local_reposeeds a remote-less bare repo from files with a.localmarker; fetch short-circuits so worktree creation never attempts a remote pull. (#108)
Dashboard¶
- Import acknowledgement gate — Enterprise/Themes/Extra addon checkboxes now gated behind a licensing acknowledgment, linking to
oduflow.dev/odoo-sh-import-notesfor documentation. (#108)
Documentation & Testing¶
- Odoo.sh import decision record — specs/0030 captures the architecture and rationale for the resumable, push-based import model. (#108)
v1.63.0¶
Features¶
- Native Odoo database neutralization — sanitized template-based environments now run Odoo's own
odoo-bin neutralizeinside the serving container after auto-installed modules are present, then continue through the existing custom.odoo_sanitizescripts. This gives copied databases Odoo's module-aware baseline protections for outgoing mail, crons, payment providers, connectors and webhooks without adding a new config knob; failures remain warning-only so provisioning is not blocked. (#107)
Dashboard¶
- Unlicensed badge signal — the unlicensed license badge now plays a brief red edge pulse and jolt once on dashboard load as a registration nudge, while keeping the text as the actual status signal and skipping the animation entirely for reduced-motion users. A follow-up tightened the animation cleanup path so reduced-motion sessions do not retain unused listeners. (#104, #105)
Documentation¶
- Release workflow captured — a project-scoped
/publish-releaseskill now records the Oduflow release flow: analyze commits since the last tag, propose the version/changelog/title for sign-off, bump and push the release commit, publish the GitHub Release that triggers PyPI and Docker, then verify artifacts. (#106)
v1.62.0¶
Features¶
- HTTP-only Traefik mode — a new
[routing].tlstoggle (bool, defaulttrue). Withmode = "traefik"andtls = false, Traefik listens on plain HTTP:80only — no HTTP→HTTPS redirect and no Let's Encrypt/ACME — for running behind an upstream that already terminates TLS (e.g. a Cloudflare tunnel). Environment/service routers bind to thewebentrypoint without TLS, theacme_emailrequirement is lifted, andforwardedHeaders.insecurelets the upstream'sX-Forwarded-Proto: httpsthrough so cookies staySecureand public URLs stayhttps://._ensure_traefikself-corrects on a flippedtlssetting by recreating the container. (#103)
Bug Fixes¶
- Agent checkout removal guard —
_agent_remove_envnow refuses when an environment name slugifies to an empty string, which would resolve the checkout dir to/workspace/itself and letrm -rfwipe every environment's checkout from the shared volume (matching the guardclone-env.shalready had on the create side). (#100) - Agent session file permissions —
agent_sessions._savenow creates its temp file0600from birth viaos.open(the mode carries over throughos.replace), closing the brief default-permissions window that existed with chmod-after-rename. (#100) - Surfaced degraded agent modes — non-fatal
_chat/noticeframes now render as system lines in Agent Chat, making two previously silent degraded modes visible: an environment without a scoped MCP token, and Codex chat's missing Oduflow MCP wiring. (#100)
Documentation¶
- Docs synced with code — a documentation audit corrected several drifts:
auto_delete_hoursdefault corrected to0(opt-in) across pages;[server].allow_local_path,allow_insecure_httpand[routing].hostnamedocumented; and the v1.61.0 coding-agent feature fully documented (newdocs/agent.md, installation/web-api pages,llms.txt/llms-full.txt). (#101, #102)
v1.61.0¶
Features¶
- Per-team coding agent — Agent CLI & Agent Chat — an opt-in hosting feature that runs one coding-agent container per team (
oduflow-{team}-agent) on the team's isolated network, with persistent HOME/workspace volumes and one git checkout per environment. The agent drives environments only through the Oduflow MCP server (git push →pull_and_apply). Two front-ends ship: Agent CLI, the agent's own TUI in xterm.js over a WebSocket ↔docker execbridge; and Agent Chat, a framework-free browser client talking to the agent's ACP adapter, with a durable session per (environment, agent) that resumes via ACPsession/loadand minimizes to a dock so several chats run in parallel. Configured statically inoduflow.toml(agent_enabled,agent_default,[team.X.agent_env]); the container carries a config-hash label and is recreated automatically on drift. The agent UI is hidden for live-mount environments. (#99) - Scoped per-environment MCP tokens for agents — the team
auth_tokennever enters the agent container; each console/chat session injects the environment's scoped/mcp/<env>token (default-deny dev-loop allowlist) into its owndocker execenvironment via a${ODUFLOW_MCP_TOKEN}placeholder, so no secret lands on disk and a leaked session credential grants only the one environment it already controls. (#99) - Coder image (
oduist/oduflow-coder) — a new merge-gatedpublish-coder.ymlworkflow publishes the agent image, which redistributes only Apache-2.0 software (Codex CLI + codex-acp); Claude Code and its ACP adapter are installed at first container start onto the persistent home volume. (#99)
Bug Fixes¶
- Traefik dynamic config now actually loads — the per-team hostname→dashboard routing config was written as
.json, which Traefik's file provider rejects ("unsupported file extension"), so it never loaded. It is now written asoduflow.yml(JSON is valid YAML, so no new dependency), placed underetc_dir(/etc/oduflowwhen writable, else~/.oduflow/conf) with the parent directory created first so a first traefik-mode start no longer crashes on macOS. Migration 0005 removes a still-running Traefik container that mounts the old.jsonpath (the ACME volume persists, so certificates survive) and system init recreates it with the corrected path. (#98)
v1.60.0¶
Features¶
- Scoped single-environment MCP access — a new
/mcp/<env>endpoint exposes only the in-environment dev-loop tools (sync, install/upgrade modules, tests, Odoo shell, SQL, file read/write/search, HTTP request, logs, info, restart) and denies every lifecycle/system tool. Access is isolated by a per-environment token generated at create time and stored in theoduflow.mcp_tokencontainer label; the Secret Key works as a Bearer token or an OAuth client credential. A dashboard More → MCP Access modal surfaces the URL and Secret Key. (#96)
Dashboard¶
- Minimize-to-dock windows — the Log, Console (Odoo shell) and SQL Console modals gain a minimize button that docks the window as a chip in the system bar. Minimizing only hides the overlay, so the WebSocket, xterm buffer and log state stay alive and restoring resumes the exact session; reopening a window of the same type replaces the docked session without leaking its WebSocket, and focus returns to the launching control across the cycle. (#95)
Documentation¶
- Per-version Odoo development guides (15–19) —
get_odoo_development_guidenow serves concise, self-contained per-version cheat sheets covering the key conventions and breaking changes for each release, plus a "Migrating a module to this version" block (migration scripts, pre/post split, openupgradelib helpers) for 16–19. Version-boundary facts were verified againstodoo/odoo16.0–19.0. (#97)
v1.59.0¶
Features¶
- Multi-tenant hosting: hard tenant isolation — a hosting-grade tenancy pass so environments running arbitrary client code cannot reach across teams. Container names are now team-scoped (
oduflow-{team}-{env}-{type}andoduflow-{team}-svc-{name}); each team gets its own Docker network (only the shared PostgreSQL and Traefik bridge across teams), its own PostgreSQL tablespace (oduflow_team_{id}), and default per-container memory/pids limits auto-derived from host size. Odoo-style startup migrations (recorded inmigrations.json) retrofit existing installs in place — renaming containers, moving tablespaces, and re-homing networks — with no manual steps and a clean resume after a partial run. (#91) - Per-team quotas —
db_quota_gb(default 50, 0 = off) caps the combined size of a team's PostgreSQL databases, checked before any operation that creates a new database;disk_quota_gb(default 0) is enforced on XFS with project quotas, giving a team's files and its PG tablespace one project ID and a single kernel limit. (#91) - Per-environment usage stats — environment cards show DB size / disk usage after the CPU/RAM stats with a per-card refresh control, backed by a cached storage-stats subsystem and a
GET /api/usage+POST /api/usage/refreshREST surface that external billing/quota tooling can consume per team. (#91) - Strict HTTP team resolution — the single-team fallback now applies to stdio only; in HTTP mode an unresolved request is rejected unless
allow_insecure_httpis set, and multi-team HTTP requires anauth_tokenfor every team at startup. (#91)
Bug Fixes¶
- Review cleanups — dropped dead team-guard branches now that
teamis a required argument, extracted the duplicated Traefik-label helper to module level, andfsyncbefore the atomic storage-cache rename so a crash mid-write cannot leave an empty file. (#93)
v1.58.0¶
Licensing¶
- Updated license terms and commercial plan descriptions. (#90)
v1.57.0¶
Features¶
- Neutral dashboard create buttons — the
.btn-createprimary buttons move from a solid Console Blue fill to the outline treatment (surface fill, hairline border, ink text) that reveals a blue border, text and 10% tint on hover/focus, matching the Sync/Logs actions; weight 600 and the larger radius keep them distinguishable without a resting fill. The destructive confirm keeps its solid red. (#87)
CI¶
- Automatic Docker image publishing — a new
docker.ymlworkflow builds and pushesoduist/oduflow:<version>plus:latest(linux/amd64 + arm64, via Buildx/QEMU) to Docker Hub on every published GitHub Release — the same trigger as the PyPI publish. The version comes from the release git tag;latestis skipped for pre-releases. (#88)
v1.56.0¶
Features¶
- Import templates from Odoo.sh — Odoo.sh blocks
pg_dumpand/web/database/manager, so import is inverted: a push-based, resumable shell client (served at/import-odoo.sh, launched from the dashboard "Import from Odoo.sh" button) rides the platform's own daily backup, streaming the SQL dump as-is and the filestore tar'd per hash-directory chunk to short-lived (15 min), token-authed ingest endpoints that stage and restore it through the existing template machinery. Resume state is derived from what is already staged on disk, so a fresh token minted after the previous one expired continues the transfer where it stopped. (#85)
Security¶
- Odoo.sh import hardening — closed an auth-bypass where the public ingest prefix also exposed sibling routes like template deletion; uploads now land in a staging dir (no fake-resume over an existing template, no writes into a mounted overlay lower layer); chunk extraction is atomic (a truncated upload is never treated as finished on resume); and the client resolves http→https redirects up front, streams with
curl -Tinstead of buffering whole payloads in RAM, and fails fast on non-JSON responses instead of crashing with a raw traceback. (#89)
Dashboard¶
- Import from Odoo.sh modal — widened to 80vw, with the input placeholder updated to
oduist-prod. (#92, #94)
v1.55.0¶
Security¶
- Security & bug audit remediation (17 fixes) — a broad hardening pass across the tenancy and request surfaces:
template_nameis now validated at both the filesystem and PostgreSQL-identifier sinks (blocking path traversal and SQL injection); env DB roles no longer receive cluster-superuser membership (closing aSET ROLE→COPY … TO PROGRAMRCE) and the post-clone ownership transfer was made comprehensive; reservedoduflow-*volumes can no longer be mounted by a service; SSRF guards were added forimport_from_odooand git remote URLs; zip-slip/tar extraction is now containment-checked;get_clientraises a catchableFlowErrorinstead ofSystemExit; the web create handler no longer releases a lock it does not own; and overlapping team port ranges are rejected inSettings.validate(), with constant-time secret compares and atomic credential writes. (#82)
Bug Fixes¶
- Destructive auto-delete off by default —
auto_delete_hoursnow defaults to0(opt-in) so a stopped environment is never silently deleted; the non-destructive 48h auto-stop stays on, and enabling auto-delete logs a prominent warning naming the consequence and how to disable it. (#82) oduflow cleanup --forceactually removes orphans —cleanup_orphansnow passes the per-team settings to_unmount_filestore, fixing anAttributeErrorthat was silently swallowed while the command reported success but removed nothing. (#82)
v1.54.0¶
Bug Fixes¶
- Repo
odoo.confchanges now actually apply — a changed.oduflow/odoo.confis now reconstructed (mergedaddons_path, strippeddb_*) and copied into the container before the restart duringpull_and_apply, in both the restart and upgrade-then-restart paths. Previously (#69) it only triggered a plain restart, which reused the stale/etc/odoo/odoo.confcopy and silently ignored the new config; the regeneration only ever ran on a fullupdate_environmentrecreate. (#81)
v1.53.0¶
Features¶
addons/sub-directory auto-detected — when the main repo keeps its modules under a top-leveladdons/directory, the generatedodoo.confnow pointsaddons_pathat/mnt/extra-addons/addonsinstead of the repo root. Odoo scansaddons_pathnon-recursively, so the root mount alone would miss modules nested underaddons/; detection is automatic and falls back to/mnt/extra-addonsotherwise. (#80)- Complete bundled config template — the shipped
oduflow.tomlnow includes every optionSettings.from_tomlreads, adding the[lifecycle]section (auto_stop_hours,auto_delete_hours) and a commentedrouting.hostnamefallback, so no knob is left undocumented. (#86)
v1.52.0¶
Dashboard¶
Createdlabel for services — service cards now show a Created timestamp. (#79)
Documentation¶
- Release process — documented the mandatory GitHub Release step (the event that actually triggers the PyPI and Docker publishes) in
AGENTS.md.
v1.51.0¶
Features¶
- Auto-generated DB superuser password — on a fresh install the bootstrap injects a random
secrets.token_urlsafe(24)password into the generatedoduflow.toml[database]section, so every install gets a unique PostgreSQL superuser secret. The bundled template no longer ships the hardcodedpassword = "odoo"; an explicit[database].passwordstill overrides, and existing configs are never rewritten. (#74) - Auto-tuned PostgreSQL config — first-time system init now generates
postgresql.conffrom the host's detected CPU/RAM (newpg_tune.py) instead of copying a static file hardcoded for "2 vCPU / 4 GB / HDD". The tuning is lean and SSD-oriented for a host running many lightweight single-user Odoo environments (e.g.shared_buffers≈ 10% of RAM, floored at 128 MB and capped at 1 GB); the generated file is marked# KEEPsooduflow upgradenever overwrites it, and falls back to the static bundled config on any failure. The templatedb_maxconnwas lowered from 16 to 8. (#67) - Restart on repo
odoo.confchanges — changes to the repository-levelodoo.confnow trigger an environment restart duringpull_and_apply. (#69) - Version logging on init — the Oduflow version is now logged during system initialization. (#70)
Dashboard¶
- Remove templates — each card in the Templates tab gains a Remove action (with a danger confirm dialog), wired to a new
POST /api/templates/{name}/deleteendpoint. (#76) - Wider logs modal + wrap toggle — the logs modal now fills 80vw instead of being capped at ~900px, and a new "Wrap" toggle switches long log lines (tracebacks, SQL) between no-wrap and soft-wrap to avoid horizontal scrolling. (#75)
- Calmer template badges — the Templates tab's readiness badges are toned down (lower-saturation green fill plus a hairline border) and the
Copy/Overlaymode badge is now a neutral gray chip rather than a success-green one, ending the eye-straining "wall of green". (#77) - Branch-header commit & PR links — the Pull Requests and Commits icon-links moved from the variable-length Repo URL line into the card header next to the branch name, giving them a stable, easy-to-find position. (#73)
- Copyable container names — running-environment container names are now click-to-copy via the existing clipboard affordance.
- Stopped status + service env vars — environments whose containers are all stopped now report a distinct "stopped" status (amber) instead of "partial", and the redundant exited-container row is dropped; service environment variables moved out of the card into an "Info" modal that lists the real (unmasked)
KEY=valuepairs. (#66)
Bug Fixes¶
oduflow callasync tools — the CLIcallsubcommand printed<coroutine object ...>(and a "coroutine was never awaited" warning) for tools wrapped by@handle_errors; their awaitable results are now run to completion viaasyncio.run(). Sync tools are unaffected. (#68)
Documentation¶
- Architectural decision records — the project's macro-level decisions were reconstructed from git history into ADR-style records under
specs/(0001–0022) with a chronological index, and a "Record Architectural Decisions" rule was added toAGENTS.mdso future decisions are captured alongside the change that introduces them. (#72)
v1.50.5¶
Features¶
- Light theme — the dashboard and login page gain a light theme alongside the dark "Engineer's Console" field. It follows the OS (
prefers-color-scheme) by default; a header toggle cycles System → Light → Dark and persists the choice tolocalStorage, applied before first paint so there is no flash. It is built by re-pointing the existing:rootcolor tokens — signal colors switch to AA-verified darkened variants on the light field, while the embedded terminal stays dark — and the logo is now transparent so it reads cleanly on either field. - Larger dashboard type scale — 68 hardcoded
pxfont sizes were replaced with six semantic rem tokens (--text-2xs…--text-xl), raising every tier ~+2px (body 13→15px, titles 15→17px, header 20→23px) for readability at density. (#63) - Note dialog Delete button — the environment note editor now has an explicit Delete action. (#63)
v1.50.4¶
Features¶
- MCP bootstrap instructions — the MCP initialize response now tells agents to load
get_agent_instructionsfirst and then callget_odoo_development_guide(version=...)before writing Odoo module code.
v1.50.3¶
Features¶
- Git-independent live-mount apply flow —
local_pathenvironments now use an Oduflow-owned per-environment file snapshot instead of Git status to detect local changes. Git commits are optional in live-mount mode, non-git folders are supported as-is, and already-applied dirty/untracked files are no longer reported repeatedly. - Config-gated live-mounts — added
[server].allow_local_path(default:true) to enable the single-developer live-mount workflow by default while still allowing operators to disable local bind mounts explicitly. - Mode-aware agent instructions —
get_agent_instructionsnow starts with a dynamic code-delivery-mode preface. When a live-mounted environment is active, agents see the local workflow first, including the explicitinstall/upgrade/restartrules that apply to snapshot-based detection.
Bug Fixes¶
- Live-mount templates in Web UI — environments created from templates that record
local_pathcan now be recreated as live-mounts whenallow_local_pathis enabled, instead of always being rejected as HTTP-only. - Local
pull_and_applyrepeat detection — snapshots advance only after successful apply operations, and stay unchanged when strict guardrails block or install/upgrade fails.
Documentation¶
- Agent guide local workflow — clarified the split between
repo_urlmode (commit→push→pull_and_apply) andlocal_pathlive-mount mode (edit local files directly; commits optional; pass explicit actions for database-affecting changes).
v1.50.2¶
Bug Fixes¶
- License config path — license activation now stores
license.keyin the resolved Oduflow config directory instead of hard-coding/etc/oduflow; when/etc/oduflowis not writable it follows the same~/.oduflow/conffallback as the rest of the config. The dashboard license API now uses the running server settings, so the unlicensed badge still appears when no key is installed. (fe142ec) - CLI startup warning — suppress the noisy Authlib deprecation warning emitted by FastMCP on startup. (44872e8)
Documentation¶
- MCP tool count — correct the documented MCP tool count from 43 to 54. (513a53d)
- Release process — document that version and changelog updates must be committed before creating the release tag. (9371be5)
v1.50.1 (since v1.20.1)¶
Breaking Changes¶
rebuild_environmentrenamed toupdate_environment— the tool (and its REST route/api/environments/{branch}/update) now also acceptsodoo_imageandenv_varsto switch the image and replace container environment variables. Called with no arguments it behaves exactly like the oldrebuild_environment(re-create the container, preserving DB and filestore). (a13c73a)- Removed
template-up/template-downCLI commands (8342768)
Features¶
- Stdio live-mount + explicit
pull_and_applyguardrail —create_environment(local_path=...)bind-mounts the agent's own checkout live into the container (stdio transport only), so edits apply instantly with no git push/pull;pull_and_applyis transport-agnostic and takes explicitinstall/upgrade/restartactions, with a guardrail that cross-checks the requested action against the detected diff and surfaces non-blocking warnings (aca9445) - Environment lifecycle automation — idle environments auto-stop and long-stopped environments auto-delete after configurable hours (protected environments are exempt); container-level tools auto-wake a stopped environment and prepend a note; activity is tracked per team in
activity.json(e992971) - Environment variables for environments —
create_environmentacceptsenv_vars(comma-separatedKEY=VALUE) injected on top of the database connection variables;update_environmentcan replace them later. Env vars are persisted on the container, reported byget_environment_info, and editable from the web dashboard create form (a13c73a) - Self-hosted OAuth Authorization Server for MCP clients (5f32f58)
- GitHub OAuth support for MCP HTTP transport (97c3fc8)
auto_install_modulesparameter for environments and templates (e868d90)- Docker volume management — manage persistent service data via Docker volumes, mount external Docker volumes to services, and browse/manage files inside volumes (6b05f8a, a396583, 5cabc2c)
- Host network mode for services (d4609e6)
privilegedandNET_ADMINcapability options for services (edad2ef)restart_serviceandrun_service_commandtools with increased summary limits (a2fa612)update_servicecan change any setting — env, image, port, hostname, host_mode, volumes, privileged, and net_admin (970aba4, 5bb3f7e)get_service_infotool (27ec4c9)- Non-destructive template update for fuse-overlayfs environments (1edafff)
reload-template --sourceflag for S3/local sync (2e2e0cd)# KEEPmarker to protect files from being overwritten during upgrade (a97e13f)- Read repo-level
odoo.conffrom.oduflow/directory (48eea3e) created_attimestamp, editable notes, and preset-based service updates (b849551)- odoo.conf
db_maxconn=16— bumped from 4, which was too small (90e7e19)
Dashboard¶
- Engineer's Console redesign — visual system aligned with oduflow.dev (OKLCH tokens, Outfit + Geist Mono, signal palette); all assets vendored (xterm.js and fonts, no CDN, works air-gapped); state-driven Start/Stop toggle; in-system dialogs replace native
confirm()/prompt(); accessibility (focus traps, ARIA roles, keyboard navigation, ≥44px touch targets); responsive down to 390px; single polling interval; real RAM/CPU metrics on macOS (e992971) - Session-cookie auth with login form and logout — replaces the HTTP Basic browser dialog; signed
itsdangeroussession cookies (7-day expiry, persistent server-side secret); Basic auth retained for API/CLI clients (9e24ab2) - Show git branch in env list when it differs from the environment name (ae5441d)
host_modecheckbox on service creation and restore forms (73fbee7)- PR and commits icon-links next to the repo URL (25017cd)
- Copy DB name on click (7f8d654)
Bug Fixes¶
- WebSocket terminal auth — Console/SQL terminals failed because browsers can't send a Basic auth header on a WS handshake; added a signed cookie auth fallback for HTTP and WebSocket scopes (9e24ab2)
- Greenfield DB init race — for
template_name=none, initialize the empty DB in an isolated short-lived container before the serving container starts, avoiding a concurrentorm_signaling_registrycollision that leftbaseuninstalled (aca9445) - Orphan PG role on template restore — restore env-derived templates with
--no-ownerso deleting the source environment can drop its per-environment role (aca9445) - Test port flag by Odoo version — pick
--longpolling-portvs--gevent-portbased on the environment's Odoo major version (79b1c35) run_odoo_testsport & execution — pass--no-http --workers 0to avoid an 8069 conflict; override test ports and use-uso tests actually run (6311dc4, 738263e)- Post-clone DB fixup — transfer object ownership and drop signaling sequences (205e79f)
- Filter containers by prefix to isolate test/production environments (4a50e1c)
- Skip submodule recursion on git fetch to tolerate inaccessible submodules (5a1a476)
- Raise
NotFoundErrorwhen deleting a non-existent environment (0a1d875) update_serviceURL KeyError; fresh pull on create (27ec4c9)- Propagate
cap_add/privilegedinrestore_serviceand surface them inlist_service_presets(6bca16c) - Strip docker volume name prefix in
resolve_volume_binds(2573b17) - Exclude checkboxes from full-width input styling in forms (7f685c9)
- Pass missing
teamarg to_get_used_portsintemplate_up(fbfc85e) - Recreate live-mount environments as live-mounts — record
local_pathin template metadata and restore it on create (9e24ab2)
Performance¶
reload-template— pipegunzipintopsqlto avoid a temp file (0bd0838)
Documentation¶
- Use Odoo 19.0 across examples and bump default image fallbacks to
odoo:19.0(a1fba72) - Docs Material redesign (7980fea)
- Document self-hosted OAuth in the config reference, quick start, and llms docs (73aa9b9)
- Consolidate agent guidance into
AGENTS.md, makingCLAUDE.mda thin include (7b60d8c) - Document
refresh_templateMCP tool in mcp-tools, llms, llms-full (73a4768) - Rework transport mode documentation (stdio/http), add
uvxexamples (f2c8eef) - Fix
create_environmentCLI examples — wrong positional arg order (67dae80) - Docs auto-deploy and
.oduflow/dependency files (7f8d654)
v1.20.1 (since v1.15.1)¶
Breaking Changes¶
- Stdio transport is now the default —
oduflowstarts in stdio mode by default (previously HTTP). Useoduflow --transport httpfor HTTP mode. (34e42fa, f33dfe6)
Features¶
- Move repo-level
odoo.confto.oduflow/odoo.conf— per-repo Odoo config is now read from<repo>/.oduflow/odoo.confinstead of the repo root - Auto-initialize on startup —
oduflowautomatically runs system setup and the Docker check on first start (34e42fa, f33dfe6) - MCP tools refinement — output cache for long-running tool results, 7 new MCP tools, 3 enhanced tools; renamed
exec_in_odootorun_odoo_command(44810aa) - Include odoo.conf in upgrade — module upgrades now apply odoo.conf changes, skipping files that haven't changed (f69bc2f)
- Show template database name in
list-templatesoutput (c816229) - Default odoo.conf values — added
workers=0anddb_maxconn=4to the odoo.conf template for single-process development (f16ab37)
Dashboard¶
- Rebuild button — added rebuild button to web UI; pull Docker image before create/rebuild operations (0004ac7)
Bug Fixes¶
- Sanitization: delete mail servers — delete mail servers entirely instead of just disabling them during database sanitization (4f729b1)
search_in_odoofilenames and limit — always show filenames in search results and fixmax_resultslimit not being applied correctly (fc3c53c)- Missing team parameter in
pull_environment— pass team parameter when running module operations during pull (dc43104) delete_service_presetlock — fix locking for delete_service_preset operation (18b18b1)
Documentation¶
- Add MCP tools refinement spec (
mcp-ref.md) (87edd27, 417eba7) - Rename Mutex → Lock in documentation, add glightbox for image zoom (18b18b1)
- Rename
exec_in_odoo→run_odoo_command, add 7 missing tools to docs (518e2c8) - Advise agents to prefer local search over container search for Odoo sources (961b9dc)
v1.15.1 (since v1.10.1)¶
Breaking Changes¶
- Team-based multi-tenancy replaces instance-based isolation — configuration migrated from
.env/ODUFLOW_INSTANCE_IDto TOML-basedoduflow.tomlwith per-team settings (workspaces, templates, credentials, port ranges, hostnames). The.env.examplefile has been removed. (ad3b382, 14503a0)
Features¶
- Team-based multi-tenancy — per-team isolation with dedicated hostnames, git credentials, port ranges, and MCP token auth; auto-generated Traefik dynamic config for per-team routing; team resolution from Host header or bearer token (ad3b382, 14503a0)
- CLI:
run-instancecommand,--versionand--instanceflags — run a named instance directly from CLI with version info support (0094369) - CLI:
systemd-install/systemd-uninstallcommands — install/remove Oduflow as a systemd service (d266ca9) - Per-environment PostgreSQL credentials — each environment gets its own isolated database role and password (e4949b0)
- Two-tier database sanitization — system-wide sanitization scripts plus per-repo
.odoo_sanitize/folder support (cd0b9ec) - MCP tool:
read_file_in_odoo— read files and list directories inside Odoo containers without shell commands (abcc525) - MCP tool:
reset_admin_password— reset the admin user password in any environment's database (608b476) - Extra repos: fetch summary and propagation —
update_extra_reporeturns a summary of fetched branches and propagates updates to running environments (a80ba53) - Per-branch locking module — new
LockManagerreplaces the single global mutex with per-branch and per-team locks (ad3b382) - Docker publishing support — added
.dockerignoreand Docker publishing instructions (24f653a)
Dashboard¶
- Interactive SQL console (psql) — run SQL queries directly from the web dashboard (15d0e59)
- Interactive Odoo shell console — access Odoo shell via the web dashboard (894f1ea)
- Colored logs rendering — dashboard shows ANSI-colored logs; MCP tools receive clean stripped output (2e3989b)
- Detailed sync results popup — sync operations show detailed results in a popup instead of a plain toast (ade3ee6)
- Editable restore service dialog — service restore dialog shows editable preset values before confirming (4eac120)
- Wider logs modal — logs modal expanded to 80vw with horizontal scroll (e9d0273)
Bug Fixes¶
- MCP concurrency — pass
stateless_http=Trueto unblock concurrent MCP requests; run sync MCP tools in thread pool to prevent event loop blocking during long operations (81457b1, ed4a265) - TLS certresolver name — match Docker label to Traefik's ACME provider name ("letsencrypt" not "le") (727448b)
- HOME fallback for systemd — add
HOME=/roottoGIT_ENVso git operations work under systemd (a982b9d) - Reject SSH repo URLs — SSH URLs caused hangs due to interactive host-key prompts; now rejected early with a clear error (ad3b382)
- Database ownership on template clone — fixed ownership reassignment for tables, sequences, views, and materialized views; switched to
GRANT roleapproach (31df320, afb66d0, 5c7b737) - Environment deletion — drop database before role to avoid dependency errors (4b28e9e)
- Strip
db_passwordfrom odoo.conf — ensures the Docker entrypoint uses environment variables instead (f9f89d5) - Template publish — prevent self-copy in
reload_template, scope publish to environments matching the template (b92631a) - Git sync — replace
git pull --rebasewithfetch + reset --hardfor reliable sync; use explicit refspec inpull_repo(d57ee7e, 74ef53d) - Load extra_addons from template metadata in
create_environment(3d629e6) - Dump restoration — handle gzip files and verify table count (7ef7a02)
- Fallback data dir — fallback
ODUFLOW_HOMEto~/oduflow_data_{id}when/srvis read-only (d45307c)
Refactoring¶
- TOML-based configuration — replace
.envwithoduflow.toml; first launch auto-bootstraps the default config (ad3b382) - Merge
external_hostandbase_domaininto per-teamhostname(ad3b382) - Rename
ODUFLOW_HOME→ODUFLOW_DATA_DIRwith instance subdirectories (52224cb) - Move odoo.conf and odoo_sanitize from
etc/to instance data directory (dbfc969) - Require explicit branch for extra_addons — format is now
name:branch(9254169) chown_recursive()helper — replaces manualos.chownloops with macOS fallback support (872059d)- Rename
get_environment_status→get_environment_infowith comprehensive details (20e4abf) - Rename
get_agent_guide→get_agent_skill(8625790) - Rename MCP tools for clarity (fbbcf54)
- Async MCP tool wrapper —
handle_errorsdecorator converted to async, offloading all tools to thread pool (ed4a265)
Documentation¶
- Comprehensive docs rewrite for TOML-based multi-team architecture (ad3b382)
- Add MCP tools refinement spec (
mcp-ref.md) (87edd27) - Add missing features to documentation, fix inaccuracies (e0c3bc3, 1aaa6a5)
- Add
llms-full.txtfor complete LLM-consumable documentation (e330ad4) - Add screenshots to documentation pages (8726d01)
- Add macOS vs Linux file ownership section (ac4a7a8)
- Add
CLAUDE.mdwith project architecture and dev commands (008d43f)
v1.10.1 and earlier¶
Features¶
- Add
reset_admin_passwordMCP tool to reset the admin password in Odoo environments (608b476) - Refactor Agent Guides into multi-guide system with Odoo version-specific development guides (8680a18)
- Add
import_template_from_odoo— import templates from running Odoo instances (a6d53fc) - Store
use_overlayflag in template metadata instead of computing filestore size on every env creation (e393c44) - Show
use_overlaymode in CLIlist-templatesand dashboard UI (cf85489) - Show template name in
get_environment_infoand dashboard UI (e79a6ae) - Add Sync button to environment UI (66ceed8)
- Add
ODUFLOW_TRACE=1trace logging for sync/classify pipeline (af7b9bc) - Docker deploy added (e934bea)
- Append
/web?debug=1to environment URLs in dashboard (ff913ad) - Add logo to Web UI header (6fd52ce)
- Add
run_db_queryMCP tool for executing SQL queries against environment databases (d7b1b44) - Credentials management UI + git auth improvements (259164e)
- Per-user git credentials for repo operations (5c28100)
- Add
oduflow cleanupCLI command to remove orphaned resources (d1fa09c) - Add protect/unprotect for extra repos (1245d6c)
- Add
update_extra_repoMCP tool; remove one-off migration SQL (3c678cc) - Report elapsed time in
create_environmentresponse (90ca9e1) - Show filestore and dump sizes in template listing (a1403a7)
- Add Recreate button for environments in web UI (31afb9e)
Fixes¶
- Show friendly error when Docker is not available (5df6df7)
- Service logs filtering and state isolation from env logs (9926c2a)
- Update
test_settingsto match current Settings API (b92fe80) - Restart container after successful module installation (b819c65)
- Fix tests: expect
ToolErrorinstead ofValueError(dcf7f23) - License load fix (8e66e8f)
- Stream dump file to container to avoid OOM; save dump to workspace on
reload-template --dump-path(8615fe2) - Eliminate OOM in all container file extraction (
init_template,template_down,publish_env_as_template) (b40395b) - Surface database drop failures as warnings in
delete_environment(02a628b) - Use
odoo_imageversion as fallback branch for extra addons worktree (ea297fd) - Configure fetch refspec for bare extra repos so
git fetchupdates branches (54bc937)
Documentation¶
- Rework README — add missing config vars, licensing, template metadata, CLI flags (0b8de7e)
- Restructure README — reorder sections, remove odoo.sh comparison, merge internals (70af4df)
- Update README.md (fe6674f)
- Add MkDocs with Material theme, build site to docs/ for GitHub Pages (bbcf1e9)
- Switch to GitBook theme, use gh-pages branch for deployment (3cfbbb2)
- Add
requirements-docs.txtfor mkdocs setup (b620ff5) - Split README into structured mkdocs documentation (c52084f)
- Add database migrations workflow guide for agents (63629fd)
- Document extra addons pinning behavior in environments (651f877)
Refactor¶
- Rename CLI command
promotetotemplate-from-env(ecaaf84) - Rename template DB prefix to
oduflow_template_{id}_{name}, maketemplate_namerequired (c3bee0f)