How to read this page

Every number quoted elsewhere in this wiki should be checkable here. Each block below names its provenance:

  • measured here — a command run while writing this page, output quoted as it came back.
  • build phase — a receipt from the build and deploy work of 2026-08-20, recorded but not re-run here. Named as such so nobody mistakes it for a standing gate.
  • not proven — an unverifiable-on-this-host claim, with the reason it cannot be closed. These are listed, never smoothed over.

Dates and counts are of 2026-08-20. A later change invalidates this page rather than updating it silently; the reproduce block at the bottom regenerates every number.

Gate results

Parser contract, build phase:

  • npx vitest run in the app directory: 1 test file, 33 of 33 tests passed.
  • the test subject is a frozen fixture, never the live document: src/lib/mdframes/fixtures/qwizz-tour.md.
  • contract asserted by those tests, on the fixture:
    • 76 sections: 11 at level 2, 65 at level 3, no h1.
    • 324 frames: 321 image, 2 table, 1 code.
    • of the images, 316 resolve under /shots/ and 5 under /boards/.
    • 66 frames carry the badge finding; the sections carry 139 findings between them.
    • all 321 image frames carry a non-empty sub.
    • with includeText: true: 77 sections and 381 frames, the extra 57 being text frames.
  • npm run build: exit 0, zero accessibility warnings.

Fixture identity, measured here:

$ cd /home/loca/dev/tandem-audit
$ md5sum src/lib/mdframes/fixtures/qwizz-tour.md
a15fb4f0ffb6d07e8256402563193d15  src/lib/mdframes/fixtures/qwizz-tour.md
$ wc -l < src/lib/mdframes/fixtures/qwizz-tour.md
1619

That md5 is what makes the contract numbers durable: they describe one exact 1619-line byte sequence, so they never need re-baselining. md-parser explains the rules those numbers encode.

Live deck census

Measured here, from GET /api/decks/qwizz-2026-08-20:

deck id      : qwizz-2026-08-20
sections     : 76 by level: {2: 11, 3: 65}
frames       : 323 {'code': 1, 'table': 2, 'image': 320}
images       : 320 shots: 315 boards: 5 with sub: 320
finding badge: 65  section findings: 139
sample src   : /api/decks/qwizz-2026-08-20/assets/boards/board-A-public.png

Assets on disk, measured here: 320 regular files under the deck’s asset directory, in exactly two subdirectories, shots/ and boards/. The import that created them reported 320 copied and 0 missing (build phase), which agrees with the file count.

The live deck is one image, one shot and one finding badge below the fixture contract for a single reason: the frozen fixture predates the withdrawal of v01-d-p02-player-intro.png from the source gallery. Everything else matches, which is the whole point of freezing a fixture rather than tracking a living document. index carries the line-level citation for the withdrawal.

Bus proof

One session carries the bus evidence: 127d38c42c95512f, 18 events, events.jsonl 1981 bytes, doneSections reported as ["boards"] by GET /api/sessions (measured here). Every event kind the bus defines except status was exercised.

KindSeqProduced byReceipt
say1POST to the session events routetext tandem link up; also observed replayed on the SSE stream by curl -N. Not visually confirmed in the dialog rail.
nav2-4, 6-9, 11-13, 17the app’s own scroll tracking, not by hand11 events, section ids only
verdict5browser, real DOM click on the frame card icon button labelled Keepverdict: keep on frame boards/1
note10browser, the Note icon, text typed into the modal textarea, submitted with Ctrl+Entertext ui smoke note on boards/1
task14browser, the Task icon, title typed into the task title input, submitted with Entertext ui smoke task on boards/1
speech15POST to the session events routetext spoken smoke transcript. The event kind is proven, the microphone path is not.
route_done16POST to the session events routesection boards, which is why doneSections is ["boards"]
scribble18browser end to end: the Scribble icon, a stroke drawn with genuine pointer input, then Save scribblefile boards-1--18.png

The scribble is the one event with an artifact, so it gets checked rather than trusted. Measured here: boards-1--18.png is 82403 bytes, a 1500x2128 PNG with colour type 6, so it has a real alpha channel. Image inspection finds one thin saturated crimson polyline forming a chevron — two straight segments of uniform width meeting at an apex, no other marks, text or noise anywhere in the frame. The stroke is there; the canvas is not blank.

