Blog Β· release
Latch 0.13.0
The Pup ID, consent and the safeword, packs, your own AI Handler, AI kennels and the chat bot. Migrations 13 to 19 (one transaction each). Built on 0.12.3 and 0.12.2, below, and on the 09-26 issue sweep merged from main β which neither of those releases carried, so it is new here.
- New in this release β the 09-26 issue sweep (merged from main; not in 0.12.2 or 0.12.3):
- Achievements (#25): the ten core-stat badges β First Latch, Seven Days, A Month Kept,
Good Dog, Long Haul, Said Out Loud, On the Record, Let the Wheel Decide, Still Here, A Year
Under β are real rules now, in a small
achievementsmodule, read as-of the sweep through the same arithmetic as the stamped photos, dated at the event that crossed the line, with their drawn art in the cabinet.tools/badges/badges.tomlis a mirror of the module's catalogue and a test holds the two together. - Gallery filters (#26): chips for check-ins, verifications and kit, a wardrobe chip for every gear category, and a category row under kit. The filter is the query string, so a filtered gallery is a link; a chip at zero is disabled, never hidden; an unknown filter is refused rather than quietly showing everything.
- Kit on the API (#24):
GET /kit,POST /kit/bout,POST /kit/bout/{id}/end,POST /kit/{id}/consumeβ hood on, hood off, lube, from a Shortcut. An item's name rides out on the inventory read only; the event feed carries ids. - Egress guard (#27): a test reads every module's imports and fails when a module opens a socket without declaring it β or declares egress it no longer has. The core is held to zero network imports.
- Fixed (#17): the undo refusal for a closed task pointed at a "Reopen" button that never existed. It now says what the manual says: closed is closed, assign it again.
- Fixed: a habit added this week was nagged about after 18:00 as an "empty board". A fresh habit says nothing until it has a week behind it.
- CI pipeline file for Woodpecker (#9): tests on push, the release guard and an image on tag.
-
docs/install.mdpoints at the setup wizard as the easy path (#7). -
The issue sweep, 0.12.3 and 0.12.2 under this release's rules. Kit on the API is the owner token's alone (a bot token gets 403 on all four routes, and a delegated voice is refused), and a bout's end echoes the clock as blank as
/statewhile a hidden lock runs. Log a finish is yours alone too β your AI, a Handler and the chat bot are refused β and, like a bout, it is on no bot feed, card or AI context. The achievements are filed under their own module, and what each is evidence of is declared there β all ten are your own game β together with the permission its number needs: a pack sees one under Your papers only beside that permission too (Long Haul the clock, Good Dog your duties, so pack-mates never get the six that state either), and the chat room names one only when it is shown that number itself (First Latch; Long Haul and A Year Under only with Show the clock; never a count of training, reflections, check-ins or spins β the room gets today's duties as flags and the clock as time left). The kit's badges answer to the same rule: with a cage in service every lock is a stretch of wearing it, so Well worn, Second skin and Workhorse or Veteran on the cage state the time locked and need the clock beside Badges earned with kit (pack-mates never get them), and Hands free and Well trained need the lock status; a badge whose module is unknown is judged as kit, figure and all. A playset's badges too, by their rule: locked on N days and a count of spins need the clock, a count of training, reflections or check-ins the duties, of locks the lock status, and a finished badge the permission for every number its rule states (locked on 25 of 31 days the clock, reflected on 20 the duties too) β pack-mates get an event's joined badge, a count of locks only with the lock status, and never one that states the clock or the duties β and one counting your kit (it rode under Badges earned with kit), your health, a refusal or any other kind is shown to nobody. A playset badge keeps the rule it was earned under, and after an upgrade of its playset is judged by the stricter of that and the new one, so an upgrade never shows others a badge the old rule hid (migration 20). A badge of a module this Latch does not know is shown to nobody (it fell through to Your papers); the pack's Collared and A week in the pack need Your papers alone. A recorded track's title never reaches a model or the room. The gallery's filters never show a photo that arrived from another box β the page says how many there are and links to What travels. Adding a finish and starting a track run off the event loop like every other write. CI installsgit(the release guard's test lists tracked files) and the newcryptographydependency. The release tarball carries the achievements' art β it had left out every image, so an install from it drew the emoji β and refuses to build while any tracked image carries metadata (an image generator's prompt in a PNG, a phone's EXIF). It judges a file by its bytes, not its name β a PNG tracked asartorart.png.origis vetted like any other image β and refuses any other binary it cannot read. -
AI kennels (guide). A box whose Pup ID says it is an AI hosts a pack its model handles. For each collared member, once a cycle and one model call each, the model reads only what a Handler's kennel view shows β the card that member's box signed for it (duties as they leave the box: never health), their signed grants, and the codes of their box's last twenty answers β and asks for a line and at most three commands, in the local AI's shape and through the same checks, each clamped to what that member granted and queued like any Handler's. The model is asked in three phases, with no transaction open while it thinks; its own switches (off, the operator's safeword, Prepare to move, the disk floor) are asked again after the model check and again when a reply arrives, and a reply that comes after any of them queues nothing; a model that is down queues nothing; a line telling a hidden lock's time applies nothing; a playset's badge reaches the model as from a playset, never by its title; a card of the wrong shape (validly signed or not) is refused when it arrives, and one member the kennel cannot read never ends a run for the others. Membership is let in automatically, a collar still waits for the operator's fingerprint check, countersignatures are automatic and say
ai, and the operator's command form is refused on an AI den. The AI page on a kennel's box lists its members and what the model did, and Think now thinks about each. - Packs (migration 17, Pack in the menu, guide). Join a pack hosted on a
Handler's (or an AI kennel's) den with a collar tag β who is asking read offline, with zero
calls; fingerprints compared before a collar; the collar ceremony; grants with item-by-item
whitelists for photos and kit; the board, "show them", and kudos; a Handler's commands under
your caps; countersigned papers; leaving and what stays. A pup's box never accepts a
connection; a den answers only
/den/v1, best on its own listener (latch.den_app:app). Every envelope is sealed and signed; nothing unsigned changes anything on your box. - A restored box is sent its pack's board again when it re-binds, so the card you show your mates reaches them from the new box (a board is a cache and never rode the backup).
- A den cannot read when you paused out of its commands' answers. A command sent while you were paused (or packing to move) still never lands β but each command is judged against a stretch between two of your box's syncs (the one since the last sync; for a command that waited behind a full batch, the one before), and every command of a stretch a pause touched is answered alike β expired once the pause is over β so the answers say at most which stretch a pause touched, never when inside it; a command older than that is expired whatever happened then. When a command was sent is read on your box's clock, from the den's own answers, so a den clock running fast no longer lets a command sent in a pause land after the resume, and one running slow no longer has an honest command refused after a resume (migration 19 keeps the sync before the last). A box restored from a bundle made while packing keeps the record of when it was away.
- A den keeps what it understands of a grants statement from a newer box (a capability it does not know is ignored, the rest kept), and a statement it refuses clears the one it held.
- The Pup ID and export pages name every pack a
pup.jsonwould leave behind, with a link to leave each β pup.json carries no pack (a warning, never a block). On a retired box, which never syncs again, they point the way back instead of offering a leave that could not arrive. - The last state-changing pages that still wrote on the event loop (settings, activities, the Handler's check-in and spoken reflection, kit, trance, training, the photo check-in, habit and playset imports, the API's uploads) run in the threadpool too.
- Adding a calendar feed no longer holds the database while its first fetch runs (behind a slow calendar server the emergency key could fail with "database is locked"); a feed fetch ends at its deadline β its sockets shut, however slowly a server sends its headers or its TLS handshake β and a hosted instance's address check has a time limit too.
-
The lab (contributing):
tests/test_lifecycle.pywalks the whole lifecycle in one process, andtools/lab/labctl.pyruns it over real sockets on127.0.0.1β four boxes, two den listeners, a fake Ollama, a fake homeserver and the bot. -
The chat bot (guide,
tools/matrixbot/latch_matrixbot.py). One file, standard library only, run beside your box on the bot token from/consent: reminders when your reflection, check-in or mantra is due (once a day each),!status,!today(today's question and your AI's relay-safe line),!pauseβ the safeword, never a resume β and, only if you switched them on,!reciteand!log, always as now. It posts its own sentences and the box's two texts, never a field it does not know; news from your box is a whitelist of kinds, and a burst is a few lines and β¦and N more. It checks its room at startup and on every sync β invite or knock, not world readable, not encrypted (it cannot encrypt), no more members than you allow, you and it still in it, both tokens still good β and stops, posting nothing more, the moment one fails (exit 2). Its config must be0600; its box must be this machine unless you say otherwise; commands come only from you, only fresh, each at most once; the first run answers none of the room's history; β€ 1 message per 2 s and 30 an hour. While it runs the room is checked one change at a time β a gap the homeserver skipped read back first β so a rule broken and put back between two syncs still stops it; at a start it checks the room as it is then, and after a stop you fix the room and start it again. What the box withholds is never said later (the feed tells the bot how far it looked βX-Latch-Feed-Next), what has to wait is judged again by the box just before it goes, and while you are paused no voice's line reaches the room. Reminders arrive as ordinary messages, so they notify you β the reflection's from four hours before it is due (/bot/todaynow carries the box'shour). -
Your AI Handler (migration 16, AI in the menu, guide). A language model on your own Ollama speaks as your Handler: every few hours it reads the day's facts and may say a line and β only under what you tick on
/consentβ add time, freeze, assign training or write tomorrow's question, each capped per day and through the same checks a pack's Handler gets. It calls nothing until you give it an address and switch it on, and the address is re-checked on every call: plain http to this machine or your own network only (a VPN overlay address once you say it is yours), the model's exact name on that server, never a cloud model, never on a hosted instance. It sees facts and only your own words β never a playset's, a Handler's or its own earlier ones β and nothing of your health but "go gently". A line about a hidden lock's clock is dropped (and nothing else that reply asked for is applied); a line written knowing a hidden lock's clock is shown masked while it runs, and training it assigns is refused; a line naming your kit, repeating a training title or a name, or written knowing the clock, your kit or "go gently", is never relayed. "Your own network" is an allowlist (loopback, the private ranges, link-local, IPv6 unique-local) β never an address that only looks private (6to4, Teredo, NAT64, a proxy's fake-IP range). Chat with it on its page β talk only; never shown a hidden lock's clock; your reflections reach it there alone, under their own grant, never while paused β a safeword said while it checks the address included; the AI page names what each turn was shown (never the contents). The emergency key and the safeword never wait on a model. The footer says reaches out: ai while it is on; switched off, it calls nothing β its Check button included. Not on a hosted instance. - Lines from others on the Handler page: today's line from your AI and (with packs) from a Handler, labelled with the kind the signer declared; a line about time while your lock is hidden shows masked β tap to reveal. A pause, revoking Say a line or switching the AI off takes it off the page β and off the AI page's Today and the chat bot's today too.
/consentsays what your AI Handler is actually sent for kit: all your owned toys or gear, up to 40 of each, not "only the ones you tick" (that is the packs' rule).- A habit counted from your health data stays on your box. Tick "count as a habit" on Slept well (or make a habit of a health kind) and it is left out of everything that leaves β the chat bot's today, a pack's view, your AI's context β along with the reason it would have given; your own pages keep it.
- No training title reaches the room. A line β your AI's or a Handler's β that names any training title in your box is never relayed, checked again each time the bot reads it; the AI page's safe to relay is that same answer, read now.
- Your AI's chat no longer re-sends a reply it wrote knowing your reflections (or kit, or the clock) once you pause or take that grant back. Its Check button stays shut while packing to move or with the disk nearly full.
-
An AI's or a Handler's line now stores its words as
text(the chat bot reads either the old or the new shape). -
Your Pup ID (migration 15, Pup ID in Settings β System and the header chip, guide). A key that is you rather than your box: made with a loud warning, kept in
identity/pup.key(0600), sealed under a passphrase intopup.key.enc, optionally sealed at rest, with a printable recovery sheet β and no escrow, ever. β οΈ A new dependency:cryptography. - Papers: every badge you hold gets your key's signature, on the next tick, in a trophy case (Pup ID β Papers) that says what was signed and never "verified". Deleting a kit item keeps its badges' papers.
/export/pup.json: your pup without the game β the sealed key, papers, and the toys, gear and photos you mark π§³ (photos re-encoded, no EXIF; kit without its text fields). Arrive with it on your own new box: nothing mints, one Arrived from another box line in the log. π΄ It holds your sealed key: never hand it to anyone.python -m latch.restorerestores abundle.ziponto a fresh box, checks every table's count against the MANIFEST before committing, and pauses your packs and AI if you had granted anything. The bundle carries your sealed key (never the plain one) once you have set a passphrase; a bundle made before that brings the game back with no Pup ID β make a new one, or arrive yours from apup.json. A new Pup ID papers every badge afresh: the old key's papers stay, under Signed by an earlier Pup ID, and never travel.- Restore copies back only the photo files its rows name (anything else under
media/is skipped and counted), and refuses a box whose clock is behind the bundle's newest record β set the clock and run it again. - A box that lost its key takes it back: on the Pup ID page, from your
pup.jsonor your recovery sheet β only that pup's own key, with any papers the box lacks; its kit, photos and profile stay as they are. - Passphrases are tried one at a time, off the page's main thread, and after five wrong ones
in a row the box makes you wait (a second, doubling to a minute). Changing your passphrase
re-seals this box's copy only β a
pup.jsonor backup you already made still opens with the old one. - Stamped export refuses a photo that arrived from another box (it would stamp this box's history on it).
- Moving is explicit: Prepare to move stops everything that signs; Retire this box deletes its plain key once your pup lives elsewhere; Bring my pup back undoes it.
- π§³ Carry flags on photos (gallery, Pup ID β What travels; off by default) and on kit (the edit form; on by default): what travels to your own next box β never what a pack sees.
- β οΈ A photo that arrived from another box counts toward no statistic, card or recap.
-
Restore checks everything that can refuse β file paths, photo rows, the sealed key and your passphrase β before it writes, and commits the rows with the pause; a mistyped passphrase costs nothing. Retire this box asks for the passphrase that opens its sealed copy. Seal-at-rest needs a seal first. A command from anybody else while you pack to move is refused as paused.
-
A token for the chat bot (migration 14,
/consentβ The chat bot). A second kind of API token, for one machine: shown once, stored only as a sha256, revoked with one tap. It speaks only asbot, reaches only/api/v1/bot/today,/api/v1/bot/feedand/api/v1/pause(plus reciting today's mantra and logging a habit, if you switch that on β always as now, never back-dated), and never receives the clock unless you switch it on β never while a lock is hidden, not even as the moment a hidden lock turns done. The footer says reaches out: bot while one is live. See the API. What it relays is decided on the server: a badge's title only for a playset badge earned by your own game (one counting your kit, your health or a refusal is a badge), and only when the room is shown the number it states (a count of locks; a lock's time with Show the clock), a question somebody else wrote for today only under their lines' rules (your AI's own, never), and your box's localdayso its reminders follow your day wherever it runs. - β οΈ An unset
LATCH_API_TOKENturns off your own token, not the API: a bot token you made still answers its own routes (Settings β System now says how many are live), and a malformedAuthorizationheader is a 401 either way. - The safeword from the API:
POST /api/v1/pause, from your own token or the bot's. There is no resume on the API. -
β οΈ An unknown
X-Latch-Actoris now a 400 (it was recorded asapi). Useuser,apiorshortcut, or leave it out. -
Consent and the safeword (
/consent, guide). One page for everything anybody but you may see or do β per permission, per day, nothing ever ticked for you β a ceiling over all of them together, and π Pause everyone, which suspends every grant at once and can only be lifted there. Your AI Handler appears there once it is set up, and a pack's Handler and pack-mates once a den lets you in; until then it reads Nobody else. - Only your own voices count toward your streak and the heatmap (
user,api,shortcut,bot). β οΈ This applies to your whole log: writes previously recorded withX-Latch-Actor: aithrough your own API token no longer count toward the streak, the heatmap or the Insight tab, so an upgraded box can show a shorter streak than before. Their clock time and the XP already recorded for them are unchanged. - The API token no longer accepts
X-Latch-Actor: ai(norbot,systemorpack:β¦): those writes are refused with a 403. The token is your own hand; the AI acts only through what you grant on/consent. (Any other unknown value is now a 400 β see above.) - What only you can do is enforced for every other voice: locking, unlocking, notes,
photos, undo, taking time off, spinning the wheel, an open-ended freeze, your words, kit and
finishing training all refuse
ai(and the future pack and bot voices) with a 403.systemβ timed freezes expiring, overdue training, health and habit rewards β is unaffected. -
A badge now records which module awarded it and its XP, frozen at the award; older badges are filled in from the catalogue at boot.
-
Latch answers only to its own names. A name that is not in
LATCH_ALLOWED_HOSTS(or, with that unset, the one you gave on Settings β System) gets "This box does not answer to that name" β the DNS-rebinding read of/export/bundle.zipis closed. IP addresses andlocalhostalways work. A name is never learnt from a request: self-hosters, setLATCH_ALLOWED_HOSTS, or open the box by its IP address and name it under Settings β System. The setup wizard writesLATCH_ALLOWED_HOSTS=localhost. A form sent from another site is refused (403). Behind a reverse proxy, passHostthrough (or setX-Forwarded-Host) β a proxy that rewritesHosthides the name the guard checks. - The setup wizard publishes Latch on
127.0.0.1β this computer only β unless you tick Let other devices on my network open Latch on its Options page. The choice is a newLATCH_BINDline in the.envit writes (127.0.0.1, or0.0.0.0when ticked; empty or missing means127.0.0.1). β οΈ An install made by an earlier wizard is open to your whole network: its compose publishes"${LATCH_PORT:-8770}:8000"on every interface β whatever the guide said β and Latch has no login of its own. Re-run the wizard into the same folder (it rewrites the compose and the.env; your play data stays; paste your API token into its token field to keep it), or put127.0.0.1:in front of the port on thatports:line yourself anddocker compose up -d. β οΈ Re-running it without ticking the box closes network access you may have relied on β with your login (a reverse proxy) on the same computer, that is what you want (Authentication is yours). Over Tailscale or NetBird, leave the box off and put the computer's tailnet address inLATCH_BINDyourself: ticked means0.0.0.0β every network the computer is on, a Wi-Fi it joins later too β which is for a computer that never joins one you don't trust. Bound to the tailnet address, Latch no longer answers onhttp://localhost:<port>: open it athttp://<tailnet address>:<port>on that computer too. The wizard's own API now takes instructions only from its own page (JSON, from its own origin, with a session written into the page): a web page open in another tab could previously install, uninstall or reconfigure while it ran. And no other page may frame the wizard (X-Frame-Options: DENY,frame-ancestors 'none'), so none can steer its buttons. - Pages send
Referrer-Policy: same-origin(wasno-referrer, which made browsers sendOrigin: nullon the app's own forms). A referrer still never leaves the box, and a form now returns you to the page you sent it from. - A hidden lock stays hidden on the API.
/stateand every write that echoes the clock returnend_at,remainingand the numbers that add up to them asnullwhile a hidden lock runs β the wheel spin included, which also no longer echoes the session note; the lock event on/eventsand/session/undo/candidatecomes withoutbase_seconds; a refused unlock no longer prints the countdown. /handler/todayhas areasonβ one code for what matters now β and no longer carries a running event's banner line (the playset author's words).- Badges are awarded without the kit module. The sweep moved into the core tick.
- The footer tells the truth: reaches out: habits while a calendar URL is configured, local only otherwise. Settings no longer claims exports strip EXIF β the full backup keeps your original photos as uploaded.
- The backup carries your playsets' art (
playsets/inbundle.zip), and the JSON lists tables in an order that really is foreign-key safe. - Fixes: trance filed sits under the server's calendar day, not yours; the Health page's apply now button logged its rewards as you instead of the app; a non-ASCII bearer token answered 500 instead of 401; a migration that failed half-way could leave the database unbootable (from migration 13 on, each is one transaction).
- Before release (the whole-branch review): Your papers carries a collar β and a
Handler's countersignature β only to the pack it belongs to; a den queues no command for a
pup who paused, and the pup's box refuses one sent while it was paused or packing to move; a
countersignature lands only on a paper of that pair's collar, and the den never signs a
collar paper dated before its offer; bring my pup back re-binds every den; a member cannot
bury the others' queues (a clear of nothing queues nothing, a result counts only as the
answer to a command sent); a pup moved by
pup.jsonis told why its rejoin is refused, and the tag is not burnt; the board shows what each mate shows you, and the kennel view each member's photos; a slow calendar server, a lock wait or a slow disk no longer freezes the whole box (the tick and every state-changing route run off the event loop); an automatic den whose own listener cannot unlock a sealed key signs at the next sync; the kennel view reads a member's commands through an index (migration 18).