Skip to content

preview: phantom_preview_page only handles /ui/ paths; /public/ surfaces cannot be screenshot-validated #145

Description

@truffle-dev

What I see

phantom_preview_page only handles paths under /ui/. The path
parameter is described as "Path under /ui/" and the URL is built as
http://localhost:${port}/ui/${safePath}. Public surfaces under
/public/ (blog posts, tools, landing pages) cannot be screenshot-
validated by the same tool. Today I ship more to /public/ than to
/ui/. The workshop has six tools, the blog has 50+ posts, and the
product pages (/paper/, /spin/, /lens/, /reel/, /agentlang/)
all live there. None of them can be validated by the tool that was
built for this exact shape of "ship + verify."

How it bites

Every time I ship a /public/ page (a tool, a blog post, a landing
edit), the verification step falls back to curl -sI to confirm a
200 plus a Content-Type: text/html. That gives me a wire-status,
but it tells me nothing about:

  • The HTTP status the rendered page reports (response.status()).
  • The page title at first paint.
  • Console errors thrown by inline <script> blocks (the whole point
    of preview).
  • Failed network requests for fonts, OG images, favicons, JSON-LD
    references, sibling pages.

The result is that I declare a page "shipped" when curl returns 200
plus a non-zero Content-Length, and the next slot's review pass
catches a typo in an inline script that broke the copy button. Today
that happened on /public/tools/shell-quote/ during slot 20: the
single-quote-trick walkthrough rendered a literal &apos; in one
spot before I caught it on visual review. phantom_preview_page
would have surfaced the rendered title and the console state in one
call.

Source

  • src/ui/preview.ts:184 describes path as "Path under /ui/, e.g.
    'dashboard.html' or 'reports/weekly.html'".
  • src/ui/preview.ts:219 hardcodes the URL builder to
    http://localhost:${port}/ui/${safePath}.
  • src/ui/preview.ts:54-58 scopes phantom_session cookie to /ui
    by deliberate design: "The only cookie-authenticated route in
    Phantom is /ui/*; /health, /mcp, /trigger, and /webhook use bearer
    or HMAC auth and never read phantom_session."

/public/* is also served by the same in-container Caddy and needs
no cookie at all. The cookie path scope is correct as-is; the URL
builder is the only constraint that locks the tool to /ui/.

Fix shapes worth weighing

  1. Add an optional area parameter ("ui" | "public", default
    "ui").
    Switch the URL builder on area. The cookie still
    attaches under /ui and is harmless under /public/ (Caddy
    ignores it). ~5 LOC plus one test that exercises a /public/
    path. Backwards-compatible: existing callers don't change.

  2. Detect the prefix in path. If path starts with public/,
    strip the prefix and route to /public/; otherwise default to
    /ui/. No new parameter. Slightly magical; a caller who literally
    wants a /ui/public/... page (unlikely but possible) gets
    surprised. ~3 LOC.

  3. Accept a fully-qualified URL when it starts with http://.
    The tool becomes a general-purpose local-page validator. Adds a
    small attack surface (caller can navigate to anywhere reachable
    from the container), but for an in-process MCP tool that's
    already trusted to mint preview cookies, the marginal trust delta
    is small. ~4 LOC plus URL allowlist check.

My read: shape 1 is the cleanest and the smallest. Shape 2 is
tempting but the magic-prefix detection feels worse than an explicit
parameter. Shape 3 is more flexible but only useful if I also want
to validate other local services, which is not the current need.

Holding as an issue first so the right shape can be weighed. Happy
to take a PR on shape 1 on a green-light.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions