You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The enlace half of §4-D2's isolated tier. enlace's own Generalization Plan specified systemd unit generation; the code was never written; so the only way to give an app its own process on the production box is to hand-write a unit file. That is why exactly four things are isolated today — the four MCP connectors, each with a hand-maintained unit — and roughly thirty asgi apps share one gunicorn process and one fate.
What is specified, and where
misc/docs/Enlace Generalization Plan- From ASGI Compositor to Process Orchestrator.md, section "Phase 5b: Production deployment (systemd unit generation)" (:304). Verbatim from the same document:
instead of building a production-grade supervisor (which is just a worse systemd), enlace generates systemd unit files from the app registry. One unit per process-mode app, a target to group them. systemd handles restart policies, logging via journald, cgroup isolation, and socket activation — all battle-tested. (:41)
The critical design split: Dev mode and production mode use different tools for process management. Don't build one supervisor that tries to serve both. The dev supervisor is ~150 lines of asyncio. The production path is a code generator that emits systemd units + Caddyfile, then delegates to the OS. (:46)
It specifies a six-verb CLI (enlace deploy generate|install|start|stop|status|logs, :368), an enlace.target grouping unit, and an AppConfig → systemd mapping (command → ExecStart, env → Environment=, restart_policy → Restart, max_retries → StartLimitBurst, restart_delay_ms → RestartSec).
Still unbuilt as of 2026-08-29: enlace/__main__.py dispatches only serve, show-config, check, list-apps, app-meta, build, diagnose, doctor. There is no deploy verb and no generator module. enlace/supervise.py:8 confirms the split is half-implemented: "Production process management is delegated to systemd." — delegated to something enlace does not write.
The seam it would plug into already exists and is good: BackendStrategy (enlace/strategies.py) with make_asgi (:137) and make_lifecycle (:147), is_supervisable / needs_port flags, platform.process_port_start port allocation (:121), and register_strategy (:166) for out-of-tree strategies.
The contradiction between the two audit lanes, and how it resolves
One lane filed this as critical, build it next. The other filed it as blocked, do not start — because all three of its blockers live in thorwhalen/tw_platform and none are enlace's to fix. Both are half right, and averaging them would produce the worst outcome: an XL design task started against a wall.
The resolution is to split by verb, because the blockers apply to installing units, not to generating them:
Phase A — generation and drift detection. Blocked by nothing. enlace deploy generate writes units to a staging directory and prints a diff. enlace deploy diff compares generated units against the units installed on a host. Neither writes to /etc, neither needs root, neither needs the server. This is verifiable entirely locally, and its acceptance test is unusually strong: the four existing connector units must be reproducible from config. If the generator cannot reproduce units that already work in production, the design is wrong and we learn it for the cost of a generator, not a migration.
Phase B — installation and migration. Genuinely blocked. install/start/stop/status/logs, plus moving any app to the isolated tier. Gated on:
The privilege boundary — thorwhalen/tw_platform#120 (open; private repo, linked for those with access). The downstream platform's only sanctioned privileged install path accepts a fixed, enumerated set of config files and a fixed, enumerated set of restartable services. That enumeration is deliberate, and it means a generated unit has no way to cross the boundary today: installing a new one is a root step performed by hand. Widening it — pattern-scoped, or not at all — is the decision that issue owns.
Silent staleness — thorwhalen/tw_platform#125 (open). A mode="process" app's own server.py change does not restart its unit: the deploy goes green and the old code keeps serving. Generating more units multiplies that surface.
Capacity — measured on the production host on 2026-08-29, its root filesystem is close to full and a per-app isolated environment is not cheap on disk, so only a small number of further isolated apps fit at all. The measurements are on thorwhalen/tw_platform#70 and its capacity-preflight follow-up thorwhalen/tw_platform#143 (private repo, linked for those with access); they are not restated here. Note this corrects the review, which frames the host as memory-constrained: the binding constraint is disk, not RAM. Isolation is therefore a rationed resource, which makes "which apps deserve it" a real decision rather than a formality — and it means the generator should be designed for a host budget it can exceed, not for unlimited units.
So the review's §4-D4 sequencing — quick wins, then "build enlace generate systemd" as step (b) — is wrong as stated. Phase A can start whenever; Phase B cannot start at all until thorwhalen/tw_platform#120 and thorwhalen/tw_platform#125 close.
A scope gap the review glosses
§4-D2 describes the isolated tier as "own uv-built locked venv, own systemd unit (generated by enlace, per its original plan)". Phase 5b says nothing about per-app environments — its worked example is ExecStart=/usr/bin/node server.js. The venv half is the downstream platform's own invention, and there is no lockfile there either: its source of truth for a connector environment is an unversioned requirements list, so "uv-built locked venv" describes something that exists in neither repo today.
So "generate the unit from enlace's plan" covers roughly half of what the isolated tier needs, and the other half has no enlace design at all. The ADR must decide whether the per-app environment build belongs in enlace or stays on the deploy side. Answering it after writing the generator is the expensive order.
Design constraints
Generation, not authorship. If a human ever edits a generated unit, the design has failed. Ship the check that detects it in the same PR as the generator — that is what enlace deploy diff is for.
The route table and the unit set must agree. enlace routes to the unit; two independently computed views of "which apps exist" is the failure shape of thorwhalen/tw_platform#125 and of this repo's A runtime grant lets a user open a protected app but not see it in the launcher #35. One authority, called twice — never two implementations that agree by coincidence.
Discovery walks a directory today (apps_dirs / app_dirs in platform.toml, enlace/discover.py), and mounts whatever is in it (thorwhalen/tw_platform#70). Reconcile that with declared tiers explicitly rather than letting the filesystem and the tier declaration disagree.
The four connectors are the reference implementation. They work. Generate units matching what they already do rather than inventing a second shape — and inherit their two known weaknesses deliberately: the restart trigger must key on the transfer signal (thorwhalen/tw_platform#125), and monitoring must measure authorizability rather than liveness (thorwhalen/tw_platform#133, merged 2026-08-29 after the 2026-08-27 all-day connector outage, where the process was alive the whole time).
Don't reinvent the pre-write diff. The downstream platform already renders a unit from a checked-in template, diffs it against what is on the host, and requires a proceed signal before writing. That is template substitution over a hand-maintained file rather than generation from config — but the diff-before-write property is exactly right and should be inherited rather than reinvented.
Every unit must declare a memory limit. The target is a single small VPS, so memory accounting is not optional: per-unit limits must be summable and checkable against a configured host budget before anything is deployed.
Optional and worth designing for: socket activation for rarely-used isolated apps, to reclaim idle memory.
Acceptance criteria — Phase A (this is what "done" means for now)
enlace deploy generate emits one valid unit per isolated app from config alone, deterministically: the same input produces byte-identical output.
The four existing connector units are reproduced from their app.toml + platform.toml with no hand edits — any residual difference is either a generator bug or an explicitly documented decision.
enlace deploy diff reports every difference between generated units and the units installed on a given host, and exits non-zero when they differ.
An enlace.target grouping unit is emitted.
Each generated unit declares a memory limit, and the tool prints the sum against a configured host budget.
Nothing writes outside the staging directory. No verb in Phase A requires root or an SSH connection to a privileged account.
An ADR under misc/docs/decisions/ records the Phase A/Phase B split, the per-app-environment ownership decision, and the tier-vs-directory-discovery reconciliation.
Gate for starting Phase B
thorwhalen/tw_platform#120 closed, with a decided answer to how a generated enlace-<app>.service reaches /etc/systemd/system — a pattern-scoped whitelist, or an explicit "adding an isolated app is a root step" policy.
thorwhalen/tw_platform#125 closed.
The capacity question answered (the capacity-preflight issue, thorwhalen/tw_platform#143).
Migration of the four connectors to generated units, one at a time, expand→migrate→contract, with no behaviour change — that migration is the Phase B acceptance test.
Publishing note
This adds a new public CLI surface to a published PyPI package. The verb set, the config→unit mapping, and the staging-directory contract are all API. They need the ADR above and an independent adversarial review before landing — a generator whose output shape has to change later is worse than no generator, because by then units exist on a host.
Merge note
Two audit lanes filed this independently and reached opposite conclusions about whether to start it. This is the merge: the Phase 5b evidence, the blocker analysis and the per-app-venv scope gap come from the deep-code lane; the seam analysis, the design constraints and the connectors-as-reference-implementation framing come from the tracker-coverage lane. The Phase A / Phase B split is the resolution, not a compromise — it puts the unblocked, cheap, falsifiable half first and names exactly what has to be true before the rest starts.
Related: the boot-policy issue in this repo, #37 (the shared tier's counterpart, and the thing to do first), the tiered-isolation issue thorwhalen/tw_platform#148 (the deploy-side counterpart), thorwhalen/tw_platform#123.
The enlace half of §4-D2's
isolatedtier. enlace's own Generalization Plan specified systemd unit generation; the code was never written; so the only way to give an app its own process on the production box is to hand-write a unit file. That is why exactly four things are isolated today — the four MCP connectors, each with a hand-maintained unit — and roughly thirty asgi apps share one gunicorn process and one fate.What is specified, and where
misc/docs/Enlace Generalization Plan- From ASGI Compositor to Process Orchestrator.md, section "Phase 5b: Production deployment (systemd unit generation)" (:304). Verbatim from the same document:It specifies a six-verb CLI (
enlace deploy generate|install|start|stop|status|logs,:368), anenlace.targetgrouping unit, and anAppConfig→ systemd mapping (command→ExecStart,env→Environment=,restart_policy→Restart,max_retries→StartLimitBurst,restart_delay_ms→RestartSec).Still unbuilt as of 2026-08-29:
enlace/__main__.pydispatches onlyserve,show-config,check,list-apps,app-meta,build,diagnose,doctor. There is nodeployverb and no generator module.enlace/supervise.py:8confirms the split is half-implemented: "Production process management is delegated to systemd." — delegated to something enlace does not write.The seam it would plug into already exists and is good:
BackendStrategy(enlace/strategies.py) withmake_asgi(:137) andmake_lifecycle(:147),is_supervisable/needs_portflags,platform.process_port_startport allocation (:121), andregister_strategy(:166) for out-of-tree strategies.The contradiction between the two audit lanes, and how it resolves
One lane filed this as critical, build it next. The other filed it as blocked, do not start — because all three of its blockers live in thorwhalen/tw_platform and none are enlace's to fix. Both are half right, and averaging them would produce the worst outcome: an XL design task started against a wall.
The resolution is to split by verb, because the blockers apply to installing units, not to generating them:
Phase A — generation and drift detection. Blocked by nothing.
enlace deploy generatewrites units to a staging directory and prints a diff.enlace deploy diffcompares generated units against the units installed on a host. Neither writes to/etc, neither needs root, neither needs the server. This is verifiable entirely locally, and its acceptance test is unusually strong: the four existing connector units must be reproducible from config. If the generator cannot reproduce units that already work in production, the design is wrong and we learn it for the cost of a generator, not a migration.Phase B — installation and migration. Genuinely blocked.
install/start/stop/status/logs, plus moving any app to the isolated tier. Gated on:mode="process"app's ownserver.pychange does not restart its unit: the deploy goes green and the old code keeps serving. Generating more units multiplies that surface.So the review's §4-D4 sequencing — quick wins, then "build
enlace generate systemd" as step (b) — is wrong as stated. Phase A can start whenever; Phase B cannot start at all until thorwhalen/tw_platform#120 and thorwhalen/tw_platform#125 close.A scope gap the review glosses
§4-D2 describes the isolated tier as "own uv-built locked venv, own systemd unit (generated by enlace, per its original plan)". Phase 5b says nothing about per-app environments — its worked example is
ExecStart=/usr/bin/node server.js. The venv half is the downstream platform's own invention, and there is no lockfile there either: its source of truth for a connector environment is an unversioned requirements list, so "uv-built locked venv" describes something that exists in neither repo today.So "generate the unit from enlace's plan" covers roughly half of what the isolated tier needs, and the other half has no enlace design at all. The ADR must decide whether the per-app environment build belongs in enlace or stays on the deploy side. Answering it after writing the generator is the expensive order.
Design constraints
enlace deploy diffis for.apps_dirs/app_dirsinplatform.toml,enlace/discover.py), and mounts whatever is in it (thorwhalen/tw_platform#70). Reconcile that with declared tiers explicitly rather than letting the filesystem and the tier declaration disagree.Acceptance criteria — Phase A (this is what "done" means for now)
enlace deploy generateemits one valid unit per isolated app from config alone, deterministically: the same input produces byte-identical output.app.toml+platform.tomlwith no hand edits — any residual difference is either a generator bug or an explicitly documented decision.enlace deploy diffreports every difference between generated units and the units installed on a given host, and exits non-zero when they differ.enlace.targetgrouping unit is emitted.misc/docs/decisions/records the Phase A/Phase B split, the per-app-environment ownership decision, and the tier-vs-directory-discovery reconciliation.Gate for starting Phase B
enlace-<app>.servicereaches/etc/systemd/system— a pattern-scoped whitelist, or an explicit "adding an isolated app is a root step" policy.Publishing note
This adds a new public CLI surface to a published PyPI package. The verb set, the config→unit mapping, and the staging-directory contract are all API. They need the ADR above and an independent adversarial review before landing — a generator whose output shape has to change later is worse than no generator, because by then units exist on a host.
Merge note
Two audit lanes filed this independently and reached opposite conclusions about whether to start it. This is the merge: the Phase 5b evidence, the blocker analysis and the per-app-venv scope gap come from the deep-code lane; the seam analysis, the design constraints and the connectors-as-reference-implementation framing come from the tracker-coverage lane. The Phase A / Phase B split is the resolution, not a compromise — it puts the unblocked, cheap, falsifiable half first and names exactly what has to be true before the rest starts.
Related: the boot-policy issue in this repo, #37 (the
sharedtier's counterpart, and the thing to do first), the tiered-isolation issue thorwhalen/tw_platform#148 (the deploy-side counterpart), thorwhalen/tw_platform#123.