Build Report · Week 36 · the week a shared room learned to outlive the person who made it

A place several organisations can share without any of them — or us — holding the guest list

88 changes this week, against 177 last week — and the drop is the story rather than a dip, because last week's number was one enormous measurement exercise and this week's is two features built to a design. Data rooms landed end to end: a shared space whose door is governed by credentials the room itself issues, with deliberately no member list anywhere — not at the host, not with us. Around that came the four things a shared space needs before anyone can rely on it: a way for a member to lend their AI agent strictly less access than they hold, an audit trail that records every action without learning who took it, group custody so a key-holder still has the group tomorrow, and succession so a room outlives one person becoming unreachable. Alongside it, personas reached the point where the app can show you what each face actually tells each community, and the messaging bridge learned Microsoft Teams.

📅 August 30 → September 6, 2026 · 9 projects in scope · 6 of them shipped
88
changes shipped across six of the nine projects
0
member lists held by the host of a private room — the design's central claim, now enforced in the storage
+523
new automated checks across the nine projects (11,185 → 11,708)
23
changes on the busiest day — Sunday September 6
The Big Picture

Four things changed this week

Last week installed a referee and counted what it found. This week spent the credit: two substantial features built end to end, each one turning on the same discipline — decide what the system must never be able to do, then make that structural rather than a rule someone has to remember. A room that holds no roster cannot leak one. An agent that can only ask for less than its human holds cannot be handed more.

🚪

A shared room with no guest list

A data room is a space several parties share, where the door is governed by credentials the room itself issues. The storage deliberately records an owner, a visibility, an epoch and a retention period — and no member list. Not "not yet": there must not be one. The moment the host keeps a roster and consults it, three things stop being true at once — the room can no longer move to a different host without reissuing everyone's credentials, the host becomes part of the room's definition of membership, and a room whose contents the host cannot read has just acquired a membership list it can. Written as storage first, so the layer above became a thin wrapper over settled behaviour instead of a place where storage decisions get made under time pressure.

🤖

Your agent gets less than you hold

The whole design turns on a member equipping their AI agent with strictly less than they have — read only, four hours, one host — and until this week nothing minted that. A member wanting to give an agent access had two real options: hand over their own credentials, which is precisely what this exists to prevent, or hand-craft a reduced one, which nobody does. Now the agent asks and the member's own agent-service mints it, because that service already holds their keys and is already trusted; the room's host never is, and only ever sees the result. Four things a caller cannot get by asking, each closing a route back to the credential hand-off it replaces — asking for more than you hold fails in the reduction step itself, not at a policy check somebody could forget to write.

🎭

What each face actually tells each community

A holder can present different faces to different communities. The panel could already say which face a community sees; it could not say what that face tells them — and both halves are needed to answer "what does this community actually know about me". Each membership row now reads presents: work (3 claims), and says plainly when it could not ask rather than showing a confident blank. Underneath, the holder's own identity became a fourth separate store, because disclosure control is its whole point, with a boundary a context cannot read across by construction rather than by rule.

💬

Microsoft Teams, both ways it can be done

The messaging bridge — how a person talks to their agent from an app they already use — gained Teams. Not one connector but a neutral core plus two genuinely different backends, because Teams offers no equivalent of the outbound-only trick Slack allows. One acts as you, polls for new messages, and needs no public address; the other is a bot people @-mention, is real-time, and requires a public endpoint that validates every inbound token. They are different products, not two routes to one thing, so only one runs at a time — running both would deliver every conversation twice. 131 tests, none of which need a Microsoft tenant or an internet connection.

Why It Matters

What this means, in plain terms

📋

The safest list is the one that does not exist

Every shared-workspace product on the market keeps a list of who is in each room, because it is the obvious way to check whether you may come in. That list is also the thing most worth stealing, and the thing a subpoena asks for first. Here the check is inverted: you arrive holding a credential the room issued you, the door reads it, and nobody had to look you up — so there is no list to steal, leak, or hand over. It also makes a room portable: because the host is not part of what defines membership, a room can move to a different host without reissuing anyone's access. The rule was enforced in the storage before the feature was built on top, which is the difference between a principle and a promise.

🕵️

An audit log that knows what happened but not who

Every serious system logs who did what. On a private room, that single habit hands the host exactly the membership it was built never to learn — assembled one entry at a time, by the component least likely to notice it is doing so. It is a silent failure: nothing breaks, no test goes red, and the log looks exactly like a normal one. So the log records the action and not the actor. Reads are logged as carefully as writes, and on shared material reads are the more interesting half — a write log tells you what a room contains; a read log tells you who has seen it, which is the question an incident review actually asks.

