docs: B2 execution plan and the 2026-08-28 roadmap amendments (#1433)

* docs(roadmap): record B0/B1 complete and point B2 at its execution plan

* docs(roadmap): keep the hard break after the status header

* docs(roadmap): write down the phase execution pattern B0 and B1 proved

* docs(roadmap): add the 2026-08-28 amendments to B3-B10

* docs(plans): add the B2 execution plan

* docs(plans): index the B2 plan and the roadmap amendment

* docs(plans): record that the owner already synced dev after alpha.4 (#1432)

* docs(plans): apply the final review — verify three claims, serialize B2-5, add the step table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plans): do not point readers at a public repro from the B2-9 table

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-28 07:36:40 +00:00
committed by GitHub
co-authored by Claude Fable 5
parent dd7ed091c0
commit b3f39419e9
3 changed files with 581 additions and 23 deletions
+12 -11
View File
@@ -10,17 +10,18 @@ authority**.
## Active — these drive current work
| Plan | State |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [beta-product-requirements-2026-08-23](beta-product-requirements-2026-08-23.md) | Approved beta scope, frozen. 57 `BPR-*` requirements. |
| [repo-health-roadmap-2026-08-23](repo-health-roadmap-2026-08-23.md) | Phase order and gates, B0B10. **B0 and B1 complete** (HP-0 and HP-1 both accepted); B2 next. B3B10 not started. |
| [repo-health-issue-register-2026-08-23](repo-health-issue-register-2026-08-23.md) | 88 planning rows. Public-safe; not a replacement for the ledger. |
| [beta-requirements-traceability-2026-08-23](beta-requirements-traceability-2026-08-23.md) | Requirement → phase → evidence map. No row is release-qualified. |
| [b0-baseline-2026-08-25](b0-baseline-2026-08-25.md) | **Supersedes the roadmap's "current evidence snapshot."** B0 measurements and dispositions. |
| [b1-repository-foundation-2026-08-25](b1-repository-foundation-2026-08-25.md) | **B1-0 through B1-8 all done.** B1 execution plan. Re-verifies every RL-\* claim against HEAD; several are refuted. |
| [hp-0-scorecard-2026-08-25](hp-0-scorecard-2026-08-25.md) | **HP-0 accepted 2026-08-25.** The single baseline-acceptance artifact. Part-closes `R-08`. |
| [hp-1-scorecard-2026-08-27](hp-1-scorecard-2026-08-27.md) | **HP-1 accepted 2026-08-27.** Structural-diff proofs for the flatten and module rename, plus the B1 exit gate. |
| [audit-2026-08-19-remediation](audit-2026-08-19-remediation.md) | Phases 16 done 2026-08-20; **phase 7 pending**. Its header still reads "in progress 2026-08-19" — stale; the phase table is correct. |
| Plan | State |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [beta-product-requirements-2026-08-23](beta-product-requirements-2026-08-23.md) | Approved beta scope, frozen. 57 `BPR-*` requirements. |
| [repo-health-roadmap-2026-08-23](repo-health-roadmap-2026-08-23.md) | Phase order and gates, B0B10. **B0 and B1 complete** (HP-0 and HP-1 both accepted); **B2 in progress** — plan below. B3B10 not started. **Amended 2026-08-28:** dated lines in B3B10, a "Phase execution pattern" section, and a truthful status header. |
| [repo-health-issue-register-2026-08-23](repo-health-issue-register-2026-08-23.md) | 88 planning rows. Public-safe; not a replacement for the ledger. |
| [beta-requirements-traceability-2026-08-23](beta-requirements-traceability-2026-08-23.md) | Requirement → phase → evidence map. No row is release-qualified. |
| [b0-baseline-2026-08-25](b0-baseline-2026-08-25.md) | **Supersedes the roadmap's "current evidence snapshot."** B0 measurements and dispositions. |
| [b1-repository-foundation-2026-08-25](b1-repository-foundation-2026-08-25.md) | **B1-0 through B1-8 all done.** B1 execution plan. Re-verifies every RL-\* claim against HEAD; several are refuted. |
| [hp-0-scorecard-2026-08-25](hp-0-scorecard-2026-08-25.md) | **HP-0 accepted 2026-08-25.** The single baseline-acceptance artifact. Part-closes `R-08`. |
| [hp-1-scorecard-2026-08-27](hp-1-scorecard-2026-08-27.md) | **HP-1 accepted 2026-08-27.** Structural-diff proofs for the flatten and module rename, plus the B1 exit gate. |
| [b2-protocol-trust-compat-2026-08-28](b2-protocol-trust-compat-2026-08-28.md) | **B2 in progress from 2026-08-28.** Entry gate 1 of 3 met at draft; B2-1 and B2-9 close the other two. Step order: B2-0, B2-1, B2-8, then B2-2 → B2-3 → B2-4, with B2-6/7/9 in parallel and B2-5 serialized. Ends at HP-2. |
| [audit-2026-08-19-remediation](audit-2026-08-19-remediation.md) | Phases 16 done 2026-08-20; **phase 7 pending**. Its header still reads "in progress 2026-08-19" — stale; the phase table is correct. |
## Partially implemented
@@ -0,0 +1,455 @@
# B2 — Freeze server protocol, trust, and compatibility contracts
**Drafted:** 2026-08-28
**Base commit:** `64d2e108` (`dev`, post-PR #1425); `main` @ `b7d388a3` =
`v1.2.0-alpha.4` — claims verified at `64d2e108`; the branch was rebased
onto `dd7ed091` (#1432) before merge
**Status:** in progress — entry gate 1 of 3 met at draft time (see below); B2-0
is the first step. Update this line, not only the step table, when a step
lands.
Primary inputs:
- [beta roadmap](repo-health-roadmap-2026-08-23.md), B2 section and HP-2
- [HP-1 scorecard](hp-1-scorecard-2026-08-27.md), "Hand-off to B2" and "Open
items carried past B1"
- [issue register](repo-health-issue-register-2026-08-23.md) — every row
tagged `B2`
- [requirement traceability](beta-requirements-traceability-2026-08-23.md) —
BPR-031, 032, 040, 050, 051, 080083
## Context
B1 made the repository discoverable and gave the protocol one owner
(`protocol/schema.json`). It changed no behaviour. B2 is the first phase that
does: it freezes the contracts every later server service and every client
must obey — the protocol epoch, the update order, the trust model, the
permission predicates, the audit surface, and the boundary of what beta does
not promise (plugins, federation, a directory).
The owner's decision on 2026-08-28: execute B2 as written rather than cut the
roadmap to ship sooner. BPR-002 (no deadline) and BPR-003 (frozen scope)
stand. Sizes below are personal estimates and never a gate; the honest total is
one to one and a half weeks with agents working steps in parallel.
## Steps at a glance
| Step | What | Size | Parallel with |
| -------- | -------------------------------------------------------------------------------------------------- | -------- | ---------------------- |
| **B2-0** | Alpha.4 verified; `dev` synced (done, #1432); `environment: release`; ENV-03; `dev` `strict: true` | hours | — |
| **B2-1** | Capture the epoch-1 fixtures; retire `voice_speakers` and `member_leave` | 1 day | B2-6, B2-7 |
| **B2-2** | Protocol epoch and negotiation | 1 day | serialized, after B2-8 |
| **B2-3** | Server-first updates through the signed manifest | ½ day | after B2-2 |
| **B2-4** | Compatibility matrix | ½ day | after B2-2 |
| **B2-5** | One permission predicate per security property | 12 days | serialized |
| **B2-6** | Safe audit coverage | ½ day | B2-1, B2-7 |
| **B2-7** | Trust model, absence proofs, plugin boundary | 1 day | B2-1, B2-6 |
| **B2-8** | The nine B2-tagged findings | 1 day | before B2-2 |
| **B2-9** | Security owners and acceptance tests | spread | — |
| **HP-2** | Protocol and threat-model sign-off | — | — |
Order: B2-0, B2-1, B2-8, then B2-2 → B2-3 → B2-4; B2-5 is serialized on its
own; B2-6, B2-7 and B2-9 run in parallel where the table allows.
## Entry gate
| Condition | State 2026-08-28 |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| B1 complete; protocol source has one owner | **Met.** HP-1 accepted 2026-08-27; `protocol/schema.json` generates both consumers from `npm run generate`. |
| Confirmed security findings have private owners and acceptance tests | **B2-9.** Seven local reports under the gitignored `docs/security-findings/`, each already mapped to a public row (HP-0, question 4). |
| Alpha protocol fixtures and updater contracts are captured | **B2-1.** Must land before any protocol change — alpha.4 is the last client that speaks the pre-epoch wire, and it is live now. |
## What B1 carried over that B2-0 closes
| Item | State | B2-0 action |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `dev` `strict: false` (B1 exit condition 6) | 12 checks pinned; a PR can still merge without re-testing against a moved base | Flip to `true` — owner approved 2026-08-28 |
| `environment: release` absent from workflow | The `release` environment exists (1 reviewer) but `release.yml` does not name it, so the reviewer gate never fires | Add it to the publishing job |
| ENV-03 `MSYS_NO_PATHCONV` | `Server/scripts/docker-smoke.sh` still needs the variable set by the caller; Git Bash on Windows otherwise reports a false boot failure | Export it inside the script |
## Verify before you implement
Every claim the roadmap's B2 section rests on, re-tested against `64d2e108`.
| Claim | Verdict | What it means for the work |
| --------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No protocol epoch exists | **Confirmed** | `protocol/schema.json` has `"version": 1` — the schema _format_, read by `Server/cmd/genprotocol/main.go:35`. The `auth` payload (`Server/ws/serve_auth.go:46-52`) is `token`, `last_seq`, `active_channel_id`. Nothing carries a version. |
| The client has no "update required" path | **Half refuted** | `Client/src/lib/ws.ts:306-314` already treats `auth_error` as non-recoverable: sets `intentionalClose`, dispatches, disconnects. So a rejected client **does not** loop today. B2-2 rides that path and pins it with a test; it does not build a new one. |
| The update endpoint ignores compatibility | **Confirmed** | `Server/api/client_update.go:34` advertises the newest GitHub release for the target. The signed manifest (`Server/updater/verify.go:29`) has `version`, `asset`, `sha256`, `assets` — no epoch. |
| WASM is disabled by default | **Confirmed, already done** | `Server/config/config.go` (plugins `Enabled` defaults false; the example config says "Disabled by default"). B2-7 adds wording only. |
| Reserved entries with no sender (S-15) | **Confirmed** | `protocol/schema.json:65` `voice_speakers`, `:75` `member_leave`. No production emit site in `Server/`. |
| L-08 prebuilt WASM still tracked | **Refuted** | Deleted in #1418; `.gitignore` ignores it; the example's README documents the build. Only the deterministic-build gate remains, and it is blocked (B1 plan). B2-7 item 4 is that decision, not the untracking. |
| Audit events exist | **Confirmed** | `Server/db/audit_writer.go` (async batched writer, drops are logged); the `WriteAudit(`, `LogAudit(` and `EnqueueAudit(` call sites (a grep for `Audit(` catches all three). Invite create/revoke have none (S-02). |
| Permission helpers exist but are mirrored | **Confirmed** | `Server/permissions/permissions.go`: `HasPerm`, `HasAnyPerm`, `HasAdmin`, `HasServerPerm`, `EffectivePerms`, `EffectiveChannelPerms`. Typing checks a weaker permission than posting (S-01); ready/refresh/ws mirror send policy by hand (S-12). |
| Trust model has one document | **Refuted** | Pieces live in `docs/protocol.md`, `docs/architecture/voice-e2ee.md`, `docs/credential-storage.md`, `docs/security.md`. No single statement of what the operator can read. |
| Federation / directory / central identity | **Absent, unproven** | Nothing exists. B2-7 turns "nothing" into a test that fails if something appears. |
| WS application close codes in use | **Refuted** | None. Non-test code closes only with the library's 1000/1011. The `4000`/`4096` grep hits are unrelated constants (`CONFIRM_TIMEOUT_MS`, `maxMessageLen`, `Vec::with_capacity`, `touchThrottleMaxEntries`). `4426` will be the first application close code. |
| CI needs a new job for the compatibility test | **Refuted** | A Go table test runs under `go test ./...`, which the required `Server Build & Test` check already executes. No new job, no pin-script change for B2-4. |
Net effect: B2-2 is smaller than the roadmap implies (the rejection path
exists), B2-4 needs no CI change, and the trust-model document is a genuine
gap.
## B2-0 — Alpha.4 out the door, `dev` synced, release hygiene
Done in this order; the tag is already pushed.
1. **Verify the release.** Run `33144028968` completed `success` 2026-08-28
05:23 UTC. `gh release view v1.2.0-alpha.4 --json assets --jq '.assets|length'`
**20**, the same set alpha.3 shipped: server binary + archive + `.sig` +
`checksums.sha256` + source snapshot + signed `server-update-manifest.json`,
plus the AppImage/deb/NSIS client assets and their signatures. The
workflow's own `Verify signed assets against pinned server update key` step
passed inside that run. Record the asset list in HP-2 as the pre-epoch
updater contract.
2. **Sync `dev`.** **Done 2026-08-28 by the owner**#1432 "merge main into
dev after the v1.2.0-alpha.4 release" (`dd7ed091`), the same shape as #1425.
`main` was 5 ahead of `dev` before it. **Known trap, still live:**
squash-merging a sync PR re-diverges `dev` from `main` by content-identical
commits; the next release PR then shows phantom conflicts and is unblocked
by creating `release/<tag>` with `git merge --no-ff main` (how alpha.4
itself was unblocked). Accept the trap; it is cheaper than changing the
merge model mid-phase.
3. **`environment: release`.** In `.github/workflows/release.yml`, add
`environment: release` at job level on the `publish` job (its header is
near line 490 of `release.yml`; the "Generate server update manifest" and
signing steps live inside it). The environment
exists with one reviewer, so the next tag waits for approval. Verify with
`actionlint` (the hygiene job runs it in CI; locally it may be absent on
Windows — CI is the gate).
4. **ENV-03.** Add `export MSYS_NO_PATHCONV=1` near the top of
`Server/scripts/docker-smoke.sh`. Verify: on Git Bash, build and run the
smoke **without** setting the variable in the shell —
`docker build --build-arg VERSION=ci -t owncord-smoke:candidate Server/ && bash Server/scripts/docker-smoke.sh owncord-smoke:candidate`
exits 0. Remember the build context is `Server/`, not the root.
5. **`dev` strict.** Edit `docs/plans/b0-dev-branch-protection.sh` line 63,
`"strict": false,``"strict": true,`, run it
(`bash docs/plans/b0-dev-branch-protection.sh`), and read back:
`gh api repos/J3vb/OwnCord/branches/dev/protection --jq '.required_status_checks.strict'`
`true`. Repository-settings writes need a person; the owner approved this
one on 2026-08-28. Record the read-back in HP-2.
Items 35 are one PR (`chore(b2-0): release hygiene`). Item 2 landed as
#1432. Item 1 is evidence, not a change.
## B2-1 — Capture the epoch-1 fixtures
Lands **before** B2-2. Alpha.4 is the last client on the pre-epoch wire; once
B2-2 merges there is no clean way to record what "epoch 0/1" looked like.
1. **Golden wire transcripts.** A contract test in `Server/ws`
(`protocol_epoch1_contract_test.go`, matching the existing
`*_contract_test.go` tier) drives the required journeys through the
in-process hub harness the package's tests already use (reuse it — do not
write a second harness) and compares each journey's frame sequence with a
JSON file under `protocol/fixtures/epoch-1/`. Journeys: fresh connect
(`auth``auth_ok``ready`); resume with `last_seq` and a replay burst;
`chat_send` and its fan-out; edit and delete; reaction add/remove; typing;
`mark_read`; DM send; `voice_join``voice_state`, `voice_e2ee_announce`
and `voice_e2ee_offer` relay, `voice_leave`; `ping`; failed auth
(`auth_error`). Volatile fields (`id`, `seq`, timestamps, tokens, user ids)
are normalised to stable placeholders before comparison, which is what lets
the fixtures survive refactors. `go test ./ws -run TestEpoch1Fixtures -update`
regenerates; the default run compares.
2. **Client side of the same contract.** `Client/tests/contract/ws-auth-frame.test.ts`
pins the `auth` frame `Client/src/lib/ws.ts:447` sends today (`token`,
`last_seq`, optional `active_channel_id`). B2-2 extends this test rather
than replacing it.
3. **Updater contract.** Before B2-3 changes them, assert the current shapes:
the manifest fields in `Server/updater/release_manifest_test.go`, and the
`client-update` 200/204 bodies in the handler's test next to
`Server/api/client_update.go`.
4. **Retire the reserved entries (S-15).** Remove `voice_speakers` and
`member_leave` from `protocol/schema.json` via the `protocol-change` skill
(`npm run generate`, then `git grep -nE 'VOICE_SPEAKERS|MEMBER_LEAVE|MsgTypeVoiceSpeakers|MsgTypeMemberLeave'`
must return only the generated files' history, i.e. nothing at HEAD), and
delete their rows from `docs/protocol.md`. Own commit, before the fixtures
are captured, so epoch 1 does not carry dead entries.
5. **Document the fixtures** in `protocol/README.md`: what a fixture is, how to
regenerate, and the rule that a fixture may only change with an epoch bump.
Verification: `npm run check:server` (regenerates and diffs both generators;
runs the new test under `go test ./...`), `npm run check:client`.
## B2-2 — Protocol epoch and negotiation
Design fixed 2026-08-28; the numbers and names below are the contract.
1. **One number, generated.** `protocol/schema.json` gains
`"protocol_epoch": 1` beside `"version": 1` (schema format stays 1).
`Server/cmd/genprotocol/main.go` reads it (`ProtocolEpoch int` on the schema
struct at lines 3437) and emits `const ProtocolEpoch = 1` into
`Server/ws/message_types.go` and `export const PROTOCOL_EPOCH = 1` into
`Client/src/lib/protocolTypes.ts`. Follow the `protocol-change` skill; the
drift gate covers both outputs. Own commit.
2. **Handshake, server.** In `Server/ws/serve_auth.go` the auth payload gains
`` Epoch int `json:"epoch"` `` and `` ClientVersion string `json:"client_version"` ``.
Rule, with N = `ProtocolEpoch`: absent → 0; accept when
`max(0, N-2) ≤ epoch ≤ N`; otherwise `buildAuthError`
(`Server/ws/messages.go:368`) with `code: "protocol_epoch_unsupported"`,
`server_epoch`, `min_epoch`, `update_url` (the server's own
`/api/v1/client-update` origin), then close with code **4426**. A client
newer than the server gets the same frame. `ready`
(`Server/ws/serve_ready.go:361`) gains `protocol_epoch`. Table test over
epoch ∈ {absent, N-3, N-2, N-1, N, N+1} in `Server/ws`.
3. **Handshake, client.** `Client/src/lib/ws.ts:447` sends `epoch:
PROTOCOL_EPOCH` and `client_version` (from the app metadata the settings
Logs tab already reads). `AuthPayload`/`AuthErrorPayload` in
`Client/src/lib/types.ts` gain the fields. On
`code === "protocol_epoch_unsupported"` the dispatcher
(`Client/src/lib/dispatcher.ts:293`) shows one plain line naming
`server_epoch` and whether the client or the server is the older one; the
existing non-recoverable path (`ws.ts:306`) already stops reconnecting —
add the unit test that proves no reconnect timer is armed after the frame.
Extend `Client/tests/contract/ws-auth-frame.test.ts` from B2-1.
4. **`GET /api/v1/server-info`.** New unauthenticated handler beside
`Server/api/client_update.go`, returning
`{ "version", "protocol_epoch", "min_client_epoch" }`, rate-limited the way
`client-update` is (`Server/api/constants.go:67`). Handler test; row in
`docs/api.md`. B6 adds the browser-hosting flag here; B8 reads it.
5. **`docs/protocol.md` § Compatibility** (new section) and a pointer in
`protocol/README.md`: within an epoch changes are additive (new optional
fields; new message types the other side may ignore — unknown server→client
types are ignored, unknown client→server types get an `error` frame; both
pinned by tests); a breaking change is a new epoch; the epoch-1 fixtures
replay against the server for as long as epoch 1 is in the window, and a
failing fixture means "bump the epoch", not "fix the fixture". An
**obligations table** with dates, first row: the headerless E2EE key-offer
blob (`docs/protocol.md` ~line 1235 today says "scheduled for removal in the
next release") stays parseable until epoch 0 leaves the window, i.e. until
N = 3. It is client↔client through a relay, so the server's epoch cannot
police it; a written date does.
6. **Scope of "negotiation".** Epoch only. No capability flags at beta; a
client either speaks the epoch or it does not. The roadmap's "protocol
changelog" is the obligations table above plus the `CHANGELOG.md` entry
that ships the epoch.
Four commits (schema+generator; server; client; server-info+docs). Record the
pre-squash SHAs in HP-2 — the fixture commit from B2-1 and the negotiation
commits must be reviewable apart.
## B2-3 — Server-first updates
1. `releaseManifest` (`Server/updater/verify.go:29`) gains
`` ProtocolEpoch int `json:"protocol_epoch,omitempty"` `` and
`` ClientVersion string `json:"client_version,omitempty"` ``. A missing field
is epoch 0.
2. `.github/workflows/release.yml`, step "Generate server update manifest"
(~line 568): write both fields, reading the epoch from
`jq .protocol_epoch protocol/schema.json` so the workflow cannot drift from
the generated constant.
3. `Server/api/client_update.go`: before advertising a release, fetch and
verify its manifest through the updater's existing signature path and
require `manifest.ProtocolEpoch <= ws.ProtocolEpoch`; otherwise fall back
to the newest compatible release; otherwise 204. Tests: newer-epoch
candidate skipped → older compatible advertised → none → 204; unsigned or
tampered manifest → candidate ignored.
4. Docs: `docs/api.md` client-update section (the filter and the manifest
fields); one paragraph "the server upgrades first" in `docs/deployment.md`;
and the next tag line, `v1.2.0-beta.1`, recorded as the heading
`CHANGELOG.md`'s next entry will use and in one sentence in
`docs/contributing.md` (no release-procedure document exists today; do
not create one for this).
## B2-4 — Compatibility matrix
Extend `Server/ws/protocol_epoch1_contract_test.go` from B2-1 into the matrix:
for each client epoch in {absent, N-3, N-2, N-1, N, N+1} connect and, for
accepted epochs, replay every epoch-1 fixture; for rejected ones, assert the
`protocol_epoch_unsupported` frame and close code 4426. It runs under
`go test ./...`, inside the required `Server Build & Test` check — **no new CI
job and no pin-script change.** Exit evidence for BPR-032 and BG-07's server
half.
## B2-5 — One permission predicate per security property
1. **Inventory.** `git grep -nE 'HasPerm|HasAnyPerm|HasServerPerm|EffectivePerms|EffectiveChannelPerms' Server/ -- ':!*_test.go'`
plus every hand-rolled visibility/send check in `Server/ws` and
`Server/service`. Table (file, line, property it decides) goes into the
HP-2 scorecard.
2. **Canonical predicates** in `Server/permissions`: value-taking functions,
no database access — `CanViewChannel`, `CanSendMessage`, `CanType` (defined
as `CanSendMessage`), `CanModerateVoice`, `CanJoinVoice`, `CanAdmitSession`
— each over a small struct of role bits, channel overrides, channel flags,
DM/block state.
3. **Parity, then delegation.** For each predicate, a table test runs the old
call site's logic and the new predicate over the same inputs and asserts
equality; then the site delegates; then the old logic is deleted. One
commit per property.
4. **Closes:** S-01 (typing delegates to `CanSendMessage`), S-12
(ready/refresh/ws delegate to `CanViewChannel`/`CanSendMessage`), and the
server half of SEC-02 (voice moderation delegates to
`CanModerateVoice`, which takes the channel override) — the specifics are
in the local report `docs/security-findings/voice-moderation-channel-overrides/`.
5. **Invariant rule candidate.** If, after migration, no file outside
`Server/permissions` calls the bit helpers directly, add an
`authz-chokepoint` rule to `Server/invariants` that fails on the first new
one. If residual calls remain with a reason, record them in HP-2 and leave
the rule to B3 (roadmap B3 item 15).
## B2-6 — Safe audit coverage
1. Enumerate the security-sensitive mutations: credential and TOTP changes,
role assignment, invite create/revoke, ban/kick/timeout, channel permission
edits, TLS/config changes, API-token create/revoke, plugin install,
account/message deletion. Cross with `git grep -n 'Audit(' Server/ -- ':!*_test.go'`.
2. A table test in the package that owns the handlers calls each mutation with
a fake `db.AuditStore` and asserts an entry with the expected `action`
arrives. Invite create/revoke fail first (S-02); add their `Audit` calls.
3. A second table asserts `detail` never carries a token, password, recovery
secret, or message body — a denylist over the recorded corpus. Fix any hit
at the call site, never by loosening the list.
## B2-7 — Trust model, absence proofs, plugin boundary (documents)
Runs in parallel with B2-1 and B2-6.
1. **`docs/trust-model.md`** (new; BPR-050/051). Sections: what the server can
read and why (text, files, metadata — delivery, search, moderation, backup);
what is end-to-end encrypted (voice, video, screen share; the key-holder
model and identity TOFU from `docs/architecture/voice-e2ee.md`; the server
relays what it cannot read); transport (TLS modes; desktop certificate
pinning; browser: publicly trusted or local-CA, no pinning); the desktop
preview destination policy (C-09) — what the native boundary must own:
resolution, redirects, destinations, time and body limits — stated as a
contract so B7 implements it rather than rediscovers it; at rest (SQLite
is not encrypted; secrets are hashed; desktop secrets in the OS keychain);
what the operator can and cannot do; multi-device sessions; what beta does
not claim. Linked from `docs/security.md`, `docs/deployment.md`,
`docs/quick-start.md`, `docs/README.md`. Exit evidence for BPR-051 is one
non-developer reading it and answering "who can read my messages?"
correctly — record who and when in HP-2.
2. **Absence proofs** (BPR-040/082/083). A contract test beside
`Server/api/client_update.go` walks the mounted router and fails if any
route matches `federat|directory|discover|listing`; a table in
`trust-model.md` lists every outbound host the server contacts and why
(GitHub for updates, LiveKit download, the existing preview/GIF/YouTube
providers on user action) so B6's network capture has a checklist. Same
username on two servers = two unrelated identities is already true; state it.
3. **Plugin boundary** (BPR-080/081, BG-17). New
`docs/architecture/plugins.md`, linked from `docs/architecture/server.md`
and `docs/README.md`: WASM is experimental and disabled by default; no
beta API promise; the post-beta candidate list (GIF/embed providers, slash
commands and automation, webhooks, optional moderation automation with
human authority retained, UI tabs, import/export bridges, observability
exporters); the core list that never moves (authentication, authorization,
TLS, safe fetch, quotas, E2EE, updates, deletion, recovery, moderation
audit). Release-notes wording for beta goes in the same file.
4. **L-08.** B1-6 (#1418) already untracked the prebuilt example WASM,
ignores it in `.gitignore`, and documented the TinyGo build in
`Server/plugin/examples/hello/README.md` (pinned TinyGo 0.40.1; rejects
Go 1.26). What remains is the register's "deterministic source build
passes" gate, which B1-6 found blocked on a second Go SDK. B2-7 decides
one of two: a compile-and-compare job using a second Go SDK, or
re-tagging the gate to B10 with the reason recorded in HP-2. Doing
neither is not an option.
## B2-8 — The B2-tagged findings
Lands **before** B2-2; they touch the same replay/resume files.
| Finding | Area | Public summary |
| ------- | ------------------- | --------------------------------------------------------------------------------------- |
| OC-0311 | Client voice/E2EE | A leave from another readable voice channel can mutate the active call's peer-key state |
| OC-0315 | Client replay | Replay-gate timestamps mix naive-UTC server values with local wall-clock parsing |
| OC-0316 | Server/client E2EE | Resume restores peer public keys but not a room key rotated during the outage |
| OC-0317 | Client DM state | Replay can regress a DM's `lastMessageId` |
| OC-0318 | Server plugins | Install-time and restart-time manifest precedence differ between JSON and TOML |
| OC-0322 | Client connection | TypeScript host validation accepts a hostname form the native proxy rejects |
| OC-0328 | Client unread state | Channel badges lack the message-id replay guard DMs already have |
| OC-0337 | Server replay | Cold-tier voice replay truncation can discard the newest events |
| OC-0338 | Server plugins | TOML manifests can omit configured memory and CPU limits |
Run `bughunt-fix` on exactly these nine (test-first, per-file agents, one
`ci-check` gate, one PR). A finding that turns out to need B7 (client
platform seam) is re-tagged in the issue register with the reason, per the roadmap's
phase execution pattern, not silently skipped.
## B2-9 — Security owners and acceptance tests
The seven local reports in `docs/security-findings/` (gitignored, never
committed; the directory-to-row mapping lives in its local README) and
where each goes:
| Public row | Owner phase | Acceptance test lives | Lands with |
| ---------- | ------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------- |
| S-01 | **B2** | beside the report until B2-5 merges | B2-5 |
| SEC-02 | **B2** (server half) | beside the report until B2-5 merges | B2-5; UI half in B5 |
| C-09 | **B2** (contract) / B7 (client) | beside the report | contract in B2-7 docs; code in B7 |
| SEC-03 | B2 if small, else **B5** | beside the report | B2-9 or B5 item 11 |
| SEC-01 | **B4** | private GitHub advisory (owner creates it) | B4 |
| SEC-04 | **B3/B6** | private GitHub advisory (owner creates it) | B6 |
| OC-0324 | **B4** | beside the report; no advisory — the tracked ledger already carries this finding in full | B4 |
An acceptance test demonstrates the defect, so it is exploit detail: it stays
local until its fix lands, then lands publicly in the same PR. The two
advisories (SEC-01, SEC-04) are created by the owner in the GitHub UI (Security → Advisories →
New draft), not by CLI with the report text; their IDs are recorded in
`docs/security-findings/README.md`, which is local. Public commits, issues and
PR bodies never name the mechanism (`docs/security.md`).
## HP-2 — Protocol and threat-model sign-off
`docs/plans/hp-2-scorecard-<date>.md`, in the HP-1 shape. Questions it must
answer with commands, not assertions:
1. Is the epoch wire frozen? Schema, generated constants, `auth`/`ready`/
`auth_error` shapes, close code — with the pre-squash SHAs of B2-1's
fixture commit and B2-2's negotiation commits.
2. Does downgrade behave? The B2-4 matrix output for every epoch case.
3. Are the trust claims true? `trust-model.md` reviewed by the owner and one
non-developer; every claim traced to a test or a code line.
4. Are the E2EE membership and key-change rules stated and tested? A table of
rules with the test that pins each (identity TOFU, re-pin TOCTOU, rekey on
leave, rotation during outage from OC-0316) — existing tests inventoried,
missing adversarial cases added.
5. Is there one predicate per property? The B2-5 inventory before and after:
direct bit-helper calls outside `Server/permissions` → 0 or a listed
residue with reasons.
6. Are the deferred systems bounded? The absence test, the plugin document,
and the configuration audit (WASM off in fresh, upgraded, Docker and
standalone configurations).
7. Is `dev` strict? The API read-back from B2-0.
The owner signs. Acceptance authorises B3 and claims nothing about beta
readiness.
## Exit gate
The roadmap's seven conditions, plus two:
| # | Condition | Evidence |
| --- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| 1 | Clients from epochs N, N-1, N-2 pass the matrix; N-3 fails safely and actionably; the bundled browser client matches the server epoch | B2-4 output (browser client: n/a until B8) |
| 2 | Protocol and update-metadata changes are generated, documented, and downgrade-tested | B2-2, B2-3, drift gate |
| 3 | Effective-permission and resource-existence sibling cases have parity tests | B2-5 |
| 4 | Voice/video/screen E2EE membership and key-change behaviour pass adversarial tests | HP-2 question 4 |
| 5 | No central identity, directory, federation path, or required external service exists | B2-7 absence test + host table |
| 6 | WASM disabled by default; release artifacts do not imply API stability | B2-7 plugin document + config audit |
| 7 | No unresolved B2 security advisory remains | B2-9 table, B2-owned rows closed |
| 8 | _(added)_ Epoch-1 fixtures were captured before the first protocol change, in a separate commit | B2-1 SHA precedes B2-2's |
| 9 | _(added)_ Pre-squash SHAs recorded for the fixture and negotiation commits | HP-2 question 1 |
## Explicitly out of scope for B2
- The BPR-033 update experience (B7). B2 ships the frame and the "no reconnect
loop" property only.
- External-fetch policy beyond SEC-03's bounded reads (B5).
- Any database, service or domain extraction (B3).
- The client platform seam (`Client/src/platform/`, B7).
- Re-tagging or fixing findings not tagged B2, other than as B2-8 records.
## Traps carried forward
- **Docker:** build context is `Server/`; compare `docker images --format '{{.Size}}'`
(50.1 MB), not `docker image inspect` (12.5 MB); until B2-0 lands, set
`MSYS_NO_PATHCONV=1` yourself.
- **Squash merges hide structure.** Record `refs/pull/<n>/head` SHAs at merge
time for every B2 PR a hold point will review.
- **`check:docs` counts.** `docs/plans/README.md` is watched; never write two
`<n> <status>` pairs on one line there unless they match the ledger.
- **`strict: true` after B2-0:** every open PR needs "Update branch" after
another merges. Keep one PR in flight per hot file.
- **Schema edits go through the `protocol-change` skill**; `make` is not on
PATH on Windows, so use `npm run generate` and `npm run check:server`.
- **Never pass `-c user.email` to git.** The identity is configured.
+114 -12
View File
@@ -3,7 +3,12 @@
**Prepared:** 2026-08-23
**Audited head:** 5cc0888964e26276d1aca145e83270a2c1b9febd on dev
**Release target:** first public beta after the 1.2.0-alpha line
**Status:** proposed implementation sequence; no phase is complete yet
**Status:** B0 and B1 complete (HP-0 accepted 2026-08-25, HP-1 accepted
2026-08-27); B2 in progress from
[b2-protocol-trust-compat-2026-08-28.md](b2-protocol-trust-compat-2026-08-28.md);
B3B10 not started. Amended 2026-08-28 — see the `_(added 2026-08-28)_` lines
in B3B10 and the "Phase execution pattern" section. [README.md](README.md) is
the status authority when this header and a README row disagree.
**Planning model:** quality-gated, with no calendar deadline
Primary inputs:
@@ -43,6 +48,9 @@ change.
## Current evidence snapshot
This is audit evidence, not a release claim. It must be refreshed in B0.
_Superseded 2026-08-25 by
[b0-baseline-2026-08-25.md](b0-baseline-2026-08-25.md), which holds the
measured numbers. The table below is kept for the audit trail._
| Area | Evidence at the audited head | Consequence |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
@@ -161,6 +169,29 @@ An item is done only when:
- tracker, requirement map, and phase scorecard agree;
- rollback, compatibility, and data-migration notes exist where applicable.
## Phase execution pattern
_Added 2026-08-28._ What B0 and B1 proved, written once so later phases do not
rediscover it.
1. Each phase gets an execution plan, `docs/plans/bN-<slug>-<date>.md`, at its
start, and a scorecard, `docs/plans/hp-N-scorecard-<date>.md`, at its hold
point. The plan re-verifies this roadmap's claims against HEAD before
implementing anything — B1 refuted several audit findings on contact — and
records each verdict. [README.md](README.md) rows are the status authority;
a plan header that disagrees with its README row is stale.
2. A phase cannot exit while any `OC-*` finding tagged to it in the
[issue register](repo-health-issue-register-2026-08-23.md) is open, unless
the finding is re-tagged to a later phase with a written reason in the
scorecard. The tags exist today; this rule is what enforces them, and it is
what makes "zero open P0/P1 at B10" a measured fact rather than a hope.
3. Record pre-squash pull-request SHAs at merge time whenever a hold point
reviews commit structure. `dev` is squash-only, so a pure-move/rewrite or
fixture/behaviour split survives only on `refs/pull/<n>/head`
([hp-1-scorecard-2026-08-27.md](hp-1-scorecard-2026-08-27.md)).
4. `dev` requires branches to be up to date before merging (`strict: true`,
applied in B2-0). One pull request per plan step, branched from `dev`.
## B0 — Restore truth, freeze scope, and reconcile the audit
**Objective:** replace stale or contradictory status with one reproducible
@@ -326,6 +357,9 @@ server services and clients must obey.
**Primary requirements:** BPR-031, BPR-032, BPR-040, BPR-050, BPR-051, and
BPR-080 through BPR-083.
**Execution plan** _(added 2026-08-28)_:
[b2-protocol-trust-compat-2026-08-28.md](b2-protocol-trust-compat-2026-08-28.md).
### Entry gate
- B1 is complete and protocol source has one owner.
@@ -435,6 +469,21 @@ for every later requirement.
11. Establish performance baselines for permission invalidation, read-state
writes, broadcast/replay, database waits, reconnect storms, and upload
admission.
12. _(added 2026-08-28)_ Build the alpha-shaped test dataset: a deterministic
alpha-shape profile in `Server/cmd/seed` plus one anonymised
`v1.2.0-alpha.4` database snapshot at a documented path. B4's HP-4 drills,
B6's upgrade rehearsal and B10's in-place upgrade all consume it; until B3
builds it, nobody owns it.
13. _(added 2026-08-28)_ Workstreams 2 and 3 are the design already written in
[bug-detection-improvements.md](bug-detection-improvements.md), Tiers 3a,
3b and 3c. Execute that design; do not write another.
14. _(added 2026-08-28)_ Workstream 1's coverage floor starts at B0's measured
74.6% CI aggregate and ratchets from there.
15. _(added 2026-08-28)_ Adopt an `authz-chokepoint` rule in
`Server/invariants` only if B2-5's predicate inventory shows a mechanical
shape. The rule was dropped once for lack of evidence; B2-5 produces it.
16. _(added 2026-08-28)_ Run the Docker smoke nightly on `dev`. The CI job is
`main`-gated, so `dev` never proves the image.
### Hold point HP-3 — First vertical-slice review
@@ -507,6 +556,13 @@ retained, and erasable without central services or misleading privacy claims.
policies and attachment cleanup.
12. Keep diagnostics local, make support-bundle export user initiated, and
prevent automatic product or usage telemetry.
13. _(added 2026-08-28)_ HP-4's destructive drills run against the B3
alpha-shaped dataset, not ad-hoc copies.
14. _(added 2026-08-28)_ OC-0321 is a must-close: a TOTP key-file read error is
treated as absence and the key is replaced. Data-loss class.
15. _(added 2026-08-28)_ Workstream 7 is smaller than written: per-session
listing and individual revocation already exist server-side. B4 adds the
new-login event and sign-out-everywhere only.
### Hold point HP-4 — Irreversible-data review
@@ -586,6 +642,9 @@ operation before building their full cross-client experience.
rotation, and stale-subscription cleanup. There is no OwnCord relay.
10. Keep automation optional. Human moderation authority and audit remain core
even if automation becomes a post-beta plugin candidate.
11. _(added 2026-08-28)_ SEC-03 (bounded per-response and aggregate
preview/media reads) is first in line unless B2-9 already landed it. It is
P1 and confirmed.
### Hold point HP-5 — Abuse and privacy review
@@ -670,6 +729,21 @@ service.
SBOM, provenance, checksums, and source-snapshot evidence.
12. Document local logs, support-bundle generation, capacity limits, ports,
storage growth, certificate trust, recovery, updates, and safe failure.
13. _(added 2026-08-28)_ Must-close before any ARM64 server asset ships:
OC-0320 (self-update selects linux-amd64 regardless of architecture),
OC-0332 (a bare IPv6 address breaks the updater URL), OC-0344 (the
HTTP→HTTPS redirect assumes port 443) and OC-0339 (an empty configuration
section is reported as unknown).
14. _(added 2026-08-28)_ The 250/100/25 run starts from
`Server/scripts/k6/ws-load.js`. k6 cannot drive voice; a 25-participant
LiveKit load harness is a real gap and needs a named owner before HP-6.
15. _(added 2026-08-28)_ R-09: the exact-SHA gate already runs at tag time
(the `gate-evidence` job in `release.yml`, B1-7); `environment: release`
lands in B2-0. B6 rehearses one tag against both before HP-6.
16. _(added 2026-08-28)_ Add the browser-hosting flag to
`GET /api/v1/server-info` (defined in B2-2) alongside workstream 6's
default-off hosting switch, so one endpoint answers "what is this
server, and is the browser client on".
Public IP certificates are feasible only for eligible stable public addresses
and currently require short-lived certificate handling. Private or reserved IP
@@ -758,6 +832,14 @@ without regressing the current application.
disclosure, and local support-bundle export.
12. Build and smoke Windows x64/ARM64 and Linux x64/ARM64 desktop artifacts,
using native Windows ARM64 evidence and declared Linux ARM64 evidence.
13. _(added 2026-08-28)_ Delete the two Rust commands nothing invokes,
`probe_credential_store` and `ptt_get_key`
([platform-contracts.md](../architecture/platform-contracts.md)).
14. _(added 2026-08-28)_ Workstream 8 consumes B2's
`protocol_epoch_unsupported` frame and `GET /api/v1/server-info`; it
defines no new contract.
15. _(added 2026-08-28)_ Re-run Stryker (C-16) before workstream 6 decomposes
modules, so the mutation baseline is honest.
### Hold point HP-7 — Desktop parity before browser behavior
@@ -835,6 +917,13 @@ behavior.
real supported browsers/devices for release qualification.
10. Preserve one active server connection and per-server account/profile
isolation.
11. _(added 2026-08-28)_ No certificate pinning in a browser. Workstream 7's
LAN/offline path uses publicly trusted or local-CA certificates only —
never a TOFU shim ([platform-contracts.md](../architecture/platform-contracts.md),
hard case one).
12. _(added 2026-08-28)_ The browser client reads `GET /api/v1/server-info`
(B2-2, flag from B6) for the hosting flag and epoch; no separate
discovery endpoint.
Service workers, camera/microphone, Web Push, and screen capture require secure
contexts in normal browser use. The plan follows the relevant
@@ -1004,6 +1093,11 @@ re-verifies every BPR.
accessibility, support, feedback, and contribution documentation.
12. Prepare safe release notes and coordinated fixed-vulnerability disclosure.
Do not publish private exploit material by default.
13. _(added 2026-08-28)_ `ci.yml` cancels in-progress runs on every ref,
`main` included, so item 2's thirty consecutive runs cannot be counted as
written. Exempt `main` from `cancel-in-progress` before the count starts.
14. _(added 2026-08-28)_ The 14-day soak runs on named hosts: the owner's LAN
self-hosted instance and one Docker instance.
### Hold point HP-10 — Human go/no-go
@@ -1103,17 +1197,25 @@ Every hold point and phase exit records at least:
The actual values and links belong in phase evidence, not as optimistic edits
to this plan.
## First implementation slice
## Current implementation slice
Start with B0 only:
_Updated 2026-08-28._ B0 and B1 are complete (HP-0 accepted 2026-08-25, HP-1
accepted 2026-08-27). The active slice is B2, executed from
[b2-protocol-trust-compat-2026-08-28.md](b2-protocol-trust-compat-2026-08-28.md)
in this order:
1. repair the two failing Vitest contracts;
2. make Playwright terminate reliably;
3. collect matched server lint and Docker evidence;
4. finish issue/security/requirement reconciliation;
5. run the exact-SHA full matrix and publish the non-sensitive baseline
scorecard.
1. B2-0 — alpha.4 verified, `dev` synced with `main`, release hygiene, `dev`
set to `strict: true`;
2. B2-1 — epoch-1 fixtures captured **before** any protocol change;
3. B2-8 — the B2-tagged findings, because they touch the same replay/resume
files as B2-2;
4. B2-2 → B2-3 → B2-4, serialized;
5. B2-6, B2-7 and B2-9 in parallel where the plan allows; B2-5 is serialized.
Then execute the B1 structural work as isolated changes. Do not begin server
feature implementation, client architecture extraction, or browser work before
those two gates close.
Do not begin B3 domain extraction, client platform extraction, or browser work
before HP-2 closes.
The original 2026-08-23 slice (B0 only, then B1 as isolated changes) was
executed as written; see
[b0-baseline-2026-08-25.md](b0-baseline-2026-08-25.md) and
[b1-repository-foundation-2026-08-25.md](b1-repository-foundation-2026-08-25.md).