Commit Graph
24 Commits
Author SHA1 Message Date
J3vbandClaude Fable 5 83d924c10a docs(audit): record D13 closure of A-2026-07-16
Design note (permission-middleware-consolidation.md, status implemented),
closure-table + §3 rows for A-2026-07-16, the A-2026-07-07 amendment
recording the missed fifth site, the D13 decision row, and the settled
two-scope authorization contract in architecture/server.md. Backlog row
12 stays untouched: the auth-route sweep is deferred to a future D14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:45:42 +02:00
J3vbandClaude Opus 4.8 cac23c763f docs(security): add continuation plan for the 2026-07-22 security-scan remediation
Handoff doc: F1/F2/F5/F7 and F4/F8 committed, F6 done but riding with the permission-consolidation WIP, and the full F3 (voice E2EE identity keys + TOFU) design + PR split for the remaining work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:57:15 +02:00
J3vbandClaude Fable 5 a11dd7eab5 docs: record per-operator GIF key decision and surface setup
GIFs are off by default and each operator supplies their own Klipy key,
but nothing user-facing said so — README and quick-start had zero mentions,
so a fresh self-hoster had no way to learn the feature exists.

Records the decision as D12 with the rejected alternatives, so the
trade-off is not silently revisited later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:27:47 +02:00
J3vb 52659e4a39 Merge pull request #1200 from J3vb/feat/narrow-tauri-capabilities
feat(client): narrow Tauri HTTP capability scope
2026-07-20 16:25:26 +02:00
J3vb 59ec405595 docs: record final Tauri capability posture and residual risk
Update the security doc's capability section with the deny list, the fact
that `http:allow-fetch` is the only URL-scoped HTTP identifier, and why
the https wildcard cannot be removed without moving the link-preview
fetch into Rust. Note under Known Limitations that narrowing the plugin
scope alone does not bound exfiltration while CSP `connect-src` allows
`https:` to any host. Add a capability row to the client architecture
doc and mark the design note implemented.
2026-07-20 14:51:23 +02:00
J3vb 2511af345e docs: design note for narrowing Tauri HTTP capabilities
Investigates whether the https wildcard on the three http:allow-fetch*
identifiers can be enumerated now that the HTTP TOFU proxy has landed.

Two findings change the answer:

- Only http:allow-fetch is URL-scoped. tauri-plugin-http validates the URL
  in the `fetch` command only; `fetch_send` and `fetch_read_body` take a
  ResourceId and never consult a scope, and Tauri's ACL resolver keeps a
  command-declaring permission's scope as command scope. The allow blocks
  on the other two identifiers are inert.
- The host set is not enumerable: api.ts, profiles.ts and attachments.ts
  are loopback-only (attachment URLs are always server-generated
  /api/v1/files/<id>) and media.ts hits one fixed YouTube oEmbed URL, but
  embeds.ts fetches arbitrary user-posted URLs by design.

Decision: keep the https wildcard with a loopback deny list, drop the two
inert scope blocks, and record the Rust-side link-preview command as the
follow-up that would actually make the set enumerable. Residual risk
(CSP connect-src already allows https:) stated explicitly.
2026-07-20 14:45:34 +02:00
J3vb 605f97051a docs(audit): correct CRITICAL #4 closure — Dispatch has a live hub caller
The closure rationale for audit finding #4 claimed in five places that
nothing in the server calls EventSink.Dispatch. That is disprovable by
grep: ws/hub.go:1034 calls Dispatch on every broadcast message, and
api/router.go:134-139 wires h.pluginSink whenever plugins are enabled.
The call site is pre-existing on main, not introduced by this branch.