The same event is the privacy receipt. events.jsonl for that session contains zero occurrences of dataUrl or data:image (measured here by grep), and the whole file is 1981 bytes, which cannot hold an 82 KB data URL under any encoding. Only the filename was logged.

Seq integrity, build phase: 60 concurrent appendEvent calls against one session produced seqs exactly 1 through 60, with no gap and no duplicate. The mechanism is a per-session promise chain that serialises the seq assignment and the append together (src/lib/server/store.ts:302-320).

Safety probes

Every probe below was measured here against https://tndm.loca.zone, with the response quoted as received.

  • import outside the dev root:
    • POST /api/decks/import with {"md_path":"/etc/passwd","id":"probe"}
    • HTTP 400, body {"ok":false,"error":"md_path must be inside /home/loca/dev/"}
    • the same for /home/loca/.bashrc, one directory above the allowed root
  • the containment gate proven by the errors that are not it:
    • {"md_path":"/home/loca/dev"} gives HTTP 400 {"ok":false,"error":"md_path is not a regular file"}
    • {"md_path":"/home/loca/dev/nope.md"} gives HTTP 400 {"ok":false,"error":"md_path not found"}
    • both are downstream failures, so containment had already passed. The gate accepts inside the root and refuses outside it, rather than refusing everything.
  • non-http(s) md_url:
    • POST /api/decks/import with {"md_url":"file:///etc/passwd","id":"probe"}
    • HTTP 400, body {"ok":false,"error":"md_url must be http(s)"}
  • asset traversal, percent-encoded so the segments survive the client and reach the handler:
    • GET /api/decks/qwizz-2026-08-20/assets/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
    • HTTP 400, body {"ok":false,"error":"resolved path escapes the data directory"}
    • the ..%2f..%2f..%2f..%2fetc%2fpasswd spelling returns the identical 400
  • asset traversal, literal ../ segments:
    • GET /api/decks/qwizz-2026-08-20/assets/../../../../etc/passwd, with and without curl’s --path-as-is
    • HTTP 404, and the body is the application’s own not-found page, not a file. Worth stating precisely: this one never reaches the asset handler. The service journal records it as [404] GET /etc/passwd, so the dot segments were collapsed before routing and the traversal guard was never consulted. It is the router refusing the path. The guard is the probe above.
  • malformed collab link:
    • POST /api/sessions/127d38c42c95512f/collab with {"link":"https://example.com/not-a-collab-link"}
    • HTTP 400, body {"ok":false,"error":"link is not a recognizable collab link"}, and the rejected input is not echoed anywhere in the response
  • collab link not set on a session:
    • GET /api/sessions/127d38c42c95512f/collab
    • HTTP 200, body {"link":null}
  • prototype key offered as an event kind:
    • POST /api/sessions/127d38c42c95512f/events with {"actor":"agent","kind":"__proto__","text":"x"}
    • HTTP 400, body {"ok":false,"error":"kind must be one of note|task|verdict|speech|say|scribble|nav|route_done|status"}
  • prototype key offered as a verdict:
    • POST /api/sessions/127d38c42c95512f/events with {"actor":"user","kind":"verdict","verdict":"constructor"}
    • HTTP 400, body {"ok":false,"error":"verdict must be 'keep', 'kill' or 'recapture'"}
    • both of the above pass because membership is tested with Object.hasOwn, not in (src/lib/server/store.ts:269, :291)
  • unknown session:
    • GET /api/sessions/deadbeefdeadbeef/events
    • HTTP 404, body {"ok":false,"error":"no such session: deadbeefdeadbeef"}
  • unparseable after:
    • GET /api/sessions/127d38c42c95512f/events?after=abc
    • HTTP 400, body {"ok":false,"error":"after must be a non-negative integer"}
  • attempt to mutate the log in place:
    • DELETE /api/sessions/127d38c42c95512f/events
    • HTTP 405, body DELETE method not allowed. There is no edit or delete path for an event; the route file exports only GET and POST.
  • a legitimate asset, as a control:
    • GET /api/decks/qwizz-2026-08-20/assets/boards/board-A-public.png
    • HTTP 200, image/png, 329992 bytes

