AI interop runs of libtacky 1.2.0-rc2 and fix playbooks for issue #1 #2

Merged
asxpi merged 539 commits from ai-runs-2026-11-07 into main 2026-11-08 00:34:56 +00:00
Member

AI interop runs of libtacky against slixmpp on the three test servers (Prosody, ejabberd, anotherim-server), 2026-11-04 to 2026-11-08, plus the fix playbooks for every bug they found.

What is in it

  • Runs (runs/): AI-verified runs, all --verified-by ai --confidence low, of libtacky v1.2.0-rc2 (tackyd-json). There are also live re-checks on upstream v1.2.0 (1dae0a6c) and verify runs of local fix builds. Every committed run passes interop check; AI-RESULTS.md, FEATURES.md and matrix.md are regenerated.
  • Harness (tools/ai/): the AI runner brief, aish, peer.py, tacky.py, common.py, and scenarios for each feature tested, including repro-* scripts for single bugs. Harness fixes along the way:
    • each Peer's wire log keeps only its own stream
    • stanza logs are split on \n only
    • OMEMO pre-flight gates compare against each slot's own state
    • the XEP-0463 fake room at a contact's JID is off by default
  • Fix playbooks (fixes/): one directory per bug of #1, indexed in fixes/README.md, with a triage of the minor bugs.
    • 42 fix branches. Each is in a local libtacky clone, signed, with a test that fails without it, and a run pair showing v1.2.0 failing and the fix passing. Each has a step-by-step README, an issue.md and pr.md for upstream, and a fix.patch.
    • 17 verify-only (already fixed in v1.2.0).
    • 6 not bugs.
    • 6 that need a maintainer decision.
    • fixes/embcheck.py checks that a tackyd binary embeds a given branch's code; all 42 fix binaries matched.

Findings

#1: 71 libtacky bugs (13 security/privacy) and the live re-check results.

Before merging

  • Two runs of 2026-11-04 committed XEP-0379 pre-approval tokens in the clear. They are redacted now but remain in history, so the tokens should be revoked.
  • The XEP-0065 Prosody run 20261107-113200 is recorded fail, but the fault is Prosody's mod_proxy65 (libtacky passed). It may need relabelling.
  • Not committed because they fail interop check: 181318, 184109, 191208, 191231, 205307 (all 2026-11-07, superseded).
  • The branch is large (about 6,300 files, mostly run bundles; logs are in LFS). It merges cleanly into main.
AI interop runs of libtacky against slixmpp on the three test servers (Prosody, ejabberd, anotherim-server), 2026-11-04 to 2026-11-08, plus the fix playbooks for every bug they found. ## What is in it - **Runs** (`runs/`): AI-verified runs, all `--verified-by ai --confidence low`, of libtacky v1.2.0-rc2 (tackyd-json). There are also live re-checks on upstream v1.2.0 (`1dae0a6c`) and verify runs of local fix builds. Every committed run passes `interop check`; `AI-RESULTS.md`, `FEATURES.md` and `matrix.md` are regenerated. - **Harness** (`tools/ai/`): the AI runner brief, `aish`, `peer.py`, `tacky.py`, `common.py`, and scenarios for each feature tested, including `repro-*` scripts for single bugs. Harness fixes along the way: - each Peer's wire log keeps only its own stream - stanza logs are split on `\n` only - OMEMO pre-flight gates compare against each slot's own state - the XEP-0463 fake room at a contact's JID is off by default - **Fix playbooks** (`fixes/`): one directory per bug of #1, indexed in [`fixes/README.md`](fixes/README.md), with a [triage of the minor bugs](fixes/MINOR-TRIAGE.md). - **42 fix branches.** Each is in a local libtacky clone, signed, with a test that fails without it, and a run pair showing v1.2.0 failing and the fix passing. Each has a step-by-step README, an `issue.md` and `pr.md` for upstream, and a `fix.patch`. - **17 verify-only** (already fixed in v1.2.0). - **6 not bugs.** - **6 that need a maintainer decision.** - `fixes/embcheck.py` checks that a tackyd binary embeds a given branch's code; all 42 fix binaries matched. ## Findings [#1](https://dev.another.im/another-im/interop/issues/1): 71 libtacky bugs (13 security/privacy) and the live re-check results. ## Before merging - Two runs of 2026-11-04 committed XEP-0379 pre-approval tokens in the clear. They are redacted now but remain in history, so the tokens should be revoked. - The XEP-0065 Prosody run `20261107-113200` is recorded `fail`, but the fault is Prosody's mod_proxy65 (libtacky passed). It may need relabelling. - Not committed because they fail `interop check`: `181318`, `184109`, `191208`, `191231`, `205307` (all 2026-11-07, superseded). - The branch is large (about 6,300 files, mostly run bundles; logs are in LFS). It merges cleanly into `main`.
asxpi added 532 commits 2026-11-07 21:13:31 +00:00
The rehearsal of the AI loop: tools/ai/scenarios/XEP-0184.py drives
libtacky and slixmpp through receipts both ways inside an interop run,
and passes once the accounts are mutual contacts. That needed
tools/ai/common.py: the run's accounts, and befriend(), since libtacky
answers receipts (and markers, chat states, caps) only to those who see
its presence, as its features.md says; Prosody has no subscription
pre-approval, so the peer's request is approved when it arrives.

Assisted-by: Claude:opus-5-5
Each feature gets one subagent with its own account slot: what to read
first (libtacky's own row decides what is a bug), the scenario and
feature steps to write, the interop start/finish per server, when a
result is pass, fail with a bug written up, or n/a, and what never to
touch (git, issues, servers, the harness). queue.txt: the 63 ai_ok
features, P0 first.

Assisted-by: Claude:opus-5-5
Parallel agents blocked each other: tackyd-json kept its config in the
shared ~/.config/libtacky, which libtacky locks, so a second instance
died at boot. Each account now has its config dir beside its data.
interop start no longer prints the passwords of the headless (AI)
accounts, which scripts read from the accounts file anyway, and finish
takes runs/<id> as check and normalize do. Reported by the RFC 7590 agent.

Assisted-by: Claude:opus-5-5
Direct TLS and STARTTLS both end in TLS 1.3 on all three; libtacky
refuses a self-signed certificate (CertificateUntrusted self_signed)
and a server that offers no <starttls/> (no <auth/> sent), both played
by local fakes so no other run's server log saw them. Steps and
reviewed markers in features/RFC-7590.toml.

Assisted-by: Claude:opus-5-5
Roster get and set, subscriptions both ways (subscribe, subscribed,
deny, unsubscribe), per-resource presence and messages all work on
prosody, ejabberd and anotherim-server. What fails, the same way on
each: on a SASL2/Bind 2 session libtacky never sends the stored roster
ver, although its XEP-0237 row says it does on a Bind 2 session, and
roster preapprove refuses although every server advertises
urn:xmpp:features:pre-approval (Prosody after <success>, the ejabberds
in the stream features before authentication). Described in each
run's bug; the agent's ejabberd detail corrected after a look at the
logs. Prosody also sent no unavailable presence after an unsubscribe
(RFC 6121 3.3.3), noted in its run. The first Prosody run is the
scenario's development, finished n/a.

Assisted-by: Claude:opus-5-5
TLS, SASL2 with Bind 2, the bound JID and service-unavailable for an
unhandled IQ work on all three. On ejabberd and anotherim-server a
stanza the server answers with a stream error (here a forged from,
<invalid-from/>) is resent on every new stream: the session resume is
refused, libtacky replays its unacked queue, forged stanza included,
and the server closes again; 24 times in 46 s, retry_in always 1 s
(RFC 6120 4.9, 3.3). The forged stanza was written through libtacky's
XML console, so libtacky did not make it; the loop is its own, and any
stanza drawing a stream error would loop the same way. Prosody ends
the stream at the first trigger already and passes.

An earlier ejabberd run finished as a pass before the loop was found
is relabelled n/a, pointing at the run that supersedes it.

Assisted-by: Claude:opus-5-5
Agents testing one feature on all three servers in turn shared each
server with the others, so a run's server log, cut by time, held other
runs' traffic. Now each server has one agent at a time and its own
slot (prosody 1, ejabberd 2, anotherim-server 3): an author writes a
feature's scenario and steps on prosody, then runners run it unchanged
on the two ejabberds and say how their server differed. AI-RESULTS.md
gains a feature x server table that flags where the servers disagree.

From the agents' reports: Tacky.send_raw (raw XML through the debug
tap), a cache dir per account, slixmpp's subscription answers
switchable and its subscription events recorded, and normalize on a
run not finished yet.

Assisted-by: Claude:opus-5-5
The kept per-account store holds a FAST token, so a sign-in test
signed in with HT-SHA-256 instead of the mechanism under test; the
RFC 7677 agent had to point the store elsewhere by hand. Runs of
features marked fresh now get an empty store in their .state.

Assisted-by: Claude:opus-5-5
A fresh profile signs in with SCRAM-SHA-256-PLUS over tls-exporter on
prosody, ejabberd and anotherim-server. A local fake SCRAM server
confirms the choice for six offers: -PLUS where offered (tls-exporter
on TLS 1.3, tls-server-end-point on TLS 1.2 when the server names
it), y without -PLUS, SHA-1 only when nothing better, no PLAIN.

Not a bug, but libtacky's own docs are behind its code: its RFC 5929
and XEP-0440 rows say SCRAM never binds to tls-server-end-point, while
sasl.tcl does, correctly (RFC 5929, RFC 5802).

Assisted-by: Claude:opus-5-5
Receipts both ways between libtacky and slixmpp as mutual contacts:
libtacky asks, marks delivered on slixmpp's <received/>, and answers
slixmpp's request once to the full JID without a store hint.
ejabberd differs only in archiving: no stanza-id on bare receipts,
mam:tmp beside the stanza-id of messages with a body. The feature file
gets the steps, the expected result and the reviewed marker the
rehearsal left out.

Assisted-by: Claude:opus-5-5
Receipts both ways as on Prosody and ejabberd; the server only stamps
differently (mam:tmp with a numeric stanza-id, none on bare receipts).
The brief now says server.log arrives with finish: both runners
looked for it before.

Assisted-by: Claude:opus-5-5
Runners on the two ejabberds sat idle while prosody authored. A server
with no runner work now authors the next feature itself, and runners
compare with the authoring server, whichever it was. Still one agent
per server.

Assisted-by: Claude:opus-5-5
Its DEBUG stream lines went to the log file and, through propagation,
to stderr too, burying the scenario's PASS lines.

Assisted-by: Claude:opus-5-5
libtacky asks the server (server/im, features, components) and
slixmpp (identities per xml:lang, features, a XEP-0128 form, items with
a node, item-not-found for an unknown node); slixmpp asks libtacky
(client/pc with 90 features, the caps node#ver answering the same
list, its sha-1 matching libtacky's presence, item-not-found for a
bogus node, empty items). Runners on the ejabberds follow.

Assisted-by: Claude:opus-5-5
Plugin events (user_mood_publish and the like) needed a handler in
each scenario; Peer(..., events=[...]) records them for wait().

Assisted-by: Claude:opus-5-5
Carried by XEP-0107 mood: caps say mood+notify, each side's publish
reaches the other from the publisher's bare JID, libtacky ignores a
forged event from a full JID (its row: full-JID events dropped), and
an empty <mood/> clears it on both sides. Runners on prosody and
ejabberd follow.

Assisted-by: Claude:opus-5-5
libtacky finds blocking in disco#info, fetches the list, blocks two
JIDs in one <block>, unblocks one, then all (empty <unblock/>),
answers each server push, and reports Prosody's bounce of a message
to a blocked JID (<blocked xmlns='urn:xmpp:blocking:errors'/>) as
fail_reason blocked. Worth knowing, not wrong: it refetches the whole
list after every push instead of applying it, and it asks a blocked
JID for OMEMO devices before sending. Runners on the ejabberds follow.

Assisted-by: Claude:opus-5-5
The same both ways as on Prosody; the server differs only in what it
says about itself: ejabberd plus pubsub/pep, 63 features to Prosody's
16, and a urn:tacky:schedule:0 form beside serverinfo, both parsed.
The brief now names slixmpp's plugin[...] (the short form is
deprecated).

Assisted-by: Claude:opus-5-5
The same as on anotherim-server: publishes both ways from bare JIDs,
the forged full-JID event ignored, empty <mood/> clearing it. Prosody
delivers each notification twice (to the full and the bare JID, same
id); harmless here, recorded in the run.

Assisted-by: Claude:opus-5-5
libtacky's sha-1 ver is recomputable from its node#ver answer (the
same 90 features as our_caps); it verifies slixmpp's caps once,
reuses them for the same ver without asking, and keeps nothing of a
forged ver whose answer does not hash to it. Two earlier runs are n/a:
the scenario counted ejabberd's own caps query as libtacky's, then
missed that libtacky remembers a verified ver across runs. The brief
now says so: values a scenario checks must be unique per run.

Assisted-by: Claude:opus-5-5
Servers compared: per server the passes, fails and n/a of the newest
agent run per feature and the features failing where, and how the
servers behaved differently feature by feature, kept in
tools/ai/differences.toml from the agents' reports. Tacky now writes
every libtacky event to client-events.log, so a run records what
libtacky reported, not only the stanzas.

Assisted-by: Claude:opus-5-5
anotherim-server (ejabberd) bounces a message to a blocked JID with
<not-acceptable/> and privacy-list text but without <blocked
xmlns='urn:xmpp:blocking:errors'/>, which XEP-0191 3.3 (1.3) makes a
MUST, so libtacky can report the failure but not fail_reason blocked.
A server bug, not libtacky's; Prosody includes the element. XEP-0030
on ejabberd behaves as on the others; the server reports pubsub/pep,
64 features and an empty serverinfo form.

Assisted-by: Claude:opus-5-5
Publishes both ways from bare JIDs, the forged full-JID event dropped
by libtacky (ejabberd routes it on), empty <mood/> clearing it.
ejabberd adds XEP-0033 replyto addresses to the events.

Assisted-by: Claude:opus-5-5
Prosody's and ejabberd's <enabled id=…/> and the previd of a resume
went into the runs' server and peer logs (libtacky's console hides its
own previd only): a stream's resume token, dead once the session ends,
as all of these have, but not something for a public proof. Both are
redacted now, also JSON-escaped, with tests; the 32 finished runs are
redacted again and their run.toml hashes updated, nothing else in them
changed. Found by the XEP-0198 agent.

Tacky.send_raw also follows the HANDBOOK now: debugtap on answers the
tap, debugtap write takes {stanza, tap}.

Assisted-by: Claude:opus-5-5
Stream management: enabled inline in SASL2/Bind 2 with resumption,
acks both ways, an inline resume after a footerless drop keeping the
full JID, a stanza queued while down sent once after <resumed/>.
Caps on anotherim-server behave as on ejabberd, one query per ver,
forged ver refused; its mod_caps probes peers itself, as ejabberd's.

Assisted-by: Claude:opus-5-5
Runs recorded its version as master, which says nothing; it is the
commit flake.lock pins.

Assisted-by: Claude:opus-5-5
ejabberd 26.09 enforces blocking through mod_privacy and bounces a
message to a blocked JID with a plain <not-acceptable/> (type modify,
privacy-list text), without the <blocked
xmlns='urn:xmpp:blocking:errors'/> XEP-0191 3.3 requires, so libtacky
reports fail_reason not-acceptable, as its row says it would. All else
of XEP-0191 works. A possible libtacky improvement, not a bug: take a
not-acceptable bounce from a JID on its own block list as blocked.

Assisted-by: Claude:opus-5-5
One query per ver, the forged ver refused, the real caps back without
asking again, as on the ejabberds. Prosody's mod_pep sends its own
caps queries from the accounts' bare JIDs, which the scenario rightly
does not count. The XEP-0030 and XEP-0115 scenarios, all their runs
done, use slixmpp's plugin[...] instead of the deprecated short form.

Assisted-by: Claude:opus-5-5
Inline enable with resumption, acks both ways, an inline resume after
a footerless drop keeping the full JID, the stanza queued while down
sent once. The server says resume='true' and max='300' (Prosody: 1, 600)
and offers sm:2 beside sm:3; its archive keeps both copies of a message
to the account's own full JID, noted in the run, not an SM matter.

Assisted-by: Claude:opus-5-5
The previous commit's check also took ejabberd's run still in progress,
failed on it, and so skipped the report.

Assisted-by: Claude:opus-5-5
Inline enable with resumption, acks both ways, inline resume with the
same full JID, the queued stanza sent once. ejabberd puts <resumed>
before <additional-data>. A flaw seen through the test itself: an <r/>
written through libtacky's debug tap is kept in its SM queue as if it
were a stanza, so the count is off by one and after the resume
libtacky resent an IQ ejabberd had acked and answered already (the
RFC 6120 runs saw the same with an unknown element). Normal sends do
not go that way; the debug tap should keep nonzas out of the queue.

Assisted-by: Claude:opus-5-5
Carbons enabled inline in Bind 2; with a second slixmpp device on
libtacky's own account, libtacky's sends are carbon-copied there, the
other device's sends reach libtacky as <sent> and are stored as its
own outgoing rows, messages to the other device reach it as
<received>; <private/> gets no carbon; forged wrappers from a contact
make no message, as libtacky's row says. Runners on the ejabberds follow.

Assisted-by: Claude:opus-5-5
libtacky finds mam:2, reads the search form (with, start, end), pages
a query with RSM <after> to <fin complete> with every message once in
order, and after clear_history the next sign-in's catch-up refills the
chat from the archive without duplicates, server ids kept. The server
refuses mam metadata, as it may without #extended. Runners on prosody
and ejabberd follow.

Assisted-by: Claude:opus-5-5
Carbons through Bind 2, <sent/> stored as outgoing and <received/> as
incoming, no carbon of a <private/> message, both forged wrappers
ignored. The server does not answer the Bind 2 <enable/> with
<enabled/> (XEP-0386: no response needed), so the expected result now
says so.

Assisted-by: Claude:opus-5-5
Scenarios found an IQ's answer by the text id='N', which matched an
OMEMO <pk id='73'> in the XEP-0357 run. common.wire(run) reads a log's
elements and common.iq_answer(run, id) matches the <iq>'s own id and
result/error type only; the brief names them.

Assisted-by: Claude:opus-5-5
push enable and disable, with a publish-options form and a secret,
answered by mod_push; an empty node refused before the wire; a target
declared with set_target withdrawn by account disable before the
stream closes and enabled again on the next fresh session; a direct
enable not re-enabled. The slot's idle peer account plays the push
service, so no App Server is needed. Runners on prosody and
anotherim-server follow.

Assisted-by: Claude:opus-5-5
As on the others: carbons through Bind 2, sent and received carbons
stored the right way round, none for <private/>, forged wrappers
ignored. ejabberd, like anotherim-server, does not answer the Bind 2
<enable/>, and delivers a contact's forged wrappers to every resource.

Assisted-by: Claude:opus-5-5
libtacky's side works: one origin-id on its sends, kept as origin_id;
the server's stanza-id by its own account kept as server_id; its own
row's archive id taken from the next catch-up. But Prosody, given a
message from the contact carrying <stanza-id by='<recipient>'/>,
removes only the one claiming the sender and delivers (and archives)
the forged one beside its own, two stanza-ids by the recipient's
account; XEP-0359 says the server MUST delete it. libtacky takes the
first and stores the contact's chosen id as server_id, which replies,
retractions and markers then use. A Prosody bug; libtacky could harden
by preferring the archive result id or refusing two ids by itself.

Assisted-by: Claude:opus-5-5
RSM paging to <fin complete>, the metadata refusal allowed without
#extended, the catch-up refilling a cleared chat once each with the
stanza-id as server_id, as on anotherim-server; numeric archive ids.

Assisted-by: Claude:opus-5-5
The same paging and catch-up as on the ejabberds; Prosody alone
advertises mam:2#extended and answers <metadata/> (start and end ids
and timestamps), so the scenario's success path ran here, and its form
also offers before-id, after-id and ids.

Assisted-by: Claude:opus-5-5
http upload has an argument called method, which collided with call's
own; module and method are positional-only now.

Assisted-by: Claude:opus-5-5
Uploads work end to end on the S3 store: the service and its limit
from disco, https slots without headers, a PUT and GET round trip of
the same bytes, libtacky's own upload, the ciphertext size (+16) for
an OMEMO chat, one byte over the limit refused locally, send_file to
an OMEMO chat with an aesgcm:// link. But libtacky's row says empty
files are refused, and only send_file and file upload do: file slot
asks the server for size 0 (bad-request stops it) and message
upload_slot adds the 16-byte tag first and gets a 16-byte slot granted.
Both should refuse size 0 before any <request/>.

Assisted-by: Claude:opus-5-5
ejabberd strips both forged stanza-ids and delivers only its own, so
the forgery is Prosody's alone. But step 5, libtacky's own row getting
its archive id after a new sign-in, can only happen when the archive
copy comes back: the catch-up resumes after the last stanza-id seen,
and on a correct server the older own message is not refetched, as
libtacky's row allows. It came back on Prosody only because the forged
id moved the start. The step is printed as INFO now; the ejabberd run,
made with the old step, is n/a and will be run again.

Assisted-by: Claude:opus-5-5
Enable and disable answered, the empty node refused locally, a
declared target withdrawn by account disable and enabled again by the
next session. Prosody sent no SM ack for the withdrawing disable before
the close, and libtacky replayed that already-answered iq into the
fresh session (harmless here, it comes first; the same replay family as
RFC 6120). anotherim-server behaves as ejabberd through stock mod_push;
its own push component (apushd) is listed and queried but its extras
were not exercised.

Assisted-by: Claude:opus-5-5
ejabberd's own mod_http_upload works (discovery, https slots without
headers, PUT 201 and the same bytes back, libtacky's upload and
send_file, ciphertext sizes, the limit refused locally), and libtacky
again sends <request size='0'/> for file slot and a 16-byte request
for an empty file through message upload_slot, which ejabberd grants;
its row says empty files are refused.

Assisted-by: Claude:opus-5-5
One origin-id on libtacky's send, stanza-ids stamped both ways, both
forged stanza-ids removed and the server's own stored as server_id:
the forgery is Prosody's alone. The step that is information now
behaves as on ejabberd.

Assisted-by: Claude:opus-5-5
One origin-id, the server's stanza-id stored, both forgeries stripped
and the real id kept; the same wire as the earlier n/a run, which only
counted the step that is information now. Its marker is checked in a
passing run, so markers_reviewed.

Assisted-by: Claude:opus-5-5
The libtacky bug again, worse here since Prosody grants a size 0 slot:
file slot and message upload_slot ask for empty files, which libtacky's
row says it refuses. The round trip could not be judged: Prosody's
slots pointed at https://prosody.asxp.io:5281/file_share/…, a host
that does not serve file_share (404), because of our global
http_external_url; fixed in the next commit, and this server runs
again. The scenario crashed on Prosody's braced Authorization header;
the runner judged steps 2-8 from a scratch copy with a Tcl parser.

Assisted-by: Claude:opus-5-5
Our global http_external_url (https://prosody.asxp.io:5281/) made
every HTTP module build its URLs on the main host, so http_file_share
handed out slot URLs where nothing serves it: every PUT got 404. Each
host now keeps its own name (upload.prosody.asxp.io serves file_share,
200), host-meta's BOSH and WebSocket links unchanged. The XEP-0363
scenario parses file slot's Tcl answer with common.tcl_list, braced
header values included, and goes on after a failed upload.

Assisted-by: Claude:opus-5-5
Prosody's raw debug log carries each XEP-0363 slot's <header
name='Authorization'>Bearer <JWT></header>, and the first Prosody
XEP-0363 run, already pushed, held two. Those tokens allow one PUT to
one path for minutes and are long expired, but a proof is no place for
them: slot headers (Authorization, Cookie, Expires) and any Bearer
token, Tcl-braced too, are redacted now, with tests, and that run is
redacted again and rehashed. Its LFS objects before this commit still
hold the expired tokens.

Assisted-by: Claude:opus-5-5
With the upload host fixed, Prosody's round trip (PUT 201, the same
bytes back), file upload and an OMEMO send_file all work; the run
fails only on libtacky's slot calls taking empty files, which Prosody
even grants (size 0). XEP-0363 fails on all three servers, for that
libtacky bug alone.

Assisted-by: Claude:opus-5-5
libtacky binds by Bind 2 only (tag, SM with resume and carbons
inline), takes the full JID from <authorization-identifier/>, sends no
RFC 6120 bind, session, SM or carbons iq; a second resource's carbon
arrives; a resume inside SASL2 keeps the JID; disable and enable bind
again with the same tag. The roster ver gap stays filed under RFC
6121. An earlier run is n/a: its first draft looked for
<stream:features>, which libtacky logs as <features>.

Assisted-by: Claude:opus-5-5
A fresh profile signs in over SASL2 (SCRAM-SHA-256-PLUS, a user-agent
with the stable id libtacky reports), takes the bound JID from
<authorization-identifier/>, no stream restart, no <auth/>; reconnects
by FAST. Against a local fake: success binds on the same stream, a
wrong server signature and a SASL2 failure end the sign-in without a
legacy retry, a server without SASL2 gets RFC 6120 <auth/>, an unknown
<continue/> task ends it (no <abort/>, a MAY). Runners follow.

Assisted-by: Claude:opus-5-5
Bind 2 only, tag, SM with resume and carbons inline, the full JID from
<authorization-identifier/>, the same JID on resume and after disable
and enable. Prosody also offers CSI inline (libtacky sends a separate
<active/>), confirms carbons in <bound/> and sends a second <features/>
after <success/> without a restart, which libtacky takes.

Assisted-by: Claude:opus-5-5
Bind 2 only with the tag, SM and carbons inline, the JID from
<authorization-identifier/>, carbons without an iq, the JID kept on
resume and the same tag and resource after disable and enable; the
same as on anotherim-server.

Assisted-by: Claude:opus-5-5
SASL2 sign-in with the user-agent and its stable id, the JID from
<authorization-identifier/>, FAST afterwards; the fake server's five
cases as on ejabberd. Prosody offers no SHA-512 or SHA-1 SCRAM and no
upgrade task, and its FAST token lasts 21 days (ejabberd 30).

Assisted-by: Claude:opus-5-5
ejabberd offers PLAIN beside SCRAM and FAST; libtacky never uses it.
Against a local fake offering it: PLAIN only over TLS (direct, or after
STARTTLS), the right payload over RFC 6120 SASL and SASL2, no retry
after a refusal, SCRAM preferred when both are offered, and nothing
sent over a cleartext stream. Every payload is redacted in the logs
and the password scan finds nothing.

Assisted-by: Claude:opus-5-5
From an empty store: a token requested with the password
(HT-SHA-256-EXPR, the right choice on TLS 1.3), a sign-in on it with
a counted <fast/>, the token alone after a daemon restart,
invalidate_fast_token sending <fast invalidate/> and storing the
replacement, a bogus token refused and the password used instead, a
new token stored. Tokens are redacted everywhere; no token value is in
any file. Three earlier runs are the scenario's development, n/a.

A HANDBOOK gap: planting a token with account add needs
token-mechanism and user-agent-id too, which it does not mention. The
scenario's last matcher fix (attribute order) is tested on this run's
log; the runners' runs are its first full ones.

Assisted-by: Claude:opus-5-5
libtacky's parse_uri decodes or refuses 17 URIs as RFC 5122 and
XEP-0147 say (%20 is a space, + stays +, bad escapes and unregistered
query types refused). On the wire: a roster invitation URI from
slixmpp becomes a subscribe with its pars:0 token and the decoded
name; libtacky's own invitation URI is pre-approved when slixmpp
uses it; a room password from a link joins, while the form-encoded
spelling is refused. Three earlier runs are the scenario's
development, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged: SCRAM-SHA-256-PLUS in
<authenticate/> with a stable user-agent id, <success/> with a FAST
token, Bind 2 and authorization-identifier, no stream restart; the
local fake's five outcomes (ok, bad server signature, failure,
legacy fallback, unknown task) and a FAST sign-in behave as the steps
say. Same shape as ejabberd, which it runs on.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. On Prosody libtacky signs
in with SCRAM-SHA-256-PLUS and then FAST, never PLAIN; against the
local fake it sends a correct PLAIN only over TLS (RFC 6120 and SASL2),
never in cleartext and never as a fallback after a SCRAM refusal.

Also corrects Peer's docstring: auto_authorize=False makes slixmpp
refuse subscriptions; answering them in a scenario needs
roster.auto_authorize = None (found by the RFC 5122 author).

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. anotherim-server offers
no PLAIN, so on the real server the run shows only that libtacky signs
in with SCRAM-SHA-256-PLUS and FAST; PLAIN itself (over TLS only, no
cleartext, no fallback after a SCRAM refusal) is shown against the
local fake, as on the other servers.

Assisted-by: Claude:opus-5-5
ejabberd runs TLS 1.3 and names tls-exporter, so libtacky rightly
binds to that there. Against a local fake that names only
tls-server-end-point, SCRAM-SHA-256-PLUS carries p=tls-server-end-point
and the RFC 5929 4.1 hash of the certificate (SHA-256, SHA-384, SHA-1
mapped to SHA-256, also on TLS 1.3), and an HT-SHA-256-ENDP FAST
token signs in with the bound HMAC. libtacky's RFC 5929 row (not used
for SCRAM) and XEP-0440 row (gs2 n for unknown types) are out of date.
The first run crashed making a SHA-1 certificate before any check, n/a.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, and its first full
run after the last matcher fix: a token on the password sign-in,
counts 1 to 3 across a daemon restart, invalidate with a replacement,
and a bogus token refused with not-authorized and replaced over SCRAM.
No token value appears in any file.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged: parse_uri's 17 cases,
roster invitations both ways with the pars:0 token passed through
untouched, and a room joined by a link with an escaped password while
the form-encoded spelling is refused.

The brief now tells agents to keep scratch files apart: two agents
wrote the same scratch file name at once.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: token on the
password sign-in, counts 1 to 3 across a restart, invalidate with a
replacement, a bogus token refused and replaced over SCRAM; no token
value in any file. Same behaviour as anotherim-server in every detail.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged: parse_uri's cases, both
roster invitations with their pars:0 tokens, and a password room
joined by link while the form-encoded spelling is refused. RFC 5122
now passes on all three servers.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. anotherim-server names
both binding types and every check passes, as on ejabberd. Prosody
names only tls-exporter, which XEP-0440 section 3 says a server MUST
also advertise tls-server-end-point beside, so the scenario's check
on the server's offer fails; libtacky did everything right (exporter
on the real server, the end point and ENDP FAST against the fake), so
the run is n/a rather than a libtacky fail. The scenario should turn
that server check into INFO.

Assisted-by: Claude:opus-5-5
The scenario finds wss://ejabberd.asxp.io:5443/ws through host-meta and
the framing works (<open/> in xmpp-framing both ways, no
<stream:stream>). ejabberd offers SCRAM without -PLUS and no SASL2 on
that stream, libtacky answers with gs2 y as RFC 5802 section 6 says
for a client that can bind but was offered no -PLUS, and ejabberd
refuses it with "Invalid channel binding" (client.log, server.log).
gs2 n signs in. A server bug; libtacky sends y on purpose
(sasl.tcl) but could fall back to n on wss. The first run, the
scenario's draft, is n/a.

The brief now asks authors to print server-only choices as INFO, after
the RFC 5929 scenario failed on Prosody's offer alone.

Assisted-by: Claude:opus-5-5
On TLS 1.3 libtacky binds SCRAM-SHA-256-PLUS and the FAST
HT-SHA-256-EXPR token to the exporter, and the server accepts the
token on reconnect. Against a local fake: exporter on TLS 1.3 whether
or not the end point is also named, the end point on TLS 1.2, a refusal
when TLS 1.2 meets an exporter-only list, and an EXPR HMAC over the
connection's exporter (zero-length context as RFC 9266 2 asks). The
first run's case d was too strict (libtacky rightly sends nothing), n/a.

Assisted-by: Claude:opus-5-5
message send neither checks nor prepares the JID it is given. It
accepts localparts RFC 7622 forbids (" & ' : < >, control characters,
over 1023 bytes) and sends OMEMO iqs to them (client.log:
to='q&#34;...@prosody.asxp.io', answered jid-malformed); the message
then stays pending with no Status. roster add sends the same item.
AI1B@, a fullwidth address and a final dot each become a chat of their
own with to= as typed, though libtacky's row says JIDs are
character-checked, case-folded and width-mapped, and roster add does
prepare them. Prosody prepares and delivers them itself. The first
run read the wrong body field, n/a.

Assisted-by: Claude:opus-5-5
Through a slixmpp ad-hoc command and a CAPTCHA: libtacky parses every
field type with title, instructions, media and options; refuses an
empty required field and two values in a list-single before sending;
submits only what a submit form carries; reads the result's reported
columns; cancels; and answers the CAPTCHA with its hidden fields. Two
earlier runs had scenario mistakes, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. Prosody serves its wss
endpoint from host-meta and offers SCRAM-SHA-256-PLUS (tls-exporter)
and SASL2 there, so libtacky binds the channel, signs in with Bind 2
inline (one framing <open/> each way, no <stream:stream>) and gets its
ping answered. So the ejabberd failure is ejabberd's: it offers no
-PLUS over WebSocket and refuses gs2 y.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, shows the same libtacky bug:
message send accepts JIDs RFC 7622 forbids and keys chats and to= by
the JID as typed (client.log: <message to='AI3B@ANOTHERIM.ASXP.IO'>).
Here it costs more: the server does not prepare a final-dot address,
so that message is lost. The server keeps an upper-case to= towards
libtacky, which exercises libtacky's addressed-to-me check: it passes.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: SCRAM-SHA-256-PLUS
bound to tls-exporter on TLS 1.3, an HT-SHA-256-EXPR token issued and
accepted on reconnect (server.log: accepted by mod_auth_fast), and the
local fake's six cases as on anotherim-server.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged. Prosody names only
tls-exporter; libtacky binds SCRAM-SHA-256-PLUS and the EXPR token to
it, and the fake's six cases match. RFC 9266 now passes on all three.

Assisted-by: Claude:opus-5-5
The same ejabberd bug as on ejabberd itself: the framing works, but
over wss the server offers SCRAM without -PLUS and refuses libtacky's
gs2 y with "Invalid channel binding" (client.log, server.log), so
sign-in over WebSocket is impossible. Prosody passes.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same results:
every field type parsed, required and single-value checks before
sending, a correct submit form, cancel, and the CAPTCHA answered;
Prosody routes the <captcha/> message and its BoB data untouched.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server: message send takes the JID
as typed (client.log: <message to='AI2B@EJABBERD.ASXP.IO'>), accepts
forbidden localparts, and a final-dot address is lost because ejabberd
does not strip the dot. libtacky's addressed-to-me check, exercised by
ejabberd keeping an upper-case to=, passes.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same results on
the third server: forms parsed, validated, submitted and cancelled,
and the CAPTCHA answered.

Assisted-by: Claude:opus-5-5
roster last_activity returns what the wire carries for slixmpp's full
JID (seconds and status), for the server (its uptime) and raises a
refusal for a missing resource; libtacky answers slixmpp's own query
with service-unavailable at once, as its row says it does not answer
jabber:iq:last.

Assisted-by: Claude:opus-5-5
libtacky keeps bookmarks in PEP when the server offers it, as its row
says, and sends no jabber:iq:private of its own; the server's private
storage stores and returns a run-unique element written through the
debug tap and forbids another user's. libtacky's XEP-0048 fallback
needs a server without PEP bookmarks and is not exercised here.

Assisted-by: Claude:opus-5-5
libtacky writes its vCard in two steps, reading it back before each
set so the second keeps the first's fields; slixmpp reads the result,
publishes its own card, and libtacky's fetch and get read every field
back (source vcardtemp: the server keeps no vCard4 copy). The first
run's scenario mis-read a slixmpp list field, n/a.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged: libtacky returns exactly what
the wire carries for slixmpp, a missing resource (refusal with the
server's text) and the server's uptime, and refuses slixmpp's own
query. ejabberd answers a connected user's bare JID with seconds='0'
where Prosody refuses it; libtacky reports both as sent.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged: PEP bookmarks chosen, no
jabber:iq:private from libtacky, the server's private storage stores
and returns a run-unique element. Prosody refuses another user's
storage with service-unavailable where ejabberd says forbidden.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with ejabberd's answers:
XEP-0012 now passes on all three servers. No server is asked about a
fully disconnected user yet (slixmpp only goes unavailable).

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, with the same
result: two writes that keep each other's fields, the merged card read
by slixmpp, slixmpp's card read back field by field, no vCard4 copy.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with ejabberd's answers on
every point. XEP-0049 now passes on all three servers; libtacky's
legacy XEP-0048 path stays untested, as every server offers PEP
bookmarks.

Assisted-by: Claude:opus-5-5
Prosody converts vcard-temp into the vCard4 PEP item. libtacky
publishes that converted item back (iq 58, though the server
advertises pep-vcard-conversion:0) and Prosody refuses it
(precondition-not-met, access_model). After set_name's newer card is
published (iq 63), libtacky retries the refused payload unchanged
(iq 64, empty <fn/> and <n/>), which becomes the current item: the
name is lost (a read afterwards, in peer-slixmpp.log, shows empty FN,
N and NICKNAME). Prosody's DESC-to-NOTE mapping also fails two of the
scenario's checks; that part is the server's conversion.

Assisted-by: Claude:opus-5-5
libtacky reads slixmpp's run-unique version; with
software_version_private=1 (the default) it refuses a contact's query
with service-unavailable; at 0 it answers a contact with name and
version and no <os/>, and still refuses a non-contact. tackyd-json
names itself Tacky 0.1, not the library's version.

Assisted-by: Claude:opus-5-5
libtacky pages a MAM archive backwards (<before/>, then
<before>first</before>, max 2) to <fin complete='true'>, follows a
paged disco#items listing with <after>last</after> to its end, and on
a listing that breaks off answers what came and emits Truncated. Minor,
not failing: mam query exposes no RSM <count>, and Truncated carries an
empty count though page 1 sent one. Two earlier runs, the scenario's
development, are n/a.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same libtacky
behaviour; ejabberd 26.09 names no OS for itself.

Assisted-by: Claude:opus-5-5
slixmpp's two run-unique forms reach libtacky's disco info in order,
FORM_TYPE and multi-values intact, and match slixmpp's caps hash.
libtacky offers its XEP-0232 softwareinfo form only while
software_info_public is 1, after all identities and features, with a
caps ver slixmpp recomputes. The first run expected a presence on the
setting change, which libtacky documents it does not send, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same libtacky
results. Prosody sends no RSM <count> and answers an unknown <after>
with item-not-found where ejabberd returns an empty complete page.

Assisted-by: Claude:opus-5-5
A nick in slixmpp's subscribe and in a message names libtacky's chat
row (carried), a PEP nick overrides it (pep) and an empty one clears
it; libtacky's own subscribe carries its nick, and its PEP publish and
clear reach slixmpp. The first run's INFO missed the notification,
which arrives before the publish result (fixed with past=True); both
runs pass.

Assisted-by: Claude:opus-5-5
With TCP made unreachable in a network namespace, libtacky falls back
to host-meta, fetches ejabberd's XRD and dials the wss URL it names
(framing <open/> both ways; server.log GET /ws, 101). Against a local
fake it falls back to host-meta.json after an XRD 404, follows no
redirect, dials no ws:// or BOSH link, and takes nothing from an
unpinned certificate. Sign-in over wss still fails on ejabberd's gs2 y
refusal (the RFC 7395 bug). The first run, still requiring a sign-in,
is n/a.

Assisted-by: Claude:opus-5-5
libtacky lists urn:xmpp:ping and answers a ping to its full JID with an
empty result carrying the same id. Not covered: answers to other
entities (no peer in a server-kind run) and the keepalive ping without
stream management (tackyd-json cannot turn SM off).

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with identical libtacky
results down to the caps vers: slixmpp's forms read in order, the
softwareinfo form only while software_info_public is 1.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. With TCP unreachable
libtacky reads Prosody's host-meta (and host-meta.json after an XRD
404), dials the wss URL and signs in: server_info shows transport
websocket and endpoint_source host-meta. The fake's refusals (redirect,
ws:// and BOSH, unpinned certificate) hold as on ejabberd.

Assisted-by: Claude:opus-5-5
A throwaway account registers through the server's data form (proof of
work solved), signs in, changes its password and signs in again. But
account unregister never answers: the server sends the <remove/>
result from the user's own full JID, which libtacky's iq matcher drops,
then <conflict/> 'User removed', which libtacky reads as a resource
conflict and redials the removed account (client.log). The account is
gone. The local fake's legacy, data-form and no-feature cases pass.
The scenario was fixed after this run (per-account iq matching, removal
seen from the wire); runners use that version.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same libtacky
results; XEP-0059 passes on all three servers. On an unknown <after>
each server answers differently: Prosody item-not-found (as XEP-0313
asks), ejabberd an empty complete page, anotherim-server the first
page.

Assisted-by: Claude:opus-5-5
libtacky did its part (nicks in subscribes and messages, its own PEP
nick and clear), but ejabberd sent slixmpp's PEP nick events to none
of its contacts: the scenario removes and redoes the subscription
inside slixmpp's session, and ejabberd only notifies contacts the
publisher's session has sent presence to (server.log: the pep_message
reaches only the publisher's own c2s). Recorded as found, with the
server bug text.

The scenario now has slixmpp broadcast its presence again once the
subscription is mutual, so it tests libtacky rather than this
ejabberd behaviour; ejabberd is re-run with it.

Assisted-by: Claude:opus-5-5
libtacky reads slixmpp's time with a run-unique clock skew and zone
(offset_ms matches), advertises urn:xmpp:time and answers a contact
with its zone and second-precision UTC, and refuses a stranger with
service-unavailable. slixmpp 1.17's own xep_0202 handler cannot answer
(it mis-parses its own time), so the scenario answers for it; the
first run hit that, n/a.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with identical libtacky
stanzas; XEP-0092 passes on all three servers.

Assisted-by: Claude:opus-5-5
Prosody registers by invitation only: libtacky reads its form and
submits it as x:data, and the refusal (not-acceptable, invitation
only) reaches the host as an Error with the server's text. No account
was created, so password change and unregister are untested here; the
fake's legacy, data-form and no-feature cases pass. No password is in
clear in any run file.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with identical libtacky
results and caps vers; XEP-0128 passes on all three servers.

Assisted-by: Claude:opus-5-5
With slixmpp re-broadcasting its presence, ejabberd still sent its PEP
nick events only to slixmpp's own resource; libtacky reads the nick
only by fetching it. server.log suggests ejabberd fans PEP out through
the caps it stored from contacts' presence, and libtacky's caps
presence reached slixmpp before the subscription was mutual. Steps 1
and 3 also failed on libtacky's cached nick from the first run, whose
clear was never delivered (documented: a PEP nick beats a carried one).

The scenario now has libtacky send fresh presence too once the
subscription is mutual, and refetches the nick at the start so a stale
cache cannot leak in. One more ejabberd run with it.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with ejabberd's results:
discovery through the XRD and host-meta.json and every refusal hold;
sign-in over wss is refused by the server's gs2 y bug. XEP-0156
passes on all three servers.

Assisted-by: Claude:opus-5-5
libtacky's console keeps the password attribute of extdisco <service/>
elements, so a server handing out real TURN credentials would put them
in a committed log. finish now redacts <service password=…> and
turn:user:pass@ URLs, and replaces only the matched secret: replacing
every occurrence inside the match also blanked a host name that
contained a short password. The XEP-0215 run's client.log is redacted
and rehashed (its two over-redacted fake host names restored).

XEP-0215 on prosody: Prosody has no extdisco, which libtacky answers
with an empty, uncached list; against a local fake libtacky asks
credentials for restricted services only, builds the stun/turn URLs,
drops refused, expired and invalid entries and caches per session.

Assisted-by: Claude:opus-5-5
XEP-0172 on ejabberd, third run: with both sides re-sending presence
once the subscription is mutual, ejabberd delivers slixmpp's PEP nick
and its clear, and every step is right on the wire. The script still
printed fail on steps 1 and 3, from a nick libtacky cached in an
earlier run whose clear ejabberd never sent (a PEP nick outranks a
carried one, as documented), and the step-0 refetch was refused
because it ran after the contact was removed. The scenario now
refetches before removing the contact.

XEP-0199 on anotherim-server: the Prosody author's scenario,
unchanged, with the same results.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same results;
XEP-0199 passes on all three servers.

Assisted-by: Claude:opus-5-5
The scenario as finally fixed on ejabberd, unchanged: every step
passes on the wire, PEP nick and clear included. XEP-0172 now passes
on all three servers.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same libtacky
results; ejabberd itself does not answer time queries (no mod_time).

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same libtacky
results; XEP-0202 passes on all three servers.

Assisted-by: Claude:opus-5-5
libtacky writes its mute list, a draft, an OMEMO chat choice, a tag
and a chat pin to private PEP nodes, every publish carrying
publish-options (whitelist, persist_items, never, max_items) and a
sealed payload; the node configurations read back as asked and
everything is undone after. The first run matched iq ids across two
daemon sessions (ids restart per session), n/a.

