Releases: sloev/spore
Release list
SPORE nightly 2026.08.25
Dated nightly — 0.7.202608251641+d725c81, built from master at
d725c81. Kept as one of the last five nightlies so a bad build
can be rolled back from; for the current build use
rolling.
One day, one build: a later merge the same day replaces this, so what is
here is that day's last master. Use an earlier date to go further back.
SHA-256 69aae9ca2aa097b036f67f9cffc9f0ff8636e88145b06fd8b02130d21b2290c3.
SPORE nightly 2026.08.24
Dated nightly — 0.7.202608241135+0e2d07e, built from master at
0e2d07e. Kept as one of the last five nightlies so a bad build
can be rolled back from; for the current build use
rolling.
One day, one build: a later merge the same day replaces this, so what is
here is that day's last master. Use an earlier date to go further back.
SHA-256 d781898ffbf8f029b8648d399ec27e28a830f9c187dc3412bc15d83103dc56c8.
SPORE nightly 2026.08.23
Dated nightly — 0.7.202608232302+5c31e59, built from master at
5c31e59. Kept as one of the last five nightlies so a bad build
can be rolled back from; for the current build use
rolling.
One day, one build: a later merge the same day replaces this, so what is
here is that day's last master. Use an earlier date to go further back.
SHA-256 ecd716eac1f1f1645231b0cea7717b21fe13aea8e55c6426fc0e478c8330febd.
SPORE nightly 2026.08.22
Dated nightly — 0.7.202608221402+5d6acf3, built from master at
5d6acf3. Kept as one of the last five nightlies so a bad build
can be rolled back from; for the current build use
rolling.
One day, one build: a later merge the same day replaces this, so what is
here is that day's last master. Use an earlier date to go further back.
SHA-256 af8f59eb47c40d631fa1ac9a2841af7e11297d2e2f787939a0c29daea5d9f0ae.
SPORE nightly 2026.08.21
Dated nightly — 0.7.202608212006+9dd0ae0, built from master at
9dd0ae0. Kept as one of the last five nightlies so a bad build
can be rolled back from; for the current build use
rolling.
One day, one build: a later merge the same day replaces this, so what is
here is that day's last master. Use an earlier date to go further back.
SHA-256 67ba1e87a23c1245569bab9372b06b4b8305e661a44ab6ca59173aa1fe5ffbe9.
SPORE v0.7.0
-
A Direct medium is now a name, not a hardcoded enum — and an unknown one is
ignored instead of poisoning the offer it arrived in.direct::Mediumwas a
closed#[repr(u8)]enum, which was the wrong shape twice over.DESIGN.md
already says the nutrient list is closed while the bridge list stays open, and
a medium is the Direct plane's version of a bridge — so enumerating them in the
core made every new medium an edit tosrc/and an allocation somebody had to
hand out. Worse, an unknown code had to be a decode error:Offer::decode
propagated it with?, so a peer advertising one unfamiliar path alongside three
usable ones got nothing at all. A medium is now a length-prefixed name carried
verbatim; an unrecognised one decodes cleanly and is simply a candidate nobody
declared willingness for, so it is skipped like any other unusable path, and an
offer of only unknown mediums answersno_medium— a reason rather than silence.
The name is bound into the KDF, so a record still cannot be replayed onto a
different medium. The SPDR profile isVERSION = 2for the encoding change,
bumped rather than finessed because a v1 peer would mis-parse every candidate —
cheap now, since until the daemon wiring below nothing could start a pipe at all.
The frozen v1 envelope wire is untouched; SPDR is opaque payload riding on it. -
Direct now offers a global IPv6 address when the host has one — the WAN path
that needs no traversal at all. Candidates were IPv4-only, so a node with a
global v6 from its ISP — which is most of them — advertised only a LAN address
and a reflexive one that needs a hole punch to work. A global v6 has no NAT in
front of it: it is already the address a peer dials. It is ranked between the
LAN locator and the reflexive one, sochooseprefers it over a path that does
not exist yet, and it is offered only when the host actually has one —
link-local, unique-local, loopback and multicast are all rejected, since
advertising any of those is a candidate that can never connect. A path firewall
may still drop unsolicited inbound; that is a pinhole a punch can open rather
than a mapping that must first be discovered, so it is better odds and not a
promise. The daemon now prints which locators it is offering and what each one
needs, so "why did it not connect" is answerable without a packet capture. -
A node can now discover the address the outside world sees, and answer that
question for other nodes (P-Direct-NAT step 2). Direct could only ever offer
the address a node was told it had, so it worked on a LAN and nowhere else.
direct::stunis a minimal binding client and echo — RFC 5389's binding
exchange and nothing more: no auth, no FINGERPRINT, no ICE, no dependency, with
unknown attributes skipped rather than rejected so a full STUN server still
interoperates. The echo half ships with it and is the point: a daemon's
stun:port answers statelessly (one packet in, one out, nothing retained), so
one SPORE node is a reflexive-locator server for another and the network need
not quietly depend on a third party's STUN server. A discovered locator is
offered as a second candidate ranked below the LAN one, so a path that works
without crossing a NAT is always preferred. This does not make NAT traversal
work: most NATs drop an unsolicited inbound datagram, which is what the
coordinated hole-punch (step 3) exists to fix — the daemon says so at startup
rather than implying the new locator is reachable. Verified with two daemons,
one asking the other's echo. -
Android can bring a Direct pipe up too — through the same code as the
daemon. Wiring it separately would have meant two implementations of one
negotiation, which is exactly the per-platform punch logic the roadmap's
engineering pattern forbids, so the runner moved into the core as
direct::UdpRunnerand both native runtimes became thin adapters over it: the
daemon supplies stderr and its config, the JNI layer supplies a handle and five
poll-driven calls. Kotlin never touches a Direct socket — it says where the
device is reachable, feeds delivered envelopes in (getting back whether each was
signalling, so an app message is never swallowed), and ticks. Compile-checked
only: there is no Android SDK in this environment, so the JNI symbols and
Kotlin declarations are verified symmetric in both directions and the crate
builds and lints clean, but no phone has run it. The daemon path is the one with
two-process evidence, and it was re-verified unchanged after the refactor. -
The daemon can now bring a Direct pipe up. The core seam below made
signalling possible; nothing called it, so Direct still could not be started
from anything you can run.src/cli/direct.rsis that consumer:direct:in
the config says where a node is reachable,direct-to:names a peer to keep a
pipe to (the daemon has no control surface to start one from, so without it
both ends would sit waiting to be offered a pipe), and the runner dispatches
delivered DMs throughSignalling, opens aUdpPortfor whichever candidate
won, and carries the reply back oversend_direct. Verified with two real
daemon processes negotiating over a shared folder bridge and bringing up a
pipe, not only in a unit test. Honest limits, printed by the daemon itself:
LAN only — a node cannot yet discover its own reflexive address, so it
advertises what it was told and NAT traversal remains the unbuilt
P-Direct-NAT track; and there is no app above the pipe yet, so inbound records
are logged and dropped rather than routed somewhere that does not exist. -
Direct signalling can now reach a peer at all —
SPDRridessend_direct.
The negotiation codec, key schedule and socket adapters were all built and
tested, but nothing tied them to the mesh: no code anywhere outside
src/direct.rslooked at anSPDRpayload, so no app could start a pipe, which
is why NAT traversal had never actually been hit in practice.direct::Signalling
is the missing seam — it turns the plaintext of a delivered DM into aSignal
saying what to open and hands back the state to finish with. A whole negotiation
now runs over the realsend_direct/on_rxpath in a test, sealed and signed
like any other DM, and the resulting pipe carries traffic both ways.
A real API gap fell out of it:Pipe::answertook the port before
chooseran inside it, so a responder willing to use more than one medium could
not use it — it would have had to guess which medium would win.accept(decide,
no port) andPipe::answer_with(open, then derive) split the two;answer
stays as the single-medium convenience it always effectively was. Deciding is the
core's, opening is the runtime's — the seamdocs/DESIGN.md's runtime model
already implied. Unanswered offers expire rather than holding an ephemeral secret
forever. Purely additive; the SPDR wire anddirect::VERSIONare unchanged.
No daemon or Android build calls this yet — that wiring is the next step. -
The palette is now defined once and generated into every surface that renders
it, with the contrast ratios computed instead of typed. The same hexes were
hand-maintained in four places —site/style.css, the standalone node's inlined
CSS, Android's ComposePalette, anddocs/VISUALDESIGN.md's tables — each
carrying its own re-typed WCAG ratios, so a palette change meant a manual
four-way audit and the ratios could silently stop describing the colours they
sat beside. They had already drifted: Android carried three ratios per colour
where the CSS carried one,--dimon void was written as both 4.68:1 and 4.6:1,
and--warn/--badexisted only in the standalone.design/tokens.jsonis now
the one place a colour is defined anddesign/generate.pyemits all four,
following the provenbindings/spec.jsonpattern with a matching CI job
("design tokens in sync"). The generator refuses to run if a pairing stops
matching the grade the source claims for it, in either direction — a colour
that is no longer readable, or a "never do this" pair that has quietly become
fine, since a stale safety claim is as misleading as a stale colour. No
rendered colour changed: every hex in all four files is identical, counts
included; the diff is comments, ordering and formatting only. -
A node now maintains itself on a timer instead of only when traffic
arrives — and the desktop daemon retries unacked sends for the first time.
The expiry sweep and prekey rotation were reachable only fromNode::on_rx,
so a quiet node pruned nothing and never advanced its forward secrecy; and
resend_unackedhad exactly one production caller anywhere in the tree — the
Android JNI — so §8 resend-with-backoff worked on Android and nowhere else.
NewNode::tick(now)is the one periodic entry point: it runs the sweep and
returns whatever fell due for resend.Hub::tick()wires it for hosted
nodes, the daemon calls it in its existing beacon loop, and Android's existing
periodic call now goes through it (no Kotlin change). Purely additive — the
ingest-side sweep still runs and both are idempotent, so a runtime that never
ticks behaves exactly as before. SPEC's runtime contract makes the timer
normative. -
Storage is now something a runtime supplies, not something the core
assumes. The store could only ever spill to a filesystem directory, so a
runtime whose storage is not a disk — a browser tab with IndexedDB, an MCU
with flash — had somewhere to put bytes and no way to offer it, and silently
ran memory-only.SpillBackendis now a public trait
(put/`g...
SPORE v0.6.0
- The Android app now looks like the design language instead of describing it.
VISUALDESIGN.md§3's shapes — the ammo crate, the Toughbook input with its screw
dots, the radio-switch button that physically throws, the segmented LED, the
sticker badges — exist as Compose primitives inChrome.kt, and every screen was
rebuilt on them from a Claude Design mock. Chat gets right/left-aligned crate
bubbles; Feed gets inline markdown, image attachments and a dedicated Compose Post
screen; Bridges is grouped by transport with a status LED per row. Three places
Android cannot match the spec exactly (no Impact font, the hard shadow is drawn by
hand because Compose's is blurred, reduced motion is inferred from
ANIMATOR_DURATION_SCALE) are now recorded in the spec rather than left to be
found as bugs. - File transfers report their fragmentation in both directions.
Msgcarries the
magnet, so a file bubble reads chunk state out of the existingtransfersflow
rather than keeping a second copy that can drift. Incoming showshave/count · fetchingand fills as chunks land; outgoing fills at once and says "served from
this node" — not "delivered", because whether a peer pulled a chunk is not
observable from here. - Feed posts can carry an image, referenced from the markdown body. A post is one
signed envelope of UTF-8, so the bytes ride the ordinary manifest-and-chunk path
and the body carriespointing at them. Readers without
the chunks see the transfer fill; clients that do not know the marker see a plain
markdown image link. Decoding isinSampleSize-capped onDispatchers.IO, since a
phone photo decoded whole for a 220 dp row costs ~100 MB of heap. - "Reveal seed" showed
unavailableon every upgraded install. The encryption
change moved the seed into the Keystore-backed store and cleared the plaintext
file, but the Advanced screen still read the plaintext prefs directly — all the
call sites inNodeControllerwere replaced and none in the UI. The same shape as
S-015, S-019, S-023, S-025, S-026, S-029 and S-030: verified on the artefact the
change was written for, assumed on its neighbour. There is now one accessor and the
UI cannot go around it. - One thing from the mock was not implemented: its "+ subscribe" chip puts pink
text on kevlar olive, which is 2.32:1 and the single pairing §1 forbids outright.
It is outlined on void instead.StickerBadgetakes its own background rather than
inheriting the crate fill, specifically so this is hard to reintroduce.
⬇ spore-android.apk
— a permanent link that always serves the newest stable release.
This number versions the distribution, not the protocol. The SPORE v1
wire format and the crate's API shape are frozen and unaffected by it.
Before you rely on it: no radio bridge has been verified against real
hardware (every 🧪 in
BRIDGES.md),
and the Still open section of
SECURITY_FINDINGS.md lists
what is known and unfixed. Builds are debug-signed until a release keystore
is configured, so Android will warn about an unknown developer.
SHA-256 9fd280962ce13af4ed767843c751f371dd263c83a7c46649934418c4c6171434
SPORE v0.5.0
- S-031 Any sound in the room could saturate a CPU core indefinitely.
Demod::pushrescanned its whole retained buffer every call, so work grew with
the buffer rather than with the new samples — 13 ms per 100 ms push at 1 s
buffered, 94 ms at 6 s, and the buffer caps at 175 s, about 27x real time. No key
or protocol participation needed; on Android it runs in a foreground service off
the mic. A scan cursor makes it flat at 1.5 ms. Found by the discovery audit and
measured before and after. - The visual design language is implemented, not just written.
VISUALDESIGN.md
described an appearance no surface had:site/style.cssstill carried the old
green-and-blue palette and Android an inline Compose scheme. Both now consume the
same tokens, withprefers-reduced-motionhonoured and no webfont anywhere (the
standalone must make zero network requests). The spec gains an
implementation-status table so it can never again claim more than the code does.
⬇ spore-android.apk
— a permanent link that always serves the newest stable release.
This number versions the distribution, not the protocol. The SPORE v1
wire format and the crate's API shape are frozen and unaffected by it.
Before you rely on it: no radio bridge has been verified against real
hardware (every 🧪 in
BRIDGES.md),
and the Still open section of
SECURITY_FINDINGS.md lists
what is known and unfixed. Builds are debug-signed until a release keystore
is configured, so Android will warn about an unknown developer.
SHA-256 6c3a7ba86636c990df6b09a93c1ab88e3ee2980c0ca7508be6b969f08f7cedcc
SPORE v0.4.0
- The release-bump workflow's first run failed on its last line. Three separate
defects, all mine: GitHub Actions cannot open pull requests unless the repository
enables it (off by default, and I did not check), a re-run would have died on the
branch the failed run left behind, and the "is## Unreleasedempty" guard counted
the section's own explanatory boilerplate as content — so it would have cut0.4.0
with a changelog consisting of the text describing what changelogs are for. The PR
step is now best-effort with a printed link, the push is--force-with-lease, and
the guard requires an actual-bullet. - S-030 The tag-cutting step looked up
v0.3.0but the existing tag was
V0.3.0; git tags are case-sensitive, so it missed it and published a second
release for the same version. That is S-025's exact trap reproduced inside the fix
for S-025. Now checks both cases. One accidental upside: the duplicate became
"latest" andreleases/latest/download/spore-android.apkreturns 206 for the first
time — a correct outcome from a broken mechanism, which is the most misleading
state a release pipeline can be in.
Entries accumulate here as work merges. release.yml retitles this heading to the
new version when you bump, and refuses to release if it is empty — a release nobody
can read about is the failure this guards against.
Nothing yet.
⬇ spore-android.apk
— a permanent link that always serves the newest stable release.
This number versions the distribution, not the protocol. The SPORE v1
wire format and the crate's API shape are frozen and unaffected by it.
Before you rely on it: no radio bridge has been verified against real
hardware (every 🧪 in
BRIDGES.md),
and the Still open section of
SECURITY_FINDINGS.md lists
what is known and unfixed. Builds are debug-signed until a release keystore
is configured, so Android will warn about an unknown developer.
SHA-256 7d5beaaeae207012079687cfbd61f6f71560ebb0b6da075a68fcbb92fd12e40e
SPORE v0.3.0
Tagged V0.3.0 at 27bea16. Release plumbing only — no Rust behaviour changed, wire
untouched. Both entries are the same finding recurring: a fix verified on the artefact
it was written for and assumed on its neighbour.
Fixed
- S-029 The S-026 fix was racy and destroyed the release it repaired. Deleting a
tag with--cleanup-tagand recreating it a second later left
nightly-2026.07.27as a live tag with no release attached, on a job that
reported success at every step. Both release steps now clear the existing
assets and upload over them, never touching the tag or the release. Accumulation
is still prevented; the cost is thatpublished_atlags again, which the release
name and body make up for. - Nightly releases accumulated assets — the fix for
rolling(S-021) was not
applied to the dated nightly beside it, and the versioned filename now embeds a
minute and a commit sha, so a second merge the same day added a pair rather than
replacing one. 2026-07-27 ended up holding four assets with nothing marking the
current one, and itspublished_atsat an hour behind its contents. Today's
nightly is now replaced per build, likerolling(S-026). - The tag glob was case-sensitive.
tags: ['v*']silently ignoredV0.1.0and
V0.2.0: GitHub created releases for both and no build ever ran, leaving a
non-prerelease "latest" holding zero assets — so
releases/latest/download/spore-android.apk, the linkdocs/APPS.mdpromises,
404s from a page that looks like a real release. Worse than having no release at
all. Now['v*', 'V*'], and the tagged path fails loudly if the tag's
major.minordisagrees withCargo.toml—V0.2.0was cut whileCargo.toml
still said0.1.0, and nothing complained.
Cutting this release also exercised the guard added in it. V0.3.0 was tagged while
Cargo.toml still said 0.2.0, and the build refused: "tag V0.3.0 is 0.3.x but
Cargo.toml says 0.2.0". That is the drift S-025 was about, caught before it produced
another release nobody could download — bump first, then tag.
⬇ spore-android.apk
— a permanent link that always serves the newest stable release.
This number versions the distribution, not the protocol. The SPORE v1
wire format and the crate's API shape are frozen and unaffected by it.
Before you rely on it: no radio bridge has been verified against real
hardware (every 🧪 in
BRIDGES.md),
and the Still open section of
SECURITY_FINDINGS.md lists
what is known and unfixed. Builds are debug-signed until a release keystore
is configured, so Android will warn about an unknown developer.
SHA-256 ffa05ee1a43211f824acf5e08a2fdf5f30bc9494b60665780ba667a95e45ed23