Batch 7: cover the untested features, code coverage, hostile-server fuzzing #29
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Batch 7: cover what the harness has never run, measure how much of libtacky it reaches, and attack it with hostile server input. Starts after batch 6 (#25) is merged.
At the start, 98 of the 208 rows of libtacky's docs/features.md show no run in FEATURES.md, most because they were
ai_ok = false(agents' runs of those were hidden); 11 have no scenario at all. Only 3 cells were run by a person.Every finding of batch 7, fuzzer included, is recorded in this issue, not in #1 or #9.
Feature list per spec (scenarios)
Written 2026-11-11 from libtacky develop
65a31465(docs/features.md,HANDBOOK.md,docs/protocol/), interopmain(features/*.toml,tools/ai/scenarios/,runs/,AI-RESULTS.md,tools/ai/NIGHT-2026-11-05.md,vms/AGENTS.md) and #1, #9, #24, #25. One bullet with an id (0166-2) is one scenario, which is one run per server. Each scenario holds the main path, the MUST edge cases and at least one malformed or unauthorised case, as 7a asks.Names used below: T = tackyd-json under test (
ai{N}a); S = slixmpp (ai{N}b); T2 = a second tackyd as the peer (ai{N}b, a store of its own); T' = a second tackyd device of T's own account; own = a slixmpp session of T's account; D1, D2 = slixmpp-omemo devices made for the run; fake = a local server on 127.0.0.1 as inXEP-0215.py; pr / ej / ai = Prosody, ejabberd, anotherim-server.Section numbers are quoted from the existing
RULESlists or from libtacky's source. Where a line gives none, the author takes it from the spec (step 0 oftools/ai/PROMPT.md), never from memory.What the earlier batches already did (read before writing anything)
ai_ok = false). Of the rows 7a lists, only 11 have no scenario and no run: XEP-0068, 0176, 0260, 0261, 0293, 0294, 0320, 0338, 0339, 0343, 0453. Every other row has a scenario intools/ai/scenarios/, aRULESlist, filled steps infeatures/<slug>.tomland runs on all three servers. Do not write those again: rerun them on the batch's build and add what is listed here.media statusof the interop build answersbackend ""("neither webrtc nor datachannel is in this build"), socalls startnever reaches a session-initiate; that is why XEP-0166 part C, XEP-0298 and XEP-0507 ended as INFO orna. Theremotebackend opens in the same binary (--media-backend remote: audioCalls, videoCalls, dataChannel, renegotiate, trickleIce; checked ontackyd-json-develop-65a31465). With it the scenario plays the page: it answerscalls <MediaOpen>with a canned SDP (calls local_description,local_candidate,media_state) and readscalls <MediaRemote>/<MediaCandidate>. All Jingle call scenarios below use it.clients.tomllacks: XEP-0234, 0260, 0261). 7d names Dino and Gajim for it; only Dino and anotherim-web can run those cells.Rules for every scenario (mistakes made before)
tools/ai/differences.toml: ejabberd's mod_caps and Prosody's mod_pep send their own disco queries (count only the client's full JID); Prosody delivers each PEP notification twice; anotherim-server archives a message to a blocked JID before bouncing it. A fail that is the server's gets--cause server(XEP-0065 on Prosody was relabelled for this).file_transfer_p2p,attachment_autofetch, transports,auto_trust, blind trust). Take run-made OMEMO devices off the device lists and delete their bundles. RFC 7622's junk chats and XEP-0478's extra device stayed in the stores for days.since=mark().Tacky.send_rawgoes through the debug tap and is counted by stream management (XEP-0198 on ejabberd resent it). Do not use it around a resume or a stream error.tools/ai/aish, nevernix develop .#ai(it filled the disk twice). Scratch inmktemp -d; delete export folders that hold history.account link_deviceURI (it holdsosec). Check the base64 forms too. Room passwords from config forms appeared in clear inpeer-slixmpp.logonce.xep_0202cannot answer time queries, plugins throughp.xmpp.plugin[...]. OMEMO 2 is python-omemo (slixmpp-omemo).naand redone. A run a revised scenario replaced is finishednawith "superseded by ".low confidence: checked …; not checked: ….Harness changes these scenarios need (before 7a starts)
Tacky(..., media_backend="remote")(today--media-backend autois fixed intacky.py) and aPagehelper incommon.py: canned SDP offers and answers (audio, audio+video, data channel), candidates,media_state, a byte pipe for<MediaData>/data_message. Fixtures: libtacky'stests/client/test_calls_remote.tcl(CHROME_OFFER,CHROME_CANDIDATE).log_name(peer-tackyd.log), events log (client-events.logis fixed today, so two instances would share it) and debug log;interop start --under-test tackyd --client tackydfor akind = "peer"feature.Tacky(state_dir=…)), for T'.also = [...]in run.toml (already in 7a). A run that fails onealsospec's rules is rerun split, so one spec's bug does not fail the others' rows.tools/ai/fake.py: direct TLS on 127.0.0.1, pinned certificate, PLAIN, programmable disco and answers), fromXEP-0215.pyandRFC-5929.py, which each carry a copy.Open points for the maintainer
Decided 2026-11-11:
ai_ok = truein everyfeatures/*.toml. VERIFIED stays a person's pass; rows with a display or media part are still runconfidence = "low".mod_muc_mention_notificationsthere (on the MUC component:modules_enabled = { "muc_mention_notifications" }andmuc_mmn_notify_unaffiliated_users = true).0452-2's fake part is dropped; its hostile cases go into0452-1on Prosody. ejabberd and anotherim-server stay[na].tools/ai/fake.py) is its only run.0453-1, recorded on Prosody; ejabberd and anotherim-server have an[na]reason.0364-2), about 260 runs, about 170 without P2.Harness H1–H5 and these decisions are in the batch 7 harness PR (branch
ai/batch-7-harness).P0
XEP-0166 Jingle · has
XEP-0166.py(R1–R17, file session over IBB), develop: pr ✓ ej ✓ ai ✓ (C31 fixed)0166-1T×S, rerun, then add to the same file session: content-modify (answered, senders unchanged), content-remove (ends the session), description-info and security-info (unsupported-info), a responder's transport-replace crossing T's own (<conflict/>+<tie-break/>). R16 (a refused sid sent again) becomes a check, not INFO. Hostile:initiatornaming a third JID; a session-initiate from a bare JID; a sid of 4 KB with markup characters; a 9th open incoming session (XEP-0234 row:resource-constraint).0166-2T(remote)×S, raw Jingle, call session (new). Tcalls start, S answers the XEP-0353 proceed, the page gives the offer: session-initiate hasinitiator, every<content/>one description and one transport. S session-accept →<MediaRemote type answer>,media_state connected→<Active>. Then: S's transport-replace (refused; record the condition), S's content-add video →<MediaRemote offer>, the page's answer → content-accept,<Renegotiated>; S's content-remove; T's own content-add (a mid-calllocal_descriptionoffer), S content-reject →<Renegotiated rejected>; crossing session-initiates (tie-break); session-terminate both ways, then unknown-session. Hostile: session-accept from S's other resource; a second session-accept;media_failedfrom the page (session-terminate with a reason,<Failed>); a propose while a call is up (busy,<Ended>missed).0166-3T(remote)×T2(remote), both ways (new): audio call, video call, reject, hang up from each side, a call to an offline peer. Pass when every action is acked, both sides end in the same state, and the SDP that reaches the far page has every line class the near page sent (codecs, rtcp-fb, extmap, ssrc, group, fingerprint, candidates).also = [XEP-0167, XEP-0353].XEP-0176 Jingle ICE-UDP · no scenario
0176-1T(remote)×S (new). T's initiate carriesufragandpwdfrom the page's SDP; every trickled transport-info carries both (libtacky cites XEP-0176 §5.3) and a full candidate (component, foundation, generation, id, ip, port, priority, protocol, type; rel-addr and rel-port for srflx and relay). S's candidates sent before the page opened are buffered and replayed as<MediaCandidate>after<MediaRemote>. A candidate under the known ufrag without a pwd is applied. New ufrag and pwd together are an ICE restart: onremote(iceRestartfalse) record what T does as INFO. TCP candidates are not signalled: S'sprotocol='tcp'candidate and the page's tcp candidate cross neither way. Hostile: priority 2^32, port 70000, an ip that is none, a ufrag with spaces or 300 characters, a candidate for an unknown content, 500 candidates at once, a transport-info from another resource. Pass when each is refused or dropped, no<MediaCandidate>carries it, and the session lives.0176-2T(remote)×T2(remote) (new): trickle both ways with scripted host, srflx and relay candidates;<MediaOpen>'sice_serversequalextdisco fetchon that server (stun and turn on all three since #13).also = [XEP-0215].XEP-0320 DTLS-SRTP in Jingle · no scenario
0320-1T(remote)×S (new). T's initiate has<fingerprint xmlns='urn:xmpp:jingle:apps:dtls:0' hash setup/>in every transport, equal to the page'sa=fingerprintanda=setup; S's accept withsetup='active'reaches the page so; as responder the page's answer role is in T's session-accept.calls <Security>:dtls_local,dtls_remote, 7 SAS indices,encryption none. Hostile: an accept with no fingerprint; a fingerprint that is not colon-separated hex;setup='bogus'; two contents with different fingerprints; a fingerprint changed by a later transport-info. Pass when the malformed ones never reach the page and the call ends with a reason, not<Active>.0320-2T(remote)×T2(remote), OMEMO on, both devices trusted and advertisingurn:tacky:jingle:dtls-omemo:0(new): the fingerprints cross sealed asdocs/protocol/jingle-dtls-omemo-0.mdhas it,<Encryption>/<Security>sayomemowith the device and its trust, both sides show the same SAS. Then T2's device undecided: the trust says so. Hostile (S in a third session): S replays T2's sealed element in a session of its own →<Failed>, session-terminatesecurity-error.also = [urn:tacky:jingle:dtls-omemo:0].XEP-0420 Stanza Content Encryption · has
XEP-0420.py(R1–R19), develop: pr ✓ ej ✓ ai ✓ (S2 fixed)0420-1T×S+D1, rerun, then add what 1.2.0–1.2.4 brought: a retraction in an OMEMO 2 chat is sealed (<retract/>, fallback body and<fallback/>inside, no body beside), a bare<retract/>beside the ciphertext, none withretract_hint0; a<time/>more than 30 days before a stamp the server vouches for (D1 sends while T is offline, T catches up) is refused<DecryptFailed>sce; an OMEMO 2 room message without<to/>, or with another room's, is refused. R19 stays INFO for receipts, markers and reactions (no server advertisesurn:tacky:push:sealed-signals:0, #9 S15).0420-2T×T2, OMEMO 2 both (new): body, reply, correction, retraction, reaction, file share, in a 1:1 chat and in a members-only non-anonymous room. Pass when no<DecryptFailed>appears and R6–R8 hold on every stanza of both logs. A helpercheck_sce(stanza)incommon.pyruns the same rules in every OMEMO 2 run (XEP-0384, 0434, 0450, 0454, jet-omemo:2), which then carryalso = [XEP-0420].P1, protocol
XEP-0048 Bookmarks · has
XEP-0048.py(R1–R10), rc2: ✓✓✓, R9–R10 never ran (all three servers have PEP bookmarks)0048-1T×S+own, rerun.0048-2T×fake without PEP bookmarks (new, H5): R9, R10: private XML fetched on connect, the whole<storage/>written back with other conferences,<url/>and foreign children unchanged. Hostile: 5000 conferences, a malformed jid,autojoin='maybe', the same jid twice.XEP-0050 Ad-Hoc Commands · has
XEP-0050.py(R1–R15), develop: ✓✓✓ (M2 fixed)0050-1T×S, rerun, add: a required field left empty is refused before any IQ (row: validated before submit); the default action when<actions/>names noexecute; the responder'sbad-action,bad-sessionidandsession-expiredreach the caller. Hostile: the sessionid changed mid-session; a result from another JID under the same iq id; a 500-field form; an unknown note type.XEP-0060 Publish-Subscribe · has
XEP-0060.py(R1–R16),repro-C11.py, develop: ✓✓✓0060-1T×S, rerun the full scenario on all three (ej and pr last ran only the C11 repro), add: subscriptions survive a restart of T; apendingorunconfiguredsubscribe answer is reported; a notification from the right service with a node T never subscribed to, and from another service with the right node, is nopubsub <Notification>. Hostile: an event with 1000 items; a 200 KB payload;<delete><redirect/></delete>. Not checked (row): §6.3, §8.7, §8.8, collections.XEP-0066 Out of Band Data · has
XEP-0066.py(R1–R8), rc2: ✓✓✓0066-1T×S, rerun, add the 1.2.2 and 1.2.5 rules: a room occupant's picture is not fetched by itself;contactsmeans subscription both (a from-only contact's file waits); a storedeveryonereads back ascontacts. Hostile: a url with userinfo, of 4 KB,aesgcm://with a broken fragment, https to a local listener (count connections).XEP-0068 Field Standardization · no scenario
0068-1T×S+fake (new). Out: everyx:datasubmit T sends in the run carries the rightFORM_TYPE: MAM query, push enable, pubsub node config and publish-options, MUC config andmuc#register, a CAPTCHA answer, an ad-hoc stage (the responder's own). In: on the fake, an upload form whoseFORM_TYPEis missing, not hidden, or another value gives nomax-file-size(storage-0 §5); a CAPTCHA with a wrongFORM_TYPEis not told (XEP-0158 R2); a caps answer with two forms of oneFORM_TYPEis not cached (XEP-0232 R7).XEP-0082 Date and Time Profiles · has
XEP-0082.py(R1–R8), rc2: ✓✓✓0082-1T×S+own, rerun, add malformed forms:23:59:60, year 0000 and 10000, lower-casetandz.XEP-0203 Delayed Delivery · WIP · has
XEP-0203.py(R1–R10), rc2: ✓✓✓0203-1T×S+own, rerun, add: after a XEP-0198 resume a resent stanza's server stamp is taken, never earlier than that session's start; a forgedfrom=DOMAINstamp 16 s after login with no inbound traffic gets the arrival time (the window closed). o3 stays INFO (the row's open issue).XEP-0221 Data Forms Media Element · has
XEP-0221.py(R1–R11), develop: ai ✓; pr ✗ and ej ✗ are rc2 runs (S5, fixed in 1.2.1)0221-1T×S, rerun on pr and ej first: their cells still show the rc2 fail. Add: asha256+cid; 100<uri/>s in one<media/>; width 65536.XEP-0231 Bits of Binary · has
XEP-0231.py(R1–R12), rc2: ✓✓✓0231-1T×S, rerun, add: a cid with max-age 2 is fetched again after 3 s; a result for T's BoB get from another JID is dropped; a fetched blob over the 64 KiB cap is refused; no fetch from a non-contact.XEP-0260 Jingle SOCKS5 Bytestreams Transport · no scenario (parts ran inside
XEP-0065.pyandXEP-0234.pypart S)0260-1T×S, S with the SOCKS5 client and server ofXEP-0065.py(new). Candidates carry cid, host, jid, port, priority, type;dstaddr= SHA1(sid + initiator + responder); candidate-used and candidate-error both ways; both sides used: the higher priority wins, a tie goes to the initiator's; a proxy is activated by its offerer (<activated/>),<proxy-error/>moves to IBB by transport-replace;mode='udp'→unsupported-transports;file <Update>saystransport socks5. Hostile: 12 candidates (trap listeners count: 8 dialled at most); candidates on 0.0.0.0, 224.0.0.1, 255.255.255.255, 127.0.0.1 (none dialled); candidate-used naming an unknown cid;<activated/>for a cid nobody used.0260-2T×T2 (new): a 1 MiB file each way with transports[socks5], direct, then through the server's proxy where it is reachable (ejabberd's proxy65 announces 127.0.0.2, #9 P6: INFO there). Pass when the bytes are intact and verified.XEP-0261 Jingle IBB Transport · no scenario (parts ran inside
XEP-0047.py)0261-1T×S (new). The responder's smallerblock-sizeis honoured; the transport'ssidis the<open/>'s; after both SOCKS5 sides report candidate-error T sends transport-replace to IBB and continues on S's transport-accept; as responder T accepts S's transport-replace to IBB. Hostile: transport-replace to an unknown namespace (transport-reject); block-size 0, 70000 andabcin the accept; a transport-accept nobody asked for (out-of-order). Not checked: the responder's replace when the account's proxy takes the data channel away.0261-2T×T2 (new): transports[ibb], 1 MiB each way, intact,transport ibb.XEP-0264 Thumbnails · has
XEP-0264.py(R1–R10),repro-C15.py, develop: ✓✓✓ (ej and pr by the repro only)0264-1T×S, rerun the full scenario on all three, add 1.2.5: an upload's SFS share names the thumbnail twice, by cid and as adata:URI when it is at most 16 KiB; between 16 and 64 KiB by cid only. The same inside an OMEMO 2 envelope is checked in0420-2.XEP-0292 vCard4 Over XMPP · has
XEP-0292.py(R1–R11),repro-C16.py, develop: ✓✓✓ (ej and pr by the repro only)0292-1T×S, rerun the full scenario, add: org, title, role, impp, tz, pronouns and adr round trip; twovcard setback to back on Prosody leave the second on the node (the retried publish that overwrote a newer one, NIGHT 2026-11-05); no echo of the server's converted item. Check for #9 C47: aftervcard set, S reads T's vcard-temp: R10 says FN, N and NICKNAME are mirrored, the VM runs saw Kaidan read an empty card on ej and ai. Report which holds. Hostile: a 200 KB item; an item id that is notcurrent; two items; a forged event from a full JID.XEP-0293 RTP Feedback, XEP-0294 RTP Header Extensions, XEP-0338 Jingle Grouping, XEP-0339 Source-Specific Media · no scenario
0293-1T(remote)×S, one run withalso = [XEP-0294, XEP-0338, XEP-0339](new). One SDP fixture, both directions (the page's SDP → T's Jingle; S's Jingle →<MediaRemote>SDP), a call T starts and a call S starts:a=rtcp-fb:PT nack pli,ccm fir,transport-cc,a=rtcp-fb:* …andtrr-int↔<rtcp-fb type subtype/>and<rtcp-fb-trr-int value/>inurn:xmpp:jingle:apps:rtp:rtcp-fb:0, under the payload-type or, for*, the description. Hostile: an<rtcp-fb/>withouttype; a non-numeric trr-int; 300 of them.a=extmap:ID[/direction] URI [params]↔<rtp-hdrext id uri senders/>inurn:xmpp:jingle:apps:rtp:rtp-hdrext:0, the direction assendersfor an initiator and for a responder, parameters kept,a=extmap-allow-mixed↔<extmap-allow-mixed/>. Ids: 1, 14, 15, 255, 4096 accepted; 0, 256, 4352, -1 andxrefused (jinglesdp.tcl).a=group(BUNDLE and LS) ↔<group semantics><content name/></group>inurn:xmpp:jingle:apps:grouping:0. A group naming a content the session lacks loses that name; semantics that is no token is refused.a=ssrc:ID name:value, a valueless parameter anda=ssrc-group:FID a b↔<source ssrc><parameter/></source>and<ssrc-group semantics>inurn:xmpp:jingle:apps:rtp:ssma:0. ssrc 4294967295 accepted; 4294967296, -1 andabcrefused.XEP-0300 Hash Functions · WIP · has
XEP-0300.py(R1–R15), rc2: ✓✓✓0300-1T×S, rerun, add: a sha-256 value of 31 bytes;algo='SHA-256'. Not checked (row): BLAKE2b.XEP-0317 Hats · has
XEP-0317.py(R1–R10), rc2: ej ✓ ai ✓ prna(Prosody 13 runs no hats commands)0317-1T×S, rerun, add:<hats/>in a message is not read; a 10 KB title. Prosody: R1 and R10 only, verdictnaas before.XEP-0334 Message Processing Hints · has
XEP-0334.py(R1–R11), rc2: ✓✓✓0334-1T×S+D1, rerun, add: a sealed retraction (1.2.4) carries<store/>; hints on a carbon and on an archived copy never drop the row.XEP-0343 WebRTC Data Channels in Jingle · no scenario
0343-1T(remote)×S (new). Transports[datachannel, ibb]: T's offer has theurn:xmpp:jingle:transports:webrtc-datachannel:1transport around ice-udp withsctp-portandmax-message-size;<MediaOpen data=1 role offer>; afterdata_state openthe file leaves as<MediaData>chunks and<MediaDataClose>; the bytes equal the file. S offers:file accept, the page feedsdata_messagechunks, the file is kept verified.media_state failed→ transport-replace to the next transport (XEP-0260, 0261 rows). Hostile: more bytes than<size/>;data_messagebeforeopen; a wrong hash (not kept,<media-error/>); a non-numericsctp-port(5000 assumed).0343-2T(remote)×T2(remote), the scenario pipes the bytes between the pages (new): a file each way in a clear chat, then in an OMEMO 2 chat (JET-OMEMO 2, fingerprints sealed).also = [XEP-0391, urn:tacky:jingle:jet-omemo:2, urn:tacky:jingle:dtls-omemo:0].XEP-0367 Message Attaching · has
XEP-0367.py(R1–R15), develop: ✓✓✓ (C6 fixed)0367-1T×S+own, rerun, add: an attach-to naming its own message; A → B → A; 100 captions for one file; in a room, the same nick under another occupant id is not folded.XEP-0372 References · has
XEP-0372.py(R1–R10), rc2: ✓✓✓0372-1T×S, rerun, add: 1000 references in one message; a mention after XML character references in the body.XEP-0380 Explicit Message Encryption · has
XEP-0380.py(R1–R13),repro-C17.py, develop: ✓✓✓ (ej and pr by the repro only)0380-1T×S+D1, rerun the full scenario, add #9 M27: the stored record of an OMEMO 2 message T sent (raw_xml) namesurn:xmpp:omemo:2. Hostile: 50<encryption/>elements; a 5 KB namespace; anamewith markup and bidi overrides.XEP-0385 SIMS · has
XEP-0385.py(R1–R18),repro-C13.py, develop: ✓✓✓ (ej and pr by the repro only)0385-1T×S, rerun the full scenario, add: 30 sources; a size of -1 and of 2^64; a name with a path, a bidi override and a NUL.legacy_file_metadata0 (#1 S3) stays in the XEP-0384 runs, which getalso = [XEP-0385].XEP-0391 Jingle Encrypted Transports · has
XEP-0391.py(R1–R17), rc2: ✓✓✓0391-1T×S+D1, rerun, add 1.2.5 (#9 P4): Dino's cipher spelling is sent and both spellings are read; an offer of 256 MiB + 1 is refused before any byte. The libtacky-against-libtacky transfer is0343-2.XEP-0398 Avatar to vCard Conversion · has
XEP-0398.py(R1–R6), rc2: ✓✓✓0398-1T×S, rerun, add: a vCard PHOTO of 5 MB, of type SVG, and an EXTVAL http url (nothing fetched); the conversion feature in a forged disco#info result is not believed.XEP-0428 Fallback Indication · has
XEP-0428.py(R1–R10), develop: ✓✓✓ (C4 fixed)0428-1T×S, rerun, add: 500<fallback/>elements; start = end; overlapping and unsorted ranges (row: merged).XEP-0434 Trust Messages · WIP · has
XEP-0434.py(R1–R13), rc2: ✓✓✓0434-1T×S+D1+D2, rerun, add: two<trust-message/>in one message (R1: exactly one); a usage other than ATM; a trust message in the clear; a contact's device naming a third JID as key owner (ignored). Not checked (row): OMEMO 0.3 keys, the archive.XEP-0446 File Metadata · has
XEP-0446.py(R1–R12), rc2: ✓✓✓0446-1T×S, rerun, add: a<size/>that is not the bytes'; a negative<length/>; width 0 and 10^9; a 4 KB name; two<file/>elements.XEP-0450 Automatic Trust Management · WIP · has
XEP-0450.py(R1–R13, R6 not run), rc2: ✓✓✓0450-1T×S+D1–D4, rerun, add R6 (a key not fetched yet) and the bounds: the 33rd trust message from a device not authenticated and the 257th unknown key are not kept.0450-2T+T'×T2 (new, H3): T verifies T': T' gets every key T verified and refused; T's refusal of a T2 key goes to T' and not to T2; T2, verified, gets T's and T''s keys. Pass when the trust lists of T and T' are equal at the end.XEP-0452 MUC Mention Notifications · has
XEP-0452.py(R1–R5), rc2:na×3 (see open point 2)0452-1T×S, rerun on Prosody only if the module is enabled; else[na]on all three.0452-2T×fake (new, H5): R1, R3, R5 with the room's bare JID as the sender. Hostile: a forwarded message of another room; a non-groupchat one; a stamp in the future (never later than now); a 5000-character text (cut at 1024); the same mention twice (told once); a room T does not know.XEP-0469 Bookmark Pinning · has
XEP-0469.py(R1–R7), rc2: ✓✓✓0469-1T×own, rerun, add: own changes the bookmark's name, then T pins: the pin's republish keeps the new name;<pinned/>twice;<pinned/>with children.urn:xmpp:mucsub:0 ejabberd MUC/Sub · has
urn-xmpp-mucsub-0.py(R1–R13), develop: ej ✓ ai ✓, pr[na]mucsub-1T×S, rerun, add: a kick, a ban and the room's destruction each end the stored subscription; S unsubscribes T while T is offline, and T's list is reconciled at connect; a reaction, a correction and a retraction through MUC/Sub land on the room chat's rows; the archive catch-up files the room's copy once. Hostile: a wrapper from the MUC service's domain; a wrapper from room A carrying a messagefrom='roomB/nick'.urn:fluux:poll:0 ProcessOne polls · has
urn-fluux-poll-0.py(R1–R27), develop: pr ✓ ej ✓, ai rc2 ✓poll-1T×S, rerun (ai first), add the room part: the tally by occupant id, a late joiner reading poll, votes and close from the archive ({urn:xmpp:mam:2}ids),<poll-results/>asked of the creator. Hostile: a results IQ from a non-occupant; 21 options; the same emoji twice; a deadline in the past; a close from the creator's nick under another occupant id.poll-2T×T2 in a room (new): poll, votes, close and results between two libtackys. Pass when both show the same tally.P1, tacky-only
These catch crashes and regressions, not misread specs: the spec is
docs/protocol/<name>.mdof the tested commit.urn:tacky:sealed:1 · has
urn-tacky-sealed-1.py(R1–R23), develop: pr ✗ ej ✗ (#1 M18 left: a replayed draft is dropped without a debug line), ai rc2 ✓sealed-1T×own, rerun on all three; say for M18 whether the item is refused (the security rule) apart from whether the line is logged.sealed-2T+T' (new, H3): T' publishes its seal key and T trusts it: T seals the drafts, pins, tags and mute list again to T' under their own stamps, and T' reads them; a draft T' writes reaches T; T' set untrusted gets no key in the next item. Hostile (own): a seal key item signed by another identity; a key under a device id not on the OMEMO list.urn:tacky:omemo-link:0 · has
urn-tacky-omemo-link-0.py(R1–R15, the showing side only), rc2: ✓✓✓link-1T×own, rerun.link-2T+T' (new, H3): Taccount link_device, T' (an empty store)account add_from_uri:omemo <LinkVerified>on both, each trusts the other's device. A link whoseofpis changed →<LinkVerifyFailed>on T'. The same link used by a third store verifies nothing (spent). The URI is a secret: never in a log, a note or the scenario's output.urn:tacky:storage:0 · has
urn-tacky-storage-0.py(R1–R12), rc2:na×3 (see open point 3)storage-1T×own, rerun;[na]stays while no server offers the service.storage-2T×fake with an upload and a storage item (new, H5): R2, R6–R9, R12:-personal 1asks the storage service;file keepruns thekeepcommand, anaesgcm://URL goes without its fragment and comes back with it. Hostile:keepanswers an http url or another host's; a stage that never completes; two storage services (the first is used).urn:tacky:service:0 · read-only · has
urn-tacky-service-0.py(R1–R15), develop: ✓✓✓ (C5 fixed)service-1T×S+own, rerun, add: the same rows after an archive catch-up (T offline while S sends);<service/>twice in one message; a 300-character kind; 200 attributes.urn:tacky:spans:0 · read-only · has
urn-tacky-spans-0.py(R1–R17),repro-S6.py, develop: pr ✓ ej ✓, ai rc2 ✓spans-1T×S+D1, rerun the full scenario on all three, add: spans inside an OMEMO 2 envelope read the same; spans beside a ciphertext are ignored (XEP-0420 R16).urn:tacky:live-location:0 · read-only · has
urn-tacky-live-location-0.py(R1–R11), develop: ej ✓, pr and ai rc2 ✓live-1T×S+own, rerun, add: moves that arrive from the archive after T was offline leave one row at the last place with no revision; a live location in a room is moved only by the same occupant id;untilalready past at the start.urn:tacky:push-hint:0 · has
urn-tacky-push-hint-0.py(R1–R9, in the clear only), rc2: ✓✓✓hint-1T×S, rerun.hint-2T×T2, OMEMO 2 (new): an album with a caption:<no-push kind='sources'/>andkind='caption'stand beside the ciphertext; a caption that mentions someone or answers a message carries none. Not checked: what a push service does with it (no app server in the lab).P2, if time allows
One scenario each; "rerun" means the existing scenario unchanged on the batch's build, with the adds in the same run.
XEP-0047.py, develop ✓✓✓ (C2 fixed)0047-1T×S rerunXEP-0055.py, rc2 ✓✓✓0055-1T×S rerunmuc search(1.2.0) against a room S playsXEP-0065.py, rc2 ✓✓✓ (pr relabelled: mod_proxy65)0065-1T×S rerun0260-1covers the transport; ej's proxy announces 127.0.0.2 (#9 P6)XEP-0080.py,repro-S8.py, develop ✓✓✓0080-1T×S rerun the full scenarioXEP-0107.py, rc2 ✓✓✓0107-1T×S+own rerunXEP-0108.py, rc2 ✓✓✓0108-1T×S+own rerunXEP-0118.py, rc2 ✓✓✓0118-1T×S+own rerunXEP-0158.py, develop ✓✓✓ (S5 fixed)0158-1T×S rerunfromfield names another room (not solved)XEP-0201.py, develop ✓✓✓ (M3 fixed)0201-1T×S rerunXEP-0232.py, rc2 ✓✓✓0232-1T×S rerunXEP-0277.py, develop ej ✓ ai ✓, pr rc2 ✓0277-1T×S rerun (pr first)XEP-0298.py,na×3 (no call headless)0298-1T(remote)×S<conference-info isfocus/>in its accept), full, partial and deleted documents merged by version, taken only from the call's peer,calls conference_info; a 200 KB documentXEP-0358.py, develop ✓✓✓ (C1 fixed)0358-1T×S rerun<start/>: record the refusal (row: not served)XEP-0364.py, develop ✓✓✓ (S9 fixed)0364-1T×S rerun;0364-2T×T2 (new)XEP-0394.py, rc2 ✓✓✓0394-1T×S rerunXEP-0426.py, develop ✓✓✓ (C3 fixed)0426-1T×S rerunXEP-0439.py, rc2 ✓✓✓0439-1T×S rerun0453-1offline, recorded on one server (see open point 4)python3 tools/doap.py --checkon the tested commit;schema/tacky.doapparses as RDF; one SupportedXep per XEP row with its status and versionXEP-0463.py, develop ej ✓ ai ✓, pr rc2 ✓0463-1T×S rerun (pr first)sinceT never reached forgets the list (the next join asks for all)XEP-0470.py, develop ✓✓✓ (C10 fixed)0470-1T×S rerunXEP-0472.py, develop ✓✓✓0472-1T×S rerunXEP-0477.py, rc2 ✓✓✓0477-1T×S+own rerun<encrypted by/>naming another JID than the publisher is refused; the OMEMO 2 mechanism has no public method (not checked)XEP-0483.py, develop ej ✓ ai ✓, pr rc2 ✓0483-1T×S rerun (pr first)XEP-0488.py, rc2 ✓✓✓ (no server hands out tokens)0488-1T×S rerun<expired-token/>, a refusal written as a result with<error/>XEP-0491.py, rc2 ✓✓✓0491-1T×S rerunXEP-0501.py, develop ej ✓ ai ✓, pr on fix-M7 ✓0501-1T×S rerun (pr first)XEP-0503.py, rc2 ej ✓ ai ✓, prna0503-1T×S rerunnareason if the service still lacks the §4 featuresXEP-0507.py, onenarun (pr)0507-1T(remote)×Sset_screen_share→ description-infoslides, back →speaker); a description-info with anything beside categories → unsupported-infoXEP-0518.py, develop pr ✓ ej ✓, ai rc2na0518-1T×S rerun (ai first)7d, GUI peers (anotherim-js in the VMs)
aim = anotherim-js; one scenario is one run per peer and server. Read
vms/AGENTS.mdfirst. Both VMs sit behind QEMU user NAT as 10.0.2.15, so a direct SOCKS5 candidate never connects; aim's XML log lags a click by up to 30 s.d-ft-1aim sends a picture with "Send files directly" on, plain chat; then the peer sends oned-ft-2the same on ejabberd, where SOCKS5 comes to nothing--cause peer; web is the cell that can passd-ft-3aim ↔ web in an OMEMO chat<security/>JET-OMEMO 2, sealed fingerprints, the picture decrypted on bothd-call-1audio then video call withvms/examples/netem.pyadding 5 % loss in the video callrtcp-fbandrtp-hdrextand the answer is a subset of the offer;rtp.logshows RTP with the extension bit set and RTCP feedback packets (PT 205 or 206) during the lossd-thumb-1a picture each way, plain and OMEMOdata:thumbnail (1.2.5)d-hint-1typing, a reaction, a retraction, a reply, a file with a caption each way; then a second device of each account reads the archived-sub-1on ejabberd and anotherim-server: aim follows a room without joining (first check that the app has a control for it; if not,nawith that reason), the peer writes, reacts, corrects and retracts; aim answers; aim restarts[na]d-nat-1each VM in a network namespace with outbound UDP dropped except to the lab's TURN server; audio and video call; then UDP dropped altogether<services/>answer has the TURN service with credentials, both sides offer relay candidates, the selected pair is relay–relay, tones both ways; without UDP the call uses TURN over TCP or fails with a reason, not a hangRFC rows: seeds and oracles for 7c
The RFC rows have runs; these are the cases the 7c proxy injects, and what counts as a wrong decision. Each finding becomes a
repro-<id>.py.<starttls/>; a stream header with anotherfrom, no id, version 0.9; a bind result with another JID;<see-other-host/>to a foreign host; a stream error after every nth stanza; an unknown top-level element; an IQ result under a pending id with anotherfromfrom; presence floods; a subscription approved nobody asked forfrom, our bare JID or our domain)fromand in roster itemsconn <State> connectedin any of these<open/>with anotherfrom;see-other-uritows://<a h/>above what was sent;<resumed/>with another previd;<failed/>mid-resumefrom; a<stanza-id by='our JID'/>written by the peer; a<fin/>under another query idpolicy-violation(NIGHT 2026-11-05)0. Before start
ai/batch-7(headless: 7a, 7b, 7c) andvm/batch-7(7d). One PR each into main; results go here.7a. Headless scenarios for the untested rows
tackyd-json against slixmpp, on Prosody, ejabberd and anotherim-server, one agent per server.
also = [...]), so the existing call and OMEMO runs also count for the Jingle and SCE rows they exercise.7b. libtacky code coverage
coverage.mk: Rust-C instrument-coverage, C--coverage, and Tcl's procedure record.tools/cov-union.pyandstats.py.COVERAGE.md: per module, the lines interop reached next to the lines libtacky's own tests reached.7c. Fuzzing as a hostile server
tackycalls)7d. VM part (GUI peers, anotherim-js)
7e. Hand test (after the runs)
Once the runs are finished, a checklist with step-by-step instructions goes here, for a person to run in the VMs. It covers:
Each step gives the VM command, the accounts, what to click, what to expect and where to look (screenshot, log line). Those cells are what let FEATURES.md show VERIFIED.
Future (not in batch 7)
Results
Filled in per part when its PR is merged.
🤖 Generated with Claude Code