Files
mempalace-toolkit/scripts/test-device-stamp.sh
T
pi 553d86570c provenance: stamp device+harness at the edge, not in the agent's head
RFC 001 §7.3.2 ranks "agent stamps provenance via a skill instruction" as the
❌ worst possible place — per-call boilerplate, forgettable, improvisable. It
was right, and we had shipped exactly that: the mempalace skill told the agent
to pass added_by="<harness>@<device>" by hand. Measured on the shared palace,
199 rows had reached it unresolvable, 10 of them filed by the very agent that
wrote the instruction, in a drawer about host provenance. The trigger was a
cross-host misattribution: a session on tor-ms22 read its own diary, could not
tell that the entries were written on EMB-7KJ4VR4G, and reported another
machine's verification as this one's.

Move the same convention into the ⚠️ edge row, where it is uniform and
unforgettable (§7.3.5):

* extensions/pi/mempalace.ts defaults the writer field on every tool that has
  one — added_by (add_drawer, checkpoint), agent (mine), from_agent
  (event_append), created_by (artifact_put) — from $MEMPALACE_PI_DEVICE. An
  explicit value always wins, so filing for another device stays possible. The
  allowlist is per tool, never blanket: 3.8.0's dispatcher hard-rejects
  undeclared args with -32602, so injecting added_by into diary_write or kg_add
  (which have no such property) would break the call outright.
* mine gets miner@<device> when the caller invokes it, but <harness>@<device>
  for the bridge's own transcript feed — bulk extraction is not agent-authored
  memory, and that keeps the pi/opencode/miner taxonomy honest.
* diary_write has no metadata slot at all, and the device must never go in
  agent_name (wing = f"wing_{agent_name}" would splinter the diary per host).
  So the entry TEXT carries an AAAK field, HOST:<device>|SESSION:… — which is
  also the only channel a READER sees: search projects a fixed key set and
  diary_read returns content, so no metadata fix, not even a
  server-authoritative one, would have prevented the misattribution.
* The wake-up block now states the device and warns that diary_read interleaves
  every machine's diary.
* R1: doubly gated on MEMPALACE_PI_DEVICE and MEMPALACE_REMOTE_URL, so a
  solitary devbox stamps nothing and behaves exactly as before — which is also
  the correct semantics per §7.3.3.

Version the reconciler that was living only on synlig (bin/ + contrib/systemd/),
add --dry-run, and teach it two new rules: diary_host_marker reads the HOST:
field, and sibling_chunk propagates a resolved origin across a drawer's chunks
(a text marker lands in chunk 0 only, so a 5-chunk diary entry would otherwise
stamp 1 and leave 4 blank).

--dry-run against the real palace before deploying earned its keep twice, and
scripts/test-device-stamp.sh pins both findings with the strings it found:
HOST: was ALREADY in use with a composite grammar
(HOST:emb-7kj4vr4g.f1d3c3f89e3e.v1.8.3.pi0.84.2) and for bare container ids, so
an unvalidated rule invented devices like "f1d3c3f89e3e.pi0.84.2"; and HOST:
also carries a different SENSE elsewhere (HOST:exec.via.ssh-controlmaster->…,
meaning where I was executing). Validating against the known-device set both
refuses those and recovers the composite entries correctly. A marker convention
inherits every prior meaning of its own name.

Deployed and verified on synlig: device 14,217 → 14,317, integrity ok,
idempotent on immediate re-run, no invented device values.

RFC updates: §7.3.1 corrected (the arg whitelist is a hard -32602 in 3.8.0, not
a silent drop; get_drawer DOES return metadata, search structurally cannot;
triples and logstream live in separate databases the stamper cannot reach),
§7.3.5 added (what is deployed, including the divergence from §7.3.4's opaque
origin_device — tor-ms22 vs tor-ms22-native is that cost already visible), and
Phase 4 now carries per-device tokens motivated FIRST by revocation, with the
finding that tokens are the cheap half: core holds one scalar auth_token and has
zero device concept, so authoritative stamping needs a component we own.
2026-08-25 22:26:46 +02:00

