Release

Release

Announcing Toise 0.20.0 — twenty advisories closed, nothing unreachable

Released 2026-10-12 · written up 2026-10-17

Two headlines. Every binary published before this one was compiled against a standard library with known advisories, so this is an upgrade rather than a choice. And a tenant holding 236 entities of one type could retrieve 200 of them, with no sequence of calls that reached the other 36; the list tools now page.

The rest came from three consumers reading production and one producer reading its own code. Several items below exist because someone measured something that had been asserted, and two planned items were withdrawn the same way.

Upgrade rather than wait.

Twenty vulnerabilities reachable from Toise's own call paths were reported by govulncheck: six in net/http, three in html/template, others in the standard library of the Go version earlier releases were built with, five in golang.org/x/net and two in google.golang.org/grpc. One of the grpc advisories is a server panic on a missing authority or Host header, reachable through the OTLP ingest receiver, which is the one surface a stranger can reach. The exposure is in the artefacts already deployed, not in a future build. This release is built on Go 1.26.9 and both modules report zero reachable vulnerabilities. Otherwise it is drop-in: no wire-contract break, no data migration.

Nothing is unreachable any more

find_entities and get_neighbors take after and return next_cursor, with a sentence beside the truncation flag saying what was withheld. Measured on a live tenant: describe_schema reported 236 network endpoints and find_entities could return 200. That type carries no attributes by design, so there was not even a filter to slice on. An answer declared what it was withholding and offered no way to obtain it.

Pages are ordered by identity_fingerprint, which every replica computes identically and which re-minting does not move, so a page sequence survives a failover. Neighbours order by depth first, so the closest still come first. It is not an offset: over a live projection an offset silently skips and repeats. The first cut sorted by recomputing each fingerprint inside the comparator and the bench gate caught it, at twelve times the time and far more allocations; the ordering keys are now read from the projection's index and only the page is materialised. Against the pre-paging baseline, time is at parity and memory is down by half.

A change says who observed it, and which commit it belongs to

The producer contract puts provenance on the instrumentation scope, one per collection method, and no read surface could reach it. "Which of my collectors is losing its entities" was unanswerable: on one deployment, 1312 liveness expirations in twelve hours with no way to attribute them. Every change event now carries scope, on both surfaces, and recent_changes filters on it. On a liveness expiry the scope names the collector that stopped speaking, remembered on the liveness reference and read back when it lapses, because Toise authors that event.

In the same spirit, an entity now lists collected_by beside asserted_by: who sends it, and how it was gathered. An entity seen by two collection methods of one agent is a different situation from one seen by one method from two agents, and the two were indistinguishable.

Every change event also carries commit_seq. The events of one producer observation share it, and events from different commits do not. A gateway change is deliberately a route deleted plus a route created, and the engine committed both in one durable append while telling no one, so every consumer re-invented the pairing with its own tolerance window. Pair on equality now, with no window. Toise still does not say what the pairing means: that a route moved is the consumer's conclusion, not the engine's. The field is absent on events written before it existed, where absence means unknown rather than a commit of its own.

Two more things a reader of the change feed needed. match on recent_changes filters by attribute as the event recorded it, not as the live graph holds it, so an entity that died mid-window still matches on the attribute it died with. And a change page now declares its order and whether it holds the newest or the oldest event: the two change pages run in opposite directions while sharing one type, and a reader who took the last edge of a generous history page believed they held the newest event while holding one days old.

GraphQL stops being a mute source

graphScope now reports freshness, read from the journal as the moment a producer last spoke rather than the moment the projection was rebuilt, and accepts asOf. Entity gained assertedBy and collectedBy, which GraphQL had never carried in any form. A source that cannot declare its own scope and freshness loses an arbitration by default, and GraphQL consumers were holding one. A test asserts that both surfaces report the same numbers for the same question at the same instant.

The governance keys leave the entity.* namespace

The keys Toise invented for ownership, location, lifecycle and labels are now senhub.owner.team, senhub.owner.contact, senhub.location.site and its siblings, senhub.lifecycle.status, senhub.label.<key> and senhub.device.role. The semantic-conventions naming rule is explicit that an existing OpenTelemetry namespace must not be used as a prefix for application-specific keys, and entity.* belongs to the entity-events convention. The keys that come from that convention are unchanged.

Nothing is rejected and nothing is rewritten. These are descriptive attributes, so the values already in a graph are still there and still readable, and no entity is re-minted. describe_schema lists each key's previous spelling in a was field, because a consumer filtering over history needs the key to find as well as the key to use. A producer migrates by emitting the new spelling. Done before 1.0 rather than after: had upstream later defined a key with the same name and other semantics, field data would have meant two things at once.

Also in this release

One reserve left open

One module-level advisory in klauspost/compress was left unbumped here: it was unreachable from Toise's code, and bumping it then would have meant shipping a stable that nothing had qualified. It was closed in 0.21.0.


Get it

go install github.com/toise-dev/toise/cmd/toise-server@v0.20.0

Binaries for linux and darwin (amd64 / arm64) are on the release, each with a checksum, and the multi-architecture container image is on GHCR. See the 0.20.0 docs and the changelog.