Assisted-by: Claude:opus-5-5
Both servers list STUN and TURN services with the TURN credentials
inline; libtacky builds the ICE URLs (userinfo percent-encoded), asks
for no credentials it already has, and answers the next fetch from its
cache. The local fake's cases pass as on Prosody.

ejabberd's debug server.log writes the ephemeral TURN password in a
"Created ephemeral credentials" line and in the Erlang records
(password = <<"…">>); finish now redacts both. Both runs' logs are
redacted with them and rehashed before this commit; no earlier run
contains either line.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged: every private node written
with publish-options and a sealed payload, the node configurations
read back as asked, everything undone after.

Assisted-by: Claude:opus-5-5
Across three daemon sessions Prosody advertises rosterver and stamps
its pushes ver='109' and ver='110', but each roster get libtacky
sends is <query xmlns='jabber:iq:roster'/> without the stored ver
(client.log), so the server always returns the full roster. libtacky's
row says the get carries the stored ver, Bind 2 sessions included. On
a Bind 2 session conn features holds only the inline features, so
roster.tcl's rosterver test is false; the post-auth <features> are
never consulted. The same gap as the RFC 6121 Bind 2 bug, now shown
on its own feature.

Assisted-by: Claude:opus-5-5
ejabberd logs the XML it receives, so a registration submit put its
throwaway password into server.log, and the RFC 5122 runs' server logs
held the test rooms' MUC passwords. finish now redacts <password/>
element contents; the affected runs are redacted and rehashed (the
RFC 5122 ones only held throwaway room passwords). check also accepts
a client's own placeholders ([redacted], [xxxx]) and no longer prints
the secret it found, only the text before it.

XEP-0077 on ejabberd: legacy fields read and submitted, the refusal
(forbidden, access policy) reaches the host as an Error; registration
is closed, so password change and unregister are untested here. The
fake's cases pass.

Assisted-by: Claude:opus-5-5
libtacky sends bare <inactive/> and <active/> nonzas when the app
state changes, says <inactive/> again right after an SM resume, and
starts a fresh session already inactive through Bind 2; it never sends
CSI where none is offered. The first run looked for <stream:features>
in logs that write <features xmlns=…streams>, n/a.

Assisted-by: Claude:opus-5-5
Every forward libtacky sends leaves the inner <message/> without
xmlns='jabber:client', so it inherits urn:xmpp:forward:0 (client.log;
slixmpp parses it as {urn:xmpp:forward:0}message). XEP-0297 3.2 says
the forwarded stanza's namespace MUST be preserved and calls the
inherited one wrong. message.tcl's ForwardNodes builds it without -ns.
Everything else works: delay, inner from and id, the fallback body,
the stored forward_* fields, and forwards received from slixmpp
(including a nick-only author). Two earlier runs crashed in the draft
scenario, n/a.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same wire;
XEP-0223 passes on all three servers.

Assisted-by: Claude:opus-5-5
The same libtacky bug: no ver on any of three Bind 2 roster gets,
though the server stamps its pushes. Here the post-auth <features>
naming rosterver arrives only after the roster get has gone out, so
libtacky must send the ver on Bind 2 sessions as its row says rather
than wait for features.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server: no ver on any Bind 2 roster
get (client.log lines 9, 159, 357) though ejabberd stamps its pushes
and advertises rosterver after SASL2 <success>, here after the get
has already gone out.

Assisted-by: Claude:opus-5-5
XEP-0352 on both ejabberd-based servers, the Prosody author's scenario
unchanged: set_active nonzas, <inactive/> again after a resume, and a
fresh inactive session; as these servers leave CSI out of the Bind 2
inline list, libtacky sends that <inactive/> as a nonza after bind.

XEP-0368 on prosody: libtacky merges _xmpps-client and _xmpp-client by
priority and weight, dials direct TLS with SNI the domain and ALPN
xmpp-client, and falls back to STARTTLS when direct TLS is lower,
refused in the handshake or absent ("."), with a fake DNS server in a
network namespace for the cases real DNS cannot show.

Assisted-by: Claude:opus-5-5
The same libtacky bug: the inner <message/> of its forwards carries no
xmlns='jabber:client' (client.log, and slixmpp parses it as
{urn:xmpp:forward:0}message). Prosody re-serialises the payload but
does not touch its namespace. Everything else passes as on
anotherim-server.

Assisted-by: Claude:opus-5-5
ejabberd logs at debug and rotates every 10 MB, often inside a run's
window, and interop-log read only the current file, so thirteen
ejabberd runs kept only the tail of their server traffic (the XEP-0368
run 2 s of 71). interop-log now reads the rotated files oldest first
(skipping .gz), deployed to ejabberd. The thirteen runs' server.log
are refetched over the whole window, redacted and rehashed; each
passes check. Prosody rotates daily and no run crossed midnight.

Assisted-by: Claude:opus-5-5
XEP-0368: the Prosody author's scenario, unchanged, with the same
results on both; the equal-priority tie chose STARTTLS on ejabberd and
direct TLS on anotherim-server (INFO).

XEP-0390 on prosody: libtacky advertises and answers a caps 2.0 set
(sha-256, sha3-256), resolves slixmpp's set through the sha-256 node
once and caches it, ignores a sha-1-only set, rejects a forged hash,
and rebuilds its set when its disco#info changes.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server: the inner <message/> of its
forwards lacks xmlns='jabber:client' (client.log out, unchanged by
ejabberd). Everything else passes.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same libtacky
behaviour: its own sha-256/sha3-256 set, one sha-256 query per new
set, sha-1-only ignored, a forged hash rejected, the set rebuilt.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same libtacky
behaviour; XEP-0390 passes on all three servers.

Assisted-by: Claude:opus-5-5
After an SM resume libtacky self-pings its occupant JID and stays
joined on a result; kicked while away, it reports the kick once and
ignores the room's late not-acceptable. It answers a ping to its own
occupant JID. The 10-minute idle ping is not tested (too long for a
run).

Assisted-by: Claude:opus-5-5
xmpp: URIs with ;preauth=, <preauth token=…/> and Prosody's "Got
unknown token" line carry tokens that let anyone subscribe to (or, for
an account invitation, register on) a test account. finish now
redacts them; the six RFC 5122 runs, which held their roster-invite
tokens in clear, are redacted and rehashed.

Assisted-by: Claude:opus-5-5
XEP-0401 on prosody: register invite runs urn:xmpp:invite#invite and
returns the server's contact invitation URI; Prosody refuses
create-account to a non-admin and libtacky passes the refusal on in
words; against a local fake libtacky handles both form shapes, the
two-stage create-account, and rejects a non-xmpp URI or foreign form.
No invitation was redeemed and no account created; the tokens are
redacted.

XEP-0421 on ejabberd: libtacky and slixmpp see the same occupant ids,
the room replaces a forged one, and libtacky uses them to recognise
its reflected message, follow a nick change, accept a correction and
replace a reaction across a nick change. The first run's draft
scenario misread two harness details, n/a.

Assisted-by: Claude:opus-5-5
The server offers no full-text search (its MAM form has only with,
start and end; no urn:xmpp:fulltext:0). libtacky reports that,
searches its own store with match ranges, refuses a remote search
without sending an unfiltered query, and falls back to the store for
source=both. The server-side search branch is left to the runners.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, with the same
results. Its error-condition reader took ejabberd's <text/> (same
namespace) for the condition; it only fed an INFO line here, and the
scenario now skips <text/>.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, with the same
results; XEP-0410 passes on all three servers. On Prosody the queued
kick arrives before libtacky's post-resume self-ping, which libtacky
still sends to the room it has just left: harmless, but it could skip
rooms left during the resume replay.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same results on
ejabberd's MUC underneath.

Assisted-by: Claude:opus-5-5
ejabberd's archive offers no full-text search either (mnesia backend:
with, start and end only); libtacky's handling of that is right, as on
anotherim-server. The server-side search branch remains untested.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with the same results;
XEP-0421 passes on all three servers.

Assisted-by: Claude:opus-5-5
None of the three servers offers full-text search, so XEP-0431 is n/a
everywhere; libtacky's handling of a server without it (local search
with match ranges, a refused remote search, the store fallback) is
right on all three. The server-side search branch needs a server or a
local fake with a full-text archive.

Assisted-by: Claude:opus-5-5
ejabberd has no invitation commands (item-not-found for both);
libtacky passes the refusal on in words, and the local fake's cases
pass. No invitation was issued and no account created.

Assisted-by: Claude:opus-5-5
mam prefs reads the server's rules, set_prefs writes a run-unique
default and always/never lists that read back equal, a JID in both
lists or a full JID is refused before anything is sent, and the
original rules are restored and read back. The HANDBOOK's mam intro
still calls prefs/set_prefs "reserved, not implemented", though they
are (since 1.1.9).

Assisted-by: Claude:opus-5-5
ejabberd offers no ibr-token and issues no invitations, so no real
token exists; libtacky sends no <preauth/> there and asks the plain
form. Against a local fake libtacky sends a held token before the
form, turns a refusal into an Error, registers only by invitation
when that is all the server offers, holds a link's token from
add_from_uri until it is used, and parse_uri reads and limits tokens
as its row says. No account was registered; tokens are redacted.
account untrust_certificate on a registration domain fails where
trust_certificate works (HANDBOOK says both are handled).

Assisted-by: Claude:opus-5-5
Against a local fake on SASL2 libtacky does not abort where XEP-0440
1.0.0 section 3 rules 4 and 5 say it MUST: offered -PLUS without
<sasl-channel-binding/> it sends SCRAM-SHA-256-PLUS on tls-exporter,
and offered types without any -PLUS it sends SCRAM-SHA-256 with gs2 y
(client.log; sasl.tcl SelectWith treats SASL2 as SASL1). Not
exploitable as such, but both MUSTs are unmet. Rules 1, 6 and 7 hold
on the real server and the fake. libtacky's row says a list naming
only types the stream lacks gets gs2 n; it refuses instead (its own
code and test agree), so the row is out of date. The first run's
draft scenario missed the server's features, n/a.

Assisted-by: Claude:opus-5-5
XEP-0474 on prosody: Prosody sends no h=, so the protection is shown
against a local fake whose server-first carries one: with a matching
or absent h= libtacky answers with a valid proof, and when h= covers a
different mechanism or channel-binding list (stripped -PLUS, cut cb
types, an added mechanism, the other SASL profile's list) it stops
before any proof with an AuthError naming XEP-0474, on RFC 6120 SASL
and SASL2 alike, with no PLAIN retry.

XEP-0441 on ejabberd: the Prosody author's scenario, unchanged; the
preferences end as they started.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged; XEP-0441 passes on all three
servers, and every account's preferences ended as they started.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: libtacky does right
by Prosody (tls-exporter, the only type it names), but the local
fake's SASL2 cases l and m fail the same way (no abort where rules 4
and 5 say MUST; client.log lines 553 and 560).

Assisted-by: Claude:opus-5-5
Prosody's debug log writes a SASL element twice on one line,
<challenge>…<challenge …>BASE64</challenge>; the pattern consumed the
'<' that opens the second, so the second copy (the SCRAM server-first:
nonce, salt, iterations) was left in clear. The pattern now only looks
ahead to that '<'. 47 runs held such copies (no credentials) and are
redacted and rehashed; all 185 finished runs pass check.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged: the ten fake cases behave as
on Prosody, and libtacky signs in to both servers with
SCRAM-SHA-256-PLUS, so any h= they send matched. Whether they send h=
cannot be read, since every log redacts the challenge.

Assisted-by: Claude:opus-5-5
XEP-0401 on anotherim-server: its mod_invites issues both an ibr=y
contact invitation and a create-account invitation; libtacky's
register invite and create_account return them, and the
create-account URI registers claude-inv-20261105012915, which signs in
and is removed again (the removal hit the known XEP-0077 unregister
bug: no answer, then a redial). ejabberd's debug log wrote the
invitation token as "processing token (domain): T"; finish now redacts
that line too, and this run's server.log is redacted and rehashed.

XEP-0445 on prosody: Prosody offers ibr-token, and libtacky sends the
token in <preauth/> before any form request and turns the refusal
into an Error; no token that allows registration was available
(member invitations are contact-only), so no account was created.

XEP-0440 on ejabberd: right by the real server; the same local-fake
SASL2 failure (rules 4 and 5) as on the other two.

Assisted-by: Claude:opus-5-5
The first server where real tokens work: libtacky presents a token in
<preauth/> before the form only where ibr-token:0 is offered, turns a
refused token into an Error, opens the form with an ibr=y invitation,
and with a create-account token held by add_from_uri registers
claude-ibr-20261105021755, which signs in and is removed (the removal
hits the known unregister timeout; later sign-ins are refused, so the
account is gone). No token or password is left in the run's files.

Assisted-by: Claude:opus-5-5
ejabberd offers only UPGR-SCRAM-SHA-512, which libtacky does not ask
for, so nothing was stored there. Against a local fake libtacky asks
for UPGR-SCRAM-SHA-256 and -SHA-1 and answers each with a hash of the
same password the fake verifies, answers 1,000,000 iterations and
refuses 2,000,000, refuses tasks it did not ask for or that were not
offered, and stops on a wrong server signature. The first run judged
the TLS refusal before the certificate was pinned, n/a.

Assisted-by: Claude:opus-5-5
libtacky honours every advertised size limit: Prosody's 262144 after
auth (just under delivered, just over refused locally with too-large
and nothing sent), the post-auth value winning both ways on a fake,
and its 256 KiB default without <limits/>. But when the answer to a
request is over the limit (a fake's disco#info get under a 4012-byte
limit), libtacky sends nothing: send raises (client-events.log
"over the 4012 this stream's server accepts") inside iq respond and
the asker gets no answer, where its row says the request is answered
policy-violation with the reason.

Assisted-by: Claude:opus-5-5
Against local fakes libtacky discovers the OAuth server through an
empty OAUTHBEARER dial, registers as a native client, asks for a code
with S256 PKCE and state, refuses a wrong state, exchanges the code,
signs in over SASL2 with a FAST request, refreshes once on refusal
(once only, then forgets the token, on RFC 6120 SASL), and ends on a
user's access_denied. anotherim-server offers no OAUTHBEARER and no
OAuth pages, so only the discovery ran against it. libtacky's row
says the metadata issuer is not checked; rc2 checks it. The first
run's fake had a bug, n/a. No token in any committed file.

Assisted-by: Claude:opus-5-5
ejabberd has none, and libtacky reports it unsupported after asking
both namespaces. Against a local fake libtacky lists and revokes in
urn:xmpp:cam:0, falls back to Prosody's manage-clients namespace,
parses every field including urn:tacky:client-access:0 sessions,
explains a <password-reset-required/> refusal without retrying, and
errors on an unknown id. ai2a's credentials were never touched. The
first run's scenario misread an error <text/> and crossed iq ids, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged. Prosody offers no upgrade
tasks, so libtacky asks for none and nothing is stored; the local
fake's cases match ejabberd's run exactly.

Assisted-by: Claude:opus-5-5
The same libtacky bug, shown against the local fake: an answer over
the limit is dropped instead of answered policy-violation
(client-events.log line 290). anotherim-server advertises no
<limits/>, so libtacky applies its 256 KiB default and refuses larger
sends locally.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server (client-events.log line
280): an answer over the limit is dropped instead of answered
policy-violation. ejabberd advertises no <limits/>; libtacky's local
refusal over its 256 KiB default works as elsewhere.

Assisted-by: Claude:opus-5-5
The first real server with client management: libtacky asks
urn:xmpp:cam:0, falls back to Prosody's manage-clients namespace for
list and revoke, finds the scenario's own second session among 27
rows and revokes only that one; Prosody closes that stream but answers
undefined-condition, which libtacky reports as a failed revoke, as the
wire says. libtacky's own client and ai1a's credentials were untouched.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged: no upgrade offered, none
asked for, nothing stored; the fake's cases as on the other two.
XEP-0480 passes on all three servers.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: the local-fake code
flow and refresh rules as before; ejabberd offers no OAuth mechanism,
so oauth_begin answers that and nothing is fetched or granted.

Assisted-by: Claude:opus-5-5
The local-fake flow passes as on the other servers. Prosody offers
OAUTHBEARER and publishes metadata, but without an authorization or
registration endpoint (mod_http_oauth2 turns them off without
oauth2_registration_key), so libtacky's oauth_begin rightly refuses;
the scenario counted that as a fail where it should be INFO, so the
run is n/a. Nothing was registered or granted.

Assisted-by: Claude:opus-5-5
anotherim-server answers urn:xmpp:cam:0 itself: libtacky lists the
clients, finds its own row by its session resource, revokes only the
scenario's second session (the server ends that stream), and gets an
error for an unknown id. The server revokes a password client where
XEP-0494 has it answer <password-reset-required/>, the server's
choice. XEP-0494 passes on all three servers.

Assisted-by: Claude:opus-5-5
Against a local fake libtacky answers a SCRAM server-first whose t=
names the stream's TLS version, or carries none, with a valid proof,
and stops before any proof with an AuthError naming XEP-0515 when t=
names another version or is malformed, on RFC 6120 SASL and SASL2,
TLS 1.3 and 1.2 (including the TLS 1.2 stream told 0304), with no
fallback. ejabberd's challenge is redacted, so whether it sends t=
cannot be read; libtacky signs in to it normally.

Assisted-by: Claude:opus-5-5
libtacky's clients list reproduces exactly the sessions, push and
last-address the server sends for every row, across a second client's
sign-in, a push registration on and off, a dropped connection
(hibernated), its resume (online, same since) and its revoke; libtacky
finds its own row by its bound resource. A local fake covers the
parsing rules (other namespaces and unknown elements ignored, order
kept, missing attributes empty). Only the scenario's own client was
revoked.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with identical fake-case
results; libtacky signs in to the server normally.

Assisted-by: Claude:opus-5-5
With iap=1 set and the daemon restarted, libtacky never pipelines
against a local fake that advertises <config-version/>: on every
redial its <authenticate/> follows the server's <features> (client.log,
e.g. ts 1793847043345 after a 2 s held header) and carries no version.
connOptions (client.tcl ~1099) reads the setting through
tacky_setting_get on an object without -owner, so the caught error
yields the default 0, and tacky_client delegates no -iap anyway; the
happy_eyeballs setting takes the same path. ejabberd itself advertises
no config version (INFO). The first run changed iap without a restart,
n/a.

Assisted-by: Claude:opus-5-5
Prosody lists no sos form. Against a local fake that does, libtacky
keeps the status addresses in order, fetches nothing while connected,
and once the server is unreachable fetches the status document once
per failure burst over https only (skipping http and a closed port),
reporting planned times and the message in the configured language in
<ServiceOutage> and outage_status; addresses are dropped when the form
goes. Two earlier runs failed on the draft fake, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with identical fake-case
results; XEP-0515 passes on all three servers.

Assisted-by: Claude:opus-5-5
The same libtacky bug: with iap=1 and a restart, no redial to the
local fake pipelines; each <authenticate/> follows the server's
features and holds no version (client.log, iap2 at 1793847442925).

Assisted-by: Claude:opus-5-5
With happy_eyeballs=1 stored and the daemon restarted, libtacky still
dials one SRV target at a time, exactly like the setting-0 baseline:
behind a blackholed target it waits 10 s before resolving the others,
never tries IPv6 first, and waits out a slow IPv6 path instead of
racing IPv4; "(happy eyeballs)" never appears in client-debug.log.
The same cause as XEP-0509: connOptions reads the setting through
tacky_setting_get on an object without -owner, so it gets the
default. The setting and endpoint are restored.

Assisted-by: Claude:opus-5-5
Plain and sealed (argon2id) XEP-0227 exports hold every seeded,
run-unique client value in urn:tacky:pie:0 (row, read marker, notify,
own reaction, draft and mute module-state, custom tag) and stay a
whole XEP-0227 account without it; they hold none of 13 device
secrets checked by value. A sealed import into a fresh offline store
refuses a wrong passphrase and restores the values; only
summary counts go into the run, never an export. Minor: a restored
custom tag shows only after a restart; <row/> can carry an
undocumented <quick/>. The first run read the wrong body field, n/a.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: Prosody's rows
carry no client-access elements and libtacky reads them all as empty;
the local fake's parsing cases pass as on anotherim-server. Nothing
revoked.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same results:
no sos form on the real server, the fake's outage fetch once per
burst over https only, reported in <ServiceOutage> and outage_status.

Assisted-by: Claude:opus-5-5
The same libtacky bug: with happy_eyeballs=1 the SRV targets are still
dialled one at a time in every case (10 s behind a blackholed target,
IPv4 only, a slow IPv6 path waited out); no "(happy eyeballs)" line.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged: ejabberd has no
client list, so only the local fake's parsing cases run, as on the
others. urn:tacky:client-access:0 passes on all three servers.

Assisted-by: Claude:opus-5-5
Every libtacky check passed as on anotherim-server, but the scenario's
device-secret scan matched omemo_own's "watched" value, a bare clock
seconds timestamp that equalled one in the export's device/ state.
The scan now skips all-digit values.

Assisted-by: Claude:opus-5-5
storage export writes the account's own vcard-temp card as a PEP node
named "vcard-temp" (a pubsub#owner configure plus items with the
<vCard/>), not as the <vCard xmlns='vcard-temp'/> child of <user/>
that XEP-0227 1.1 section 4.7 requires, so another importer would
make a bogus open PEP node and restore no vCard (vcard.tcl keeps it
under the pseudo-node; backup.tcl exports every own node, and
PepNodesSkipped does not list it). Everything else holds: roster, PEP,
archive, hosts, passwords only as <user password/> when asked, the
sealed format (argon2id, XChaCha20, tamper and truncation refused) and
an offline round trip. Exports stayed outside the repo. The first
run's draft required a vCard4 item, n/a.

Assisted-by: Claude:opus-5-5
The re-run with the timestamp-skipping secret scan: every check
passes as on anotherim-server, and no device secret is in the export.

Assisted-by: Claude:opus-5-5
In a network namespace whose only ways out are a recording local
SOCKS5 proxy on the 1080, 9050 and 4447 ports, libtacky names the host
to the proxy as a domain name and signs in: plainly, with an RFC 1929
login from the proxy URL, through "Tor" with a random per-account
login and a .onion host passed unresolved, and through "I2P" without a
login. It makes no DNS query of its own, never connects directly, and
on a proxy that refuses every CONNECT only retries the proxy. No real
Tor or I2P was used.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same results;
XEP-0455 passes on all three servers (none lists a sos form).

