@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.
- package/dist/conformance/entry.d.ts +8 -2
- package/dist/conformance/entry.js +23 -8
- package/dist/conformance/harness.js +4 -4
- package/dist/conformance/host-kit.d.ts +8 -3
- package/dist/conformance/host-kit.js +58 -23
- package/dist/conformance/limits.js +6 -0
- package/dist/conformance/served.d.ts +4 -4
- package/dist/conformance/served.js +26 -16
- package/dist/guide/guide.js +4 -4
- package/dist/guide/limits.js +1 -1
- package/dist/node/index.d.ts +11 -3
- package/dist/node/index.js +70 -10
- package/dist/server/host.d.ts +42 -8
- package/dist/server/host.js +35 -5
- package/dist/server/index.d.ts +3 -1
- package/dist/server/pages.js +15 -1
- package/dist/server/proxy.js +2 -0
- package/dist/server/server.js +9 -2
- package/dist/server/types.d.ts +15 -6
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/DECISIONS.md +1774 -0
- package/docs/DESIGN.md +1067 -0
- package/docs/OBJECTIVES.md +116 -0
- package/docs/PRINCIPLES.md +155 -0
- package/package.json +4 -3
- package/src/conformance/entry.ts +39 -10
- package/src/conformance/host-kit.ts +71 -28
- package/src/conformance/limits.ts +8 -0
- package/src/conformance/served.ts +34 -21
- package/src/guide/guide.ts +4 -4
- package/src/guide/limits.ts +1 -1
- package/src/node/index.ts +73 -10
- package/src/server/host.ts +84 -14
- package/src/server/index.ts +4 -0
- package/src/server/pages.ts +15 -1
- package/src/server/proxy.ts +2 -0
- package/src/server/server.ts +11 -2
- package/src/server/types.ts +18 -7
|
@@ -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.
|