rfc-001: resolve open questions 1 and 6 — both permissive

Recon before planning the implementation, and both blockers dissolved.

Q1, does opencode support remote MCP: yes. Its published schema defines
McpRemoteConfig (type/url/headers/oauth) as a sibling of McpLocalConfig, with
headers as a free string→string map. The RFC had been treating "every sampled
config in myconfigs is type:local" as evidence about the schema when it was
only evidence about deployments. Consequence: the edge proxy is not the only
option for opencode, so nothing in the phasing hangs on this. What opencode
still lacks is offline/local-first, which is the honest Phase 2 argument.

Q6, how opencode-devbox learns the opt-in: it already does. generate-config.py,
run from entrypoint-user.sh:117, registers mempalace as remote+bearer when
MEMPALACE_REMOTE_URL is set and local stdio otherwise, and its comment says it
deliberately mirrors the mempalace.ts env contract. The image also ships
mempalace by default, so R5's premise was wrong and is corrected. Phase 2 there
is a third branch in an existing script, not a new mechanism.

One new constraint found while confirming this, and it is the sharpest edge in
the whole opt-in story: generate-config.py never overwrites an existing config
and ~/.config/opencode is a named volume, so flipping the .env on a container
that already has a config is a no-op — it only writes an opencode.jsonc.proposed
sidecar. pi re-reads env every start; opencode does not. Documented in §4.1 and
§9.6, and it applies symmetrically to R6 reversibility.
This commit is contained in:
Joakim Persson
2026-08-09 22:42:00 +02:00
parent 052dbb8038
commit fdcd5871de
+49 -19
View File
@@ -6,6 +6,7 @@
| **Created** | 2026-08-08 |
| **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. **Centralization is strictly opt-in — solitary devbox operation remains the default and must not change (§1.1).** |
| **Recon update** | **2026-08-09 — §9 Q1 and Q6 are RESOLVED, both in the permissive direction** (opencode supports remote MCP; opencode-devbox already templates the mempalace entry). Neither is a blocker. See those entries for evidence; §2, §4.1 and R5 were corrected accordingly. |
**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
@@ -45,7 +46,7 @@ contain **zero** mempalace references.
| **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). |
| **R5** | **Degrade, never fail, when mempalace is absent.** The published `opencode-devbox` image *does* ship mempalace by default (`Dockerfile.base:380,402``ARG INSTALL_MEMPALACE=true`, `MEMPALACE_VERSION=3.6.0`, installed via `uv tool install`), but it is a build arg precisely so it can be omitted to save ~300 MB — and a bind-mounted host config or a non-devbox client may have no mempalace either. So any wiring must stay additive and skip 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_*`
@@ -89,7 +90,7 @@ Two of the three pieces already exist. This is not greenfield.
| **Primary server** | ✅ **Exists.** `mempalace serve --host --port --token --tls-cert --tls-key --read-only --allow-insecure`. Bearer token compared with `hmac.compare_digest`, **mandatory** on non-loopback binds (unless `--allow-insecure`), TLS 1.2+ resolved *before* bind, Host-header pinning + `Origin` allowlist (anti-DNS-rebinding), 16 MiB body cap, token-free `/healthz`. | `cli.py:cmd_serve` (~1448); `mcp_server.py:5205-5215`, `5284-5289` |
| **Remote client** | ⚠️ **Exists for pi only, and it is either/or.** `createClient()` picks stdio *or* HTTP once at process start. | `extensions/pi/mempalace.ts:629-641` |
| **Fallback + resync** | ❌ **Absent everywhere.** On remote failure the pi bridge re-handshakes the same URL, then **de-registers all palace tools** and runs blind. | `extensions/pi/mempalace.ts:665-673` |
| **opencode client** | **stdio only.** `{"type":"local","command":["mempalace-mcp"]}` in `~/.config/opencode/opencode.json`. No remote entry exists anywhere in `myconfigs`. | `myconfigs/tor-ms22.home.arpa/.config/opencode/opencode.json` |
| **opencode client** | **Remote is supported and already wired.** opencode's published schema (`https://opencode.ai/config.json`, `$defs.McpRemoteConfig`) makes `{"type":"remote","url","headers","oauth"}` a first-class sibling of `McpLocalConfig`, `headers` being a free string→string map (so bearer is a convention, not a constraint). `opencode-devbox` already emits exactly that entry when `MEMPALACE_REMOTE_URL` is set. The all-`type:local` configs in `myconfigs` are a *deployment* fact, not a capability limit. | `generate-config.py:107-118`; hook at `entrypoint-user.sh:117`; schema `$defs.McpRemoteConfig` |
| **Server compose** | ✅ Exists: `pi-devbox/docker-compose.mempalace.yml` (canonical) + a tor-ms22 derivative (`docker-compose-repo/tor-ms22/pi-devbox/`, `df2c2ae`, port 8766, uid 1000, binds the real `~/.mempalace`). Not enabled. | pi-devbox CHANGELOG v1.3.0 (2026-07-02) |
> **Stale docs warning.** `docker-compose.mempalace.yml`, `.env.example` and CHANGELOG v1.3.0 all say the
@@ -185,10 +186,25 @@ image (it costs nothing unused) and inserted into the path only when `MEMPALACE_
- **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).
- **opencode**: the MCP server entry is static JSON — but **the templating step already exists and
already implements the first two rungs of the §1.2 ladder.**
`rootfs/usr/local/lib/opencode-devbox/generate-config.py` (run unconditionally from
`entrypoint-user.sh:117`) registers `mempalace` as `{"type":"remote", url, headers:{Authorization:
Bearer …}}` when `MEMPALACE_REMOTE_URL` is set, else `{"type":"local","command":["mempalace-mcp"]}`
when the binary is on PATH, else nothing — R5-compliant already. Its own comment states it uses the
"*same env contract as the mempalace.ts pi extension … so one shared MemPalace can serve pi +
opencode + native*", i.e. the two images are deliberately kept in step. **Phase 2's opencode work is
therefore a third branch in an existing script, not a new mechanism.**
> ⚠️ **But the opt-in does not propagate to an existing container.** `generate-config.py` *never*
> overwrites an existing config, and `~/.config/opencode` is the named volume
> `devbox-opencode-config` (`docker-compose.yml:64,153`) — so a config generated during solitary use
> survives recreate, and later setting `MEMPALACE_REMOTE_URL`/`MEMPALACE_EDGE` only produces a
> non-loaded `opencode.jsonc.proposed` sidecar for manual merge (`generate-config.py:203-244`).
> Flipping the `.env` alone is a no-op there. Phase 1/2 docs must say: merge the sidecar, or
> `docker volume rm` the config volume, to adopt the change — and the same applies in reverse for R6
> (reversibility). This asymmetry with pi — whose `createClient()` re-reads env every start — is the
> single biggest behavioural difference between the two harnesses under this RFC.
### 4.2 Routing policy
@@ -492,8 +508,8 @@ across models silently degrades recall.
| Phase | Effort | Deliverable |
| --- | --- | --- |
| **0 — hygiene** | hours | §7 runbook: move KG/hallways/entities, ban `sync` on shared palaces, fix stale "unauthenticated" docs (incl. `pi-devbox/.env.example:21`). **No provenance work here** — it is not backfill-critical (§7.3.3) and belongs to the stamper, not the agent |
| **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. **Ships with the §1.2 opt-in wiring (compose + `.env.example` + entrypoint templating) and must pass the R1 acceptance test.** |
| **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`. **opencode clients can be repointed in the same breath** — remote MCP is supported and `generate-config.py` already emits it (§9.1, §9.6), subject to the sidecar caveat in §4.1. **Shared memory today, no offline.** |
| **2 — `mempalace-edge`** | ~1 week | The actual ask: local-first writes + outbox flush + merged reads + per-wing policy. **Not** "fixes opencode" — opencode's *transport* is already fine after Phase 1; what edge adds there is offline/local-first, since `generate-config.py`'s switch is remote **or** local with no failover. **Ships with the §1.2 opt-in wiring (compose + `.env.example` + a third branch in the existing `generate-config.py`) 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. |
| **4 — authz** | days | Per-wing ACL, per-device scopes, audit log, token rotation |
@@ -504,10 +520,18 @@ for online clients.
## 9. Open questions
1. **Does opencode's MCP config support a remote/HTTP transport at all?** Every sampled entry in
`myconfigs` is `"type":"local"`; no documentation found either way. **If it does not, the edge
proxy is not merely nicer — it is the only option for opencode clients.** Verify against opencode's
MCP client source before Phase 2.
1. ~~**Does opencode's MCP config support a remote/HTTP transport at all?**~~ **RESOLVED 2026-08-09:
YES.** opencode's published JSON Schema (`https://opencode.ai/config.json`) defines
`$defs.McpRemoteConfig` = `{type:"remote" (enum), url (required), headers?: {string:string},
oauth?, enabled?, timeout?}` as a sibling of `McpLocalConfig` inside
`Config.properties.mcp.additionalProperties.anyOf`. So `headers` carries any bearer/API-key scheme,
and set `oauth: false` to stop opencode probing the URL for OAuth. The earlier "every sampled entry
is `type:local`" observation was **deployment evidence being read as schema evidence** — the schema
settles it in one fetch. **Consequence: the edge proxy is *not* the only option for opencode.** A
plain Phase 1 `mempalace serve --token` can be consumed directly, so nothing in the phasing depends
on this question any more. What opencode still lacks is *local-first writes and offline fallback*
`generate-config.py`'s switch is remote **or** local per container, with no failover — and that is
the real Phase 2 justification for it.
2. **Upstream or local?** `mempalace-edge` needs no core changes, so it belongs in this repo
(`bin/` + `extensions/`). But `origin_device` stamping from the authenticated token, per-wing ACL, the `kg_supersede`
classification fix, and a `sync --refuse-shared` guard all want to go **upstream**
@@ -525,13 +549,19 @@ for online clients.
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
(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.
6. ~~**How does opencode-devbox learn the opt-in?**~~ **RESOLVED 2026-08-09: it already does.** The
image ships `rootfs/usr/local/lib/opencode-devbox/generate-config.py`, invoked from
`entrypoint-user.sh:117` on every start, which auto-registers the `mempalace` MCP server —
remote+bearer when `MEMPALACE_REMOTE_URL`/`MEMPALACE_REMOTE_TOKEN` are set, local stdio when
`mempalace-mcp` is on PATH, nothing otherwise — documented in that repo's `.env.example:38-49` and
`.env.shared.example:30-35`. It also ships mempalace by default (see R5). So the config *is*
templated at container start, not baked, and Phase 2 only adds a third branch. **Two residual
constraints replace the original unknown, and both are documented in §4.1:** (a) the script never
overwrites an existing config and `~/.config/opencode` is a *named volume*, so an env flip yields
only an `opencode.jsonc.proposed` sidecar — adopting or reverting the opt-in needs a manual merge
or a volume removal; (b) it no-ops entirely unless `OPENCODE_PROVIDER` is set. Prefer extending
this script over inventing a parallel mechanism — treat it as the reference implementation, and
check it before designing any pi-devbox-side mechanism with an opencode counterpart.
---