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
- Build the viewer first so <viewerRoot>/index.html and assets/ exist, and pass that directory as viewerRoot.
- Check the flag/env var supplying viewerRoot for typos or an unset value; pass an absolute path.
- Replace any symlink with the real directory (copy/bind-mount), since symlinks are rejected for security.
- 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
- Run the viewer build as a required CI step before generating public reports.
- Pass an absolute, verified path to viewerRoot (fail fast if the env var is unset).
- Never point viewerRoot at a symlink; stage real copies.
- Smoke-check index.html and assets/ exist before invoking.
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
- Private state directory is not a real directory: ${diagnosti
- Materialized OpenCode executable has unsafe permissions
- Trusted viewer must not use symlinks
- Unexpected trusted viewer asset
- Refusing public report symlink ${absolute}
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/d4fc06cb557252a6.
Report an issue: GitHub.