Clients
Capsule’s clients are native per platform, with as little divergence as possible. The cross-platform logic — including the entire verify_asset chokepoint, the import pipeline, and the library layout — lives in capsule-core and is consumed by every native client through capsule-sdk. Each native client’s job is the surface above that: rendering, input, and platform integration.
The boundary this doc owns is what every client must do — the client-class duties that, if skipped, put the client in the faulty class (see Threat Model — Client Class Taxonomy). Plus the sandboxed-decoder pattern, which is the largest remaining attack surface on the client.
Design Priorities
Section titled “Design Priorities”- Native. Native implementations per platform ensure familiar usability and enable platform-specific optimizations.
- Minimal divergence. Heavy and complex logic is centralized in
capsule-coreandcapsule-sdk; client-specific code is generally minimal and focused on display.
Platform Limitations
Section titled “Platform Limitations”Given the quantity of distinct native clients (each with its own platform-specific portion), certain features are limited to certain platforms — notably auto sync on platforms where the necessary APIs are not available.
Client Validation Duties
Section titled “Client Validation Duties”Clients are not trusted to enforce their own correctness — but they are responsible for refusing to apply state they cannot validate. The full client-side validation checklist is owned by Threat Model — Client-Side Validation Invariants; the duties are summarized here so client implementations have a single in-doc reference for what they must do:
- Run
verify_asseton every received asset manifest. Quarantine on failure; never silent-drop, never silent-accept. This is the chokepoint every client must route through — it is implemented once incapsule-core::cryptoand called by every receiving path (sync, federation, peering, backup-restore). - Refuse forward-version writes. Reject any incoming
sidecar_schema,crypto_suite_id, orprotocol_versionabove the client’s max known. Reading is allowed only in read-only mode if explicitly opted into. - Enforce the protocol handshake. Send
X-Capsule-Protocolon every request; honor426 Upgrade Requiredby stopping the request, never by silently downgrading. - Check the provenance chain. Maintain a local
latest_provenance_hashper asset; refuse to apply a manifest whoseprior_provenance_hashis behind it. See Import — Stale-Revival Detection. - Reject unknown closed-enum values.
action,content_type,DerivativeManifest.role, andgps.sourceare closed per protocol version; unknown values are structural errors, not “future to ignore.” - Preserve unknown CBOR keys within a known schema (Postel’s Law) but never act on them.
- Decode remote-origin asset bytes only in the Sandboxed Decoder.
- Honor the forbidden behaviors checklist. A client that backdates timestamps, strips unknown sidecar fields, overwrites provenance, signs for an epoch it does not hold, or invokes
revoke_all_sessionswithout master-key proof is buggy by definition.
Centralizing the validation logic in capsule-core ensures each native client gets the same enforcement; the wrapper layer that issues UI surfaces for quarantine and protocol-mismatch errors is the platform-specific portion.
Reading State From a Newer Client
Section titled “Reading State From a Newer Client”A client routinely encounters state a newer client wrote: unknown CBOR keys inside a known schema (always preserved per Postel’s Law), or — under an explicit read-only opt-in — a sidecar whose sidecar_schema exceeds the reader’s max known. The duty is to render what it can without ever destroying what it cannot interpret:
- Render the known, surface the unknown. The client displays every field it understands and shows a non-destructive indicator on the affected asset/album — “Created with a newer version of Capsule; some details may not be shown or editable here” — rather than failing, hiding, or quarantining the asset.
- Never strip, never rewrite. Unknown CBOR keys and forward-schema sidecars are strictly read-only: the client never writes back a structure it cannot fully represent, because doing so would strip the extension and invalidate the signature — a forbidden behavior. Editing such an asset is disabled behind the same indicator, pointing the user to update.
- Writes still fail closed. Reading newer state is best-effort and read-only; writing under a
protocol_version,crypto_suite_id, orsidecar_schemathe client does not implement remains rejected at the handshake. Tolerant reads, fail-closed writes — the tightened Postel’s Law.
This is the resolution of the former “new client UI surface” question: forward-written state is legible and safe, never silently dropped and never destructively rewritten.
Sandboxed Decoder
Section titled “Sandboxed Decoder”Capsule’s server never holds plaintext, so server-side image/video decoding is impossible by design. Decoding happens on the client, and the decode path is the largest remaining attack surface — image-format CVEs (libjpeg, libwebp, libheif, libavif have all shipped exploits in recent years) reach the client directly with attacker-controlled bytes.
The defense is structural isolation:
- Every remote-origin asset is decoded in a separate OS process or a WASM sandbox that has no filesystem write access, no network access, and no shared memory with the host app process.
- The sandbox communicates with the host via a narrow IPC channel that exchanges only the produced pixel buffer (or an error code) — not arbitrary structured data.
- The sandbox is allowed to crash. A decoder CVE that triggers a segfault kills the sandbox, not the app. The host process logs the crash, surfaces “asset failed to decode,” and continues. The sandbox is restarted on the next decode request.
- Local-origin assets (this device was the uploader and the bytes have never left local storage) bypass the sandbox at the user’s option — they have not crossed a trust boundary. By default the sandbox is still used uniformly, because the modest perf cost is worth the categorical guarantee.
- A media file that still fails to decode after a small fixed retry budget (default 3 attempts, to absorb a transient sandbox crash) is flagged in the UI as “unreadable on this device” rather than removed from the library — the bytes are preserved (per Filesystem — Repair) for inspection on another device.
This is the canonical declaration of the sandbox; Federation — Security Against Malicious Files references it for the federated-asset case, and Backup & Recovery — Backup Verification references it for dry-run decode sanity checks.
Validation
Section titled “Validation”The validation duties above translate directly to test surface. Most live in capsule-core (so they apply uniformly to every client); the per-platform pieces are the sandbox harness.
verify_assetper-receiver-path (unit). Every receiver code path (sync entry, federation pull, peering artifact, restore) routes throughverify_asset; assertion test confirms the same chokepoint is used, not a divergent implementation.- Forward-version rejection (unit). Per-validation-duty unit test: synthesize an input whose declared version exceeds the client’s max; assert write refusal.
- Forward-state read surface (unit). Present a sidecar with unknown CBOR keys and (opt-in) a higher
sidecar_schema; assert known fields render, the non-destructive “newer version” indicator shows, editing is disabled, and any write-back attempt is refused without stripping the unknown keys. - Sandbox crash isolation (smoke per platform). Feed the sandbox a known-CVE corpus; assert the host process survives every crash; assert the asset is surfaced as “unreadable on this device” and not removed from the library.
- Sandbox boundary (smoke per platform). Assert the sandbox cannot read the parent process’s filesystem, open network sockets, or write outside its scratch area. Per-platform fixtures verify each restriction.
- Forbidden-behavior tripwire (unit). For each item in the forbidden-behaviors checklist, a unit test confirms that calling the corresponding
capsule-coreAPI in the forbidden way panics or returns a structural error (so a buggy client cannot accidentally do the wrong thing).
There is no client-only E2E case; the closest cross-module test is the upload-and-display round-trip used by the Import pipeline, which is bounded E2E in Module Map.