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

  1. Regenerate the report and copy the full viewer/ directory from the same trustedViewerFiles(viewerRoot) build used at publish time.
  2. Ensure PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR points to the viewer build matching the report's generation run.
  3. Remove any stale viewer/ directory before copying the fresh one so hashed asset names stay consistent.
  4. 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

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


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