Restate the closure on the claim the evidence actually supports:
Dispatch has exactly one caller outside the plugin package's tests
(ws/hub.go, on the hub's broadcast goroutine under seqMu), but its loop
body invokes no guest code and no production code calls Subscribe, so
the subscriber set is always empty and no guest code executes on the
event path. Finding #4 stays closed; the reason changes.

Also warn on Subscribe that adding the first production caller turns
Dispatch's loop live on the hub's hot path, and note in the SECURITY
GATE that the call site already exists so wiring delivery is not a new
integration.

Corrected in: plugin/host_events.go (Dispatch + Subscribe comments),
plugin/audit_closure_test.go, docs/audit-2026-04-07.md (row 4 and the
structural-mitigation paragraph), docs/audit-2026-07-19.md §1 row,
docs/plans/audit-2026-07-19-decisions.md D11.

Comments and docs only — no behaviour change.
2026-07-20 14:36:24 +02:00
J3vb 4ab01c39df docs(audit): close plugin CRITICALs 1-4, accept #5 as residual risk
P3 item 4. Each of the five plugin CRITICALs in audit-2026-04-07.md was
re-verified against the current Server/plugin/ code rather than the tracker:

- #1 invokeCommand timeout — CLOSED. Per-call CPU budget (manifest →
  config → 100ms floor) + WithCloseOnContextDone + lazy re-instantiation.
  Landed in PR #1182 (7b178ff, b13adf2); pinned by the W1-1 test.
- #2 storage key isolation — CLOSED. The premise did not hold: the namespace
  is the caller's Instance.ID and plugin_kv PRIMARY KEY (plugin_id, key).
- #3 per-command ACL — CLOSED by the manifest `commands` ACL in 3d2dd19.
- #4 event rate limit — CLOSED as not reachable: EventSink.Dispatch invokes
  no guest code and has zero callers; the requirement is recorded as a
  SECURITY GATE at the point delivery would be wired.
- #5 HTTP exfiltration — OPEN, accepted residual risk. An allowlisted host is
  by definition a permitted destination; closing it needs egress content
  policy and per-plugin allowlists, i.e. a runtime redesign, out of scope
  for P3.

Because #5 stays open the standing rule fires as written: plugins ship
default-disabled at the beta gate. Re-verified in config.DefaultConfig() —
Plugins.Enabled false, HTTPAllowlist empty. Also records the structural
mitigation covering #2/#4/#5: no host imports are wired into the wazero
runtime, so command_dispatch and list_commands are the only guest-reachable
entry points today.

Mirrors the outcome in the §1 carried-over row of audit-2026-07-19.md,
records decision D11 in plans/audit-2026-07-19-decisions.md, and notes in
plans/slash-commands.md which slice of its manifest design already landed.
2026-07-20 14:10:15 +02:00
J3vbandClaude Fable 5 3e42c4a4f0 docs: close audit items 3 and 11 (channel-visibility + V2 dispatch)
- audit-2026-07-19.md: A-2026-07-07 and A-2026-07-09 → RESOLVED
  2026-07-20 in the findings tables; §6 backlog rows 3 and 11 struck
  through as DONE (D9/D10)
- plans/audit-2026-07-19-decisions.md: D9/D10 status → Implemented
- plans/channel-visibility-unification.md, v2-dispatch-migration.md:
  status → implemented; v2 note records the applier-trigger shape the
  voice handlers actually landed with
- architecture/server.md: WS box "V1+V2 dispatch" → "typed command
  dispatch"
- architecture/websocket.md: intro + §D4c redrawn as the single typed
  path (no V1 fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:44:32 +02:00
J3vbandClaude Fable 5 215a194325 docs: design notes for audit backlog 3 (channel-visibility) and 11 (V2 dispatch)
Greenlight backlog items 3 (A-2026-07-07) and 11 (A-2026-07-09) for
implementation. One design note each in docs/plans/ matching the existing
per-decision format (problem, approach, files touched, test plan, non-goals),
plus D9/D10 rows in the maintainer decisions doc dated 2026-07-20.

Design-only step of the audit-backlog PR; no code changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:08:52 +02:00
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).

SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:

  - service.Store   (service/datastore.go, renamed from store/store.go)
  - ws.EventStore   (ws/eventstore.go)
  - plugin.PluginStore (plugin/pluginstore.go)

The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.

Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.

Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:33:58 +00:00
Claude 071426c0d8 feat(server,client): announcement channels (D1, closes A-2026-07-01)
Make 'announcement' a real channel type, resolving the contradiction where
it was documented and offered by the admin API but hard-rejected by the
migration-013 DB triggers.

Model: announcement channels are readable like text channels (same
READ_MESSAGES visibility), but posting is restricted to users with
MANAGE_MESSAGES — no new permission bit, migration, or client permission
plumbing needed.

Server:
- migrations/016: recreate the channel-type triggers to allow
  text/voice/announcement/dm.
- service/message.go: checkSendPermission now takes the channel type and
  rejects posts to announcement channels from users lacking MANAGE_MESSAGES
  (SendMessage + CanPost paths). Added a service test.
- Unread counts: ready-payload builder (ws/serve.go) and
  GetChannelUnreadCounts (db) now include announcement channels alongside
  text, so they track unread/last-message like text channels.

Client:
- ChannelSidebar renders announcement channels with a megaphone icon
  (added to the icon set) instead of the '#' text prefix; they otherwise
  behave like text channels (already typed in ChannelType).

Specs + trackers (api.md, protocol.md, schema.md incl. migration 016,
architecture/data-model.md, audit A-2026-07-01, decisions D1) updated.

Verified: go build ./...; go test ./service ./db ./ws ./api ./admin;
sqlc-verify; client tsc + oxlint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:00:18 +00:00
Claude f66d6c5274 refactor(server/db): delegate messages + reactions to dbgen; finalize D2
messages/reactions: CreateMessage, GetMessage (messageFromGen mapper),
EditMessage (EditMessageContent), DeleteMessage (SoftDeleteMessage),
AddReaction, RemoveReaction, GetReactions (GetReactionCounts),
SetMessagePinned, UpdateReadState. Retired the obsolete scanMessage.

Kept raw by design (no clean sqlc mapping): FTS search, cursor-paginated
GetMessages/GetMessagesForAPI/GetPinnedMessages, getReactionsBatch,
GetChannelUnreadCounts, GetLatestMessageID (interface{} MAX result).

D2 status: 97 db.DB methods now delegate to dbgen across every domain;
43 raw d.sqlDB calls remain by design (db.go passthroughs, migrate.go,
variable-length IN(), FTS, multi-statement transactions, PRAGMA/VACUUM).
sqlc is no longer dead code — audit A-2026-07-05 resolved. Full rationale
+ the kept-raw list in docs/plans/sqlc-adoption.md.

Verified: go build ./...; go test ./db ./service ./ws ./auth; sqlc-verify;
gofmt + go vet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:48:39 +00:00
Claude 44323373e3 refactor(server/db): adopt sqlc as the query layer — phase 1 (D2)
Wire the sqlc-generated dbgen package into db.DB so it stops being dead
code (audit A-2026-07-05) and becomes the real, CI-verified query layer.

db.DB now holds a *dbgen.Queries (initialized in Open via dbgen.New).
Query method bodies delegate to it; sqlc owns the SQL text and parameter
binding (make sqlc-verify), while db keeps its stable public API and
domain model types so no caller in api/admin/ws/service changes. The
migration is incremental — a method either delegates to d.q.* or still
runs raw SQL — so both layers are correct during the transition.

Converted domains (now load-bearing through sqlc):
- blocks: BlockUser, UnblockUser, IsBlocked, IsEitherBlocked,
  ListBlockedUsers (added the query to blocks.sql + regenerated).
  Empty ListBlockedUsers now returns []int64{} instead of nil, matching
  the MemStore backend — a latent inconsistency fixed, not a regression.
- lockouts: UpsertLockout, LoadActiveLockouts, CleanupExpiredLockouts,
  DeleteLockout (RFC3339 time formatting/parsing kept in the wrappers).
