paperclipai/paperclip · error · Error
Unexpected trusted viewer asset
Error message
Unexpected trusted viewer asset
What it means
trustedViewerFiles() reads a pre-built trusted viewer bundle (index.html plus an assets/ directory) that will be published as a public eval report. Because that bundle is served publicly under a strict CSP, every asset entry must be a regular, non-symlink file whose name matches a strict allowlist regex (ASCII-alphanumeric start, then [A-Za-z0-9._-], ending in .js, .css, or .woff2). If any directory entry fails that check, the function throws "Unexpected trusted viewer asset" to stop potentially unsafe or unexpected files from being shipped.
Source
Thrown at packages/paperclip-runner/scripts/public-eval-viewer.mjs:45
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");
files.set(
`viewer/assets/${entry.name}`,
await readFile(join(viewerRoot, "assets", entry.name)),
);
}
if (
![...files.keys()].some((name) => name.endsWith(".js")) ||
!index.includes('<script type="module"')
)
throw new Error("Incomplete trusted viewer build");
return { index, files };
}
export function validatePublicChatPayload(payload) {
if (
payload?.publication?.schema !== PUBLIC_CHAT_SCHEMA ||
payload.view?.sessionId !== "public-report" ||
payload.view?.composer?.state !== "disabled" ||View on GitHub (pinned to 01ad858492)
Solutions
- Remove or move out any files in viewerRoot/assets that do not match ^[A-Za-z0-9][A-Za-z0-9._-]*\.(js|css|woff2)$ (especially .map sourcemaps and dotfiles).
- Rebuild the viewer with sourcemaps disabled (e.g. build.sourcemap: false in the Vite config) so only .js/.css/.woff2 are emitted.
- Replace any symlinks or subdirectories inside assets/ with real regular files flattened into assets/.
- If a new asset type is genuinely required, update the ASSET regex in public-eval-viewer.mjs and get the change reviewed as a CSP/publication surface change.
Example fix
// before: assets/ contains app.js, app.js.map, .DS_Store // after: clean the build output so only allowlisted assets remain rm viewer/assets/app.js.map viewer/assets/.DS_Store pnpm --filter @paperclipai/paperclip-runner build:viewer
Defensive patterns
Strategy: validation
Validate before calling
import { readdir } from "node:fs/promises";
const ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css|woff2)$/;
const bad = (await readdir(join(root, "assets"))).filter((n) => !ASSET.test(n));
if (bad.length) throw new Error(`Non-publishable assets: ${bad.join(", ")}`); Type guard
const isPublishableAsset = (entry) => entry.isFile() && !entry.isSymbolicLink() && /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css|woff2)$/.test(entry.name);
Prevention
- Disable sourcemaps and other extra artifacts in the viewer build config
- Never hand-place files into the viewer assets/ directory; treat it as build output only
- Add a CI step that lists assets/ and fails on non-.js/.css/.woff2 entries before publishing
- Copy build output with dereferencing (real files, no symlinks)
When it happens
Trigger: Calling trustedViewerFiles(viewerRoot) when the assets/ directory contains: a subdirectory, a symlink, a file with a disallowed extension (e.g. .map, .png, .txt), a file starting with a dot or non-ASCII/odd character, or a name with characters outside [A-Za-z0-9._-].
Common situations: Vite build outputs sourcemaps (.js.map) or license files into assets/; a developer drops a README or screenshot into assets/; a misconfigured build with assetsInclude emits images/fonts outside the allowlist; symlinks left by a copied node_modules-based build.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- Incomplete trusted viewer build
- Codex working directory cannot be a filesystem root
- Invalid canonical workspace path
- ${label} is not a regular file at ${canonical}.
- sandbox runtime asset key is not a simple path segment: ${ke
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/86351e8c0e8baeec.
Report an issue: GitHub.