Skip to content

Commit e23a02f

Browse files
committed
revert: interactive web-thread playgrounds
Reverts the changes merged through #701.
1 parent fdad55e commit e23a02f

29 files changed

Lines changed: 16 additions & 1395 deletions

plugins/portal/README.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ fronts every surface. It does provider-neutral **OIDC** sign-in (Google Workspac
66
is the email-first deployment default) and reverse-proxies — over Fly's private 6PN — to the
77
surfaces, which all stay **private** (no public `[http_service]` of their own):
88

9-
| Path | → upstream | Notes |
10-
| ------------- | ----------------- | ----------------------------------------------------------------------------- |
11-
| `/*` (root) | `<prefix>-web-ui` | Pi web UI SPA, root-mounted (`/web-ui/*` 308-redirects to root for old links) |
12-
| `/admin/*` | `<prefix>-admin` | governance — admin access derived from the core (`canAdminister`) |
13-
| `/m/:id/:key` | core | public bearer HTML playgrounds (miniapps); no session; CSP-sandboxed |
9+
| Path | → upstream | Notes |
10+
| ----------- | ----------------- | ----------------------------------------------------------------------------- |
11+
| `/*` (root) | `<prefix>-web-ui` | Pi web UI SPA, root-mounted (`/web-ui/*` 308-redirects to root for old links) |
12+
| `/admin/*` | `<prefix>-admin` | governance — admin access derived from the core (`canAdminister`) |
1413

1514
User deployments are never served on this authenticated origin; they use the dedicated apps domain.
1615

@@ -28,12 +27,10 @@ surfaces, and it does **not** import the core.
2827
the subject from userinfo. The verified `sub` **is** the core principal id. It mints a
2928
signed `portal_session` cookie (`{sub, org, auth, exp}`, HMAC, 8h sliding lifetime with a
3029
24h absolute maximum by default).
31-
3. **Proxy** — inbound webhooks, OAuth callbacks, and miniapp playgrounds (`GET /m/:id/:key`)
32-
pass through to core without a session. Every other path requires a valid session. The portal
33-
picks the upstream by the **exact first path segment**, strips the prefix, and proxies to the
34-
private upstream, synthesizing the surface cookie for compatibility and attaching a short-lived
35-
signed portal identity. Surfaces pass that identity to core, which verifies it before any
36-
user-scoped action.
30+
3. **Proxy** — every other path requires a valid session. The portal picks the upstream by the
31+
**exact first path segment**, strips the prefix, and proxies to the private upstream,
32+
synthesizing the surface cookie for compatibility and attaching a short-lived signed portal
33+
identity. Surfaces pass that identity to core, which verifies it before any user-scoped action.
3734

3835
## Security model (the parts that must be right)
3936

plugins/portal/src/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -959,11 +959,6 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void>
959959
return proxyToUpstream(req, res, { baseUrl: CORE, path: pathname, search: url.search }, FORWARD_WEBHOOK_HEADERS);
960960
}
961961

962-
if (method === "GET" && /^\/m\/[^/]+\/[^/]+$/.test(pathname)) {
963-
res.removeHeader("x-frame-options");
964-
return proxyToUpstream(req, res, { baseUrl: CORE, path: pathname, search: url.search }, ["accept"]);
965-
}
966-
967962
const consentBounce = (): void => {
968963
res.writeHead(302, { location: `/auth/login?returnTo=${encodeURIComponent(`${pathname}${url.search}`)}` });
969964
return void res.end();

plugins/portal/test/router.test.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -230,21 +230,6 @@ test("the webhook passthrough is exact-shape + POST-only (no widening of /v1)",
230230
assert.equal((await fetch(`${base}/v1/webhooks`, { method: "POST" })).status, 404);
231231
});
232232