- roles: GetRoleByID, ListRoles, GetRoleForUser via a shared roleFromGen
  mapper (int64 position/is_default -> int/bool). GetUserWithRole stays
  raw for now.

Remaining domains stay on raw SQL and are tracked in
docs/plans/sqlc-adoption.md; store/ event+plugin SQL is intentionally
excluded (that layer is removed in D3). Decisions doc + audit closure
updated (A-2026-07-05 -> in progress).

Verified: go build ./...; go test -race ./db ./service ./auth ./ws (api
green non-race, race run matches CI's -timeout 20m); make sqlc-verify and
protocol-verify pass with the regenerated output committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:01:54 +00:00
Claude dab4d73e09 feat(client): TOFU HTTP proxy for REST — close audit A-2026-07-02 (D5)
The REST path previously used tauri-plugin-http with
danger.acceptInvalidCerts, so it accepted ANY certificate while the WS
and LiveKit paths were TOFU-pinned in Rust — and the bearer token rides
every REST request. This routes REST through a new Rust loopback
TCP->TLS proxy that pins the server certificate to the same
trust-on-first-use fingerprint as the WS proxy.

Rust (src-tauri):
- New http_proxy.rs: per-host loopback tunnels (HttpProxyState map);
  per-connection TOFU via CaptureVerifier + tofu_check, sharing
  ws_proxy's cert store (cert_store_key) and emitting the same
  cert-tofu events (first-use banner / mismatch modal). First request's
  Host is rewritten and Connection: close injected so one request rides
  each connection. Mismatch returns a clean 502 to the loopback fetch.
- Register HttpProxyState + start_http_proxy/stop_http_proxy in lib.rs.
- Drop the dangerous-settings feature from tauri-plugin-http.

TypeScript (src):
- New lib/httpProxy.ts: ensureHttpProxy(host) (per-host cache +
  concurrent-start dedup) / stopHttpProxy(host).
- api.ts, profiles.ts (health), attachments.ts (image + download) resolve
  server URLs to http://127.0.0.1:{port}; remove the allowSelfSigned
  config field and every acceptInvalidCerts block. External hosts (CDNs,
  OG previews, YouTube) keep normal TLS validation.
- main.ts constructs the API client without allowSelfSigned.
- capabilities/default.json: allow http://127.0.0.1:* fetch scope.

Tests:
- New tests/unit/http-proxy.test.ts (cache, dedup, stop/restart).
- api.test.ts and attachments-render.test.ts: mock httpProxy, replace the
  acceptInvalidCerts assertions with proxy-origin assertions.

Verified: tsc --noEmit clean; new + affected vitest suites green
(176 tests); the http_proxy pure logic (host validation, header rewrite)
passes as standalone Rust unit tests; oxlint/eslint counts unchanged
from HEAD; prettier clean. The full Tauri build (cargo) requires GUI
system libs not present in this environment and runs on CI/real runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 14:26:25 +00:00
Claude 8c768bf4c6 docs: add design note for the client HTTP TOFU proxy (D5)
Pins the implementation approach for closing audit finding A-2026-07-02:
a loopback TCP-to-TLS tunnel reusing the livekit_proxy pattern and the
shared per-host fingerprint store, with ws_proxy-equivalent TOFU
first-trust/mismatch flows (required because the first TLS contact with
a server is the login HTTP request), per-host tunnel lifecycle for
multi-profile health polling, and removal of the acceptInvalidCerts
path plus the dangerous-settings feature flag as the ratchet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:58:58 +00:00
Claude bf508c9e6b chore(client): remove abandoned SolidJS beachhead (D6)
The Solid.js migration was abandoned (per CHANGELOG); the 154-LOC
beachhead and its scaffolding remained in-tree, leaving two UI paradigms
for contributors. Removed:

- src/components/solid/ (Badge, ChannelListItem, PluginContainer — none
  imported by production code)
- src/lib/solidMount.ts and src/lib/solidAdapter.ts
- tests/setup-solid.ts and tests/setup-solid.test.tsx
- vite-plugin-solid from vite.config.ts and vitest.config.ts (and the
  now-unneeded tsx test include + setupFiles)
- jsx/jsxImportSource from tsconfig.json
- solid-js, @solidjs/testing-library, vite-plugin-solid from package.json

docs/client-architecture.md (which described the SolidJS design) is
retired to a pointer at docs/architecture/client.md; README links
updated. Audit A-2026-07-12 and decision D6 marked closed.

Verified: tsc --noEmit clean (previous 3 test-file errors were caused by
the Solid jsx config and are gone); oxlint/eslint error counts identical
to HEAD (pre-existing); vitest runner healthy on a sample suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:57:28 +00:00
Claude ca3b0fee3e docs: refresh api.md, protocol.md, schema.md against current code (D7)
One-PR spec refresh per decision D7, using the 2026-07-19 audit's
conformance matrix as the checklist:

api.md
- Remove the deleted version field from /health and /api/v1/info
  (anti-fingerprinting C-2) and document the removal.
- Document the profile surface (PATCH /users/me, PUT /users/me/password,
  GET/DELETE /users/me/sessions), the user-blocks surface
  (GET/PUT/DELETE /api/v1/blocks), and the plugin admin surface
  (/api/v1/admin/plugins).
- Correct GET /api/v1/files/{id}: auth is required and caching is
  'private, no-cache' (was documented as public/immutable, unauthenticated).
- Add search (30/min) and upload (10/min) rate limits; note announcement
  channel type as planned-only.

protocol.md
- Document auth_ok.replay_source and the 3-tier reconnect replay
  (ring buffer -> events table -> full resync) with the visibility
  watermark.
- Add the Voice End-to-End Encryption section (voice_e2ee_announce/offer
  in both directions, key-holder semantics, rate limits) and
  voice_token.is_key_holder.
- Document user_update; mark voice_speakers and member_leave as defined
  but not currently emitted; extend voice_config fields.
- Update the reference tables (19 client->server / 30 server->client)
  and point them at protocol-schema.json as the generated inventory.

schema.md
- Correct the migration history to the real 001-015 numbering.
- Document the previously missing tables: login_attempts, settings,
  emoji, sounds (dead schema), rate_lockouts, user_blocks, events,
  plugins, plugin_kv; add attachments.uploader_id and new indexes.
- Fix the channel-type list to text/voice/dm (013 triggers) and correct
  the permission formula to (base & ~deny) | allow to match
  permissions.EffectivePerms.
- Note the sqlc/dbgen layer and link the architecture data-model doc.

Also correct the audit_log_v6 claim (transient rename inside migration
003, not a coexisting table) in the audit and data-model blueprint, and
update the audit/decisions trackers (A-2026-07-03 closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:51:44 +00:00
Claude 2e7a80171b feat(server,client): protocol codegen + audit quick-wins batch
Protocol codegen (decision D4, audit A-2026-07-08):
- Add docs/protocol-schema.json as the real single source of truth for
  WS message-type constants, making the long-standing 'generated from'
  comment in both constant files true.
- Add Server/scripts/genprotocol, a generator emitting both
  Server/ws/message_types.go and Client .../lib/protocolTypes.ts
  (constants byte-for-byte value-identical to before; only headers,
  ordering alignment, and provenance comments changed).
- Add make protocol-generate / protocol-verify and wire protocol-verify
  into CI next to sqlc-verify.

Quick wins (decision D8):
- admin: log LogAudit write failures in the backup handlers instead of
  discarding them (prior audit #10).
- api: fix self-contradictory upload Cache-Control to 'private,
  no-cache' per remediation plan W3-4; drop the now-unused
  fileCacheMaxAgeSeconds constant; update test.
- ws: route the hub settings cache through db.GetSetting instead of
  inline SQL.
- ws: fix a latent data race — main.go wires SetEventPersister and
  SetEventStore after NewRouter has already started the hub Run loop,
  which reads those fields on the broadcast/replay paths. They (and
  pluginSink, which one test sets post-Run) are now atomic pointers;
  the remaining pre-Run-only setters reject late calls with an error
  log instead of racing silently.

Update the audit closure table and decisions doc statuses accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:32:58 +00:00
Claude fde254cea0 docs: record maintainer decisions for the 2026-07-19 audit
Add docs/plans/audit-2026-07-19-decisions.md capturing the eight decisions
taken on the audit's open questions (announcement channels: implement;
data layer: adopt sqlc + remove store/; protocol constants: real codegen;
client HTTP TOFU pinning: next security work; Solid beachhead: delete;
specs: one refresh PR after codegen) with sequencing, and mark the greenlit
items (protocol codegen + quick-wins batch).

Update the closure table in docs/audit-2026-07-19.md from OPEN to DECIDED
for the seven findings these decisions cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 12:51:06 +00:00
J3vb d0da33f1da Merge remote-tracking branch 'origin/main' into fix/security-hardening-review 2026-07-19 08:09:56 +02:00
J3vbandClaude Fable 5 b4c34d2bc6 chore(repo): P0 hygiene for the alpha reset
- gitignore .serena/ and Client/tauri-client/.env; untrack the .env
  (the KLIPY key it held is treated as burned; rotation + server-side
  proxy tracked for P3)
- CI: verify generated sqlc output (make sqlc-verify) on the ubuntu leg
- CHANGELOG: honest reset narrative (v1.1.0-alpha series), remove
  references to deleted roadmap files
- delete stale docs/phase-a-status.md; fix dangling ref in
  docs/plans/slash-commands.md
- add root SECURITY.md (GitHub-surfaced policy; reporting works while
  the source repo is private)
- docs/audit-2026-04-07.md: add maintained finding-closure table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:17:29 +02:00
J3vbandClaude Opus 4.8 21816f52e7 docs(plans): remediation plan for security-hardening review regressions
Adds docs/plans/security-hardening-remediation.md capturing the code-review
findings against fix/security-hardening-review and sequencing the fixes into
three waves (availability/backend-breaking, behavioral regressions, cleanup).
Each item names root cause, right-altitude fix, files, and verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:55:24 +02:00
Claude d9dc415436 docs(plans): slash command dispatcher design (phase D parity #1)
First of two phase-D parity plans. Grounded in the existing V2 command
dispatcher (Server/ws/command.go) and the dormant
plugin.Registry.DispatchCommand + host_commands.go — this is not a
green-field design, it's a wiring plan for code that already exists.

Covers:
- Wire format: command_invoke, command_autocomplete, command_reply,
  command_autocomplete_result
- Manifest extension: commands[] with option types, default_member_permissions,
  contexts, autocomplete flag
- Schema: migrations/016_plugin_commands.sql with a unique name index
  so two plugins can't both own /ban
- Code surface: enumerated file-by-file touch list
- Permission model: server-enforces default_member_permissions BEFORE
  the plugin is invoked, plugins never get to gate their own commands
- Built-in commands: /me + /shrug ship in-tree as reference handlers
- Concurrency: 3s deadline via context.WithTimeout passed to DispatchCommand
- Failure modes & UX: 6-row table from "unknown command" through panic
  auto-disable
- Testing: unit + integration + contract round trip
- Telemetry: 3 new counters + OTel span
- 4-stage rollout, each step independently shippable
- Open questions: bot identity for broadcasts, component v2 reservation,
  cross-plugin imports, DM-context handling

Plan #8 (E2EE DMs + DAVE voice) and PHASE_D_PARITY_TODO.md items 2-7
to follow in a subsequent commit.

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 13:38:48 +00:00