Build Report · Week 38 · the week the whole stack changed protocol at once

Everyone must be introduced before they can speak

302 changes this week, and an unusual shape: instead of one project building one feature, six projects in two languages moved to a new revision of the same underlying protocol — 72 changes, landed in the same seven days. The new revision adds one rule that sounds mild and is not: two parties must be formally introduced before either will accept anything from the other, and an unintroduced message is dropped in silence. Nothing is refused, nothing is logged, nothing comes back. Most of the week was spent making that rule survivable — relationships that outlive a restart, a way to re-introduce yourself automatically, and an alarm on the one event nobody could previously see. Alongside it: post-quantum keys reached all the way from the key store to a signed credential, round two of the security review closed an information leak and five classes of gap in the identity-log host, and the community app made joining a community something a newcomer can actually walk through.

📅 September 14 → September 20, 2026 · 10 projects scanned, 8 shipped
302
changes shipped across eight of the ten projects in scope
72
of them on one coordinated protocol migration, in six projects and two languages
+1,006
new automated checks (12,787 → 13,793), like-for-like across the same eight projects
58
changes on the busiest day — Wednesday September 16
The Big Picture

Six things changed this week

A protocol migration is the least glamorous kind of week and one of the most revealing, because a rule that changes everywhere at once finds every place that was quietly getting away with something. Nearly every fix below has the same shape: a limit that was configured and never charged, a gate that was correct and never called, a drop that was counted and never announced. The theme of the week is not new capability. It is saying something.

🔗

One protocol change, six codebases, two languages

The secure-messaging protocol the stack rides on moved to revision 3 — and there is no partial path: the envelope, the encryption and all five message layouts change together, so a message is wholly old or wholly new. It landed the same week in the shared toolkit, the trust backbone, the community app, the identity-log host, the trust registry and the browser wallet's independent JavaScript implementation. The two implementations were then checked against the specification's own worked examples byte for byte, not field by field — which is the only version of that check that catches a disagreement about padding.

🤫

The silence, and what it took to fix

The new rule: an endpoint silently drops traffic from a party it holds no relationship with. Drop, not refuse — so the sender sees a service that accepts connections and never replies. Three separate bugs rode that rule into invisibility: introductions were received and never recorded, the record of who you knew was lost on restart, and one component could send but had no way to introduce itself. The fix is a full recovery mechanism — relationships that survive a restart, automatic re-introduction before a send, paced retries so a genuinely-down peer is not hammered, and idle cleanup — plus, finally, a counter on the drop itself.

⚛️

Post-quantum, from the key store to the credential

Quantum-resistant signing stopped being a capability and became a path. Keys are now derived from the same seed chain as every other key and carry the algorithm they were minted with; an identity template declares which algorithms its keys use; the provisioning bundle can carry a second signing key; and a community's credential service signs with every key it holds, so one credential carries both a classical and a post-quantum signature. Three separate front doors had been shut the whole time — a command-line tool that matched three key names, a library dependency taken without the feature switched on, and a test suite that had never compiled.

📮

One way to send, whatever the wire

Adding a new service to the stack should mean choosing an address and a message name. It did not: four components had each hand-rolled their own way of putting the same message on the wire, and a fifth would have been a fifth. There is now one outbound path, and it picks its carrier from what the recipient advertises rather than from what was compiled in. A service that speaks only the newer transport is now reachable; one that speaks plain web requests joins the same router rather than bypassing it.

🛡️

Round two of the security review

Fourteen hardening changes in five projects. The identity-log host closed five classes at once — reading another tenant's records, exceeding a quota, replaying an old request, being pointed at an internal address, and acting without the right. The message hub's forwarding path was put behind a guard that refuses to dial any address on a private network, and now forces certificate checking on in real builds whatever the config says. And the agent stopped distinguishing "not yours" from "not there" — answering those separately had been quietly confirming which identifiers exist.

🚪

Joining a community, made walkable

