paperclipai/paperclip · error · Error

Unsafe protocol eval campaign ID

Error message

Unsafe protocol eval campaign ID

What it means

createProtocolEvalBundleManifest validates the campaignId argument against SAFE_CAMPAIGN before doing any work, because the ID becomes an S3 key segment and a public URL path. An ID with path separators, dots, or unexpected characters is rejected as unsafe. The report's own campaign.json is validated separately, so this guard fires on the caller-supplied argument.

Source

Thrown at packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs:248

      );
  }
  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,
  { viewerRoot } = {},
) {
  if (!SAFE_CAMPAIGN.test(campaignId))
    throw new Error("Unsafe protocol eval campaign ID");
  const { files, campaign } = await validatePublicProtocolEvalReport(
    reportRoot,
    { viewerRoot },
  );
  if (campaign.campaignId !== campaignId)
    throw new Error("Report campaign ID does not match publication target");
  const entries = await Promise.all(
    files.map(async (file) => {
      const absolute = resolve(reportRoot, ...file.split("/"));
      const [content, metadata] = await Promise.all([
        readFile(absolute),
        stat(absolute),
      ]);
      return {
        path: file,
        sha256: createHash("sha256").update(content).digest("hex"),
        bytes: metadata.size,
      };

View on GitHub (pinned to 01ad858492)

Solutions

  1. Pass a campaignId matching /^gha-[1-9][0-9]*-[1-9][0-9]*(-report-[a-z0-9][a-z0-9-]{0,39})?$/
  2. Derive the ID from GITHUB_RUN_ID/GITHUB_RUN_ATTEMPT instead of free-form names
  3. Pre-validate with the SAFE_CAMPAIGN regex before calling
  4. Sanitize a slug suffix: lowercase, strip non [a-z0-9-], cap at 39 chars after 'report-'

Example fix

// before
await createProtocolEvalBundleManifest(reportRoot, process.env.CAMPAIGN_ID);
// after
const campaignId = `gha-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT ?? 1}`;
if (!/^gha-[1-9][0-9]*-[1-9][0-9]*(-report-[a-z0-9][a-z0-9-]{0,39})?$/.test(campaignId))
  throw new Error(`bad campaign id: ${campaignId}`);
await createProtocolEvalBundleManifest(reportRoot, campaignId);
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_CAMPAIGN = /^gha-[1-9][0-9]*-[1-9][0-9]*(-report-[a-z0-9][a-z0-9-]{0,39})?$/;
if (!SAFE_CAMPAIGN.test(campaignId)) throw new Error(`refusing unsafe campaign id: ${campaignId}`);

Type guard

const isSafeCampaignId = (v) => typeof v === "string" && /^gha-[1-9][0-9]*-[1-9][0-9]*(-report-[a-z0-9][a-z0-9-]{0,39})?$/.test(v);

Try / catch

try {
  await createProtocolEvalBundleManifest(reportRoot, campaignId);
} catch (e) {
  if (String(e.message).includes("Unsafe protocol eval campaign ID")) {
    throw new Error(`campaignId '${campaignId}' must match gha-<run>-<attempt>[-report-<slug>]`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling createProtocolEvalBundleManifest(reportRoot, campaignId) with a campaignId like '../evil', 'gha_1_1', 'gha-1-1/report', or an empty string; programmatic callers deriving the ID from an untrusted source.

Common situations: Wrapping the publisher in custom CI tooling and passing a raw branch/PR name as the campaign ID; typos in environment interpolation producing empty or malformed IDs.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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