Assisted-by: Claude:opus-5-5
The same libtacky bug: the account's own vcard-temp card is exported
as an open PEP node "vcard-temp", not as the <vCard/> child of <user/>
XEP-0227 4.7 asks for (x227-summary.txt: vcard_temp_child false).
Everything else passes as on anotherim-server; exports stayed outside
the repo.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, with the same
results; urn:tacky:pie:0 passes on all three servers.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server: happy_eyeballs=1 never
reaches the connection, so targets are dialled one at a time in every
case.

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same results: the
host always named to the proxy, logins as given or made up for Tor, no
DNS of libtacky's own, no direct fallback.

Assisted-by: Claude:opus-5-5
On Prosody libtacky holds the account's card as a vCard4 node (access
presence) and also caches the server's converted vcard-temp; the
export writes both as PEP nodes, the vcard-temp one being the bogus
node seen on the other servers, and gives the vCard4 node
pubsub#access_model open: backup.tcl (line 584) sets whitelist for
bookmarks and open for every other node, so a server importing the
file would publish a contacts-only vCard. The scenario looked the
vCard4 node up under its payload namespace; it now uses
urn:xmpp:vcard4 (two of the three FAIL lines came from that).

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server: with iap=1 no redial to the
local fake pipelines (client.log lines 330-333, 397-400, 464-467).

Assisted-by: Claude:opus-5-5
The Prosody author's scenario, unchanged, with the same results. The
last AI run of the night: all 63 ai_ok features now have runs on all
three servers.

Assisted-by: Claude:opus-5-5
What did not fit a run's verdict: libtacky doc mismatches and minor
behaviours, harness ideas the agents asked for, server-side state the
runs left (junk chats and client rows on the test accounts, an unspent
invitation), and the export folders in /tmp to delete.

Assisted-by: Claude:opus-5-5
The -up/-down apps rebuild up.auto.tfvars.json from the shared state's
"up" output, but their targeted apply never updates outputs. After
prosody-down the output still named prosody, so every later toggle,
the CI's builder-up/-down included, planned module.nixos_test["prosody"]
and failed with "Invalid index" on the missing server; ejabberd and
anotherim could not be stopped until the output was put right by hand.
A refresh-only apply after the targeted one keeps the output in step.

Assisted-by: Claude:opus-5-5
145 features have ai_ok = false because a person judges part of them
(display, sound, call media, trust decisions), so no agent ran them.
Much of each still shows on the wire and in libtacky's API. An agent's
run of such a feature now carries confidence = "low" (finish
--confidence low; check refuses it otherwise): AI-RESULTS.md shows it
with a marker and FEATURES.md never counts it. The brief tells the
agent to check what the wire and the API show and to name what only a
person could judge; tools/ai/queue-low.txt orders the 135 not skipped.

Assisted-by: Claude:opus-5-5
The PREROUTING REDIRECT sent every TCP 443 connection to the local
HTTPS port, a container's way out included, so a freshly installed
anotherim server could not build its image: the ejabberd .deb download
from GitHub hit the redirect and got "connection refused". Match only
destinations that are this host's own addresses.

Assisted-by: Claude:opus-5-5
Authors read the XEP only "if needed", so a scenario could test what
libtacky says it does rather than what the spec requires. The brief now
has them list the spec's client-side MUST/SHOULD rules the wire or API
can show (a RULES list in the scenario, written to spec-rules.txt in
every run), check each one by number, name the rules left unchecked,
and cite the section in every bug.

Assisted-by: Claude:opus-5-5
Two run-unique rooms, one created by each side, destroyed at the end:
join with history and subject, groupchat and private messages both
ways, nick change, configuration, affiliations and roles, a kick,
mediated and direct invitations with a decline, leave, and the
nick-conflict and members-only errors; libtacky's events and room
state follow. Not checked: how an app shows any of it.

Assisted-by: Claude:opus-5-5
libtacky does every step as on prosody, but ejabberd 26.09 admitted
slixmpp, whose affiliation had been set back to none, into a room just
made members-only, as a visitor (peer-slixmpp.log line 80), where
XEP-0045 7.2.6 says the service MUST deny access with
<registration-required/>. A server finding, low confidence; its cause
(a role ejabberd keeps from the run's earlier member and kick steps?)
is not confirmed.

Assisted-by: Claude:opus-5-5
The same server finding as on ejabberd, on its ejabberd 26.09: the
non-member peer is admitted to a members-only moderated room as a
visitor (peer-slixmpp.log line 78), where 7.2.6 says MUST deny with
<registration-required/>; likely mod_muc_room's stored per-JID role
skipping the check. libtacky passes every step it drives. A history
check also failed, a timing artefact of a server slow after its
restore: the line reached the room after the peer had joined.

Assisted-by: Claude:opus-5-5
Against slixmpp with raw Jingle iqs, a XEP-0234 file over IBB both
ways: initiate, accept, acks, session-info, the error conditions
(unknown-session, out-of-order, bad-request, unsupported-info) and
libtacky's own terminates all follow XEP-0166 1.1.2. But after
sending a file, libtacky ends the session on the receiver's
<received/> without a terminate of its own, then answers the
receiver's session-terminate <success/> with unknown-session, where
6.7 says the other party MUST acknowledge it (jinglefile.tcl's own
header expects that terminate). Media and tie-break not checked: the
headless daemon has no media backend. The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
All 14 rules of XEP-0084 1.1.5 the wire shows: metadata+notify only,
data published before metadata, the SHA-1 ItemID, plain base64, a
complete <info/>, the PNG chosen over a url rendition, data cached by
hash and dropped on a hash mismatch, an empty <metadata/> clearing it
both ways, and no subscription to the data node. Not checked: display.
Minor: an avatar is fetched twice when the server notifies both bare
and full JID, and libtacky keeps its own avatar after the nodes are
deleted. Three runs broke on the draft scenario, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, with identical results: the
file sender refuses the receiver's session-terminate with
unknown-session (client.log lines 371-381).

Assisted-by: Claude:opus-5-5
Corrections both ways in a 1:1 chat: each correction names the
original's id (also when correcting twice), one row keeps the last
body and the revisions, a chained or early correction is resolved,
corrections by another sender become new messages, and corrections in
carbons and MAM catch-up apply; libtacky advertises message-correct:0.
Not checked: how an edited message is shown.

Assisted-by: Claude:opus-5-5
The scenario now lists 26 rules of XEP-0045 1.35.5 (spec-rules.txt)
and checks each the wire shows, adding the reserved-room and
password-room flows, ban, kick and 321/322 removals, voice requests,
declines, the shape of admin items and of libtacky's own presence,
and refusing a decline or voice request that does not come from a
room; server checks print apart from libtacky's. The members-only
check uses a room the peer was never in, which anotherim-server
refuses correctly, so the earlier admissions came from ejabberd's
stored per-JID role in moderated rooms; history now waits for the
room's echo. libtacky does not look up its reserved nick before
joining (a SHOULD, INFO). The first run compared a redacted password,
n/a.

Assisted-by: Claude:opus-5-5
The same libtacky bug on the third server (client.log line 408): the
file sender refuses the receiver's session-terminate.

Assisted-by: Claude:opus-5-5
Every rule held in the logs, but ejabberd notifies libtacky of
slixmpp's empty <metadata/> before slixmpp has its publish result, so
libtacky's clearing <Update> fired before the scenario began waiting
for it. The scenario now starts listening before slixmpp disables.

Assisted-by: Claude:opus-5-5
The re-run with the listener started in time: all 14 rules as on
prosody. ejabberd notifies once and mirrors avatars into the vCard.

Assisted-by: Claude:opus-5-5
16 rules of XEP-0333 1.0.0 from the wire and libtacky's events, in 1:1
chats and a run-unique room: the marker names the message's id (not
its origin-id), goes to the bare JID with a store hint, one marker
for the newest viewed and forward only, incoming markers move read
state only forward, no markers for unmarkable, own-device, non-contact
or opted-out messages, room markers give readers by occupant-id, and
own markers travel as carbons both ways. Not checked: display, and
the room-without-sid rule. libtacky still sends <received/> markers,
which 1.0.0 removed (its row lists them). The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
All 26 rules as on anotherim-server, and every server check: ejabberd
refuses the members-only join with <registration-required/> in a room
the peer was never in, so the earlier admission is tied to a room
where the peer had held roles.

Assisted-by: Claude:opus-5-5
Prosody relays an invitation with the inviter as an occupant JID
(<invite from='room@service/nick'>), as XEP-0045 7.8.2 allows.
libtacky's ParseInvite (message.tcl line 10754) strips every inviter
to a bare JID, which for an occupant JID is the room, so
decline_invite sends <decline to='room@service'>: the room bounces it
to itself and Prosody discards it ("Discarding unhandled error
message"), so the inviter never hears of the decline. The inviter
should be kept as the room named it. ejabberd and anotherim-server
name the inviter's real JID, where bare is right. Everything else
passes as there.

Assisted-by: Claude:opus-5-5
The prosody author's scenario, unchanged: R1-R16 as there; R17 still
cannot be checked, as ejabberd's rooms announce urn:xmpp:sid:0 too.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, every rule as there. The
disk filled before the runner could finish the run; the orchestrator
finished it with the runner's verdict and notes once space was back.

Assisted-by: Claude:opus-5-5
ai3a's store had OMEMO turned on for ai3b by an earlier run, so the
outgoing phase's plain body never arrived and phase 2 stopped; every
part that ran (R1, R3-R5, R7-R16) passed as on the other servers. The
scenario now turns OMEMO off for the peer and restores it afterwards.

Assisted-by: Claude:opus-5-5
The re-run with OMEMO off for the peer (restored afterwards): R1-R16
as on the other two servers; R17 is not checkable on any of them, as
all their rooms announce urn:xmpp:sid:0.

Assisted-by: Claude:opus-5-5
16 rules of XEP-0402 1.2.0: one item per room keyed by its JID with
the whitelist publish-options, foreign extensions kept on edit,
retract with notify on removal, +notify, autojoin applied at sign-in
and on another device's notifications (join, leave, drop), and forged
events from another JID ignored. ai2a's bookmarks left as they were.
Not checked: display, and the XEP-0048 fallback (ejabberd has PEP
bookmarks). The first run compared a redacted password, n/a.

Assisted-by: Claude:opus-5-5
`nix develop .#ai` copies the whole repo into the Nix store whenever
the tree changes, and with the run logs that is about 2.5 GB a time:
the store grew by 12 GB in half an hour of runs and commits and
filled the disk twice. tools/ai/aish runs the same shell from a fixed
rev (tools/ai/ai-shell.rev), copied once; the brief now says to use it.

Assisted-by: Claude:opus-5-5
XEP-0384 0.9.1 against slixmpp-omemo, both OMEMO 2 and 0.3: device
lists and 100-prekey bundles published (open nodes), key exchanges
both ways decrypted and their prekeys replaced, keys for every
device including libtacky's own others (never itself), the fallback
body with EME and a store hint, the SCE envelope, and a removed
device left out. Not checked: trust decisions, device labels, rooms.
libtacky did not refetch a device list on a message from a device no
longer listed (a SHOULD, INFO). The first run waited on the wrong
event field, n/a.

XEP-0308 on anotherim-server stopped at its first plain message, as
ai3a's store has OMEMO on for ai3b; the scenario now turns it off for
the run and restores it.

Assisted-by: Claude:opus-5-5
The re-run with OMEMO off for the peer (restored afterwards): every
rule as on ejabberd and prosody; XEP-0308 passes on all three.

Assisted-by: Claude:opus-5-5
All 14 rules as on prosody and ejabberd; XEP-0084 passes on all three.
Both accounts started with an empty <metadata/> item and end with no
avatar nodes: no avatar either way.

Assisted-by: Claude:opus-5-5
Two libtacky bugs against XEP-0424 0.5.0. A 1:1 retraction naming the
message's id (5.2: "the sender should use the value of the 'id'
attribute") is ignored when the message also carries a different
origin-id, as libtacky matches only the origin-id (client.log lines
306, 310: no <Retracted>). And a retraction it rightly refuses (wrong
sender, three cases) is stored as a new message showing its fallback
body, where 5 says a client MUST NOT display the body of a message
carrying <retract/>. Everything else passes: libtacky's own
retractions by id and by room stanza-id with fallback and store hint,
retracted rows keeping their history, and retractions via carbons,
MAM, early arrival and the room. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, R1-R16 as there; ai3a's
(empty) bookmarks left as they were.

Assisted-by: Claude:opus-5-5
XEP-0444 0.2.1 3.3: "When receiving this error message, the
reaction-sending entity MUST revert the 'message reactions' to its
previous state." slixmpp answered libtacky's {🙏, 💯} with a
not-acceptable error; libtacky kept {🙏, 💯} and emitted nothing
(SetReactions stores the set before sending, with no error path). The
other 105 checks pass: ids (origin-id or id in 1:1, stanza-id in the
room), the full set each time incl. clearing, one element with a store
hint, emoji validation sending and receiving, reactions on a
correction, per-sender sets (by occupant-id in the room), carbons and
MAM. The first run's draft misread a clear, n/a.

Assisted-by: Claude:opus-5-5
The ejabberd author's scenario, unchanged, R1-R16 as there; XEP-0402
passes on all three servers. ai1a's (empty) bookmarks left as they were.

Assisted-by: Claude:opus-5-5
The same two libtacky bugs, every rule as on ejabberd (client.log 303:
the id-named retraction ignored; client-events.log 98, 112, 194:
refused retractions stored with their fallback body). Also moves the
XEP-0402 prosody note the last commit put under RFC 6121 by mistake,
where it made the file invalid.

Assisted-by: Claude:opus-5-5
The prosody author's scenario, unchanged, every rule as there; ai2b's
OMEMO nodes empty before and after, ai2a's devices and identity
unchanged.

Assisted-by: Claude:opus-5-5
The same two libtacky bugs (client.log 150; client-events.log 99,
109, 193), every other rule as on ejabberd; XEP-0424 fails on all
three servers.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log 251-254, peer-slixmpp.log 64): the
reaction set is not reverted after a not-acceptable error; the other
105 checks pass as on prosody.

Assisted-by: Claude:opus-5-5
The same libtacky bug, also seen in libtacky's store: the reaction
row is never rewritten after the not-acceptable error. XEP-0444 fails
on all three servers on this one rule.

Assisted-by: Claude:opus-5-5
XEP-0384 (OMEMO) on anotherim-server: every rule as on the other two.
The scenario printed one FAIL, R11, only because ai3b's device list
already held two devices from before the run; libtacky encrypted to
exactly the four listed devices (5.5.2), so the runner judged it a
pass. The scenario's R11 should compare with the list as fetched, and
its bundle cleanup retracts several ids as one.

XEP-0461 (Message Replies) 0.2.1 on prosody, low confidence: 14
rules in 1:1 and a room, the reply id (origin-id, else id; stanza-id
in rooms), the fallback quote with a code-point range, stripping on
receipt, replies to corrected and retracted messages, carbons and MAM.
The first run's draft used the bare room JID, n/a.

XEP-0447 (Stateless File Sharing) 0.3.1 on ejabberd, low confidence,
fails: send_album's source-attaching messages carry the url as a body
without the <fallback for='urn:xmpp:sfs:0'/> that 3.3 says MUST be
added when one is included (send_file marks it correctly). Everything
else passes: metadata and hashes, HTTP upload, verified downloads, a
mismatching hash refused, late sources by the author only, and rooms.

Assisted-by: Claude:opus-5-5
XEP-0447 (Stateless File Sharing) on prosody: the same libtacky bug,
send_album's source messages without the sfs fallback marker
(client.log sfs-688fdf8eebbc6f9d, sfs-5018be22ab73df89); every other
rule passes.

XEP-0461 (Message Replies): R1-R14 on both, as on prosody; it passes
on all three servers.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log sfs-c36b593d5c7c433d,
sfs-e878642fbc2ae5fe); XEP-0447 fails on all three servers on this
one rule.

Assisted-by: Claude:opus-5-5
XEP-0454 (OMEMO Media Sharing) 0.1.0, low confidence: libtacky uploads
AES-256-GCM ciphertext with the tag appended, sends the aesgcm:// URL
(12-byte IV, 32-byte key) only inside OMEMO, decrypts and verifies
received files (12- and 16-byte IVs), and refuses a tampered tag,
keeping nothing.

XEP-0420 (Stanza Content Encryption) 0.5.0 inside OMEMO 2, low
confidence: the envelope's content, rpad, time, to and from; bodies,
replies and corrections inside; wrong to/from and a future time
refused; inner elements act as in the clear. Recorded as a pass, but
for a person to review: libtacky keeps <request/> (receipts),
<markable/> and <active/> outside the envelope, where 9 says such
elements MUST NOT be in plaintext and libtacky's row says an encrypted
message's outer elements are only the server's (PEER_SIGNALS in
omemo.tcl keeps them out on purpose). It also refuses an envelope
without <rpad/> or <from/>, which neither spec asks a receiver to do.

Assisted-by: Claude:opus-5-5
XEP-0420 (Stanza Content Encryption) on ejabberd, low confidence,
fails: libtacky sends <active/>, <request xmlns='urn:xmpp:receipts'/>
and <markable/> in plaintext beside its OMEMO 2 ciphertext (client.log
id 1794024218012148). XEP-0420 0.5.0 9: elements not in Table 2 "MUST
NOT be available in plaintext outside the <envelope/>", and libtacky's
row says "an encrypted message's outer elements are only the
server's". The prosody author recorded the same behaviour as INFO and
passed; the scenario now checks it as R7, and prosody is re-run.

XEP-0454 (OMEMO Media Sharing) passes on prosody as on ejabberd.

XEP-0353 (Jingle Message Initiation) 0.8.0 on anotherim-server, low
confidence, as the callee without media: rings on a contact's propose
and sends <ringing/>, ends on retract (missed), its own reject
(decline), another device's <proceed/> carbon or <accept/>
(other_device), drops a stranger's propose, and answers a second
propose busy. Not checked: its own proceed and the Jingle session (no
media backend). Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
The re-run with R7 as a check: the same libtacky bug as on ejabberd,
<active/>, <request/> and <markable/> in plaintext beside the OMEMO 2
ciphertext (client.log line 266); the earlier pass used the looser
scenario. Every other rule passes.

Assisted-by: Claude:opus-5-5
The anotherim-server author's scenario, unchanged, R1-R11 as there;
roster entry, strangers setting and OMEMO setting left as they were.

Assisted-by: Claude:opus-5-5
The same libtacky bug: <active/>, <request/> and <markable/> in
plaintext beside the OMEMO 2 ciphertext (client.log message
1794024614885970, and on replies and corrections); XEP-0420 fails on
all three servers on R7 alone.

Assisted-by: Claude:opus-5-5
R1-R11 as on the other two; XEP-0353 passes on all three servers for
what can be tested without media (libtacky as the callee).

Assisted-by: Claude:opus-5-5
R1-R12 as on ejabberd and prosody; XEP-0454 passes on all three.
ai3b's two older devices kept, slixmpp's removed.

Assisted-by: Claude:opus-5-5
An old client's storage:bookmarks, in private XML and the legacy PEP
node, reach libtacky's rows through ejabberd's compat (name, nick,
password, autojoin; renames live), autojoin rooms are joined, and
libtacky's own bookmarks appear to the old client; libtacky never
uses jabber:iq:private where PEP bookmarks exist. ejabberd sends no
retraction when an old client removes a bookmark (INFO). All three
stores left as they were. The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
XEP-0050 1.3.0 5: "the requester SHOULD consider any command node that
does not match the JID of the responder to be suspicious, and ignore
those command nodes." slixmpp's command list included an item for
another JID (mallory-…@example.org); libtacky's `adhoc list` returned
it as a command. A SHOULD, but a security one, so counted as a fail;
the other 32 checks pass: the server's command list and a read-only
uptime, a three-stage command with next, prev and complete, actions
and default action, error notes and XMPP errors, cancel, sessionid on
later stages, and submit forms only. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
R1-R8 as on ejabberd; prosody's compat also sends retractions for an
old client's removals, which libtacky applies live. ai1a's three
stores left as they were.

Assisted-by: Claude:opus-5-5
`pubsub publish` and `pubsub create` return the item id or node name
through Tcl's expr (pubsub.tcl lines 1158 and 1044: expr {... ? $a :
$b}), which rewrites a number-like string: the service's
<item id='1E20261107042352'/> came back to the host as "Inf", a
server-chosen id 70262E937439 too, and 0x10 as 16. XEP-0060 ids are
opaque strings the publisher needs back (7.1.2, 7.2). The other 72
checks pass on PEP and on pubsub.anotherim.asxp.io: create and
configure, publish with and without id, publish-options retry,
items, retract with and without notify, whitelist and affiliations,
subscribe and unsubscribe, +notify, forged notifications dropped,
purge, delete and errors. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
libtacky advertises jabber❌oob, sends a file's https url in x:oob
with the body equal to it, stores and downloads a received https url
(respecting the autofetch setting and strangers), never fetches plain
http to another host and refuses ftp: and file:, answers jabber:iq:oob
with service-unavailable, and puts no x:oob in the clear beside OMEMO.
It sends no <desc/> and keeps none received (MAY).

Assisted-by: Claude:opus-5-5
XEP-0050 (Ad-Hoc Commands): the same libtacky finding (client.log iq
35): `adhoc list` keeps a command node naming another JID (5 SHOULD);
every other check passes; ejabberd offers a normal user only ping.

XEP-0048 (Bookmarks): R1-R8 as on ejabberd and prosody; it passes on
all three servers. ai3a's three stores left as they were.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log iq 55, 56: <item id='1E…'/> and
<create node='1E…'/> returned to the host as "Inf"); the other 72
checks pass.

Assisted-by: Claude:opus-5-5
XEP-0085 (Chat State Notifications) 2.1, low confidence: 20 rules,
discovery both ways by reply and by caps (no states until support is
shown, <active/> on the first body, none after a stateless reply),
standalone states as bodyless chat messages with no-store, the opt-out
setting, incoming states reported but not stored, own-device carbons
ignored, and groupchat. Sends states to a removed contact that showed
support (10 SHOULD NOT, INFO). The first run's caps phase broke on
slixmpp, n/a.

XEP-0066 on anotherim-server: every rule passes except the scenario's
"contact's file is fetched", because this server's upload URLs are on
object storage outside the domain and libtacky rightly holds those
back (attachment_autofetch_foreign). The scenario now checks that
refusal and turns the setting on for the fetch, restoring it.

Assisted-by: Claude:opus-5-5
XEP-0060 (Publish-Subscribe) on prosody: the same libtacky Inf bug
(client.log iq 45, 46). Prosody's pubsub component lets only admins
create nodes, so the service step did not run; the scenario should
print that refusal as INFO, not FAIL.