Forty changes on the one path a newcomer actually takes. You can now create the face you will be known by without leaving the flow, apply as a second persona, read a community's requirements from the endpoint it publishes rather than guessing, see every way in to a community at once instead of one, and abandon an application you no longer want. The small ones matter most here: telling apart the two different ways of having no invitation, and an oversized submission that now fails loudly instead of vanishing.

Why It Matters

What this means, in plain terms

🔇

The worst failure is the one that says nothing

A service that refuses you is annoying. A service that accepts your connection, takes your message and silently discards it is far worse, because every party involved believes it is working. That is the failure mode the new protocol rule creates by design, and it is why so much of this week went on plumbing rather than features. The single most valuable change of the week is probably the smallest: a counter that increments when a message is dropped for having no relationship. It turns an invisible outage into an alert.

🔑

Post-quantum is a plumbing problem, not a maths problem

The hard cryptography for quantum-resistant signatures has been settled for a while and was already in the codebase. What stopped anyone using it was three unremarkable things: a tool that only recognised three key names, a library included without its optional feature enabled, and a set of tests that had silently never been compiled. Every one was found by trying to use the feature end to end rather than by reviewing the cryptography. Capability nobody can reach is not capability.

🗑️

Show people what they are about to destroy

A delete confirmation listed the one thing you named. The delete itself removed that thing and everything underneath it. So a workspace with empty pockets of its own, sitting above children full of keys and identities, previewed as holding nothing — and every tool that asked "is this safe?" got the wrong answer. Two functions were answering different questions about the same act. The preview now walks the whole tree, reports what it could not remove from the servers that host it, and is the thing the confirmation is built from.

302
changes across 8 of 10 projects — busiest day: 58 on Wednesday September 16
+1,006
new automated checks (12,787 → 13,793) — like-for-like across the same eight projects
+124k
lines added (−10.9k removed) across 927 files
6
projects that adopted the new protocol revision in the same week, in two languages
★ Big outcome #1 · The week's headline

A protocol that insists on an introduction — and the silence that followed

Imagine a members' club where the rule is that you may only speak to someone you have been formally introduced to. It is a good rule: it means nobody can walk in off the street and start making claims. The problem is how it is enforced. Speak to a stranger and they do not say "we haven't met" — they simply do not hear you. You keep talking; nothing happens; you have no way to tell whether they are ignoring you, whether the room is empty, or whether you are in the wrong building.

What changed. Revision 3 of the secure-messaging protocol replaces the encryption scheme, rewrites every message layout and moves the proof-of-sender into a different part of the message. There is no gradual migration available — a message is entirely old or entirely new — so every project that speaks it had to move together, and did: the shared toolkit, the trust backbone, the community app, the identity-log host, the trust registry, and the browser wallet's separate JavaScript implementation. Both implementations were then validated against the specification's published examples re-packed byte for byte, which is a materially stronger check than comparing the fields and is what catches two implementations that agree on meaning and disagree on encoding.

What it exposed. The introduction rule turned three latent bugs into invisible ones. Introductions arriving over the wire were unpacked as if they were ordinary traffic, so no relationship was ever recorded and every message afterwards was discarded. The record of who you knew lived only in memory, so a restart lost half of every relationship. And one component could send but had no way to introduce itself, so anything built on it went quiet after a restart with no way back. Each was fixed, and then generalised into a recovery mechanism: durable relationships, automatic re-introduction before sending, paced retry so a peer that really is down is not re-invited forever, a cap on how often an introduction can be accepted, and cleanup of relationships nobody has used in a week.

🤝
You must be introduced
Two parties exchange an introduction before any real traffic. Without it, traffic is dropped — not refused.
💾
The introduction is remembered
Relationships are written to disk, so a restart no longer erases who you know.
🔁
And re-made if it is lost
A sender that finds its half missing re-introduces itself and sends, automatically, with paced retries.
🚨
And the drop is now audible
The one event nobody could see during the incident increments a counter, so it is an alert rather than a single log line.
★ Big outcome #2 · Post-quantum end to end, and round two of the security review

A quantum-resistant key that reaches all the way to a signed credential — and five classes of gap closed behind it