🔑

A shared space should not end because one person is unreachable

The room's owner is its sole committer, so a room whose owner goes quiet cannot move forward: it cannot admit anyone, cannot be renewed, and lapses to read-only. Without a way to hand over, one person becoming unavailable quietly ends a space several organisations depend on. Handover now works two ways — the owner passing it on while present, or a nominated successor claiming it — and the claim needs three things at once: a nomination the room itself issued naming them, a room that has gone genuinely dormant rather than merely lapsed, and their own membership. A nomination grants a permission deliberately spelled as a word no room operation accepts, so while the owner is present it confers nothing at all.

88
changes across 6 of 9 projects — busiest day: 23 on Sunday September 6
+523
new automated checks (11,185 → 11,708) — like-for-like, same nine projects as last week
+68.5k
lines added (−2.0k removed) across 375 files
131
tests in the new Teams connector — none needing a Microsoft tenant or a network route
★ Big outcome #1 · The week's headline

Data rooms — a shared space governed by credentials it issues itself

Think of a serviced meeting room used by several firms working on one deal. The obvious way to run it is a receptionist with a clipboard: a list of who may enter, checked at the door. It works, and it means the building knows exactly who is meeting whom — and the clipboard is the first thing anyone asks for. The alternative is a room that issues its own keys. You arrive holding one; the lock reads it; nobody looks you up, because there is no list to look you up in. The building can be replaced without recutting anyone's key, because the building was never what made you a member.

What was built. The storage and its invariants first, deliberately, so that the layer above became a thin wrapper over settled behaviour rather than a place where storage decisions get made under time pressure. The room record carries an owner, a visibility, an epoch and a retention period — and no roster. On top of that came the parts a shared space needs before anyone can depend on it: group custody, so the group survives one key-holder; a member curating a record; the full lifecycle with the host never deciding anything; and audit on every operation that records the action but not the actor.

Why the last piece is the one that matters. A room that cannot change hands is a room with a single point of failure wearing a person's name. The owner is the sole committer, so an unreachable owner means a room that cannot advance — cannot admit anyone, cannot be renewed, lapses to read-only. Succession closes that: transfer while present, or a claim by a nominated successor that requires a nomination the room itself issued, a room that has gone dormant rather than merely lapsed, and the claimant's own membership — three independent conditions, each closing a different route to a takeover. The nomination grants a permission spelled as a word no room operation will accept, and that word is deliberately kept out of the list of real actions, so no later edit can quietly make it mean something.

🎟️
The room issues the keys
Access is a credential the room grants you, not a row in a list it keeps about you. The door reads what you present.
🚫
Nobody holds a roster
No member list in the storage — by rule, not by omission. So the host cannot leak one, and the room can move hosts without reissuing anyone.
👁️
Log the act, not the actor
Every operation audited, reads included. On a private room the actor's identity is never written — that one habit would rebuild the roster an entry at a time.
🔄
Outlive any one person
Transfer while present, or a dormant room claimed by a nominated successor. Three conditions must hold at once.
★ Big outcome #2 · Personas, and the boundary a context cannot read across

Different faces for different communities — and now you can see what each one says

A person is not the same person to their employer, their doctor and a hobby forum. This stack lets a holder keep separate faces and choose which one each community sees. This week the feature became something a person can actually inspect and an operator can actually run.

👁️ "What does this community know about me?"

The panel could say which face a community sees. It could not say what that face tells them — and you need both halves to answer the question anyone actually asks. Each membership row now carries a third clause: presents: work (3 claims). When it cannot ask, it says so, rather than showing a confident blank that reads as "nothing shared".

🧱 A boundary that is structural, not a rule

The holder's identity became a fourth separate store, beside the vault, the agent's memory and app state — distinct because disclosure control is its entire point, and app state promises never to interpret what it holds, so it cannot host the thing whose job is deciding who may see what. Two scopes share it deliberately: the correlation index has to be able to see one value presented through two personas in two contexts, which a per-context index cannot see by construction.

🧪 A suite that only tested refusals

The honest note of the week. End-to-end tests were added and the seam called closed — but every one asserted what must be refused, and almost none asserted the thing works. A handler that rejects everything passes a suite like that, and one of them did. Adding a positive test per operation found five shipped defects, including one that rejected every valid request, whose only existing test passed for the wrong reason.

