Audit of every doc touching the global-palace rollout against the running
fleet. Each correction below was verified against the filesystem or the host,
not against another doc:
- synlig-primary-runbook: the decommission `rm -rf ~/.mempalace` now carries a
STOP block. That tree holds the fleet palace *and* the only copy of the
bearer token every client authenticates with; the old "empty today" comment
stopped being true when the palace was seeded on 2026-08-14. Adds an ordered
safe decommission, and drops count-based join verification.
- phase-1-exposure-runbook: new S3.8, how to verify a flip actually took --
the procedure that until now existed only in an untracked handover file.
Three claims that fail independently (env var / curl / the palace-path
discriminator) plus an explicit list of checks that produce FALSE POSITIVES:
drawer counts (both sides were seeded from the same palace, and `status`
counts chunks not drawers), write-then-read through the same transport, and
the `mempalace` CLI -- which has no remote support at all, so post-flip it
reads the dead local archive and reports success.
- rfc-001: status Draft -> Phases 0-1 implemented. Records that the join was a
file-level copy, which SIDESTEPPED the S7.6 diary-dedup question rather than
answering it -- so S7.6 remains a hard blocker for the second machine, which
is the one that will actually exercise merge semantics.
- ARCHITECTURE, SKILL, contrib/README, extensions/pi/README all claimed pi
feeds the palace automatically, unconditionally. That is gated on
mempalace-toolkit >= 29e660e and every deployed image predates it, so the
claim is currently false fleet-wide. Each site now states the gate plus a
check that inspects the *deployed* file rather than repo HEAD.
- extensions/pi/README: plaintext http://mempalace.lan example -> https
endpoint; the two transports are either/or (no dual-write, no local mirror);
the bridge fails CLOSED, so "the agent has no mempalace_* tools" is the
expected symptom of a server/token/DNS fault, not of a broken install.
- contrib/README: documents mempalace-serve.service, which this directory has
shipped since day one without explaining it (linger, the load-bearing
172.17.0.1 bind and why loopback is the unsafe-looking-safe option, the
token path, and an uninstall warning).
- Fixes a pre-existing stray ```sh fence that was swallowing S3.2's heading and
the token command into a code block.
Docs only; no behaviour change.
15 KiB
synlig primary — Phase 0 runbook and handoff
Companion to rfc-001-global-palace.md. Records what was actually done
on the primary, with verified evidence, so the next session (or the next machine) does not re-derive it.
Status 2026-08-14 17:00 — SUPERSEDED IN PART. The primary is live, exposed, and seeded. Serving since 2026-08-12 at
https://mempalace.jordbo.se/mcp. Seeded 2026-08-14 15:07 from EMB-7KJ4VR4G's palace — 14,777 drawers / 9 wings / 16,337 embeddings / KG 46 entities, 34 triples, now 14,803 drawers. One client (EMB-7KJ4VR4G's pi-devbox container) is flipped and verified end-to-end. Both Phase 0 blockers below are cleared.Read §3 "Deliberately NOT done" as a record of the 2026-08-10 state, not of today's — every item in it has since been done. And before running anything in §5 Rollback, read the warning at the top of it:
~/.mempalaceon synlig is no longer disposable.
Original status, kept for the record:
Status 2026-08-10 00:30 — Phase 0 prep complete. Not serving. Nothing exposed.
Blocked on two things, both deliberately left to Joakim: the Pangolin update on nyvaken, and one sudo.
1. What synlig is (discovered, not assumed)
| Fact | Value |
|---|---|
| SSH | synlig → synlig.erdc.ericsson.net, user ecsjper (from ~/.ssh/config) |
| OS | Ubuntu 24.04.4 LTS, 7.8 GiB RAM, 78 G disk (29 G free), uptime 12 d |
| Python / uv | system python3 3.12.3; uv at ~/.local/bin/uv (not on the non-login PATH) |
| Interfaces | lo 127.0.0.1, ens3 10.0.0.4/16, docker0 172.17.0.1/16, br-… 172.19.0.1/16 |
| Already listening | 22, 80, 443, 3000 (node), 3389 + 3350 + 4822 (xrdp/guacamole), 631 |
| Docker | present; running act_runner-runner-1 (Gitea Actions runner) and digikam |
| Pre-existing MemPalace | none — no mempalace binary, no ~/.mempalace. Greenfield. |
The Gitea Actions runner living here is worth remembering: synlig is not a dedicated appliance, and CI load competes with the palace for the same 7.8 GiB.
2. Done tonight
2.1 MemPalace installed, pinned to the fleet version
~/.local/bin/uv tool install "mempalace==3.6.0" # → mempalace, mempalace-mcp
Pinned deliberately: the clients run 3.6.0, and the id recipes / idempotency probes this RFC leans on are
version-specific. Reversible with uv tool uninstall mempalace.
2.2 Embedder model pre-warmed — the corporate-network risk that wasn't
The first embed pulls all-MiniLM-L6-v2 ONNX (79.3 MB) from the chroma CDN into
~/.cache/chroma/onnx_models/ (167 M on disk once unpacked). This was the main unknown — an
egress-filtered work VM would have failed here, at the worst possible moment (first client write).
It downloaded at ~20 MB/s with no proxy interference. Done in a throwaway palace, since deleted, so the
real palace never saw it. Same model as the clients use, so the semantic space matches.
2.3 Palace created with the §7.1 landmine structurally removed
~/.mempalace/palace — the stock default, so no MEMPALACE_PALACE_PATH and no config.json is
needed anywhere on synlig. One less thing to drift.
RFC §7.1 says to mv three HOME-anchored stores into the palace dir before first serve. On a
greenfield primary there is nothing to move — but the hazard is not actually a migration hazard, and the
RFC understated it: with stock defaults palace_path is ~/.mempalace/palace while DEFAULT_KG_PATH
is ~/.mempalace/knowledge_graph.sqlite3. Those differ, so the split is the out-of-the-box behaviour,
not a consequence of a custom path. It is permanent, not one-time: serve always passes --palace (KG
inside the palace), while any CLI command run without --palace uses the HOME path. Two KGs on one box,
forever, silently.
Fixed by making both resolution rules land on one inode:
ln -sfn palace/knowledge_graph.sqlite3 ~/.mempalace/knowledge_graph.sqlite3
ln -sfn palace/known_entities.json ~/.mempalace/known_entities.json
Relative targets, so a home-directory move survives. hallways.json was originally left unsymlinked:
it is already palace-derived, and its HOME path is a warning-only legacy probe (hallways.py:73-95) that
never auto-migrates.
Update 2026-08-14 —
hallways.jsonis now symlinked too, during the seeding session:ln -sfn palace/hallways.json ~/.mempalace/hallways.jsonRationale changed: the point is no longer "only symlink what the code demands" but all real state lives under
palace/as a single backup unit, so onepalace/copy is a complete copy. All three parent-level paths now resolve, which matters because mempalace 3.6.0 resolves these three paths inconsistently (MCP server: palace-relative; KG CLI default:~/.mempalace;hallways.json:dirname(palace_path);known_entities.json: hardcoded~). Revert by deleting the symlink if it ever causes trouble.
Verified the symlink assumption rather than trusting it (python3 sqlite3 on synlig, temp dir):
| Check | Result |
|---|---|
Dangling symlink + sqlite3.connect |
creates the target |
-wal / -shm placement |
next to the target, inside the palace dir — not beside the symlink |
| Write via symlink → read via palace path | same data, same inode |
The WAL placement is the part that mattered: it keeps the palace directory a single self-contained backup/bind-mount unit.
2.4 §6.2's Host/Origin policy verified by experiment, not by reading
Ran on synlig, loopback and docker0 binds, then stopped. 11/11 as predicted:
| # | Bind | Request | Expected | Got |
|---|---|---|---|---|
| A1 | 127.0.0.1 | /healthz, correct Host |
200 | ✅ 200 |
| A2 | 127.0.0.1 | /healthz, Host: palace.example.com |
403 | ✅ 403 |
| A3 | 127.0.0.1 | /healthz, Origin: https://evil.example |
403 | ✅ 403 |
| A4 | 127.0.0.1 | POST /mcp, no token |
401 | ✅ 401 |
| A5 | 127.0.0.1 | POST /mcp, wrong token |
401 | ✅ 401 |
| A6 | 127.0.0.1 | POST /mcp, correct token |
200 | ✅ 200 (tools/list → 36 tools) |
| B1 | 172.17.0.1 | /healthz, bound-host Host |
200 | ✅ 200 |
| B2 | 172.17.0.1 | /healthz, Host: palace.example.com |
200 | ✅ 200 |
| B3 | 172.17.0.1 | /healthz, Origin: https://evil.example |
403 | ✅ 403 |
| B4 | 172.17.0.1 | /healthz, loopback Origin |
200 | ✅ 200 |
| B5 | 172.17.0.1 | POST /mcp, foreign Host + token |
200 | ✅ 200 |
Operational conclusions:
- Do not bind loopback behind the tunnel. A2 vs B2 is the whole story: the reflex "bind 127.0.0.1, it's safer" produces a 403 that looks like a Pangolin misconfiguration and is not one.
- Bind
172.17.0.1(docker0). Non-loopback, so the Host pin relaxes — but reachable only from synlig and its containers, so a newt container on this box can reach it while the LAN cannot. This is strictly better than0.0.0.0here. It is whatcontrib/systemd/mempalace-serve.serviceuses. Originis never relaxed (B3). No browser-based MCP client, and no proxy that injectsOrigin./healthzis Host/Origin-gated but token-free — a usable liveness probe for the tunnel.
Test script kept at /tmp/synlig-phase0-test.sh on this container (ephemeral — re-create from the table
above if needed; it starts, probes and stops the server, and asserts nothing is left listening).
2.5 A start unit, written but not installed
contrib/systemd/mempalace-serve.service — user unit, follows the existing contrib/systemd/ style,
carries the bind rationale inline so nobody "fixes" it back to loopback. A copy is already staged on synlig
at ~/.config/systemd/user/mempalace-serve.service.staged — the .staged suffix is deliberate:
systemd only reads *.service, so the file cannot be activated by accident, not even by a stray
daemon-reload. Not installed, not enabled: it needs one sudo loginctl enable-linger, and
standing up a network-reachable service while you were asleep was not mine to decide.
3. Deliberately NOT done
⚠️ Historical — this section describes 2026-08-10 and is no longer true. All five items were done between 2026-08-12 and 2026-08-14. Kept because the reasoning for deferring them is still the record of why the order was chosen. Current state per item is inlined below.
- Nothing is serving. No listener on 8765; no mempalace process. Re-verified at the end of the run.
→ Now serving since 2026-08-12 (
mempalace-serve.service,systemctl --user), reachable athttps://mempalace.jordbo.se/mcpvia newt/Pangolin. - No client
.envwas touched. Your working setup is exactly as you left it (R6: reversible). → One client flipped 2026-08-14: four variables on EMB-7KJ4VR4G,docker-compose.yamlunchanged. Still reversible in ~30s (§3.8 ofphase-1-exposure-runbook.md). - No data joined. The palace is empty. The §4.4 join needs the diary-dedup decision (§7.6) first — replaying diaries today duplicates them, and the primary is the one place that must stay clean. → Seeded 2026-08-14 from one palace by file-level copy. This sidestepped §7.6 rather than solving it: a file-level copy replays no diaries, so it cannot duplicate them. §7.6 is still a hard blocker for the second machine to join.
- nyvaken untouched. Read nothing, changed nothing.
- No sudo.
sudo -non synlig requires a password.
4. Tomorrow, in order
2026-08-12: items 1–2 and 5 now have their own runbook —
phase-1-exposure-runbook.md. Pangolin on nyvaken is updated (done), and newt is now installed on synlig and connected to Pangolin (done 2026-08-12) — so the blocker is now item 3, the onesudo. That doc also records why per-device Pangolin users are the wrong layer, why the HTTPS tunnel and the feeder's SSH path are not redundant (§1.3), the client-flip variable trap (§3.7), and an additional loopback finding: a loopback bind does not merely 403, it also silently starts the server with no token at all (auto-minting is gated on the bind being non-loopback).
- Pangolin update on nyvaken (yours). ✅ done 2026-08-12.
⚠️ synlig has no tunnel client.✅ done 2026-08-12 — newt installed and connected to Pangolin. (Kept for the reasoning:docker psshowed only the Gitea runner and digikam. Pangolin on nyvaken cannot reach synlig by itself; synlig had to dial out. Easy to miss because Pangolin looks healthy on its own side — which is also why "connected" is not yet proof it can reach the palace: verify172.17.0.1:8765/healthzfrom inside newt's namespace, exposure runbook §3.3.) Since newt runs in Docker here, the docker0 bind above is already correct for it.- One sudo, then start (the unit is already staged; just drop the suffix). ✅ done 2026-08-12 —
linger enabled, unit enabled,
172.17.0.1:8765/healthz→ok.⚠ Corrected 2026-08-12: this line predictedsudo loginctl enable-linger ecsjper cd ~/.config/systemd/user && mv mempalace-serve.service.staged mempalace-serve.service systemctl --user daemon-reload && systemctl --user enable --now mempalace-serve curl -s 172.17.0.1:8765/healthz # ok curl -s 127.0.0.1:8765/healthz # NOTHING — refused, exit 7 (not 403; see below) ss -ltnp | grep 8765 # 172.17.0.1:8765 only403. The real run returned empty, which is more reassuring. With the docker0-only bind nothing listens on loopback, so the connection is refused before any header is sent (%{http_code}→000,$?→7). The 403 in §2.4 is the loopback-bind case: a server on127.0.0.1answering a proxy-forwarded foreignHost:. Two different failures that were collapsed into one expectation here. - Collect the shared token (auto-minted on first non-loopback start, stable across restarts):
That directory name is
cat ~/.mempalace/server/f5d849287f6d73f0141b29d7/tokensha256(realpath(palace))[:24]— it changes if the palace path ever changes. - Route it through Pangolin, then verify
/healthzend-to-end through the public hostname before pointing any client at it. - Then, and only then, Phase 1 client flip — one machine first, and remember opencode containers
need the §4.1 sidecar merge (or Phase 1.5) before the
.envtakes effect. - Before the first join: settle §7.6 diary dedup, then dry-run §4.4 from one palace.
Correction 2026-08-14 — do NOT verify a join "by checking counts", which is what this item originally said. Counts are not evidence, in either direction.
mempalace statuscounts chunk rows, not logical drawers (3 drawers plus one 2-chunk diary presented as +9), and chunk counts legitimately differ between two palaces whenever a drawer was updated on either side, because an update re-chunks to the new length and deletes the surplus rows. Diffing chunk-id sets is a useful first pass but over-reports: a chunk id present on one side only is the ordinary signature of an edit, not of loss. This cost real time on 2026-08-14 — a missingchunk_000007was read as a truncated seed, when in fact the two palaces held two revisions of one drawer and nothing was lost. Adjudicate by fetching the parent drawer on both sides and comparing the reassembledcontent.
5. Rollback
⚠️ STOP — 2026-08-14. Do not run this block as it was originally written.
~/.mempalaceon synlig is now the fleet primary. That tree holds the only central palace (14,803 drawers, seeded from EMB-7KJ4VR4G) and the server's bearer token at~/.mempalace/server/<hash>/token— the single credential every flipped client authenticates with, of which there is no second copy.rm -rf ~/.mempalacedestroys both. The original comment ("empty today — check before running once it isn't") is far too soft for a destructive command someone runs under pressure, which is exactly why it is being replaced rather than amended.
Stopping the service is safe and reversible on its own, and is the whole of what "rollback" should normally mean now:
systemctl --user disable --now mempalace-serve # clients fail CLOSED — they lose the palace
# tools; they do NOT fall back to a local palace
To genuinely decommission the primary, in this order:
- Flip every client back first (§3.8 of
phase-1-exposure-runbook.md, in reverse) so nothing is pointed at a host that is about to lose its palace. - Copy
~/.mempalace/palace/and the token file off the host, and verify the copy by comparing reassembled drawercontent, not counts (see §4 item 7). - Only then remove anything. Never
rsync --deleteinto~/.mempalace— the token lives inside it.
The two destructive steps below were written on 2026-08-10, when ~/.mempalace was genuinely empty.
Kept for the record; must not be run while the primary is live:
~/.local/bin/uv tool uninstall mempalace
rm -rf ~/.mempalace # ⚠️ DESTROYS THE FLEET PALACE AND THE ONLY TOKEN