Tacky is high-level library for building XMPP clients
  • Tcl 67.7%
  • C 14.2%
  • Rust 11.5%
  • Makefile 2.4%
  • Python 1.8%
  • Other 2.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-13 02:31:01 +03:00
docker A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
docs pte_seal.tcl is our XEP-0477 mechanism urn:tacky:sealed:1 on its own, out of chatsync.tcl (the user, 2026-10-12: "use something like pte_seal.tcl"), and the one way a list of the account is kept in step with its other devices, in place of five near-identical modules (chatpinsync, tagsync, autofetchsync, omemochatsync, omemotrustsync, ~1100 lines with the same nine methods). The module pte_seal, installed after pubsub and caps so its <Ready> read of the seal keys goes out before any list's read, carries the cipher (::xmpp::client::drafts is ::xmpp::client::pte_seal), the urn:tacky:drafts-keys:0 node with this device's key (still kept in the store under chatsync, where existing installations have it), sealFor / seal / pteEnvelope / openPte / openSealed (errorcodes TACKY_PTE_SEAL REFUSED|UNREADABLE|UNVERIFIED), and sync, which makes a tacky_pte_list: one item current, read on <Ready> and on PEP events, refused unless sealed by this account, stamped and signed by a device of ours (one the user verified for -signer verified), ignored when no newer than -stamp, applied by the owner's -apply, the node's newest stamp kept, and the list here published when -ahead (by default newer than the node) or a merging list (-merge) says so. Each list is declared by the module that owns its data, which now also serialises it: chatlist the pinned chats (unstamped pins go up only to a node that showed no list) and the installation's tags, file the auto-download hosts, omemo the per-chat choices and the trust decisions (merged per device, stamped past the node's and the last list's). chatsync keeps the mute list, the pinned messages and the drafts, sealing through pte_seal. Behaviour kept but for three edges: the tags list as v0.9.9-alpha4 published it (a bare urn:tacky:drafts:1 envelope, "read for one release") is no longer read; a tags list sealed but not to this device now counts by its stamp, so it is not published over, as the pinned chats already did; and a malformed list item logs the fixed reason "not a list of NS" for every list. Log lines say pte_seal. The nodes, the items and the wire are unchanged; the schema drops the five method-less modules for pte_seal. Tests follow the names; test_tags plays other devices with PTE items. Client suites 5297 passed, 0 failed; check-api clean. 2026-10-12 23:36:54 +03:00
embed An interpreter created after the backend's, or tackyd's, finds the image's Tcl library: embed/tacky.c and embed/tackyd_main.c set env(TCL_LIBRARY) to //zipfs:/libtacky/tcl_library beside tcl_library. tcl_library is the one interpreter's; a child reads the environment and Tcl's default places, and snit compiles every type in a child, so loading the shared modules (tacky::xmpp::component's disco, bob, adhoc) died with "Cannot find a usable init.tcl" on a machine with no Tcl installed: a component shipped as tackyd with its program appended, in an image that carries nothing else. On a build machine the child found the installed Tcl instead, which is why no suite saw it. tests/tackyd_program.tcl checks that a child interpreter's [info library] is the image's own; it fails on the tackyd before this change (//zipfs:/app/tcl_library) and passes after. Checked in the container: make all test MEDIA_BACKEND=datachannel REQUIRE=1 exit 0, the tree 4,707 total, 4,706 passed, 0 failed, 1 skipped. 2026-10-07 18:34:38 +03:00
examples Comments, build files and the crates' docs no longer cite documents this tree does not have: TRANSFER.md, HTTP-TCL.md, LOCALSTORE.md, ARCHITECTURE.md (the workspace's §9.1 and §9.2), MEDIA.md, JINGLE-FT.md, LIVENESS.md, NOTIFY.md, CHATLIST.md, THREADS.md, STORAGE.md, CHAOS-FINDINGS.md, ONE-CLIENT.md, ROADMAP-another-tacky.md, docs/DEPLOY.md, docs/STABILIZE.md, docs/discovery.md, docs/storage-lock.md, reports/OMEMO_issues.md and reviews/2026-09-*, nor ROBOTS.md by section numbers it lost long ago (§2.2, §7.1, §8, §10.80 and the rest). Each pointer now names the place that exists (tacky/http/README.md, tacky-keyring/README.md, ROBOTS.md's HTTP, "The store", "The shape" and rule 2 or 6, tacky_media.h's data channel section, session/liveness.tcl, modules/file.tcl, ext/common.h's rule 1, deps.mk's and wasm.mk's own sqlite3 paragraphs) or is dropped where the sentence already says what the document did; generated outputs (dist/HANDBOOK.md, REPORT.md under COV_DIR) and the guides' pattern are left as they are. Not touched, being other branches' files: tacky/xmpp/modules/ (about 110 such citations), ext/core/keyring.c and keyring.h, ext/core/tests/test_tls.tcl, tacky/core/keyring.tcl and storage.tcl, and the schema's doc strings. Only comments changed; check-native-neutral, check-headers, check-api and test-tacky are green. 2026-10-07 20:42:28 +03:00
ext A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
platforms platforms/ios.mk makes the SQLCipher amalgamation once, before the slices, so a clean parallel iOS build no longer races on it. Each slice's ios-lib generated the shared amalgamation (deps.mk: rm -rf of the source tree, tar xzf, make sqlite3.c) when it found it missing, and under -j both slices found it missing: in a clean anotherim-swift build on Xcode 27 the simulator slice's rm -rf ran while the device slice was generating sqlite3.h ("couldn't open .../ext/fts5/fts5.h"), and the backend failed; a second run went through because the amalgamation was then there. The amalgamation is now a rule of ios.mk (deps.mk's sqlcipher-sources with HOST_TCLSH as TCLSH9), and each slice's libtacky-<sdk>.a and ios-deps depend on it instead of on the tarball. A clean -j build of this tree through anotherim-swift's gmake backend TACKY=../libtacky on the Mac unpacks SQLCipher once, before both ios-lib, and writes the xcframework with both slices. 2026-10-10 13:47:35 +03:00
schema Merge remote-tracking branch 'origin/develop' into feat/edit-history 2026-10-13 01:40:56 +03:00
tacky The store zeroes what is deleted or overwritten (PRAGMA secure_delete = ON on every connection) and truncates its write-ahead log when it opens (wal_checkpoint(TRUNCATE), no error when a reader holds it), so a removed value cannot be read back from a copy of the file: the app's old PIN hash, which another.im 0.5.0 wipes as its lock becomes the device's own (the app's review, 2026-10-13), a retracted message, a forgotten key. test_storage_package checks the pragma, and that a value large enough to spill onto overflow pages, set and then overwritten, is in neither the file nor its log afterwards -- which fails without the pragma -- and that the log is empty after the next open. 2026-10-13 02:18:47 +03:00
tacky-core A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
tacky-crypto A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
tacky-keyring A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
tacky-media A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
tacky-omemo Merge fix/omemo2-spk-verify: an OMEMO 2 bundle in OMEMO 0.3's form is refused by name, own bundles round-trip through the fetch path, and an unusable pre-key id is warned about once per device 2026-10-12 15:57:26 +03:00
tests The store zeroes what is deleted or overwritten (PRAGMA secure_delete = ON on every connection) and truncates its write-ahead log when it opens (wal_checkpoint(TRUNCATE), no error when a reader holds it), so a removed value cannot be read back from a copy of the file: the app's old PIN hash, which another.im 0.5.0 wipes as its lock becomes the device's own (the app's review, 2026-10-13), a retracted message, a forgotten key. test_storage_package checks the pragma, and that a value large enough to spill onto overflow pages, set and then overwritten, is in neither the file nor its log afterwards -- which fails without the pragma -- and that the log is empty after the next open. 2026-10-13 02:18:47 +03:00
tools A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
.gitignore The XMPP half of the Tcl tree moves under lib/xmpp and the tacky::xmpp packages, the first step of the rename pass. lib/taco/session becomes lib/xmpp/session with package and namespace tacky::xmpp::session (::tacky::session:: renamed everywhere as one token); lib/taco (taco.tcl, redact.tcl, modules/) becomes lib/xmpp/client (client.tcl, redact.tcl, modules/) with the package tacky::xmpp::client; the package xmpp becomes tacky::xmpp; tests/taco and tests/taco_integration become tests/client and tests/integration, all by git mv. lib/taco/pkgIndex.tcl goes and lib/xmpp/pkgIndex.tcl registers tacky::xmpp and its session, client and component subpackages, since pkgUnknown looks only one level below lib/. The internal names stay: the taco_* snit types, :🌮:, procs, State keys and every package version. Everything that named a moved path or package follows in this commit: tests/run.tcl's suite directories and auto_path, test_session.tcl's auto_path and test_session_scram.tcl's source path, deps.mk's load check list and comments, .gitignore's tmp directory, tools/api/tcl.py's module paths, tools/stats.py's parts and test-to-part map, the schema's per-module tcl paths and doc strings (methods.tcl came out unchanged from make gen-api), README.md, ROBOTS.md (with a decision paragraph naming the old names), docs/, the READMEs of lib/, ext/, examples/ and tests/servers, and comments across lib/ and tests/; notes about what a file was called before 2026-10-07 and the paths of the legacy repositories keep their old names. Checked in the container: make test-tacky (tests/run.tcl 4551 total, 4550 passed, 0 failed, 1 skipped, as before; the load check sees tacky::xmpp, tacky::xmpp::client and tackywire; 22 integration suites load), test-tackykeyring and test-tackycore (the tests/tcl suites), test-tk-example, test-build (24 passed) and check-api. 2026-10-07 12:49:27 +03:00
.gitmodules native/ dissolves: tacky-core, tacky-crypto, tacky-keyring, tacky-media and tacky-ratchet move to the repository root with git mv, the libdatachannel submodule's path following in .gitmodules, so the parts a host builds sit beside ext/, lib/ and embed/ instead of one level down. Every path that named native/ follows: the root Cargo.toml's members, deps.mk, the Makefile, coverage.mk, platforms/*.mk, the Windows image's script and Dockerfile, tools/stats.py and check-core-header.py, test_build.sh, the bindings' and Tcl modules' comments, the crates' own READMEs and the docs, with HANDBOOK.md regenerated from them. check-native-neutral keeps its name and now scans the tacky-*/ directories by glob, pruning the submodule as before; its rule reads that nothing under a tacky-*/ directory knows Tcl. Checked in the container: test-rust, check-headers, check-native-neutral, check-api and test-build (24 of 24) pass. 2026-10-07 09:15:32 +03:00
coverage.mk Closing the media backend that holds the native log sink no longer silences every other backend's engines, and an older-minor library's capabilities no longer hand the binding a NULL name (audit ext-c S1, S2). The sink is process-wide and the newest open with an on_log owned it; its close cleared the sink and set the level to none, and tacky_media_set_log_level acted only for the owner, so after closing backend B the binding's log-level, which tells every open backend, reached only A, a non-owner, and A's libdatachannel, rtc-ma and rtc-mv lines stayed dropped until another open. Both implementations now hand the sink on at the owner's close to the newest other open backend opened with an on_log, at the level set (tacky_media.c keeps a list of open backends under g_log_lock, webrtc's logging.cc a list of the sinks given), and set_log_level on a backend opened with an on_log takes the sink over; tacky_media.h says so, and media.c's log-level comment now holds. capabilities asked the library for all 15 flag names, and a -lib library of ABI 1.0 or 1.1, which open accepts, has no name for 1.2's dataBackpressure and answers NULL; the binding now asks only for the flags the negotiated minor defines and skips a NULL name. test_contract gains logsink (both implementations: a closed owner's sink reaches the other backend, and set_log_level takes it back; it failed at its first check before this change), test_media.tcl tackymedia-log-after-a-close (no line after the owner's close before) and tackymedia-capabilities-older-minor over libtackymedia_oldabi.so, a new test shim (ext/media/tests/oldabi.c, built beside nofd by tacky-media-testnull) that answers ABI 1.1 and no 1.2 name (15 flags with an empty name before). 2026-10-07 20:35:46 +03:00
deps.mk The OMEMO test peers come in three kinds and get a suite of their own for OMEMO 2 interop. One omemo-bot container now runs every bot OMEMO_BOTS names, by default bot@ in mode legacy (OMEMO 0.3 alone, as Gajim), bot-v2@ in mode v2 (urn:xmpp:omemo:2 alone with XEP-0420, as Kaidan) and bot-dual@ in mode dual (both, each peer device under the best both speak, as Conversations with OMEMO 2), on slixmpp 1.8.5, slixmpp-omemo 2.2.0, OMEMO 2.1.0, Oldmemo 2.1.0 and Twomemo 2.1.0, pinned. slixmpp-omemo 2.2.0 loads both backends but has no XEP-0420 (it never encrypts under :2 and raises NotImplementedError on a :2 payload), so the new omemo_peer.py, shared by the bots and the smoke test, keeps its plugin for the PEP side, loads only the mode's backends (and drops the +notify of a version the mode does not speak), and writes and reads the SCE envelope itself under libtacky's rules (rpad and from required, to required in a groupchat and matching whenever present, time within an hour); a stanza carries legacy's <encrypted/> before :2's. A bot echoes, answers INFO:<text> with the version that opened our message and the versions our stanza carried, still floods on BURST:n, joins a room on JOIN:<room> or an invite, and answers every live room message from someone else encrypted to the occupants it can reach. with_bot.sh registers the bots, waits for each to come online, exports OMEMO_BOT_JID (the legacy bot, so test_omemo*.tcl are unchanged) and OMEMO_BOT_LEGACY_JID, OMEMO_BOT_V2_JID, OMEMO_BOT_DUAL_JID (passed into the image by in_image.sh), and tags the image by a hash of the directory so rigs of other checkouts never retag it mid-run; bot.py redials once per failure instead of once per event. run-smoke.sh checks the bots from Python: a client of each mode against each bot (the shared version both ways, legacy against v2 refused), a members-only non-anonymous room where one dual message carries both elements and the legacy and v2 bots both answer readably, and the envelope checks; 21 passed on ejabberd 26.09 and on Prosody 13. tests/integration/test_omemo2_interop.tcl, on its own account omemo2@ (added to _lib.sh's USERS with the smoke accounts), asserts through the account's XML console which <encrypted/> elements each stanza carried: 1:1 with each bot, each bot's device profiles, a room with a legacy-only member and one with a v2-only and a legacy-only member at once, our second device (a second backend on the same account) reading the carbon of a legacy and of a v2 message and the peer's answer, and omemo mode (auto by default, legacy to a dual peer, modern to a v2 peer and refusing a legacy one). Cases that need OMEMO 2 carry the constraint omemo2 (the binding seals under -profile v2 and the omemo module lists v2), two carry noOmemo2 and document today's build: the dual bot is spoken to in legacy, and a v2-only peer is not an OMEMO peer at all, so the message goes out unencrypted. The Prosody rig loads mod_carbons now, as ejabberd's has all along, for the second-device cases. Today 7 of the 17 cases run and pass on both servers, 10 are skipped by constraint until OMEMO 2 lands; test_omemo.tcl (10) and test_omemo_peers.tcl (2) pass beside the three bots on both servers, and the whole Prosody integration set passes with carbons on (107 passed, 0 failed, 63 skipped by constraint). 2026-10-10 07:54:15 +03:00
LICENSE libtacky v0.9.9-alpha3: the whole tree as of this release, in one commit (the user chose to fold the history once, at this tag, so the forge's commit list is readable; from here on every branch lands as one squashed commit). The full history up to this point, 1,000-odd commits with the v0.9.9-alpha1 and v0.9.9-alpha2 tags, is kept unchanged on the branch archive/pre-alpha3 (43b06d4), whose tree is byte-identical to this one. 2026-10-02 02:46:29 +03:00
Makefile A live message asking both an XEP-0184 receipt and XEP-0333 markers is answered by one stanza carrying both <received/> elements, and neither a receipt nor a <received/> marker asks the archive to keep it any more; only a <displayed/> still carries <store xmlns='urn:xmpp:hints'/>. A user's catch-up after a few hours away brought 14.7 MB in 4,242 stanzas over some five minutes, and the XML console showed the archive handing back our own devices' bodyless receipts: AutoReceipt sent two stanzas per incoming message, each with <store/>, so every message received by any of our devices added two archived stanzas that every other device then walked and parsed (message.tcl SendMarker). Our own receipt moves nothing on our other devices (ApplyMarkerVerdict's own branch reads only displayed, which keeps its hint and with it the read-state sync, beside XEP-0490), the peer's online devices have the receipt live (to the full JID, and carbons), and Conversations sends the pair as one stanza with no hint. What another client can see changes: on ejabberd, whose mod_mam keeps a bodyless message only with <store/>, a peer's device that was offline when our receipt came no longer finds it in its archive, so its row shows delivered only once our displayed comes; Prosody archives receipts by its own policy either way. Measured with tests/integration/bench_catchup.tcl on the ejabberd rig (1,000 rounds, the archive filled the old way and, BENCH_MIX=merged, the new): what the account walks shrinks from 5,334 stanzas and 6.08 MB in 22 pages to 2,906 and 4.06 MB in 11, since ejabberd keeps neither the merged pair nor the peer's, and the walk's CPU from 6.5 s to 4.9 s (both runs counting bytes); an archive already full of the old receipts is walked as before, and they keep coming from any of our devices or peers still running the old code. test_message.tcl's message-autoreceipt-and-received-marker-go-out-as-one-stanza and message-autoreceipt-goes-to-the-full-jid-without-a-store-hint fail without the change; test_message 449 pass. 2026-10-12 09:34:20 +03:00
README.md The README's figures are the tree to be tagged v1.0.0-rc3: tools/stats.py --readme, the coverage figures still those of the 2026-10-07 run. rc3 is rc2 and secure_delete with the write-ahead log truncated on open. The gate on this tree: the Tcl unit suite 5,365 passed, 0 failed, 1 skipped on both media lanes (webrtc and datachannel); the Rust suites pass but for cov_transport's a_dead_first_address_does_not_cost_the_whole_step, which this host's instant refusal fails as before; the integration suites on Prosody 13 166 passed, 0 failed, 19 skipped, and on ejabberd 26.09 170 passed, 0 failed, 15 skipped. 2026-10-13 02:31:01 +03:00
release.mk Comments, build files and the crates' docs no longer cite documents this tree does not have: TRANSFER.md, HTTP-TCL.md, LOCALSTORE.md, ARCHITECTURE.md (the workspace's §9.1 and §9.2), MEDIA.md, JINGLE-FT.md, LIVENESS.md, NOTIFY.md, CHATLIST.md, THREADS.md, STORAGE.md, CHAOS-FINDINGS.md, ONE-CLIENT.md, ROADMAP-another-tacky.md, docs/DEPLOY.md, docs/STABILIZE.md, docs/discovery.md, docs/storage-lock.md, reports/OMEMO_issues.md and reviews/2026-09-*, nor ROBOTS.md by section numbers it lost long ago (§2.2, §7.1, §8, §10.80 and the rest). Each pointer now names the place that exists (tacky/http/README.md, tacky-keyring/README.md, ROBOTS.md's HTTP, "The store", "The shape" and rule 2 or 6, tacky_media.h's data channel section, session/liveness.tcl, modules/file.tcl, ext/common.h's rule 1, deps.mk's and wasm.mk's own sqlite3 paragraphs) or is dropped where the sentence already says what the document did; generated outputs (dist/HANDBOOK.md, REPORT.md under COV_DIR) and the guides' pattern are left as they are. Not touched, being other branches' files: tacky/xmpp/modules/ (about 110 such citations), ext/core/keyring.c and keyring.h, ext/core/tests/test_tls.tcl, tacky/core/keyring.tcl and storage.tcl, and the schema's doc strings. Only comments changed; check-native-neutral, check-headers, check-api and test-tacky are green. 2026-10-07 20:42:28 +03:00
ROBOTS.md The ejabberd rig grants the component XEP-0356 privileges when XMPP_COMPONENT_PRIVILEGE=1: mod_privilege with roster both, message outgoing and presence roster for component_acl, the rig's one component, which is what a gateway's deployment (anotherim-server's ejabberd.yml) grants and what tackygram's roster push needs its end-to-end test to run under. Off by default, since with it on the server sends the component a <message> carrying its grants on every connect, which the component suite does not expect. 2026-10-13 01:36:05 +03:00
tcl.mk Comments, build files and the crates' docs no longer cite documents this tree does not have: TRANSFER.md, HTTP-TCL.md, LOCALSTORE.md, ARCHITECTURE.md (the workspace's §9.1 and §9.2), MEDIA.md, JINGLE-FT.md, LIVENESS.md, NOTIFY.md, CHATLIST.md, THREADS.md, STORAGE.md, CHAOS-FINDINGS.md, ONE-CLIENT.md, ROADMAP-another-tacky.md, docs/DEPLOY.md, docs/STABILIZE.md, docs/discovery.md, docs/storage-lock.md, reports/OMEMO_issues.md and reviews/2026-09-*, nor ROBOTS.md by section numbers it lost long ago (§2.2, §7.1, §8, §10.80 and the rest). Each pointer now names the place that exists (tacky/http/README.md, tacky-keyring/README.md, ROBOTS.md's HTTP, "The store", "The shape" and rule 2 or 6, tacky_media.h's data channel section, session/liveness.tcl, modules/file.tcl, ext/common.h's rule 1, deps.mk's and wasm.mk's own sqlite3 paragraphs) or is dropped where the sentence already says what the document did; generated outputs (dist/HANDBOOK.md, REPORT.md under COV_DIR) and the guides' pattern are left as they are. Not touched, being other branches' files: tacky/xmpp/modules/ (about 110 such citations), ext/core/keyring.c and keyring.h, ext/core/tests/test_tls.tcl, tacky/core/keyring.tcl and storage.tcl, and the schema's doc strings. Only comments changed; check-native-neutral, check-headers, check-api and test-tacky are green. 2026-10-07 20:42:28 +03:00

libtacky

An XMPP client library in Tcl 9, Rust and C. The protocol, the local store, end-to-end encryption, calls and file transfer live here, behind one JSON wire API that apps speak, and as Tcl packages that programs and gateways load directly. No client of libtacky speaks XMPP itself.

Version 1.0.0, beta: the wire API is frozen but may still change before 1.0.0 proper. Licence GPL-3.0 (LICENSE).

What you get

  • A backend for apps. dist/libtacky.a is the whole library with its Tcl interpreter inside, driven through three C functions (embed/tacky.h) with JSON requests, replies and events. dist/tackyd-json is the same backend over stdin and stdout, for a host that would rather spawn a process than link. platforms/wasm.mk builds it for a browser worker. The contract is schema/tacky.schema.json; the handbook generated from it (make handbook, dist/HANDBOOK.md) documents every module, method, event and type, and the hosts' bindings are generated from the same file.
  • Packages for Tcl programs. The tacky::* packages under tacky/ load without the client's apps: tacky::xmpp::session and tacky::xmpp::client; tacky::xmpp::component for an XEP-0114 external component, which is how a gateway or a service bot is written; tacky::core for the stream, XML, JSON and tacky::core::net (the socket dial and TLS); tacky::http for HTTP uploads and downloads over them. dist/tackyd runs such a program on a machine with no Tcl, either a script or the main.tcl of a zip appended to a copy of it. The handbook's Tcl reference lists every public command.
  • The native parts as crates with C ABIs: tacky-core (the stream core, Rust), tacky-crypto, tacky-keyring and tacky-omemo (Rust), and tacky-media (C and C++) for calls.

Architecture

 app or tackyd-json over libtacky.a; a browser worker over the wasm build
   │  JSON  ["module","method",{args},token] →   ["result"|"error",token,…] ←
   ▼
 embed/tacky.c      one backend thread, one Tcl interpreter
   ▼
 tacky/core/wire    dispatch through the schema's allow-list; replies and events
   ▼
 tacky/xmpp/client  the client: one module per area (account, roster, message,
   │                muc, omemo, calls, groupcall, file, …)
   ├── tacky/xmpp/session   the XMPP session, in Tcl
   │     └── tacky::core    tacky-core (Rust): resolve, connect, TLS, XML, JSON
   ├── tacky/core/storage   one SQLCipher store per account (ext/sqlite3)
   ├── tacky::core::keyring tacky-keyring (Rust): the key vault
   ├── tacky::omemo         tacky-omemo (Rust): the OMEMO ratchet
   ├── tacky/http           HTTP/1.1 in Tcl over tacky::core::net: its
   │                        socket, its TLS (pluggable) for https
   └── tacky::media         calls: webrtc | datachannel | host | remote

 tacky/xmpp/component      an XEP-0114 component over the same session,
                           with no client loaded

The session is Tcl, the stream is Rust. tacky-core owns one connection per handle: it resolves the domain (SRV, XEP-0156 host-meta), walks the candidates (STARTTLS, direct TLS, WebSocket), verifies the certificate (rustls, the system roots plus the user's pins and exceptions) and turns bytes into elements and back under bounds, handing them to Tcl as plain C structs. Everything an element means is Tcl: stream negotiation, SASL and SASL2, Bind 2, FAST, SCRAM with channel binding, stream management with resume, CSI, liveness and reconnects, and above the session every feature of a client.

One contract. The schema lists every module, method, argument, reply, event and type. A method not in the schema does not exist; the dispatch allow-list, the handbook and the hosts' bindings are generated from it.

The store. Each account has its own SQLCipher database, unlocked by a data key sealed in the keyring with Argon2id. A stanza is acknowledged to the server only after it is stored.

HTTP. Every HTTP exchange is the library's own: tacky/http, a Tcl library speaking HTTP/1.1 over tacky::core::net (its socket, and its TLS with the connection's roots and the user's pins, pluggable). Plain http goes only to this machine, https never downgrades, and any number of uploads and downloads run side by side without stalling the event loop. Apps reach it through the http module, Tcl programs through tacky::http::get, post and put.

Media. Calls are Jingle in Tcl; the media engine is one of tacky-media's two implementations of one C API, libwebrtc (webrtc, the default) or libdatachannel with the rtc-ma and rtc-mv engines (datachannel), or the host's own (host, remote). A build carries one native backend. Group calls are XEP-0272 Muji: a room and a mesh of one-to-one legs.

Trust. A peer's from, delay stamps, hashes and ids are checked before they are believed; a certificate is accepted only through verification, a pin or a time-limited exception; secrets never reach a log.

Boundaries. Three languages, each where it earns its place, and a rule for every crossing.

layer language why there
the stream core, the socket dial and TLS, the XML and JSON parsers, the keyring, the crypto primitives, the OMEMO ratchet Rust these are the parts that read bytes from strangers and hold keys: parsers under bounds, rustls instead of OpenSSL, Argon2id, X3DH and the Double Ratchet, all memory-safe and written once for desktop, mobile and the browser (the same crates build as static libraries and as wasm)
the Tcl bindings in ext/, the media engines in tacky-media, the host entry in embed/, SQLCipher C and C++ Tcl's extension API is C, so each native part has one thin binding that turns its ABI into commands, channels and events; libwebrtc and libdatachannel are C++ and C and real-time media wants no interpreter in the path; tacky.h is what a host links
the session, the client, the component, the wire, the store's logic, HTTP Tcl everything that means something is a state machine over elements and dicts, where an event loop, introspection and fast tests pay; one script tree serves every host, and 4,700 tests run it without a server

The crossings are narrow and all alike. A crate exports a C ABI (tacky_core.h, tacky_ffi.h): plain C structs and byte views, no JSON, no text parsed twice, and nothing under a crate knows Tcl (check-native-neutral enforces it). A binding in ext/ wraps that ABI once: a stream or a keyring call becomes a Tcl command, TLS becomes a stacked channel on any Tcl channel, a connected socket becomes an ordinary Tcl channel, and anything that completes later arrives as an event source on the Tcl event loop, polled, never a callback from a foreign thread. The Tcl packages (tacky::core, tacky::crypto, tacky::omemo, tacky::media::rtc) are those commands, and the rest of tacky/ is Tcl over them. Above it all sits one more crossing, the wire: a host, in any language, speaks JSON frames to embed/tacky.c or to tackyd, and the browser worker speaks the same frames to the wasm build. So a host never links a crate directly, a crate never sees an interpreter, and the schema is the only contract a host has to know.

component language what it is code tests coverage
core Tcl tacky/core: tacky::core::wire, the wire, a host's calls dispatched through the schema's allow-list, replies, events; tacky::core::storage, the store, taco_store over the sqlite3 package, keyed with the key the keyring wraps; tacky::core::keyring, the typed layer over the vault: the .keys file, its slots and autolock, with dicts in and out 1,817 1,601 98.9 % procs
C ext/core: tacky::core, the binding of tacky-core: the stream, the TLS channel, the XML fragments crossing as node dicts, json over serde_json, and the keyring's native calls and the profile lock; ext/common.h, shared by the bindings; tacky-crypto/sqlcipher: the SQLCipher provider on tacky-crypto (the sqlite3 package itself is the SQLCipher amalgamation, built and not counted) 3,768 3,134 95.5 % lines
Rust tacky-core: the stream core: SRV and host-meta, TCP, STARTTLS, direct TLS and WebSocket over rustls, the certificate verdicts, the bounded XML stream; sans-io TLS and the C ABI, which also carries the keyring's; tacky-keyring: the key vault: Argon2id, XChaCha20-Poly1305 10,723 6,624 97.8 % lines
crypto C ext/crypto: the tacky::crypto binding: the push envelope, boxes, AEAD, hashcash, digests, HMAC, PBKDF2 531 407 97.0 % lines
Rust tacky-crypto: SHA-1/2/3, HMAC, HKDF, PBKDF2, AES (CBC, CTR, GCM) and X25519 over the RustCrypto crates, with a C ABI 543 1,375 91.5 % lines
http Tcl tacky/http: tacky::http, HTTP/1.1 for uploads and downloads over tacky::core::net's socket and a pluggable TLS wrap (rustls by default): handles, transfers side by side on the event loop, redirects, the loopback rule for plain http, the one-call verbs 1,033 2,056 100.0 % procs
media Tcl tacky/media: the tacky::media contract calls run on, and its backends: the native adapter over ext/media (webrtc or datachannel, one per build), host and remote 996 1,895 96.3 % procs
C ext/media: tacky::media::rtc over tacky-media's C ABI and tacky::media::view, the Tk viewer of a frame stream; tacky-media: libtackymedia, the media library's C API (tacky_media.h) over libdatachannel, rtc-ma (miniaudio devices, Opus, the jitter buffer, RTP) and rtc-mv (V4L2 and test cameras, VP8 over libvpx, RTP, frame streams) 9,409 10,318 --
C++ tacky-media/webrtc: libtackymedia_webrtc, the same C API over libwebrtc, with its frame stream writer 3,714 271 --
omemo C ext/omemo: the tacky::omemo binding of the ratchet; its Tcl side, omemo.tcl, is the client's 858 371 98.3 % lines
Rust tacky-omemo: the OMEMO ratchet: X3DH, the Double Ratchet, XEdDSA, the wire framing and payload cipher of OMEMO 0.3 and OMEMO 2, under one identity, with a C ABI 1,767 3,774 97.2 % lines
xmpp Tcl tacky/xmpp: JIDs (RFC 7622), the stanza node, xsearch, data forms and XEP-0420's envelope; session, the XMPP session over the stream core (SASL2, Bind 2, FAST, SCRAM, stream management, CSI, liveness and reconnects, the XML console); client, one module per area (accounts, roster, messages, MUC, OMEMO, calls, files); component, the XEP-0114 host with its iq router and the XEP-0030 and XEP-0050 responders; and tests/driver, backend driven in process 48,569 85,963 99.7 % procs
everything else C, Tcl, Python, sh, make, CMake embed/ (the host side: tacky.h, tackyd, the image builder, the browser notifier), examples/, tools/, the test runner and server scripts, the build files (tacky-media's among them); not in the total 9,450 3,521 --
total (without everything else) Tcl 52,415 · C/C++ 18,280 · Rust 13,033 83,728 117,789 Tcl 99.6 % procs · Rust 97.3 % lines · C 96.1 % lines

One row per component and language, the component named on its first row. code: lines of code, without blank lines, comments and generated files; the total counts the components alone, by language in its second column, and leaves the everything else row out. tests: the lines of test code that exercise that part (tools/stats.py's COMPONENTS and TEST_RULES say which go where; an extension's Tcl suites count as tests of its C). The total and the summary count the library alone, its Tcl, C, C++ and Rust and their tests; the everything else row is shown but not counted. coverage: Tcl the procedures of tacky/ some suite called, Rust and C the lines run, from the make coverage-all run on 2026-10-07 at commit 4201d46c.

The architecture in detail, the decisions behind it and the rules for agents working here are in ROBOTS.md. The handbook's prose is under docs/.

Features

Every specification the library touches has a row in docs/features.md, with what is implemented, what is missing and a status. The same table is a part of the handbook and is what schema/tacky.doap (XEP-0453) is generated from.

status meaning specifications
🔵 MOSTLY implemented and its tests pass; not yet verified by hand against real servers and clients 110
🟡 WIP implemented but not finished, or not fully covered by tests 19
🔴 TODO not implemented; at most reserved in the API 33
🟢 DONE verified and stabilised, set by a maintainer by hand 0

Two things worth knowing before the table: OMEMO speaks both deployed versions, OMEMO 0.3 and OMEMO 2 (with XEP-0420), under one identity, each device written under the newest it speaks and a mixed chat getting both elements; and the rows marked "reserved in the API" are methods that exist in the schema and answer "not implemented", so a host can be written against them today.

Status and tests

Beta: the API is settling, though breaking changes may still be made. Every layer is tested against in-process peers: a fake server with real SCRAM, TLS and WebSocket on loopback, a scripted stream relay, the fault shims of the C bindings, and the picomemo oracle and libsignal's vectors for the ratchet. The integration suites (tests/integration) run the client against a real server in a container, with independent OMEMO echo bots beside it (python-omemo: one OMEMO 0.3 only, one OMEMO 2 only, one speaking both). The two rigs run side by side on their own ports; on the tree tagged v1.0.0-rc2:

server passed failed skipped
Prosody 13 166 0 19 (notProsody: MUC/Sub, ejabberd's registration challenges and OAuth page; separateBind: neither rig server binds separately; the CSI case Prosody does not offer)
ejabberd 26.09 170 0 15 (notEjabberd: Prosody's OAuth, XEP-0494, a shared-nick case; separateBind; the no-CSI case)

Nothing has yet been verified by hand against public servers or other clients, which is why no feature row is DONE.

83,728 lines of code and 117,789 lines of tests (the table under Architecture), 6,865 of them the integration suites run against a real server; 7,236 test cases defined (below). Coverage: Tcl 99.6 % procs · Rust 97.3 % lines · C 96.1 % lines (the make coverage-all run on 2026-10-07 at commit 4201d46c).

The figures here and under Architecture are generated by tools/stats.py (make docs); the coverage ones come from the last make coverage-all, whose full report is _build/cov/REPORT.md.

suite family files cases
Tacky unit suites (tests/client, tests/http, tests/tcl) 187 5384
Tacky integration suites (tests/integration, need a server) 27 183
Rust: tacky-core 48 341
Rust: tacky-keyring 5 46
Rust: tacky-crypto 9 64
Rust: tacky-omemo 14 166
Tcl extensions (ext/*) 19 1008
C: tacky-media: libtackymedia, rtc-ma, rtc-mv (ctest programs) 41 41
C++: tacky-media: webrtc (ctest programs) 3 3
total 353 7236

Building

Everything builds and tests in libtacky's container image; the machine needs only git and docker or podman. Every make target on the host runs inside the image.

git clone --recurse-submodules ssh://git@dev.another.im:3022/another-im/libtacky.git
cd libtacky
make all test               # build everything, run every suite that needs no server
make all test REQUIRE=1     # the gate: a missing prerequisite fails instead of skipping

MEDIA_BACKEND picks the one native media backend of a build: webrtc (the default, a pinned prebuilt libwebrtc), datachannel (libdatachannel with Opus and VP8 in process, the submodule under tacky-media/third_party checked out) or none.

target what it does
make, make dist build everything; dist/libtacky.a
make tackyd-json dist/tackyd-json, the backend behind length-prefixed JSON on stdin and stdout
make tackyd dist/tackyd, the interpreter running a Tcl program on the tacky:: packages
make test every suite that needs no server: Rust, the bindings, the Tcl suites, test-build, check-api
make test-tacky-prosody, make test-tacky-ejabberd the integration suites against a server the target starts in a container
make coverage-all the whole-tree coverage run; make stats then writes the README's numbers
make docs the handbook, the README's generated sections, the DOAP file, the Rust API docs
make host-web, make host-swift what the browser and Apple hosts build against
make help every target, and where each one runs

Using it

An app links embed/tacky.h and speaks JSON:

tacky *tacky_create(const char *const *backend_args, tacky_emit_fn emit, void *ud);
void   tacky_send(tacky *t, const char *json, size_t len);   /* ["module","method",{args},token] */
void   tacky_destroy(tacky *t);                              /* replies and events arrive at emit */

A Tcl program loads the packages, from a built checkout (tacky/ and the built extensions under _build/ext/* on auto_path) or through dist/tackyd, and needs nothing of the apps' side:

package require tacky::xmpp::component
set h [tacky::xmpp::component new -domain gateway.example.org -secret $secret \
           -host 127.0.0.1 -port 5347 -command onEvent]
tacky::xmpp::component connect $h

The handbook (make handbook) has the wire API and the Tcl reference; examples/component/echo.tcl is a complete small component; ROBOTS.md is the map of the tree.