paperclipai/paperclip · error · Error
Missing public viewer asset: ${file}
Error message
Missing public viewer asset: ${file} What it means
Thrown by validatePublicProtocolEvalReport when the report uses the chat rich-attempt viewer (detected via attempts/<id>/index.html pages) but one or more files required by the trusted viewer build (viewer.files from trustedViewerFiles(viewerRoot)) are absent from the report directory. The published bundle must ship the complete viewer asset set so the rich pages render; a partial viewer copy is rejected. The related check also rejects mixing legacy attempts/<id>.html pages into a chat evalbook.
Source
Thrown at packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs:226
root,
...file.split("/").slice(0, -1),
...clean.split("/"),
);
const rel = relative(root, target).split(sep).join("/");
if (
!isPublicProtocolEvalPath(rel) ||
!(await lstat(target).catch(() => null))?.isFile()
) {
throw new Error(
`Public report contains a broken or unsafe link in ${file}: ${href}`,
);
}
}
}
if (viewer) {
for (const file of viewer.files.keys())
if (!files.includes(file))
throw new Error(`Missing public viewer asset: ${file}`);
if (files.some((file) => /^attempts\/[^/]+\.html$/.test(file)))
throw new Error(
"Chat Evalbook must not mix in legacy plain attempt pages",
);
}
const campaign = await loadObject(join(root, "campaign.json"));
if (
campaign.schema !== "paperclip.runner-protocol-eval.campaign/v1" ||
!SAFE_CAMPAIGN.test(String(campaign.campaignId ?? ""))
) {
throw new Error("Public report campaign metadata is invalid");
}
return { files, campaign };
}
export async function createProtocolEvalBundleManifest(
reportRoot,
campaignId,View on GitHub (pinned to 01ad858492)
Solutions
- Regenerate the report and copy the full viewer/ directory from the same trustedViewerFiles(viewerRoot) build used at publish time.
- Ensure PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR points to the viewer build matching the report's generation run.
- Remove any stale viewer/ directory before copying the fresh one so hashed asset names stay consistent.
- If legacy attempts/<id>.html files exist alongside rich pages, delete or migrate them to attempts/<id>/index.html form.
Example fix
// before: partial copy aws s3 cp report/ s3://bucket/prefix/ --recursive --exclude "viewer/assets/*.woff2" // after: copy the full report including all viewer assets aws s3 cp report/ s3://bucket/prefix/ --recursive
Defensive patterns
Strategy: validation
Validate before calling
import { trustedViewerFiles } from "./public-eval-viewer.mjs";
import { readdir } from "node:fs/promises";
export async function assertViewerAssetsPresent(reportRoot, viewerRoot) {
const viewer = await trustedViewerFiles(viewerRoot);
const present = new Set(await readdir(`${reportRoot}/viewer/assets`));
for (const file of viewer.files.keys()) {
if (file.startsWith("viewer/") && !present.has(file.slice("viewer/assets/".length)))
throw new Error(`Missing viewer asset: ${file}`);
}
} Try / catch
try {
await validatePublicProtocolEvalReport(reportRoot, { viewerRoot });
} catch (err) {
if (String(err.message).startsWith("Missing public viewer asset")) {
console.error("Copy the complete viewer/ directory from the same build referenced by PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR.");
}
throw err;
} Prevention
- Always generate the report and copy viewer assets in the same CI step so hashed asset names match.
- Delete stale viewer/ directories before copying a fresh viewer build.
- Pin PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR to the run's viewer build, not a shared cached path.
- Avoid legacy attempts/<id>.html pages in chat evalbooks; use attempts/<id>/index.html only.
When it happens
Trigger: Publishing a report containing attempts/<id>/index.html pages where the viewer/ directory is missing some asset listed by the trusted viewer build (e.g. viewer/assets/app-abc123.js), typically because an old viewerRoot was used or the copy step skipped files.
Common situations: PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR points at a stale viewer build whose hashed asset names no longer match the copied viewer/ directory; the report was generated by a different run than the viewer build; an rsync/copy step excluded viewer assets; a manually assembled report omits some assets.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- Public report contains credential/session material: ${file}
- Public report contains active or remote HTML: ${file}
- Unsafe report navigation in ${file}
- Public report contains a broken or unsafe link in ${file}: $
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/c8bd76f6381dd02c.
Report an issue: GitHub.