None of this is authentication. Every probe above was made without a credential of any kind, which is exactly the open question security describes.

Deploy probes

All measured here.

ProbeCommandResult
servicesystemctl is-active tndmactive
bindingss -ltnLISTEN 0 511 127.0.0.1:51818 0.0.0.0:* — loopback only, no wildcard listener
app over TLScurl to https://tndm.loca.zone/HTTP 200, curl ssl_verify_result=0
wiki over TLScurl to https://wiki.tndm.loca.zone/HTTP 200, curl ssl_verify_result=0
certificatesudo certbot certificatesname tndm.loca.zone, ECDSA, domains tndm.loca.zone wiki.tndm.loca.zone, expiry 2026-11-18 07:46:12+00:00, reported VALID with 89 days left

The two hosts share one certificate, so one renewal covers both. operations owns the unit, the vhosts and the renewal detail.

Not proven

These are open. Nothing below should be quoted as working.

  • browser text to speech. The host has no audio device at all: ls /dev/snd returns ls: cannot access '/dev/snd': No such file or directory (measured here), and omp say fails with no Linux playback backend available (build phase). Headless Chromium additionally ships zero speechSynthesis voices, so even a silent success cannot be distinguished from a no-op. What is proven is only the degradation path: ttsSupported() gates every call (src/lib/voice.ts:59-64) and the module is documented as degrading silently so callers render a disabled icon rather than catch errors (src/lib/voice.ts:8-10). Confirming speech needs a real desktop Chrome with audio.
  • push to talk. Headless Chromium exposes no microphone, so SpeechRecognition cannot be driven. The speech event at seq 15 was posted over HTTP: it proves the event kind and that the bus stores it, and says nothing about the recogniser.
  • the dialog rail rendering an agent say. Seq 1 was confirmed in events.jsonl and on the SSE stream, not visually in the rail. The rail code path exists and is cited in index; a screenshot of it does not.
  • a real collab room connection. What is proven: the three collab routes are live and answer JSON (measured here, including {"link":null} when unset), a malformed link is rejected with 400 without echoing the input, the link is stored on the session record and never written to events.jsonl (src/routes/api/sessions/[id]/collab/+server.ts:12-14 and src/lib/server/store.ts:360-379), and the relay origin does not block framing — curl -sI https://my.omp.sh/ returns HTTP/2 200 with no x-frame-options and no content-security-policy header of any kind (measured here). What is not proven: an actual room. That needs a live link from a running OMP collab session, which nothing on this host currently holds. The iframe mount at 383x811 with its open-in-new-tab, copy and unlink actions was observed during the build phase; what the frame displayed was not part of that receipt.
  • the count of accepted and rejected collab link forms. The build phase recorded 15 documented forms accepted and 13 malformed ones rejected with 400, exercised against the exported _parseCollabLink. That was a one-off run: npx vitest run covers exactly one test file, the parser’s, so no checked-in test defends those counts. Treat them as a measurement, not a gate. The authoritative link-format reference is omp read omp://collab.md.

Reproduce

Everything on this page, in order. The app directory is the checkout of the SvelteKit app; S is a session id.

cd /home/loca/dev/tandem-audit
 
# gates
npx vitest run                                    # 1 file, 33/33
npm run build                                     # exit 0, no a11y warnings
md5sum src/lib/mdframes/fixtures/qwizz-tour.md    # a15fb4f0ffb6d07e8256402563193d15
wc -l < src/lib/mdframes/fixtures/qwizz-tour.md   # 1619
 
# live deck census
python3 - <<'PY'
import json, urllib.request, collections
d = json.load(urllib.request.urlopen("https://tndm.loca.zone/api/decks/qwizz-2026-08-20"))
secs = d["sections"]; frames = [f for s in secs for f in s["frames"]]
imgs = [f for f in frames if f["type"] == "image"]
print("sections", len(secs), dict(sorted(collections.Counter(s["level"] for s in secs).items())))
print("frames", len(frames), dict(collections.Counter(f["type"] for f in frames)))
print("shots", sum("/shots/" in (f.get("src") or "") for f in imgs),
      "boards", sum("/boards/" in (f.get("src") or "") for f in imgs),
      "with sub", sum(bool((f.get("sub") or "").strip()) for f in imgs))
