* docs(b2-7): trust model — who can read what (BPR-050/051, C-09 contract)
One document states the operator trust model in plain language and traces
every claim to a code line or test: server-readable text and files and why,
E2EE voice/video/screen with the key-holder and TOFU rules, transport per TLS
mode with desktop pinning and the browser rule, the C-09 preview destination
contract B7 implements, at-rest storage, operator can/cannot, multi-device
sessions, and what beta does not claim. Linked from security.md,
deployment.md, quick-start.md and docs/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* test(b2-7): absence proofs — no federation/directory routes, outbound host table
TestAbsenceContract_NoFederationDirectoryOrListingRoutes builds the production
router with uploads, voice and GIF on, walks the whole mounted tree with
chi.Walk and fails on any route matching federat|directory|discover|listing.
A floor on the route count and a check that the admin subtree was traversed
keep it from passing vacuously. trust-model.md gains "What OwnCord does not
have" (BPR-040/082/083) and the outbound-host table B6's network capture
checks against.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* docs(b2-7): plugin boundary — off twice, compiled out of releases, no API promise
docs/architecture/plugins.md records the experimental WASM boundary (BPR-080/
081, BG-17): disabled by build tag and by config, absent from release.yml and
Dockerfile builds, the HP-2 configuration audit (fresh, upgraded, Docker,
standalone), the beta release-notes wording, what exists today with its limits
and tests, the post-beta plugin candidates that stay in core during beta, and
the core concerns that never move. Linked from architecture/README.md,
architecture/server.md and docs/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* docs(b2-7): L-08 re-tagged to B10 with the reason; B2-7 evidence block
L-08's remaining gate ("deterministic source build passes") cannot pass in
principle — TinyGo embeds host paths and has no -trimpath — and a compile-only
drift check would need a second Go SDK, TinyGo and Binaryen on every PR for a
subsystem release builds compile out. Re-tagged to B10, which runs the compile
once against the release candidate or closes on the provenance record; the
"no API promise" half is closed by docs/architecture/plugins.md. The plan's
B2-7 evidence block records the four pre-squash SHAs, the absence test's RED
output, the release-build finding, the decision, and the BPR-051 reader
placeholder. CHANGELOG gains a Documentation block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex review — TOFU windows disclosed, legacy TOTP, absence test at three boundaries
Two P1 and three P2 from the Codex review of 56f23a36, all verified against
the code. trust-model.md now states the desktop's first-connection TLS TOFU
window and the out-of-band fingerprint check; scopes identity-key pinning to
changes after the first pin, not first contact; discloses that databases from
before TOTP encryption may still hold plaintext secrets and how re-enrolment
fixes that. The absence contract gains two sibling tests — WebSocket wire
types from protocol/schema.json and every koanf key of config.Config (one
allowlisted on-disk path whose presence the test asserts) — and the document
states what the three tests bound. plugins.md corrected: unknown config keys
are warned about and ignored, not rejected. Evidence block records PR #1443
and the review outcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex re-review — desktop first-use window in every TLS mode; tls off is plaintext
The desktop pins the fingerprint it sees on first connection in every
tls.mode (CaptureVerifier, no web-PKI validation on the server connection),
so a public-CA certificate closes the first-use window only for a browser;
the short answer and the pinning list now say so. tls.mode off served
directly is plaintext HTTP with nothing enforcing a proxy; the transport
table row states it. Evidence block records the round-2 outcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 3 — E2EE does not survive a hostile operator on first contact; memory cap is server-wide
Identity keys are trusted on first use, so a modified server can deliver an
unpinned peer's first announce with keys the operator holds and the key
holder wraps the room key to it. trust-model.md now scopes E2EE to an
operator who reads, and to a modified server only for peers pinned and
compared out of band beforehand, in every place the stronger claim stood.
plugins.md: the wazero runtime is sized from plugins.max_memory_mb alone;
a manifest's memory value is validated but not applied. Evidence block
records the round.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 4 — transport intro excludes tls off; ipAllowed is not the full C-09 deny-set
The transport section opened with "everything is TLS"; it now excludes
tls.mode off. The C-09 contract cited the server's ipAllowed as the complete
address deny-set; it rejects loopback, private, link-local, unspecified,
multicast and CGN only, so the clause now lists the documentation and
benchmarking ranges the native broker must add and records that widening
ipAllowed is a separate server change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 5 — backups exclude uploads; release-time appimagetool fetch listed
The built-in backup is VACUUM INTO of the SQLite file only; the trust model
and deployment.md now say uploaded files are not in it and upload.storage_dir
needs its own backup. The outbound-host section scopes "no script fetches an
external host" to scripts the server runs and lists the release workflow's
build-time appimagetool download.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 6 — one pin per account, LiveKit media in the host table, quick-start wording
Identity pins are keyed by host and user id, so a peer's second device
overwrites the pin and the first device then mismatches; the document no
longer claims per-device pinning. The outbound-host table gains the
supervised LiveKit subprocess's WebRTC media and scopes the capture contract
to traffic the server initiates. The quick-start cross-link no longer says
voice and video are unreadable by the operator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
17 KiB
Plugins — the experimental boundary
Kind: reference + boundary record. Verified against: dev @
2b2d58ab, 2026-08-29. Satisfies: BPR-080, BPR-081, BG-17; feeds HP-2
question 6.
OwnCord has a WASM plugin runtime. It is experimental, off by default, and absent from every shipped artifact. This document says exactly what exists, what it may not be relied on for, what could become a plugin after beta, and what never will.
Status: off, twice — and not in the release at all
| Layer | Fact | Anchor |
|---|---|---|
| Build tag | WASM execution compiles only under -tags wazero. The default build discovers and lists manifests but never executes a module |
Server/plugin/sandbox_default.go:1-5 (//go:build !wazero); sandbox_wazero.go is the tagged twin |
| Config | plugins.enabled defaults to false; the example config block is commented out and says "Disabled by default so existing operators are unaffected" |
Server/config/config.go:352; :449-457 |
| Startup | The registry is only built when the flag is on; otherwise the plugin admin handler gets nil, lifecycle calls answer 503 and the list is empty |
Server/main.go:394; Server/api/router.go:35-37, :184 |
| Release builds | release.yml builds chatserver.exe / chatserver with a plain go build — no wazero tag. The Docker image does the same. No shipped binary can run a plugin. |
.github/workflows/release.yml:261, :268; Server/Dockerfile:13 |
| CI | The wazero variants are compiled on every PR so they cannot rot, but their tests run only under the tag |
.github/workflows/ci.yml:52-55, :79 |
Configuration audit for HP-2 question 6 — is WASM off in every shape a server can take?
| Shape | Verdict | Why |
|---|---|---|
| Fresh install | off | default false (config.go:352); the generated config.yaml has the block commented out (:452-457) |
| Upgraded install | off | an older config.yaml has no plugins key; koanf loads defaults first, so the absent key reads as false. A misspelled key is warned about and ignored (Server/config/config.go:520), which also leaves the default false in force |
| Docker | off | image built without the tag (Dockerfile:13); OWNCORD_PLUGINS_ENABLED=true (config.go:525-526 env provider) would flip the flag but the binary still cannot execute a module |
| Standalone release binary | off | same — no tag (release.yml:261, :268) |
| Source build with the flag | on | only go build -tags wazero and plugins.enabled: true together run WASM. This is the developer path and the only one |
No API promise
The plugin ABI — the five guest exports (command_dispatch, allocate,
deallocate, list_commands, and the manifest contract) — may change or be
removed in any release without a deprecation period
(Server/plugin/examples/hello/README.md:6-10). There are no supported
plugins, so there is nothing to keep compatible with. The example under
Server/plugin/examples/hello/ is a reference for the ABI as it stands, not a
promise about the ABI to come; its prebuilt .wasm is not tracked
(.gitignore:59) and is not byte-reproducible (TinyGo embeds host paths —
hello/README.md:70-74).
Release-notes wording for beta. Use this text, unchanged, in the beta release notes and anywhere plugins are mentioned to operators:
Plugins are experimental. The WASM runtime is compiled out of release binaries and disabled by default in source builds. The plugin API carries no compatibility promise and may change or be removed without notice. Do not build on it for production use.
What exists today (for the record)
Enough to know what "experimental" is guarding. All paths under Server/plugin/.
| Aspect | State | Anchor |
|---|---|---|
| Runtime | wazero, one module instance per plugin, closed on context done | sandbox_wazero.go:102-103 |
| Manifest | JSON plugin.json; TOML plugin.toml only under the tag and preferred when both exist. Entrypoint must be a relative .wasm path. A zip with both is rejected as ambiguous (OC-0318) |
manifest.go:52-61, :115, :144-148; manifest_toml.go:1-5; loader.go:39; test registry_test.go:696-700 |
| Capabilities | closed set commands, events, storage, http, ui; anything else fails load |
manifest.go:97-112, :151 |
| Memory | server-wide cap: the shared wazero runtime is sized from plugins.max_memory_mb (default 64 MiB), clamped to the wazero page limit. A manifest's resources.max_memory_mb is validated but not applied — a plugin declaring 8 MiB can still grow to the server limit |
sandbox_wazero.go:83-102; config.go:126, :354; manifest value only read in manifest.go |
| CPU | a wall-clock deadline per invocation — the manifest's cpu_budget_ms if positive, else plugins.cpu_budget_ms if positive, else 100 ms as the final default (not a minimum: the example's 50 ms stays 50 ms). Not fuel-based: a runaway loop is killed by closing the module |
sandbox_wazero.go:300-325; config.go:128, :355; TOML resource keys decode (OC-0338) — test manifest_test.go:149-155 |
| HTTP | only hosts on plugins.http_allowlist (empty by default), 10 s timeout, body cap, at most 5 redirects re-validated, through the guarded dialer that refuses private, loopback, link-local and CGN answers |
host_http.go:39, :66, :87-102, :112, :136-155, :182-249; tests TestGuardedDial_* |
| Storage | a per-plugin key/value namespace, scan size clamped; no other table is reachable | pluginstore.go:12-24; host_storage.go:27-66 |
| Filesystem | none — no WASI preview-1 filesystem is mounted | sandbox_wazero.go:77-78 |
| Host imports | wasi_snapshot_preview1 is instantiated (sandbox_wazero.go:105) — clocks, randomness, standard streams; no preopened filesystem. The OwnCord-specific imports are not wired yet: the http, storage, events and ui APIs exist on the Go side of the registry, and a guest module today can only receive command_dispatch |
sandbox_wazero.go:311-316 |
| Admin surface | GET /, POST /install (zip), POST /{id}/enable, POST /{id}/disable, DELETE /{id} under /api/v1/admin/plugins, behind the admin IP allowlist and an admin session — a LAN peer on the allowed CIDR cannot install without one. Install and uninstall are audited |
Server/api/plugins_handler.go:43-47; Server/api/router.go:171-186; test TestAuditCoverage_PluginLifecycle |
| Client | no plugin UI exists in the desktop client | grep -rn plugin Client/src hits only @tauri-apps/plugin-* imports |
Could become a plugin after beta (BPR-081)
Cohesive features and provider integrations that are natural plugin shapes. During beta they stay where they are, in core, behind the same tests — the audit names them so the boundary is a decision, not drift. Nothing on this list moves behind the experimental runtime before a supported plugin API exists.
| Candidate | Lives today | Why it is a candidate |
|---|---|---|
| GIF provider (Klipy) | Server/api/gif_handler.go (server proxy, gif.api_key) |
one vendor behind one endpoint; swapping vendors should not touch core |
| Link-preview and embed providers (YouTube oEmbed, OG) | Client/src/components/message-list/media.ts, embeds.ts (moves behind the B7 native broker first — C-09) |
provider-shaped; the destination policy stays core |
| Slash commands and automation | the plugin_command protocol family already exists (protocol/schema.json) |
the one thing the runtime can dispatch today |
| Webhooks (inbound and outbound) | not implemented | classic integration seam |
| Optional moderation automation | not implemented; human moderation in Server/service/moderation.go |
may suggest; human authority and the moderation audit trail stay core |
| UI tabs / panels | ui capability reserved (host_ui.go), no client side |
display-only extension point |
| Import/export bridges (other chat platforms) | not implemented | one-shot data movers with no security surface of their own |
| Observability exporters | Server/telemetry/ (OTel, -tags otel, exporter none by default) |
already a build-tagged optional edge |
Core that never moves
These stay in the server and client proper, permanently, whatever the plugin system becomes. A plugin may call some of them; none may replace or bypass them.
| Concern | Owner |
|---|---|
| Authentication, sessions, 2FA | Server/auth/, Server/api/auth_handler.go |
| Authorization | Server/permissions/ (one predicate per property — B2-5) |
| TLS and certificate pinning | Server/auth/tls.go; Client/src-tauri/src/tofu.rs |
| Safe outbound fetch | Server/plugin/host_http.go guarded dialer; the B7 native broker (C-09) |
| Quotas and rate limits | Server/auth/ratelimit*.go, per-action limits in Server/service/ |
| Voice/video E2EE | Client/src/lib/e2eeCrypto.ts, livekitE2EE.ts, identity.ts; Server/ws/voice_e2ee.go |
| Updates and signatures | Server/updater/, Server/api/client_update.go |
| Deletion and account removal | Server/db/account.go |
| Backup and recovery | Server/admin/handlers_backup.go, Server/db/admin_queries.go |
| Moderation audit | Server/db/audit*.go, Server/db/audittest/, TestAuditCoverage_* |
The trust statements these back are in ../trust-model.md.
Maintenance
Source of truth: Server/plugin/, Server/config/config.go (PluginsConfig
and its defaults), Server/main.go (registry construction),
.github/workflows/release.yml and Server/Dockerfile (build flags),
Server/plugin/examples/hello/README.md (ABI and toolchain). A change to any of
those that alters a row above updates this document in the same PR. The
release-notes paragraph is quoted verbatim by the beta release; change it here
first.