🗑️ Drift retired, and what still blocks the rest

Two older shapes were carried "for now" and were quietly diverging from the published specification. One is now retired outright; for the other, the report names precisely what still blocks it rather than leaving it as an untracked intention. The operator surface landed alongside — twenty-four request shapes and a preview built to be read, hand-written on the client side on purpose, because a generated mirror is a second definition of one contract and free to drift.

The Quality Delta · last report → now

How much healthier is it than a week ago?

Counted the same way as the previous seven weeks, and again a clean like-for-like: the same nine projects, no additions, each measured from the exact commit it had reached in the Week 35 report. Every figure below is main-line only — the unmerged branch described further down contributes nothing to any of them.

Automated checks (run on every change; all nine projects)
11,185→11,708
▲ +523 checks (+4.7%) — an eighth consecutive weekly rise
Test stand-ins (fakes that let a component be tested without the rest of the system)
750→774
▲ +24 (+3.2%) — most of them the Teams backend seam, which is why 131 tests need no tenant
Future-proofed interfaces (surfaces that can gain a field without breaking everyone downstream)
279→283
▲ +4 — a third consecutive rise, this time deliberate rather than incidental
Unused-code markers (the measure that moved the wrong way last week)
127→126
▼ −1 — small, but the first fall in four weeks, and reported here whichever way it moves

In one line: a quieter week by volume — 88 changes against 177 — spent building two designed features rather than measuring, and all four health measures moved the right way for the first time in a month. Four honest notes. First, the volume drop is real and worth reading correctly: last week's 177 was one enormous conformance sweep, and a week of two features landing end to end is not a slower week, but it is a smaller number and is reported as one. Second, the persona tests were wrong in a way that reads as good news — a suite that only ever asserted refusals passed a handler that refused everything, and adding one positive test per operation surfaced five shipped defects; the count of tests rose all year while that gap was open, which is a caution about the headline measure on this very page. Third, the data-room work is not finished by the parts that shipped: what landed is real and driven end to end, but one privacy profile remains outstanding and is a decision for a standards group rather than a task anyone here can close. Fourth — unchanged from the last five reports — the phone approver shipped nothing for a sixth consecutive week, and the approval flow it partners moved again.

In flight · not counted above
◐ Included by request · unmerged work, excluded from every figure on this page

The secure-messaging rewrite that ran all week on a branch

Alongside everything above, a second front ran the whole week in the developer kit and has not landed: the migration to revision 3 of the protocol the stack uses for private, metadata-resistant messaging. It is 41 changes and roughly 10,000 lines — comparable to the week's two headline features combined — and it is shown here because leaving it out would misrepresent where the effort went. It is excluded from the 88, from the line counts and from the health measures, because it has shipped nothing: no release, and no merge request opened yet. It will be counted in full in the week it lands.

📐 Marked against the specification's own answer sheet

The new revision publishes official test vectors — worked examples produced by the reference implementation. Eight of the nine pass; the ninth is truncated at the source. These catch what testing against another implementation structurally cannot: if both sides misread the specification the same way, they agree with each other and the test goes green. That is not hypothetical — two values were in the wrong order on this branch until the specification's own text settled it, and an implementation-to-implementation test would have been perfectly happy.

🔎 Two privacy defects the audit found

The specification forbids a relay from storing the addresses of the two endpoints whose traffic it carries. It was storing one — written into a queue that is durable by design, so a relay in the middle was accumulating who is talking to whom on disk, from relationships that were none of its business. Fixed, along with padding that was not being discarded as required.

🤝 Interoperability chosen over a local convenience

A bridge that let non-participating software be reached through this transport was removed, because the new revision carries the inner message in a shape that a conformant peer rejects. Keeping it would have produced a wire that only works against ourselves. Interoperability is the entire point of implementing a published specification, so the convenience goes.

🤐 Refusals that stopped answering questions

The three reasons a delivery is refused are each a fact about somebody else's address. Answering them separately let an authenticated peer map which addresses a relay serves and probe their access lists. All three now give one identical answer.

The Detail · 9 Projects in Scope

What changed, project by project

The published report tracks around eleven projects. This update is drawn from the nine repositories present in this workspace — the same nine as last week. Other parts, such as the browser plugin and the personal-AI agent, aren't included. Figures are read from each project's published main line, not from a local copy, and each project is measured from the exact commit it had reached in the Week 35 report.

The trust backbone ★

ENGINE
The engine room — identities, communities, shared rooms, and the agent that acts on your behalf. (verifiable-trust-infrastructure)
  • Data rooms, end to end — the week's headline and the bulk of the engine's work: storage, dispatch, verification, the group-messaging layer and a host, then group custody, record curation, the full lifecycle with the host never deciding, and audit on every operation. The storage carries an owner, a visibility, an epoch and a retention period and deliberately no member list — because a host that keeps one becomes part of the room's definition of membership, and a room whose contents it cannot read would acquire a roster it can.
  • Succession, so a room outlives one person — the owner is a room's sole committer, so an unreachable owner leaves a space that cannot admit anyone or be renewed. Handover now works both as a transfer while the owner is present and as a claim by a nominated successor, the claim requiring three independent things at once: a nomination the room itself issued, a room genuinely dormant rather than merely lapsed, and the claimant's own membership. The nomination grants a permission spelled as a word no room operation accepts, kept out of the list of real actions so no later edit can quietly give it meaning.
  • An agent that can only ever hold less than its human — the design depends on a member equipping their agent with a reduced credential — read only, four hours, one host — and nothing minted one, so the real choice was hand over your own credentials or hand-craft a reduction nobody actually does. The member's own agent-service now mints it on request; the room's host never sees anything but the result. Asking for more than the principal holds fails inside the reduction step itself rather than at a policy check somebody could forget to write.
  • An audit trail that never learns who — room operations wrote no audit entry at all, which every other consequential surface here does. Reads are audited alongside writes and are the more interesting half on shared material. The part worth the review time was which actor may be recorded: on a private room, writing the acting party's identity would hand the host the membership it was built never to learn, assembled one entry at a time by the component least able to notice.
  • Personas: the holder's own identity, and a boundary contexts cannot read across — a fourth distinct store beside the vault, agent memory and app state, because disclosure control is its point and app state promises never to interpret its records. The operator surface and a readable preview landed with it, along with a run of fixes: five defects found by finally testing that the operations work rather than only that they refuse.
  • Releasing, with the version bump policed — a check now blocks a release whose version increase is smaller than the change it actually contains, the release documentation says plainly what counts as a breaking change, and the test pipeline was split so each half runs only when something it covers has changed.
Data rooms end to endSuccessionAudit without identityPersona boundary 53 changes · +50.2k

The messaging bridge ★

SERVICE
Lets a person talk to their agent from a chat app they already use. (vti-message-bridge)
  • Microsoft Teams, in two shapes — a platform-neutral connector (formatting both directions, mentions, reply chains, reactions, attachments) behind a swappable backend, then both backends the design called for. One acts as you and polls, needing no public address but delivering at the poll interval; the other is a bot people @-mention, real-time, but requiring a public endpoint that validates every inbound token. Only one runs at a time — both would double-deliver every conversation they can both see. 131 tests, none needing a Microsoft tenant or a network route.
  • WhatsApp stopped silently losing messages — two paths dropped delivered messages with no trace. A participant who re-registers or rotates keys while the daemon is offline leaves a record that can never decrypt their messages: the library logged a warning and raised no event, so from inside the group the bot simply appeared to be ignoring one person, while everyone else's messages worked. It now reports the gap, clears the stale record, and re-requests the message.
  • A guard against committed key material — the pipeline now fails if secrets are committed, and a session store that should never have been tracked was removed from the repository.
Teams · two backends131 tests, no tenantNo more silent drops 8 changes · +9.4k

The developer kit ★

SDK
The shared toolkit every project here builds on. (affinidi-tdk-rs)
  • Private messaging caught up to its own rulebook — five fixes with one shape: the documentation described an operator control that the newer transport never actually enforced. The address on an envelope is now checked against the connection it arrived on (before, an authenticated sender could borrow any permitted address and reach an inbox that does not admit them), the switch that forces traffic through a relay now governs both transports rather than only the older one, and the trusted-relay list reaches the new transport too. Separately, cross-relay delivery had been failing outright in the default mode and is fixed, and a delivery given up on now actually reaches the sender — the notice was being sent in a form the recipient's own security policy refuses.
  • An unmaintained dependency removed, with eight security advisories — an optional, off-by-default resolver for one identity method was compiled by nothing and still put eight advisories into the build record, including a live denial-of-service. It publishes no source repository at all and had a single release from 2025, so there was nothing to upgrade to and nothing to fork; support for resolving that method was retired. Security warnings went from one vulnerability plus twelve advisories to zero plus one, and the dependency graph from 1,064 packages to 942.
  • Three shared dependencies handled as the breaking changes they are — each sits in published interface surface, so bumping it breaks everyone downstream rather than being routine housekeeping. The release machinery caught one of them mid-flight, about to ship a breaking change disguised as a routine patch; it was pulled out and landed properly. A fourth was sealed away entirely so future updates of it stop being anyone else's problem.
  • Plus, on a branch and not counted here — the revision-3 rewrite described above: 41 changes, ~10,000 lines, unmerged.
