- Nested Agent And DLUX Commands: Replaced flat agent/DLUX/self routes with
agent check/update/restart/off/watch/run/enable,dlux check/update/rollback,self update, andexecutor run/enable; leading-f/-dstill route to nested commands, generated service commands useagent runandexecutor run, and wrapper history advances to version 2.
- Rollback Chose The Wrong Release:
staged_versions()ordered release directories as strings, so1.8.10sorted below1.8.9— the first two-digit patch would have sent a rollback to the wrong release and madeprune_releasesdrop the wrong one. Ordering is numeric now (version_sort_key, mirroring the candidate sort indlux_release_source). The rollback target is also restricted to releases strictly below the active one, which is what the function always claimed: taking the newest staged release that merely differed rolled forward onto the release the deployment had just stepped back from. 4 tests. - The Executor Socket Reports When It Is Actually Listening:
_bind()creates the socket file atbind()and only starts accepting atlisten(), so anything waiting on the path alone called it ready during the window in between, where a connect is refused outright.Executor.wait_until_listening()blocks on the real thing; the server test waited onos.path.existsand failed in CI asConnectionRefusedError.
dlux-updateWorks From The Project Root:./start.sh dlux-updatefailed on every correctly deployed stack with "No DjangoLux runtime volume at /opt/dlux-runtime" — that path is a container mount, and the wrapper's composer has none. Composer now finds the volume behind the runtime root in the merged compose config (<project>_dlux_runtime, or an explicitname:), refuses to continue if Docker does not already have it (docker run -vwould create an empty one), and re-runs itself in a sibling container with that volume, the project directory, the Docker socket and the deployment's secrets attached. The deployer CLI is the only place with both outbound network and Docker authority; the child carries--no-delegateso it can never loop. 22 tests.- Inline Updates Without Putting The Executor On The Internet:
composer-executorran the package-update trigger but sits alone on theinternal: truedocker_proxy network, so it could never fetch a wheel — the automated path could not work at all. The two halves now split it:composer-agentownspackage-update-request.json, resolves the release, verifies the attestation and digest and stages the wheel indownloads/on the runtime volume, then sendsdlux_package_apply(version, filename, sha256) over the private socket; the executor re-hashes the staged bytes, re-reads the wheel's manifest, activates, restarts and health-gates it entirely offline. The digest travels over the socket, not in a file beside the wheel, becausecelerymounts that volume read-write too.dlux_package_rollbackneeds no staging. Exit 3 still means "needs a human". New--staged-wheel/--staged-sha256also give an operator an air-gapped apply. Update the resident pair together, asagent-updatealready does. - Schema-2 Releases Could Not Be Activated:
DluxRuntime.verify_releasereadinline_safestraight off the wheel's manifest, but schema 2 derives it frominstall.inline+migrations.effect+rollback_compatibleand never carries the key — so every current release failed at the last step of the swap, after being fetched, verified and staged ("Release 1.8.7 does not declare inline_safe"). It now defers to the samenormalize_manifestboth halves of the update path use. - The Image Can Verify A Release Again:
dlux_release_sourcemirrorsdjango-lux[updater]'s refusal to install an unverified wheel, but the image never shippedpypi-attestations— so everydlux-update --check,--dry-runandapplyfailed closed with "attestation verification is unavailable". The image now installs it at the same floor as the DjangoLux extra (>=0.0.29, +95MB).
check --fixRetires Local DLUX Tools Wiring: The guarded Compose transform now rewrites retiredtools.dlux_runtime_supervisorandtools.smtp_relaymodule commands todlux.updater.supervisoranddlux.smtp_relay, removes generated localtools/bind mounts targeting/app/toolsor the scoped SMTP relay path, and still adds thedlux_reconcileguard for survivingdlux-updaterservices. The fix is idempotent, uses the existingdocker compose configvalidation plus.xpose/backup path, and gates the rewrite on the highest packaged DLUX module floor it needs (1.6.2for the supervisor,1.7.0for the SMTP relay).check --fixCovers Current Scaffold Drift: Legacycomposer-updaterstacks now converge to the hardenedcomposer-executortopology in one run,dlux-updaterretirement strips nativeweb.post_starthooks instead of converting them to stale labels, generated services gain missingorg.dlux.restartlabels, andcheck -d --fixnormalizescompose.dev.ymlso the dev override cannot reintroducedlux-updater, force Celery'sdlux_runtimemount read-only, or leave inline updates enabled in development.
- DjangoLux Manifest Schema 2: Inline package checks now understand schema-2 install, migration, and service requirements, reject unsupported deployment contracts, and enforce the manifest's minimum Composer version.
- Release Test Dependency: CI and tag workflows install pinned PyYAML before full test discovery, so Compose transform tests run in clean release environments.
check --fixRetires dlux-updater On Existing Stacks: new guarded compose transform that removes the service, moves its runtime reconcile and migrations into Compose init containers (pre_start) on celery, strips the now-orphandepends_onedges, grants celery write access to the runtime volume and staticfiles, and drops web'sorg.dlux.post-startmigrator hook — which would otherwise be a second, redundant run now that the same work happens before start. Named volumes and their releases are kept. Double-gated, because either mistake is destructive: it refuses when the host's Compose predates 5.3.0 (it would ignorepre_startand boot the stack unmigrated) and when the project image still ships DjangoLux below 1.8.0 (that service is then the deployment's only update path). Handles a project whose scaffold block markers were hand-edited away, and removes adepends_onmapping left with no children rather than emitting invalid YAML. Idempotent. 22 transform tests + 6 gating tests, all mutation-verified, with the migrated file validated bydocker compose configitself rather than only parsed.- Compose 5.3.0 Enforced For DjangoLux Stacks: generated projects run their runtime reconcile and migrations as Compose init containers (
pre_start), socheckFAILs a host whose Compose plugin predates 5.3.0 — an older plugin ignores the key rather than rejecting it, which would start the stack with no migrations applied and every gated service waiting forever. Version parsing tolerates avprefix, prerelease suffixes and short forms (5.3means 5.3.0, which the bare tuple comparison would otherwise refuse). Composer's only remaining part in the boot chain is supplyingDLUX_MIGRATOR_FLAGSthrough the compose environment, so a deploy's-mm/-nm/-astill reach a step that is static in the file. 8 tests. - Composer Is Now A Required DjangoLux Service: DjangoLux 1.8.0 hands inline updates to Composer, so a Composer service belongs in the deployment, not only on the deploying machine.
checkFAILs a DjangoLux stack that has none, andcheck --fixinstalls the hardened trio (docker-socket-proxy,composer-executor,composer-agent) with their volumes and thedocker_proxynetwork, deriving image and labels from the project's ownwebservice. Projects generated by the 1.8.0 scaffold already ship the block, so this targets stacks generated before it. Idempotent, refuses anything it does not recognize (nodlux_runtimevolume, nowebservice, an orphan socket proxy), and goes through the existingdocker compose configvalidation +.xpose/backup + atomic write path. 20 tests. checkReports Update-Path Readiness: thedlux-updater-executorcheck was rewritten after verifying what the service actually does. It also runsdlux_reconcileandmigrator,webdeclaresdepends_on: dlux-updater: condition: service_healthy, anddlux_update_workeris the only caller ofUpdateService.process_next()— the queue drainer that writes the hand-off. So the service is never retired; only the executor code inside it is, in DjangoLux 1.9.0. An earlier entry here claimed otherwise. The check now verifies a composer-side loop exists and mountsdlux_runtime(FAIL if not — it would see no requests and publish no availability, silently), and tells a pre-1.8.0 stack that nothing needs changing yet.- Composer Publishes DjangoLux Availability:
composer dlux-update --checkresolves the newest release, verifies its attestation, readsinline_safefrom the manifest inside the wheel and publishesstate/package-available.json— the document DjangoLux 1.8.0 reads instead of polling PyPI itself.composer watch, the agent and the executor's watch loop publish on the existing check cadence and re-publish immediately after a swap, so the panel is never left showing the version it just installed. Failures are published as reports too: "could not check" is correct where a stale "up to date" is dangerous. - FIXED Package Requests Never Executed On Agent-Only Stacks: a stack with
composer-agentbut nocomposer-executorprocessed the image trigger but not the package trigger, so a DjangoLux 1.8.0 update request would sit unacknowledged forever — and DjangoLux refuses to queue a second operation while one is pending, making it a permanent wedge rather than a slow update. The agent now owns the package trigger when no executor is configured; an image deploy still takes precedence within a tick. Mutation-verified. composer dlux-updateAnd Its Trigger: new subcommand (apply|rollback,--version,--dry-run,--status-file) wires the health-gated orchestration to real Docker work — a scoped restart of the services that load DjangoLux (web,celeryby default;dlux-updateris deliberately excluded) and the same health waitcomposer updateuses. Exit 3 is reserved for "rollback also unhealthy", so a caller that retries on failure does not retry a deployment that needs a human. The executor and watcher now watch a second trigger,package-update-request.json, beside the image one — separate files because the two have different blast radii — serialized on the same op lease, acked by token so a request runs once and a failed or unstartable child can never wedge the loop. The agent observes the package ack under its own marker, so an image deploy and a package swap cannot mask each other's completion. 13 tests.- Health-Gated Inline DjangoLux Updates: new
composer/dlux_package_update.pyfetches and verifies a release, stages and activates it, restarts, and then decides whether it stands. If the deployment does not come back healthy the previous release is restored, the bad one is quarantined with the health failure recorded as its reason, and the services are restarted again. A rollback that is also unhealthy is reported ascriticalrather than as a tidy failure. A failed restart counts as unhealthy; re-applying the active release is a no-op that does not restart anything; a fetch/verification failure never touches the running deployment.prune_releases()keeps a rollback target and never removes the active or protected release.restartandhealth_checkare injected, so the module is Docker-free and testable. 12 tests, weighted toward the failure branches. - Composer Fetches And Verifies DjangoLux Releases: new
composer/dlux_release_source.pyreads the PyPI simple index, pins or picks the newest stable release, verifies the PyPI Trusted Publisher attestation, checks the SHA-256 from the index fragment, reads the manifest carried inside the wheel and refuses anything declaringinline_safe: false, then unpacks with path-traversal rejection. Trust decisions are mirrored fromdlux/updater/manifest.py— approved hosts only, bounded reads, and fail-closed attestation: a missing verifier is a refusal, never a pass. 20 tests, weighted toward the refusal paths. - Composer Writes The DjangoLux Runtime Volume: new
composer/dlux_runtime.pystages a release intoreleases/<version>/, verifies it against the manifest the wheel carries, flipsstate/active.jsonand bumpsgeneration— all atomic, all validated. Verification runs before the pointer moves, so a bad artifact is inert rather than fatal;quarantine()moves a failed release out ofreleases/andrestore()returns to the previous one (or to the image copy, when the first-ever volume release is rolled back).composer-executoralready mountsdlux_runtime:/opt/dlux-runtime:rw, so no Compose change is needed. 17 tests, including an interop check that DjangoLux's ownRuntimeStorereads back what Composer wrote (skipped when dlux is not importable). dlux.package_updateAgent Action: new bridged action for inline DjangoLux package updates, mirroringdlux.image_update. The payload is typed and bounded —mode(apply|rollback, deliberately no default),target_version(pattern-checked because it becomes a directory name underreleases/; empty means latest eligible) andbackup_mode(data|full|skip). It routes through the dlux bridge like the other dlux actions, so DjangoLux still records the run and takes its pre-update backup, and Composer performs the staging. Step 1 of the updater consolidation — seedocs/updater-consolidation.mdin the django-lux repo.
- Missing Migrator Declaration No Longer Means No-Op: DLUX stacks containing
dlux-updater+webbut neitherorg.dlux.post-startnor nativepost_startnow run the standard supervised migrator compatibility command;check --fixinstalls the missing label with its existing validation, backup, and atomic-write path. - Post-Start Failures Are Fatal And Visible: Hook execution uses non-interactive
docker compose exec -Twith streamed progress; unhealthy targets and nonzero migrators now make Composer exit nonzero instead of printing a false “Environment ready.” - Direct Migrator Subcommand: Added
composer migrate [-s SERVICE] [-f FILE] [-d] [MIGRATOR_ARGS...], defaulting toweb, preferring its configured post-start migrator, and forwarding options such as-mm,-nm, and-a APPwith attached output and exit status.
- The Migrator Ran Twice, Concurrently: The generated
compose.ymldeclared the migrator as a native Composepost_starthook, which Compose runs itself the momentwebstarts — unflagged — while composer separately scraped the same block out of the YAML and re-exec'd it after health, with-mm. Two overlapping runs, onecollectstatic --clearwipingSTATIC_ROOTwhile the other collected into it. The declaration moved to anorg.dlux.post-startservice label that Compose ignores and composer reads, leaving exactly one runner. - Post-Start Discovery Reads Resolved Compose Config:
parse_post_start_labels()uses the newcompose_config_json()(config --format json) instead of regex-scraping each active compose file, so overrides merge once — under-da dev file repeating the block used to queue the command twice. Nativepost_startblocks still run as a legacy fallback, announced with acheck --fixhint so-mmkeeps working on un-migrated deployments. -nmNo Longer Means Two Things:-nmnow runs the hook and passes-nmto the migrator (skip makemigrations + migrate, still collect static). The internal "run no hooks at all" case thatagent-updateneeds moved toskip_post_start, so replacing the resident pair no longer looks like a user migration flag.-mmand-nmare mutually exclusive at the CLI.check --fixMigrates The Hook:enable_post_start_labelfolds a nativepost_startinto the label through the existing guarded transform (dry-run default,docker compose configvalidation,.xpose/backup, atomic write, idempotent). Refuses hooks holding more than one command.
- Versioned Launcher Wrappers:
start.sh/start.ps1carry a# composer-wrapper: Nmarker, andcomposer checkcompares them against the copies now baked into/app/wrappers/in the image — no registry call, so it works air-gapped.wrappers-history.jsonrecords each published version's sha256, which separates a stale-but-pristine wrapper (check --fixupdates it) from one with local edits (reported, replaced only after the confirmation names it). A marker newer than the image reports the image as behind and points atupdate-selfinstead, so--fixcan never downgrade a wrapper.--fixarchives to.xpose/composer-check/<stamp>/and swaps viaos.replace, so astart.shexecuting the very check that replaces it keeps reading its original inode; a non-executablestart.shis repaired rather than preserved. Composer owns both files (the DLUX scaffold writes them once and refuses to overwrite); DLUX'sscaffold_templates/project/copies are mirrors, pinned by a scaffold test. - Wrappers Were Excluded From The Image:
.dockerignorelistedstart.sh/start.ps1under "sensitive files", so theCOPYfor the new reference directory failed the build outright. They are neither sensitive nor optional now. - Piped Input Reached Nothing:
start.sh/start.ps1added-ionly as part of-it, gated on stdin and stdout both being TTYs, soecho yes | ./start.sh run -m web migrategave the container no stdin at all andexec_in_service's non-interactive-Tpath could never receive the piped data. Both wrappers now attach-iunconditionally and add-tonly when a terminal exists ([Console]::IsInputRedirectedon Windows). - start.sh Failed On macOS Without A Secrets File:
"${secret_flags[@]}"on an empty array is an unbound-variable error underset -uin bash 3.2 — the /bin/bash macOS ships — so./start.shaborted immediately in any project with no.env/secrets/.env/.secrets/.env. Guarded with the${arr[@]+"${arr[@]}"}form. Covered by two newtest_wrapper_secrets.pycases (piped vs pty argv), suite 286.
- Pull Bar Names The Right Image: The status line used to show whichever service Compose mentioned last — "postgres Pulling" while web's layers were actually downloading — because Compose interleaves layers and never attributes one to a service.
PullProgress.scope()now names every image still in flight and drops each as Compose reports it pulled (postgres, web→web), falling back to thePulling fromrepository for a plaindocker pull. Reused layers are counted as(n cached), so "did it re-download everything?" is answerable from the line itself. - Services In Flight Stop Showing Green: New
SERVICE_UPDATINGstate (🔵) applied bymark_services_updating()when a pull, recreate, or restart starts — scoped to the targeted services, never the excluded ones. A green circle beside a container being replaced reported health measured on the container that was going away; health monitoring resolves each service back to 🟡/🟢/🔴. - Image Pull Progress Bar: Long pulls no longer look like a hang.
composer/progress.pyaggregates the per-layer phasesdocker pullanddocker compose pullreport (Pulling fs layer→Downloading→Download complete→Extracting→Pull complete, plus the byte counts) into one in-place bar:████████░░░░ 62% · 2/4 layers · 144MB/240MB. Wired intoupdate-self(now streamed instead of handed to a baredocker pull),pull_images, andlaunch_containers. Progress never moves backwards, non-pull output keeps the existing status line, and a detached run logs coarse summaries. - Wrapper Stops Pulling Silently:
start.sh/start.ps1probed the installed version withdocker run ... cat /app/VERSION 2>/dev/null, which pulls a missing image with all progress sent to the discarded stderr — the "waiting forever with no output" on./start.sh --update. They now check withdocker image inspect(never pulls) and announce a first-run fetch with a visibledocker pullbeforedocker run. - Stale "Update Available" After A Deployer Update: The availability document was only re-published by the agent's own update path or its hourly
--check-interval, so an update deployed from the project root (composer update), by the executor in the hardened topology, or by a manualdocker compose pullkept advertising an update that was already installed.WatchRuntimenow tracks the local digest behind each published entry and polls it every 30s (LOCAL_DIGEST_PROBE_SECONDS); a moved digest forces a re-publish. An unreadable digest stays "unknown" and never re-publishes, so a transient Docker error can't flip the flag. The agent also forces a re-check after observing an executor.ack. - Runs Survive A Closed Terminal: A hangup no longer aborts a deploy midway.
composer/session.pyinstalls a SIGHUP guard (plus ignored SIGTTIN/SIGTTOU), redirects stdout/stderr tocomposer-detached.log(COMPOSER_DETACH_LOG, or a configuredCOMPOSER_LOG_FILE) and stdin to/dev/null, and the run finishes in the background;render/progress lines drop cursor-control escapes once detached.start.shtrapsHUPso the inherited disposition keeps thedocker runclient alive too. - Compose Children Detached From The Terminal:
run_command/run_command_streamingstart Compose withstart_new_session=True, so a terminal hangup can't reach it; Ctrl+C is relayed explicitly (_interrupt_child: SIGINT to the child's group, then terminate/kill) and still exits130.run,log, andlogsstay terminal-bound and restore default hangup handling in the child.
- check --fix Migrates The dlux-updater Runtime:
composer check --fixnow surgically migrates a deployed project's dlux-updater command to the packaged runtime (python -m dlux.updater.supervisor) and adds the pre-migrationdlux_reconcileguard — marked block only, idempotent, never touching your services. Version-gated on the dlux the image actually ships (probed withdlux --versionviadocker compose run --no-deps, which works even when dlux-updater is crash-looping): when the image is older than 1.6.2 it reports "update the image first" and changes nothing, so the compose never points at modules the image lacks. A pulled deployment has no requirements.txt, so the image is the only authoritative signal. - Deployer Can Read Project Secrets: The deploying role (
composer-executor, andcomposer-agentin the agent-only topology) addscap_add: DAC_READ_SEARCHon top ofcap_drop: ALL, so the uncapped UID-0 process can read the project's0600.secrets/.envto deploy. Fixes inline deploys failing the secrets guard with a Permission-denied error and needing a manualsetfacl. Read-only override only; the network-facing agent in the hardened topology keeps no file caps. - check --fix Repairs the Cap In Place:
composer check --fixnow heals an already-hardened stack that lacks the read cap — a targeted insert (not a full re-render, so the dlux-scaffold and composer-generated blocks are both safe) that addscap_add: DAC_READ_SEARCHto the deployer only.enable-agent/enable-executorself-heal the same way instead of no-opping on an already-migrated stack.
- agent-check Compose Fallback: With no IMAGE argument and no
COMPOSER_CHECK_IMAGE/WEB_IMAGE,agent-checknow discovers the watched images from the deployment's own compose file (--check-imageentries andWEB_IMAGEin the composer-agent/executor/updater block, with${VAR:-default}resolution); added-f/--fileto scope discovery.
- Docker Authority Isolated In composer-executor: New
executorrole holdsdocker.sockand does every Docker write (trigger-watched image update + typedrestart/recovery_deployover a private unix socket). Thecomposer-agentkeeps read-only proxy access only and delegates writes. - enable-executor + check --fix:
composer enable-executormigrates a composer-agent stack to the hardened topology (.xpose/backup,docker compose configvalidate, atomic write; idempotent);composer check --fixruns it automatically.agent-update/agent-restart/agent-offnow target the resident pair.
- Clear Update Command Vocabulary: Added
pull,update-self,agent-update,agent-restart, andagent-off; retained-uplus legacy one-argument--update, and retired-uo,update -o, and the ambiguous long application--update. - Agent Image Availability Check: Added
agent-checkwith tagged-image orWEB_IMAGEdiscovery, human/JSON output, atomic file publication, and explicit unknown-registry failure semantics using the agent digest/version/manifest contract.
- Latest-Only Snapshot Relay: Coalesces pending snapshots to the newest value and collapses existing snapshot backlogs on startup, preventing stale runtime versions from appearing after first enrollment.
- Pinned Agent Control Origin: Persisted the normalized control URL after enrollment, rejected conflicting pairing requests and startup overrides while credentials remain active, and allowed replacement only after revocation/re-enrollment or local state reset.
- No Credentialed Redirects: Control-plane and registry requests reject HTTP redirects; registry token challenges also require an HTTPS realm.
- Protocol Input And Redaction Hardening: Recovery
forcenow requires a JSON boolean, and full Bearer authorization values are removed from relayed logs. - Legacy Proxy Cleanup:
check --fixarchives and removes recognized pgAdmin routes, reloads Caddy/direct Nginx configs, and restarts plus verifies template-backed Nginx so its rendered configuration is refreshed. - Mixed Topology Guard:
checknow fails whencomposer-agentandcomposer-updatercoexist instead of treating the agent-first branch as healthy.
- Obsolete DLUX Service Cleanup:
composer checknow warns onpgadmin,db-backup, anddb_backup; guardedcheck --fixvalidates the candidate, archives originals, runs targeteddocker compose rm -sfonly for those services, applies their service-block removal, and verifies the services are gone while pre-existing named volumes remain.
- Typed Confirmation Guard For Destructive Actions: New
composer/confirmation.pyconfirm()requires a literaly/yesbefore-v/--volumesand-p/--purgerun, printing the exact consequences first.-y/--yes(orCOMPOSER_ASSUME_YES=1) skips the prompt; a non-interactive stdin without either fails closed instead of destroying data. stopSubcommand:composer stop [-v] [-p] [-y] [-f FILE] [-d] [service...]is dispatched before the flat parse likerun/restart, viaconfigure_stop();down_containers()forwards named services so a single service can be stopped. The project-wide-v/-pflags are rejected alongside service names.composer downis an alias andcomposer --downstill works.updateSubcommand:composer update [-o] [-b] [--force] [-nm] [-mm] [-a APP] [--status-file PATH] [service...]runs the-upipeline throughconfigure_update(), with-o/--onlycovering-uo.scoped_service_list()incomposer/service_selection.pyletspull_images(),launch_containers(),compose_config_images(), and the panel labels accept several service names instead of a single string; the flat-u/-uoflags are unchanged.logSubcommand:composer log|logs [-n N|all] [-F] [-t] [--since] [--until] [--no-color] [service...]streamsdocker compose logsattached to the terminal throughstream_service_logs(), defaulting to--tail 50;-n all/-n 0lifts the limit and--no-coloris forced when stdout is not a TTY.checkDoctor Subcommand: Newcomposer/checkup.pyCheckupMixinaddscomposer check [--fix] [-y] [--deep] [--json], verifying Docker/Compose v2, compose config resolution, secrets source, unmet required compose vars, topology mode, and deployer↔resident version drift.--fixmigrates a legacycomposer-updatertopology throughenable_agent()behindconfirm();--deeprelays a configurable in-container doctor (python manage.py dlux_doctorinwebby default). One evolving command replaces per-changeenable-*one-offs: composer owns the outside checks and delegates the rest to the container.- Distinct Resident Agent Version:
publish_agent_status()now writescomposer_version(the resident composer-agent binary's own version) intoagent-status.jsonalongside the back-compatagent_version. It is distinct from theCOMPOSER_VERSIONenv a panel sees, which reflects the deploying composer that last recreated the stack — the two can drift and both are now reportable.
- enable-agent Runs On Deploy Hosts:
enable_agent()no longer requiresmanage.pywith aGenerated with django-luxbanner. Deploy directories hold onlycompose.yml,.proxy/, and.secrets/, so the source-tree gate blocked every real migration; project identity already comes from the Composename:,services:, and the# Composer-as-updatermarkers_transform_compose()verifies. - Non-Blocking Bridge Check Without A Manifest:
_dlux_readiness_warning()returns(message, blocking). A missingrequirements.txt/pyproject.tomlreports an advisory warning and--applyproceeds; a declared DjangoLux below 1.5.0 or an unparseable pin still blocks unless--allow-unverified-dluxis passed.
- Baked DjangoLux Version In Availability Manifest:
_release_manifest_from_label()incomposer/watcher.pypasses through an optionalbaked_dlux_version(string, capped at 32 chars) so the published availability document carries the candidate image's baked framework version alongside the project version and highlights. Additive underschema_version: 1; absent fields stay absent, and the version label the preflight gate reads is untouched.
- enable-agent Migrates Pre-1.5 Scaffolds:
_agent_stack()no longer derives networks,COMPOSER_VERSION_LABEL, orWEB_IMAGEfrom the Composename:; new_legacy_topology()carries the replacedcomposer-updater/docker-socket-proxyvalues forward verbatim. Projects generated before the DjangoLux 1.5 scaffold (egress/docker_proxy, deployment-specific baked-version label) emitted references to undeclareddlux_update_egress/<slug>_docker_proxynetworks plus a wrong label, sodocker compose configrejected the migration. - Undeclared Network Preflight:
_transform_compose()checks carried network names against top-levelnetworks:keys and fails naming the missing ones instead of deferring to an opaque Compose error; the agent end marker is emitted at service indentation.
- UI-Driven Agent Pairing:
composer agentnow accepts a DjangoLux-writtenenroll-request.jsonbridge file ({control_url, pairing_code}), redeems the code through the existing/api/agent/v1/enroll/endpoint, persists the control URL in the durable store so restarts rebuild the client withoutCOMPOSER_CONTROL_URL, and writesagent-status.json(enrolled/connection state) for the DLUX Control Panel tile. Enrollment is idempotent peroperation_id; env-var bootstrap remains a fallback.
- Outbound Composer Agent: Added
composer agentwith HTTPS enrollment/long polling, SQLite command and replay durability, typed update/backup DLUX spool relay, operation-aware status, safe restart allowlists, two-phase rotation, fresh-token re-enrollment after revocation, and localwatchcompatibility. - Composer-Owned Agent Migration: Added dry-run-first
composer enable-agentto diff, Compose-validate, preserve, and atomically migrate recognized DLUX updater scaffolds; DjangoLux now keeps only a temporary forwarding alias. - Protocol Security: Added strict schema-v1 fields/timestamps and payload bounds, UUID deduplication, monotonic events, inherited-secret/log redaction, capability reporting, read-only project support, and stateful-service exclusions through the existing Docker socket proxy.
- Restart Subcommand: Moved restart into
composer restart [-f FILE] [-d] [--status-file PATH] [service]with dedicated help and early dispatch likecomposer run; leading-r/--restartaliases retain the same restart-and-health pipeline. - Private Secrets Handoff To Resident Updater:
start.sh/start.ps1now pass the selected plaintext file through Docker--env-filewith a key manifest, and Composer's mode-0600runtime override forwards those inherited values only tocomposer-updater. Resident image updates validate and reuse that environment instead of reopening the host bind-mounted file, so mode-0600secrets work without ACLs or added capabilities. Direct legacy containers retain strict fail-before-pull behavior plus mapped-UID ACL diagnostics.
- Secrets Never Fall Through To Compose Defaults:
SecretsMixin.resolve_secrets()now refuses any env candidate that exists but is unreadable (permissions/userns) or yields no values, andparse_env_file()no longer swallows read errors. Becauserequired_compose_vars()excludes every${VAR:-default}interpolation, a defaults-heavy compose previously let an unreadable.secrets/.envvacuous-succeed and deploy onadmin/admin_pass; the run now fails loudly at the secrets stage before any pull or recreate.
- Quote-Safe Project Manifest Labels: Extended
watcher._release_manifest_from_label()to decode boundedbase64:<URL-safe-base64-JSON>image labels before applying the existing schema-1 normalization. Raw JSON labels remain supported, and malformed encoded or raw metadata is still omitted without affecting digest availability or optional version fallback.
- Optional Project Image Release Manifest: Added one-pass remote image-label discovery through
registry.remote_image_labels()and extendedwatcher.check_availability()to publish independently optionalversionand normalizedmanifestfields for digest-detected updates.COMPOSER_RELEASE_MANIFEST_LABELdefaults toorg.dlux.project.release-manifest; missing, malformed, oversized, empty, or unsupported manifest JSON is omitted without changing digest availability, version fallback, or deployment behavior.
- Project-Mount-Independent Runtime Overrides: Changed
DockerComposeMixin.sync_runtime_compose_override()to create its atomic.composer-runtime-*.compose.ymlthrough Python's verified writable system temporary directory instead of the current project directory. Resident updaters now work with host-owned mode-0755 or read-only project mounts while retainingcap_drop: [ALL]; temp-file creation failures become Composer diagnostics instead of uncaughtPermissionErrortracebacks. - Guaranteed Terminal Watcher Failure: Changed
composer watchso every non-zero child exit atomically publishes a token-matchedfaileddeploy status before writing the request ack, preserving any detailed child error and appending a generic failure todeploy-log.txt. Child spawn errors now terminalize with exit 127 instead of crashing the watcher, giving downstream maintenance controllers both status and ack signals even when the one-shot launcher fails before its first phase. - Read-Only Runtime Regression Gates: Added standard-library unit coverage for override placement, content, cleanup, and creation-failure diagnostics, plus an image smoke test that runs with a read-only image, a read-only mounted Compose project,
--cap-drop ALL, and writable/tmponly, then executes a real mergeddocker compose configthrough the generated override. CI and release workflows now execute the unit suite before building or publishing images.
- Resident Watcher Self-Exclusion: Added
COMPOSER_EXCLUDE_SERVICESfiltering across service discovery, generated runtime overrides, bulk pulls, version-gate image resolution, bulkup -d, health state tracking, and diagnostics;composer watchnow exportsCOMPOSER_EXCLUDE_SERVICES=composer-updaterby default (override withCOMPOSER_WATCH_SELF_SERVICE) so an in-compose updater does not recreate the container supervising its own app update after the v1.1.9watch→composer -uchange.
- Pull-Only Update Flag (
-uo): ChangedDockerComposeLauncherso-uo/--update-only [service]now runs only thepull_images()phase (scoped bypull_servicewhen a service is named), writes the newpulledstatus, renders only the secrets/pull rows, and exits beforepreflight_version_gate(),launch_containers(), health checks, or post-start hooks.composer watchnow shellspython -m composer -ufor the full update pipeline, preserving the existing deploy/update behavior and service-scoped-u <service>recreate semantics.
- Availability Check Publishes The Target Version (
composer watch): The registry availability check now includes the newer image's own version inimage-available.json, so a downstream reader (e.g. the dlux admin panel) can show "update available to v2.4.0" instead of only a digest. Newregistry.remote_image_version(ref, token, label)reads the image's OCI version label (org.opencontainers.image.versionby default, override withCOMPOSER_VERSION_LABEL— the same env the preflight version gate uses, so the surfaced version matches the gated version) by fetching the tag manifest, descending into a concrete image manifest for multi-arch indexes, and reading.config.Labelsfrom the image config blob — reusing the existing Bearer-token challenge flow via a new_fetch_byteshelper.watcher.check_availabilityadds"version"per image best-effort and only when an update exists (avoids an extra registry round-trip on every poll); any failure (older/private/unsupported registry, missing label, network error) is silently omitted so the check never breaks and readers fall back to the digest. Fully backward compatible with the existingimage-available.jsonshape.
- Update Console Log (
composer watch --log-file):watchnow records a clean, ANSI-free console for each update run so a resident proxy can show a live console during the recreate window (when the app itself is down). The-uochild appends progress toCOMPOSER_LOG_FILE(set by the watcher; defaultdeploy-log.txtbeside--status-file, truncated per run) via a newOutputUtilsMixin.append_console()— reusing the already-sanitizedemit_progress/emit_statustext (no escape codes, no panel redraw noise) plus— <phase> —markers and the failure detail fromwrite_status(). New--log-file PATHflag; the terminal panel and docker logs are unchanged. Opt-in (no-op without a log path).
- Registry Availability Check (
composer watch):watchcan now poll a registry for a newer image and publish the result, so another process (e.g. a Django admin) can surface "update available" without registry access of its own. New flags--check-image IMAGE(repeatable),--check-interval SECONDS(default 3600, min 60), and--availability-file PATH. On each check it compares the remote tag digest (newcomposer/registry.py— a minimal registry v2 client doing the standard Bearer-token challenge flow;COMPOSER_REGISTRY_TOKENfor private repos) against the locally-pulled digest (docker image inspect … RepoDigests, via the socket) and writes{available, checked_at, images:[{image, remote_digest, local_digest, update_available}]}atomically. Unreadable remote = "unknown" (never a false positive). The check runs immediately on start, on the interval, and is forced right after an applied update so the signal clears. Availability polling is opt-in (needs both--check-imageand--availability-file); the trigger-file watch is unchanged.
- Resident Updater (
composer watch): Newwatchsubcommand turns composer into a trigger-driven, in-compose updater.composer watch --trigger-file PATH [--interval N] [--status-file PATH] [-f FILE] [-d] [--once]watches a trigger file and, on each new request (a changedtokenfield, or the file'smtimewhen there is no token), shells the existing one-shotcomposer -uopipeline (pull → version gate → recreate → health → post_start) in a child process — so all one-shot behavior/exit codes stay intact. It records the processed token in<trigger-file>.ack(atomic write) so a request is applied exactly once and survives a restart of the watcher container. Clean ownership split: the child writesCOMPOSER_STATUS_FILE; the watcher owns the ack. Implemented as an earlyargv[1] == "watch"intercept →composer/watcher.py:run_watch()withcli.parse_watch_args(); documented in the main--helpepilog andcomposer watch --help. - Deploy Status File (
--status-file/COMPOSER_STATUS_FILE): New opt-inStatusWriterMixinwrites an atomic JSONdeploy-status.json(temp-file +os.replace) so an external reader (Django admin panel, dashboard, health probe) can observe the run without scraping the terminal UI.DockerComposeLauncher.run()writes lifecycle states through the pipeline —starting→pulling→recreating→migrating→ready, andfailed(with a truncatederror) at every abort; the restart branch reportsrestarting/ready/failed. Payload includesstatus,updated_at(UTC ISO-8601),composer_version,compose_files, and (when the version gate ran)target_images/target_version/active_version. No file is written unless configured; write failures never abort a deploy. - Preflight Version Gate (
--force): New opt-inVersionGateMixinrefuses an update (-u/-uo) that would recreate onto an image whose version label is older than the deployment's currently-active version — the one move a generic pull-and-restart can't safely undo (old code against a forward-migrated schema). Runs afterpull(target label is local by then) and before recreate. Reads the target version from an image label (COMPOSER_VERSION_LABEL, defaultorg.opencontainers.image.version) viadocker image inspect, and the active version from a JSON file+key (COMPOSER_ACTIVE_VERSION_FILE+COMPOSER_ACTIVE_VERSION_KEY, defaultversion— e.g. the dlux runtimeactive.json). Fully generic and disabled unless an active-version source is configured; missing labels/metadata pass with a note;--forceoverrides a block. Ships a dependency-free PEP440/semver-liteparse_version(nopackagingneeded). - Removed SOPS/AGE Encryption: Dropped the optional SOPS/AGE encrypted-secrets path entirely; secrets now resolve exclusively from a plaintext env file (
.env→secrets/.env→.secrets/.env).SecretsMixinlostdecrypt_secrets_raw(),encrypt_secrets_raw(),encrypted_secrets_path(), theENCRYPTED_CANDIDATESlist,parse_dotenv_text(), and theenc_fileattribute;resolve_secrets()no longer takesargs, drops the encrypted fallback + AGE key prompt, and storesself.secrets_sourceas the plaintext path string. Removed the-k/--key,--encrypt,--decrypt,-i/--input,-o/--output, and positionalkey_positionalCLI arguments (composer/cli.py) and the encrypt/decrypt branches inDockerComposeLauncher.run().RenderingMixin.render()now always shows the🔓 PLAINTEXT <path>source flag (the🔐 DECRYPTEDvariant is gone). - Slimmer Image & Entrypoint:
Dockerfileno longer installs theageapt package or downloads thesopsbinary;entrypoint.shdrops thekeygen/encrypt/decrypt/sopsroutes and now execspython -m composer "$@"directly.scripts/smoke-test.shdrops the--encrypt/--decryptflag assertions, theage/sopsrunnable checks, the keygen route, and the age+sops round-trip; it still gates on version, core flags, therunsubcommand, and docker/compose availability. - Docs:
README.mdrewritten to remove the secrets encryption/decryption/keygen workflow and the-k/--encrypt/--decryptflags.
runSubcommand: Addedcomposer run [-m] [-s] [-F] [-f FILE] [-d] <service> <command...>to run a command inside a Compose service without hand-writingdocker exec/docker run. Defaults todocker compose exec <service> <command...>;-m/--manageprependspython manage.py,-s/--shellwraps the command insh -c,-F/--freshswitches to a one-offdocker compose run --rm. TTY is auto-managed (-Tadded when stdin/stdout aren't a terminal). Implemented as an earlyargv[1] == "run"intercept inDockerComposeLauncher.run()→handle_run()→DockerComposeMixin.exec_in_service(), with a newSubprocessRunnerMixin.run_command_interactive()(inherited stdio) andDockerComposeMixin.resolve_compose_cli()(one-shot plugin/legacy probe since interactive runs can't inspect captured output). Compose-file resolution extracted toresolve_active_compose_files()and reused;runhonors-f/-d. Documented in the main--helpepilog andcomposer run --help.
- Update-Then-Recreate (
-u/--update [service]):-unow pulls the latest image(s) and recreates immediately in one step. With a service name it scopes both the pull and the recreate —composer/cli.pykeepsnargs="?"/const=True,DockerComposeLaunchernow setsup_servicealongsidepull_service, andDockerComposeMixin.launch_containers()appends the service toup -dso only that service is recreated (Compose still starts its dependencies; recreate is image/config-change driven, no--force-recreate). Native Compose semantics: dependents aren't auto-restarted unless their own image changed. - Update-Only (
-uo/--update-only [service]): New flag preserving the previous-ubehavior — pull (optionally one service) before the normal fullup -dstartup, without scoping the recreate. Maps toupdate_images/pull_servicewithout settingup_service. - Restart (
-r/--restart [service]): New flag that runsdocker compose restart [service]instead of--down+ start, preserving containers so baked-in env vars survive. AddedDockerComposeMixin.restart_containers()and a dedicated launcher branch that resolves secrets, restarts, then monitors health (no post-start/migration hooks).RenderingMixin.render()is restart-aware: shows "Restart Services (svc)", hides the Pull/Post-Start rows, and labels a scoped-urecreate as "Start Compose (svc)".
- Skip Commented-Out Var Refs:
required_compose_vars()now strips YAML comments before scanning, so a${VAR}inside a full-line or trailing# …comment is no longer counted as required. A mid-token#(e.g.url#frag) is preserved, so a real${VAR}after it still counts. AddsConfigMixin._COMMENT_RE.
- Smarter Required-Var Detection:
required_compose_vars()no longer produces false "missing variable" failures. It strips$$escapes before scanning (so shell variables in command/healthcheck scripts like$$attemptsare not mistaken for compose interpolations), and subtracts variables the compose already supplies itself —ConfigMixin._compose_env_keys()collects keys anenvironment:block assigns a concrete literal value to (mapping and list syntax), while bare pass-throughs (- KEY) and interpolated values (KEY: ${KEY}) are still treated as needing a value.
- Plaintext-First Secrets Resolution (default): Running with no secrets flags now auto-resolves secrets.
SecretsMixin.resolve_secrets()searches plaintext env candidates (.env,secrets/.env,.secrets/.env) and uses the first file that satisfies every variable required by the compose (computed via the newConfigMixin.required_compose_vars(), which parses${VAR}interpolations and skips those with a:-/-/:+/+default). If none qualify, it falls back to an encrypted file (secrets.enc,secrets/secrets.enc,.secrets/secrets.enc), prompting for the AGE private key only when one was not supplied via-k/positional/SOPS_AGE_KEY. Added env helpersparse_env_file(),parse_dotenv_text(),apply_env_values()and source helpersplaintext_env_candidates()/encrypted_secrets_path();DockerComposeLaunchernow trackssecrets_source. - Removed
-sd/--skip-decrypt: The skip-decrypt flag is obsolete and fully removed fromcomposer/cli.py,launcher.py,rendering.py, andsecrets_manager.py(droppedload_secrets()/load_secrets_from_file()and the dev-mode coupling that forced it).-d/--devis now purely the two-compose-file override mode and no longer dictates the secrets source. - Dev Mode Forces Debug:
-d/--devnow always turns debug on regardless of the project'sDEBUG/DEBUG_STATUSvalue (or its absence).DockerComposeMixin.sync_runtime_compose_override()injectsDEBUG: "True"andDEBUG_STATUS: "True"into every service'senvironmentin the last-applied override file (overriding any compose declaration),build_compose_env()exportsDEBUG=True/DEBUG_STATUS=Truefor${DEBUG}/${DEBUG_STATUS}interpolation, and the launcher forces thedebug_modeUI flag. Both names are added to the injected set so they never count as missing required secrets. - UI Refresh: Reworked the status panel in
RenderingMixin.render()— lighter━rules replacing the solid block bars, a bold title, the compose-file list on its own📂line, and a secrets-source flag (🔐 DECRYPTED <path>/🔓 PLAINTEXT <path>) replacing the old⚠️ BYPASS DECRYPTIONindicator. The first step is relabeledLoad Secretsand shows the resolved source path.
- Runtime-Gated Image Publishing: Added
scripts/smoke-test.sh, which runs the built image and asserts--versionmatches theVERSIONfile,--helpexposes the core flags (--down/--purge/--volumes/--update/--build/--encrypt/--decrypt), the bundledage/sops/docker/docker composebinaries are runnable, thekeygenentrypoint route emits an AGE key, and an end-to-end age+sops encrypt/decrypt round trip succeeds..github/workflows/release.ymlnow builds the amd64 image withload: trueand runs the smoke tests before the multi-arch Docker Hub push, so a runtime-broken image can no longer be published..github/workflows/ci.ymlruns the same smoke tests on every push/PR tomain.
- Composer Rebrand: Relaunched under the
Composername, replaced the oldDecrypterbranding, removed obsolete passphrase-based encryption/decryption support, and improved the modular package structure (composer/mixins) and single-status-line terminal UI. - Purge Flag (
-p/--purge): Added a--downchild flag incomposer/cli.pydriving a full compose teardown inDockerComposeMixin.down_containers()— appends-v(implies volume removal even without-v),--rmi localto drop built untagged images, and--remove-orphans. AddsDockerComposeMixin.prune_build_cache()runningdocker builder prune -ffor dangling BuildKit cache (not compose-scopeable). Wired throughdown_volumes/purgeonDockerComposeLauncher. - Tag-Driven Release Pipeline: Added
.github/workflows/release.ymltriggered byv*tags — verifies the tag matches theVERSIONfile, builds the multi-arch (linux/amd64,linux/arm64) image with Buildx, pushesdebeski/composer:<version>anddebeski/composer:latestto Docker Hub, and publishes a GitHub Release using the matchingCHANGELOG.mdsection. Added.github/workflows/ci.ymlrunningcompileall+ CLI smoke and a no-push Docker build on pushes/PRs tomain. - Changelog Renormalized: Folded the pre-release
v1.0.0–v2.0.0history into thev0.1.xseries so the first GitHub-Actions-published image starts a cleanv1.0.0.
- Improved compose file reporting by listing all active filenames in the UI and debug logs. Standardized compose file resolution (including
docker-compose.ymlfallback) across all orchestration steps.
- Added
--updateflag to wrapper scripts (start.sh,start.ps1) to explicitly update the Docker image. Removed automatic image pull on every run.
- Separated progress messages from state circles to prevent terminal output overwrites, and added dynamic waiting/failing status output during the health check loop to clearly identify stuck containers.
- Added
--decryptand--encryptflags for standalone crypto operations. Added-i/--inputand-o/--outputto customize file paths for encrypt/decrypt.
- Updated start templates for bash and powershell.
- Fixed a visual bug where the end result erased previous terminal output.
- Passed the launcher version into Compose and automatically injected it into all launched services via a generated runtime override, so deployed projects can read the Composer version without per-project compose edits.
- Fixed launcher UI redraw issues that could repeat header lines, kept compose/pull progress on a single in-place status line, improved compose startup diagnostics, and accepted quoted
DEBUG_STATUSvalues such as"True"when parsing compose config.
- Streamed Docker Compose build/pull progress during startup, improved failure diagnostics for compose health/post-start errors, and treated running services without healthchecks as ready instead of hanging.
- Added
--downflag to stop containers and-vflag to remove volumes when stopping.
- Added
-u/--updateflag to force pull container images. Support for specific service targeting (e.g.,-u web).
- Shifted core target pattern to Docker Compose (
:composetag default). Removed container-internal web reachability checks in favor of native health states.
- Added MIT License, detailed project
.gitignore, and clarified multi-platform Windows (.ps1) usage.
- Initial release: Core orchestration for SOPS age encryption and Docker deployment setups.