XEP-0066 (Out of Band Data) on anotherim-server, re-run with the
foreign-host step: all eight rules pass, the contact's file fetched
once attachment_autofetch_foreign is on; settings restored.

XEP-0153 (vCard-Based Avatars) 1.1.1 on ejabberd, low confidence,
fails: on a second sign-in libtacky's first presence carries the photo
hash kept from the last session (client.log ts 1794026628623) before
its own vCard get goes out (…624), where 4.2 says a client "MUST NOT
advertise an avatar image without first downloading the current
vCard" (presencemod.tcl keeps it on purpose). The rest that ejabberd's
PEP-vCard conversion leaves reachable passes; both vCards and avatar
nodes restored. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
Assisted-by: Claude:opus-5-5
The same libtacky finding (client.log iq 37): a foreign-JID command
node kept by `adhoc list` (5 SHOULD); every other check passes.
XEP-0050 fails on all three servers on this rule alone.

Assisted-by: Claude:opus-5-5
R1-R8 as on ejabberd; XEP-0066 passes on all three servers.

Assisted-by: Claude:opus-5-5
The prosody author's scenario: every rule as there. The scenario set
send_chat_states back to "1" where it had been unset; it now restores
the exact previous value (the runner reset ai2a's by hand; ai1a's,
left "1" by the author's run, means the same as unset).

Assisted-by: Claude:opus-5-5
The same libtacky finding (client.log line 192 before 198): the kept
photo hash in the first presence before the own vCard is fetched.
Everything else as on ejabberd; vCards and avatar nodes restored.

Assisted-by: Claude:opus-5-5
The same libtacky finding (client.log ts 1794026974453 presence with
the kept hash, 454 the vCard get). Prosody passes an explicit hash
through, so the mismatching-hash rule ran for real here and passes.
Its R15-clear FAIL line is a scenario artifact: Prosody kept the
peer's PEP avatar, which libtacky rightly prefers. vCards and avatar
nodes restored. XEP-0153 fails on all three servers on R4.

Assisted-by: Claude:opus-5-5
Every rule as on prosody and ejabberd; XEP-0085 passes on all three.
send_chat_states, OMEMO and the roster entry back as they were.

Assisted-by: Claude:opus-5-5
XEP-0234 0.19.1, 71 checks over IBB and SOCKS5 both ways: the offer's
<file/> (name, size, media-type, sha-256), IBB ranked last, the
checksum, a received file with a path-traversal name stored under
its bare name, mismatching hashes and checksums refused with nothing
kept, unknown algorithms kept unverified, a request for an unknown
file refused, ranged requests, and resume after a broken stream. The
refused session-terminate is XEP-0166's finding (INFO). ejabberd's
proxy65 advertises 127.0.0.2 (INFO). The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
XEP-0231 1.1, 32 checks: inline thumbnails taken without a fetch,
missing cids fetched from the sender's full JID, cached by max-age
(0 = fetch again), hash mismatches and over-size blobs refused,
item-not-found for unknown cids, its own inline blobs well formed,
and Jingle offer thumbnails served and fetched. The first run's
draft used a tag that repeats per server, n/a.

Assisted-by: Claude:opus-5-5
R1-R18 as on ejabberd; settings restored.

Assisted-by: Claude:opus-5-5
libtacky trusts a stamp only from its own account, the server in the
offline or resume window, a room during its join history, or MAM's
<forwarded/>; everything else is stored at arrival time, never in
the future. It parses every XEP-0082 form tried to the microsecond,
falls back to arrival time on malformed stamps, sorts by the stored
stamp, and sends no <delay/>. Prosody passes a peer's forged
from=domain delay through on 1:1 messages (INFO, server side).

Assisted-by: Claude:opus-5-5
A XEP-0158 CAPTCHA whose inline <data/> does not hash to its cid
reaches the host as the challenge's media bytes (client-events.log,
message cap2m20261107050619): captcha.tcl only decodes the data and
never calls ::tacky::bob::check, which adhoc.tcl uses (line 312) and
libtacky's XEP-0221 and XEP-0231 rows promise ("checked against its
cid"); it also skips the size cap and raster-only filter. In ad-hoc
forms every rule passes: cid and https renderings, types, sizes,
unsupported schemes dropped, mismatching and SVG bytes dropped.

Assisted-by: Claude:opus-5-5
Every rule as on prosody; ejabberd also passes a forged room delay
through, so the room case was tested for real (stored at arrival).
The scenario found a <forwarded/>'s own <delay/> only when it came
first (Prosody's order); ejabberd puts it after the inner message, so
R4 printed "not checked" (the runner confirmed it by hand). It now
parses the stanza and takes the delay that is a direct child of
<forwarded/>, in either order.

Assisted-by: Claude:opus-5-5
R1-R18 as on ejabberd and anotherim-server; XEP-0234 passes on all
three. The SOCKS5 proxy path is still untested: the peer always takes
the direct candidate.

Assisted-by: Claude:opus-5-5
Every date libtacky writes (entity time, idle, MAM start, XEP-0446
date, XEP-0080 timestamp) is a UTC DateTime with Z; it reads Z and any
+/-hh:mm offset with 0-9 fraction digits to the right instant, and
refuses or ignores 21 malformed forms without crashing. It does not
read the legacy XEP-0091 form, which its row does not claim (INFO).

Assisted-by: Claude:opus-5-5
R1-R12 as on anotherim-server; it passes on all three. ejabberd
delivered the 88 KB size-test stanza about 15 s late, close to the
scenario's 20 s wait, which is now 60 s.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log cap2m20261107051534): CAPTCHA inline
cid bytes passed on without the hash check; ad-hoc forms pass.

Assisted-by: Claude:opus-5-5
Every rule as on prosody and ejabberd; it passes on all three. None
of the servers flushes offline storage (all deliver by MAM), so
libtacky's offline-window trust is still untested.

Assisted-by: Claude:opus-5-5
R1-R8 as on anotherim-server; geolocation and OMEMO state restored.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log line 168): CAPTCHA inline cid bytes
passed on without bob::check, so also with no size cap; ad-hoc forms
pass. XEP-0221 fails on all three servers on this one rule.

Assisted-by: Claude:opus-5-5
12 rules of XEP-0249 1.2: jabber❌conference advertised, invitations
sent with jid and the optional password, reason, continue and thread,
continue read in both lexical forms, received ones as an event and a
pending row, accept joining with the password, decline sending
nothing, malformed and strangers' invitations dropped. INFO: three
invitations to one room leave three rows; a refused accept leaves an
autojoin bookmark; the HANDBOOK says the password never reaches the
host, but <Invite> and the row carry it. The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
libtacky publishes, reads and follows vCard4 as the spec and its row
say (one 'current' item, the whole card, unknown elements kept,
access by publish-options with the precondition retry, every mapped
field both ways, vcard-temp fallback), but its disco#info lists only
urn:xmpp:vcard4+notify, not urn:ietf:params:xml:ns:vcard-4.0, which
XEP-0292 0.12.2 7 says a supporting client MUST advertise
(peer-slixmpp.log line 35). Also noted: with the server's avatar-only
conversion, the vcard-temp name stays stale. Profiles restored. Two
draft runs, n/a.

Assisted-by: Claude:opus-5-5
R1-R8 as on anotherim-server and ejabberd; it passes on all three.

Assisted-by: Claude:opus-5-5
`message send_file` with a 70,106-byte thumb puts it inline as a cid:
thumbnail (peer-slixmpp.log, th-t20261107052434-o4.png: 93,476 base64
characters), above the 64 KiB libtacky's row claims; a Jingle offer
with the same thumb names none, and receiving refuses over 65536.
thumb_data_parts (file.tcl line 2763), used for outgoing shares,
checks syntax and type but not size, though its comment says it is
"checked as strictly on the way out as on the way in". The other 79
checks pass: thumbnail attributes, cid: only fetched (never http(s)),
SVG and HTML refused, data: thumbs capped at 16 KiB, garbage sizes
dropped. The first run's draft, n/a.

Assisted-by: Claude:opus-5-5
Every rule as on prosody. anotherim-server puts no stanza-id on direct
messages, so libtacky's IsDuplicate content check (same second, sender
and body) merged two different invitations to one room: their bodies
are libtacky's generated text, which leaves out the reason. Harmless
here (XEP-0249 4 asks to show one), but two different messages from
such a server can be merged in general. Rooms, bookmarks, roster and
settings restored.

Assisted-by: Claude:opus-5-5
Every rule as on prosody; the same row folding as on anotherim-server
(no stanza-id). XEP-0249 passes on all three servers.

Assisted-by: Claude:opus-5-5
The same libtacky finding on both (peer-slixmpp.log line 51 on
prosody, 39 on ejabberd): disco#info and caps list only
urn:xmpp:vcard4+notify, not urn:ietf:params:xml:ns:vcard-4.0 (7
MUST). Everything else passes. On prosody the peer's own publish was
refused because its node was already open from an earlier run (a
scenario limit), so the incoming side was not tested there. Profiles
restored on both. XEP-0292 fails on all three servers on R7.

Assisted-by: Claude:opus-5-5
XEP-0334 1.0.0 on the wire: chat states carry no-store; retractions,
reactions, displayed markers, JMI and OMEMO messages carry store;
nothing carries store together with no-store; no-copy only on the OTR
query to a full JID; ordinary bodies carry no storage hint; and hints
on received bodies don't keep them out of libtacky's own history.
Prosody's archive and carbons handling is INFO. The scenario ran twice
in the run folder; the verdict is the second pass's.

Assisted-by: Claude:opus-5-5
From 05:30:57 to about 06:20 UTC both client streams received nothing
(ejabberd and the ejabberd XEP-0292 run were stalled over the same
window, so most likely the test machine's side). O1-O3 passed; the
author's bug shows on the wire too (client.log ts 1794029456950, a
95,180-byte share with the inline thumbnail). Note for libtacky: its
unanswered stream-management <r/> at 05:30:57 timed nothing out; it
noticed the dead stream only at the RST 50 minutes later.

Assisted-by: Claude:opus-5-5
libtacky puts the EME hint on its OMEMO 2, OMEMO 0.3 and OTR messages
and handles incoming ones as XEP-0380 0.4.0 says: a scheme it cannot
read becomes a notice naming it (by namespace, never trusting a bogus
name for a listed one), the fallback body is never stored, and a hint
alone never makes a cleartext body count as encrypted. But its
disco#info does not list urn:xmpp:eme:0, which 3.3 says a supporting
entity MUST (caps.tcl never adds it). It also sends name='OMEMO' and
name='OTR', which 3.2 says SHOULD NOT be sent for listed protocols
(INFO). The first run read the wrong body field, n/a.

Assisted-by: Claude:opus-5-5
The re-run after the outage: the same libtacky bug (peer-slixmpp.log
ts 1794033100613: a 70 KB thumb inline in a share); the other 79
checks pass as on ejabberd.

Assisted-by: Claude:opus-5-5
R1-R11 as on prosody; ejabberd's archive and carbons handling matches
Prosody's. OMEMO device and setting restored.

Assisted-by: Claude:opus-5-5
The same libtacky finding: disco#info without urn:xmpp:eme:0; every
other rule as on ejabberd. Throwaway OMEMO device removed, ai3b's
older devices kept, chat setting restored.

Assisted-by: Claude:opus-5-5
R1-R11 as on prosody and ejabberd; it passes on all three.

Assisted-by: Claude:opus-5-5
The same libtacky bug (peer-slixmpp.log, th-t20261107063013-o4.png: a
70 KB thumb inline in a 95 KB share). Prosody then stopped routing
anything the peer sent after a 20.8 KB share (bytes received, never
parsed; perhaps rate limits), so the later share and Jingle checks did
not run there; libtacky was not at fault in those FAIL lines.
XEP-0264 fails on all three servers on the size cap.

Assisted-by: Claude:opus-5-5
The same libtacky finding (disco#info without urn:xmpp:eme:0); every
other rule as on the other two. XEP-0380 fails on all three servers
on R1.

Assisted-by: Claude:opus-5-5
XEP-0425 0.3.1: libtacky's moderate iq names the room's stanza-id over
message-moderate:1, waits for the room, and applies the room's notices
with moderator and reason (in all three role set-ups); forged notices
never retract anything. But a forged moderation notice an occupant
sends through the room (both :1 and :0 forms, client.log lines
249-250) is stored as a new room message showing its fallback body,
where 5 says such messages MUST be discarded (and XEP-0424 5 says a
<retract/>'s body MUST NOT be displayed): the same class as the
XEP-0424 refused-retraction rows. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
A reply whose <fallback for='urn:xmpp:reply:0'> holds only a
<subject start end/> child is dropped: reply::fallback_ranges
(reply.tcl line 45) counts <body/> children only and, finding none,
treats the fallback as covering the whole body, though XEP-0428 0.2.1
2.2 assumes the whole body only when the element has no children at
all. Everything else passes: libtacky's fallbacks always name `for`,
reply ranges are in code points (45 over ZWJ, flags and combining
marks), whole-body fallbacks on retractions and shares, exact
stripping of received ranges, unknown or missing `for` kept as text,
malformed offsets skipped. The first run predates a control case, n/a.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log lines 244, 246; client-events.log
117, 122): forged occupant moderation notices stored as room
messages with their fallback body; everything else passes.

Assisted-by: Claude:opus-5-5
10 rules of XEP-0372 0.5.0: mention references with xmpp: uris (bare
JID in 1:1, the percent-encoded occupant JID in rooms) and code-point
ranges over ZWJ, flag, skin-tone and combining characters, with a
XEP-0513 <mention/> beside each; bad mentions refused by the API;
received references read in code points and malformed or non-xmpp
ones dropped; references shifted around reply quotes; and mentions_me
set only by a valid reference to libtacky's own occupant or bare JID.
The first run's draft took offsets from the wrong body, n/a.

Assisted-by: Claude:opus-5-5
XEP-0379 0.3.3: invite URIs, single-use 160-bit tokens that expire and
can be revoked, refusal of missing, unknown, expired, revoked and used
tokens, auto-approval with a reciprocal subscribe for a valid one, and
accept_invite's subscribe with <preauth/> all pass. But libtacky never
pre-approves the inviter (3.4 SHOULD, and its row and HANDBOOK
promise it "where the server offers it"): Prosody offers pre-approval
in the <features> sent after SASL2 <success/> (client.log line 11),
which libtacky never reads (roster.tcl StreamHas sees only the inline
Bind2 features). The same cause makes `roster preapprove` fail on
Prosody. No token left in any run file. The first run's draft read the
roster too early, n/a.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log i15-3f03e6): a subject-only reply
fallback drops the reply; the control is stored. Every other rule as
on anotherim-server.

Assisted-by: Claude:opus-5-5
Prosody's rooms advertise no message moderation, so the scenario
stopped after checking that libtacky refuses to moderate there and
sends nothing. The forged-notice discard (5) is a client rule and
could still be tested on such a server; the scenario should run that
step regardless.

Assisted-by: Claude:opus-5-5
The same libtacky bug (client.log line 263, i15-1c0ba9); the control
is stored. XEP-0428 fails on all three servers on R9 alone.

Assisted-by: Claude:opus-5-5
ejabberd's debug log prints a parsed <preauth token=…/> as
{<<"token">>,<<"…">>}, sometimes across a log-line prefix, which no
SECRETS pattern matched: two RFC 5122 runs of 2026-11-04 committed
pre-approval tokens in clear in server.log, and the XEP-0379 ejabberd
runner found the same. Match that form, and re-redact those two runs'
server.log (hashes updated). The tokens remain in earlier commits.

Assisted-by: Claude:opus-5-5
XEP-0379 (Pre-Authenticated Roster Subscription) on ejabberd: the
same libtacky finding (client.log line 7: pre-approval offered in the
<features> after SASL2 <success/>, never read); everything else
passes; roster restored, tokens scrubbed.

XEP-0385 (Stateless Inline Media Sharing) 0.2.1 on anotherim-server,
low confidence: the SIMS <file/> libtacky sends has no <hash/> and no
<desc/>, which 4.1 says MUST be there (the XEP-0447 <file/> beside it
has both), so a SIMS-only reader cannot verify the download (4.2). The
rest passes: reference without range, cid: thumbnail, https source
from upload, received SIMS read, verified and refused on a bad hash,
and SIMS merged with OOB and XEP-0447. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
R1-R10 as on ejabberd.

Assisted-by: Claude:opus-5-5
The same libtacky bug: the SIMS <file/> without <hash/> and <desc/>
while the XEP-0447 <file/> beside it has both; everything else as on
anotherim-server. INFO: a song's XEP-0447 <file/> carries <length/>,
which libtacky's row says it sends only for voice and video notes.

Assisted-by: Claude:opus-5-5
The first pattern missed a {<<"token">>,<<"…">>} pair broken across a
log-line prefix and anotherim-server's Elixir formatting
{"token", "…"}; six tokens were still in clear in the 2026-11-04
RFC 5122 anotherim-server log. Match both and re-redact it.

Assisted-by: Claude:opus-5-5
The same libtacky finding on the third server: pre-approval is offered
only in the <features> after SASL2 <success/> (client.log line 37),
which libtacky never reads; everything else passes; roster restored;
tokens scrubbed (the runner redacted 36 by hash before the patterns
were widened).

Assisted-by: Claude:opus-5-5
R1-R10 as on ejabberd and prosody; it passes on all three.

Assisted-by: Claude:opus-5-5
libtacky sends <idle since/> (UTC, whole seconds) on set_idle and
keeps it on later presences, sends a presence without it when idle is
cleared, sends nothing for an unchanged time, refuses bad times and
clamps a future one, reads slixmpp's idle into its presence state and
events (a future since clamped, a malformed one ignored), and lists
urn:xmpp:idle:1. Presence restored. Two draft runs, n/a.

Assisted-by: Claude:opus-5-5
The same libtacky bug: the SIMS <file/> without <hash/> and <desc/>;
everything else as on the other two. XEP-0385 fails on all three
servers on R4/R5.

Assisted-by: Claude:opus-5-5
libtacky's <attach-to/> is right everywhere it sends one (captions
after the file, set_caption's correction and retraction, album
sources, in rooms by the reflection's stanza-id) and its 1:1 reading
holds (by origin-id, same author only, early captions folded later).
But in a room it also folds a caption whose attach-to names the file's
origin-id, where XEP-0367 0.3 5 says only the stanza-id should be
matched in a group chat and anything else shown as a normal message
(a SHOULD; FoldCaption's lookup matches server, own and origin ids
alike). The same-author check still holds. The first run's draft,
n/a.

Assisted-by: Claude:opus-5-5
libtacky creates, lists, assigns, updates, unassigns and destroys hats
through the room's hats commands (execute, then the room's form back
as submit under its session), reads hats from occupant presence into
its occupant state through to none, matches the room's lists, and
reports a non-owner's refusal. The malformed-hat and 32-hat cases
could not be tried: the server strips occupants' own <hats/>. Two
draft runs, n/a.

Assisted-by: Claude:opus-5-5
R1-R10 as on prosody; ejabberd passed the malformed since values
through, so libtacky's handling of them was really exercised.
Presence restored.

Assisted-by: Claude:opus-5-5
The same libtacky finding (client.log line 399): a room caption naming
the file's origin-id is folded; R1-R14 pass as on ejabberd.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass against ejabberd: libtacky ran all six
hats commands, parsed occupant hats (uri, title, hue) and their
updates, and reported the non-owner refusal. Server behaviour matches
anotherim-server. R3, R5 and R7 stay unchecked on both servers: they
strip occupants' own <hats/> and new rooms advertise hats by default.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R10 pass and libtacky's own presence ends as it
started. The server log was cut at 10000 lines during sign-in, so
the verdict rests on client.log and the slixmpp log.

Assisted-by: Claude:opus-5-5
New low-confidence scenario checking XEP-0300 1.0.0 with XEP-0414's
recommendations: libtacky advertises sha-256, sha-512, sha3-256 and
sha3-512, sends well-formed sha-256 sets, and verifies every known
hash it receives over XEP-0447 and Jingle, refusing any mismatch.
BLAKE2b is not supported, as its features row already says.

Two earlier runs are marked na: the scan of libtacky's sent hashes
mixed up hash sets echoed in Jingle session-accept.

Assisted-by: Claude:opus-5-5
Low-confidence runner passes with the prosody author's scenario
unchanged: same result rule for rule, upload service found on both,
settings, transports and OMEMO restored (checked from a second
session, since tacky.py does not log API calls).

Assisted-by: Claude:opus-5-5
Same R15 failure as on ejabberd and anotherim-server: in a room
libtacky folds an incoming caption whose attach-to names the file's
origin-id, though XEP-0367 section 5 says only the room's stanza-id
should be matched. Its own outgoing captions name the stanza-id
correctly. R1-R14 pass.

Assisted-by: Claude:opus-5-5
Prosody's rooms advertise hats but offer no hats commands, and
they strip occupants' own <hats/>, so only R1 and R10 could run;
both passed. R3, R5 and R7 are now unchecked on all three servers
and would need a local fake MUC.

