You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The service can serve a Mini App's page through a per-Mini-App loopback proxy, with the credential and
the page bridge injected, without yet enabling the capability for this client's runtime owner.
Scope note
Criterion "with explicit Mini App options and no declared owner kind, the service starts a Mini App and
produces a view URL for it" moved to #256. Producing that URL needs the client-facing operation, and the
only legal route to the Mini App's origin and credential is a runtime-port capability member — which packages/webui/AGENTS.md says fails pnpm typecheck:webui without a binding. Its consumer is the open
operation in #256, and ADR 0020 already schedules the proxy's consumer with the cell, the presenter and
the view as one change.
What is delivered here: the proxy itself, its per-plugin stable port, credential injection, unchanged
paths, forwarded upgrades, the framable document and the bridge seam.
Acceptance criteria
Fetching that URL returns the page the Mini App's own process serves, with the bridge injected and
the document framable.
Paths are forwarded unchanged, including the absolute paths a page uses for its own subresources.
The credential is added on the outbound request and never appears in a response, in the view URL,
or in anything sent to the client.
WebSocket upgrades to the Mini App are forwarded under the same rules as requests.
One plugin's view URL is stable across a restart of the service, so the page's own storage survives.
Nothing in this change flips the Mini App service on for this client's runtime owner.
Six of the original seven criteria are proven; the seventh moved to #256 because its consumer lives there.
Branch feat/miniapp-proxy, commits e2ca0be3 → 55b90b08 → 1f009695, stacked on #263. Suite: packages/webui/test/unit/miniapp-proxy.test.ts, 9 cases against a real upstream http.Server.
1. Fetching the view URL returns the page the Mini App's process serves, bridge injected, document framable
startMiniAppProxy returns viewUrl; fetching it returns the upstream body. The document is the one
response the proxy rewrites: a script tag for the bridge is injected before </head>
(injects the bridge before the document head ends), after the opening <body> tag when a page has no
head (injects before the body when the document has no head), and ahead of the document when it has
neither. The bridge script is served by the proxy itself at its own path and never forwarded upstream
(serves the bridge itself and never forwards that path upstream).
Framability (makes the document framable: drops X-Frame-Options and the frame-ancestors directive): X-Frame-Options is dropped, frame-ancestors is removed from the page's CSP while default-src and script-src survive. Nothing is added. The rules are now recorded in ADR 0020 under
"The proxy makes the document framable", which settles that record's first open question; the bridge's
method-level error mapping is the only one left.
The bridge body arrives with #260 — this ticket owns which document, where the tag goes and what
headers survive.
2. Paths are forwarded unchanged, including the absolute paths a page uses
forwards the path unchanged and returns the upstream response: /echo reaches the upstream as /echo.
The proxy is one listener per Mini App rather than one origin with a path prefix precisely so that /dashboard, /api/... and asset URLs need no rewriting.
3. The credential is injected outbound and never reaches the client
injects the credential outbound and never returns it to the caller: the upstream observes the
credential header, and the response the caller receives has no such header. viewUrl is http://127.0.0.1:<port> — the credential is not in it and is not otherwise exposed.
4. WebSocket upgrades are forwarded under the same rules
forwards an upgrade with the credential: the caller receives the upstream's 101, and the upstream
observes the credential on the upgrade request. A closed peer tears down its pair; without that, a
half-closed pipe held the upstream server open forever, which is how the test teardown first failed.
5. One plugin's view URL is stable across a restart
keeps one plugin on the same port across a restart, so the page storage survives: start on the derived
port, close, start again from a fresh derivation, identical viewUrl. The port is a pure function of the
plugin id because the page's localStorage and IndexedDB are keyed by its origin.
6. Nothing here flips the service on for this client's runtime owner
Diff scope confirms it: this branch touches packages/webui/src/server/miniapp/ and its test only. No
owner-policy cell, no capability flag, no service composition.
Moved to #256: starting a Mini App with explicit options and producing its view URL
The ticket body now says so. Producing that URL needs the client-facing operation, and the only legal
route to a Mini App's origin and credential is a runtime-port capability member — packages/webui/AGENTS.md states that such a member fails pnpm typecheck:webui without a binding, and
its consumer is #256's open operation. ADR 0020 already schedules the proxy's consumer with the cell, the
presenter and the view as one change.
Gates at 1f009695
$ pnpm typecheck:webui # server, client and test projects, passed
$ pnpm check:source # 5459 files, passed
pnpm check:webui-dependency fails on a violation this branch did not introduce: document.fonts.ready
used from client/features/workspace/ui/WorkspacePanels.tsx:489,534, a bindings-layer file that already
fails at the base commit 9713c4cf. The new module contributes zero violations
(grep -c "miniapp/proxy" over the report). That failure will follow every branch in this stack until
the panel file is fixed.
Not run: pnpm verify in full; browser, package and release steps are left to CI.
Activity
feat(webui): make the proxied Mini App document framable and carry th…
test(webui): keep a Mini App's view URL stable across a proxy restart
Fectivnfy112357 commentedon Oct 26, 2026
Six of the original seven criteria are proven; the seventh moved to #256 because its consumer lives there.
Branch
feat/miniapp-proxy, commitse2ca0be3→55b90b08→1f009695, stacked on #263. Suite:packages/webui/test/unit/miniapp-proxy.test.ts, 9 cases against a real upstreamhttp.Server.1. Fetching the view URL returns the page the Mini App's process serves, bridge injected, document framable
startMiniAppProxyreturnsviewUrl; fetching it returns the upstream body. The document is the oneresponse the proxy rewrites: a script tag for the bridge is injected before
</head>(
injects the bridge before the document head ends), after the opening<body>tag when a page has nohead (
injects before the body when the document has no head), and ahead of the document when it hasneither. The bridge script is served by the proxy itself at its own path and never forwarded upstream
(
serves the bridge itself and never forwards that path upstream).Framability (
makes the document framable: drops X-Frame-Options and the frame-ancestors directive):X-Frame-Optionsis dropped,frame-ancestorsis removed from the page's CSP whiledefault-srcandscript-srcsurvive. Nothing is added. The rules are now recorded in ADR 0020 under"The proxy makes the document framable", which settles that record's first open question; the bridge's
method-level error mapping is the only one left.
The bridge body arrives with #260 — this ticket owns which document, where the tag goes and what
headers survive.
2. Paths are forwarded unchanged, including the absolute paths a page uses
forwards the path unchanged and returns the upstream response:/echoreaches the upstream as/echo.The proxy is one listener per Mini App rather than one origin with a path prefix precisely so that
/dashboard,/api/...and asset URLs need no rewriting.3. The credential is injected outbound and never reaches the client
injects the credential outbound and never returns it to the caller: the upstream observes thecredential header, and the response the caller receives has no such header.
viewUrlishttp://127.0.0.1:<port>— the credential is not in it and is not otherwise exposed.4. WebSocket upgrades are forwarded under the same rules
forwards an upgrade with the credential: the caller receives the upstream's101, and the upstreamobserves the credential on the upgrade request. A closed peer tears down its pair; without that, a
half-closed pipe held the upstream server open forever, which is how the test teardown first failed.
5. One plugin's view URL is stable across a restart
keeps one plugin on the same port across a restart, so the page storage survives: start on the derivedport, close, start again from a fresh derivation, identical
viewUrl. The port is a pure function of theplugin id because the page's
localStorageandIndexedDBare keyed by its origin.6. Nothing here flips the service on for this client's runtime owner
Diff scope confirms it: this branch touches
packages/webui/src/server/miniapp/and its test only. Noowner-policy cell, no capability flag, no service composition.
Moved to #256: starting a Mini App with explicit options and producing its view URL
The ticket body now says so. Producing that URL needs the client-facing operation, and the only legal
route to a Mini App's origin and credential is a
runtime-portcapability member —packages/webui/AGENTS.mdstates that such a member failspnpm typecheck:webuiwithout a binding, andits consumer is #256's open operation. ADR 0020 already schedules the proxy's consumer with the cell, the
presenter and the view as one change.
Gates at
1f009695pnpm check:webui-dependencyfails on a violation this branch did not introduce:document.fonts.readyused from
client/features/workspace/ui/WorkspacePanels.tsx:489,534, a bindings-layer file that alreadyfails at the base commit
9713c4cf. The new module contributes zero violations(
grep -c "miniapp/proxy"over the report). That failure will follow every branch in this stack untilthe panel file is fixed.
Not run:
pnpm verifyin full; browser, package and release steps are left to CI.feat(webui): serve a Mini App page through its own loopback proxy
feat(webui): make the proxied Mini App document framable and carry th…
test(webui): keep a Mini App's view URL stable across a proxy restart