docs(rfc-001): require solitary-first operation, opt-in centralization

Adds §1.1 (R1–R6) and §1.2 as a hard constraint on the design rather than a
preference. A shared palace is valuable only for the multi-machine /
multi-container pattern; for most users of the published pi-devbox and
opencode-devbox images it is useless overhead. Evidence: all three sampled
opencode-devbox deployments contain zero mempalace references.

Requirements: solitary operation stays the default and stays byte-identical
(no extra process, no outbox, no network calls); opt-in via docker-compose.yml
+ .env only, never an image rebuild; credentials only in .env, never in
compose, the image, a command line, or a log; no new required services (the
primary stays a separate standalone compose project); degrade-not-fail where
mempalace is absent; fully reversible.

§1.2 extends the convention pi-devbox already ships (.env.example:12-23,
"local by default" with commented MEMPALACE_REMOTE_URL/_TOKEN) into a
three-state ladder — local stdio (default, unchanged) / direct remote
(unchanged) / edge (new, MEMPALACE_EDGE=1) — instead of inventing a new
mechanism. Notes that the devbox-palace volume coupling reverses under edge
mode: the local palace holds the outbox, so persisting it becomes required
rather than irrelevant.

Consequence recorded in §4.1: mempalace-edge must be selected at registration
time, not left always-in-path to decide by env at runtime, since that would
insert a process and a failure mode into every solitary user's setup. pi
branches in createClient(); opencode needs its static MCP JSON templated at
container start, which is new open question §9.6.

