@botiverse/porthole 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ # Architecture
2
+
3
+ This describes the layers, their contracts, the flows and trust boundaries, and
4
+ the extension points. It is meant to stay stable while concrete APIs change; the
5
+ phase-1 API lives in [DESIGN.md](DESIGN.md).
6
+
7
+ ## The core abstraction
8
+
9
+ Porthole renders **agent-drawn HTML plus a declared set of host functions.**
10
+
11
+ - The HTML is untrusted. It may draw anything, but on its own it can only draw.
12
+ - A **host function (capability)** is any power beyond drawing: send a prompt to
13
+ the agent, open a link, read or write state, call a tool, read data, report an
14
+ error. Each is named in the config and carries three facts: its **gate** (needs
15
+ a user gesture, needs a person's confirmation, is rate-limited), its **egress
16
+ needs** (origins it must reach), and its **guide entry** (how the manual
17
+ describes it).
18
+
19
+ One capability list therefore drives three artifacts: the **bridge** (which
20
+ functions the shell wires and the host handles), the **CSP** (what the sandbox
21
+ may reach), and the **manual** (what the agent is told). This is why the config
22
+ is the single source (PRINCIPLE 3) and why "talk to the agent" is not special —
23
+ it is one host function (PRINCIPLE 4).
24
+
25
+ ## Layers
26
+
27
+ ```
28
+ ┌──────────────────────────────────────────────────────────┐
29
+ │ Host app (owns product policy, theme, callbacks) │
30
+ └───────────────┬──────────────────────────────────────────┘
31
+ │ picks a preset, passes theme + callbacks
32
+ ┌─────────────────────────▼─────────────────────────────────────────┐
33
+ │ PRESET POLICY LAYER │
34
+ │ inline-widget · artifact · mcp-app · url │
35
+ │ each = a config: CSP+origins, capabilities+gates, theme, guide │
36
+ └─────────────────────────┬─────────────────────────────────────────┘
37
+ │ one config
38
+ ┌─────────────────────────▼─────────────────────────────────────────┐
39
+ │ FRAMEWORK LAYER (mechanisms only, no policy) │
40
+ │ │
41
+ │ config compiler ── CSP + sandbox/allow attrs │
42
+ │ └── shell runtime (what to wire) │
43
+ │ └── guide + tool description │
44
+ │ │
45
+ │ isolation core · egress hardening · streaming render · auto-size │
46
+ │ theming · host-function bridge · guide gen · serving (/server) │
47
+ └───────┬───────────────────────────────────────┬────────────────────┘
48
+ │ host side │ in-frame side (shell)
49
+ ┌───────▼─────────┐ ┌───────▼──────────────────┐
50
+ │ PROTOCOL ADAPTER│ ◄── transport ──► │ SHELL (runs first in │
51
+ │ (MCP Apps; …) │ (postMessage/ │ the isolated frame; │
52
+ │ │ native bridge) │ renders HTML, wires caps)│
53
+ └───────┬─────────┘ └───────────────────────────┘
54
+ │
55
+ ┌───────▼───────────────────────────────────────────────────────────┐
56
+ │ PLATFORM HOST web (iframe) now · WKWebView / Android later │
57
+ └─────────────────────────────────────────────────────────────────── ┘
58
+
59
+ CONFORMANCE / LEAK SUITE drives a platform host through the adapter and
60
+ probes every egress and escape channel (runnable by any host).
61
+ ```
62
+
63
+ ### Framework layer (mechanisms)
64
+
65
+ Pure mechanism, no product policy, no host or vendor types.
66
+
67
+ - **Config compiler.** Turns one config into the CSP string, the iframe sandbox
68
+ and `allow` attributes, the shell's wiring, and the guide + tool description.
69
+ Deterministic and pure, so a test can assert the three outputs agree.
70
+ - **Isolation core.** Places the content on an origin that is never the host's,
71
+ with nothing of the host's reachable: an opaque-origin frame, or a served
72
+ origin of its own (domain mode: a subdomain per storage scope on a separate
73
+ registrable domain; url mode: the app's own origin). The mode is chosen by
74
+ config; the rest of the framework does not depend on which.
75
+ - **Egress hardening.** The closed-by-default network and navigation walls, plus
76
+ the in-shell defenses for channels a CSP does not cover (see Trust boundaries).
77
+ - **Shell runtime.** The code that runs first inside the frame: applies theme,
78
+ inserts the HTML (streaming-aware), reports size and errors, and exposes the
79
+ declared host functions to the content.
80
+ - **Host-function bridge.** Carries capability calls from the shell to the host
81
+ and results back, over a transport, with validation and gating at the boundary.
82
+ - **Streaming render, auto-size, theming, guide generator.** The remaining
83
+ mechanisms, each usable on its own. Content is plain HTML and SVG; its look
84
+ comes from the theme variables, never from a component vocabulary (P015).
85
+ - **Serving.** For served modes, a request handler a host's backend mounts: it
86
+ serves documents on their own origins with their CSP in headers, and turns a
87
+ one-time ticket the host minted into a cookie of that origin only.
88
+
89
+ ### Preset policy layer
90
+
91
+ A preset is a named config with safe defaults. It fixes the CSP and allowed
92
+ origins, the capabilities and their gates, the theme vocabulary, and the manual.
93
+ A host adopts a preset and overrides only theme and callbacks.
94
+
95
+ - **inline-widget** — host injects agent code into a strict, opaque-origin frame
96
+ with a shell; smallest capability set; for cards inside a conversation.
97
+ - **artifact** — a standalone page: written into an opaque frame where the host
98
+ has it, or served at a URL on a separate registrable domain, CSP in headers,
99
+ versioned.
100
+ - **mcp-app** — renders a standard MCP Apps view (a `ui://` template plus tool
101
+ input/result), using the MCP Apps adapter.
102
+ - **url** — frames a running app by URL (a dev server's preview): isolation and
103
+ access auth only; no shell, no capabilities, no egress control.
104
+
105
+ ### Protocol adapters
106
+
107
+ A thin adapter maps the framework's capability and lifecycle events onto a wire
108
+ protocol. **MCP Apps** is the first and default. The adapter is the only layer
109
+ that knows protocol method names and shapes; the core speaks capabilities.
110
+
111
+ ### Platform hosts
112
+
113
+ The platform host owns the actual isolation primitive and the transport: an
114
+ `iframe` + `postMessage` on the web now; a `WKWebView` + message handler on iOS
115
+ later. The framework layer is written against a small platform interface so a new
116
+ platform is an implementation of that interface, not a fork.
117
+
118
+ ### Conformance / leak suite
119
+
120
+ A standalone test runner that drives a platform host through an adapter and
121
+ attempts every known egress and escape channel, asserting none succeeds. Any host
122
+ can run it against its own integration. It is both our regression gate and the
123
+ artifact we offer upstream (PRINCIPLE 5, 6).
124
+
125
+ ## Contracts between layers
126
+
127
+ - **Host → preset:** a preset name, theme tokens (resolved per color scheme), and
128
+ callbacks for each enabled host function. Nothing else is required.
129
+ - **Preset → framework:** one config (capabilities with gates and egress needs,
130
+ allowed origins, theme variable map, limits, layout, mode and isolation).
131
+ - **Config → {CSP, shell, guide}:** derived, never hand-written; a test asserts
132
+ agreement.
133
+ - **Framework ↔ shell:** lifecycle and capability messages over a transport.
134
+ Validated both directions; unknown or malformed messages are dropped.
135
+ - **Framework ↔ adapter:** the adapter translates between capability/lifecycle
136
+ events and protocol messages; it may add protocol-only messages (handshake,
137
+ teardown) that the framework does not model.
138
+ - **Framework ↔ platform host:** create an isolated surface, set its policy
139
+ (CSP/sandbox/allow or the native equivalent), send and receive messages,
140
+ report size, tear down.
141
+
142
+ ## Flows per mode
143
+
144
+ ### Inline widget
145
+
146
+ ```
147
+ Host app Framework/Adapter Shell (opaque frame)
148
+ │ mount(config, cbs) │ │
149
+ │──────────────────────────►│ compile config → CSP, │
150
+ │ │ sandbox attrs, shell doc │
151
+ │ │ create opaque frame ───────►│ (shell runs first)
152
+ │ │◄──── initialize ─────────────│
153
+ │ │──── host context (theme, │
154
+ │ │ capabilities, size) ────►│
155
+ │ content (may stream) ───►│──── content (partial*) ─────►│ draw inert
156
+ │ │──── content (complete) ─────►│ run
157
+ │ │◄──── size / error ───────────│
158
+ │◄── onError / onResize ────│ │
159
+ │ │◄──── capability call ────────│ (e.g. sendPrompt)
160
+ │◄── gate (confirm?) ───────│ validate + gate │
161
+ │── result ────────────────►│──── result ─────────────────►│
162
+ ```
163
+
164
+ `*` partial content is drawn inert (no scripts, handlers or navigation) until
165
+ complete; see Streaming in DESIGN.md.
166
+
167
+ ### MCP Apps view
168
+
169
+ Same shape, with the adapter speaking MCP Apps: the view is an MCP Apps `App`;
170
+ `initialize`/`initialized` is the handshake; content arrives as tool input
171
+ (streamed as tool-input-partial, then tool-input) and tool result; capability
172
+ calls are `ui/message`, `ui/open-link`, etc., which map to host functions. On the
173
+ web a second-origin sandbox proxy relays messages so arbitrary MCP App HTML
174
+ (which expects `allow-same-origin`) is isolated from the host; the proxy is
175
+ transparent to the view and the host.
176
+
177
+ ### Artifact page
178
+
179
+ The content is served as a real document at a URL on a separate registrable
180
+ domain with its CSP in HTTP headers; the host embeds that URL in a frame whose
181
+ `frame-ancestors` names the host. No code injection, and the shell is optional.
182
+ Access is gated by a one-time ticket exchanged for a scoped cookie. Where the host
183
+ holds the page and needs no stable origin, the shell writes it into an opaque
184
+ frame instead.
185
+
186
+ ### URL frame
187
+
188
+ ```
189
+ Host app Host backend (+ /server) App origin (served)
190
+ │ mount(url preset) │ │
191
+ │── locate ─────────►│ decide the viewer may see it, │
192
+ │◄── ticketed URL ───│ mint a one-time ticket │
193
+ │ frame src = URL ──────────────────────────────────────►│ ticket → cookie
194
+ │ │ (that origin only)
195
+ ```
196
+
197
+ No shell and no protocol: the app is someone else's. The embedding page frames
198
+ only the declared content origins.
199
+
200
+ ## Trust boundaries
201
+
202
+ 1. **Host ↔ content.** The hard boundary. The content runs on an origin that is
203
+ never the host's, so same-origin policy denies it the host's DOM, storage,
204
+ cookies and credentials. Everything it wants beyond drawing crosses the bridge
205
+ as a validated, gated capability call. *What each side may know:* the content
206
+ never holds a host secret; whatever it says, the host treats as the content's
207
+ own word and confirms with a person before acting where the capability's gate
208
+ says so.
209
+
210
+ 2. **Content ↔ the outside network.** Closed by default and opened only to the
211
+ origins the config's capabilities declare. Two kinds of attacker matter:
212
+ - *Attacking the host* (steal credentials, read other data, escape the
213
+ sandbox): stopped by origin isolation — the content is a stranger to the
214
+ host origin.
215
+ - *Leaking or deceiving* (exfiltrate what the content was already given, or
216
+ draw a convincing fake to fool the viewer): origin isolation does not help
217
+ here; egress hardening and gating do. Some channels (a peer connection's
218
+ ICE gathering, a self-navigation of the frame, a nested frame in a fresh
219
+ realm) reach the network outside a frame's own `connect-src`. These are
220
+ closed by in-shell defenses and by the embedding page's own policy, each
221
+ proven by a leak-suite probe (DESIGN.md lists them).
222
+ - *An opaque frame is as closed as its page.* A `srcdoc` frame inherits the
223
+ embedding page's whole policy as well as its own, so on a page with
224
+ `default-src 'none'` and a tight `connect-src`/`img-src` its content is more
225
+ closed than its config says: an origin the config opens stays shut. On such
226
+ a page prefer domain mode, whose frames inherit nothing (P058).
227
+
228
+ 3. **Message boundary.** Every message crossing the frame is validated against a
229
+ schema and checked for sender identity before use; the shell accepts messages
230
+ only from its host, and the host only from its frame. What the frame says about
231
+ itself (its size, its errors, an annotation's anchor and where it is) is the
232
+ content's word even when the shell says it, since they share a realm: the host
233
+ bounds it and uses it as a UI hint, never as a permission (P053).
234
+
235
+ 4. **Host's decision ↔ content domain.** Authentication and authorization
236
+ *policy* are the host's; carrying the host's decision safely onto the content
237
+ domain is Porthole's *mechanism*. The host decides who may open an instance
238
+ before it calls `issue` (a viewer it knows, or a public link), and may decide
239
+ again on every request (`ServerOptions.authorize`). Porthole carries the decision
240
+ as a one-time ticket, then a signed cookie of that origin and instance only,
241
+ with the host's opaque `subject` inside, and never interprets identity. A deny
242
+ ends access from the next request on and tells the display why.
243
+
244
+ ## Extension points
245
+
246
+ - **Capabilities.** New host functions are added to the capability vocabulary
247
+ with a gate, egress need, and guide entry. Adding one extends the bridge, the
248
+ CSP, and the manual together.
249
+ - **Protocol extension points.** MCP Apps gaps go through the spec's own
250
+ mechanisms (experimental capabilities, own-prefixed methods), so the external
251
+ contract stays standard (PRINCIPLE 2).
252
+ - **Theme variables.** The theme map is config; the standard MCP Apps variable
253
+ names are the default vocabulary.
254
+ - **The person's clipboard** is the host's alone: content asks for a copy
255
+ (`copyText`), the host performs it and shows what it copied (P051, P052).
256
+ - **Annotations** (`annotate`): the host drives a select mode and marks anchors in
257
+ the shell; anchors are W3C Web Annotation selectors a host stores with its own
258
+ comments (P053).
259
+ - **Served content.** A host plugs its own policy and infrastructure into `/server`:
260
+ who may see what (`authorize`), where tickets live (`TicketStore`: memory, D1),
261
+ how origins are named (`content.name`, other servers' `reserved` namespaces),
262
+ and where apps and artifact versions come from (`upstream`, `artifacts`). It runs
263
+ on any runtime with web-standard `Request`/`Response` (Node, Workers).
264
+
265
+ ## How a new protocol or platform plugs in
266
+
267
+ - **A new protocol** is a new adapter implementing the capability/lifecycle
268
+ translation. It does not touch the core, the shell mechanisms, or the presets
269
+ beyond adding a preset that selects it. The capability abstraction is the seam.
270
+ - **A new platform** is a new implementation of the platform-host interface
271
+ (create isolated surface, set policy, transport, size, teardown). The framework
272
+ layer, adapters and presets are unchanged; the shell runtime is plain,
273
+ dependency-free script so it runs as-is in a new engine.
274
+
275
+ This is what keeps the three answers independent: *which protocol* (adapter),
276
+ *which platform* (platform host), and *what policy* (preset/config) each vary
277
+ without the others changing.