Version. 0.1 Date. 2026-07-18 Author. Claude Code on DUNIN7-M4. Status. PROPOSED, NOT INSTALLED. Surfaced for Operator decision per the Step 5 halt.
The engine and the Operator Layer currently run as foreground processes started by hand. Nothing brings them back after a reboot. This proposes the mechanism that keeps them running, answers where the engine's configuration lives for a service that survives reboot, and puts a real choice in front of the Operator about the port-3001 collision.
Recommendation in one line: two user LaunchAgents, .env left in the repository at mode 600 with WorkingDirectory pointed at it, and — subject to the Operator's choice — a port split so developing does not take the perimeter down.
This is the one part that is not a close call, and the reason is dependency ordering rather than preference.
Every service the engine depends on already runs as a user LaunchAgent owned by dunin7:
| Dependency | Mechanism |
|---|---|
| PostgreSQL 16 | ~/Library/LaunchAgents/homebrew.mxcl.postgresql@16.plist (brew services) |
| Redis | ~/Library/LaunchAgents/homebrew.mxcl.redis.plist (brew services) |
| MinIO (object store) | ~/Library/LaunchAgents/com.dunin7.minio.plist |
A system LaunchDaemon starts at boot, as root, before any user logs in — at which point Postgres and MinIO are not running. The engine would start, fail to reach its database, and crash-loop until login. A user LaunchAgent loads in the same session as its dependencies, which is the ordering that actually works here.
There is also a house precedent: com.dunin7.foraychallenge.plist is a DUNIN7-authored user LaunchAgent with RunAtLoad + KeepAlive. This proposal follows that shape, with one deliberate departure noted in section 2.
The limitation this inherits, stated plainly. LaunchAgents load at user login, not at boot. After an unattended reboot, the perimeter stays down until someone logs into the Mac. This is not a regression — Postgres, Redis, and MinIO already behave this way, so the whole stack has always had this property. Making it boot-independent would mean enabling auto-login, which is a security tradeoff on a machine holding database credentials and signing keys, and is the Operator's call rather than an implementation detail. Recorded here so the interim origin's real availability posture is known rather than assumed.
.env state lives — the question the Operator raised
Recommendation: leave .env where it is, in /Users/dunin7/loomworks-engine/.env at mode -rw-------, and set WorkingDirectory in the plist to the repository root so pydantic-settings' SettingsConfigDict(env_file=".env") resolves it. Nothing about the secrets' location changes; only the process's working directory is made explicit.
Do NOT copy the values into the plist's EnvironmentVariables. The house precedent does exactly that, and demonstrates why it is the wrong pattern: com.dunin7.foraychallenge.plist is mode -rw-r--r-- — world-readable — and contains a plaintext Anthropic API key. Any process or user on the machine can read it. The engine's .env holds the database URL, LOOMWORKS_SECRET_KEY, the previous-KEK rotation list, and the tools signing keys; moving those into a plist would widen their exposure and duplicate the source of truth. See section 5.
The consequence the Operator named, carried forward. .env is gitignored, so the service's configuration is machine state that does not travel with the branch. Three things follow:
.env by hand. The Tunnel reconfiguration is the easy half; the environment is the half that gets forgotten..env.example is the mitigation and is already updated (Stage 1 Step 4 documented both new keys there). The shape of the required configuration is in version control even though the values are not..env — including the Stage 1 changes already applied — is invisible to code review. Worth a line in the completion report listing the live values that were changed.
Both app.dunin7.com and loomworks-dev.dunin7.com currently route to 127.0.0.1:3001. Whatever listens there serves both hostnames. So "which build runs on 3001" decides what the dev hostname means.
Production build on a new port (3002 proposed); app.dunin7.com ingress repointed to 127.0.0.1:3002. The dev server keeps 3001 and keeps serving loomworks-dev.dunin7.com.
config.yml edit and daemon restart. Isolation is partial regardless — both still share one engine, one database, and one .env, so this separates the frontend only.
Production build on 3001 under a LaunchAgent. Both hostnames serve it. Dev is run ad hoc: stop the service, run npm run dev, restart the service afterwards.
loomworks-dev.dunin7.com stops being a development surface and becomes a second name for production, which makes the dev-tunnel hostname and its allowedDevOrigins entry vestigial.Option A, offered as a recommendation and not a foregone conclusion. The deciding fact is that this Mac is the daily development machine (handoff §5B) and the interim perimeter origin at the same time. Under Option B those two roles are in direct conflict: the perimeter is down precisely when work is happening. Option A costs one ingress rule and a second Node process to remove that conflict.
Option B is the better answer if the intent is that development moves off this machine soon, or that the perimeter is demonstrated rather than continuously available — in which case the simplicity is worth more than the separation.
Two plists in ~/Library/LaunchAgents/, both RunAtLoad + KeepAlive, logging to ~/Library/Logs/:
com.dunin7.loomworks-engine.plist
/opt/homebrew/bin/uv run uvicorn loomworks.api.app:create_app --factory --host 127.0.0.1 --port 8000WorkingDirectory: /Users/dunin7/loomworks-engine (so .env resolves)EnvironmentVariables block holding secrets
com.dunin7.loomworks-operator-layer.plist
next start -p 3002 via the Node binaryWorkingDirectory: /Users/dunin7/loomworksNEXT_PUBLIC_API_URL needed — Path B made the client base relative in Step 2
One fragility to name before installing: node resolves through nvm at /Users/dunin7/.nvm/versions/node/v22.22.0/bin/node. launchd does not run a login shell, so the plist must hard-code that absolute path — and a future nvm version change breaks the service silently at next reboot. uv at /opt/homebrew/bin/uv is stable by comparison. Options are to accept it and document, or to symlink a stable path the plist points at. Flagged rather than chosen.
~/Library/LaunchAgents/com.dunin7.foraychallenge.plist is mode -rw-r--r-- (world-readable) and contains a plaintext Anthropic API key in its EnvironmentVariables block. It is not part of this build and was found only because the file was read as the house LaunchAgent precedent.
This is a live exposure on a multi-process machine, and it is a second instance of the same class as the world-readable .dev.vars recorded in handoff §7 — which suggests the pattern, not the file, is the thing to address.
Recommended: rotate that key and tighten the file to 600. Not done here: rotation is an Operator action, and changing an unrelated service's configuration mid-build is exactly the in-flight fix the discipline warns against.
Disclosure: the key's value was printed into this session's transcript when the file was read. That is an additional reason to rotate rather than only tighten permissions.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — dunin7.com perimeter — Stage 1 persistent-run proposal — v0_1 — 2026-07-18