Also: adds an R1 acceptance test, marks pi-devbox/.env.example:21 as stale
(advertises mempalace-mcp --transport http rather than mempalace serve
--token/--tls-cert), and extends the evidence index.
This commit is contained in:
2026-08-09 15:02:51 +02:00
parent 35b1e3d81d
commit 661ee20b39
+77 -5
View File
@@ -5,7 +5,7 @@
| **Status** | Draft — design agreed, not implemented | | **Status** | Draft — design agreed, not implemented |
| **Created** | 2026-08-08 | | **Created** | 2026-08-08 |
| **Applies to** | mempalace 3.6.0, mempalace-toolkit @ `96699f2`, pi-devbox ≥ v1.3.0 | | **Applies to** | mempalace 3.6.0, mempalace-toolkit @ `96699f2`, pi-devbox ≥ v1.3.0 |
| **Decision** | Phases 02 + 4 in scope. **Phase 3 (full pull replication) explicitly deferred** — "a laptop that can reach its own stuff plus whatever it can reach" is good enough. | | **Decision** | Phases 02 + 4 in scope. **Phase 3 (full pull replication) explicitly deferred** — "a laptop that can reach its own stuff plus whatever it can reach" is good enough. **Centralization is strictly opt-in — solitary devbox operation remains the default and must not change (§1.1).** |
**Read this first if you are asked to "centralize MemPalace" / "sync palaces between machines".** Most of the **Read this first if you are asked to "centralize MemPalace" / "sync palaces between machines".** Most of the
hard-won facts below are non-obvious and two of them are actively destructive if you guess wrong hard-won facts below are non-obvious and two of them are actively destructive if you guess wrong
@@ -28,6 +28,56 @@ One palace per machine per harness. Today: a pi-devbox container on `EMB-7KJ4VR4
primary is unreachable (writes buffer locally, reads degrade to local), and reconciles when it primary is unreachable (writes buffer locally, reads degrade to local), and reconciles when it
returns. Access is authenticated per device, with a sane authorization policy. returns. Access is authenticated per device, with a sane authorization policy.
### 1.1 Hard requirement: solitary-first, centralization strictly opt-in
**This is a requirement, not a preference, and it constrains every choice below.**
Centralization solves a problem specific to *this* usage pattern: several machines and containers
(pi-devbox, opencode-devbox, native hosts) doing development and other work against a **common set of
artifacts**. For most users of the published `joakimp/pi-devbox` and `joakimp/opencode-devbox` images
it is **useless** — one machine, one palace, done. Evidence that this is already the norm: all three
sampled `opencode-devbox` deployments (`docker-compose-repo/{synlig,nyvaken,devbox-affection}/`)
contain **zero** mempalace references.
| ID | Requirement |
| --- | --- |
| **R1** | **Solitary operation stays the default and stays unchanged.** With no `MEMPALACE_*` variables set, a devbox must behave exactly as it does today: harness → local `mempalace-mcp` over stdio, palace at `~/.mempalace`. No extra process, no outbox, no network calls, no new failure mode, no measurable startup cost. |
| **R2** | **Opt-in lives in `docker-compose.yml` + `.env`** — the mechanism users already know. No opt-in via image rebuild, no baked-in defaults, no `latest-central` variant. |
| **R3** | **Credentials only in `.env`** (`chmod 600`, gitignored). Never in `docker-compose.yml`, never in the image, never on a command line (visible in `ps`), never logged. Compose passes them through with a `${VAR:-}` empty default so solitary users never define them. |
| **R4** | **No new required services.** The primary stays in the separate standalone `docker-compose.mempalace.yml` project; it is never merged into the main compose file. A solitary `docker compose up` starts exactly what it starts today. |
| **R5** | **Degrade, never fail, when mempalace is absent.** opencode-devbox may not ship mempalace at all, so any wiring must be additive and skipped when the binary is missing — the probe-and-warn idiom `install.sh` already uses (`warn` + `return 0`, never halt). |
| **R6** | **Reversible.** Commenting the `.env` lines out returns the container to pure solitary operation, with the local palace intact and readable. |
**Acceptance test for R1** (must pass before Phase 2 ships): bring up a devbox with no `MEMPALACE_*`
variables; assert the palace tool list, drawer counts and diary writes are identical to the previous
image, and that no edge/outbox process exists (`pgrep -f mempalace-edge` → empty).
### 1.2 The opt-in surface
The existing convention is already the right one — **extend it, do not invent a new one.** pi-devbox's
`.env.example` lines 1223 already ship a commented `MEMPALACE_REMOTE_URL` / `MEMPALACE_REMOTE_TOKEN`
pair under the heading "MemPalace memory (local by default)", and `docker-compose.yml:79-83` already
ships the `devbox-palace` volume commented out. So the opt-in ladder becomes three states derived
from two variables — **both existing behaviours are preserved, the third is new**:
| `MEMPALACE_REMOTE_URL` | `MEMPALACE_EDGE` | Behaviour | Status |
| --- | --- | --- | --- |
| unset | — | local stdio `mempalace-mcp`, palace at `~/.mempalace` | **default, today, unchanged (R1)** |
| set | unset/`0` | direct remote HTTP; no local palace, no offline | today (`96699f2`), unchanged |
| set | `1` | `mempalace-edge`: local-first writes + outbox → primary + merged reads | **new (Phase 2)** |
> **Volume coupling reverses under edge mode — document it prominently.** Today `.env.example` correctly
> says that with `MEMPALACE_REMOTE_URL` set "*the devbox-palace volume is then irrelevant*", because the
> remote owns all state. Under **edge** mode that flips: the local palace holds the outbox and all
> local-first writes, so an un-persisted palace means **losing un-flushed writes on container
> recreate**. Opting into `MEMPALACE_EDGE=1` therefore *requires* uncommenting
> `devbox-palace:/home/developer/.mempalace`.
Corollary for §4.1: **`mempalace-edge` must not be in the path unless opted in.** Registering it
unconditionally (letting it decide by env at runtime) is tempting — one static config for every
harness — but it violates R1 by inserting a process and a failure mode into every solitary user's
setup. Selection must happen at registration time. See open question §9.6.
--- ---
## 2. Verified starting point (as of 2026-08-08) ## 2. Verified starting point (as of 2026-08-08)
@@ -45,7 +95,10 @@ Two of the three pieces already exist. This is not greenfield.
> **Stale docs warning.** `docker-compose.mempalace.yml`, `.env.example` and CHANGELOG v1.3.0 all say the > **Stale docs warning.** `docker-compose.mempalace.yml`, `.env.example` and CHANGELOG v1.3.0 all say the
> HTTP transport is unauthenticated and should be fronted by a reverse proxy. That was true for > HTTP transport is unauthenticated and should be fronted by a reverse proxy. That was true for
> `mempalace-mcp --transport http` in the v1.3.0 era. **mempalace 3.6.0's `serve` has token + TLS > `mempalace-mcp --transport http` in the v1.3.0 era. **mempalace 3.6.0's `serve` has token + TLS
> built in** (upstream #1877). Fix those comments during Phase 1. > built in** (upstream #1877). Fix those comments during Phase 1. Specifically:
> `pi-devbox/.env.example:21` still advertises `mempalace-mcp --transport http --host 0.0.0.0 --port
> 8765` as the way to serve a shared palace — replace with `mempalace serve --token … --tls-cert …`,
> and change the example URL from `http://mempalace.lan:8765/mcp` to `https://`.
### Two things that sound like the feature and are not ### Two things that sound like the feature and are not
@@ -127,6 +180,16 @@ Why this shape wins:
- The remote transport is **sessionless JSON-RPC** today (per `96699f2`'s own note), so reconnect - The remote transport is **sessionless JSON-RPC** today (per `96699f2`'s own note), so reconnect
after an outage is cheap — there is no session to re-establish. after an outage is cheap — there is no session to re-establish.
**But per R1, that one-line change is conditional, not baked in.** The edge binary is present in the
image (it costs nothing unused) and inserted into the path only when `MEMPALACE_EDGE=1`:
- **pi**: `createClient()` (`extensions/pi/mempalace.ts:629`) already branches on env at startup — add a
third branch. Zero change to the default path.
- **opencode**: the MCP server entry is static JSON, so the entrypoint must template it — write
`command: ["mempalace-edge"]` only when opted in, otherwise leave today's
`["mempalace-mcp"]` untouched. If mempalace is absent entirely (as in all three sampled
opencode-devbox deployments), write nothing and warn (R5).
### 4.2 Routing policy ### 4.2 Routing policy
`service.py:29,54,72` already ships the exact three-way split we need — `READ_TOOLS`, `service.py:29,54,72` already ships the exact three-way split we need — `READ_TOOLS`,
@@ -309,9 +372,9 @@ across models silently degrades recall.
| Phase | Effort | Deliverable | | Phase | Effort | Deliverable |
| --- | --- | --- | | --- | --- | --- |
| **0 — hygiene** | hours | §7 runbook: move KG/hallways/entities, add provenance metadata, ban `sync` on shared palaces, per-device tokens, fix stale "unauthenticated" docs | | **0 — hygiene** | hours | §7 runbook: move KG/hallways/entities, add provenance metadata, ban `sync` on shared palaces, per-device tokens, fix stale "unauthenticated" docs (incl. `pi-devbox/.env.example:21`) |
| **1 — primary up** | hours, **no code** | `mempalace serve --token --tls-cert` on a private-net host (reuse `docker-compose.mempalace.yml`, mind port 8765 vs pi-studio tor-ms22 already moved to 8766). Repoint pi clients via `MEMPALACE_REMOTE_URL`/`MEMPALACE_REMOTE_TOKEN`. **Shared memory today, no offline.** | | **1 — primary up** | hours, **no code** | `mempalace serve --token --tls-cert` on a private-net host (reuse `docker-compose.mempalace.yml` — keep it a **separate standalone project**, R4 — and mind port 8765 vs pi-studio; tor-ms22 already moved to 8766). Repoint pi clients via `MEMPALACE_REMOTE_URL`/`MEMPALACE_REMOTE_TOKEN`. **Shared memory today, no offline.** |
| **2 — `mempalace-edge`** | ~1 week | The actual ask: local-first writes + outbox flush + merged reads + per-wing policy. Fixes opencode as a side effect. | | **2 — `mempalace-edge`** | ~1 week | The actual ask: local-first writes + outbox flush + merged reads + per-wing policy. Fixes opencode as a side effect. **Ships with the §1.2 opt-in wiring (compose + `.env.example` + entrypoint templating) and must pass the R1 acceptance test.** |
| **3 — pull replication** | ~1 week | **DEFERRED (2026-08-08).** Server-side op-log with monotonic seq → each edge a full offline replica. Only needed if a laptop must hold *everything* offline. Accepted trade-off: offline recall = own writes + last-reachable state. | | **3 — pull replication** | ~1 week | **DEFERRED (2026-08-08).** Server-side op-log with monotonic seq → each edge a full offline replica. Only needed if a laptop must hold *everything* offline. Accepted trade-off: offline recall = own writes + last-reachable state. |
| **4 — authz** | days | Per-wing ACL, per-device scopes, audit log, token rotation | | **4 — authz** | days | Per-wing ACL, per-device scopes, audit log, token rotation |
@@ -343,6 +406,13 @@ for online clients.
5. **Does `_HTTP_REQUEST_LOCK` stay held for the duration of an MCP-triggered `mine`?** Strongly 5. **Does `_HTTP_REQUEST_LOCK` stay held for the duration of an MCP-triggered `mine`?** Strongly
suggested by the code shape; if yes, a remote mine makes the primary unusable for its duration suggested by the code shape; if yes, a remote mine makes the primary unusable for its duration
(another argument for §5). (another argument for §5).
6. **How does opencode-devbox learn the opt-in?** pi's transport choice is code (`createClient()`), so
it reads `.env` for free. opencode's MCP registration is **static JSON** in
`~/.config/opencode/opencode.json`, which per R1/R2 must be templated at container start rather
than baked. Unknown: whether opencode-devbox's entrypoint has a config-templating step to hook, or
whether the file is user-owned and bind-mounted (in which case the opt-in is a documented manual
edit — acceptable, but say so). Also unknown whether the published `opencode-devbox` image ships
mempalace at all; if not, edge mode there is host-side only until it does.
--- ---
@@ -370,6 +440,8 @@ Re-verify without re-reading the package. Paths relative to
| Migrate as export primitive | `migrate.py:extract_drawers_from_sqlite`, `:326` | | Migrate as export primitive | `migrate.py:extract_drawers_from_sqlite`, `:326` |
| pi remote transport + fail-soft | `mempalace-toolkit/extensions/pi/mempalace.ts:11-12`, `87`, `108`, `390`, `629-641`, `665-673` (commit `96699f2`) | | pi remote transport + fail-soft | `mempalace-toolkit/extensions/pi/mempalace.ts:11-12`, `87`, `108`, `390`, `629-641`, `665-673` (commit `96699f2`) |
| Server compose + port history | `pi-devbox/docker-compose.mempalace.yml`; `pi-devbox/CHANGELOG.md` v1.3.0; `docker-compose-repo/tor-ms22/pi-devbox/` (`df2c2ae`) | | Server compose + port history | `pi-devbox/docker-compose.mempalace.yml`; `pi-devbox/CHANGELOG.md` v1.3.0; `docker-compose-repo/tor-ms22/pi-devbox/` (`df2c2ae`) |
| Existing opt-in convention (§1.2) | `pi-devbox/.env.example:12-23` ("MemPalace memory (local by default)", commented `MEMPALACE_REMOTE_URL`/`_TOKEN`); `pi-devbox/docker-compose.yml:79-83` (`devbox-palace` volume commented out) |
| opencode-devbox does not wire mempalace | `docker-compose-repo/{synlig,nyvaken,devbox-affection}/opencode-devbox/docker-compose.yml` — zero mempalace references in any |
## 11. See also ## 11. See also