127 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-device-stamp.sh — rule tests for bin/mempalace-device-stamp.
#
# The stamper writes directly to the SHARED fleet palace, so its rules must
# never be tried out there: a wrong rule invents device names that then have to
# be un-invented across 30k rows. This builds a THROWAWAY palace under a fake
# $HOME and asserts each rule against it.
#
# Every HOST: fixture below is verbatim from the real palace, collected by
# running `mempalace-device-stamp --dry-run` against it on 2026-08-25 — which is
# how we learned that `HOST:` was already in use with two OTHER grammars:
# * a composite fingerprint, HOST:<device>.<container>.<image>.<pi-version>
# * a different SENSE entirely: HOST:exec.via.ssh-controlmaster->alpserv-2,
# meaning "the box I was executing on", not "the box that wrote this".
# Hence the KNOWN_DEV validation the tests below pin down.
#
# Usage: scripts/test-device-stamp.sh (exit 0 = all rules behave)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STAMPER="$REPO_ROOT/bin/mempalace-device-stamp"
FAKE="$(mktemp -d)"
trap 'rm -rf "$FAKE"' EXIT
python3 -m py_compile "$STAMPER"
# Devices exist iff they have a feed inbox — that is what KNOWN_DEV reads.
mkdir -p "$FAKE/.mempalace/palace" \
"$FAKE/mempalace-feed/tor-ms22" \
"$FAKE/mempalace-feed/emb-7kj4vr4g" \
"$FAKE/mempalace-feed/mbp-m1-2020"
FAKE="$FAKE" python3 - <<'PYEOF'
import os, sqlite3
db = os.path.join(os.environ['FAKE'], '.mempalace/palace/chroma.sqlite3')
c = sqlite3.connect(db)
c.execute("create table embedding_metadata (id text, key text, string_value text, primary key(id,key))")
docs = {
# --- the marker the pi bridge writes (extensions/pi/mempalace.ts) --------
'new_convention': 'HOST:tor-ms22|SESSION:2026-08-25|did.things',
# --- the older composite grammar already in the palace ------------------
'composite_v183': 'HOST:emb-7kj4vr4g.f1d3c3f89e3e.v1.8.3.pi0.84.2|SESSION:x',
'composite_om': 'HOST:emb-7kj4vr4g.f1d3c3f89e3e.pi0.84.2.om3.0.4.bedrock|SESSION:x',
'composite_devbox': 'HOST:emb-7kj4vr4g.devbox.pi0.84.2.om.699ccc7|SESSION:x',
# --- must NOT invent a device ------------------------------------------
'container_only': 'HOST:f1d3c3f89e3e.pi0.84.2|SESSION:x', # container id, not a device
'container_bare': 'HOST:2efe2b06f480|SESSION:x', # ditto, and it changes per recreate
'garbage_str': 'HOST:str|SESSION:x',
'other_sense': 'x|HOST:exec.via.ssh-controlmaster->alpserv-2(Alpine,user=joakim)|y',
'prose': 'we discussed the HOST:tor-ms22 marker convention', # not at a field boundary
}
rows = []
for k, doc in docs.items():
rows += [(k, 'chroma:document', doc), (k, 'parent_drawer_id', 'd_' + k), (k, 'added_by', 'pi')]
# Chunk propagation: one write call => one origin. Marker lands in chunk 0 only.
rows += [('chunk0', 'chroma:document', 'HOST:tor-ms22|SESSION:y'), ('chunk0', 'parent_drawer_id', 'multi')]
rows += [('chunk1', 'chroma:document', 'continuation, no marker'), ('chunk1', 'parent_drawer_id', 'multi')]
rows += [('chunk2', 'chroma:document', 'more continuation'), ('chunk2', 'parent_drawer_id', 'multi')]
# An existing stamp must survive untouched and seed its blank sibling.
rows += [('keep0', 'device', 'mbp-m1-2020'), ('keep0', 'device_source', 'inbox_path'),
('keep0', 'parent_drawer_id', 'kept'), ('keep0', 'chroma:document', 'x')]
rows += [('keep1', 'parent_drawer_id', 'kept'), ('keep1', 'chroma:document', 'y')]
# The edge convention on a normal drawer.
rows += [('edge0', 'added_by', 'pi@tor-ms22'), ('edge0', 'chroma:document', 'z'),
('edge0', 'parent_drawer_id', 'edged')]
# Defaults that carry no device: must stay blank rather than guess.
rows += [('dflt0', 'added_by', 'checkpoint'), ('dflt0', 'chroma:document', 'q'),
('dflt0', 'parent_drawer_id', 'dflt')]
c.executemany("insert into embedding_metadata values (?,?,?)", rows)
c.commit()
PYEOF
# --dry-run must write nothing.
HOME="$FAKE" python3 "$STAMPER" --dry-run >/dev/null
if [ -n "$(FAKE="$FAKE" python3 -c "
import os,sqlite3
db=os.path.join(os.environ['FAKE'],'.mempalace/palace/chroma.sqlite3')
print(''.join(r[0] for r in sqlite3.connect(db).execute(
\"select id from embedding_metadata where key='device' and id!='keep0'\")))")" ]; then
echo "FAIL: --dry-run wrote stamps" >&2
exit 1
fi
HOME="$FAKE" python3 "$STAMPER" >/dev/null
SECOND="$(HOME="$FAKE" python3 "$STAMPER")" # idempotence: a second pass adds nothing
FAKE="$FAKE" SECOND="$SECOND" python3 - <<'PYEOF'
import os, sqlite3, sys
db = os.path.join(os.environ['FAKE'], '.mempalace/palace/chroma.sqlite3')
c = sqlite3.connect(db)
got = {r[0]: r[1] for r in c.execute("select id,string_value from embedding_metadata where key='device'")}
src = {r[0]: r[1] for r in c.execute("select id,string_value from embedding_metadata where key='device_source'")}
want = {
'new_convention': 'tor-ms22', 'composite_v183': 'emb-7kj4vr4g',
'composite_om': 'emb-7kj4vr4g', 'composite_devbox': 'emb-7kj4vr4g',
'container_only': None, 'container_bare': None, 'garbage_str': None,
'other_sense': None, 'prose': None,
'chunk0': 'tor-ms22', 'chunk1': 'tor-ms22', 'chunk2': 'tor-ms22',
'keep0': 'mbp-m1-2020', 'keep1': 'mbp-m1-2020',
'edge0': 'tor-ms22', 'dflt0': None,
}
want_src = {
'new_convention': 'diary_host_marker', 'composite_v183': 'diary_host_marker',
'chunk1': 'sibling_chunk', 'chunk2': 'sibling_chunk', 'keep1': 'sibling_chunk',
'keep0': 'inbox_path', # pre-existing stamp not rewritten
'edge0': 'agent_at_device',
}
bad = 0
for k, exp in sorted(want.items()):
act = got.get(k)
if act != exp:
print("FAIL %-17s device expected=%s got=%s" % (k, exp, act)); bad += 1
for k, exp in sorted(want_src.items()):
act = src.get(k)
if act != exp:
print("FAIL %-17s source expected=%s got=%s" % (k, exp, act)); bad += 1
if 'device+0' not in os.environ['SECOND']:
print("FAIL not idempotent, second pass said: %s" % os.environ['SECOND'].strip()); bad += 1
print("device-stamp rules: %s (%d checks)" % ("PASS" if not bad else "%d FAILURES" % bad,
len(want) + len(want_src) + 1))
sys.exit(1 if bad else 0)
PYEOF