From b3f39419e9ca56d3eaa8528694f003b1648c67e4 Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:36:40 +0200 Subject: [PATCH] docs: B2 execution plan and the 2026-08-28 roadmap amendments (#1433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * docs(plans): do not point readers at a public repro from the B2-9 table --------- Co-authored-by: Claude Fable 5 --- docs/plans/README.md | 23 +- .../b2-protocol-trust-compat-2026-08-28.md | 455 ++++++++++++++++++ docs/plans/repo-health-roadmap-2026-08-23.md | 126 ++++- 3 files changed, 581 insertions(+), 23 deletions(-) create mode 100644 docs/plans/b2-protocol-trust-compat-2026-08-28.md diff --git a/docs/plans/README.md b/docs/plans/README.md index 53a3e2fa..ad62e58e 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -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, B0–B10. **B0 and B1 complete** (HP-0 and HP-1 both accepted); B2 next. B3–B10 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 1–6 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, B0–B10. **B0 and B1 complete** (HP-0 and HP-1 both accepted); **B2 in progress** — plan below. B3–B10 not started. **Amended 2026-08-28:** dated lines in B3–B10, 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 1–6 done 2026-08-20; **phase 7 pending**. Its header still reads "in progress 2026-08-19" — stale; the phase table is correct. | ## Partially implemented diff --git a/docs/plans/b2-protocol-trust-compat-2026-08-28.md b/docs/plans/b2-protocol-trust-compat-2026-08-28.md new file mode 100644 index 00000000..fdb29d63 --- /dev/null +++ b/docs/plans/b2-protocol-trust-compat-2026-08-28.md @@ -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, 080–083 + +## 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 | 1–2 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/` 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 3–5 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 34–37) 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-.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//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 + ` ` 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. diff --git a/docs/plans/repo-health-roadmap-2026-08-23.md b/docs/plans/repo-health-roadmap-2026-08-23.md index 19e82317..147ffb6c 100644 --- a/docs/plans/repo-health-roadmap-2026-08-23.md +++ b/docs/plans/repo-health-roadmap-2026-08-23.md @@ -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); +B3–B10 not started. Amended 2026-08-28 — see the `_(added 2026-08-28)_` lines +in B3–B10 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--.md`, at its + start, and a scorecard, `docs/plans/hp-N-scorecard-.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//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).