Skip to content

feat: html apps tool#1

Open
aidenybai wants to merge 2 commits into
mainfrom
html-apps
Open

feat: html apps tool#1
aidenybai wants to merge 2 commits into
mainfrom
html-apps

Conversation

@aidenybai

Copy link
Copy Markdown
Member

Summary

Pookie can now generate small self-contained HTML apps (calculators, configurators, mini dashboards, embedded viewers, "let me poke at this" tools) and surface them in Slack as a card with an "Open app ↗" button. The button links to ${BASE_URL}/a/<token> on Pookie itself, where the HTML renders inside a strict iframe sandbox.

  • New create_html_app tool: takes title / description / a complete HTML document, persists encrypted in Redis (30-day TTL), returns a HMAC-signed URL.
  • New /a/[token] Next.js route: verifies the token, loads the row, and renders <iframe sandbox="allow-scripts" srcDoc={html} /> — no allow-same-origin, no allow-top-navigation, no popups, no forms. The iframe runs in an opaque origin so model-generated code cannot reach Pookie cookies or the parent DOM.
  • CSP (frame-ancestors 'self'), X-Frame-Options: SAMEORIGIN, and Cache-Control: private, no-store configured for the route.
  • System prompt entries teach the model when to reach for the tool and how to surface it via the existing card footerAction.
  • 14 new unit tests across sign-token, store, and tool (size limit, document-shape validation, encryption, expiry, signature tampering, cross-tenant isolation).
  • Phase 2 PRD + follow-up issue filed at .scratch/html-apps/ for the bidirectional postMessage bridge / MCP-Apps-host upgrade.

The lazy Redis state adapter was extracted from slack-bot.ts into server/state.ts so the new render route can share it.

Security model

  • Iframe sandbox="allow-scripts" only. Without allow-same-origin the iframe is in an opaque origin and cannot read cookies, parent DOM, or storage.
  • HTML rendered via srcDoc, never via a public-fetch URL the iframe could navigate to.
  • Token URLs are unguessable (HMAC-SHA256 over { teamId, id, exp } keyed by SLACK_ENCRYPTION_KEY) and time-limited (30 days).
  • Outer page sets frame-ancestors 'self' so other sites can't embed the wrapper.
  • HTML stored encrypted at rest via the existing Cryptr / SLACK_ENCRYPTION_KEY path used by memory.

Future hardening called out in the Phase 2 PRD: host /a/* on a separate origin (e.g. apps.pookie.app) so even a sandbox regression cannot reach Pookie cookies; gate the route via Slack OAuth before the bidirectional bridge ships.

Test plan

  • pnpm test from apps/api — 20 files / 176 tests pass (3 new files / 14 new tests).
  • pnpm typecheck — no new errors. (One pre-existing missing-vaul error in unrelated website mock components.)
  • pnpm lint — no new errors. (One pre-existing consistent-type-imports warning in unrelated website mock components.)
  • pnpm format:check clean on every touched file.
  • Manual: ask Pookie "make me a calculator" in a configured workspace, click the resulting button, confirm the iframe renders and the parent page can't be reached from devtools.
  • Manual: tamper with a token URL (flip a character), confirm /a/<token> shows the friendly 404.
  • Manual: wait until expiry (or test with a short TTL locally), confirm 404.

Out of scope (tracked in .scratch/html-apps/)

  • Bidirectional postMessage bridge (apps calling Pookie tools) — Phase 2.
  • MCP Apps host compatibility (consuming ui:// resources from connected MCP servers) — Phase 2.
  • Edit-in-thread iteration on existing apps.
  • Slack unfurl screenshot previews.
  • Multiplayer / shared state.

Made with Cursor

Pookie can now generate self-contained HTML apps and surface them in
Slack as a card with an "Open app" button. The /a/[token] route renders
the HTML inside a strict iframe sandbox (allow-scripts only, no
allow-same-origin) so model-written code runs in an opaque origin and
can't reach Pookie cookies, parent DOM, or top-level navigation.

- create_html_app tool with size + document-shape validation
- Encrypted Redis store keyed by team, 30-day TTL
- HMAC-signed token URLs (so links aren't guessable)
- Render route + 404 + CSP / frame-ancestors header config
- System prompt entries (tool routing + common workflow)
- Tests for sign/verify/expire, store round-trip + encryption, tool schema
- Phase 2 PRD + follow-up issue for the bidirectional/MCP-Apps-host upgrade

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pookie Ready Ready Preview, Comment May 6, 2026 9:58am

The channel-panel preload type used a forbidden `import()` type
annotation (`Promise<typeof import("emoji-picker-react")>`), which
tripped the typescript-eslint(consistent-type-imports) rule and broke
lint CI. Replace with `import type * as M` so the same module-namespace
type is referenced via a regular type-only import.

Co-authored-by: Cursor <cursoragent@cursor.com>
aidenybai added a commit that referenced this pull request May 7, 2026
Closes the four actionable findings from the review pass:

#1 (medium) -- detection-source mismatch between auto-react and system
reminder. The auto-react in handleSlackMessage previously only inspected
[currentMessage, ...skippedMessages], so a uwu/owo/meow that arrived as
a Redis-drained follow-up mid-turn (round 2+) flipped pet-mode on in
the system reminder but never got its cat reaction. Auto-react
bookkeeping now lives at the turn scope in handleSlackMessage with a
single \`petAutoReacted\` flag and a \`tryPetAutoReact(candidates)\`
helper called both on round 0 input AND after each Redis drain. To
keep the messageId attached, drainFollowUps now returns the full
QueuedFollowUp[] (text + messageId) instead of \`string[]\`; callers
that only need text \`.map((f) => f.text)\`. The downstream
followUpMessages -> system reminder pipeline is unchanged.

#2 (low) -- belt-and-suspenders: findUwuTriggerMessage now filters out
candidates with empty \`id\` so a malformed Slack message with text but
no ts can't trigger a 400 from reactions.add.

#5 (low) -- test coverage: resolveSlackEmojiShortcode now has cases
for the bare cat emojis (🐱 → cat, 🐈 → cat2), skin-tone modifiers
(👍🏻 / 👍🏿 → +1), and the bare-codepoint heart without its variation
selector (\\u2764 → heart).

#6 (low) -- tracing observability: stamp \`pookiebot.uwu_mode: true\`
when the per-round detection lights up, plus
\`pookiebot.uwu_trigger_message_id\` when the auto-react fires.
Consistent with existing \`pookiebot.dropped_card\` etc. attributes.

#7 (doc nit) -- the \`already_reacted\` comment now reflects Slack's
exact-emoji semantics: that branch only fires when the SAME shortcode
is already on the message, so reporting success there is genuinely
correct (the user-facing reaction state matches what the model wanted).

Tests:
- thread-lock tests rewritten for the QueuedFollowUp[] return shape
- agent-follow-ups mocks updated to mock objects with messageId
- four new resolveSlackEmojiShortcode cases for #5
- 281 total tests passing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant