paperclipai/paperclip · error · Error

A trusted viewer build is required for public chat reports

Error message

A trusted viewer build is required for public chat reports

What it means

trustedViewerFiles prepares a viewer build for public chat reports and requires a real, pre-built, trusted viewer directory: an existing non-symlink directory. A missing, non-directory, or symlinked root means no trusted build is available, so serving public reports is refused.

Source

Thrown at packages/paperclip-runner/scripts/public-eval-viewer.mjs:27

const ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css|woff2)$/;

export function publicViewerShell(index, encodedPayload) {
  return index
    .replaceAll('"./assets/', '"../../viewer/assets/')
    .replace(
      "<head>",
      `<head>\n    <meta http-equiv="Content-Security-Policy" content="${PUBLIC_VIEWER_CSP}">`,
    )
    .replace(
      '<script type="module"',
      `<script type="application/json" id="paperclip-eval-report">${encodedPayload}</script>\n    <script type="module"`,
    );
}

export async function trustedViewerFiles(viewerRoot) {
  const rootStat = viewerRoot ? await lstat(viewerRoot) : null;
  if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory())
    throw new Error(
      "A trusted viewer build is required for public chat reports",
    );
  const indexStat = await lstat(join(viewerRoot, "index.html"));
  const assetsStat = await lstat(join(viewerRoot, "assets"));
  if (
    indexStat.isSymbolicLink() ||
    !indexStat.isFile() ||
    assetsStat.isSymbolicLink() ||
    !assetsStat.isDirectory()
  )
    throw new Error("Trusted viewer must not use symlinks");
  const index = await readFile(join(viewerRoot, "index.html"), "utf8");
  const files = new Map();
  for (const entry of await readdir(join(viewerRoot, "assets"), {
    withFileTypes: true,
  })) {
    if (!entry.isFile() || entry.isSymbolicLink() || !ASSET.test(entry.name))
      throw new Error("Unexpected trusted viewer asset");

View on GitHub (pinned to 01ad858492)

Solutions

  1. Build the viewer first so <viewerRoot>/index.html and assets/ exist, and pass that directory as viewerRoot.
  2. Check the flag/env var supplying viewerRoot for typos or an unset value; pass an absolute path.
  3. Replace any symlink with the real directory (copy/bind-mount), since symlinks are rejected for security.
  4. Verify with 'ls <viewerRoot>/index.html <viewerRoot>/assets' before invoking.

Example fix

// before
await trustedViewerFiles(process.env.VIEWER_ROOT); // unset
// after
const viewerRoot = path.resolve(process.env.VIEWER_ROOT ?? "ui/dist");
await fs.access(join(viewerRoot, "index.html"));
await trustedViewerFiles(viewerRoot);
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises';
const st = await lstat(viewerRoot);
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error('viewerRoot must be a real built viewer directory');

Type guard

async function isTrustedViewerRoot(p) {
  try { const st = await lstat(p); return st.isDirectory() && !st.isSymbolicLink(); } catch { return false; }
}

Try / catch

try {
  const files = await trustedViewerFiles(viewerRoot);
} catch (err) {
  if (err.message === 'A trusted viewer build is required for public chat reports') {
    throw new Error(`Viewer build missing at ${viewerRoot}; run the viewer build step first`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling trustedViewerFiles with viewerRoot undefined/null, a path that does not exist, a path that is a file, or a symlinked path, when generating a public chat report.

Common situations: The viewer build step was skipped in CI so the dist directory doesn't exist; the env var/CLI flag pointing at the viewer build is unset or mistyped; the path was replaced by a symlink (rejected for security).

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/d4fc06cb557252a6. Report an issue: GitHub.