pyrtan / sourcev0.13.0 · main

Releases

v0.13.0latest

2026-10-27 · 895f92c · pyrtan-0.13.0.tar.gz

Public lobbies: --chat-public lets strangers chat without the token, people only; release 0.13.0

A hub that keeps a token could only chat with people who had it, so a public hub had to drop its token, opening /follow, /keep and the rest too. --chat-public ($PYRTAN_CHAT_PUBLIC) opens /chat to callers without the token and nothing else. Such a caller is a guest: it meets people, on this hub or on a hub it follows, and never a bot, because a bot runs the hub's trees and the token is what guards them. want: bot from a guest is refused (bots-need-token) and anyone is read as people.

The server takes a predicate for extra token-free paths; the hub passes the chat paths when the lobby is public. /api says chat_public, and chat_bots for the caller asking. The console offers a guest "people only" and says why, and opens a #chat link without the token.

The chat design note now says what it already did: there is no chat server and no seed, every hub started --chat is a lobby, and hubs that follow each other share their strangers.

v0.12.0

2026-10-27 · 26c93ae · pyrtan-0.12.0.tar.gz

Stranger is typing: a typing indicator for the chat lobby, on or off per person and per hub; release 0.12.0

While a person types, their client says so (POST /chat/typing, again every two seconds while they go on) and the partner's poll answers partner_typing: true; the console shows "Stranger is typing…", and "Bot is thinking…" while a bot's trees run. It is a state, not an event: never stored, gone with the next line or five seconds after the last word of it. A poll that says what it shows is woken when that changes; one that does not is never woken by typing, so it cannot spin.

