Skip to content

Releases: sloev/spore

SPORE nightly 2026.08.25

Pre-release

Choose a tag to compare

@github-actions github-actions released this 25 Aug 16:44
d725c81

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 24 Aug 09:30
b30c6c8

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 23 Aug 08:34
2eba724

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 22 Aug 05:12
9b08b11

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 21 Aug 20:10
9dd0ae0

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

Choose a tag to compare

@github-actions github-actions released this 01 Aug 19:35
82e19b4
  • 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::Medium was 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 to src/ 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 answers no_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 is VERSION = 2 for 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, so choose prefers 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::stun is 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::UdpRunner and 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.rs is 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 through Signalling, opens a UdpPort for whichever candidate
    won, and carries the reply back over send_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 — SPDR rides send_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.rs looked at an SPDR payload, 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 a Signal
    saying what to open and hands back the state to finish with. A whole negotiation
    now runs over the real send_direct/on_rx path 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::answer took the port before
    choose ran 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) and Pipe::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 seam docs/DESIGN.md's runtime model
    already implied. Unanswered offers expire rather than holding an ephemeral secret
    forever. Purely additive; the SPDR wire and direct::VERSION are 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 Compose Palette, and docs/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, --dim on void was written as both 4.68:1 and 4.6:1,
    and --warn/--bad existed only in the standalone. design/tokens.json is now
    the one place a colour is defined and design/generate.py emits all four,
    following the proven bindings/spec.json pattern 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 from Node::on_rx,
    so a quiet node pruned nothing and never advanced its forward secrecy; and
    resend_unacked had exactly one production caller anywhere in the tree — the
    Android JNI — so §8 resend-with-backoff worked on Android and nowhere else.
    New Node::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. SpillBackend is now a public trait
    (put/`g...

Read more

SPORE v0.6.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 08:29
1461638
  • 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 in Chrome.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. Msg carries the
    magnet, so a file bubble reads chunk state out of the existing transfers flow
    rather than keeping a second copy that can drift. Incoming shows have/count · fetching and 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 carries ![name](spore:<magnet>) pointing at them. Readers without
    the chunks see the transfer fill; clients that do not know the marker see a plain
    markdown image link. Decoding is inSampleSize-capped on Dispatchers.IO, since a
    phone photo decoded whole for a 220 dp row costs ~100 MB of heap.
  • "Reveal seed" showed unavailable on 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 in NodeController were 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. StickerBadge takes 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

Choose a tag to compare

@github-actions github-actions released this 27 Jul 15:10
e977c47
  • S-031 Any sound in the room could saturate a CPU core indefinitely.
    Demod::push rescanned 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.css still carried the old
    green-and-blue palette and Android an inline Compose scheme. Both now consume the
    same tokens, with prefers-reduced-motion honoured 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

Choose a tag to compare

@github-actions github-actions released this 27 Jul 14:03
c62590e
  • 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 ## Unreleased empty" guard counted
    the section's own explanatory boilerplate as content — so it would have cut 0.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.0 but 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" and releases/latest/download/spore-android.apk returns 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

Choose a tag to compare

@github-actions github-actions released this 27 Jul 13:28
ba9ab78

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-tag and recreating it a second later left
    nightly-2026.07.27 as 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 that published_at lags 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 its published_at sat an hour behind its contents. Today's
    nightly is now replaced per build, like rolling (S-026).
  • The tag glob was case-sensitive. tags: ['v*'] silently ignored V0.1.0 and
    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 link docs/APPS.md promises,
    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.minor disagrees with Cargo.tomlV0.2.0 was cut while Cargo.toml
    still said 0.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