Files
OwnCord/docs/architecture/platform-contracts.md
T
J3vbandClaude Opus 5 5a70f7ae0f B1-8: platform contract map, HP-1 structural review, and the B1 exit gate (RL-02 / L-02) (#1420)
* docs: record the desktop/browser platform contract map (B1-8, RL-02/L-02)

Client/src/platform/ does not exist — no commits, no files, zero importers.
RL-02 asked for the boundary to be *recorded* in B1 so that B7 executes a
decided plan rather than rediscovering the surface. This is that record, and
nothing more: no directory, no interface, no code.

Measured against dev @ eb873fe7, not estimated: 20 files under Client/src/
import @tauri-apps, using 26 distinct invoke command names against 30
#[tauri::command] handlers, with zero dangling calls and zero uses of the
window.__TAURI__ global. Every native dependency is an import, so a static
check can find all of them — which is what BPR-025 will eventually enforce.

The count is 26 and not 22 because Client/src/lib/ws.ts binds core.invoke to a
local tauriInvoke before calling it; a regex matching only invoke("…") misses
ws_connect, ws_send, ws_disconnect and accept_cert_fingerprint. Any future
lint rule enforcing the seam has to match the binding, not the call site.

The 20 files collapse into 13 capability clusters, three of which have no
browser equivalent and are flagged as product decisions rather than shims:
certificate TOFU in ws.ts, the OS keychain behind credentials.ts/identity.ts,
and out-of-focus push-to-talk in ptt.ts.

Ownership is recorded by phase (B7/B8/B2). No human owners exist for these
folders anywhere in the repository; the document says so rather than leaving
the absence to read as an oversight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: perform the HP-1 structural review and measure the B1 exit gate

HP-1 asks whether B1's migrations were mechanical. It had never been run, and
it cannot be run against dev: dev is squash-merge only, so #1411 landed as one
commit and the pure-move/path-rewrite separation the hold point exists to
review survives only on refs/pull/1411/head. The scorecard records the
pre-squash SHAs so the review is reproducible.

Four proofs, all passing:

- Pure move (4befe699): 473 renames, all R100, zero non-rename entries, zero
  line changes, and every renamed blob byte-identical. The blob-OID comparison
  is what actually covers the six binaries — --numstat prints "-" for them, so
  the obvious line-count filter reports false positives.
- Path rewrite (38ddca73): 983 added / 983 removed, and after normalising the
  substitution, six unpaired pairs remain — all relative-path depth arithmetic
  from losing one directory level. Each was resolved against HEAD. The release
  signer is among them and runs only on a tag, so no CI run on any branch
  executes it; it is correct (working-directory: Client, artifacts at the root)
  and guarded by a downstream verify step that fails closed.
- Go module rename (7a4e5dc3): 350 files, 728/728, zero unpaired lines. The
  largest change in B1 is provably a pure substitution.
- Active path inventory: 11 files still name tauri-client, all historical —
  ledger lens labels, dated audits, and plans that describe the move. Zero in
  code, workflows, scripts, hooks or the Dockerfile.

The seed move (93ee14d5) does change behaviour — init() deleted, os.MkdirAll
moved into main(). That was authorised by the plan and is isolated in its own
commit, which is what HP-1 asks for.

Exit gate: seven of eight conditions evidenced. Condition 6 is recorded as
PARTIALLY MET and is a real gap — dev has 11 required checks pinned but
strict:false, so when dev advances after a PR goes green that PR can still
merge without re-testing, and the squash commit that lands was never itself
tested. Deliberately not changed here: flipping strict forces a rebase on every
open PR whenever another lands, and enforce_admins is on. Owner's call.

ENV-01 is closed. Every B0 number was measured on Node 26 while CI pins 24. The
client suite now re-runs on Node 24 from a fresh clone in a node:24 container:
192 files, 5257 tests — identical to B0, and the clone doubles as the exit
gate's Linux setup smoke. ENV-02 also reproduces at 50.1 MB booting on :8443.

Corrects the plan's stale Docker command along the way: the script moved to
Server/scripts/ and now takes the image as an argument, and the build context
is Server/ rather than the repository root — building from the root streams the
whole working tree and then fails on the missing go.mod.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record the applied repository settings in the HP-1 scorecard

Both checked-in settings scripts were run on 2026-08-27 — they had landed in
#1418 and #1419 but were deliberately never executed, because repo-settings
writes need a person.

b0-dev-branch-protection.sh pinned the twelfth required check on dev,
"Docs & Ledger Consistency". Until that run the FINDINGS.md drift gate reported
but could not block a merge. Condition 6 now reads 12 pinned checks; it stays
PARTIALLY MET because strict is still false, which the script itself encodes as
a deliberate choice.

b1-release-tag-protection.sh created the "Release tags" ruleset (active, target
tag, refs/tags/v*, blocks update and deletion, zero bypass actors) and the
release environment with one required reviewer. Checked for a pre-existing
ruleset of that name first — the POST half is not idempotent and a second run
would have created a duplicate. Three rulesets existed, all targeting branches,
none named "Release tags".

Condition 7 closes: B1-7 merged, and the Discussions slugs its issue-template
config hardcodes — q-a and ideas — both exist, so the contact links resolve
rather than silently dropping the user on the category picker.

Two things the read-back surfaced, both recorded as open, neither blocking:

- The release environment has can_admins_bypass: true, GitHub's default. The
  ruleset has zero bypass actors, but the reviewer gate does not. Moot while
  the sole admin is also the sole reviewer.
- claude.yml passes secrets.CLAUDE_CODE_OAUTH_TOKEN and the repository has no
  such secret. Nothing is failing, because all five issue_comment runs are
  skipped at the B1-7 guard before the missing secret would matter — but the
  paid-automation surface RL-22 hardens is inert today.

environment: release is still absent from release.yml, deliberately. The
environment now exists, so that is a separate two-line change.

Gate re-run after rebasing onto c0c87366 so condition 8 is measured over the
final tree, B1-7 included: green, 5257 client tests, exit 0. B1-7's
check-workflow-guards.mjs runs locally; its sibling verify-gate-evidence.mjs
does not — CI runs the selftest, and the assert form needs a token and a real
SHA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:11:47 +00:00

10 KiB

Platform contract map — desktop and browser

Kind: target-state map. Status: design record only — the seam described here does not exist in the code yet. Measured against: dev @ eb873fe7, 2026-08-27. Closes: RL-02 / L-02 (B1-8). Executed by: B7.

OwnCord is a Tauri desktop app whose frontend talks to native APIs directly. Beta requires the same frontend to also run in a browser. This document records where the seam between "shared app" and "native host" will go, and what has to move across it — so that B7 executes a decided plan instead of rediscovering the surface.

Nothing here is implemented. B1 was an explicitly non-functional phase: "No native behaviour moves in B1. Adapter extraction is B7 and must not be smuggled in." This file adds no directory, no interface, and no code.

Target layout

Client/src/platform/
├── contracts/   # TypeScript interfaces only. No imports from @tauri-apps.
├── desktop/     # Tauri implementations. The ONLY place @tauri-apps may appear.
└── browser/     # Web-standard implementations, or an explicit refusal.

The rule this eventually enforces is BPR-025 (traceability):

Static checks keep native imports inside desktop ownership; the same domain/store/protocol suites run against desktop and browser adapters.

Two consequences worth stating now, because they shape the interface design:

  • Contracts must be async everywhere. Some operations are synchronous in a browser and IPC round-trips on desktop. A contract that exposes a sync method cannot be implemented by the desktop side.
  • A browser adapter is allowed to refuse. Three capabilities below have no web equivalent. The contract must let an adapter say "unsupported" and let the app degrade, rather than force a fake implementation that fails at runtime.

What exists today

Measured with git grep, not estimated:

Measure Value
Files under Client/src/ importing @tauri-apps/* 20
Distinct invoke command names called from Client/src/ 26
#[tauri::command] handlers in Client/src-tauri/ 30
TS calls with no matching Rust handler 0
Uses of the window.__TAURI__ global 0
Environment-detection helper (isDesktop() or equivalent) none
Files under Client/src/platform/ 0

Reproduce:

git grep -l "@tauri-apps" -- 'Client/src/**' | wc -l
git grep -hoE '(tauriInvoke|invoke)(<[^>]*>)?\(\s*"[a-z_]+"' -- 'Client/src/**' \
  | grep -oE '[a-z_]+"$' | tr -d '"' | sort -u | wc -l

Note the alias: Client/src/lib/ws.ts binds core.invoke to a local tauriInvoke before calling it, so a regex that only matches invoke("…") undercounts by four (ws_connect, ws_send, ws_disconnect, accept_cert_fingerprint). Any future lint rule enforcing the seam must match the binding, not the call site.

Four Rust handlers are registered but never invoked from Client/src/: get_cert_fingerprint and store_cert_fingerprint (used by Client/tests/e2e/helpers.ts), and probe_credential_store and ptt_get_key (no caller anywhere). The latter two are dead-surface candidates — B7's call, not B1's.

There is no window.__TAURI__ access and no environment branching, which is good news: every native dependency is a static or dynamic import, so a static check can find all of them. The only typeof window guards in Client/src/lib/ are in channel-mutes.ts and logger.ts, and are unrelated to desktop/browser branching.

Proposed contracts

Thirteen capability clusters. Each becomes one file under contracts/, with matching implementations under desktop/ and browser/.

Contract Files today Native surface Browser outlook
HTTP fetch lib/api.ts, lib/profiles.ts, message-list/{attachments,embeds,media}.ts plugin-http native fetch — but CORS becomes a server concern
WebSocket lib/ws.ts api/core, api/event; 4 invokes, 4 event listens ⚠ see hard cases
Secret storage lib/credentials.ts, lib/identity.ts api/core; 8 invokes ⚠ see hard cases
Settings lib/profiles.ts api/core (save_settings, get_settings) localStorage / IndexedDB
Native proxies lib/livekitSession.ts, lib/httpProxy.ts api/core; 4 invokes not needed — the proxies exist to work around desktop TLS
Notifications lib/notifications.ts plugin-notification, api/window Notification API + Page Visibility
Filesystem / logs lib/logPersistence.ts, settings/AdvancedTab.ts, settings/LogsTab.ts api/path, plugin-fs in-memory ring buffer + download
Window lib/window-state.ts, lib/notifications.ts api/window mostly unsupported; degrade
Updater / process lib/updater.ts, settings/AdvancedTab.ts api/core, plugin-process, plugin-autostart unsupported — the page reloads instead
Shell / opener lib/admin-panel.ts, main.ts plugin-opener window.open
File save / pick message-list/attachments.ts plugin-dialog, plugin-fs <a download> / File System Access API
Input / PTT lib/ptt.ts api/core, api/event; 5 invokes ⚠ see hard cases
Deep links lib/deep-link.ts plugin-deep-link URL routing
App metadata settings/LogsTab.ts api/app build-time constant

Two files appear under more than one contract (lib/profiles.ts does HTTP and settings; settings/AdvancedTab.ts spans four). That is expected — the clusters are capabilities, not a file partition, and those files split during extraction.

Hard cases — where a browser adapter cannot be a shim

These three are not implementation details. Each is a product decision that B7 must take deliberately, and each changes what the browser build is.

lib/ws.ts — certificate TOFU. The desktop client tunnels its WebSocket through Rust specifically so it can pin a self-signed certificate on first use (accept_cert_fingerprint, the cert-tofu event). A browser cannot inspect or pin a certificate; the user agent decides, and a self-signed server is simply refused. The browser adapter must degrade honestly — require a publicly-trusted certificate and say so — not emulate the flow. This trust path has been hardened twice already (identity TOFU, then a re-pin TOCTOU); do not let a browser adapter quietly reopen it.

lib/credentials.ts / lib/identity.ts — OS keychain. Secrets live in Windows Credential Manager / GNOME Keyring / macOS Keychain, and Client/src-tauri/src/secret_store.rs reads each write back before returning. The browser has no peer for this. Whatever the browser adapter stores, it is strictly weaker, and the E2EE identity key is among the secrets involved. This is a security-posture decision, not a storage swap.

lib/ptt.ts — push-to-talk. PTT is deliberately hand-rolled rather than using plugin-global-shortcut, because it must observe a key held down while OwnCord is unfocused. A browser cannot see keys outside its tab. The browser adapter can offer in-tab PTT or voice-activity detection, but not the desktop behaviour.

Ownership

No human owners are recorded for these folders, here or anywhere in the repository. That is a real gap, not an omission in this document — assigning them is unstarted work. Until then, ownership is by phase, matching the convention already used in the issue register:

Area Phase
contracts/, desktop/, browser/ B7
The static check enforcing the seam (BPR-025) B7
Browser build target and PWA packaging B8
Protocol contract both adapters speak B2

Source of truth

Per this directory's maintenance rule: a PR that adds a new @tauri-apps import to Client/src/, or a new #[tauri::command], updates the counts and the cluster table here in the same change.