diff --git a/docs/README.md b/docs/README.md index 34111dde..1a09c531 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,6 +60,7 @@ and a PR touching those updates the blueprint in the same change. - [data-model.md](architecture/data-model.md), [websocket.md](architecture/websocket.md), [voice-e2ee.md](architecture/voice-e2ee.md) - [ux/](architecture/ux/README.md) — target-state UX spec, per-view states and event→reaction maps - [platform-contracts.md](architecture/platform-contracts.md) — target-state desktop/browser seam: what has to move behind a contract before the client can run in a browser (B7) +- [plugins.md](architecture/plugins.md) — the experimental WASM plugin boundary: off by default, compiled out of releases, no API promise, what may become a plugin after beta and what never moves [client-architecture.md](client-architecture.md) is a redirect stub; the live document is [architecture/client.md](architecture/client.md). diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 82dd4e82..a777670e 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,16 +11,17 @@ natively) followed by a prose explanation and a **Source of truth** file list. ## Index -| Doc | Diagrams | Covers | -| ---------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -| [system-overview.md](system-overview.md) | D1 System context, D8 Deployment topology | All processes, trust boundaries, ports, single-instance constraints | -| [server.md](server.md) | D2 Server package map, D3 REST request lifecycle | Go package structure, DB-access styles, middleware chain | -| [websocket.md](websocket.md) | D4 WS connect / replay / dispatch | Real-time engine: auth handshake, 3-tier reconnect replay, backpressure, typed dispatch | -| [data-model.md](data-model.md) | D5 Entity-relationship overview | All 26 tables from migrations 001–028, grouped by domain | -| [voice-e2ee.md](voice-e2ee.md) | D6 Voice + E2EE flow | LiveKit token flow, loopback TLS tunnel, ECDH key-holder relay | -| [client.md](client.md) | D7 Client module map | Tauri client: bootstrap, dispatcher, stores, Rust sidecars (structure, as-built) | -| [ux/](ux/README.md) | UX flow + state diagrams | Client **behavior** spec (target state): what every view does and how it reacts to events, permissions, and failure | -| [platform-contracts.md](platform-contracts.md) | — | Desktop/browser **seam** (target state): where native dependencies will be isolated, and the three that have no browser equivalent | +| Doc | Diagrams | Covers | +| ---------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| [system-overview.md](system-overview.md) | D1 System context, D8 Deployment topology | All processes, trust boundaries, ports, single-instance constraints | +| [server.md](server.md) | D2 Server package map, D3 REST request lifecycle | Go package structure, DB-access styles, middleware chain | +| [websocket.md](websocket.md) | D4 WS connect / replay / dispatch | Real-time engine: auth handshake, 3-tier reconnect replay, backpressure, typed dispatch | +| [data-model.md](data-model.md) | D5 Entity-relationship overview | All 26 tables from migrations 001–028, grouped by domain | +| [voice-e2ee.md](voice-e2ee.md) | D6 Voice + E2EE flow | LiveKit token flow, loopback TLS tunnel, ECDH key-holder relay | +| [client.md](client.md) | D7 Client module map | Tauri client: bootstrap, dispatcher, stores, Rust sidecars (structure, as-built) | +| [ux/](ux/README.md) | UX flow + state diagrams | Client **behavior** spec (target state): what every view does and how it reacts to events, permissions, and failure | +| [platform-contracts.md](platform-contracts.md) | — | Desktop/browser **seam** (target state): where native dependencies will be isolated, and the three that have no browser equivalent | +| [plugins.md](plugins.md) | — | Experimental WASM plugin boundary: off twice and compiled out of releases, no API promise, post-beta candidates, core that never moves | ### Structure vs. behavior diff --git a/docs/architecture/plugins.md b/docs/architecture/plugins.md new file mode 100644 index 00000000..eecdfad8 --- /dev/null +++ b/docs/architecture/plugins.md @@ -0,0 +1,118 @@ +# 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`. Unknown keys are rejected, not silently ignored (`Server/config/unknown_keys_test.go`) | +| 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 | per-plugin linear-memory cap from manifest or `plugins.max_memory_mb` (default 64 MiB), clamped to the wazero page limit | `sandbox_wazero.go:84-97`, `:102`; `config.go:126`, `:354` | +| CPU | a wall-clock deadline per invocation — manifest, then `plugins.cpu_budget_ms`, then a hard 100 ms floor. 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 | **none are wired into the runtime yet.** The `http`, `storage`, `events` and `ui` APIs exist on the Go side of the registry; 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](../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. diff --git a/docs/architecture/server.md b/docs/architecture/server.md index 927f0003..0db0028d 100644 --- a/docs/architecture/server.md +++ b/docs/architecture/server.md @@ -85,6 +85,10 @@ spawns background goroutines; and mounts all routes. `main.go` performs only process-level wiring (config, TLS, DB, event persistence, HTTP server, shutdown). +The `plugin` package is experimental and compiled out of release binaries; +[plugins.md](plugins.md) records that boundary — what exists, what carries no +promise, and which core concerns will never move behind it. + **Source of truth:** `Server/main.go`, `Server/api/router.go`, package import graph (`go list -deps`), `Server/service/datastore.go`, `sqlc.yaml`.