Transport controls enforced8 advisories cleared942 packages, from 1,064 16 changes · +7.3k

OpenVTC — the community app

APP
The app a person uses to hold identities, join communities and manage what each one sees. (openvtc)
  • Each community row now says what it is told — not just which face a community sees but what that face presents (presents: work (3 claims)), and an explicit "could not ask" instead of a blank that reads as "nothing shared". The cache for it is deliberately in memory only, following a decision already recorded in this project after a stale negative result caused a bug once before.
  • A cross-relay join could not send — the list of hops started in the wrong place, omitting the sender's own relay, so joining a community reached through a different relay silently failed to send.
  • Settings stopped reporting saves that did not happen — one field is genuinely read-only and now looks it, and the screen no longer claims success for a save it did not perform.
  • Grants acknowledged by fingerprint — the app now confirms a received grant by its exact digest and keeps a copy of what it sent, so both sides can tell precisely which document they agreed on.
Sees what each face saysCross-relay join fixedNo false saves 7 changes · +1.5k

The identity-log host & git signing

SERVICE · TOOL
Publishes identity histories on the web; proves who wrote each line of code. (affinidi-webvh-service, verifiable-git-infrastructure)
  • The wallet chooses the identity — the hosting screen was reading an identity out of the vault rather than letting the wallet supply it, and was probing for a capability it did not need. Both were corrected, matching the same fix made in the engine.
  • Git signing carries the signer's identity — the authentication step now includes who the client is, a small change on a contributor branch, and the only movement in that project this week.
Wallet-chosen identityClient identity in auth 4 changes · +122 lines

The quiet projects

3 SHIPPED NOTHING
Scanned and unchanged — recorded so "unchanged" can be stated rather than assumed. (affinidi-keri-rs, affinidi-trust-registry-rs, vta-mobile-agent-ios)
  • The KERI library — unchanged for a second consecutive week, sitting where it was left on August 23.
  • The trust registry — unchanged since mid-August.
  • The phone approver — a sixth consecutive quiet week. This is the one worth naming. It is the device a person uses to approve what their agent proposes, and the approval flow on the engine side has moved in each of those six weeks. Every week the two drift further apart, and the protection it exists to provide only counts once both halves agree.
All three scannedPhone approver: 6 weeks quiet 0 changes
What's next
★ What's next · Land the branch, close the loop, and get the phone back in step

Ten thousand lines on a branch are worth nothing until they land

Get the messaging rewrite merged. It is the largest single body of work in flight — comparable to both headline features combined — and it currently ships nothing. It has no merge request open, and every week it sits apart from the main line is a week the rest of the stack keeps moving underneath it. The two privacy defects it fixes are live in the code everyone else is running. Test that things work, not only that they refuse. The persona suite passed a handler that rejected every valid request, because every test asserted a refusal. Five shipped defects came out of adding one positive test per operation — and that pattern is not going to be unique to one family; it is worth looking for wherever a security-shaped feature was tested primarily by its boundary. Take the data rooms outside. What shipped is driven end to end internally. The claim that matters — several organisations sharing a space where nobody holds the guest list — is only proven when the parties are genuinely separate and one of them is not us. One privacy profile also remains open, and it is a standards-group decision rather than a task that can be closed here. And get the phone approver moving. Sixth consecutive quiet week, against an approval flow that has changed in every one of them.

🔀
Land the branch
41 changes, ~10k lines, no merge request open. It fixes two live privacy defects that are still in everyone's running code.
✅
Test the happy path
A suite of refusals passes a handler that refuses everything. Five defects proved it. Look for the same shape elsewhere.
🏢
A room with real strangers
Prove the no-roster claim with genuinely separate organisations — the only test that settles it.
📱
Wake the phone
Six weeks quiet while the approval flow moved every week. The gap is now the whole ceremony's weakest point.