It is on by default and can be turned off two ways. Per person, with typing_indicator: false on start, next or /chat/settings (the console's "Typing indicator" box, remembered in the browser; pyrtan chat --no-typing); it works both ways, like read receipts, so a seat with it off neither shares its typing nor sees anyone else's. Per hub, with pyrtan hub --chat --no-chat-typing ($PYRTAN_CHAT_TYPING=0).

Across hubs it travels as a signed Typing activity, on a change and at most every two seconds; the logs leave it out, and a hub that refuses it is not asked again in that conversation.

v0.10.0

2026-10-27 · bf4031b · pyrtan-0.10.0.tar.gz

Chat: strangers and bots, Omegle-style, on a hub and across a follow; release 0.10.0

A hub started --chat keeps a lobby at /chat. Anyone who can reach it takes a seat and is paired at random with someone else waiting here, with someone waiting at a hub this one follows, or with a bot. A bot is one holder of trees on the network, picked at random (interests first): a line is read against its triggers, then asked of the whole swarm, and what nobody answers it says so with the tree that would, as /intent does. An ask node's question is its next line and the reply is the answer; a line that is only greeting gets its introduction. Every bot line carries its trace. Nothing is kept: no names, no history, and a federated Say is logged without its text.

Across hubs, a waiting seat sends a signed Meet to the hubs its hub follows; lines go as Say and the end as Part, signed with the pair's secret. A Meet is answered only from a follower and never passed on.

The console has a Chat tab; `pyrtan chat` is the same in a terminal. examples/small-talk answers "how are you" and "are you a bot".

Also fixes the hub dropping a trigger's fixed inputs: /capabilities now carries trigger_inputs, and the hub reads triggers with them.

v0.10.0

2026-10-27 · 65e3c9a · pyrtan-0.10.0.tar.gz

Networks: any hub starts one, pyrtan.com publishes the default; release 0.10.0

A hub could be in one federation at most (--federation-secret), and everything it heard went into one directory it passed to every hub it was linked with, so a private group and the public swarm could not be kept apart. And the public directory depended on a seed hub being up.

A network is now a name and a secret. A hub can be in any number: `pyrtan network new NAME` starts one (a network of one) and prints its invite, NAME:SECRET@HUB; `pyrtan join INVITE` puts another hub in it and follows that hub. A Follow made in a network names it by an id derived from name and secret and is signed with the secret. Directory entries keep the network of the link they arrived over and are gossiped only within it; /directory shows private networks only to the operator, and keepers are handed only the public part. Follows outside any network and members work as before; --closed now means "no follows outside my networks".

pyrtan.com publishes the default network in rendezvous/network.json, with a secret that is public on purpose: it names the network, it does not guard it. `pyrtan join` with no invite, `pyrtan follow` with no URL and `hub --join default` (or --follow seed) join it. The hourly snapshot now asks every hub it knows of, not only the seeds, and keeps the default network's part, so no hub has to be always up and the site stays static files on GitHub Pages. This replaces the Fly.io seed config, which is removed.

v0.9.0

2026-10-26 · 96f675e · pyrtan-0.9.0.tar.gz

Watches and growing from a gap: trees that run when something speaks, and the branch a gap asks for drafted by rules; release 0.9.0

A tree may carry [[watches]]: a clock (every = "15m"), a file (each time lines are appended), an MQTT topic (each message) or a serial line (each line) starts a run, with inputs rendered from the event, env and now. watch.py holds one thread per watch over a service's trees and runs them through the service's own runner, so gaps are counted and delegations route; the file is polled, the queue and the serial line are held open as streams (adapters.mqtt_stream with pings, adapters.serial_lines). The trace begins with a watch step, the way an asked run's begins with intent. Each watch remembers what it last did and the path the run took; when the path of a repeated run differs from the one before, the watch records that: a site going from up to erroring on a clock is learned before anyone asks. --watch on a service or hub, pyrtan watch DIR alone, /watches and pyrtan watches --at URL, and a Watches tab in the console. Watches are static and checked against the tree's lists by pyrtan check; a hosted service (--egress public) watches no file and no serial line. The watcher follows the files on disk: a tree that appears starts its watches, one that vanishes stops them.

pyrtan grow reads gaps (a running service or hub, a --gaps file, a /gaps answer, or the federation's reports for your catalog trees) and drafts the branch each asks for, by rules: { exists = false } for a missing value, the range of the hole a number fell into between the ranges the node already has, { type = ... } for a value a report could only carry as its type, { eq = value } otherwise, plus a fail node that says what was seen. --apply writes the branch before the node's else and the node at the end of the file; the file must check clean afterwards or it is put back. A person then says what the value means.

Unanswered sentences persist with --unanswered FILE ($PYRTAN_UNANSWERED) on a service or hub. The site-status and room-temperature examples carry a watch each. Tests: the checker's refusals, a file watch (append, rotation, path change), a clock watch (path change, sync), a queue watch against the fake broker, a serial watch over a pty, egress, /watches on a service and through a hub, the commands, persistence, every drafting rule, apply on multi-line and one-line lists with revert, the grow command from a file, a report and a running service.

v0.8.0

2026-10-26 · 5ec4627 · pyrtan-0.8.0.tar.gz

The catalog grows up: namespaces, publishing by pull request through the gate, gap reports gossiped back to authors; release 0.8.0

A catalog tree now says who publishes it: a `namespace` line, and the catalog id is namespace/name. The examples are the pyrtan namespace; everyone else publishes under catalog/<namespace>/<name>.toml with an OWNERS file of GitHub logins. `pyrtan pull weather` takes the one there is and asks when two namespaces have it; `pyrtan pull barry/weather` says which. `pyrtan index DIR... --copy OUT` builds one index over several directories and lays the files out as namespace/name.toml, the shape the site serves; entries carry id, namespace, sources and the allow-lists.

Publishing is a pull request. `pyrtan publish FILE --into CHECKOUT --owner LOGIN` places the file, writes OWNERS for a new namespace and runs the gate (catalog.gate, site/gate.py, the new catalog workflow on every pull request): every tree loads clean with a licence and a namespace, sits where its namespace and name say, ids are unique, only a namespace's owners change it, a changed file bumps its version. The pages build runs the gate before indexing.

Gaps come back to the author as a federation topic. A hub started with --report-gaps ($PYRTAN_REPORT_GAPS) shapes the gaps its catalog trees met (namespace/name@version, node, the path decided on, the value no branch covered, count, when; never inputs; long or non-scalar values become their type) into its announces; every federated hub keeps what it hears with the directory's merge rules (newer wins, no future, 30 days, 2000 rows), passes it on, persists it, and serves /reports publicly with counts and reporters summed. site/snapshot.py bakes rendezvous/reports.json from the seeds hourly, the catalog page shows each tree's gaps beside it, and `pyrtan reports NAMESPACE/NAME` reads a hub or the baked file. A tree without a namespace reports nothing.

Every example is bumped to version 2 (the file changed: it names its namespace), as the gate asks of anyone. pyrtan.js mirrors the namespace. Tests: namespaced index and pulls, the gate's every refusal, publish, the command, reports travelling with announces, shaping, persistence, the snapshot and the command. Docs: catalog/README.md, README, site, CONTRIBUTING, tree format, design, platform §2.8, deploy.

v0.7.0

2026-10-26 · de8967b · pyrtan-0.7.0.tar.gz

Forms of I/O: file, shell, serial, queue and browser sources, each behind an allow-list; release 0.7.0

An observe or act node may now name a file, a program, a serial line, an MQTT queue or a browser instead of http, and every one follows the rule hosts set: the tree lists what the source may reach (paths, commands, devices, topics; hosts for a browser and a broker), pyrtan check refuses a literal outside the list, the runtime refuses a templated one before the adapter is asked, and nothing else is reachable. The result is a plain table under `into`; every source leaves `body`, so extract reads it unchanged; the trace line reads the same (`shell df -P / -> exit 0`).

src/pyrtan/adapters.py holds the five: a file read (whole or a tail) or write; one program by name with its arguments as a list, no shell in between; a serial exchange over termios; MQTT 3.1.1 at QoS 0 over a socket (subscribe and wait for one message, or publish); and a page rendered by Playwright's Chromium on a worker thread, with wait_for and actions, where a navigation outside hosts is aborted. Playwright is the one optional extra (pyrtan[browser], PYRTAN_BROWSER). --egress public now also closes files, programs and serial lines: a hosted box is not lent out.

One fixture answers for every source, keyed by adapter_key, so the five new examples (disk-space, log-errors, modem-check, room-temperature, rendered-headline) run offline, pyrtan run --record writes such records, and the conformance vectors cover all six sources in both runtimes (37/37). pyrtan.js reads HTML with a port of html.py now, not the host's DOM, so css extracts agree on a service, in a browser and in node; a member answers a non-http source with source-unsupported, and the hub hands members only http trees. pyrtan list names a tree's sources.

Tests: allow-lists, keys, each adapter live (a pty for serial, a small MQTT broker for queue, a local page and Chromium for browser, skipped without Playwright), egress, record and replay through the command and a service. Docs: _meta/io.md, tree-format, design, README, deploy.

v0.6.0

2026-10-25 · a97167a · pyrtan-0.6.0.tar.gz

Trees in the browser: pyrtan.js, conformance vectors, trusted members that run trees; release 0.6.0

The tree runtime again, in JavaScript (src/pyrtan/js/pyrtan.js, UMD, no dependencies), served at /pyrtan.js by every hub and service and written beside the console. It must not drift from the Python runtime: tests/conformance/make_vectors.py runs every example through Python against the recorded responses with a fixed clock, run.cjs runs the same through pyrtan.js, and test_conformance.py fails on any disagreement or stale vectors (18 runs, all agree).

A member that follows with the hub's bearer token is trusted. The hub hands trusted members its trees as Keep blobs under trees/<name>: its own --trees, and --handout DIR ($PYRTAN_HANDOUT), trees for the devices that the hub does not run itself. The member announces them, the hub keeps only the names it handed out and routes by its own copy's triggers. A trusted member is a holder: its trees show in /capabilities with via: member, are announced to followers, and a sentence or /run for one of them is an Ask in its mailbox and its Answer back, with peer: <member> and via: ["member"]; member-timeout after 30 s. Answers for unknown runs or from the wrong member are refused. The console answers Asks with pyrtan.js, restores held trees from its store, shows what it runs and runs a tree on the spot.

Also: Trace's wall-clock fields are stripped from the vectors; the kept table no longer doubles rows when two renders race; docs for members §7, design sequencing, README, site and deploy; version 0.6.0.

v0.5.0

2026-10-25 · fb2ff04 · pyrtan-0.5.0.tar.gz

Members: a browser or a phone joins a hub through a mailbox and keeps its blobs; release 0.5.0

A device that cannot be reached joins a hub the other way round. A member has an id urn:pyrtan:member:<hex> and a pair secret of its own, follows with inbox "mailbox", and from then on everything the hub would post to it waits in a bounded mailbox it polls, signed; it posts its own activities to the hub's inbox like any hub. Same signatures, same one-way trust: a member carries and gossips the directory, is listed in it as a member if it wants to be, and is never routed to.

Keeping: a blob is a JSON value under a key, at most 256 KB; a hub keeps its own (--store DIR) and keeps for others, 200 keys and 64 MB per actor. Keep hands a blob to a keeper, Kept confirms it (a hub in the same round trip, a member on its next poll), Fetch asks for it back, and every Announce names the keys the sender keeps for the receiver, so a hub that came back without its store gets its blobs from its members' next announces without anyone doing anything. --replicas N is how many fresh holders each blob should have; the poll sends Keep to keepers that do not hold it, members first, someone new before a stale holder. The directory, the gaps and the unanswered sentences are kept automatically; the state file, with the pair secrets, never is. pyrtan keep, kept and fetch drive it; /keep, /fetch, /mailbox and /kept/<key> are the paths.

The console is the member: a Member tab makes the identity, follows, polls every twenty seconds, keeps blobs in IndexedDB and hands them back, with SHA-256 and HMAC written out in the page because crypto.subtle is not available on a plain http:// LAN address. A web app manifest and a service worker make it installable and offline-capable, served by every hub and service and written beside the exported console. Design in _meta/members.md; members do not run trees yet.

v0.4.0

2026-10-25 · 084dee6 · pyrtan-0.4.0.tar.gz

Keep the directory on the swarm, seed it from pyrtan.com, hold trees in the hub; release 0.4.0

The rendezvous hub of v0.3 was a server, and a swarm that needs a server to find itself is not a swarm. It is gone. In its place every federated hub keeps a directory and gossips it: a hub started --listed (or told `pyrtan follow --listed`, or POST /listed) makes an entry for itself, id, name, the trees it holds and when; every Announce carries the sender's directory; announces now go both ways, down to followers as before and up from a follower to the hubs it follows, and an inbox answers an Announce with its own gossip. A followed hub routes none of what a follower announces, being followed grants nothing, but keeps the directory it carries. Merging takes the newer entry, never overwrites a hub's own, clamps the future, drops a week-old entry and bounds the table; GET /directory (public) on any federated hub shows what it knows, each entry with how recently its hub announced. The directory lives in the state file.

pyrtan.com publishes only rendezvous/seeds.json, a static list of hubs to follow first; `pyrtan follow` with no URL and `pyrtan hub --follow seed` take the first that answers, `pyrtan directory` asks your own hub and falls back to the seeds, and the landing page reads the seeds' directories from the visitor's browser. The compose stack and Caddyfile for a rendezvous box are removed; the NAS compose takes PYRTAN_ADVERTISE, PYRTAN_FOLLOW, PYRTAN_LISTED and PYRTAN_STATE so a home hub behind a tunnel is seed #1.

`pyrtan hub --trees DIR` holds trees in the hub's own process, reached without HTTP and able to delegate back through the hub, so a federated swarm can be one process on one box. Version 0.4.0; the design notes record the change and why.

v0.3.0

2026-10-25 · 2aaf6bd · pyrtan-0.3.0.tar.gz

Federate hubs in the shape of ActivityPub, and release 0.3.0

A hub started with --federate (or --advertise URL) has an identity other hubs can follow. federation.py gives it an actor document at /actor, an inbox for Follow, Accept, Reject, Undo and Announce activities, and a pair secret per relationship; every hub-to-hub message carries an actor, a date and an HMAC-SHA256 signature over the method, path, date and the canonical JSON body, which is what goes on the wire.

Following is trust, in one direction. A hub routes only to the hubs its operator chose to follow (pyrtan follow, --follow, or POST /follow with the token); a follower receives announcements and may ask, and is never routed to. Federation is one hop: a hub announces what its own services hold, never what it learned from others, and refuses a request that has already crossed a hub. A followed hub fetches the follower's own /actor before accepting, so a Follow cannot point it at a stranger's inbox. --federation-secret takes follows only from hubs that sign with it; --closed takes none; --state FILE keeps the relationships across restarts. A followed hub announces whenever its capabilities change and is polled (signed) as a fallback, so its link is up, stale or down like a service's. A federated answer carries via: [the hub it crossed].

pyrtan follow, unfollow and peers drive it; pyrtan hub gains --name, --federate, --advertise, --follow, --federation-secret, --closed and --state. /peers lists following and followers, and the console's Peers tab shows them. Design in _meta/federation.md; the sequencing moves more forms of I/O to v0.4 and the catalog's growth to v0.5.

v0.2.0

2026-10-25 · 7d52a30 · pyrtan-0.2.0.tar.gz

Add tokens, CORS, persistent gaps, egress policy, the catalog and the console

Phase 1 of pyrtan.com (_meta/platform.md): everything the console, the catalog and future hosting need from the open-source runtime, built so a caller cannot tell a hosted Pyrtan from a self-hosted one.

Access: `serve` and `hub` take --token (or $PYRTAN_TOKEN) and require `Authorization: Bearer` on everything but /health, /api and the console; /health without the token answers status and counts only. A swarm shares one token: a service presents it on /register, the hub presents it when it polls and forwards. --cors ORIGIN names the browser origins allowed to call. A service now starts answering before it registers, so a hub's capabilities check inside /register no longer waits on the hub's answer.

Gaps: `serve --gaps FILE` keeps gaps in a JSON-lines file, deduplicated with a count and first/last seen, read back on start; a file that cannot be written is reported in /health, never fatal to a run.

Egress: `--egress public` refuses, before connecting and on every redirect, any host that resolves to a loopback, private, link-local, multicast or unspecified address, so a tree with hosts = ["*"] cannot probe the host's own network. Not retried.

Catalog: `pyrtan index DIR` builds index.json for a directory of trees; `pyrtan search` and `pyrtan pull NAME[@VERSION]` read one (default https://pyrtan.com/catalog/, or --catalog / $PYRTAN_CATALOG). A pull verifies sha256, name and version against the index and refuses an index path outside the catalog. Trees gain an optional `license` (SPDX); a catalog requires one. The examples are MIT.

Console: the demo page becomes the console, served at / and /console by a hub or service and written out by `pyrtan console --out` to host anywhere. Served elsewhere it takes a hub URL and token (kept in local storage); a hub that requires a token shows the token field alone. Tabs for ask, trees, peers (hub), gaps and the API.

deploy/pyrtan.com holds the two workflows, the landing page and the README for the site and catalog repositories; the compose stack passes --cors to the hub and keeps the `mine` service's gaps on the share. Version 0.2.0.

v0.1.0

2026-10-25 · 34229d7 · pyrtan-0.1.0.tar.gz

Initial public release