Skip to content

Production hosting, hosted coding agents, and OAuth hardening

Choose a tag to compare

@litnimax litnimax released this 28 Jul 11:12
· 198 commits to main since this release

v1.68.0

Features

  • 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-team productions.json registry, a dedicated production PostgreSQL cluster with auto-tuned settings and Odoo worker tuning, a custom-domain Traefik route, a full git clone, and a production odoo.conf chain (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, and rollback_production targets any commit in the recorded history. Backups are first-class: pg_dump streamed 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 /healthz endpoint reports dev/prod PG, Traefik, S3 and disk state. See docs/production.md and 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 *.yml files that Oduflow never overwrites; drift control recreates a Traefik container that is still on the old single-file provider. (#132, specs/0034)
  • run_odoo_shell commits by default — odoo shell rolls back its cursor when the piped script ends, so ORM writes made through run_odoo_shell were silently discarded despite the docstring promising a commit. A new auto_commit parameter (default True) commits through a private cursor handle after the script succeeds; pass auto_commit=False for 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.internal resolves on Linux. (#163)
  • Richer coder shell toolbox — the coder image now ships curl, less, ripgrep, fd, tree, and python3 + 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.3 image instead of a rolling :latest tag. The publish workflow emits only versioned multi-architecture tags, legacy official :latest configuration 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_apply switches 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 -t alias 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 share flag), 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-connect route, and update_environment recomputes 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_sanitize and resolve against the actual
    local_path checkout instead of the empty managed-clone location. Existing
    repositories keep running with an explicit migration warning, and
    get_environment_info plus 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 explicit oauth_base_url in port mode), matching the dashboard's MCP Access dialog. Local port-mode deployments retain the host.docker.internal fallback.
  • 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/authorize and /mcp/token) instead of following the root endpoints advertised by discovery. The outer scoped-access shim treated authorize/token as an environment name and rewrote the request onto the auth-protected /mcp endpoint, so the flow failed with 401 invalid_token before 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'd odoo shell/psql process 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 /tmp is now tracked and removed in a finally, and staged under a unique per-restore name, so parallel imports cannot clobber each other and the oduflow-db writable 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 worktree is 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 400 at 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.js and acp-client.js now 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 fresh chat.js paired with a stale acp-client.js. (#148, #149)

Security

  • Live-mount startup warning — every server start with
    allow_local_path=true now 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_id is no longer the secret — a team's OAuth client_id is now the non-secret identifier team_<id> (e.g. team_1); the team's auth_token is the client_secret and remains the issued access token. Previously client_id == client_secret == auth_token, so the secret appeared in the /authorize query string (and thus in server logs, browser history, and the Referer). The secret now travels only in the POST /token body. Breaking: re-enter any existing claude.ai connector as Client 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's auth_token. A minted access token expires (~1h) and can be revoked via the now-enabled /revoke endpoint; 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. The auth_token still 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.repo Docker label (they move into the team .git-credentials store, created 0700/0600); import-from-Odoo endpoints accept the bearer token only via the Authorization header and require HTTPS, since the request carries the Odoo master password; BasicAuthMiddleware rejects cross-site state-changing requests and every WebSocket handshake via an Origin/Referer check; and the Web UI now default-denies with 401 when 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_shell via PGPASSWORD instead of -w <password> on the Odoo CLI (visible via ps inside the container), and the dashboard's HTTP 500 handlers return a generic message instead of str(e), which could leak absolute paths and Docker internals. Details are still logged server-side; MCP responses, FlowError messages, 400 validation errors, and interactive terminals stay verbose. (#84, #134)

Documentation & Testing

  • mypy strict is green and gated in CI — strict = true had 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 the docker SDK by the repo's own docker/ directory), the type-checker version is pinned via a dev extra, and a mypy job now runs on push and PR. (#131)
  • Production hosting documentation — specs/0031 records the decision, docs/production.md covers configuration, deploys and rollback, webhooks, snapshots and restore, the cluster PITR runbook, /healthz, and the tool table; the bundled oduflow.toml gains commented [production] and [backup] sections. (#116)
  • First-run credentials and config paths — the legacy ~/.oduflow/oduflow.toml fallback is removed and the remaining ODUFLOW_TOML → /etc/oduflow/oduflow.toml → ~/.oduflow/conf/oduflow.toml lookup order is documented, so first-run users know where the generated auth_token and ui_password live. (#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 :latest channel.
  • Document the short transport flag — HTTP server startup examples now show oduflow -t http / uvx oduflow -t http as 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.