Two strands, and they met in the same week. One is forward-looking: making the stack's quantum-resistant signing usable by an operator rather than reachable only by a library author. The other is the second round of an external security review, which found the kind of things a first round does not — not missing locks, but locks that were installed and never engaged.

⚛️ A key that knows what it is

A derived key now carries the algorithm it was minted with rather than having it inferred, and quantum-resistant keys derive from the same seed chain as every other key — so one recovery phrase still recovers everything. An identity template declares which algorithms its key slots use, and the agent mints exactly what the template asks for.

📜 One credential, two signatures

A community's credential service now signs with every key it holds, so a credential can carry a classical signature and a quantum-resistant one side by side — verifiable today by everyone, and still verifiable later by those who need the stronger one. Every verification path was taught to read a document with more than one signature, which is the half that makes it useful.

🕵️ "Not yours" and "not there" are now the same answer

The agent checked permission and existence separately and reported them separately — so the difference between the two answers told an outsider which identifiers were real. Both now return the same refusal. Eight documented error codes that the specification declared and the service had never emitted are now emitted.

🚫 Never dial an address you were handed

The message hub's forwarding path now refuses any address on a private network — cloud metadata, internal ranges, loopback — checked at the point of the request and with the name re-resolved on the websocket path too, because a name can be pointed inwards a moment after it is checked. Certificate validation is forced on in real builds regardless of the configuration flag.

And two that were simply switched off. The message hub's per-identity rate limit had been built, garbage-collected and logged as enabled — and nothing ever called it, so it refused nothing. The traffic limiter in front of the agent trusted a forwarded-address header on a global on/off flag, which let an untrusted caller forge one to dodge its own limit while every client behind a legitimate proxy shared a single bucket; it now reads that header only from an explicit list of trusted proxy networks.

The Quality Delta · last report → now

How much healthier is it than a week ago?

Counted the same way as the previous nine reports, across the same eight Rust projects. The browser wallet and the phone approver are written in TypeScript and Swift, so they contribute nothing to these greps — their changes are in the headline totals above, but the scoreboard stays a clean like-for-like.

Automated checks (run on every change; same eight projects)
12,787→13,793
▲ +1,006 checks (+7.9%) — a tenth consecutive rise; the toolkit alone added 402 of them, most around the new protocol revision and the message-queue work
Test stand-ins (fakes that let a component be tested without the rest of the system)
803→865
▲ +62 (+7.7%) — the largest rise recorded here, and concentrated in the trust backbone (+53) where the relationship-recovery work needed a peer it could control
Future-proofed interfaces (surfaces that can gain a field without breaking everyone downstream)
349→398
▲ +49 — a second consecutive large rise, driven by the message types the new protocol revision reshaped
Unused-code markers (lower is better — and this one moved the wrong way again)
131→132
▲ +1 — worse. Smaller than last week's +5, but the second consecutive rise. It was named as something to reverse in last week's "what's next" and it was not reversed

In one line: a third of last week's volume, spent almost entirely on one protocol migration and the failures it uncovered — with the largest test-stand-in rise recorded here, and one measure that got worse for the second week running. Three honest notes. First, 302 against last week's 463 is a genuine fall, not an artefact: the same ten projects, the same method. Last week was the busiest of the year and this is a normal-to-busy week. Second, unused-code markers rose again, by one. It is a small number and it is reported because it was explicitly on last week's list to fix; a measure named and then not moved is worth saying out loud. Third, the test-stand-in figure opens at 803 where the Week-37 page published 807 as its ending value. The identical scan points re-read on origin/main now give 803 — a measurement artefact of which copy of the code was read, not four stand-ins deleted. 803 is used here so the before and after are consistent with each other. And one about the reading itself: six of the ten local copies were behind the published main line when this ran, one of them by 21 changes. Every figure on this page is read from origin/main, not from those copies.

The Detail · 10 Projects in Scope

What changed, project by project

The published report tracks around eleven projects; this page covers the ten held locally, so the totals here are a floor rather than the whole picture. Eight of the ten shipped something this week.

