{"record":{"id":"95b861a8de49c9e1","repo":"paperclipai/paperclip","slug":"public-viewer-asset-differs-from-trusted-build","errorCode":null,"errorMessage":"Public viewer asset differs from trusted build: ${file}","messagePattern":"Public viewer asset differs from trusted build: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":171,"sourceCode":"    );\n  }\n  for (const file of files) {\n    if (!isPublicProtocolEvalPath(file)) {\n      throw new Error(\n        `Refusing non-allowlisted public protocol eval path ${file}`,\n      );\n    }\n    const absolute = resolve(root, ...file.split(\"/\"));\n    const metadata = await stat(absolute);\n    if (metadata.size === 0 || metadata.size > 12 * 1024 * 1024) {\n      throw new Error(\n        `Public protocol eval file exceeds its size boundary: ${file}`,\n      );\n    }\n    if (file.startsWith(\"viewer/\")) {\n      const expected = viewer?.files.get(file);\n      if (!expected || !expected.equals(await readFile(absolute)))\n        throw new Error(\n          `Public viewer asset differs from trusted build: ${file}`,\n        );\n      continue;\n    }\n    const content = await readFile(absolute, \"utf8\");\n    const richAttempt = /^attempts\\/[^/]+\\/index\\.html$/.test(file);\n    const payload = richAttempt\n      ? validatePublicViewerPage(content, viewer.index)\n      : null;\n    if (!richAttempt) {\n      for (const pattern of CREDENTIAL_PATTERNS) {\n        if (pattern.test(content))\n          throw new Error(\n            `Public report contains credential/session material: ${file}`,\n          );\n      }\n      if (extname(file) !== \".html\") continue;\n      for (const pattern of ACTIVE_HTML_PATTERNS) {","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L153-L189","documentation":"Files under viewer/ are compared byte-for-byte against a trusted viewer build (trustedViewerFiles). If a viewer file is missing from the trusted build or its bytes differ, the script aborts naming the file, ensuring only known-good, unmodified viewer assets are published.","triggerScenarios":"Publishing when a viewer/ file was modified after the trusted build (post-processing, minifier rerun, manual edit) or when the viewer/ directory content does not match the trusted viewerRoot used for comparison.","commonSituations":"Rebuilding the viewer with different tooling/settings than the trusted build; a script rewrote viewer asset hashes; stale viewer/ files from a previous build mixed with new ones; pointing the script at a different viewer build than the one copied into the report.","solutions":["Re-copy or rebuild the viewer assets from the same trusted viewerRoot the script compares against, then re-run.","Ensure no post-processing step modifies files under viewer/ after they are placed in the report.","Clear the report's viewer/ directory and regenerate so it exactly matches the trusted build."],"exampleFix":"# before: stale/edited viewer asset\npublic-report/viewer/app.js  (hand-edited)\n# after\n$ rm -rf public-report/viewer && cp -R trusted-viewer/dist public-report/viewer\n$ node scripts/publish-runner-protocol-eval-history.mjs ...","handlingStrategy":"validation","validationCode":"import { createHash } from 'node:crypto';\nimport { readdirSync, readFileSync } from 'node:fs';\nfor (const rel of listViewerFiles(reportRoot)) {\n  const trustedPath = `${trustedViewerRoot}/${rel.slice('viewer/'.length)}`;\n  const a = createHash('sha256').update(readFileSync(`${reportRoot}/${rel}`)).digest('hex');\n  const b = createHash('sha256').update(readFileSync(trustedPath)).digest('hex');\n  if (a !== b) throw new Error(`viewer asset differs from trusted build: ${rel}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await publishReport(root);\n} catch (err) {\n  if (String(err.message).startsWith('Public viewer asset differs from trusted build')) {\n    console.error('Restore viewer/ from the trusted build before publishing:', err.message);\n  } else throw err;\n}","preventionTips":["Copy viewer assets from the trusted build as the last step, then publish immediately with no intermediate mutation steps.","Never hand-edit or post-process files under viewer/ in the report directory.","Use the same viewer build (same viewerRoot) for generating and validating the report."],"tags":["integrity","checksum","publish-script","security"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}