Files
OwnCord/docs/architecture/plugins.md
J3vbandClaude Fable 5 88c7a8249a docs(b2-7): trust model, absence proofs, plugin boundary (#1443)
* 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>
2026-08-29 11:49:24 +00:00

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.