Phase 0 on synlig: install, palace layout, verified Host/Origin policy

synlig is greenfield — no mempalace, no ~/.mempalace at all — so Phase 0
became "provision correctly from birth" rather than "migrate carefully".
Nothing is serving; no client config was touched.

Done:
- mempalace 3.6.0 installed via uv, pinned to the fleet version (the id
  recipes and idempotency probes this RFC leans on are version-specific).
- Embedder pre-warmed. This was the real unknown: the first embed pulls a
  79.3 MB ONNX model from the chroma CDN, and an egress-filtered work VM
  would have failed at the worst moment — the first client write. Pulled
  at ~20 MB/s, no proxy interference. Done in a throwaway palace so the
  real one never saw it.
- Palace at the stock default ~/.mempalace/palace, so no config file and
  no MEMPALACE_PALACE_PATH is needed on synlig at all.

Two corrections to the RFC, both from provisioning rather than reading:

- §7.1 was understated. DEFAULT_PALACE_PATH (~/.mempalace/palace) and
  DEFAULT_KG_PATH (~/.mempalace/knowledge_graph.sqlite3) already differ
  with stock defaults, so the KG split is out-of-the-box behaviour, not a
  consequence of a custom --palace, and it is permanent rather than
  one-time: serve always passes --palace, any CLI call without it uses
  the HOME path. A one-time mv does not fix that, it only picks which of
  the two files gets populated. Fixed instead by converging both rules on
  one inode via relative symlinks, and verified the load-bearing
  assumption: a dangling symlink is created on connect, -wal/-shm land
  next to the target (so the palace dir stays a self-contained backup
  unit, which is the part that mattered), cross-path read works, same
  inode. hallways.json deliberately left alone — already palace-derived,
  HOME path is a warning-only probe.
- §6.2 upgraded from "test early" to verified: 11/11 as predicted. The
  headline is that the safe-sounding reflex is the failure mode — loopback
  bind + proxy forwarding a public Host is 403, non-loopback is 200. Also
  confirmed Origin is never relaxed on either bind, and /healthz is
  Host/Origin-gated but token-free, so it works as the tunnel probe.
  Recommends binding the docker0 gateway over 0.0.0.0: non-loopback so
  the pin relaxes, but reachable only from the host and its containers.

New docs/synlig-primary-runbook.md carries the discovered facts about the
box, the evidence tables, an explicit "deliberately not done" list, and
tomorrow's ordered steps. New contrib/systemd/mempalace-serve.service
carries the bind rationale inline so nobody "fixes" it back to loopback;
staged on synlig with a .staged suffix so systemd cannot pick it up by
accident.

Flagged for tomorrow: synlig has no newt/tunnel client (docker ps shows
only the Gitea runner and digikam), so Pangolin on nyvaken cannot reach
it until one is added — easy to miss, because Pangolin will look healthy
from its own side.
This commit is contained in:
Joakim Persson
2026-08-10 00:14:06 +02:00
parent 7e51055c96
commit 3626946013
3 changed files with 247 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
[Unit]
Description=MemPalace remote MCP server (the fleet primary — RFC-001)
Documentation=https://gitea.jordbo.se/joakimp/mempalace-toolkit
Documentation=file:%h/mempalace-toolkit/docs/rfc-001-global-palace.md
After=network-online.target
Wants=network-online.target
# Refuse to start if the palace is missing — better a clear failure than a
# server quietly creating an empty palace somewhere unexpected.
ConditionPathExists=%h/.mempalace/palace
[Service]
Type=simple
# ── Bind address: NOT 127.0.0.1. This is deliberate and load-bearing. ────────
# mempalace pins the HTTP Host header to loopback literals *only on a loopback
# bind* (mcp_server.py: enforce_host_pin = _http_is_loopback(host)). Behind a
# reverse proxy / tunnel that forwards the public hostname, a loopback bind
# answers 403 Forbidden. Verified empirically on synlig 2026-08-10:
# bind 127.0.0.1 + Host: palace.example.com -> 403
# bind 172.17.0.1 + Host: palace.example.com -> 200
# 172.17.0.1 is the docker0 gateway: non-loopback (so the pin relaxes), but
# reachable only from this host and its containers — so a newt/Pangolin tunnel
# container on this box can reach it while the LAN cannot. Use 0.0.0.0 only if
# the tunnel does not run in Docker here, and only with the firewall closed.
#
# The Origin check is NEVER relaxed: a request carrying a non-loopback Origin
# is 403 with no override. Fine for MCP clients (they send none); fatal for
# browser-based clients.
#
# No --token here on purpose: for a non-loopback bind, serve reuses or mints a
# 0600 token at ~/.mempalace/server/<hash-of-palace-path>/token and keeps it
# stable across restarts. Read it from there to configure clients; never paste
# it into this unit (units are world-readable).
ExecStart=%h/.local/bin/mempalace serve --host 172.17.0.1 --port 8765
Restart=on-failure
RestartSec=5s
# The server serializes every request behind one lock, so a wedged process is
# a fleet-wide outage. Fail fast and let Restart= recover.
TimeoutStopSec=30
# Journal: journalctl --user -u mempalace-serve -f
StandardOutput=journal
StandardError=journal
# Modest hardening (user units can't do much, but these are free)
NoNewPrivileges=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectControlGroups=true
[Install]
WantedBy=default.target
# ── Enabling (needs one sudo, hence not done by the Phase 0 prep) ───────────
# sudo loginctl enable-linger $USER # else the unit dies with your login
# systemctl --user daemon-reload
# systemctl --user enable --now mempalace-serve
# curl -s localhost:8765/healthz # -> 403 (expected! see bind note)
# curl -s 172.17.0.1:8765/healthz # -> ok