Assisted-by: Claude:opus-5-5
New low-confidence scenario: reads XEP-0393 1.1.1 into 16 client
rules (spans, directives' placement, preformatted and quote blocks,
<unstyled/>, literal HTML, libtacky's own limits) and checks each on
the wire and in libtacky's styling API, both ways with slixmpp. All
pass. The first two passes inside the run failed on scenario
mistakes; the verdict comes from the third.

Assisted-by: Claude:opus-5-5
New low-confidence scenario covering XEP-0391 0.1.2 with XEP-0396
and libtacky's jet-omemo:2: libtacky sends JET files under OMEMO 2
(AES-256-GCM) and XEP-0396 (AES-128-GCM) that slixmpp decrypts,
keeps an incoming JET-OMEMO 2 file, refuses malformed, mismatched
and untrusted offers, and falls back to an encrypted upload when the
peer drops JET. Two draft runs are na for scenario mistakes.

Assisted-by: Claude:opus-5-5
New low-confidence scenario: libtacky's JET-OMEMO offers under OMEMO
0.3 use AES-128-GCM with a fresh key and IV and decrypt in
python-oldmemo; it accepts slixmpp's offer and refuses AES-256,
type mismatch and bad GCM tags. The scenario reads <iv/> from the
XML because python-oldmemo replaces it on payload-less messages.

Assisted-by: Claude:opus-5-5
New low-confidence scenario: libtacky learns the conversion from its
account's disco#info, publishes its avatar with XEP-0084 only and
writes no vCard PHOTO when the server converts. The server converts
both ways. The not-converting branch is left to prosody and
ejabberd. One earlier run is na for a scenario mistake.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the anotherim-server author's
scenario unchanged: all 16 rules pass, including the 16417-character
body that libtacky leaves unstyled.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R10 pass with the same refusals. Each offer carried
35 keys, mostly stale devices on ai2a's own list left by earlier runs.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: Prosody also converts both ways, so
libtacky publishes with XEP-0084 only and writes no vCard. R4 (the
client-side mirror when the server does not convert) is still
unexercised.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R17 pass; ai3b's OMEMO switch started on and stayed on.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: ejabberd converts too (mod_avatar), so
R4, libtacky mirroring its avatar into vCard PHOTO itself, has not
run on any of the three servers; it needs a non-converting server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged:
all 16 rules pass, as on prosody and anotherim-server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass, with one scenario FAIL line overridden:
ai3b's two devices are on both its OMEMO 0.3 and OMEMO 2 lists, and
libtacky, as its features row documents, leaves OMEMO 2 devices out
of a XEP-0396 key transport when the chat writes OMEMO 2. The
scenario's R1 expects every 0.3 device. Strictly this departs from
XEP-0396 section 3 ("MUST" encrypt to every device), the same kind of
documented gap as missing BLAKE2b; the R1 check should learn the
row's rule. R2-R10 pass as on prosody and ejabberd.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R17 pass; settings, trust and ai1b's OMEMO nodes restored.

Assisted-by: Claude:opus-5-5
New low-confidence scenario reading XEP-0446 0.2.0 into 12 client
rules: libtacky sends complete <file/> metadata (date as mtime,
matching sha-256, thumbnail, length in ms, basename only) and reads
every element of slixmpp's shares, defaulting the media type and
escaping a "../" name before storing. All pass. One earlier run is
na for a scenario mistake.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 13 rules from XEP-0448 0.2.0:
libtacky sends aes-256-gcm shares only inside OMEMO 2, with a fresh
key and IV per file, the tag appended and an https source, reads
aes-256-gcm and aes-128-gcm shares, and refuses a bad hash, a bad
tag and an http source. All pass.

INFO only: a share uploaded without a GCM tag is refused, though
section 4 only recommends the tag; and the envelope's aesgcm:// body
carries no XEP-0428 fallback mark.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 13 rules from XEP-0434 0.6.0:
libtacky sends a well-formed OMEMO 2/SCE trust message only to
authenticated devices after a manual verification, applies trust
messages from an authenticated device, holds back one from an
unauthenticated device until it is authenticated, and builds and
applies trust URIs per section 9.1.1. All pass.

Two small scenario edits made after the run (pre-existing device
bookkeeping and one INFO line) were compiled but not run; the
runners will exercise them. libtacky's XEP-0450 row claims a trust
send reaches every usable device, but code and wire show only
authenticated ones do; the row is out of date.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R12 pass; ai3b's OMEMO switch started on and ended on.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R12 pass, same as ejabberd and anotherim-server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the anotherim-server author's
scenario unchanged: R1-R13 pass with the same INFO findings (tagless
upload refused, no fallback mark on the aesgcm:// body).

Assisted-by: Claude:opus-5-5
slixmpp's retract() takes a single item id; given a list it sent the
ids run together, the server answered item-not-found, and the
slixmpp devices' bundle items stayed on accounts that keep other
OMEMO 2 devices (seen on ai3b in the XEP-0434 anotherim run, cleaned
by hand). Fix it in XEP-0334, XEP-0380, XEP-0384 and XEP-0434.

XEP-0434 also notes an incomplete clean-up on its RESULT line,
which before printed pass with a FAIL cleanup line above it.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass as on prosody, and the
author's post-run scenario edits behaved. ai3b's pre-existing OMEMO
devices kept their lists, bundles and trust. The scenario's bundle
clean-up failed on this account (fixed in the previous commit); the
runner retracted the two leftover items by hand.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 17 rules from XEP-0449 0.2.0:
libtacky sends stickers as XEP-0447 shares with <sticker/>, reads
received ones with their pack URI, previews, imports and publishes
packs to an open PEP node with correct hashes and ids, and keeps
restricted or wrong-hash packs unpublished. All pass; the body being
the URL rather than the desc (section 4.4 SHOULD) is INFO, as a
documented fallback choice. One earlier run is na for scenario bugs.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: same rules as ejabberd, R5 INFO; the
server accepted max_items=max, and ai3b's OMEMO switch was restored on.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged:
R1-R13 pass with the same INFO findings as prosody and
anotherim-server.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 13 rules from XEP-0450 0.4.0:
libtacky sends trust messages only to authenticated devices, holds
back those from unauthenticated senders until verified, applies
trust and distrust from authenticated contact devices, uses blind
trust only before the first verification, hides trust messages from
the chat, and does nothing ATM-related with auto_trust off. All
checked rules pass; R6 (key not yet fetched) could not be set up.

This settles the XEP-0434 note: the XEP (section 1 and every rule in
section 4) says trust messages go only to authenticated endpoints,
as libtacky's AtmEncrypt does. The "reaches every usable device"
line in libtacky's features.md, HANDBOOK and omemo.tcl comment is
out of date. The first run is na: OMEMO was explicitly off for ai1b.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass as on prosody and
anotherim-server, every clean-up line passed. The per-id retract fix
was not exercised (ai2b had no other OMEMO 2 devices).

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0452 0.2.2. anotherim-server's
MUC has no forwardmentions option, so it never forwards a mention
and the run is na. What could run passed: libtacky registers with
the room (muc#register), ignores a <mentions/> while in the room,
and ignores a forged forward not from the room's bare JID. R1/R3
need a server that forwards (Prosody with mod_muc_mention_notifications).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: same rules as ejabberd and
anotherim-server, R5 INFO; prosody accepted max_items=max.

Assisted-by: Claude:opus-5-5
The scenario gates the real-mention step on a forwardmentions room
config field, which Prosody's mod_muc_mention_notifications never
adds, so R1/R3 did not run. R2, R4 and R5 pass as on
anotherim-server. The scenario will always send the real mention.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: same rules pass, R6 again not exercised, clean-up passed.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: same rules as prosody and ejabberd.
ai3b's pre-existing devices kept their lists, bundles and trust, and
the per-id v2 bundle retract worked here (four retracts, all result).

Assisted-by: Claude:opus-5-5
The scenario now runs the real-mention step whatever the room config
form says, reports the forwardmentions field as INFO, judges R1 and
R3 when a forward arrives, and ends na when none does in 30 s.

On prosody the room broadcast the mention but forwarded nothing to
the out-of-room member, so mod_muc_mention_notifications looks
inactive on conference.prosody.asxp.io. R2, R4 and R5 pass again.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0469 0.1.0: libtacky's pin adds
exactly one <pinned/> inside the XEP-0402 <extensions/>, keeps every
other field and foreign extension, unpin removes it, repeats send
nothing, a room without a bookmark is refused, and Bookmark.pinned
follows another device's change. All pass. Markers now name this
XEP's namespace instead of libtacky's own chat-list pins node. The
first run is na for a scenario bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0466 0.1.0: libtacky advertises
the feature, negotiates and follows timers both ways with bodiless
stored negotiations to the bare JID, starts a received message's
timer only once it is read, and ends expired messages. R1-R11 pass.

Documented deviations, not counted as failures: with preserve_history
on (the default) an expired message is retracted but its text kept,
softer than section 3.2's "MUST be discarded"; and the element goes
in the clear beside OMEMO 0.3. Three earlier runs are na for
scenario mistakes.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 12 rules from XEP-0486 0.1.0: as
owner libtacky sets and removes a room avatar by vCard to the room's
bare JID, reports a refusal, follows the room's hash through
disco#info, status 104 and x:update presence, fetches on join, and
handles PNG and SVG. All pass. Minor: on join it sent two identical
vCard gets. The run holds four scenario passes; the first three hit
scenario mistakes and the verdict is from the fourth.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the anotherim-server author's
scenario unchanged: R1-R7 pass; bookmark nodes identical before and
after.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 13 rules from XEP-0490 1.1.0:
libtacky publishes its read point once per forward read, keyed by
the contact's bare JID with the server's stanza-id and the required
publish-options, applies its own device's items only forward, and
ignores older, unknown and forged states. All pass. ai3a's existing
MDS node (3 items, config) was backed up and restored identically.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R11 pass with the same documented deviations.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R12 pass. ejabberd keeps an empty avatarhash after
removal, which section 4 allows. libtacky again sends duplicate
vCard gets.

Assisted-by: Claude:opus-5-5
On anotherim-server ai3a's OMEMO switch for ai3b starts on, so the
first message went out OMEMO 2 encrypted and slixmpp, which reads
plaintext only, timed out; the run is na. The scenario now turns
OMEMO off for the peer when it starts on; its finally block already
puts the switch back.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the anotherim-server author's
scenario unchanged: R1-R13 pass; ai1a's MDS node restored identically.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the fixed scenario: OMEMO was
switched off for the run and back on (explicit) after, messages went
in plaintext, and every rule matched ejabberd and prosody.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged:
R1-R7 pass; bookmark nodes identical before and after.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R12 pass. All three servers keep an avatarhash field
with no avatar, against section 4's SHOULD NOT; libtacky reads it as
none. Duplicate vCard gets again.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 10 rules from XEP-0492 0.2.0.
libtacky stores, resolves and applies notification settings
correctly, but an unscoped set_notification replaces the matching
setting element and drops its <advanced/> child, against section 3
("MUST NOT delete or alter <advanced/> settings they do not
support"). Confirmed in bookmarks.tcl NotifyMerge: the element whose
identity matches is skipped and a new one is built with no children.

Also noted: a scoped write adds no attribute-free fallback (section 3
SHOULD), and `notify get` does not follow the setting though alerts
do. The first run is an na draft.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the anotherim-server author's
scenario unchanged: R1-R13 pass; ai2a's MDS node restored identically.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0511 0.1.1, using only a local
listener: libtacky shows only sender-supplied metadata about a link
in the body, requires a namespaced rdf:about, reads OpenGraph, cleans
and caps the values, never fetches anything a sender named, and as a
sender previews only under link_previews=always and refuses private
addresses before connecting. R1-R8 pass. The wire form and OMEMO
placement of libtacky's own previews need a public https fixture.

Assisted-by: Claude:opus-5-5
New low-confidence scenario with 13 rules from XEP-0513 0.1.0, in
1:1 and a run-unique room: libtacky advertises the feature, sends
person mentions by occupant-id or JID and group mentions alone,
refuses "everyone" in 1:1, counts offsets in code points, reads
mentions, noping and active, and sets mentions_me only when it
should, including role-gated group mentions. All pass. The first run
is na: the draft could not find slixmpp's occupant id.

Assisted-by: Claude:opus-5-5
ejabberd 26.09 has no mention forwarding: the room delivered the
real mention to occupants only. R2, R4 and R5 pass as on the other
servers. Mark ejabberd na for this feature, like anotherim-server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R8 pass; ejabberd passed the RDF/OpenGraph payload
through intact.

Assisted-by: Claude:opus-5-5
Same client-side bug as ejabberd: an unscoped set_notification
drops the rewritten element's <advanced/> (section 3 MUST NOT).
Also refreshes FEATURES.md and matrix.md for the ejabberd na entry
added to XEP-0452.

Assisted-by: Claude:opus-5-5
Same client-side bug on all three servers: an unscoped
set_notification drops the rewritten element's <advanced/>
(section 3 MUST NOT).

Assisted-by: Claude:opus-5-5
Low-confidence runner passes: all 13 rules match anotherim-server.
All three servers' rooms vouch for occupant ids, so the fallback to
a real JID is still untested.

The scenario's clean-up always switched OMEMO on for the peer, which
turned ai1b's and ai2b's explicit "off" on; it now restores the state
it read. Both accounts were set back to off by hand.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R8 pass on all three servers; no URL was ever fetched.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0482 0.1.0 signalling (no media
on this rig, so accept/left could not run). Invites, retracts,
rejects and other-device answers work in Dino's dialect, but
libtacky identifies an invite only by Dino's <invite id>, against
section 3.1 "Using the correct ID": an invite without that attribute
gets no <reject/> at all (origin-id or message id expected), and in
a room the reject names the invite id instead of the room's
stanza-id. Confirmed in groupcall.tcl decline (id taken only from
invite@id). Section 3.1's rules here are lowercase must/should
except one MUST NOT, which passes; counted as fail because the
missing reject breaks interop with 0.1.0 clients.

The R5 carbons FAIL line in this run is a scenario false negative,
fixed after the run.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's own drafts-0 and
sealed-1 docs: one sealed item per chat with private publish-options,
no text in clear anywhere, no replay, moved, urn: or plaintext items
accepted, retraction both ways. R1-R13 pass; R14 (applying another
device's sealed draft) needs a second libtacky device. Drafts are
sealed to 45 own device ids, most stale from earlier runs.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's sealed-1 and
drafts-keys-0 docs and XEP-0477 0.1.0, using the drafts node: seal
keys signed by the OMEMO identity, private publish-options, a single
<sealed> child with 12-byte IV and box_seal keys for every own
device, a signature over the documented message, no secret on the
wire, and every planted or altered item refused while the genuine
one is applied. All 23 rules pass. The scenario ran twice in the
run; the first pass hit scenario bugs.

INFO: signatures verify only with the Ed25519 key of sign bit 1,
libsignal-style rather than strict XEdDSA.

Assisted-by: Claude:opus-5-5
Same section 3.1 id bug as on ejabberd: no <reject/> for an invite
without Dino's <invite id>, and in rooms the invite id instead of
the room's stanza-id. Other signalling passes; accept/left need media.
The own invite row reads joined with no media backend on both servers.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's tags-0 and sealed-1
docs and the HANDBOOK: the tag list is one sealed, signed item with
private publish-options and a newer stamp per change; local-only
actions and refused calls publish nothing; replayed, moved,
re-stamped, unsigned and plaintext plants are refused. R1-R12 pass;
R13-R14 need a second libtacky device. The node was restored byte
for byte.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's chatpins-0 and
sealed-1 docs: one sealed item with private publish-options, no
pinned JID in clear, no-op pins publish nothing, ordering and
renumbering hold, forged, moved, replayed, re-stamped and plaintext
lists are refused, and a newer own-signed list is applied without
publishing back. R1-R15 pass; R16-R17 not checked. Node and settings
restored to the snapshot.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass as on anotherim-server.
Prosody answers forbidden, not not-allowed, for another account's
whitelist node (INFO). Steps text now names the slot account.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: all 23 rules pass in one scenario pass;
ejabberd accepted every planted item, libtacky refused them all.

Assisted-by: Claude:opus-5-5
Same section 3.1 id bug on all three servers. Note: in a chat with
OMEMO on, libtacky sends the call invite unencrypted (plain write in
groupcall.tcl), revealing the call room to the server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R12 pass; node restored byte for byte.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass on all three servers; the
draft text never reached the server.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R23 pass on all three servers. Here
the device's signatures verify under the published OMEMO 2 <ik>
directly; which conversion works depends on the device (INFO).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R15 pass as on anotherim-server;
node and settings restored to the snapshot.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R15 pass on all three servers.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R12 pass on all three servers.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's omemo-chats-0 and
sealed-1 docs and the HANDBOOK: each choice change publishes one
sealed, signed item with private publish-options and a newer stamp;
bad modes publish nothing; replayed, moved, re-attributed,
re-stamped and plaintext lists are ignored; a newer own list is
applied whole without publishing back. R1-R13 pass. ai2a's real
choice for ai2b stayed off+explicit. The first run is na for a
scenario clean-up bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's omemo-link-0 doc, on
the showing side: the link URI carries omemo, ofp and a 32-byte
osec; partial or malformed links are refused; claims for another
device, malformed, wrongly MACed, from a stranger or replayed are
ignored; a valid claim spends the offer and retracts the item
without changing trust; osec never leaves the device. R1-R12 pass.
Completing a link needs a second libtacky. fresh is now false, so
runs keep ai3a's existing OMEMO device.

Leftover: link_device minted one unused XEP-0484 sign-in token on
anotherim-server that the API cannot revoke.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass as on ejabberd; ai3a's
choice for ai3b stayed on+explicit, the node and stamp restored.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's omemo-trust-0,
sealed-1 and drafts-keys-0 docs, with run-only slixmpp-omemo devices
(one of them a verified own device that seals and signs lists):
manual decisions are published sealed and signed with nothing in
clear, blind trust publishes nothing, and only newer, well-formed,
correctly signed lists from a verified own device are applied. 82
PASS lines. All touched nodes and pre-existing devices' trust are
unchanged; libtacky keeps the run's undecided rows in its store. The
first run holds development attempts and is na.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's silent-0 doc: the
<silent/> marker goes directly under <message/> (outside the OMEMO
envelope), clients that don't know it get a normal message, received
silent messages are stored and notify with silent=true, XEP-0334
no-notify counts too, a nested marker does not, and a muted chat
raises no notification. R1-R14 pass. OMEMO and the chat's
notification policy were restored. The first run is na for a
scenario lookup bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's pep-expiry-0 doc:
libtacky writes <expires at/> after the mood or activity value,
clears its own status at the expiry (or at the next sign-in when
offline) and never a replaced item, refuses past and malformed
expiries, and stops showing a contact's status at its expiry. 45
PASS. Two runs both pass; the first's one FAIL line was an
over-strict R7 check.

INFO: when another item replaced ours, libtacky keeps the pending
expiry until the next sign-in instead of dropping it at the expiry
fetch (doc 5.1); and the writer accepts a 5-digit year its own
reader treats as no expiry.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R13 pass on all three servers;
ai1a's choice for ai1b stayed off+explicit.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R6 and R8-R14 pass; R7 (marker
outside OMEMO) skipped since ai2b's OMEMO is off. ejabberd relayed
<silent/> and no-notify untouched.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass, with one scenario FAIL line set aside:
the server answered one publish 4 s late, so the scenario read the
previous stored list for the "P untrusted" check (R6 in that state
is recorded as not checked). Everything else matches prosody; ai3b's
pre-existing devices, lists and bundles are unchanged.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: 45 PASS as on ejabberd, same INFO
findings (R7 pending kept to next sign-in, year 10000 written).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: as on ejabberd, R7 skipped because
OMEMO is off for ai1b; prosody relayed the markers unaltered.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R12 pass as on anotherim-server, no
link completed, trust lists unchanged. link_device again minted a
FAST token (expires 2026-12-07; redacted in the logs).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: same results on all three servers.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: 82 PASS, 0 FAIL, same as prosody; all
touched nodes and pre-existing devices' trust unchanged.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R12 pass on all three servers. On
prosody the stranger's event reached libtacky, which dropped it
itself. Another FAST token was minted (expires 2026-11-28, redacted).

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's schedule-0 doc: the
account's server holds and releases the message (never early, never
sent by libtacky itself), edits and cancels before the time work
from this and another device, forged change headlines are ignored,
a message released while signed out lands in history once, and with
OMEMO on the server holds only ciphertext. R1-R15 pass.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's storage-0 doc.
anotherim-server offers no personal storage service, so the run is
na. What libtacky shows alone passed: it discovers services, falls
back to the plain upload service for personal slots, refuses
`file keep` without sending a command, ignores a forged storage
advertisement, and never advertises the feature itself. No file was
uploaded (slots only).

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's pins-0 and sealed-1
docs, in a 1:1 chat and a run-unique room: each pin change is one
sealed, signed item with private publish-options, ids follow the
doc (stanza-id, origin-id, room stanza-id), no pin data is readable
by the server or the contact, replayed, re-stamped, moved, foreign
and plaintext lists are refused, and a newer own list is applied at
connect. R1-R18 pass. Two earlier runs are na: the scenario's R10
counted traffic mislabelled by the shared slixmpp logger.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against ProcessOne's proto-XEP
(fluux-messenger docs/XEP-POLL_PROTOCOL.md, 2026-08-24) and
libtacky's poll-fluux-0 doc, in 1:1 and a run-unique room: poll
shape and fallback, single and multi votes as reactions (room votes
by stanza-id, bound to occupant-id), deadlines, creator-only closes
with checked tallies, the results IQ both ways, and forged or
malformed votes and closes refused. R1-R27 pass.

INFO: libtacky's <fallback/> has no <body/> child, unlike its own
doc and Fluux's example (same meaning under XEP-0428). Two draft
runs are na for scenario bugs.

Assisted-by: Claude:opus-5-5
ejabberd offers no scheduler; libtacky reports it unsupported and
sends nothing, as schedule-0 section 3 says. Mark ejabberd na. The
scenario's no-scheduler branch still prints a spurious R15 FAIL.

Assisted-by: Claude:opus-5-5
No storage service on ejabberd either; libtacky-only checks pass.
Mark ejabberd and anotherim-server na for this feature.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's contact-0 doc: cards
go out as <contact/> with a XEP-0428 whole-body fallback and inside
OMEMO when it is on; bad sends are refused or trimmed; received
cards are stored with invalid JIDs dropped, long names cut, extra
phones ignored and hostile names kept as plain text; sending a card
changes no roster and sends no subscription. R1-R15 pass. The first
run is na: the scenario miscounted a sign-in roster event.

INFO: a received 5000-character phone is dropped (doc gives no
receiver limit); a domain-only JID is kept; of two cards the first
counts.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the ejabberd author's scenario
unchanged: R1-R18 pass; node and settings restored.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the prosody author's scenario
unchanged: R1-R27 pass, room checks included.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R8 and R10-R15 pass; the OMEMO step
was skipped (OMEMO off for ai1b). Prosody turned a &#10; in an
attribute into a space on the way (INFO, server-side).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R18 pass on all three servers. The
runner switched ai3b's OMEMO off for the run and back on+explicit,
since the scenario expects plaintext; the scenario should do this.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: same as prosody, OMEMO step skipped;
rosters unchanged, no subscriptions.

Assisted-by: Claude:opus-5-5
Prosody offers neither a scheduler nor a personal storage service;
libtacky reports schedule unsupported and sends nothing, and the
storage checks libtacky can make alone pass. Mark prosody na for
both. Prosody re-stamps a spoofed from instead of closing the
session (INFO).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R27 pass on all three servers;
ai3b's OMEMO switch back on+explicit after the run.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against ProcessOne's MUC/Sub page: in a
run-unique room libtacky subscribes without joining, receives
messages, subject and subscriber changes through event wrappers,
sends without joining, lists subscriptions and subscribers, drops
forged events from non-room JIDs, and unsubscribes cleanly. R1-R12
pass; R13 (MAM resync) not checked. The first run is na for
scenario bugs.

INFO: in a semi-anonymous room ejabberd omits subscriber JIDs (its
page allows it), and libtacky then lists no subscribers and emits no
SubscriberChanged.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's service-0 doc:
service rows from the chat's own peer are stored with kind, actor
and their own attributes, never unread, never the chat list's last
message; calls become call records; spoofs from own other devices,
room occupants, forged carbons and groupchat from a bare JID are
refused; clients without support see the English body.

Fails on one rule: service-0.md section 4 says a service row is
"never notified", but libtacky raises notify <Notify> for each one.
Confirmed in notify.tcl OnNew, which skips only content types call
and omemo_session. The first run is na for a scenario lookup bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's spans-0 doc: received
link and spoiler spans are read with the " (url)" fallback stripped
and offsets shifted in code points; unsafe or oversized hrefs, bad
offsets and unknown kinds are dropped; edits and replies handle
spans; libtacky sends no spans and fetches nothing. R1-R17 pass.

INFO worth raising: the stripped fallback range is not checked to be
" (href)", so a sender can hide arbitrary text from libtacky users
(other clients still show it), and a childless spans fallback drops
the whole message; forwards and quotes lose the hidden URL.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R12 pass, same as ejabberd.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's live-location-0 doc:
a received live share is one location row that XEP-0308 corrections
from the sharer move and a stop ends, with no revision, unread or
notification; invalid until, heading and coordinates are refused;
corrections from anyone but the sharer are ignored; libtacky sends
no live shares, as its row says. R1-R10 pass. The first run is na
for scenario mistakes.

INFO, undecided by the doc: a correction after a stop restarts the
share; past `until` live_stopped stays unset; an out-of-range
correction becomes a text edit whose body disagrees with the place;
a correction of the peer's share from our own other device is stored
as a new outgoing live row.

Assisted-by: Claude:opus-5-5
Every Peer hung its _Wire handler on slixmpp's single module logger,
so with two Peers in one process (an own-device session beside the
peer) each log got both streams under the wrong acc, which misled
normalize and made two pins draft runs fail. Filter records to the
Peer's own XMLStream, as XEP-0308, XEP-0280 and a few urn:tacky
scenarios already did locally. Checked with two Peers on ejabberd:
each log holds only its own bind.

Assisted-by: Claude:opus-5-5
Same client-side bug: every service row raises notify <Notify>,
against service-0 section 4. Both servers bounce the type=groupchat
spoof to a bare JID, so step 5c tests nothing on either.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R17 pass with the same INFO findings;
the server passed every spans and fallback payload through unchanged.

Assisted-by: Claude:opus-5-5
Same client-side bug on all three servers: every service row raises
notify <Notify>, against service-0 section 4.

Assisted-by: Claude:opus-5-5
urn:tacky:live-location:0: R1-R10 pass with the same INFO quirks as
ejabberd; every share stopped. urn:xmpp:mucsub:0: Prosody's MUC has
no MUC/Sub, so na; mark prosody na for it.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R10 pass on all three servers with
the same INFO quirks; every share stopped.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0055 1.3. anotherim-server has
no search service, so slixmpp answered as one on its own JID: libtacky
asks for the fields first, sends only offered legacy fields, prefers
the data form with FORM_TYPE, reads reported columns, treats empty
results as no match and errors as errors, and keeps a hostile
508-row result intact. R1-R8 pass. The scenario ran twice in the
run; the first pass had scenario mistakes.

INFO: malformed result JIDs are passed through unchecked; markup
inside a legacy field is truncated at the first child.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R17 pass on all three servers with
the same INFO findings; ejabberd relayed every payload unchanged.

Assisted-by: Claude:opus-5-5
New low-confidence scenario against libtacky's push-hint-0 doc:
<no-push/> goes, outside encryption and kind only, on album source
messages and OTR stanzas and on nothing else (plain messages,
captions, polls, receipts and markers included); a received hint
is ignored; ejabberd relays it unchanged. R1-R9 pass. Whether a push
service honours it needs mod_apush and a registered target, which
this rig lacks; the OMEMO caption hint was not exercised.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R8 pass against slixmpp as the search
service; ejabberd has no directory of its own either.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0047 2.0.1 under Jingle: IBB
works both ways (block-size honoured, strict base64, one iq in
flight, close after the last block) and every hostile input is
refused with the right error. But when the receiver answers one of
libtacky's data blocks with an error, libtacky sends no <close/>
and stalls 60 s (section 2.2 MUST close). Confirmed in the source:
ibb.tcl Callback passes `err <text>` as two arguments while
jinglefile.tcl OnIbbSent {sid n result} takes one, so the call fails
with "wrong # args" and the error is lost; OnIbbOpened has the same
signature. Also, as receiver, an out-of-sequence block ends the
Jingle session without an IBB <close/>. The first run is na for
scenario bugs.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0065 1.8.2 as libtacky uses it
under Jingle: proxy discovery, DST.ADDR = SHA1(sid+requester+target)
in both roles, proxy activation only by the requester, its own
streamhost refusing anything but a correct CONNECT, and never
dialling the planted 127.0.0.1, 0.0.0.0 or example.invalid
streamhosts. Standalone SI bytestreams are refused, as its row says.
R1-R10 pass. The first run is na for scenario bugs.

Assisted-by: Claude:opus-5-5
Low-confidence runner passes: XEP-0055 R1-R8 against slixmpp as the
search service (prosody has none); push-hint R1-R9, prosody relays
<no-push/> unchanged, no push target registered, OTR sessions
forgotten.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0080 1.10.0: libtacky publishes
to its own PEP node under a whitelist, readers added and removed
are honoured, a settled stop sends one empty <geoloc/>, contacts'
locations are read with moves and stops, and hostile input does not
break it.

Fails on a privacy race (sections 4.1 and 8): `geoloc stop` issued
right after `set_allowed []` and `set_allowed [ai2b]` is followed by
a republish of the old location, so the location is shared again
after the user stopped it and libtacky itself reports it active. In
geoloc.tcl, AfterListChange republishes whatever Stored holds when
the list sync completes; the exact path that refills Stored after
the stop was not traced. Two earlier runs are na for scenario
crashes.

Assisted-by: Claude:opus-5-5
Same two section 2.2 close bugs as on prosody (no IBB <close/> after
a data error as sender, none after an out-of-sequence block as
receiver).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: R1-R9 on all three servers. Here R10 was
also checked by hand with OMEMO on: a caption under OMEMO carries
kind=caption, a reply caption carries none. anotherim-server offers
the apushd push component; no target was registered.

Assisted-by: Claude:opus-5-5
libtacky passed R1-R10, but the run is recorded fail because
transfer T2 stalled in Prosody 13.0.7's mod_proxy65: a single
7000-byte write through the proxy delivered 4096 bytes and then
nothing. The runner reproduced it without libtacky (two raw SOCKS5
clients; 1000-byte chunks get through). This is a Prosody bug, not
libtacky's; the scenario should write in pieces of at most 4096
bytes or report the proxy's behaviour as a server INFO.

Assisted-by: Claude:opus-5-5
Same client-side privacy race as on ejabberd: a stop right after
set_allowed calls is overtaken by a republish of the old location,
which new members can then read. Everything else passes.

Assisted-by: Claude:opus-5-5
On Prosody the owner receives its own node <delete/> event, which
libtacky takes as "sharing stopped": `set_allowed []` then ends
sharing for good, and a later `set_allowed [peer]` marks the list
synced after an item-not-found affiliations read, never making the
peer a member or republishing. The allowed reader cannot read the
location and gets forbidden on the stop. Nothing leaks, but sharing
silently stops, against libtacky's own row. The ejabberd stop race
cannot show here for the same reason.

Assisted-by: Claude:opus-5-5
Same client-side section 2.2 close bugs on all three servers.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass on every libtacky check that needs no
proxy. ejabberd's mod_proxy65 advertises its streamhost at
127.0.0.2:7777 and the public port closes without a SOCKS5 reply, so
the proxy parts could not run (libtacky correctly answered
candidate-error). The test server's mod_proxy65 hostname/ip need
fixing.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0103 0.4.1 (Deferred), as
libtacky uses <url-data/>: only as a XEP-0447 file-sharing source.
What it sends has a valid target and no sid; received url-data
becomes the attachment url without any fetch; malformed targets,
bare url-data and SI-style iqs are handled safely. Checked rules
pass; the SI and scheme rules libtacky leaves out are not checked.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0100 1.0 with slixmpp acting as
a gateway at a user's full JID: address translation, refusals of
registration to user and own-domain JIDs, ignoring rosterx items
and forged roster pushes all pass.

Fails: after an ad-hoc `register` command completes at a user's JID
that claims a gateway identity, libtacky adds that full JID to the
roster and sends it a subscribe (section 6.1: a gateway address
SHOULD be a hostname only; libtacky's own GatewayTarget rule agrees).
The path is adhoc GatewayRegistered -> roster IfGateway, which asks
the JID's disco and does not reject a JID with a node. With
pre-approval (ejabberd) the code would also send `subscribed`.

INFO: libtacky sends no reply at all to a forged roster push (RFC
6120 expects one to every IQ set).

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0107 1.2.2: libtacky publishes
one mood value (with text and specific mood) to its own PEP node,
clears it with an empty <mood/>, keeps it out of presence, follows
moods from its other devices, and reads contacts' moods with
unknown values as undefined, foreign elements ignored and long text
cut. R1-R19 pass. Both mood nodes and the presence status were
restored. The first run is na for a scenario parser bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0108 1.3: libtacky publishes one
category with an optional specific and text to its own PEP node,
clears with an empty <activity/>, keeps it out of presence, follows
its other devices, and reads contacts' activities with foreign
namespaces ignored, unknown categories dropped and long text cut.
R1-R18 pass. Activity nodes and presence restored.

INFO: libtacky ignores <undefined/>, which XEP-0108 1.3 added to the
schema; with two <text/> it keeps the last.

Assisted-by: Claude:opus-5-5
Low-confidence runner passes with the authors' scenarios unchanged;
nothing fetched, mood nodes and presence restored.

Assisted-by: Claude:opus-5-5
Same libtacky bug as on prosody: after an ad-hoc register at a
user's full JID claiming a gateway identity, libtacky sets that full
JID in the roster and sends subscribe (no `subscribed` went out,
despite pre-approval support). On ejabberd, which keeps one row per
bare JID, the full-JID roster set replaced ai2a's existing ai2b
contact: the `from` half was lost and ai2b stopped seeing ai2a's
presence, and the scenario's remove did not undo it. The runner
restored both rosters by hand to byte-identical snapshots after the
run.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged;
activity nodes and presence restored.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0118 1.3.0. Neither run reached
a verdict: the first stopped when a 100 kB hostile tune took 21 s to
push (fields cut to 3000 characters since), the second when both
client connections were reset at 11:58:59 UTC without a server
restart. What ran passed: publishing all fields to the own PEP
node, no length or rating for streams, the 10 s rate limit, API
refusals, clear, other-device tunes, no tune in presence, and
reading contacts' tunes with bad numbers read as 0. A rerun follows.

Assisted-by: Claude:opus-5-5
Low-confidence pass of the new scenario: publishing, the 10 s rate
limit, refusals, clear, reading with bad numbers, long fields and
unknown elements, hostile URIs kept as data with nothing fetched,
tune_notify and persistence. The scenario now uses only the local
listener for URIs. Tune nodes and presence restored.

Assisted-by: Claude:opus-5-5
Low-confidence runner passes; both now pass on all three servers.
Nothing fetched, activity nodes and presence restored. ai2a's server
roster is back to normal after the XEP-0100 repair.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass on all three servers. One scenario FAIL
line set aside: status get fell back to ai3b's stale XEP-0108
activity, left in libtacky's store because the server deleted that
node without a delete notice (notify_delete false).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged;
nothing fetched, tune nodes and presence restored.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0147 with RFC 5122: account
parse_uri reads all 16 registered query types, refuses hostile
links (control characters, oversize values, bad UTF-8), never acts
on a link by itself, and ?message and ?join work on the wire.

Fails on address parsing (RFC 5122 section 2.8.1 MUSTs): a
percent-encoded `?` inside the address cuts it short after
decoding, so `xmpp:x%3Fy@example.invalid?subscribe` names the domain
`x`; a link can show one address and name another for ?subscribe
or ?invite. A port in the address and a `?` in a domain are also
accepted. Evidence is the API results (local parsing, no stanzas).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass; passes on all three servers.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0201 1.0 with RFC 6121 5.2.5:
thread ids are fresh and JID-free, replies and corrections keep the
thread, child threads carry parent, received threads (huge, hostile,
reused across chats) are kept apart and survive. One small fail:
`message send` with thread_parent equal to thread sends
`<thread parent='X'>X</thread>`, which RFC 6121 5.2.5 forbids (MUST
differ); BuildMessageStanza does not compare them. Only when the
host asks for it. The first run is na for a scenario bug.

INFO: libtacky adds <thread/> only inside threads, not on every
chat message (section 3.1 RECOMMENDED, 4.1 SHOULD).

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0158 1.0.1 with slixmpp as the
challenger: challenges are raised with the right id, sender and
form, answers go as an IQ-set to the challenger, refusals and spent
challenges are handled, malformed challenges are dropped, and
nothing is fetched.

Fails on the media: an inline BoB image whose bytes do not hash to
its cid is passed to the host as the challenge image, against
libtacky's own XEP-0231 rule. captcha.tcl OnMessage only checks
that the base64 decodes; adhoc.tcl uses ::tacky::bob::check (strict
base64, 1 MiB cap, hash, raster types only). So captcha media also
has no size cap or type filter: an image/svg+xml with onload reached
the host. The first run is na for scenario bugs.

INFO: challenges for stanzas libtacky never sent, or naming another
`from`, reach the host with known=false (section 3.1.3 SHOULD
ignore); cancel sends no <not-acceptable/> (documented).

Assisted-by: Claude:opus-5-5
Same client-side parse_uri bug (decoded %3F cuts the address).
?message and ?join behave correctly on the wire; roster and
bookmarks unchanged.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0232 0.3: libtacky shows the
softwareinfo form only when software_info_public is 1, with software
and version and no OS or host details; both caps hashes cover it;
a peer's form arrives through caps unchanged; icon URIs are never
fetched; hostile forms are survived. R1-R8 pass.

INFO: the tackyd-json v1.2.0-rc2 build reports software_version 0.1
and caps node https://tacky.example, as ::tacky::version is not set
at build time.

Assisted-by: Claude:opus-5-5
Same client-side bugs as on prosody and ejabberd: parse_uri cuts the
decoded address at %3F, and a thread parent equal to the thread is
sent. Roster, bookmarks and OMEMO switch unchanged.

Assisted-by: Claude:opus-5-5
Same client-side bugs: captcha inline image passed without the BoB
hash, size and type checks; thread parent equal to the thread sent.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged;
settings read back as they started, nothing fetched.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0277 0.6.5: posts with valid
Atom entries, comments nodes, edits keeping ids, replies, reposts,
retracts with notify, contacts' posts and comments, the section 9.1
author check, access models, and hostile items with nothing fetched
all pass.

Fails on section 4.1: the urn:xmpp:microblog:0 node libtacky creates
has pubsub#notify_retract 0 on ejabberd (MUST be true);
libtacky's publish-options set only access_model, persist_items and
max_items, while its create_feed already sets notify_retract. INFO:
send_last_published_item stays on_sub_and_presence (SHOULD never);
html content with <script> is passed verbatim in `content`. Two
earlier runs are na for scenario bugs.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0298 0.2. Coin lives inside a
Jingle call and this headless build has no media backend, so the
call part could not run. Outside a call libtacky advertises coin:1
and answers every Coin IQ for an unknown session with an error,
taking nothing, including hostile documents. Prosody bounces the
XEP's two-payload IQ form with bad-request (RFC 6120 one child).

Assisted-by: Claude:opus-5-5
Low-confidence runner pass: everything checkable works. The node's
notify_retract reads 1 here only because of Prosody's node default;
libtacky's publish-options still omit it, as on ejabberd. Prosody's
pubsub service forbids users creating comments nodes, so the
comments part could not run; libtacky publishes without the
replies link and logs why. The scenario's two FAIL lines come from
that server policy and were set aside.

Assisted-by: Claude:opus-5-5
XEP-0158: same client-side captcha media bug on all three servers.
XEP-0232: passes on all three servers; settings restored.

Assisted-by: Claude:opus-5-5
No media backend, so no call. ejabberd delivers the two-payload IQ
prosody bounced; libtacky refuses every no-call Coin IQ correctly.
The 200 kB document was throttled by ejabberd's c2s shaper past the
scenario's timeouts (scenario should send a smaller one).

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0358 0.4 as libtacky uses it,
the XEP-0447 source of a direct file: outgoing announcements carry
the initiator's full JID, the sid and a file description; an
incoming one lands on its session's row and the file arrives over
IBB; foreign, second-resource and malformed announcements do not
land; <start/> is refused; nothing is sent to foreign JIDs.

Fails: a received <jinglepub/> whose `from` is not a valid JID
(section 2.1 MUST) throws "Invalid JID" in ExtractSfsAttachments
(message.tcl ~11132, `jid bare $pubFrom` unguarded), so the whole
message is dropped silently, and again on every MAM catch-up. The
first run is na for a scenario bug.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0377 0.4.1 over XEP-0191.
ejabberd does not advertise urn:xmpp:reporting:1, so libtacky
rightly blocked without a <report/>; blocking, pushes, delivery
refusal and unblocking all work. Blocklist and roster unchanged.

Assisted-by: Claude:opus-5-5
XEP-0277: same notify_retract 0 on the microblog node as ejabberd;
comments nodes work; all nodes removed. XEP-0298: no media; the
two-payload IQ is delivered and refused correctly, the big document
is throttled by the shaper.

Assisted-by: Claude:opus-5-5
Same client-side message loss: an invalid jinglepub `from` throws
Invalid JID and the whole message is dropped; ejabberd delivered
and archived it normally.

Assisted-by: Claude:opus-5-5
No urn:xmpp:reporting:1 here either; blocking works and the list
and roster are unchanged. libtacky's row says it would set the
stanza-id by= to the reported JID rather than copy the received
one (XEP-0377 section 6 says copy); untestable until a server
advertises reporting.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0364 0.3.2 with an OTR v3 peer
written for the scenario (no OTR library in the ai shell; potr is
v2 only): libtacky completes the key exchange (MAC and DSA signature
verify, fingerprint matches), data flows both ways, either side can
end, SMP framing works, broken data is refused with an OTR error,
and nothing goes out in clear before encryption. All 13 OTR stanzas
carry EME, no-copy, no-store, no-permanent-store, private and
no-push; none was archived or carbon-copied. SMP completion is not
checked. Three earlier runs are na for scenario bugs.

The stranger check removed and re-added ai1b in the roster; it is
dropped from the scenario, and both rosters were read back at
`both` with no flags.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0392 1.0.1: libtacky's
`color of` and `color batch` match a reference implementation on all
five test vectors, 22 tricky texts (sent as escapes and raw UTF-8),
a saturation/lightness grid and batch JID preparation; 105 checks
pass. Local computation only, so the feature's kind is now server.

INFO: a JID with an NFD accent is not NFC-normalised before hashing
(section 6.2), a gap libtacky's RFC 7622 row already documents.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0394 0.3.0 as libtacky's row
claims it (read only): spans, bcode and bquote apply over code-point
offsets; unknown markup and bad offsets are ignored without breaking
the message; overlaps merge and 1000 spans cap at 64; markup combines
with XEP-0393, /me, reply fallbacks and corrections as the row says;
libtacky sends no markup. The run holds two passes; the first had a
scenario expectation bug.

INFO: bcode's language attribute is not kept.

Assisted-by: Claude:opus-5-5
OTR v3 works end to end and nothing OTR was archived or
carbon-copied, but in one session `otr <State>` reported the secure
session id as "Inf" instead of 02802368162e5437. otr.tcl:278 builds
it inside `expr {... ? "" : [string tolower [binary encode hex ...]]}`,
and expr turns a hex string like "02802368162e5437" into a number
(2.8e5446 -> Inf); all-digit ids would lose leading zeros. The ssid
is what users compare out of band, so a wrong one defeats that
check. The fingerprint on the line above uses the same pattern.
Random per session, so prosody's run did not hit it.

Assisted-by: Claude:opus-5-5
XEP-0358: same client-side message loss on an invalid jinglepub
from, on all three servers. XEP-0377: prosody has no
mod_spam_reporting either, so no <report/>; blocking works, list and
roster unchanged; prosody drops a blocked sender's message silently.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass with the author's scenario unchanged;
the 45 kB stanza took 4.5 s through the c2s shaper.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass; passes on all three servers.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0463 0.2.0. Prosody's MUC does
not advertise versioning, so only the negative side ran: libtacky
sends no <mav/> to such rooms, falls back to XEP-0045 lists, and
ignores unsolicited <mav/> from a fake room, hostile lists
included. The versioned-join part is written but has not run yet.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0470 0.2.0 on microblog posts:
node naming, access and subscription, react with noticed and
reactions (dedup, cap at 32), keeping unknown children, others'
items, no node creation on someone else's post (MUST NOT holds),
hostile ids and huge items, and cleanup all pass.

Fails (low severity): an <attachments> element in a foreign
namespace is read as a real attachment (sections 4.1.1 and 4.5 say
it is qualified by urn:xmpp:pubsub-attachments:1). INFO: libtacky
sets no pubsub#max_items on the node it creates; ejabberd applies
max_items=1 per publisher so it works here. The first run is na for
scenario mistakes.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0426 0.3.0 across mentions,
markup, styling and reply fallbacks with emoji, ZWJ and flag
sequences, NFD, and surrogate-looking text: every offset libtacky
reads or writes counts code points over the unnormalized body.

Fails (section 2): `message send` drops XML-forbidden characters
from the body (XmlText) but checks and sends the caller's mention
offsets unchanged (CheckMentions), so the wire reference points at
the wrong text; edit and caption paths do the same. INFO: a reply
fallback end in UTF-16 units is also accepted (reply.tcl comment).

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0439 0.1.0: responses and
actions read in order with exact ids, fallback lines stripped,
chosen responses sent as plain bodies, actions pressed as bodiless
<action-selected/>, corrections followed, and hostile input capped
and cleaned. All 13 rules pass. Two earlier runs are na because the
scenario changed after them.

INFO: a chosen response carries no xml:lang (section 5.2 says copy
it); duplicate ids are kept; headline messages from a contact are
stored with pressable actions.

Assisted-by: Claude:opus-5-5
Low-confidence runner pass, negative side only: ejabberd rooms do
not version affiliations either. The 116 kB push arrived after the
checks (c2s shaper), so the huge-list case is only partly checked.

Assisted-by: Claude:opus-5-5
New low-confidence scenario for XEP-0488 0.1.0. Prosody's rooms do
not offer tokens, so the round trip could not run. What could: the
request, list and revoke stanzas, how refusals are reported, joining
by a ?join;password= link (another room's or a forged token is
refused by the room), and malformed links refused before sending.
The first run is na as a draft.

INFO: libtacky asks rooms it has not joined without checking disco
first (its row says such rooms are not asked); a raw space or a
1100-character token is accepted; a %00 token fails with an encoding
error rather than a clean refusal.

Assisted-by: Claude:opus-5-5
str.splitlines() also breaks at U+0085, U+2028 and friends. libtacky
logs such characters raw inside its JSON lines (a MAM replay of a
XEP-0439 action id with NEL did it), so wire() split a line mid-string
and the XEP-0463 run on anotherim-server aborted with JSONDecodeError.

Assisted-by: Claude:opus-5-5
Pass: two real OTR v3 AKEs, ssid and fingerprints match the peer's,
data, TLV 1, SMP framing, hostile input and the wire audit as on
the other servers. Both ssids had hex letters, so the ssid-as-Inf bug
seen on ejabberd could not show. MAM and carbons skip the OTR stanzas,
as on ejabberd.

Assisted-by: Claude:opus-5-5
Finished na: common.wire() split a log line at a raw U+0085 from a
MAM replay and raised JSONDecodeError (fixed in 2633218). Kept for
the record; the rerun follows.

Assisted-by: Claude:opus-5-5
Pass, negative side only: the MUC service lacks
urn:xmpp:muc:affiliations:1, so libtacky sends no <mav/> and ignores
unsolicited pushes. The runner overrode the scenario's na: the last
ping timed out at 15.3 s behind a 117 KB push held by the c2s shaper,
with no drop or restart, as on ejabberd.

Assisted-by: Claude:opus-5-5
Fail: offsets are code points both ways, but message send drops
characters XML forbids (U+0001, U+FFFE) after the mention offsets
were set, so the mention lands 2 characters off (XEP-0426 §2). Same
as on anotherim-server.

Assisted-by: Claude:opus-5-5
Pass: R1-R13, same as on anotherim-server.

Assisted-by: Claude:opus-5-5
Fail: libtacky creates the attachments node without pubsub#max_items,
so Prosody's PEP default of 1 evicts libtacky's own reaction when a
contact attaches (XEP-0470 §4, one item per JID). The scenario then
aborted on Prosody refusing a non-owner retract, so R3, R13, R14 and
the foreign-namespace case did not run here.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R17 from XEP-0472 0.2.1. Pass:
create_feed config, feed discovery, Atom posts with comments and
attachments nodes, edits, reply and repeat, following with publisher
verification, retract cleanup and hostile items all behave. The gallery
profile is not implemented in libtacky and was not checked.

Side notes: the attachments node of an open feed is created with
access_model=presence (XEP-0470 §4.1.1 says SHOULD match), and comment
nodes lack itemreply=publisher, so comments read back unverified.

Assisted-by: Claude:opus-5-5
Fail, same as anotherim-server and prosody: message send strips
XML-forbidden characters after mention offsets were set (§2).

Assisted-by: Claude:opus-5-5
Pass: R1-R13. ejabberd delivered the forged carbon, so the check
that libtacky ignores it really ran.

Assisted-by: Claude:opus-5-5
Pass on what can run: ejabberd's MUC answers every token request
with service-unavailable, so no round trip, as on prosody.

Assisted-by: Claude:opus-5-5
Fail, same as the ejabberd author run: an <attachments> element in a
foreign namespace is read as an attachment (§4.5). All other rules
ran and passed, including those prosody's run never reached.

Assisted-by: Claude:opus-5-5
Pass on what can run; no server of the three hands out tokens.

Assisted-by: Claude:opus-5-5
Pass: R1, R3-R16, no difference from ejabberd.

Assisted-by: Claude:opus-5-5
na: prosody's pubsub service lists create-nodes but refuses a user's
<create/> with forbidden, so no feed can be made. libtacky's request
and its fallback without max_items were correct and it reported the
error cleanly. Only R1 (disco) ran.

Assisted-by: Claude:opus-5-5
New scenario and feature file. Pass on libtacky's urn:tacky:sealed:1
path (drafts and other private PEP state): urn:xmpp:pte:0 advertised,
every item wrapped with by=own bare JID and the right type (§3.1),
forged by or wrong type refused, no clear text anywhere. The OMEMO 2
PTE path has no public method and was not checked.

Assisted-by: Claude:opus-5-5
Joining slixmpp's bare JID as a "room" makes libtacky store
muc_features for that JID for good. omemo IsRoom then treats the 1:1
contact as a room, so chat_state reads room_not_eligible and the
user's explicit OMEMO ON for ai3b was overridden in every later run.
libtacky has no API to forget the row; the three test stores were
fixed by hand. Keep the part behind XEP0463_FAKE=1 for throwaway
stores.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R14 from XEP-0483 0.2.0. Pass:
invite advertised, the meeting request is well formed and Prosody's
service-unavailable reaches the caller, and invite, accept, reject,
retract and left go out and are handled both ways. Prosody offers no
meeting initiation, so a successful Meeting answer was not checked.
The invite carries no audio/video attributes (§6.1 lowercase should).

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R13 from XEP-0491 0.1.4. Pass
in 1:1 chat both ways: a fresh thread per app, the .xdc as SIMS,
XEP-0447 and OOB, updates in the app's thread with info, document,
summary and a JSON payload, realtime data as a no-store headline, and
received apps, updates and realtime data stored or not as libtacky's
row says. The first run is kept as na: the scenario missed the
bodiless realtime headline (slixmpp's message event needs a body).

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R14 from XEP-0503 0.3.0. na:
Prosody's pubsub component refuses node creation to users, so no space
can exist there. What could be checked passed: libtacky's create sends
the complete §3 node configuration and list asks for subscriptions and
affiliations. The scenario covers config read-back, rooms and foreign
items on servers that allow creation.

Assisted-by: Claude:opus-5-5
Pass, same as prosody: pte:0 advertised, drafts item wrapped with
by=own bare JID and type sealed:1, forged items refused, no clear text.

Assisted-by: Claude:opus-5-5
Pass, same as prosody: ejabberd offers no meeting initiation and its
service-unavailable reaches the caller; invitations work both ways.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R10 from XEP-0501 0.2.0. Pass:
both features and +notify advertised, a story is one enclosure on the
stories PEP node with access_model presence, malformed and expired
stories of a contact are not told, own stories are listed, deleted
and retracted on expiry. pubsub#type and item_expire are unset, as
libtacky's row admits. The entry lacks atom:author (RFC 4287 §4.1.2).

Assisted-by: Claude:opus-5-5
Pass, R1-R13, same as ejabberd; only the upload host differs.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R4 from XEP-0507 0.1.0. na:
the headless tackyd-json build has no media backend (no webrtc or
datachannel library), so libtacky refuses every RTP session-initiate
and no call carries a category. The scenario covers incoming
categories and set_screen_share once a backend exists.

Assisted-by: Claude:opus-5-5
Pass, same as prosody and ejabberd.

Assisted-by: Claude:opus-5-5
na: the scenario requires OMEMO off for the peer and slot 3 has it on,
so it stopped before any step. The server offers no meeting
initiation, like the other two.

Assisted-by: Claude:opus-5-5
na: the scenario requires OMEMO off. A probe in the same run shows the
documented OMEMO behaviour: send_app goes out inside one OMEMO 2
envelope with the .xdc uploaded encrypted, and send_update and
send_realtime are refused before anything is sent (WEBXDC ENCRYPTED).

Assisted-by: Claude:opus-5-5
Pass, the first real path: public and private spaces created with the
full §3 config and read back as owner, rooms added and removed as
XEP-0402 conference items, join and leave as subscribe/unsubscribe,
another account's space listed with only its conference as a room.
The first run is kept as na: the scenario waited for <Changed> after
the join answer, but libtacky emits it before; fixed.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules from XEP-0518 0.1.0, slixmpp as
the charging bot. Fail on one SHOULD: payment get_invoice sends
<get-invoice/> without first checking the target for #invoice-request
(§4.4), although libtacky's HANDBOOK says it goes to an entity
advertising it. The rest of the payer side passes: InvoiceRequired,
pay with the proof, pending retry, re-refusal, receipt, expiry, dismiss.

Assisted-by: Claude:opus-5-5
na: the scenario stopped before any step because its OMEMO pre-flight
required slot 3's state (on), and slot 1 keeps it off. The gate now
needs an explicit choice at start and the same state at the end.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules from docs/protocol/animation-0.md.
Pass: the hint makes SFS and SIMS video/mp4 and image/gif shares kind
animation, wins over the inline disposition, is ignored on other
types, and libtacky's own GIF shares carry none.

Assisted-by: Claude:opus-5-5
Fail: with ai3a's OMEMO for ai3b explicitly on, every meeting message
(invite, retract, accept, reject, left) goes out in the clear with no
error. meetings.tcl Send writes the stanza with $client write,
bypassing the OMEMO send path; the HANDBOOK says an OMEMO chat fails
closed. The meeting URL is the bearer secret (XEP-0483 §8).

The scenario now records the OMEMO state at start, branches on it and
requires the same state at the end, instead of aborting unless off.

Assisted-by: Claude:opus-5-5
Pass: send_app goes out only inside an OMEMO 2 envelope, and
send_update and send_realtime are refused before any stanza, as
documented (WEBXDC ENCRYPTED). Receiving from a clear peer passes.
The scenario gained the same OMEMO-state gate and an OMEMO branch.

Assisted-by: Claude:opus-5-5
Pass with the fixed OMEMO gate: R1-R10 as on anotherim-server, upload
through Prosody's file_share and the self-expiry retract included.

Assisted-by: Claude:opus-5-5
Fail on the same SHOULD as ejabberd: get_invoice without a disco
check for #invoice-request (§4.4). The rest of the payer side passes.

Assisted-by: Claude:opus-5-5
Pass: R1-R10, same as anotherim-server and prosody.

Assisted-by: Claude:opus-5-5
New scenario and feature file. na: libtacky ships only the design and
API stubs. What could be checked passed: the namespace is not
advertised, every method answers 'not implemented', nothing is sent
or published, and gateway iqs from a peer get service-unavailable.

Assisted-by: Claude:opus-5-5
Pass: every rule as on prosody; libtacky's own GIF share carries no
hint.

Assisted-by: Claude:opus-5-5
na: the scenario required OMEMO off (slots 1 and 2) and stopped
before any step on slot 3. The gate now needs an explicit choice at
start and the same enabled/explicit/reason at the end.

Assisted-by: Claude:opus-5-5
na: the scenario requires OMEMO off and stopped before any step. With
OMEMO on, slixmpp can't read libtacky's bodies, so the scenario needs
an OMEMO branch matching by id; from the code (payment.tcl Resend via
message resend without -plaintext), paid retries should be encrypted.

Assisted-by: Claude:opus-5-5
Pass: R1-R7 as on prosody and ejabberd. With OMEMO on, libtacky's own
GIF goes out inside an OMEMO 2 envelope and is uploaded encrypted;
nothing of the share is outside. The scenario printed fail only
because its end check compared the whole chat_state, whose derived
profiles field changed; it now compares enabled/explicit/reason.

Assisted-by: Claude:opus-5-5
New scenario and feature file, rules R1-R10 from docs/protocol/p2p-0.md.
Pass: libtacky never writes <p2p/> (its direct file announces a
jinglepub source), an older build's announcement merges onto the row of
the offering session only, and forged, malformed, old-namespace and
other-resource markers never merge. Over IBB, no media backend needed.

Assisted-by: Claude:opus-5-5
Pass with the fixed OMEMO gate: R1-R12 as on ejabberd, same mod_pubsub
behaviour (no meta-data feature, owner's private subscription pending).

Assisted-by: Claude:opus-5-5
Pass: every rule as on prosody.

Assisted-by: Claude:opus-5-5
Re-checks for issue #1 with the upstream v1.2.0 build next to rc2:
- C19 (XEP-0249): two invitations with the same body in one second
  still merge into one row on v1.2.0 (IsDuplicate falls through to
  the same-second content check when only the sender's @id is set).
- C20 (XEP-0425): forged moderation relayed by a room is no longer
  stored on v1.2.0; a 1:1 legacy :0 notice is still stored on both.
- S8 (XEP-0080): geoloc stop is still overtaken by a stale republish
  after set_allowed on v1.2.0; set_allowed [] then stop loses too.
- S1 (XEP-0483): meeting messages still go out in clear with OMEMO on.

Assisted-by: Claude:opus-5-5
Re-checks for issue #1:
- C9 (XEP-0080): set_allowed [] makes Prosody push the owner its own
  <delete/>, which ends sharing; set_allowed [peer] then answers OK
  without publishing. Same on rc2 and v1.2.0.
- S8 does not show on Prosody: C9 ends sharing before any republish.
- Prosody stall: a 20.8 KB plain body is routed fine; an SFS share
  whose thumbnail data: URI is one 20 KB attribute stalls the sender's
  stream (pings unanswered), both directions. Likely expat's reparse
  deferral holding a large token; unconfirmed.

server.log of these runs holds only an SSH host-key error: the
servers were recreated with new host keys and known_hosts is stale.

Assisted-by: Claude:opus-5-5
Re-checks for issue #1:
- S10 (XEP-0147 parse_uri): the %3F split is fixed in v1.2.0; a
  :port in the domain is still accepted, as is a %3F in a domainpart.
- C20 (XEP-0425): forged moderation relayed by a room no longer
  becomes a message on v1.2.0.
- S8 (XEP-0080): geoloc stop is still overtaken by a stale republish.
- M12: untrust_certificate with the registration domain works while a
  registration session is open; not reproduced.
- M9 (XEP-0198): with 5222 dropped on the server, v1.2.0 declares the
  stream dead 60 s after an unanswered <r/>; not reproduced.

Assisted-by: Claude:opus-5-5
Step-by-step playbooks (reproduce, verify, libtacky issue, fix branch,
retest, upstream PR) for four security bugs from issue #1, with the
issue and PR bodies and the fix patches. The fix branches live in a
local libtacky clone (fix/S5-captcha-bob-check, fix/S6-spans-fallback-
exact, fix/S10-parse-uri-port, fix/S13-secrets-host-output, off
develop 1dae0a6c); each run pair shows the bug on v1.2.0 and the fixed
build passing. repro-S6/S10/S13 scenarios cover what the existing
scenarios only noted as INFO.

Assisted-by: Claude:opus-5-5
Playbooks for four more security bugs from issue #1, with issue and PR
bodies and patches of the local libtacky fix branches
(fix/S1-meetings-calls-omemo, fix/S2-omemo2-outer-elements,
fix/S3-legacy-omemo-file-metadata, fix/S8-geoloc-stop-last-word, off
develop 1dae0a6c). Each run pair shows the bug on v1.2.0 on
anotherim-server and the fixed build passing. XEP-0420.py now lets the
OMEMO 2 envelope carry chat state, receipt request and markable beside
the body (SCE §3 allows it). Run 181318 is left out: it lacks the disco
marker and fails check.

Assisted-by: Claude:opus-5-5
Playbooks for S4 (fake room join at a contact's bare JID turns off
1:1 OMEMO), S7 (a user JID posing as a gateway gets into the roster)
and S9 (OTR ssid, fingerprint and SMP question mangled by Tcl expr),
with issue and PR bodies and patches of the local libtacky branches
fix/S4-muc-features-contact, fix/S7-gateway-user-jid and
fix/S9-otr-ssid-hex. S11 and S12 are fixed upstream; their playbooks
show rc2 failing and v1.2.0 passing.

Scenarios: XEP-0463 gains R8 (the OMEMO choice survives the fake
room), XEP-0364 R12 sends numeric-looking SMP questions as a
deterministic repro, and XEP-0227 prints INFO instead of failing when
no vCard4 node is exported. Run 184109 is left out: it lacks the
urn:xmpp:eme:0 marker and fails check.

Assisted-by: Claude:opus-5-5
Playbooks for C1 (invalid jinglepub from), C2 (IBB error without
<close/>), C3 (mention offsets after stripped characters), C4
(subject-only reply fallback), C5 (service notices raising alerts),
C6 (room caption folded by origin-id), C7 (XEP-0482 answer ids), C8
(XEP-0492 <advanced/> dropped) and C9 (Prosody's own geoloc <delete/>
event ending sharing), with issue and PR bodies and patches of the
local libtacky branches fix/C1..C9 off develop 1dae0a6c. Each run pair
on prosody shows the bug on v1.2.0 and the fixed build passing; the
full XEP-0482 and XEP-0492 scenario runs are kept for the record.

Assisted-by: Claude:opus-5-5
Fifteen bugs from issue #1 are fixed in libtacky v1.2.0; each playbook
links the upstream commit and its unit tests and shows how to see the
bug on rc2 and the fix on v1.2.0. Twelve are backed by new run pairs on
ejabberd (rc2 fail, v1.2.0 pass); C24, C30 (Prosody only) and C25
(needs in-band registration) point to their original runs and name the
server to use.

RFC-6120.py's own log reader now splits on "\n" like common.wire: a
fresh store's archive replay carried a raw U+0085 and broke it.

Assisted-by: Claude:opus-5-5
Playbooks for C10 (attachments node without max_items), C11 (pubsub
ids through expr), C12 (photo hash before the own vCard is read), C13
(SIMS without hash and desc), C14 (album sources without the sfs
fallback marker), C15 (no cap on outgoing thumbnails), C16 and C17
(missing vcard-4.0 and eme disco features, name= on EME hints), C18
(microblog publish without notify_retract) and C19 (IsDuplicate
merging different ids), with issue and PR bodies and patches of the
local libtacky branches fix/C10..C19 off develop 1dae0a6c. Each run
pair on anotherim-server shows the bug on v1.2.0 and the fixed build
passing; plaintext cases used a throwaway instant room. Runs 191208
and 191231 are left out: no peer log, they fail check.

Assisted-by: Claude:opus-5-5
MINOR-TRIAGE.md sorts the 24 minor bugs of issue #1 after reading each
against v1.2.0: 12 still bugs with local libtacky fix branches
(M2 M3 M4 M6 M7 M11 M14 M15 M16 M18 M21 M25, off develop 1dae0a6c), 6
not bugs or not reproducible (M8 M9 M10 M12 M17 M20) and 6 needing a
maintainer decision (M5 M13 M19 M22 M23 M24); every one has a playbook.
Run pairs on prosody show each fixed bug on v1.2.0 and the fixed build
passing. Scenarios gained checks for these bugs, keeping their existing
branches. Run 205307 (M6 fix) is left out: it fails check.

Assisted-by: Claude:opus-5-5
Run 205307 failed check: the M6 repro never touched disco or the
comments node, so the XEP-0472 markers were missing. repro-M6 now
reads ai1a's disco#info and publishes and retracts one post, and R3
checks that the comments-node create asks for itemreply publisher;
v1.2.0 fails it (211002), the fix passes (210942).

embcheck.py carves the script image libtacky embeds in tackyd-json and
compares it byte for byte with a branch's tacky/ tree. All 42 fix
binaries matched their branches, so the verify runs stand even though
_build directories were moved between worktrees.

Assisted-by: Claude:opus-5-5
fixes: add the playbook index
Some checks failed
/ check (pull_request) Failing after 14m0s
2e24b1fa62
fixes/README.md lists all 71 bugs of issue #1 by severity with their
kind (42 fix branches, 17 verify-only, 6 not a bug, 6 maintainer
decisions), the setup a human needs, the per-bug steps from reproduce
to upstream PR, and the branches that touch the same code.

Assisted-by: Claude:opus-5-5
AI-RESULTS: regenerate from committed runs only
All checks were successful
/ check (pull_request) Successful in 13m54s
0bc0c9295e
The report was generated in a checkout that also held run directories
never committed (superseded runs that fail check, an open run), so it
listed them and CI's report --check, which sees only committed runs,
found it out of date.

Assisted-by: Claude:opus-5-5
AI: C3 repro and fix runs for libtacky#24
Some checks failed
/ check (pull_request) Has been cancelled
9ade5402db
XEP-0426 on prosody with v1.2.0 (R9 fails: the reference keeps 30..34
after two characters were stripped) and with fix/C3 rebased on the
current develop (R9 passes, 28..32). The C3 issue text adds the
manual reproduction in anotherim-js with OMEMO on.

Assisted-by: Claude:opus-5-5
fixes/C3: fill in the PR text for libtacky#25
All checks were successful
/ check (pull_request) Successful in 13m43s
b860cf8a29
Fixes #24, the rebased branch's test counts and the new run pair.

Assisted-by: Claude:opus-5-5
fixes/C34: a destroyed room is recreated by autojoin
Some checks failed
/ check (pull_request) Has been cancelled
fbc8c8509a
Playbook, issue and PR text and the patch of the local libtacky branch
fix/C34-destroyed-room-autojoin (off v1.2.1, 8919dc12): a destroyed
room's bookmark is taken back, and autojoin no longer joins a room
whose disco#info answers item-not-found. repro-C34 on anotherim-server
shows v1.2.1 recreating the room (201) and the fix not.

Assisted-by: Claude:opus-5-5
fixes/C34: rerun with slixmpp as an independent observer
All checks were successful
/ check (pull_request) Successful in 13m48s
0116e8d079
The first C34 runs started slixmpp's run but never slixmpp itself, so
their bundles lacked peer-slixmpp.log and failed check. repro-C34 now
has slixmpp ask the room's disco#info after each sign-in: on v1.2.1 it
finds the destroyed room back (R2b, R3b), with the fix it stays gone.
The incomplete runs are dropped.

Assisted-by: Claude:opus-5-5
fixes/C34: filed as libtacky#26, PR libtacky#27, anotherim-js#95
Some checks failed
/ check (pull_request) Has been cancelled
522e0c1cbd
The issue text as filed (the host part moved to its own anotherim-js
issue) and the PR text with Fixes #26.

Assisted-by: Claude:opus-5-5
AI: S1 v1.2.0 repro run; XEP-0065 on prosody relabelled pass
All checks were successful
/ check (pull_request) Successful in 14m35s
80c6f6b2ca
The S1 run (repro-S1 on anotherim-server, v1.2.0) shows meeting and
call invites in clear with OMEMO on; upstream fixed it in v1.2.1
(7559ecfc, 9b1e961b). The XEP-0065 prosody run is relabelled pass:
libtacky passed R1-R10 and the stall was prosody's mod_proxy65, kept
in the run's notes.

Assisted-by: Claude:opus-5-5
asxpi merged commit 74355b4efb into main 2026-11-08 00:34:56 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
another-im/interop!2
No description provided.