changelog: name what the floating toolkit ref will pull into the next tag
MEMPALACE_TOOLKIT_REF=main floats and docker-publish.yml resolves it to a SHA at build time, so toolkit main moving 5b8d78f -> f0bffd1 (10 commits) ships in the next tagged image whether or not this repo has a commit. v1.8.9 adopted the rule after that shape bit twice (553d865, 5b8d78f): name the behaviour change BEFORE tagging. This is that rule obeyed rather than re-learned — the work was pushed to toolkit main earlier today and this entry was missing, which is exactly the gap that caused a cross-host misattribution in v1.8.7. Contents: the feeder-side secret scrubber (three tiers, T3 report-only after a measured 403 -> 29 false-positive calibration on 52 MB of real transcripts, fail closed); the symlink near-miss that fail-closed would have turned into a fleet-wide silent memory outage at bake time, caught before tagging; the mailbox work (hlc owed-set join, queued-until-next-turn note, explicit notify protocol modes, tmux path documented unverified); and the documentation set (RFC 003, fleet-memory.md, secret-hygiene.md). Also states what remains unscrubbed: the opencode bridge write path and the unbuilt server-side layer. No tag pushed — per the release protocol, no tag means no build.
This commit is contained in:
+112
@@ -13,6 +13,110 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
**No tag yet, so nothing is built.** This section exists because
|
||||||
|
`MEMPALACE_TOOLKIT_REF=main` floats: `docker-publish.yml` resolves it to a
|
||||||
|
concrete SHA at build time, so whatever is on toolkit `main` when the next tag is
|
||||||
|
pushed ships in that image whether or not this repo has a commit. That is the
|
||||||
|
rule v1.8.9 adopted after `553d865`/`5b8d78f` shipped undocumented twice — *name
|
||||||
|
the behaviour change before tagging, not after* — and this entry is that rule
|
||||||
|
being obeyed rather than re-learned.
|
||||||
|
|
||||||
|
**`mempalace-toolkit` main moves `5b8d78f` → `f0bffd1`** (10 commits, ~1800
|
||||||
|
insertions / ~500 deletions). No pi-devbox commit implements any of it.
|
||||||
|
|
||||||
|
### Transcripts get scrubbed before they are staged (`3d47937`, `836e35b`, `f0bffd1`)
|
||||||
|
|
||||||
|
`bin/mempalace_redact.py`, called from `mempalace-pi-session` at the moment the
|
||||||
|
staged transcript is written — one hook covering both transports, because local
|
||||||
|
mode mines that file and remote mode rsyncs the same bytes.
|
||||||
|
|
||||||
|
- **Why it exists, measured rather than argued.** One leaked bearer token had
|
||||||
|
reached 3 drawers, 13 feeder inbox files across all three devices, and 10 local
|
||||||
|
files spanning 10 days, from an agent printing an env var while debugging. A
|
||||||
|
second sweep then found `GITEA_ACCESS_TOKEN` in 2 more drawers and
|
||||||
|
`GITEA_EGL_ACCESS_TOKEN` in 3. This is routine agent behaviour, so the fix
|
||||||
|
belongs in the pipeline, not in discipline.
|
||||||
|
- **Detection is name-anchored, never entropy-anchored.** A palace's own primary
|
||||||
|
keys — drawer ids, chunk ids, event ids, replica ids, HLCs, commit SHAs — *are*
|
||||||
|
its high-entropy strings, so an entropy detector eats the memory it protects,
|
||||||
|
silently and unrecoverably. Three tiers instead: T1 literal values from this
|
||||||
|
process's env whose name says secret (zero false positives by construction);
|
||||||
|
T2 vendor shapes (`ghp_`, `glpat-`, `xox*-`, `sk-`, `AKIA`, JWT, PEM, URL
|
||||||
|
credentials, `Authorization:`); T3 key-name-says-secret.
|
||||||
|
- **T3 is report-only, because the false-positive rate was measured.** On 52 MB
|
||||||
|
of real fleet transcripts T3 fired 403 times, mostly `${VAR}` interpolation in
|
||||||
|
compose files, TypeScript identifiers, a *type annotation*
|
||||||
|
(`credentials: Credentials`), an IPA attribute holding a date
|
||||||
|
(`krbPasswordExpiration`), AAAK diary shorthand, and terminal output following
|
||||||
|
an ssh `Password:` prompt. With interpolation/code-context/key-suffix guards the
|
||||||
|
enforced count fell **403 → 29** on the same corpus. `MEMPALACE_REDACT_STRICT=1`
|
||||||
|
makes T3 enforce.
|
||||||
|
- **Operational shape.** Fail closed — no redactor, no staging (`exit 3`),
|
||||||
|
overridable with `MEMPALACE_FEED_ALLOW_UNSCRUBBED=1`. Every run prints a count
|
||||||
|
*including* `0 redaction(s)`, because silence is indistinguishable from a
|
||||||
|
scrubber that never ran. Findings carry rule, label, length and `sha256[:8]` —
|
||||||
|
never the value.
|
||||||
|
|
||||||
|
**Near-miss this image would have shipped, caught before tagging (`f0bffd1`).**
|
||||||
|
The image installs `/usr/local/bin/mempalace-pi-session` as a **symlink** into
|
||||||
|
`/opt/mempalace-toolkit/bin`, and `${BASH_SOURCE[0]}` reports the invoked path,
|
||||||
|
not the target — so the sibling-module lookup resolved to `/usr/local/bin`, the
|
||||||
|
redactor was absent, and fail-closed did as instructed: `[FATAL] ... refusing to
|
||||||
|
stage`. Measured side by side, the symlinked invocation FATALed while the direct
|
||||||
|
one scrubbed 40 findings. **At the next bake that would have stopped every
|
||||||
|
feeder tick on every device — a silent fleet-wide memory outage, worse than the
|
||||||
|
leak the scrubber prevents.** Fixed by chasing the symlink chain in portable
|
||||||
|
shell (`readlink -f` avoided: GNU/newer-BSD only, and this script also runs
|
||||||
|
directly on macOS hosts) with colon-separated fallback candidates. Verified via
|
||||||
|
the symlink, the direct path, and a second-hop symlink. General lesson: fail-closed
|
||||||
|
converts "module not found" into an outage, which makes the module lookup
|
||||||
|
load-bearing infrastructure that must be tested through the invocation path the
|
||||||
|
fleet actually uses — not the convenient one from a checkout.
|
||||||
|
|
||||||
|
### The mailbox becomes explainable and mesh-safe (`bfe9c5c`, `a92c75d`, `e917662`, `ecc2a9c`)
|
||||||
|
|
||||||
|
- **Owed-set derivation joins on `hlc`, not `seq`** (`bfe9c5c`). `seq` is a
|
||||||
|
replica-local arrival counter — the same event is `#7` in one database and `#12`
|
||||||
|
in another — so a second replica would let already-answered asks resurrect.
|
||||||
|
`hlc` is immutable and replicated, fixed-width, so string comparison *is* causal
|
||||||
|
comparison. A safe no-op on today's single replica (verified: the positive-control
|
||||||
|
pair orders identically under both keys), correct once a mesh exists.
|
||||||
|
- **Delivered text now says it is queued** (`a92c75d`). Delivery uses `steer`
|
||||||
|
with no `triggerTurn`, and the poll fires on `agent_settled`, so nothing wakes
|
||||||
|
the model — a delivered ask sits until a human starts the next turn. Measured
|
||||||
|
case: a directed report sat unread for 2.5 hours. The note explains the agent is
|
||||||
|
not ignoring the ask, it is not running.
|
||||||
|
- **`MEMPALACE_MAILBOX_NOTIFY` gains explicit `=kitty` / `=osc777` modes**
|
||||||
|
(`e917662`). Terminal autodetection inside a container is not unreliable, it is
|
||||||
|
*blind*: `docker exec` forwards neither `KITTY_WINDOW_ID` nor `TERM_PROGRAM`, and
|
||||||
|
`TMUX` is unset because tmux runs on the host. Verified on a live process:
|
||||||
|
`TERM=xterm-256color` and nothing else.
|
||||||
|
- **The terminal path through tmux is documented as UNVERIFIED** (`ecc2a9c`).
|
||||||
|
Test sequences written to the pty produced no notification on a remote client;
|
||||||
|
tmux likely drops unknown OSC types without `allow-passthrough`, and multi-client
|
||||||
|
routing (one ask pinging every attached client) is an open question.
|
||||||
|
|
||||||
|
### Documentation (`e1cc759`, `982b001`, `d4d8bb6`, `d2764bf`)
|
||||||
|
|
||||||
|
- **RFC 003, the coordination-log spec the code had been citing all along** — it
|
||||||
|
did not exist anywhere. 11 sections, retrospective against mempalace 3.8.0
|
||||||
|
`logstream.py`, incl. owed-set derivation, ten dogfooded landmines and seven open
|
||||||
|
decisions. Non-obvious findings: `event_append` has **no** idempotency guard on
|
||||||
|
the write path (verify-before-retry; the replication path *is* guarded),
|
||||||
|
coordination tools are exempt from both palace locks by design,
|
||||||
|
`GET /logstream/events` **never existed** in 3.8.0 (not proxy-blocked), and
|
||||||
|
`mempalace sync` never touches the logstream — the log is permanent and unbounded.
|
||||||
|
- **`docs/fleet-memory.md`**, operator-facing: five storage types, a decision tree,
|
||||||
|
latency expectations (~2–5 min live session; next session while offline),
|
||||||
|
broadcast exclusion by design, fan-out, and the search-before-answer /
|
||||||
|
diary-at-session-end / verify-don't-retry habits.
|
||||||
|
- **`docs/secret-hygiene.md`**, incl. the tier definitions, the measured FP data,
|
||||||
|
stated false negatives, and the three server-side call sites (specified, not
|
||||||
|
built — tier 2 only there, since the hub cannot see a client's env).
|
||||||
|
- Phase 1 exposure record moved to the private fleet repo with a moved-note stub;
|
||||||
|
retention direction for the unbounded log (logrotate-style: never rotate
|
||||||
|
still-owed events, rotation invalidates held cursors, archive-verify-delete).
|
||||||
|
|
||||||
### The other memory system finally gets explained — `docs/observational-memory.md`
|
### The other memory system finally gets explained — `docs/observational-memory.md`
|
||||||
|
|
||||||
`pi-observational-memory` has been baked for several releases and described in
|
`pi-observational-memory` has been baked for several releases and described in
|
||||||
@@ -86,6 +190,14 @@ browser and read back as an image. Recorded because it generalises:
|
|||||||
`mermaid.parse()` proves syntax and says nothing about layout, so a diagram is
|
`mermaid.parse()` proves syntax and says nothing about layout, so a diagram is
|
||||||
unverified until someone has looked at it.
|
unverified until someone has looked at it.
|
||||||
|
|
||||||
|
### Not covered by any of this
|
||||||
|
|
||||||
|
The opencode bridge is a separate write path the feeder hook never sees, and the
|
||||||
|
server-side layer is unbuilt — so a secret typed straight into `add_drawer`, or
|
||||||
|
staged by a non-pi client, still lands unscrubbed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.8.9 — 2026-08-26
|
## v1.8.9 — 2026-08-26
|
||||||
|
|||||||
Reference in New Issue
Block a user