Releases: oduflow/oduflow
Release list
Unified template import: Odoo, S3, local path, refresh
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 refreshes
metadata.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, and
update_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 agit cloneexit
128, which the dashboard masks behind the generic "Operation failed" banner.
The clone now raisesNotFoundErrorwith the same actionable message
switch_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)
Self-update and separate production MCP access
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 --force
and 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 use
sync_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 reportsdevas 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 ownodoo -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_token
andui_passwordwere printed to the startup log, which is shipped off-host
and retained far longer than the secrets stay valid. They now live in
oduflow.tomlonly and the log points at the file. The config is created
0600, 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 readable
ConfigErrorline 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.source
label still named the oldoduistorg, so a pulledoduist/oduflow-coder
pointed 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)
Dashboard 2FA, production WAL protection, service editor
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 an
acme_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 arequirements.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 newGET /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/and
metadata.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)
Team domains, SSH deploy keys, production Stacks
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_key
MCP 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
explicitbase_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. Leaving
base_domainunset preserves the legacy nested layout exactly, and existing
environments keep their hostname until their nextupdate_environment. A new
domains.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 resolve
productionField: 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. And
create_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
LATEST
base 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 dropped
odoo_image,repo_urland friends from every published template. (#237) -
search_in_volumealways failed — it used GNU-onlygrep --include
inside 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.conf
without arequirements.txtkept serving on the image's stockaddons_path
and 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,
soupdate_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, sodocs/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)
Production-to-dev clones. Per-team URL schemes.
v1.77.0
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, and
create_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 inbound
X-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 at
dev.example.com, settinghostname="qa"moves the environment to
qa.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 (default
github.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,username
andhostarguments 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 systemdUMask=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 get
0755, and the service image reference is preserved from the container's
Config.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)
Team secrets. Service lifecycle controls. Clearer OAuth configuration.
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 assecret:<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 validatedruntimemapping 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. (4c72e68) -
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)
Plain-HTTP deployments
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), and
public_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)
Share one environment by link. Rename environments.
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 behindcreate_service,update_serviceorrestart_service
fails 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_service
after 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)
Agents build and publish container images
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 request-scoped, resolved fromtoken_envat publish time and
never persisted. 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_serviceand
update_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 image
ENTRYPOINTis 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)
Clear error for an invalid environment hostname
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
exampleold_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)