DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path operations/loomworks-dunin7-perimeter-stage-1-persistent-run-proposal-v0_2.md

Loomworks — dunin7.com perimeter — Stage 1 persistent-run proposal — v0.2

Version. 0.2 Date. 2026-07-18 (proposal); status corrected 2026-08-07. Author. Claude Code on DUNIN7-M4. Status correction by Claude Code (Wave 0 session). Status. INSTALLED 2026-07-18 — Option A adopted. Both LaunchAgents are live and loaded. Supersedes. v0.1 at record b8a12b4, which carried the stale PROPOSED, NOT INSTALLED header.

Changes from v0.1 (committed at b8a12b4). Header status corrected only — the proposal body below is unchanged and is preserved as written. v0.1 said PROPOSED, NOT INSTALLED; it was in fact installed the same day it was written. Nothing in the proposal's reasoning, options, or recommendations has been altered. The outcome is recorded in the new section immediately below (B-58).


What was actually installed (added in v0.2)

The proposal was accepted and installed on 2026-07-18, the same day it was written. The header was the only thing that ever disagreed; completion-records/loomworks-dunin7-perimeter-stage-1-completion-report-v0_1 recorded the installation at its Step 5 ("Production build served on 3002 under a user LaunchAgent; engine likewise on 8000. Dev server keeps 3001.").

Option A — the port split — was adopted, as recommended in section 3. Verified against the live machine on 2026-08-07:

| What | Verified state | |---|---| | ~/Library/LaunchAgents/com.dunin7.loomworks-engine.plist | present; port 8000 | | ~/Library/LaunchAgents/com.dunin7.loomworks-operator-layer.plist | present; production on 3002, dev left on 3001 | | launchctl list | both labels loaded, both holding live PIDs |

Section 4's named fragility was resolved rather than accepted. The proposal flagged that hard-coding nvm's absolute node path would break the service silently at a future version change, and offered two options — accept and document, or symlink a stable path. The symlink option was taken: the plist invokes /Users/dunin7/.local/bin/loomworks-node, a symlink created 2026-07-18 20:25 pointing at …/node/v22.22.0/bin/node. The plist itself therefore carries no nvm version string.

Section 5's unrelated finding — the world-readable plist holding a plaintext Anthropic API key — is NOT addressed by this correction and is not claimed to be resolved here. It was surfaced-not-fixed in v0.1 and this status correction makes no assertion about its current state.


Plain-language summary

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.


1. The mechanism: user LaunchAgents, not system LaunchDaemons

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.

2. Where .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:

3. The port-3001 collision — a real choice

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.

Option A — Port split

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.

Option B — Single served app

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.

Recommendation

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.

4. What would be installed (Option A shape)

Two plists in ~/Library/LaunchAgents/, both RunAtLoad + KeepAlive, logging to ~/Library/Logs/:

com.dunin7.loomworks-engine.plist

com.dunin7.loomworks-operator-layer.plist

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.

5. Unrelated finding, surfaced not fixed — exposed API key

~/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