print("badged", sum("finding" in (f.get("badges") or []) for f in frames),
      "findings", sum(len(s.get("findings") or []) for s in secs))
PY
find /home/loca/tndm/decks/qwizz-2026-08-20/assets -type f | wc -l   # 320
 
# bus
S=127d38c42c95512f
curl -s "https://tndm.loca.zone/api/sessions/$S/events"              # 18 events, seq 1..18
wc -c  /home/loca/tndm/sessions/$S/events.jsonl                      # 1981
grep -c 'dataUrl\|data:image' /home/loca/tndm/sessions/$S/events.jsonl   # 0
stat -c '%s' /home/loca/tndm/sessions/$S/scribbles/boards-1--18.png  # 82403
 
# safety probes
p() { curl -s -w ' HTTP %{http_code}\n' "$@"; }
I=https://tndm.loca.zone/api/decks/import
H='content-type: application/json'
A=https://tndm.loca.zone/api/decks/qwizz-2026-08-20/assets
p -X POST $I -H "$H" -d '{"md_path":"/etc/passwd","id":"probe"}'
p -X POST $I -H "$H" -d '{"md_path":"/home/loca/.bashrc","id":"probe"}'
p -X POST $I -H "$H" -d '{"md_path":"/home/loca/dev","id":"probe"}'
p -X POST $I -H "$H" -d '{"md_path":"/home/loca/dev/nope.md","id":"probe"}'
p -X POST $I -H "$H" -d '{"md_url":"file:///etc/passwd","id":"probe"}'
p "$A/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
p --path-as-is "$A/../../../../etc/passwd" -o /dev/null
sudo journalctl -u tndm -n 3 --no-pager -o cat        # [404] GET /etc/passwd
curl -s -o /dev/null -w 'HTTP %{http_code} %{content_type} %{size_download}\n' \
  "$A/boards/board-A-public.png"
p -X POST "https://tndm.loca.zone/api/sessions/$S/collab" -H "$H" \
  -d '{"link":"https://example.com/not-a-collab-link"}'
p "https://tndm.loca.zone/api/sessions/$S/collab"
p -X POST "https://tndm.loca.zone/api/sessions/$S/events" -H "$H" \
  -d '{"actor":"agent","kind":"__proto__","text":"x"}'
p -X POST "https://tndm.loca.zone/api/sessions/$S/events" -H "$H" \
  -d '{"actor":"user","kind":"verdict","verdict":"constructor"}'
p "https://tndm.loca.zone/api/sessions/deadbeefdeadbeef/events"
p "https://tndm.loca.zone/api/sessions/$S/events?after=abc"
p -X DELETE "https://tndm.loca.zone/api/sessions/$S/events"
 
# deploy
systemctl is-active tndm
ss -ltn | grep 51818
curl -s -o /dev/null -w 'HTTP %{http_code} tls=%{ssl_verify_result}\n' https://tndm.loca.zone/
curl -s -o /dev/null -w 'HTTP %{http_code} tls=%{ssl_verify_result}\n' https://wiki.tndm.loca.zone/
sudo certbot certificates 2>/dev/null | grep -A5 'Certificate Name: tndm.loca.zone'
 
# the unprovable ones, for completeness: both of these are expected to fail here
ls /dev/snd
curl -sI https://my.omp.sh/ | grep -iE 'x-frame-options|content-security-policy'
  • quickstart — the shortest path from nothing to a session, if you arrived here first.
  • api — the endpoint table and the event schema these probes exercise.
  • agent-cookbook — the same API as runnable agent recipes, each verified against the instance.
  • security — what the absence of authentication means and the options still open.
  • operations — the unit, the vhosts, the data directory and the durability gaps.
  • first-deck — the document behind the census numbers.
  • md-parser — what the frozen fixture numbers actually assert.