The trust backbone ★

ENGINE
The engine room — identities, communities, credentials, the agent a person runs. (verifiable-trust-infrastructure)
  • The protocol migration, and its recovery story — 29 changes. Revision 3 adopted; relationships persisted across restarts; every client taught to introduce itself before sending; inbound introductions answered instead of recorded-and-ignored; a boot-time sweep that re-asserts what it knows and evicts what it does not use. The credential service had been sending to the trust registry with no relationship at all, so every frame was being dropped.
  • Post-quantum reached the operator — quantum-resistant keys derive from the seed chain and carry their own algorithm, templates declare what their key slots use, the provisioning bundle carries a second signing key, and the credential service issues with every key it holds. The command-line tool that had recognised exactly three key names — the front door in front of all of it — now recognises them.
  • One outbound path for tasks — four hand-rolled ways of putting the same message on the wire replaced by one seam that picks its carrier from what the recipient advertises. Plain web requests joined the same router rather than bypassing it.
  • Delete shows you the whole tree first — the preview collected the named workspace and the delete removed everything under it. It now walks the subtree, and reports what it could not remove from the servers hosting it rather than reporting success.
  • The console learned to explain a policy by running it — admission rules, statement types and vetter-eligibility are authored from the console, and the flow chart is drawn by evaluating the policy rather than reading it.
  • Security — the existence leak closed, the traffic limiter keyed on trusted proxy networks, a patched TLS library taken for an advisory, and five suppressions dropped because nothing matched them any more.
Rev 3 + recoveryPost-quantum end to endOne outbound pathExistence leak closed 133 changes · +49.4k

The developer kit ★

SDK
The shared toolkit every project here builds on — and the message hub itself. (affinidi-tdk-rs)
  • The protocol revision itself — this is where revision 3 is implemented, and where the specification's worked examples are re-packed byte for byte to prove it.
  • A message queue that can be emptied — nineteen changes on one problem. A message is held against its sender until the recipient deletes it, so a receiver whose deletions were failing filled up other people's allowances. The depth limit now counts per relationship rather than per sender in total (a community holding 200 uncollected cards is not someone aiming 200 at one victim), the limits finally apply to the paths that had no limit at all, a queue can be inspected before it refuses and cleared in one call, and a pickup deals one message per sender in turn so a live peer is not stuck behind a silent one.
  • The forwarding path closed to the private network — and certificate checking forced on in real builds. The per-identity rate limit that had been refusing nothing now refuses.
  • Three quiet correctness bugs — a quantum-resistant secret that was generated, used, and never actually written to disk; a valid credential signature rejected because one field was silently dropped on the way in; and a set of post-quantum tests that had never once compiled.
Rev 3 implementationQueue durabilityPrivate-network guardKeys that persist 51 changes · +43.0k

OpenVTC — the community app ★

APP
The app a person uses to hold identities and join communities. (openvtc)
  • The join, made walkable — create the face you will be known by without leaving the flow, apply as a second persona, read the community's requirements from the endpoint it publishes, and see every route in at once rather than one at a time. A route whose first step you can take is no longer presented as blocked.
  • The vetting desk — one place for vetting, headed by the credential that makes you a vetter; an application can be abandoned; a refusal says how to fix it rather than only that it happened; and a ticket's QR code gets a screen of its own at a size that can actually be scanned.
  • Revision 3 adopted as a first-class citizen, and tasks carried in the shared envelope in both directions — a routing prefix that had been forgotten now fails a test rather than a deployment.
  • The honest small ones — telling apart the two different ways of having no invitation; an oversized submission that fails loudly instead of disappearing; a remembered theme scoped per profile; and the questions taken off the screens that were guessing at the answers.
Guided joinVetting deskPersonas in the flowRev 3 40 changes · +13.0k

The browser wallet ★

