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 scaffold-cockpit-root \
--output-dir /path/to/workspace-root
uv run platform workspace sync-cockpit-root \
--config /path/to/workspace-root/workspace-cockpit.toml
uv run platform workspace status-cockpit-root \
--config /path/to/workspace-root/workspace-cockpit.toml
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 Launchplane's reusable
artifact workflow, which supplies the required runtime-environment payload.
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 remain unsupported and fail before writing; their resolution is a
separate setup requirement. 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.
Purpose
- Copy the shared manual multi-repo cockpit-root starter into a target directory.
- Write
workspace-cockpit.tomlas the source of truth for that root. - Keep the repo map and section-level cockpit guidance in that config instead of hand-maintaining root markdown files.
- Generate
AGENTS.md,docs/README.md, anddocs/session-prompt.mdfrom that config. - Point operators to optional
workspace.local.mdfor supplemental non-secret facts that must not be baked into generated shared docs. - Document
AGENTS.override.mdas reserved full-replacement input, never the normal additive-notes path. - Keep non-repo workspace roots thin, link-heavy, and synced from
odoo-devkitinstead of hand-maintaining the same entrypoint docs.
Purpose
- Regenerate a manual multi-repo cockpit root from
workspace-cockpit.toml. - Keep root entrypoint docs manifest-driven even when the cockpit is not a
tenant
workspace syncsurface. - Re-render both repo listings and section-level guidance bullets from the tracked cockpit config.
- Preserve local-only notes by linking to
workspace.local.mdinstead of copying implementation details into generated markdown.
Purpose
- Report whether the generated cockpit entrypoint files exist.
- Report whether those files still match the current
workspace-cockpit.tomlrender output. - Report the reserved root
AGENTS.override.mdand mark the cockpit non-current when it would replace the canonical generated guide. - Give manual cockpit roots a native drift check before or after sync. A
completed status check exits 0 even when
is_currentis false; gate on the reportedis_currentvalue.
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. - 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. - 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. 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. These flags are runtime assertions supplied by Launchplane-managed records or operator input; local runtimes remain optional unless a caller explicitly sets them. - 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. - 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. - Devkit-managed startup 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, so web never boots an unsanitized production copy, as does a failure while blocking outgoing mail right after that apply. 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. - 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, clears pending export flags, and turns off the Shopify crons. It deletes the copy's Shopify external IDs (everyexternal_idrow under theshopifyexternal system) and clears each product's export timestamps, the same data the addon's Reset Shopify clears, without contacting any store. The next export then creates products in the store Launchplane applies instead of updating production-store product IDs. It deletes 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, including providers inteststate) and 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. 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 source/revision labels; 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. Support lock evidence identifiesdocker/runtime-python/uv.lock, and 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 Launchplane 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.
- 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.