Client application states¶
Client source paths and commands in this guide are relative to the
oduflow-client repository (the platform checkout mounts it at client/).
See client releases for the managed checkout/update workflow.
These states install the published Oduflow 1.76.0 package and build Paseo 0.8.0
from the pinned oduflow/paseo commit on Ubuntu amd64.
roles.client_stack now includes the authenticated application bootstrap.
Production creation remains a separate step after the publication checks.
The combined command is salt-call state.apply roles.client_stack. For staged
installation and debugging, apply explicitly in order:
salt-call state.apply client_apps.install
salt-call state.apply client_apps.configure
salt-call state.apply client_apps.start
install needs no application credentials and starts no services. It installs
signed Ubuntu prerequisites and checksum-verified uv 0.12.13, Node 22.20.0 and
the Oduflow wheel, then builds Paseo from source. Artifact URLs, upstream
checksums and the Paseo repository, commit and workspace list are recorded in
salt/states/client_apps/artifacts.json. The same file is copied to
/var/cache/oduflow-apps/artifacts.json and is the installer's only source of
versions, so a pin is declared once.
The pinned IDE source archive is included in the client repository at
salt/states/client_apps/artifacts/. Clean installations read it from the selected
client checkout without a GitHub credential. New client plans also provide a
separate read-only deploy key for oduflow/paseo, so clients can read its source.
This access is independent of the checksum-pinned installation archive.
To prepare a replacement artifact from an authorized local Paseo checkout, run
scripts/prepare-client-artifacts.py --repository /path/to/paseo --output /path/to/staging
and review the manifest checksum before committing the artifact to the client
repository. The installer verifies its checksum, embedded commit, version and
extraction paths. The legacy docker/master-artifacts/Dockerfile remains an
optional platform packaging utility.
The Paseo build follows the upstream container recipe: fetch the pinned commit,
npm ci from the committed lockfile, npm pack each pinned workspace in
dependency order, then install those tarballs into
/opt/oduflow/paseo/<version>+<commit>. The checkout is refused unless the
resolved HEAD is the pinned commit and the source carries the pinned version;
the contributor Git-hook script is dropped before installing, as upstream's own
image build does. The build tree and its private npm cache are removed
afterwards, so a rebuild is a full rebuild rather than an incremental one.
A source build needs far more time, memory and scratch disk than a package
install did — the golden image performs it once for all clients.
The npm build uses a 4 GiB V8 old-space limit: Node's automatic limit on a
2 GiB client is insufficient for the server TypeScript build. Small clients
need sufficient swap during compilation. This setting applies only to build
commands and does not change the running Paseo service's memory settings.
Building runs the pinned source's own npm lifecycle scripts as root, as installing the published tarball already did. Review a commit before pinning it; the pinned commit and archive checksum define the trusted source.
Installation receipts are written only after installation succeeds; a missing
executable causes installation to run again. Because the fork keeps the upstream
0.8.0 version string, the installed directory, the receipt and the systemd unit
are all keyed by version and commit. Python/npm transitive dependencies are
resolved from their registries at installation time: package-lock.json pins
Paseo's own dependency tree, but the tarballs' runtime dependency resolution and
the Oduflow wheel are not fully locked. Ubuntu package versions follow the
host's signed apt repositories.
configure installs/starts Docker with verified storage guards, writes app
configuration and systemd units, but does not start the application services.
start validates the configuration with the installed Oduflow package, then
starts Paseo, its private proxy, and Oduflow. Reapplying does not create a
production environment. A running service is not production readiness; the
separate production workflow must create and verify Odoo.
Required pillar¶
The schema1 identity must match the minion ID client-<UUID>, and storage must
point at a stable /dev/disk/by-id/... device mounted at /srv/oduflow/data.
The existing storage state still requires explicit authorization before it can
format a blank disk. App configuration additionally needs:
dns.oduflow,dns.paseo, andingress.mode=direct_tls;ingress.acme.emailfor Let's Encrypt HTTP-01;- generated
oduflow.auth_token,oduflow.ui_password,oduflow.database_password, andpaseo.password; - existing Oduflow quotas/lifecycle values, if overriding the bounded defaults.
Paseo's generated password must be at least 16 URL-safe characters. Credential
files are 0600, Salt file diffs are suppressed, and passwords are not command
arguments. oduflow.database_password is a distinct, mandatory PostgreSQL credential
generated and encrypted by the control plane. It never falls back to the MCP
token or the upstream default odoo. New prepared instances also receive a
distinct production_admin_password, exposed as
oduflow.production_admin_password for the separate production workflow.
Treat database credentials as persistent: changing TOML does not rotate an
existing PostgreSQL role password. A supplied oduflow.license_key is installed
as /etc/oduflow/license.key; an omitted key does not delete an existing license.
No LLM key, provider CLI login, or license is needed merely to install/start
these authenticated web interfaces. GitHub CLI is installed on the host, including during credential-free image
installation. When the verified client GitHub bundle is present, paseo.project
writes private GitHub CLI credentials for the paseo user, configures Git's
GitHub credential helper, clones the confirmed repository branch into
/srv/paseo/projects/<repository>, and registers it with the running daemon.
Project registration must succeed before application health completes. Retries
verify the existing origin and preserve user work; they do not pull, reset, or
create duplicate projects. This state can also be applied to an existing client
without rebuilding Paseo. GitHub credentials and working copies stay on the
verified client volume and are never included in images.
Agent execution still requires the separate client_agent configuration.
The control plane omits absent optional integrations and refuses partially
provided credential bundles. Git credentials additionally require the confirmed
repository branch; no branch is guessed. Old prepared records must explicitly
receive the new database/production credentials before deployment.
Encrypted storage_allow_format maps to storage.allow_format and defaults to
false; only an actual boolean is accepted. Optional
storage_filesystem_uuid maps to storage.filesystem_uuid after canonical UUID
validation. The client storage helper also verifies volume ownership. Direct
TLS pillar declares provider=letsencrypt, challenge=http-01; it carries no
Cloudflare credential.
Storage and private routing¶
Docker's data root is /srv/oduflow/data/docker; containerd's persistent root is
/srv/oduflow/data/containerd. Config and systemd mount guards are installed
before apt can start Docker. Existing data in /var/lib/docker or
/var/lib/containerd, or an incompatible existing daemon configuration, causes
preflight refusal. Automatic migration is intentionally absent.
Paseo is launched with paseo daemon run --home /srv/paseo. The source build
removed the former launch flags, so the listen address, relay state, trusted
proxies, hostnames and web UI come from the managed /srv/paseo/config.json, and
PASEO_PASSWORD from /etc/paseo/credentials.env still overrides the stored
password hash. Passing the removed flags is a hard startup error, not a warning.
Docker, containerd, Oduflow and Paseo all depend on the verified mount and stop
when its mount unit disappears. Docker live-restore is disabled. Paseo runs as
its own unprivileged user, with /srv/paseo pointing to its private directory on
the volume. Oduflow runs as root because it manages Docker and XFS quotas, as in
the published host installation contract.
A clean client reserves Docker's 172.17.0.1/16 bridge. Oduflow listens on
172.17.0.1:8000; Paseo listens on 127.0.0.1:6767. A systemd socket proxy
listens only on 172.17.0.1:6768 and forwards to Paseo loopback. This is needed
because Oduflow's containerized Traefik cannot reach a host loopback listener.
The proxy does not bind public interfaces; do not expose/forward its private
ports. The host firewall must permit HTTP/HTTPS publicly and Docker-bridge
traffic to 8000/6768, while keeping administrative ports private. Firewall
provisioning and external probes belong to the surrounding client workflow.
Oduflow owns the only Traefik instance, with routing.mode=traefik, tls=true,
and [production].enabled=true. Its exact-host HTTP-01 certificates need public
DNS-only records and inbound port 80. No client Cloudflare API token is used.
The published package hardcodes the rolling traefik:v3 image and defaults to
postgres:15; the states do not pretend those upstream image tags are digest
pinned. Pinning that part of the upstream runtime is follow-up work.
Hostname contract¶
The listener uses [server].bind; the legacy server.host key is no longer
generated. Each team sets its explicit hostname, which also determines its
OAuth issuer. No [oauth], oauth_base_url or routing.hostname override is
generated. This follows upstream commits 6546486 and 58db708: service and
environment secret values may be expressed as secret:<name> references to the
operator-managed team vault rather than expanded into generated manifests.
Use team.1.hostname=oduflow.<client>.<platform> for the dashboard and the
explicit [route.paseo] for paseo.<client>.<platform>. The production workflow
must explicitly supply <client>.<platform> as the production domain.
Published Oduflow 1.75.0 accepts environment_hostname_mode="branch", but its
implicit branch hostname appends the entire team hostname. To obtain
feature.<client>.<platform>, creation must pass an explicit short
hostname="feature". There are no reserved devN/svcN records in these states.
Do not claim implicit branch naming already uses the client-wide namespace.
Validation and sources¶
PYTHONPATH=/tmp/oduflow-salt-minion-deps python3 -m unittest discover -s tests
-p test_client_apps.py exercises fail-closed configuration, private secret
files, mount/package ordering, include/requisite resolution, installer receipts,
refusal of a source that does not match its pin, build-tree cleanup after a
failure, and refusal to migrate existing container data. Salt 3006.25 compiled
the state files offline and rendered the templated unit, and the actual
checksum-verified Oduflow 1.75.0 wheel parsed/validated the rendered TOML.
The updated TOML was also parsed and validated with the checksum-verified
Oduflow 1.76.0 wheel, confirming bind_host and automatic OAuth. The platform
Stack and manual service specifications passed its Pydantic models. The
corresponding local suite ran 401 tests (13 skipped), with Ruff lint/format checks.
The Paseo source build was executed locally with the pinned Node 22.20.0 and the
pinned commit: the installer produced all seven workspace tarballs, installed
them, and the resulting daemon answered /api/health 200, /api/status 401
unauthenticated and 200 with the PASEO_PASSWORD bearer token on the address
taken from a copy of the managed config.json. That local run used no client
identity, no Docker and no client volume; installation on a real enrolled client,
its public TLS and the production stack remain separate verification steps.
Package and build contracts were checked against
Oduflow PyPI 1.75.0 metadata,
uv tool installation,
uv release checksums, and
Node 22.20.0 checksums.
Node 22 is the version the fork declares in .tool-versions and builds with in
docker/base/Dockerfile; that Dockerfile is also the source of the workspace
pack order used here. The source pin is a full commit. Reviewing a newer commit means updating artifacts.json alone: the
install path, receipt and systemd unit follow it.
Client package states install a list-only needrestart policy before package
operations. Salt owns service restarts; an automatic package-triggered restart
of Salt Minion can interrupt a durable provisioning operation before its result
is recorded. Interrupted operations require reconciliation before another dispatch.
Prebuilt IDE runtime¶
paseo.install_method selects source (the default) or prebuilt.
The first verified Ubuntu 26.04 amd64 build is published in
IDE Releases.
Both the source repository and release assets are public.
paseo:
install_method: prebuilt
runtime:
source: https://github.com/oduflow/paseo/releases/download/ide-0.8.0-ubuntu26.04-amd64/ide-0.8.0-ubuntu26.04-amd64.tar.gz
hash: sha256=6e60d34730f769790d138860dbd5148366f779583b2bfecfd5fd727a0d1582fa
Salt downloads the archive directly over HTTPS without authentication. Odoo stores
its URL, checksum and compatibility metadata; see instance settings.
A salt:// source must exist in the selected client's local release file roots.
The current build recipe is client/scripts/build-paseo-runtime.py. Its workflow
builds in ubuntu:26.04 on amd64 and uploads a temporary Actions artifact. Publish
verified runtime archives as release assets in oduflow/paseo for permanent URLs.
The runtime includes all seven installed workspaces and their npm dependencies. Node is installed separately from its pinned artifact. Before extraction, the installer checks SHA256, Ubuntu version, architecture, Node version, IDE version and full source commit. A mismatch fails without falling back to a source build. An already installed matching release is reused.
A runtime build is not a client VM image or proof of full Ubuntu 26.04 provisioning.