APP
The wallet that lives in the browser — and a second, independent implementation of the protocol. (pnm-browser-plugin)
  • A second implementation of revision 3, in JavaScript — packing the new format, reading both old and new during the changeover, forming relationships the new revision requires, and re-packing the specification's worked examples byte for byte. Two independent implementations validated against the same vectors is the strongest interoperability evidence the stack has.
  • Four protocol conformance fixes found by writing that implementation — a hop limit too low for real routes, an acceptance adopted for an introduction never sent, and a message body shape that was being accepted too loosely. All four were fixed in the Rust implementation too, in the same week.
  • Mint an identity and read its history from the console, realign a DID's key records, and show the subtree a workspace deletion destroys — the browser half of the trust backbone's delete-preview fix.
  • One security bump and a build fix for a preload hint the browser refuses to honour in an extension.
Independent Rev 3Byte-exact vectors4 conformance fixesDelete preview 40 changes · +10.7k

The identity-log host

SERVICE
Publishes the tamper-evident history behind an identity. (affinidi-webvh-service)
  • The heaviest security week of any project here — seven hardening changes. One closed five classes at once: reading another tenant's records, exceeding a quota, replaying an old request, being pointed at an internal address, and acting without the right. The admin proxy now fails closed on an empty allowlist rather than open, freshness is required on the authentication path so an old request cannot be replayed, and outbound calls gained timeouts and size caps.
  • Relationships persisted and inbound introductions answered — the same recovery work as the backbone, and the shared envelope adopted on the task path.
  • An abandoned sign-in no longer locks out the control plane, and refusals answer with the right status and name which service refused.
5 gap classes closedFail-closed proxyReplay protectionRev 3 23 changes · +5.6k

The supporting projects

TOOL · SERVICE
Git signing, the trust registry and the chat bridge. (verifiable-git-infrastructure, affinidi-trust-registry-rs, vti-message-bridge)
  • Git signing unblocked a publishing chain — two dependency steps taken specifically so its components could be released and the community app could follow without ending up with two copies of the same library. A quiet week by line count and a necessary one.
  • The trust registry took the whole revision-3 stack, persisted relationships and learned to answer introduction frames, then cut a release. It is the other end of the credential service's dropped-frames bug.
  • The chat bridge shipped one change, aligning with the rest of the stack.
Publishing unblockedRegistry on Rev 3Stack aligned 15 changes · +2.2k

The quiet projects

2 SHIPPED NOTHING
Scanned and unchanged — recorded so "unchanged" is distinguishable from "not looked at". (vta-mobile-agent-ios, affinidi-keri-rs)
  • The phone approver is quiet again. It ended a six-week gap last week with nine changes and shipped nothing this week. Worth naming: it is the device a person uses to approve what their agent does, and a protocol revision has just landed underneath it.
  • The KERI library — a fourth consecutive quiet week, sitting as a stable dependency. Unchanged since 23 August.
ScannedApprover risk restatedStable dependency 0 changes
★ What's next · Prove the migration against someone else's code

Two implementations agreeing with each other is encouraging. Agreeing with a third is evidence.

Take revision 3 to an outside implementation. The stack now has two independent implementations — Rust and JavaScript — validated byte for byte against the specification's own examples. That is a strong internal result and it is still internal. The next step is the same vectors against an implementation nobody here wrote. Wake the phone approver. It shipped nine changes last week after six quiet weeks and nothing this week, and the protocol underneath it has just changed; it is the standing risk on this page. Finish the post-quantum path at the verifier. Issuance now signs with every key; the credential is only as useful as the far end's willingness to read the stronger signature, and that is a cross-language piece of work. And reverse the measure that slipped. Unused-code markers rose again, by one, having been named last week as the thing to fix — small, but a stated intention that did not happen.

🌍
Interop with a third party
The same byte-exact vectors against an implementation written outside this stack.
📱
The approver, again
One active week in seven, and the protocol beneath it just moved. Named as the standing risk.
⚛️
Verify the stronger signature
Issuance is done in Rust. The verifying side, in every language a holder might use, is not.
🧹
Reverse the one that slipped
Second consecutive rise in unused-code markers, after being named as next week's work.