233-
test("miniapps pass through to the core with NO session so Slack buttons and iframes work", async () => {
234-
const r = await fetch(`${base}/m/aa/bb`, { headers: { accept: "text/html" }, redirect: "manual" });
235-
assert.equal(r.status, 200);
236-
const body = (await r.json()) as { url: string; cookie: string | null };
237-
assert.equal(body.url, "/m/aa/bb");
238-
assert.equal(body.cookie, null);
239-
assert.equal(r.headers.get("x-frame-options"), null);
240-
});
241-
242-
test("the miniapp passthrough is GET /m/:id/:key only", async () => {
243-
assert.equal((await fetch(`${base}/m/aa`, { redirect: "manual" })).status, 401);
244-
assert.equal((await fetch(`${base}/m/aa/bb/cc`, { redirect: "manual" })).status, 401);
245-
assert.equal((await fetch(`${base}/m/aa/bb`, { method: "POST", redirect: "manual" })).status, 401);
246-
});
247-
248233
test("the provider callback still passes through publicly with NO session/cookie", async () => {
249234
const cb = await fetch(`${base}/v1/connectors/oauth/google/callback?code=c&state=s`, { redirect: "manual" });
250235
assert.equal(cb.status, 200);

plugins/web-ui/server/index.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2346,27 +2346,6 @@ const routeRequest = async (req: IncomingMessage, res: ServerResponse) => {
23462346
return found.route.handle({ req, res, url, user, params: found.params });
23472347
}
23482348

2349-
if (method === "GET" && path.startsWith("/m/")) {
2350-
const rest = path.slice("/m/".length);
2351-
const slash = rest.indexOf("/");
2352-
if (slash === -1) return json(res, 404, { error: "not_found" });
2353-
const id = decodeURIComponent(rest.slice(0, slash));
2354-
const key = decodeURIComponent(rest.slice(slash + 1).split("/")[0] ?? "");
2355-
const corePath = `/m/${encodeURIComponent(id)}/${encodeURIComponent(key)}${url.search}`;
2356-
const up = await fetch(`${CORE}${corePath}`, { method: "GET", redirect: "manual" });
2357-
const buf = Buffer.from(await up.arrayBuffer());
2358-
res.removeHeader("x-frame-options");
2359-
res.writeHead(up.status, {
2360-
"content-type": up.headers.get("content-type") ?? "text/html; charset=utf-8",
2361-
"content-length": String(buf.length),
2362-
"content-security-policy": up.headers.get("content-security-policy") ?? UNTRUSTED_CONTENT_SANDBOX_CSP,
2363-
"x-content-type-options": "nosniff",
2364-
"referrer-policy": "no-referrer",
2365-
"cache-control": up.headers.get("cache-control") ?? "private, no-cache",
2366-
});
2367-
return res.end(buf);
2368-
}
2369-
23702349
if (method === "GET" && path.startsWith("/deployments/")) {
23712350
const user = cookieUser(req);
23722351
if (!user) return unauthorized(res, req);

plugins/web-ui/src/chat.ts

Lines changed: 2 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ import "./marked-dedupe";
66
import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
77
import "@mariozechner/mini-lit/dist/CodeBlock.js";
88
import { html, nothing, render, type TemplateResult } from "lit";
9-
import { unsafeHTML } from "lit/directives/unsafe-html.js";
109
import {
1110
Activity,
1211
Ban,
1312
Brain,
1413
Check,
1514
ChevronRight,
1615
Clock3,
17-
Code2,
1816
Copy,
1917
FileImage,
2018
FileText,
@@ -71,16 +69,6 @@ import {
7169
} from "./core-bridge";
7270
import { buildTimeline, toolRowKind, type TimelineItem, type ToolPayload, type ToolRowModel } from "./timeline";
7371
import { CONNECTOR_NAMES, connectorLinksIn, stripConnectorLinks, type ConnectorLink } from "./connector-link";
74-
import {
75-
miniappsIn,
76-
miniappFrameSrc,
77-
miniappSourceSrc,
78-
formatMiniappHtml,
79-
currentMiniappTheme,
80-
stripMiniappDirectives,
81-
type MiniappEmbed,
82-
} from "./miniapp";
83-
import hljs, { whenHighlightReady } from "./lazy-hljs";
8472
import { deepLinkPath, UI_BASE } from "./deep-link";
8573
import type { ChatSurface, ConvCtx } from "./conv-types";
8674
import { errMessage, swallow } from "../../chassis/src/errors";
@@ -129,53 +117,11 @@ interface SettledRowKey {
129117
approvalDecision: unknown;
130118
sendFailure: unknown;
131119
forkable: boolean;
132-
miniappKey: string;
133120
tpl: TemplateResult | typeof nothing;
134121
}
135122
const settledRowCache = new WeakMap<object, SettledRowKey>();
136123
const connectedConnectors = new Set<string>();
137-
const miniappPaneByUrl = new Map<string, "play" | "code">();
138-
const miniappSourceByUrl = new Map<string, string>();
139-
const miniappBootByPath = new Map<string, { ok: boolean; error?: string }>();
140-
const miniappBootTimerByPath = new Map<string, number>();
141124
const redrawHooks = new Set<() => void>();
142-
143-
function miniappPathOf(src: string): string {
144-
try {
145-
return new URL(src, location.origin).pathname;
146-
} catch {
147-
return src;
148-
}
149-
}
150-
151-
function noteMiniappBoot(path: string, boot: { ok: boolean; error?: string }): void {
152-
if (miniappBootByPath.get(path)?.ok && boot.ok) return;
153-
miniappBootByPath.set(path, boot);
154-
const timer = miniappBootTimerByPath.get(path);
155-
if (timer !== undefined) window.clearTimeout(timer);
156-
miniappBootTimerByPath.delete(path);
157-
for (const hook of redrawHooks) hook();
158-
}
159-
160-
window.addEventListener("message", (event) => {
161-
const data = event.data as { source?: string; path?: string; ok?: boolean; error?: string } | null;
162-
if (!data || data.source !== "qm-miniapp" || typeof data.path !== "string") return;
163-
noteMiniappBoot(data.path, {
164-
ok: Boolean(data.ok),
165-
...(typeof data.error === "string" && data.error ? { error: data.error } : {}),
166-
});
167-
});
168-
169-
function armMiniappBoot(src: string): void {
170-
const path = miniappPathOf(src);
171-
if (miniappBootByPath.has(path) || miniappBootTimerByPath.has(path)) return;
172-
miniappBootTimerByPath.set(
173-
path,
174-
window.setTimeout(() => {
175-
if (!miniappBootByPath.has(path)) noteMiniappBoot(path, { ok: true });
176-
}, 1500),
177-
);
178-
}
179125
let proactiveOpenerStarted = false;
180126

181127
export const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
@@ -563,13 +509,8 @@ export function createChatSurface(
563509
if (chatState.agent) drawActiveChat();
564510
}
565511

566-
function redrawForMiniapp(): void {
567-
redrawTranscript();
568-
}
569-
570512
function dispose(): void {
571513
redrawHooks.delete(redrawForConnector);
572-
redrawHooks.delete(redrawForMiniapp);
573514
teardownActiveChat();
574515
}
575516

@@ -1296,13 +1237,6 @@ export function createChatSurface(
12961237
(!work || ((work.status === "complete" || work.status === "failed") && !work.pendingApprovals?.length));
12971238
if (!cacheable) return chatMessage(message, index, isStreaming);
12981239
const forkable = Boolean(chatState.threadRef && chatState.sessionId && chatState.agent);
1299-
const miniappKey = miniappsIn(messageText(message))
1300-
.map((app) => {
1301-
const src = miniappFrameSrc(app.url, withBase, currentMiniappTheme());
1302-
const boot = miniappBootByPath.get(miniappPathOf(src));
1303-
return `${app.url}:${miniappPaneByUrl.get(app.url) ?? "play"}:${miniappSourceByUrl.get(app.url) ?? ""}:${boot ? `${boot.ok}:${boot.error ?? ""}` : "pending"}`;
1304-
})
1305-
.join("\n");
13061240
const hit = settledRowCache.get(message as object);
13071241
if (
13081242
hit &&
@@ -1315,8 +1249,7 @@ export function createChatSurface(
13151249
hit.errorMessage === msg.errorMessage &&
13161250
hit.approvalDecision === msg.approvalDecision &&
13171251
hit.sendFailure === msg.sendFailure &&
1318-
hit.forkable === forkable &&
1319-
hit.miniappKey === miniappKey
1252+
hit.forkable === forkable
13201253
) {
13211254
return hit.tpl;
13221255
}
@@ -1332,7 +1265,6 @@ export function createChatSurface(
13321265
approvalDecision: msg.approvalDecision,
13331266
sendFailure: msg.sendFailure,
13341267
forkable,
1335-
miniappKey,
13361268
tpl,
13371269
});
13381270
return tpl;
@@ -1525,112 +1457,19 @@ export function createChatSurface(
15251457
</a>`;
15261458
}
15271459

1528-
function loadMiniappSource(url: string, sourceSrc: string): void {
1529-
if (miniappSourceByUrl.has(url)) return;
1530-
miniappSourceByUrl.set(url, "");
1531-
void fetch(sourceSrc, { cache: "reload" })
1532-
.then(async (r) => {
1533-
miniappSourceByUrl.set(url, r.ok ? await r.text() : "Couldn't load the source.");
1534-
redrawTranscript();
1535-
})
1536-
.catch(() => {
1537-
miniappSourceByUrl.set(url, "Couldn't load the source.");
1538-
redrawTranscript();
1539-
});
1540-
}
1541-
1542-
function miniappSourceView(source: string | undefined): TemplateResult {
1543-
if (!source) return html`<pre class="miniapp-source" tabindex="0">Loading source…</pre>`;
1544-
const formatted = formatMiniappHtml(source);
1545-
if (!hljs.getLanguage("html") && !hljs.getLanguage("xml")) {
1546-
void whenHighlightReady().then(() => redrawTranscript());
1547-
}
1548-
return html`<pre class="miniapp-source" tabindex="0">
1549-
${unsafeHTML(hljs.highlight(formatted, { language: "html" }).value)}</pre>`;
1550-
}
1551-
1552-
function miniappBootState(
1553-
pane: "play" | "code",
1554-
boot: { ok: boolean; error?: string } | undefined,
1555-
): TemplateResult | typeof nothing {
1556-
if (pane !== "play") return nothing;
1557-
if (!boot) return html`<div class="miniapp-boot" role="status">Starting…</div>`;
1558-
if (!boot.ok)
1559-
return html`<div class="miniapp-boot err" role="alert">${boot.error || "Playground hit an error."}</div>`;
1560-
return nothing;
1561-
}
1562-
1563-
function miniappCard(app: MiniappEmbed): TemplateResult {
1564-
const src = miniappFrameSrc(app.url, withBase, currentMiniappTheme());
1565-
const sourceSrc = miniappSourceSrc(src);
1566-
const pane = miniappPaneByUrl.get(app.url) ?? "play";
1567-
if (pane === "code") loadMiniappSource(app.url, sourceSrc);
1568-
const source = miniappSourceByUrl.get(app.url);
1569-
const path = miniappPathOf(src);
1570-
if (pane === "play") armMiniappBoot(src);
1571-
const boot = miniappBootByPath.get(path);
1572-
return html`<div class="miniapp-card">
1573-
<header class="miniapp-card-bar">
1574-
<span class="miniapp-card-icon">${icon(Files, 16)}</span>
1575-
<strong class="miniapp-card-title">${app.title}</strong>
1576-
<nav class="miniapp-tabs" aria-label="Playground views">
1577-
<button
1578-
class="miniapp-tab ${pane === "play" ? "on" : ""}"
1579-
type="button"
1580-
aria-pressed=${pane === "play"}
1581-
@click=${() => {
1582-
miniappPaneByUrl.set(app.url, "play");
1583-
redrawTranscript();
1584-
}}
1585-
>
1586-
Play
1587-
</button>
1588-
<button
1589-
class="miniapp-tab ${pane === "code" ? "on" : ""}"
1590-
type="button"
1591-
aria-pressed=${pane === "code"}
1592-
@click=${() => {
1593-
miniappPaneByUrl.set(app.url, "code");
1594-
loadMiniappSource(app.url, sourceSrc);
1595-
redrawTranscript();
1596-
}}
1597-
>
1598-
${icon(Code2, 13)} Code
1599-
</button>
1600-
</nav>
1601-
<a class="miniapp-card-open" href=${src} target="_blank" rel="noreferrer" title="Open in a new tab"
1602-
>${icon(Maximize2, 14)} Open</a
1603-
>
1604-
</header>
1605-
<div class="miniapp-stage">
1606-
<iframe
1607-
class="miniapp-frame ${pane === "code" ? "is-hidden" : ""}"
1608-
src=${src}
1609-
title=${app.title}
1610-
sandbox="allow-scripts allow-forms allow-pointer-lock allow-modals allow-popups"
1611-
referrerpolicy="no-referrer"
1612-
></iframe>
1613-
${miniappBootState(pane, boot)} ${pane === "code" ? miniappSourceView(source) : nothing}
1614-
</div>
1615-
</div>`;
1616-
}
1617-
16181460
function assistantContent(message: AssistantMessage, isStreaming = false, hasWork = false): TemplateResult[] {
16191461
const parts: TemplateResult[] = [];
16201462
for (const chunk of message.content) {
16211463
if (chunk.type === "text" && chunk.text.trim()) {
16221464
const links = connectorLinksIn(chunk.text, location.origin);
1623-
const apps = miniappsIn(chunk.text);
1624-
let body = stripMiniappDirectives(chunk.text);
1625-
if (links.length) body = stripConnectorLinks(body);
1465+
const body = links.length ? stripConnectorLinks(chunk.text) : chunk.text;
16261466
if (body.trim())
16271467
parts.push(
16281468
html`<div class="streaming-text ${isStreaming ? "live-stream" : ""}">
16291469
${isStreaming ? streamingMarkdown(body) : markdown(body)}
16301470
</div>`,
16311471
);
16321472
for (const link of links) parts.push(connectorWidget(link));
1633-
if (!isStreaming) for (const app of apps) parts.push(miniappCard(app));
16341473
}
16351474
if (chunk.type === "thinking" && chunk.thinking.trim()) {
16361475
parts.push(
@@ -2460,7 +2299,6 @@ ${unsafeHTML(hljs.highlight(formatted, { language: "html" }).value)}</pre>`;
24602299
}
24612300

24622301
redrawHooks.add(redrawForConnector);
2463-
redrawHooks.add(redrawForMiniapp);
24642302

24652303
return {
24662304
state: chatState,

plugins/web-ui/src/lazy-hljs.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@ type Hljs = {
88

99
let real: Hljs | null = null;
1010
let loading: Promise<void> | null = null;
11-
const readyWaiters: Array<() => void> = [];
12-
13-
export function whenHighlightReady(): Promise<void> {
14-
if (real) return Promise.resolve();
15-
ensureLoading();
16-
return new Promise((resolve) => readyWaiters.push(resolve));
17-
}
1811

1912
const LANGS = ["javascript", "typescript", "python", "xml", "css", "json", "bash", "sql", "markdown"] as const;
2013
const LANG_ALIASES: Record<string, string> = { html: "xml" };
@@ -54,8 +47,6 @@ function ensureLoading(): void {
5447
}
5548
real = hljs;
5649
rerenderMountedBlocks();
57-
const waiters = readyWaiters.splice(0);
58-
for (const fn of waiters) fn();
5950
})
6051
.catch(() => {
6152
loading = null;

0 commit comments

Comments
 (0)