odoo-devkit owns the manifest-driven workspace command surface used to build
the coding-agent workspace consumed by Codex and Claude Code, plus the local
runtime assembly.
Runtime ownership is split by target type:
- manifest-local runtime targets run natively in
odoo-devkitforplatform runtime select,build,publish,up,down,inspect,logs,psql,odoo-shell,restore, andplatform runtime workflow --workflow bootstrap|init|update|openupgrade. - Non-local restore/bootstrap/update, release, and preview lifecycle flow
belongs in Launchplane.
platform runtimefails closed for shared/testing/prod mutation instead of shelling into a sibling runtime checkout. - non-local
platform runtime workflow --workflow init|openupgraderemains local-only and fails with a clear--instance localrequirement once the runtime repo resolves. - Release actions such as ship, promote, and gate execution belong in
launchplane, not underplatform runtime.
uv run platform workspace sync --manifest /path/to/workspace.toml
uv run platform workspace prepare-ide --manifest /path/to/tenant/workspace.toml \
--odoo-source /path/to/odoo-19 --odoo-series 19.0 --odoo-commit <full-source-sha>
uv run platform workspace status --manifest /path/to/workspace.toml
uv run platform workspace status --manifest /path/to/workspace.toml --check
uv run platform workspace scaffold-tenant-overlay \
--output-dir /path/to/repo --tenant opw
uv run platform workspace clean --manifest /path/to/workspace.toml
uv run platform workspace run --manifest /path/to/workspace.toml -- pwd
uv run platform dependencies inspect --manifest /path/to/workspace.toml
uv run platform dependencies check --manifest /path/to/workspace.toml
uv run platform dependencies normalize --manifest /path/to/workspace.toml
uv run platform dependencies normalize --manifest /path/to/workspace.toml \
--output-dir /path/to/normalization-output
uv run platform runtime select --manifest /path/to/workspace.toml
uv run platform runtime build --manifest /path/to/workspace.toml --no-cache
uv run platform runtime up --manifest /path/to/workspace.toml --build
uv run platform runtime down --manifest /path/to/workspace.toml --volumes
uv run platform runtime workflow --manifest /path/to/workspace.toml --workflow update
uv run platform runtime restore --manifest /path/to/workspace.toml
uv run platform runtime inspect --manifest /path/to/workspace.toml
uv run platform runtime logs --manifest /path/to/workspace.toml --service web --no-follow
uv run platform runtime psql --manifest /path/to/workspace.toml -- -c 'select 1'
uv run platform runtime odoo-shell --manifest /path/to/workspace.toml \
--script tmp/scripts/example.pyIf --manifest is omitted, the command looks for workspace.toml in the
current directory.
Optional IDE run configurations use [[ide.run_configurations]] tables.
Omitting them or setting run_configurations = [] under [ide] defines none;
other value types and non-table entries are rejected when the manifest loads.
Non-local platform runtime publish is invoked by the product's own build
workflow, which supplies the required runtime-environment payload.
Hosted startup honors ODOO_PROXY_MODE in its generated Odoo configuration.
Set it to True through the lane's managed runtime settings when Odoo sits
behind a trusted reverse proxy that supplies forwarded headers. Set it to
False to disable proxy mode explicitly. An absent or blank value preserves
the base configuration; without a base setting, Odoo's disabled default applies.
Changing web.base.url alone does not change the scheme of incoming requests
used to build OAuth callbacks.
Purpose
- Materialize the tenant, devkit, and optional shared-addons sources under the workspace root.
- When
[repos.shared_addons]declaresurl+refinstead ofpath, clone or refresh a managed checkout atsources/shared-addons. - When
[repos.runtime]declaresurl+refinstead ofpath, clone or refresh a managed checkout atsources/runtime; apathis linked there instead. - Generate runtime config under
.generated/. - Generate the canonical workspace-root coding-agent surface:
AGENTS.mddocs/README.mddocs/session-prompt.md
- Generate PyCharm metadata plus run configurations.
- Emit
workspace.lock.tomlwith the exact assembled local state.
Prepare the exact tenant checkout before opening it in PyCharm or running a
JetBrains inspection. The Odoo plugin needs the matching community core and
addons visible as project content roots to resolve references such as
website.snippets. Listing paths in the workspace metadata alone does not
attach them to the IDE.
- Obtain an official Odoo community checkout outside the tenant repository, using the series required by the runtime. Keep it in the host's shared dependency cache and pin a full commit SHA.
- Close only the exact tenant project being prepared, or prepare it before its first open. Do not run the generator while that project has an active IDE writer or inspection. Other worktrees can keep their own projects open.
- Run
workspace prepare-idewith that tenant worktree'sworkspace.toml,--odoo-source,--odoo-series, and--odoo-commit. Keep machine-local paths in local invocation/configuration, outside tracked tenant files. - Open and inspect the tenant worktree through the normal inspection helper. Core sources supply resolution context; keep the inspection scope on the tenant files. A successful preparation is setup evidence, not an inspection verdict or a runtime update.
The command checks the source checkout's commit, clean Git state, core/addon
layout, and release series without importing Odoo or changing the dependency.
It adds the content root to the Python module rooted at the exact tenant, or
creates a minimal project using the tenant's pyproject.toml project name when
no module exists. Existing SDK assignments, content roots, module
dependencies, inspection profiles, and other project state remain in place.
This implements the plugin author's supported
content-root setup.
Only already ignored, untracked IDE metadata may be changed. Tracked metadata,
ambiguous tenant modules, malformed paths/XML, a different existing Odoo content
root on the tenant module,
or symlinked project metadata require local reconciliation first. The generator
does not edit ignore policy or overwrite another project's configuration.
Paths may use $PROJECT_DIR$, $MODULE_DIR$, or $USER_HOME$. Custom IDE path
variables are resolved only when
--ide-path-variables /path/to/selected-ide-config/options/path.macros.xml is
supplied. Use the configuration directory of the IDE instance that will open
this exact project (Help > Diagnostic Tools > Special Files and Folders). The
file is JetBrains' native application / PathMacrosImpl macro table, saved
from Settings > Appearance & Behavior > Path Variables. Path variables apply to
every project in that IDE instance; see
JetBrains' path-variable documentation.
The command reads this explicit file without discovering global configuration,
changing IDE settings, or taking definitions from environment variables. Do not
commit it or its machine-local values. Save variable changes in the selected IDE
before preparing a closed project. Names must be unique and must not override
the three built-ins; values must be absolute paths without nested macros.
Missing definitions (including in unrelated local modules), duplicate definitions,
malformed tables, and conflicting tenant Odoo sources fail before any metadata
write. Existing custom macro URLs are preserved, so preparing a saved
file://$ODOO_SRC$ root again adds no duplicate. Interpreter libraries and other
modules' content roots remain outside this command's source-attachment check.
Repeated preparation with the same source and supported paths is a no-op. JSON output records the
exact project/module paths, source path, commit, series, and changed files.
This command works without workspace materialization, Docker, runtime secrets, an installed Odoo interpreter, or tenant code changes. It can serve as the repository's configured inspection preparation command when its caller supplies the local devkit/source paths and pin. Normal workspace sync continues to own the generated cockpit and run configurations; it does not change IDE roots.
Purpose
- Compare
AGENTS.md,docs/README.md, anddocs/session-prompt.mdagainst the current deterministic render, reporting each surface as current, stale, missing, or disabled. - Compare the manifest hash and typed source map against
workspace.lock.toml. - Report tenant, devkit, shared-addons, and distinct runtime source roles with workspace-relative entrypoints, resolved paths, materialization type, and editability.
- Report each source repository kind as
gitordirectory; non-Git path edit roots omit unavailable Git baseline fields instead of producing false drift. - Keep managed repository URLs out of generated files and status output;
workspace.lock.tomlrecords only their SHA-256 for contract comparison. - Report source commit/branch/dirty changes as baseline drift. Drift on editable
path-linked sources remains informational and does not make generated
guidance stale; drift on managed read-only checkouts fails
--check. - Detect missing or repointed source links, invalid managed checkouts, and a
reserved root
AGENTS.override.mdthat would shadow the canonical guide. - Reject a supplemental
workspace.local.mdthat is a symlink or non-file so the normal agent flow cannot be redirected outside the workspace notes file. - With
--check, exit nonzero when the workspace, lock contract, manifest, generated guidance, source materialization, or override state is not current. Baseline drift on editable path-linked sources alone does not fail the check.
Purpose
- Inspect tenant and shared-addon
pyproject.tomlfiles as one staged owned dependency workspace without moving shared-addon ownership into the tenant repository. - Require every owned addon project to set
tool.uv.package = false, retain an explicit exactly pinned build backend, and avoidtool.uv.managed = false, mutable VCS refs, local/archive references, orrequirements*.txtfallback. - When a tenant root
pyproject.tomlanduv.lockexist, require them as a complete pair, expand workspace members against the combined staged layout, and require the expanded members to exactly match all tenant and shared-addon projects. An explicit emptymembers = []is the exact valid set when the tenant and shared-addon trees contain no Python project metadata; pure-addon tenants do not need to invent a fake workspace member. - Evaluate member patterns against the real tenant/shared addon directory
shape, not only copied dependency metadata. A pattern may not match an addon
directory without
pyproject.toml; use explicit member paths instead of a broad glob over mixed Python-project and ordinary Odoo addon directories. - Run
uv lock --check --offline --no-configagainst that combined staged layout with operatorUV_*/PIP_*overrides removed. Devkit does not parse uv's lock internals as a substitute for uv's own currentness decision. - Treat publishable dependency metadata as Git-attributed input: root/member pyprojects and the tenant lock must be tracked regular files, symlinks and operator-local paths are rejected, and custom uv indexes/find-links cannot enter the staged contract.
- Allow a pure-addon workspace with no runtime Python dependency declarations
to remain lockless and current for local development. Such a workspace is
reported as
publishable = falsebecause Launchplane artifact schema v2 requires both support/runtime and tenant lock evidence; devkit never invents a tenant lock that is absent from the tenant repository. - A pure-addon tenant that publishes schema-v2 artifacts instead tracks a
minimal root
pyproject.tomlwithtool.uv.package = false, explicit empty workspace members, and its generateduv.lock. Those files provide exact tenant evidence without claiming runtime dependencies that do not exist. inspectprints structured JSON.checkprints the same report and exits nonzero whencurrentis false.
Purpose
- Regenerate the canonical tenant root
uv.lockinside the same staged tenant/shared-addon workspace used by dependency inspection, then atomically copy only the verified lock back to the tenant repo. - Preserve strict tenant CI: normalization proceeds only when the workspace is already publishable or stale lock state is its sole finding, and the written lock must pass the existing offline, no-config publishability check.
- Run
uv lock --python <manifest-python> --no-configwithout broad upgrade flags. Pinning the manifest's Python version keeps canonical lock generation stable across hosts while the existing lock continues to constrain unaffected packages. - Produce the frozen export shape used by tenant CI with
uv export --frozen --all-packages --no-emit-workspace --no-default-groups --no-config. The export is verified and hashed on every run; pass--output-dirto retain it astenant-requirements.txt. - Emit sorted JSON with source input hashes, tenant/shared source commits and
dirty-state flags, uv version and arguments, final artifact hashes, strict
post-check results, and
changed = falsefor a measured no-change run. Output omits credentials, repository URLs, and machine-specific source paths. - Restore the original tenant
uv.lockif the strict post-check or retained export write fails. Tenant manifests, addon metadata, shared-addon sources, and CI workflows remain owned by their existing repositories.
When [build].base_runtime_image is configured, normalization also resolves
that published image to an immutable digest and reads its baked
/opt/launchplane/base-python-constraints.txt for both supported Linux
architectures. It never reconstructs this catalog from a tenant image.
Missing catalogs and conflicting tenant requirements fail with the base digest
and resolver's package details.
The shared staging resolver merges all base entries into root
tool.uv.constraint-dependencies, retaining tenant constraints and addon
requirements. It starts from each source lock and runs ordinary uv lock
without broad upgrades. Generated constraints stay in staging. The canonical
source lock still describes the source pyproject; it need not change when a
compatible base package moves. Frozen exports and installations use the
derived lock, including support/runtime and addon-test catalogs.
Canonical dependencies normalize, including --output-dir, stays registry
independent and retains its canonical frozen export and hash contract. It does
not qualify the base or add generated constraints to source metadata.
For a host environment, use dependencies prepare --output-dir <empty-dir>
with the workspace manifest, then install the staged workspace:
uv sync --frozen --all-packages --no-default-groups --project <empty-dir>Preparation reads the published base and retains the complete derived metadata
workspace and frozen export. --all-packages includes addon member requirements
when the root itself declares none. Constraints apply to declared host overlaps;
platform markers retain Linux-only requirements outside a macOS environment.
Source-only dependencies inspect/check remain offline source contract checks.
Local runtime build uses artifact staging in working-tree mode for
docker/Dockerfile. Its ignored .platform/dependencies state and per-runtime
Compose overlay keep four derived metadata files mounted during local use. Full
build contexts are temporary and a failed build preserves the current overlay.
Local edits to tracked metadata remain allowed; add new metadata to Git and
normalize before staging. Nested addon repositories use declared shared/external
source routes. The reserved top-level addons/shared comes only from the
declared shared checkout; nested directories named shared stay included.
Ignored files stay excluded, and local/<role> Git
markers identify the baseline without requiring a remote or claiming a release
identity. Addon CI derives temporary locks from the exact published image it
builds FROM. Artifact builds use the same resolver with immutable committed
source snapshots and the exact resolved base digest for every target platform.
Different architecture catalogs receive architecture markers in the universal
lock. Catalog extraction may download both platform images, even for a single
platform build; it uses the existing Docker cache. No checked-in base package
list is authoritative. Local builds require manifest [build] base inputs,
just as artifact builds do.
The local build generates its Compose build definition, so a fresh devkit
worktree needs no operator-local docker-compose.override.yml. External addon
and OpenUpgrade selectors resolve to exact commits through the publish resolver
before Docker runs. Resolution and BuildKit source fetching use the same
transient source token: configured GITHUB_TOKEN, then the source-token
environment variables described below, then shell GITHUB_TOKEN/GH_TOKEN,
then the existing local gh auth token lookup. Registry credentials are not
source credentials. The token goes to BuildKit as a secret, outside build arguments and
generated build metadata. Tenant Python dependencies use the staged derived
locks above; no separate local installer or hand-pinned source file is needed.
Local bootstrap, restore, update, init and OpenUpgrade use the existing image
and derived-lock overlay without rebuilding, so retrying a failed operation does not
require source or registry access. Before first use or after dependency/image
changes, run platform runtime build or platform runtime up --build with
the same manifest. A missing image fails instead of triggering an implicit
build from an operator override.
Local OpenUpgrade builds use the same strict frozen dependency install. Declare
openupgradelib in owned addon metadata (including an exact VCS source when
needed), run canonical normalization, and rebuild. OpenUpgrade source fetching
and execution remain available. Remove explicit OPENUPGRADELIB_INSTALL_SPEC
and ODOO_PYTHON_SYNC_SKIP_ADDONS inputs: strict local builds reject these legacy
overrides instead of running an installer after dependency verification.
The layered installer's inherited-package refusal and final environment validation remain independent checks. Generated constraints are ordinary constraints, never overrides; they cannot make an explicit incompatible tenant or addon requirement acceptable.
Purpose
- Remove the assembled workspace so it can be recreated from source repos and trusted local inputs.
Purpose
- Run an arbitrary command with the workspace root as the current directory.
Purpose
- Copy the thin tenant-overlay starter files into a target repo directory.
- Stamp the tenant slug into the starter
workspace.toml. - Give a tenant repo a repeatable thin-root starting point.
- Keep the starter aligned with the flat tenant-addon rule: tenant-owned addons
live directly under
addons/, while shared addons stay external via[repos.shared_addons].
Purpose
- Expose the local runtime command surface through
odoo-devkitso tenant overlays and generated PyCharm run configurations do not need to call a sibling runtime repo directly. - Resolve the runtime target from
workspace.tomland execute supported local workflows natively againstodoo-devkit, while leaving non-local mutation to Launchplane service routes and reusable workflows.
Local runtime input
- Before any local
platform runtimecommand (select,inspect,build,up,down,restore,logs,psql,odoo-shell, or a workflow), setODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSONfrom an operator-owned shell, password manager, or mode-0600file outside the repository. - The payload must be a JSON object with the exact selected
context, the exact selectedinstance, and a non-emptyenvironmentobject containing only string keys and values. The checked-in stack declares which environment keys are required for the selected command. - Generated runtime and Compose env files encode literal values, including dollar
signs, comment markers, quotes, backslashes and surrounding whitespace. Values
are not shell expressions; do not source these files. Re-run
runtime selectto regenerate an environment file written by an older devkit. Password values remain in the generated files, not command arguments or diagnostic output. - This is the only supported runtime-environment input path. Do not put runtime
values in
workspace.toml, generated workspace docs, checked-in config,.env,platform/.env, orplatform/secrets.toml. runtime inspectreports selected runtime metadata and generated config paths; it does not print the payload or environment values.
Notes
-
The tenant repo remains path-based and user-owned.
workspace syncdoes not clone the active tenant checkout for you. -
Shared-addons inputs may be path-based or repo-addressable. Managed shared-addons checkouts fail closed if the workspace copy is dirty or points at a different
originthan the manifest declares. -
Startup carries its resolved addon paths into the image wrapper environment, keeping packaged shared addon groups visible to initialization, web and the scheduler. After initialization it reads back the requested installed modules and stops before serving if Odoo ignored a missing addon. Correct the declared addon inputs or paths, then run the same startup command again.
-
Keep the runtime repo explicit in the manifest. Tenant scaffolds point
[repos.runtime]at the siblingodoo-devkitcheckout so the same tracked manifest can keepinstance = "local"by default while artifact publish can still stage runtime inputs for Launchplane handoff. -
Runtime ownership remains fail-closed and explicit for non-local targets.
odoo-devkitdoes not guess a runtime repo from[repos.shared_addons], even if that path points at a siblingodoo-shared-addonscheckout. -
Repo-addressable non-local runtime definitions fail closed until
platform workspace synchas materializedsources/runtime. -
platform runtime selectandinspectgenerate the PyCharm Odoo config from the manifest-backed tenant/shared addon sources. -
Local
platform runtime upemits manifest-backed host addon mount paths for compose, so tenant checkouts can bind-mountsources/tenant/addonsplussources/shared-addonsinto the devkit-owned local runtime bundle. -
Local runtime selection reads typed
odoo_overridestables from the stack and renders theODOO_INSTANCE_OVERRIDES_PAYLOAD_B64payload consumed bylaunchplane_settings.config_parameterstables write Odooir.config_parameterkeys, whileaddon_settings.<addon>tables write supported addon settings such asauthentik_ssovalues. -
Checked-in stack
runtime_envandodoo_overridesvalues are local-only. Active dev, testing, and production values or domains belong to Launchplane runtime-environment records. -
Non-local Launchplane-managed instances (
dev,testing, andprod) always prependlaunchplane_settingsanddisable_odoo_onlineto the resolved Odoo install module list. Artifact inputs or base images make addon files available, but this install list is what activates those modules in each database. Odoo 20 Community suppliesdisable_odoo_onlinethrough shared addons without an external source. Startup and data workflows reject distinct copies of that module on the addons path; remove the redundant source rather than relying on path order. -
Artifact publication preserves resolved install roots without expanding addon dependencies. The CM context declares the required Launchplane modules explicitly so its artifact carries them through this same contract.
-
When a tenant repo contains
website-bootstrap.toml(at the tenant repo root, or besideworkspace.toml), runtime selection also folds that non-secret website intent into the same typed payload. The bootstrap contract can add install modules, provide the local canonical URL, identify a homepage page or controller route, and point at a repo-local logo asset. Keep only the local canonical URL there; testing/prod canonical URLs are Launchplane-owned runtime records, though the file format does not reject other instance keys. Data workflows and startup apply bootstrap state idempotently after modules are installed, verify required public website identity fields before reporting success, and avoid hard-coded tenant defaults. The shared settings consumer supports native string parameters on Odoo 19 and 20, preserving empty values and literal boolean strings. Bootstrap reuses the native default website in either version and compares encoded logo content through the native attachment interface. Nonproduction analytics and indexing exclusion still runs without an optional settings payload, including while the loaded website addon is scheduled for upgrade or removal. A base-only website model does not enable website settings or bootstrap on its own. Page-backed bootstrap also binds discoveredwebsite.pagerecords, their website-specific views when available, and route readback markers to the selected website so post-deploy proof can distinguish payload rendering from public website identity persistence. -
Non-local Launchplane-managed runtimes can set
LAUNCHPLANE_INSTANCE_OVERRIDES_REQUIRED=trueto require a valid typed override payload with managed settings before startup or data workflows continue.LAUNCHPLANE_WEBSITE_BOOTSTRAP_REQUIRED=trueadditionally requires a non-emptywebsite_bootstrapobject in that payload and fails when the website module is not installed; without it, a payload for a database that lacks Website logs the skip and continues. These flags are runtime assertions supplied by Launchplane-managed records or operator input; local runtimes remain optional unless a caller explicitly sets them. -
Launchplane may include
website_indexingalongside the unchangedwebsite_settingsin the existing post-deploy payload:{ "instance": "prod", "website_indexing": { "website_id": 1, "public_origin": "https://site.example.test", "production_use": "prelaunch" } }These three fields are required and no extra fields are accepted. They are actuation data derived from Launchplane's current production state and exact production website binding, never an operator SEO flag. Only
prelaunchandliveare accepted, on a production instance with a matching production envelope. Omission preserves legacy behavior. The selected native website must have that exact HTTPS domain (an optional trailing slash is ignored), with no second website competing for the host and port even under another scheme. When both payload objects are present, their website ID and origin must agree before either can write. Missing addons, fields, selectors and ambiguous managed markup refuse.plan_website_indexing(env, payload, instance=...)is the read-only dry-run function for Launchplane's generator to invoke; this does not add a CLI or enable a target operation. The producer must accept the new payload object and regenerate its embedded consumer from the reviewed devkit commit. It returns the selector, origin, supplied state, native field, SHA-256 of the current and proposed head,changed, and managed-element presence before and after.apply_website_settingsuses that same plan, preserves the caller's transaction, verifies native database readback and printswebsite_indexing=followed by that JSON plusreadback_verified. It never commits. This is tentative transaction evidence until the caller commits successfully; the legacy return digest andwebsite_settings_sha256retain their meaning. A savepoint rolls back this consumer's writes on failure, and an outer transaction rollback also restores the prior head. A rollback after an already committed transition must supply the authoritative restored state through this same consumer; no recorded production state is inferred here.Prelaunch prepends one identifiable robots
noindexmeta element to nativewebsite.custom_code_head; live removes only that exact managed prefix. All pre-existing head content is retained. Indexing-only payloads do not change analytics, consent, robots.txt, domain, routes, pages or other sites. Live removes only this protection; independent existing indexing choices, including the legacy accepted setting, continue to apply. The setting covers HTML pages rendering the native website layout, not every downloadable resource. It neither removes sitemap entries nor proves removal from search results. Existing robots rules remain unchanged: crawlers must be able to fetch a page to observe its noindex directive, as described by Google. -
Legacy setting-shaped inputs such as
ENV_OVERRIDE_CONFIG_PARAM__*,ENV_OVERRIDE_AUTHENTIK__*, andENV_OVERRIDE_SHOPIFY__*are still accepted as a compatibility input and converted into the same typed payload, but they cannot be mixed with stackodoo_overrides. The checked-in sample stack uses typedodoo_overridesinstead. Unrelated devkit control keys such asENV_OVERRIDE_DISABLE_CRONremain available until they get their own typed local contract. -
Local runtime environment input comes only from
ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON. Leftover devkit-local.env,platform/.env, orplatform/secrets.tomlfiles are a hard conflict so the runtime boundary stays single-source and fail-closed. -
Local restore, bootstrap, and update forward opaque values literally from that input, including passwords,
ODOO_KEY, andENV_OVERRIDE_*values. Dollar expressions, tilde prefixes, quotes, backslashes, and surrounding spaces are preserved. Supply actual values; a non-path alias such asENV_OVERRIDE_COPY=${ODOO_DB_PASSWORD}stays that literal string. Opaque values travel through the process environment, keeping them out of command arguments and command-failure diagnostics. -
Only these local data-workflow path fields expand home prefixes and
$NAME,${NAME}, and${NAME:-default}references:DATA_WORKFLOW_SSH_DIR,DATA_WORKFLOW_SSH_KEY,ODOO_FILESTORE_PATH,ODOO_UPSTREAM_FILESTORE_PATH,ODOO_DATA_WORKFLOW_LOCK_FILE,OPENUPGRADE_SCRIPTS_PATH,ODOO_ADDONS_PATH, andLOCAL_ADDONS_DIRS. References prefer the supplied input, then the host environment, then the default for an absent variable. References to other path fields resolve recursively; cycles fail with a value-free error. Host forwarding never expands substituted values again. Unknown references without defaults remain literal. Do not place credentials in paths: paths may appear in filesystem commands. This contract covers host forwarding; downstream whitespace and path normalization are separate.DATA_WORKFLOW_SSH_DIRalso supplies the two read-only Compose SSH bind mounts. Devkit resolves it with the same path rules before Compose starts, so home, variable, default, and recursive path references agree with workflow forwarding. It must resolve to an absolute host path; empty or relative paths fail before any Compose command. The original runtime env file and opaque values remain literal. A failed script-runner start stops the workflow before filestore preparation or stopping web; it cannot continue using an older runner. This input check applies to all devkit runtime Compose commands, including build, down, and logs. Correct the supplied SSH directory and rerun runtime select to refresh an existing env file before retrying those commands. -
Non-local
restore,workflow bootstrap, andworkflow updatenow fail closed with Launchplane handoff guidance. Devkit should not grow arbitrary checkout remote mutation flows; add or use a Launchplane service route first. -
Odoo runtimes whose
PLATFORM_INSTANCEis notlocal,dev, ordevelopment(an empty or unset value included) fail closed on unsafe startup credentials: the master password must be present and non-default, and an explicit admin password must be configured before the startup wrapper marks the runtime usable. Those three developer instance names may omit the admin password, but previews, testing, and prod must not expose an Odoo database with default credentials. -
A restore onto any non-production instance commits a login-preservation marker before the first registry load, including with
--no-sanitize. Restore, subsequent startup and maintenance preserve the copied login passwords, including the configured administrator, even when the lane's isolated bootstrap password differs. KeepODOO_ADMIN_PASSWORDconfigured: public preflight still requires it, and fresh databases still use it. The active default-password refusal still runs; an unsafe restored login fails preparation instead of being rotated. Production ignores the marker. Infrastructure credentials, user API keys and outbound integrations still follow the existing sanitization and lane-secret rules below. -
The same SQL boundary handles copied read-only QA identities using the image's registry-free
launchplane_readonly/policy.py. It qualifies the runtime/database owner, then checks a foreign account's explicit and transitive roles, authentication state and exact QA witnesses before credential clearing can obscure them. A qualified account copied to another recipient is deactivated, stripped of authentication and ownership bindings, and assigned a checked retired login so the recipient's fixture can create its own account. Its user ID and all references remain. Only the qualified QA fixture journal and product operative marker are removed, in the same transaction; other journals and SAMPLE data remain. A separate connection verifies retirement after commit before any registry load. Staff collisions, unknown ownership/runtime/witnesses, conflicting roles or retired-login collisions refuse without partial changes. A same-recipient refresh preserves its account and witnesses, including across preview generation changes. Its QA role, authentication or witness drift stays with the native QA guards and does not block unrelated registry startup. The copied flag alone never selects retirement. Missing optional QA schema and empty retained ownership columns do not make older or unavailable optional policy a new startup prerequisite. Orphan QA journals remain unchanged and never authorize a cleanup. This does not activate an account, clear a use hold or perform browser QA. -
Devkit-managed startup Odoo launches and data workflow Odoo shell subprocesses prepend
/volumes/scriptstoPYTHONPATHso shipped runtime helpers remain importable from generated shell snippets. -
Data workflows close their module-state metadata transaction before launching an Odoo install/update subprocess. This prevents the parent workflow from retaining a read lock on
ir_module_modulewhile the child process performs schema-changing module upgrades. -
Startup reports
launchplane_settings_applied=false reason=no_payloadwhen there is no override payload, orreason=no_managed_settingswhen the supplied payload has no managed settings. A successful managed settings apply reportslaunchplane_settings_applied=trueafter commit. -
Local restore, bootstrap, update, init, and OpenUpgrade workflows require a successful web stop before proceeding and restart web only after the operation succeeds. A failed stop, operation (including exit code 10), or restart fails the command. After a failed operation, web stays stopped for recovery; correct the failure and rerun the same local workflow to restart it on success. If the operation completed and only the web restart failed, correct the startup problem and use
platform runtime upwith the same manifest; the data workflow does not need to run again. Interrupting an operation also leaves web stopped. -
Upstream restores capture the custom-format database dump in a private
.<database>-upstream-restoredirectory beside the workflow lock file (on the mounted data volume by default). Keep that lock directory on persistent storage when overriding its path. The complete archive is read withpg_restorebefore changing the target database or filestore. Shell pipelines fail when any component fails, including SSH. Incomplete captures usedatabase.partial; only a validated capture replacesdatabase.dump. The last verified dump survives failed recaptures and container recreation. Later failures keep that one dump at the logged path for operator recovery; a fully successful restore, migration, and sanitization removes it. The retained dump contains unsanitized source data and credentials; restrict access and remove it after operator recovery if no successful retry follows. This is temporary recovery retention, not a backup of the previous target or protection against data-volume removal. An early capture failure leaves target data unchanged. Once the old target database has been dropped, any later failure (a partialpg_restore, the filestore copy, OpenUpgrade, sanitize, addon install or update, or the Launchplane settings apply) drops the restored database. A failure while blocking outgoing mail right after that apply also drops it. Web never boots an unsanitized production copy. Only later failures (the core schema check and GPT user provisioning) keep the sanitized database. Filestore capacity is checked again after capture so the dump's space is reflected before replacement begins. Capture and validation now finish before filestore copying begins, increasing the time web is stopped for large restores. -
ODOO_UPSTREAM_USERselects the SSH login. Withroot, the dump retains the existingsudo -u ODOO_UPSTREAM_DB_USER pg_dump -Fcbehavior. Other logins runpg_dump -U ODOO_UPSTREAM_DB_USER -Fcdirectly, without sudo. Configure that database role with read-only privileges and server-side authentication for the SSH user. Devkit does not create users, grant privileges, or deliver database passwords to the upstream host. Restrict the upstream SSH key on the server to the exact dump and read-only rsync of the configured filestore. Filestore capacity estimation uses an rsync dry-run with statistics through that same read-only source, so the key needs no remoteduor general shell access. The real transfer pulls from upstream; its--deleteapplies only to the local destination. A forced-command wrapper must admit both rsync sender forms, the dry-run and the transfer, while refusing receiver commands. Withrrsync -ro DIR,ODOO_UPSTREAM_FILESTORE_PATHnames a path within that restricted directory; use/whenDIRis the exact source database's filestore. Do not reuse its absolute host path unless the installed rrsync supports that mapping. The existing best-effort behavior when size estimation is unavailable stays. UseDATA_WORKFLOW_SSH_KEYfor a dedicated identity. Provisioning upstream access and changing hosted settings remain outside the local devkit commands. -
An offline migration that already holds an isolated restored database can call
OdooDataWorkflowRunner.prepare_restored_modules(do_sanitize=True). Normal restore calls the same sequence: OpenUpgrade, credential clearing, addon installs, the Odoo 19 website refresh or configured updates, install-queue reconciliation and final credential clearing. The offline caller first usesprepare_isolated_copy_before_registry(purpose="migration"), checks core schema health afterward and discards the copy on any error. Lane settings, admin provisioning and filestore ownership remain normal restore's later steps. -
Every upstream restore onto an instance that is not explicitly production (
PLATFORM_INSTANCEprodorproduction) clears the production integration credentials and signing keys that the copy brought with it. An empty or unknown instance counts as non-production, and the container workflow's--no-sanitize/NO_SANITIZEsetting (not exposed byplatform runtime restore) does not skip this step. It deletes the Shopify store credentials, the store URL and test-store flags, and the import cursors. It cancels open Shopify sync jobs and turns off the Shopify crons. It preserves external IDs, external-system bindings, record relationships and product export history. Only a managed Shopify development-store apply ontestingclears Shopify IDs and export state for the existing Reset Shopify flow. It clears secret-shapedebay.*parameters (these are never kept on a copy), the PrintNode key and the Mapbox, Unsplash and Tenor tokens. It deletes the Fishbowl, RepairShopr sync and cm_data database connections (host, database, user and password), so import crons that install hooks turn back on cannot reach the production databases. It disables every payment provider that reaches a remote service (anything butnone,customanddemo). Odoo 19 providers enter the disabled state, including providers in test mode; Odoo 20 providers become inactive. Copies retaining both schema fields have both disabled, and readback rejects either enabled field. It blanks the providers' credential fields. It deactivates every incoming mail server and blanks its password and its Gmail or Outlook OAuth tokens. It also deletes the Gmail and Outlook OAuth app client secrets (google_gmail_client_secretandmicrosoft_outlook_client_secret). Outgoing servers lose their OAuth tokens too. It replaces each IAP account token with a new one, so the copy cannot spend production's credits. It deletes every user API key (res_users_apikeys); GPT users get theirs again after the restore. It deletes both web push VAPID keys along with every push device and queued push; Odoo generates new VAPID keys on demand. It regeneratesdatabase.secret.database.uuidis kept: it identifies the database, authenticates nothing, and Odoo's own neutralize keeps it too. -
For an OpenUpgrade target in major 20, selected from the explicit target or
ODOO_VERSION, the existing packaged runner uses nativebase,webloading and selects its SQL-onlyprepare_core_references_20.pywith native--pre-upgrade-scripts. ExplicitODOO_INSTALL_MODULES(comma- or colon-separated) selects initial installs with--initin the same invocation as--update all, so their native post-init hooks run before module cleanup. Unset, empty andAUTOdeclarations add no initial installs. Other targets do not consume this selection during OpenUpgrade. The later restore install phase cannot substitute for an initial migration ownership handoff. It retains the discovered upgrade roots; it does not load the Odoo 19 OpenUpgrade framework or reset module states or versions. Subsequent addon maintenance and release-plan shells also skip that framework. Other targets retain their existing OpenUpgrade selection. Native 20 rejects--reset-versionsbefore asset discovery or database access; run it without that flag to preserve the installed versions.OPENUPGRADE_LOAD_FRAMEWORK=trueexplicitly loads an available framework for major20 too, including subsequent addon commands. Supply the matching upstream20 source first inODOO_ADDONS_PATH; its manifest must match the selected target series. This does not convert a19 framework into a20 framework. Unset keeps the existing selection above. For a disposable saved19-to20 diagnostic, setODOO_MIGRATION_DIAGNOSTIC_REPORTto a private JSONL report path, withODOO_COPY_PURPOSE=migration,PLATFORM_INSTANCE=rehearsaland target20.0. Independent preservation cohorts then report and continue. Each failed cohort rolls back to its savepoint and skips its changes; remaining guards inside that cohort are reported as blocked. Other cohorts still run and apply admitted changes. Mail and Payment migration preservation use the same reporting path. Without the explicit diagnostic report, the original fatal checks and before-effect behavior remain in force. Diagnostic reports are evidence of observed failures, never proof of a successful migration. Use a fresh private report path per diagnostic job. The ordinary restore entry refuses this mode before changing data.run_openupgrade.pyrefuses a normal success result when reporting is enabled. The supported entry is the OPW saved19 diagnostic driver, which owns its explicit diagnostic status, complete observed list and blocked coverage. A report pass describes a check, not committed migration effects. Native 20 also selects the packageddocker/scripts/nativeupgraderoot. Its Mail pre-stage reuses the guarded stock-view conversion from website227. Before any shared bridge effect,mail_view_20.pylocks and compares the complete server-action inheritance family against the independent source19 reference. The Mail stage rechecks its old version, live binding and complete target architecture before replacing onlyarch_dbon the same view. The replacement comes from the digest-bound target20 Mail XML; aliases, previous architecture and write attribution remain unchanged. Unknown, edited, translated, rebound, unowned andnoupdateinputs refuse. The existingCM_WEBSITE_MAIL_REFERENCEandCM_WEBSITE_MAIL_REFERENCE_SHA256inputs remain the reference contract for shared consumers. A missing or changed artifact refuses before effects. The version-qualified Payment pre-stage uses a separately qualifiedpaymentwitness in that same authenticated profile. It supports only the obsolete stock global-method family with disablednoneproviders whose connector modules remain uninstalled. The custodian binds material values and links to exact public native source, and qualifies each original image representation against native stock output before recording its immutable byte digests. Usepayment_20.qualify_payment_source(profile, witness, source_version)on the authenticated retained profile before its existing serialization and digest step. It returns a new profile and preserves every existing reference component; it requires no fresh database capture. Source and receipt metadata remain audit evidence, not extra runtime authentication by hash shape. This is source and original-artifact evidence, not an invented independent database reference. Read-only before observers usepayment_20.observe(cursor)to collect the same snapshot using only SELECT statements, without an Odoo import or a table lock. For a consistent snapshot across those queries, the caller supplies aREPEATABLE READ, READ ONLYtransaction. Reusesource_witnessandadmitto qualify it; observation alone grants no retirement authority. Nativecapturetakes its table lock before calling that same collector, andretirestill uses capture and the incoming-reference checks before effects. Unknown fields, translations, owners, configured providers, payment history, images or outside references refuse before deletion. Admission checks the whole method, alias and field-attachment cohorts, including incoming links to the retiring aliases and attachments. Only exact admitted internal edges are allowed; other reference protections still apply. The pre-stage retires those database records before Payment initializes its required provider ownership. Provider records, configuration and every file remain unchanged. Native provider addons own any later scoped methods; the bridge creates no default provider or replacement catalog. Final readback must prove the selected record fates, surviving provider settings and native constraints; a completed registry alone does not establish preservation. A source custodian can usequalify_source_family(profile, source_manifest, manifest_sha256, automation_source)to add the one stock Base Automation extension without changing the independent reference. The separate immutable source manifest containssource_image,modules,base_automation_xml_sha256andbase_automation_defaults. Its image must match the independent profile's immutable source image; its complete source-derived module/version closure must preserve every reference component version. Supply the manifest's separately recorded digest, never a manifest derived from candidate rows.automation_sourceis the exact source19base_automation/views/ir_actions_server_views.xml; its bytes must match the manifest. Its declared fields and architecture supply the added view.base_automation_defaultscontains the separately source-qualified common fields: all independent target fields exceptname,model,arch_fs,arch_dbandxmlids. They match the independent stock Mail stored shape, or the exact independent SMS server-action extension shape when its only common-field difference is the legacycustomize_show=Falseversus NULL. The SMS anchor must retain its exact ownership, model, form/extension kind, parent and all other common fields. This qualifies persisted stock storage, not universal creation defaults; no column is ignored or normalized and no candidate row supplies the expected shape: its diagnosis selects between two independently observed stock storage shapes. The profile embeds and revalidates both source artifacts. Artifact hashes prove integrity; the custodian still establishes their source provenance. The candidate's full family and module closure still have to match, including every extension field, alias and locale. Candidate rows never supply stock defaults or architecture. Group restrictions anywhere in the family refuse before effects too. The custodian writes a new immutable profile and binds its digest through the existing inputs. This adds no general view-rebase support or native19 run. Optional stored module versions may remain NULL in that exact source closure; every independently referenced component still needs its matching version. This is input metadata, not a stock-view default derived from candidate rows. The website's existinghistory.source_contract.modulesbinding is also accepted with exact closure/component and whole-family checks. Its separate target-module/reference completeness checks remain in its conversion preflight. The shared stage checks the binding, not its original provenance. If both closure bindings are present, they must agree exactly. Build an OPW-specific profile from the retained independent reference without unrelated website history; do not silently discard a website binding inside the helper.From the artifact's
/venv/bin/python, qualify an already retained independent stock reference without starting Odoo19:import hashlib import json import runpy from pathlib import Path helpers = runpy.run_path("/volumes/scripts/mail_view_20.py") profile = json.loads(Path("/workspace/independent-reference.json").read_bytes()) qualified = helpers["qualify_source_family"]( profile, Path("/workspace/source-closure.json"), Path("/workspace/source-closure.sha256").read_text().strip(), Path("/workspace/source19/base_automation/views/ir_actions_server_views.xml"), ) payload = json.dumps(qualified, sort_keys=True, indent=2).encode() + b"\n" with Path("/workspace/qualified-reference.json").open("xb") as output: output.write(payload) print(hashlib.sha256(payload).hexdigest())
Copy that qualified file into the job through the existing heavy manifest
copiesinput and set its job-containerenvironmententriesCM_WEBSITE_MAIL_REFERENCE=/workspace/qualified-reference.jsonandCM_WEBSITE_MAIL_REFERENCE_SHA256to the recorded digest before invoking the existing data workflow. These inherited names are the shared contract for OPW too. The saved independent profile, complete source manifest and its separately supplied digest must be qualified by their custodian before this command; hashing does not establish provenance. A missing independent input remains a source-qualification task, never a new native19 behavior run. When adopting the shared stage, remove the equivalent tenant-owned Mail execution pre-stage so onlymail_view_20.pyperforms the transition; retain tenant-specific reference/completeness helpers. Selecting both execution stages would attempt the same transition twice. Other tenant upgrade roots remain selected. Before native security data loads, the bridge releases registered modules' legacyir.model.accessandir.ruleXMLID names using the upstream-derived transition already used by OPW. It appends the legacy model suffix, retaining each alias's ID, record binding, ownership andnoupdate. Custom/export/Studio aliases, aliases without a registered module owner and all legacy policy rows remain untouched. Already released names and nativeir.accessbindings survive replay unchanged. Any destination collision refuses during the shared preflight, before any bridge effect. Native 20 loads its own stock defaults; this step neither converts legacy rights nor settles own-addon or data-only policy disposition. The legacy model suffix is reserved for released names; an original alias already using it needs product-owned disposition before native loading. When adopting the shared step, remove the equivalent tenant pre-migration rename so the later native module migration cannot append the suffix again. For the code-owned stock valuation client action moved fromstock_accounttoaccount, the same preflight binds the native target XMLID to the existing action. It preserves the action ID, path and incoming links, then lets native Account data update the report model and context. The old XMLID remains protected against end-of-load cleanup, so both XMLID links stay usable. This is one named ownership handoff, never a merge by matching paths. Conflicting targets, protected originals, extra aliases, ambiguous physical storage, custom fields or context, and edited or translated names refuse before any bridge effects. Preparation replays before native loading; the bridge leaves completed major-20 crossings untouched. The target's own XML supplies its name, tag and path. The renamed Stock product-template form and its Stock Account child use the same identity preparation: their new native20 XMLIDs bind to the original view rows before XML loading. Both sources, destinations, ownership, restrictions, translations, topology and independent current/history architectures qualify before any preparation effect. Supported public18 history need not equal the current public19 architecture. The canonical source records and provenance live underdocker/scripts/legacy/stock/. Alias insertion leaves view data, audit fields, parent references and history untouched; native XML loading owns subsequent updates and normal history rotation. Ordinary native END cleanup removes the old aliases while the loaded new aliases retain the same records. Same-record preparation reentry adds nothing. An installed Stock Delivery contribution keeps its existing alias and is qualified as the finite unchanged child, without staging, deactivation or another binding. Custom or unknown source/history, translations, extra owners or descendants, restrictions and conflicting or protected targets refuse. Native loader/data/UI proof remains with the migration owner's existing affected-input batch. The public and private Org Chart window actions move fromhr_org_charttohrby the same named binding: each retained action ID, path, incoming references and old protected alias stay intact. Native HR loads its own action, including mobile hierarchy mode. Preparation accepts only the source19 stock actions with their nonempty English HTML help matching the selected native20 definition. It refuses changed or translated names/help, context, domain, defaults, attached views, group restrictions, extra aliases and conflicting physical storage or ownership before any bridge effect. The private action preserves its existing mobile hierarchy mode and its explicithr.view_employee_filtersearch binding; that XMLID must resolve to the retainedhr.employeesearch view. The public source action retains its distinct kanban mobile-mode admission and empty search binding. The public and private employee hierarchy action-view links, and both department hierarchy links, also transfer their XMLIDs to HR on their retained rows, avoiding native unique(action, view mode)collisions. Each owner, typed parent action, sequence, unset view reference, defaults and aliases must qualify before any bridge effect. Existing neighboring links and incoming references remain unchanged; no view selection or action order is reconstructed. All six moved action/link records qualify together. Other HR records and security stay with their existing owners; this action handoff is not a whole-module merge or uninstall. Native cleanup keeps the protected old alias, while a later native uninstall of only the old module retains the action through its HR XMLID. The exact historical stock19 public hierarchy also gains its native HR XMLID on the retained row. Its old alias is protected, and native HR owns the new architecture, avoiding selection of a second, older primary hierarchy. Native20 already includes the Org Chart in its public employee form, so the corresponding qualified stock19 form extension becomes an empty inheritance spec on its existing row. This changes only its current architecture; previous architecture, IDs, links, attribution and all other columns remain unchanged. No view is deleted or deactivated and no module is uninstalled. The stock source is the exact official historical 19 file underdocker/scripts/legacy/hr_org_chart/. The hierarchy also accepts the exact shipped19 image-options architecture retained inhr_employee_public_hierarchy_options_19.xml, as current or previous history. That extract comes from/odoo/addons/hr_org_chart/views/hr_employee_public_views.xmlinghcr.io/cbusillo/odoo-tenant-opw@sha256:723eee28cf5e3dc5e4aec827002acb2d5413556569776c3874eafb43e9ca7b70; the full source file SHA256 is21caf7642174c099e31c49004a067b6fba0893ef5c71d8750a616f0b777f946c. Both stock forms are compared in full; changed preview values, added options and other attribute differences still refuse. Its hierarchy may retain the exact stock18 architecture as previous history while the current architecture still matches stock19. The public form extension likewise accepts exact18 history from the public record in the sibling 18 source, retained alone inhr_employee_public_form_18.xmlin the same catalog. Its current architecture must still be stock19 or the already-empty spec; a current18 architecture remains unsupported. The HR and Calendar view plans also preserve native Website's untracked, public creation defaults alongside their qualified unset storage; tracking, restricted visibility and other unqualified presentation state still refuse. Both views must qualify before any bridge effect. Extra owners, translations, custom current/previous architectures, populated custom fields, restrictions, children, dashboard customizations and competing primary hierarchies refuse. Native HR must still define the same primary hierarchy and own its form's Org Chart. This is a two-view crossing, not a general rule for retired modules: missing source modules are skipped by the native graph, and end-of-load cleanup only visits updated modules. Actual registry/UI qualification remains with the migration owner in the existing affected-input native20 batch. Like the stock valuation handoff, preparation replays before native loading; completed major-20 crossings are left to native Odoo. The bridge resolvesbase.state_in_orfrom the source database and addsbase.state_in_odon that same state row, checking the running target's own state CSV and the source country binding. With an installed Website, it also bindsbase.default_websiteto the existingwebsite.default_websiterow. Those source aliases, other owners, record data and incoming links are untouched. Replay keeps existing target metadata; missing, dangling, conflicting or unsupported custom identities refuse before either alias is inserted. An updateable custom state name refuses rather than being overwritten by target data; a protected name retains its protection. An updateable source website or unprotected existing target website alias refuses rather than accepting fresh base defaults. Before the base bank form updates, an installed Account's surviving primary bank form retargets itsacc_numberXPath toaccount_number. An installed HR Calendar's primary partner search view likewise retargets its stocktype_companyfilter locator tofilter_vat_set, before the base search view reloads. Both transitions use the same preflight and update every stored language. Only the selectors change; view and alias identities, custom content, incoming references, history and metadata stay intact. Protected ownership, unsupported inheritance and absent, duplicate or mixed selectors refuse before any bridge write. Replay is unchanged. Each owning module's later native update owns its full target architecture and validation; this step does not migrate bank or partner data. Installed Calendar also admits exactly its two stock 19 users/privacy extensions before any bridge write. Their complete current architectures, parents, aliases and populated scalar state must qualify; translations, custom edits, previous custom architectures, restrictions, children and dashboard customizations refuse. The bridge preserves the admitted stock 18/19 previous architectures and all other columns, replacing onlyarch_dbwith an empty inheritance spec. This preservation covers preparation: later native XML writes can replace stockarch_prev, and cleanup removes the retired stock row. Custom previous architectures refuse before that path. Native 20 then loads its own calendar-management UI and owns removal of the retired preference XMLID through normal module cleanup. No view family is deleted or deactivated by the bridge. The shareddocker/scripts/legacy/calendar/res_users_views_19.xmlis the exact static source from the qualified 19 image; the 18 file is the official stock history source. These are admission inputs, never target UI templates. Qualification and source bindings are recorded on Calendar transition #332. Odoo20 builds retain their selected runtime's exact, unmodified officialodoo/upgrade-utilcheckout through the existing external-source fetch and release inventory. Final selection adds that runtime-owned source after tenant artifact inputs, and keeps it in the main source list for local builds. Conflicting environment or artifact declarations refuse before source resolution; exact restatements produce one utility checkout. The checkout stays outside Python's upgrade namespace during ordinary startup, maintenance and Odoo19 upgrades. Only the existing native20 upgrade command prepends itssrcto the upgrade paths. Missing, ambiguous or differently pinned sources refuse before starting that command. The runtime support catalog supplies Markdown for both 19 and 20; the Odoo base supplies Docutils. Older runtimes without this source helper keep their existing external-source selection. This follows the upstream recommended source installation, rather than installing an ambient upgrade package. All upstream common scripts remain present and run in that explicit native20 upgrade: they can discover/enroll modules, clean relation metadata, recompute missing partner values with commits and post migration announcements on the isolated copy. Existing credential, outbound-mail, native registry and preservation barriers still apply; the source checkout is not a migration pass. The same global preparation admits the HR type model, fields, storage, aliases and prospective target names before any shared effect. Official helpers renamehr.contract.typetohr.employee.typeand the three HR job/version/employeecontract_type_idfields toemployee_type_idwhile retaining business rows, IDs, translations, audit, incoming links and history. Table renaming explicitly retains custom constraints. Existing type values need not be stock or unused, and protected old-only type aliases survive native data cleanup. Unprotected non-export type aliases must be owned by HR and still declared in the selected target data. Conflicting targets, mixed state, rebound ownership, additional relation fields, many-to-many storage and functional field aliases that upstream would replace refuse before effects. Export aliases retain their supported native semantics; generated conventional aliases follow native bookkeeping. Preparation can replay its complete handoff. Native primary HR views keep their historical architecture until their own XML reload; that writes the target architecture before validation. The bridge performs no additional architecture rewrite beyond the upstream reference/domain adapters, and native validation remains authoritative. The oldhr.version.employee_typeselection is a separate concept, not a field rename or a label-based mapping to the new relation. The recorded Employee decision fills only empty version links whose old classification isemployee, using the existinghr.contract_type_employeeidentity. Existing nonempty links and their audit/history remain unchanged. Missing or null old classifications get no backfill; other populated categories refuse before any shared effect. Backfill requires supported selection storage and an existing Employee XMLID/record declared by the selected target data, and preparation can replay without replacing a filled link. An installed HR with its stock19hr.res_users_view_form_preferencesstages only that child's architecture from the selected native20 HR XML before any native parent-view write. Native combination validates same-module descendants when their parent reloads; leaving the old email, phone and title selectors in place would fail before the child's own XML record reloads. The qualified HR primary architecture is identical in 19 and 20 and stays unchanged. Stock current and previous architectures retain comments, attributes and nonblank text in the shared comparison. The HR primary and MailBot preference child can retain their exact stock18 previous architectures after an 18→19 upgrade; these histories remain unchanged during preparation. IDs, aliases, incoming references, users/preferences, roles, attribution and every other column remain unchanged during preparation. The two surviving stock MailBot descendants are checked against their own 19 source and stay untouched; the known old Homeworking descendant also stays untouched, with its separate module disposition left to the migration owner. Unknown descendants, custom/translated/restricted/protected/rebound state, changed native ownership or primary architecture refuse before any bridge effect. A source without this HR preferences XMLID is outside this transition; the ordinary HR users form is not rewritten. Replay recognizes the staged target architecture. Later native XML loading owns its UI and normal history writes: nativeir.ui.view.writesaves the staged current architecture intoarch_prev, so preparation's preservation is not an end-to-end promise that obsolete stock19arch_prevstays in that column. Original source and backup custody remain separate. The exact public admission inputs underdocker/scripts/legacy/hr/andlegacy/mail_bot_hr/come from official 19. The two stock18 history records are extracts from official 18. Every effective saved user privacy must equal the global native fallback (publicwhen the parameter is absent); divergent preferences or ambiguous settings owners refuse before effects. User settings remain unchanged. This bounded crossing requires absent new calendar ownership tables, empty events and recurrences, and no references to the removed preference in other views, rule domains, window contexts/domains, server code or saved filters. It does not claim absence of every possible dynamic reference. Native 20's calendar initializer is an install hook, so source admission alone proves neither calendar creation during an upgrade nor registry/privacy behavior. Those remain part of the owning migration's actual-data qualification. Preparation can replay before native loading; completed major-20 crossings are untouched. Before native Discuss initializes its unique direct-chat audience, the same preflight checks every duplicate audience and every stored channel/member column. Only redundant empty partner chats with default state and matching sidebar visibility qualify. Current partner names must explain their generated chat names; read state, customization, history, aliases or incoming references refuse before any bridge write. Unknown populated columns refuse. The stored livechat weekday and start hour may be populated only when they match the native projections of that chat's UTC creation date; NULL values remain supported before computation. Livechat configuration, help state, history, notes, ratings and relations retain their state and reference guards. Physical references without foreign keys, materialized reference storage, company-dependent references, and custom deletion triggers also refuse. The reference scan covers channel and member foreign keys, indirect model references, field defaults, aliases, exact stored Reference values and JSON references. JSON Properties definitions naming either Discuss model refuse as unsupported storage. The scan reads physical base tables; ordinary views and inline-query reports hold no rows of their own. Relational metadata checks physical columns without guessing a table name from its model. Many-to-many storage retains its explicit table ownership and integer-type requirement. Ownerless same-name text columns may hold native JSON notification tuples: a database and topic, or a database, known model and record ID with an optional string topic. Candidate model/ID pairs and ambiguous model/Reference/ID prefixes or topics refuse; unrelated tuples stay unchanged. Unknown text encodings, other unsupported types and uncertain populated matches without foreign keys refuse. Pinning is compared by the native visible-state rule, without comparing application and database clocks. After all candidates pass, the lowest channel ID survives unchanged and the redundant chats and their members retire. Their old links stop resolving, as permitted by Director decision Q1113; this creates no redirects or general chat-merging path. Replay leaves the survivor unchanged. A historical partner rename or unqualified stored state can therefore require further source investigation rather than automatic retirement. For installed Resource, the same preflight recognizes legacy nonworking lunch rows before native recomputation. Only zero-duration breaks on single-week, undated calendars without sections or resource-specific intervals qualify. The optionaltwo_weeks_calendarflag may be false or SQL NULL, which Odoo reads as false; true still refuses. This does not default other NULL fields. Within calendars containing lunch rows, overlaps with work (including native full-day work), unsupported periods or clock ranges, and populated custom break fields refuse. Calendars without lunch rows stay outside this treatment. The shared reference checks also cover attendance foreign keys, indirect and Reference links, XMLIDs, defaults, JSON/Properties, physical relations without foreign keys, materialized storage and deletion triggers. Referenced breaks refuse rather than losing their links. After the whole bridge passes preflight, only the qualified break rows are removed; work intervals and stored calendar totals stay unchanged, so native 20 keeps the nonworking gap. Replay has no remaining lunch rows to change. This does not split overlapping work or translate two-week, section or date semantics. Same-major 20 updates do nothing in this bridge. Its qualification ends at the reference/base-data boundary; security conversion, later cleanup, tenant hooks and a completed major-version migration need their own proof. This selection does not replace the restored-copy credential boundary or authorize a Client data run. -
Before post-deploy maintenance, update-only work or any restored-copy Odoo process (including OpenUpgrade), the runner commits a SQL-only preparation transaction. Restored copies reuse the credential-clearing rules below; ordinary maintenance preserves lane-owned integration IDs, API keys, signing keys and push devices. The transaction applies managed
config_parametersand Shopify credential values from the existing typed payload and fences non-production outgoing mail. Crons are disabled only when requested restore sanitization calls for it, preserving--no-sanitize. Requested restore sanitization is in this transaction too, before registry loading rather than after OpenUpgrade. An independent database connection checks that the stripping, overrides and fences committed; a failed or rolled-back commit stops all later Odoo processes. A fresh transaction witness also proves a commit when all settings were already correct. Explicit production lanes preserve their integration credentials except for supplied managed overrides. Both production aliases are exported to the installed settings addon as its canonical production lane, so aproductionlane does not get non-production Shopify clearing. The non-secret commit witness is visible in System Parameters. Non-production retained-integration exceptions keep their existing scope. Addon override targets unused by the installed settings consumer stay unused. Credential parameters use the supported typedconfig_parameterssurface; new credential-capable adapter behavior must extend the SQL boundary too. On post-deploy and restore runs, Authentik's client identifiers and presentation settings, Shopify's URL/dispatcher bookkeeping and website bootstrap still use their installed ORM adapters after the credential transaction. No addon code or Odoo shell is used to strip credentials. Callers must keep other workers stopped until preparation and maintenance finish, including the scheduler during a delayed testing store change. Use the existing stopped-runtime maintenance procedure before starting the web process with changed store settings; an environment-only plan stages values and does not authorize a running scheduler to overlap the transition. On readback failure keep those workers stopped and use the supported restore/reset procedure before resuming; this is not a traffic-switch mechanism. Restore reasserts the boundary after module work before running further settings/admin hooks, because install data can re-enable integrations. OpenUpgrade also reasserts requested sanitization before addon installation, because migrations can reactivate crons. Update-only stages the credential values and updates modules; it does not activate integrations or complete ORM bookkeeping. Use--post-deploy-maintenancefor that complete operation. -
Addon maintenance starts Odoo only when the computed install or update list contains work. In the legacy install-before-update path, already installed addons skip the install pass without loading a registry; the subsequent upgrade still runs. This lets upgrade migrations repair tenant state before ordinary registry guards run, while preserving the guards and completion evidence requirements.
-
ODOO_RESTORE_KEPT_INTEGRATIONS(comma-separated) names integrations whose restored settings stay, using the integration names of Launchplane's read-back:shopify,printnode,fishbowl,repairshopr,cm_data,payment,incoming_mail,iap,mapbox,unsplash,tenor. Keepingincoming_mailpreserves incoming-server settings, passwords and OAuth tokens, along with the Gmail and Outlook OAuth app client secrets. Outgoing server OAuth tokens are cleared regardless of that allowance. Launchplane should set it from a lane'spre_liveandread_only_sourceallowances, the two kinds that permit the lane to hold production's own values. Adev_storeintegration is not listed: the restored production values are cleared and Launchplane applies the development account after the clearing. Unset keeps nothing, so until Launchplane passes it, a restore clears every integration, including allowed import sources. Web push, user API keys anddatabase.secretare cleared whatever it says, and other or unknown names are logged and ignored. -
The step runs right after
pg_restore, before any Odoo code, and again after the addon install and update. A read-back then fails the restore if any restored value of a cleared integration survived, by parameter or by table column. It also fails the restore if a remote payment provider is still enabled or an incoming mail server is still active. Launchplane's settings apply runs after that, so the instance's own settings are kept. The key list lives in one place, at the top ofdocker/scripts/run_odoo_data_workflows.py. -
Release/deploy ownership for remote environments stays in
launchplane, even when the same tenant manifest is used to anchor local runtime context. -
Every runtime subcommand accepts
--instance <name>, but it is not a remote mutation hook: onlypublishaccepts a non-local instance, and every other subcommand requires--instance local. The stable remote lane model istestingplusprod; those mutations belong in Launchplane service routes and reusable workflows. -
platform runtime logsandplatform runtime psqlare intentionally local-only helpers for manifest-backed debugging. They require--instance localand fail closed for non-local targets instead of falling through to an implicit remote path. psql readsPOSTGRES_PASSWORDinside the database container intoPGPASSWORD, keeping it out of Compose and psql arguments and command-failure diagnostics. -
platform runtime downfollows the same local-only rule and gives tenant manifests a native way to stop the local compose stack without routing through another repo. -
platform runtime buildfollows the same local-only rule and gives tenant manifests a native build-only entry point when operators want image prep without starting the stack. -
platform runtime publishis the release-handoff path. It requires clean tenant/devkit/shared Git commits; stages only tracked regular files; hashes the exact support/runtime and tenant lock bytes; resolves configured addon selectors to exact Git SHAs; resolves both base images to immutable digests and verifies their OCI index/platform source/revision annotations (or config labels on unannotated legacy bases); dry-runs the exact exported support and tenant requirements against each selected base-runtime platform's installed package constraints; then builds and pushes the requested artifact tag. Base-package overlap fails before Buildx starts instead of surfacing only inside the artifact image build. -
After the push succeeds, publish reads the immutable artifact index digest from Buildx metadata, extracts each target platform's dependency sidecar from that digest, verifies the sidecars against the staged lock hashes and source commits, and writes Launchplane artifact-manifest schema v2. The manifest includes base-image/build-tool provenance, both uv locks, per-platform exact Python package inventories, and external compatibility descriptors.
-
The artifact path uses
docker/artifact.Dockerfile; local Compose continues to usedocker/Dockerfile. In dependency provenance, each lock's repository, source ref and path identify its source inputs (docker/runtime-python/uv.lockor tenantuv.lock); its hash attests the exact derived bytes installed in the image, not the original committed blob. Existing immutable base-image provenance identifies the base used for derivation; no additional receipt or artifact schema is introduced. Nested shared-addon source markers keep installed shared distributions attributed to the shared repository commit. -
Publish does not mutate
/venvafter dependency evidence is written. An OpenUpgrade or other Python dependency must be represented by the locked support/tenant catalogs or by exact external compatibility evidence; an ad hoc post-sync install is not part of the artifact contract. -
ODOO_PYTHON_SYNC_SKIP_ADDONSis legacy-layout behavior and is rejected for schema-v2 publish because exporting the full workspace lock while skipping a member would make the evidence false. Remove the project from the tenant workspace instead. -
Non-local publish requires the product's build to supply
ODOO_DEVKIT_RUNTIME_ENVIRONMENT_JSON. The payload is authoritative for artifact build runtime keys and deliberately excludes deployment-only database/master secrets. Publish enforces stack-required values only when those keys are part of the artifact-build input contract; local and remote runtime mutation commands retain the full stack required-key gate. The payload can synthesize a missing context or instance instead of requiring hosted lanes in the shared devkit stack. Synthesized contexts do not inherit stack-level install-module lists; their artifact install intent comes from managed-instance required modules plus any repo-ownedwebsite-bootstrap.tomlmodules. Unknown contexts and non-local instances fail closed without the explicit payload. -
Publish-time GHCR credentials can be split by purpose. Private base image reads prefer
GHCR_READ_TOKEN, artifact image pushes preferGHCR_TOKEN, and private source checkout secrets still belong in the transient runtime payload asGITHUB_TOKEN. For selector resolution before the build context exists, CI can also provideODOO_DEVKIT_SOURCE_GITHUB_TOKENorODOO_SOURCE_GITHUB_TOKEN. This lets CI use a repo-scoped package-write token for the tenant artifact while using separate credentials for shared private source repositories and private base images. -
When a repo-owned
artifact-inputs.tomllives besideworkspace.toml,platform runtimecommands use it as the repo-owned source-input contract. Runtime and publish do not fall back tostack.tomlsource selector fields. -
Use artifact-inputs.md for the file schema and example shapes, including a non-Odoo example that keeps the contract repo-owned instead of runtime-specific.
-
Runtime stack config should not declare source repository selectors. Repo-owned source selection belongs in the dedicated artifact-input manifest.
-
Artifact manifests preserve selector intent in
addon_selectorswhile keepingaddon_sourcesas the resolved exact-SHA runtime truth consumed by control-plane release and deploy flows. They also include the resolvedodoo_install_moduleslist so promotion/deploy orchestration can preserve tenant module activation intent when it rewrites a live target environment. -
platform runtime odoo-shellfollows the same local-only rule. It can run interactively, consume a--scriptfile, and optionally tee output into a--log-file, but it is still a manifest-backed local helper rather than a generic remote exec path. It reads the database password from the existing container environment through Odoo's nativePGPASSWORDinput. The password is absent from process arguments and the command shown by--dry-run. Local init and administrator-password helpers use the same container-side database password forwarding. Init refreshes the private runtime environment from the current payload before Compose starts the script-runner, so password changes do not use stale selected values. Admin password updates readODOO_ADMIN_PASSWORDinside the container; scripts and command-failure diagnostics contain no configured password values.
ODOO_COPY_PURPOSE=migration selects the existing SQL credential boundary for
an isolated restored or saved copy. Put it in the Odoo container environment of
the existing heavy-run manifest, carrying that manifest's input/stage identities
and receipt forward with the saved DB and filestore. Every copy loader must bind
the purpose before its first Odoo process, including a saved-stage clone; a
production-like instance label and a database name are not purpose evidence.
There is no new snapshot catalog or queue. Do not pass this setting on a real
Client-accepted production restore; that existing runtime path retains its
production behavior. The operational caller must select migration purpose for an isolated copy.
A source caller loading an already restored database can use the SQL-only API:
runner.prepare_isolated_copy_before_registry(purpose="migration")An SQL-only stage check reuses that final prepared runner and its existing connection:
runner.verify_isolated_copy_before_registry(database_connection=connection)This readback performs only reads on the supplied connection. It neither prepares the copy nor opens a connection, starts Odoo, commits or rolls back. The runner must retain its successful current migration preparation pass; a fresh runner, failed later preparation, changed transaction witness, kept integration or service key refuses. The same readback is used by preparation's independent observer. It checks credential rotation/clearing, outgoing and incoming mail, cron and copied runnable Shopify jobs, while preserving channel identities, cursors and pending export intent. Missing optional addon tables remain supported. Present credential surfaces must be fully readable without active PostgreSQL row filtering; inaccessible columns or missing required core tables fail rather than appearing empty. The caller retains its existing database/stage identity, writer exclusion and transaction ownership checks.
Preparation requires migration purpose and commits and independently reads
back the same boundary before returning. Missing or unsupported purpose refuses
this isolated-copy API; ordinary runtime maintenance and accepted production
restores keep their existing path without that setting. The method exports the
purpose to child processes. The normal restore and standalone OpenUpgrade paths
also accept it through LocalServerSettings, including env files; local exec
forwards it. Keep it on every subsequent saved-copy invocation. The
receipt/manifest is caller-owned provenance, not permission to read private data
or to mutate production.
This boundary clears all copied integration secrets regardless of instance
label, including secret-shaped ebay.* parameters if present. It preserves
Shopify/eBay external IDs, external-system rows, relationships, product export
flags/timestamps and Shopify import cursors for reconciliation. Copied Shopify
jobs are canceled; every cron is disabled, outgoing mail is blocked, API keys
and push devices are removed, payment providers and incoming mail are disabled.
This is mandatory even with --no-sanitize or ENV_OVERRIDE_DISABLE_CRON=false.
Managed credential parameters, kept-integration exceptions and service-key
provisioning refuse before registry loading. Non-credential addon settings are
validated and left unapplied. SQL
non-credential config overrides retain their existing support. ORM integration
settings are skipped on the copy. The startup wrapper repeats the boundary
before initialization, each shell and the server; it disables server cron
threads and refuses a separate scheduler. Other workers must remain stopped
through preparation. Standalone Odoo commands must use the API first.
For the separately selected testing copy, the existing managed Shopify apply
selects its development store with test_store=true, a store key, API token,
webhook key and API version. With no migration purpose, the SQL boundary
clears that copy's Shopify IDs/export state and cancels old jobs before applying
the selected store on a restore or when the stored store key differs from the
incoming key. A delayed apply follows the same path because quarantine removed
the copied store key. Reapplying or rotating credentials for the same store
preserves its IDs and jobs. Startup prepares a testing managed-settings apply
before the ORM shell, after base initialization; data-workflow callers prepare
before their settings apply. Independent readback verifies identities, export
state and open jobs for each reset. Store changes leave cron activation to the
existing settings consumer, preserving offline-check cron state. The existing store
verification and Reset Shopify workflow then run normally. Testing without that
explicit selection and all migration copies preserve identity links. No eBay reset is added. Existing store-separation and release
boundaries still govern reconnection; this setting grants no new authority.
The migration lead owns the tenant hook/runbook integration handback on the source issue. Shared devkit docs retain the generic caller contract only.
- PyCharm should still open the tenant repo directly.
- Codex and Claude Code should start from the assembled workspace root.
For Claude Code, start with
docs/session-prompt.mdand explicitly read the generatedAGENTS.md; it is not loaded automatically. - Generated workspace-root files are a cockpit layer; they are not the source-of-truth repo.
- If a generated file is wrong, change the generator in
odoo-devkit.
Local admin application and startup preserve surrounding spaces and tabs in
ODOO_ADMIN_PASSWORD. Startup and data-workflow hardening also preserve it
when checking whether the configured password already matches.
Blank/omitted admin passwords do not request an update.
Devkit's typed runtime input rejects line separators and NUL bytes. The shared
image startup and data-workflow settings enforce the same single-line contract
for directly supplied ODOO_ADMIN_PASSWORD, before config writes, database
work, or password application. Rejected input is never trimmed into a different
password, and errors omit the supplied value. Correct the operator-supplied
environment value to the intended single-line password and rerun the command
or restart through the lane's normal control plane. Preserve intentional spaces
and tabs; remove an accidental trailing newline at the input source. Existing
passwords stay unchanged when input is rejected. Hosted environment values
and secret bindings remain Launchplane-owned.
Odoo 19 trims surrounding whitespace in config-file values. Local Odoo shell
and initialization commands forward ODOO_DB_PASSWORD as PGPASSWORD inside
the container; Odoo's environment input overrides db_password from the
file and preserves the supplied value. Use the devkit commands for this path;
loading the generated conf alone is not equivalent.
The tenant overlay's scripts/workspace-sync and scripts/workspace-status
keep using the active checkout's workspace.toml and tenant source. In a
linked Git worktree they discover the primary checkout through Git's common
repository directory, find its sibling odoo-devkit, and resolve relative
non-tenant repository paths against that primary tenant checkout. Absolute
paths and repo-addressed managed inputs keep their existing meaning.
The underlying manifest commands accept --related-repo-root /path/to/tenant
for this explicit path resolution. The tracked manifest is never rewritten;
source paths are resolved in memory. Without that option relative paths keep
resolving against the manifest directory. A workspace synced with the option
writes it into the generated devkit commands, such as the dependency check, so
they resolve the same related repositories.
Linked-worktree wrappers default ODOO_WORKSPACE_ROOT to <worktree>/.workspace,
so generated guidance lives at .workspace/<workspace.name>/ and sync does not
replace the primary workspace. This generated directory is ignored by the
starter overlay. An explicitly supplied ODOO_WORKSPACE_ROOT is honored.
Primary checkouts retain their manifest-defined workspace location.
Set ODOO_DEVKIT_REPO to an existing devkit checkout when it is elsewhere or
when testing a devkit task branch. That override selects the executable code;
related manifest inputs still resolve against the primary tenant checkout.
Run status with --check before using guidance and sync when it reports stale
or missing materialization. A missing devkit checkout reports the override
instead of attempting uv in a nonexistent directory. No primary workspace
files or credentials are copied into the task workspace.
For an isolated migration or test that leaves its frozen file-store base unchanged,
put "overlay": true on that directory's existing heavy-manifest copy entry:
{
"source": "/private/frozen-stage/filestore",
"destination": "/volumes/data/filestore/migration_copy",
"overlay": true
}This uses the Q861 Docker local overlay mechanism: the verified staged directory is the lower, with private upper/work directories in a run-owned backing volume. The container reads the lower through the overlay; writes and deletions stay in its upper. Containers in one run using the same source and destination share that upper. Different destinations and different runs get separate writable state. Lower bytes and filesystem identity are checked again after containers stop. Failure keeps the original cleanup custody and stops successor admission. There is no fallback to a copy when overlay mounting fails.
The private receiver can reuse the same retained lower across runs. Use an
original admitted with overlay: true after the matching helper/receiver pair
is adopted, so its new input has container-copy permissions and its returned
evidence includes run.json. After that run completes and its inputs are
independently qualified, keep
that original request's guest inputs in place. Obtain its UUID from the existing
request.json and its exact input name from the returned run.json, alongside
image-provenance.json. A subsequent copy entry keeps the same frozen local
source and adds the original input reference:
{
"source": "/private/frozen-stage/filestore",
"destination": "/volumes/data/filestore/migration_copy",
"overlay": true,
"retained_from": {
"job": "00000000-0000-4000-8000-000000000001",
"input": "inputs/0"
}
}Use the actual original UUID and input name. The receiver checks the original's cleanup receipt, frozen transfer inventory, current bytes and the requesting local source's hashes before execution. A missing, changed, aliased or retired lower refuses admission. The reference cannot select an arbitrary host path or chain through another reference. The lower stays owned by its original request; retiring a borrowing run removes only that run's own inputs/images. Keep the base while future runs need it. Its owner uses the original request's existing retirement path. If the original has already completed retirement, a borrower can still retire its own copies against the preserved original receipts; it cannot execute again using that retired lower.
Keep ODOO_COPY_PURPOSE=migration, source/image/stage bindings, disconnected
networking, resource caps, credential stripping and the normal heavy entrypoint.
The flag replaces the container copy for this destination, so remove any job
command that copies/extracts a file store over the same mounted destination.
Retain database restoration and the SQL boundary before the first registry.
For an already restored database, use the existing
runner.prepare_isolated_copy_before_registry(purpose="migration") API above.
Do not run restore/bootstrap file replacement or recursive chown/chmod
over the overlay: metadata changes can copy the entire lower into its upper.
The receiver has already prepared container-copy permissions. Use the
independent-copy case if a workflow requires that whole-store rewrite.
When the base itself must change, omit both overlay and retained_from; the
existing ordinary copy is the independent writable case. An overlay never
publishes its private writes as a new saved stage automatically.
Adoption requires the matching catalog heavy helper and private receiver source installed together through the receiver's documented drained-queue procedure. Do not run these examples against an older installed pair. The lead owns that adoption and the single Q1096 real-size timing/disk comparison in the Q1097 batched real-copy check. Source checks do not establish receiver mount support or a migration result. Measure file-store input transfer, verification, overlay preparation, database restore, and cleanup separately; a shared lower avoids its bulk retransfer on subsequent runs but still requires integrity reads. Preserve any full-copy headroom refusal rather than inventing a before measurement.