Docs

RPC Trust Gap

Findings snapshot — 2026-07-27. Investigation notes feeding a future planning session, not a decision. Captures why the tranquil-rpc runtime is presently injected into no page at all, so that session can start from the diagnosis. See the Security Considerations living threat model and the Guest↔Host RPC architecture doc.

Symptom

During normal browsing the dev console logs, per navigation:

[tranquil-rpc] untrusted — no runtime injected: https://en.wikipedia.org/wiki/Tom_M._Mitchell
[tranquil-rpc] untrusted — no runtime injected: https://claude.com/blog/...

This is correct behaviour, not a bug — those are hostile remote https:// pages and the trust model is deliberately default-deny. The log is our own (tranquil-rpc/lib/host.js, isTrusted() false path), firing on every non-trusted load. It surfaced only because we tightened the dev-console noise filter; the underlying behaviour predates it.

The real issue it exposes: the runtime is injected nowhere, including on content we do want trusted.

The trust model (as built)

tranquil-rpc/lib/trust.js is security-critical and default-deny. A URL is trusted only if:

  1. its scheme is file: — every other scheme (http, https, data, blob, about, …) is never trusted (isTrusted() returns false at the protocol check), and
  2. its resolved filesystem path is at, or under, a registered trusted root (exact path or a real child segment — no bare string-prefix matches).

Trusted pages get the injected guest runtime plus the full HostApi; untrusted pages get nothing. This strictness is intentional: a bug here hands host capabilities to hostile remote pages.

Two independent gaps

Gap 1 — no trusted roots are ever registered. addTrustedRoot() is exported from trust.js (and re-exported via lib/index.js) but nothing calls it. With an empty root set, isTrusted() is false for everything, including our own local content. Activation confirms it:

[tranquil-rpc] activated; trusted roots: []

Gap 2 — the model is file://-only, but our own app pages are served over HTTP. The app-template and automations content is served by browser-sync over http://localhost:<port>, a scheme the classifier rejects by design. So even if Gap 1 were fixed by registering a root, those pages still could not be trusted — trusting a localhost origin is a different, security-sensitive mechanism (any local process bound to that port could then impersonate a trusted page).

Net effect: the RPC runtime is currently injected on no page. Remote pages are correctly denied; our own pages are denied too, for the two reasons above.

The decision a fix requires

The crux is a design choice — how do we identify and serve trusted content? — not a small patch. Sketch of the option space, cheapest first:

  • Serve trusted pages from file:// and register those roots. Stays entirely within the current model; the change is calling addTrustedRoot() for the right directories and (if needed) serving our trusted app content from disk rather than browser-sync. Smallest blast radius.
  • Extend the trust model to a specific localhost origin. Lets browser-sync-served pages be trusted, but weakens the boundary — needs a story for why that origin can’t be impersonated by another local process (fixed port + loopback is not sufficient on its own).
  • Token / handshake-based trust. The host hands a freshly minted secret to content it served and accepts RPC only from holders of that secret. Most flexible, most work; touches trust.js, the ADR, and the threat model.

Any of these changes the security boundary of the entire RPC layer, so it belongs in its own session with the threat model in front of us.

References

  • tranquil-rpc/lib/trust.js — the classifier (isTrusted, addTrustedRoot, trustedRoots).
  • tranquil-rpc/lib/host.js — the isTrusted(url) gate and the “untrusted — no runtime injected” log; the activation log that prints the (empty) trusted roots.
  • tranquil-automations/lib/pane-controls-capability.js — the one live rpc.isTrusted(...) consumer.
  • ADR-0003: Cap’n Web RPC · ADR-0010: Trust-tiered pane controls
  • Security Considerations — fold any resolution back into that living doc.