paperclipai/paperclip · error · Error

Unsafe historical campaign ID

Error message

Unsafe historical campaign ID

What it means

While enriching history, the publisher downloads each historical campaign.json from S3; before doing so it re-validates every campaign ID against SAFE_CAMPAIGN so a tampered or malformed ID in history.json cannot be turned into an arbitrary S3 key or temp filename. An ID failing the gha-N-N(-report-slug) pattern triggers this error.

Source

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

  const temporary = await mkdtemp(
    join(tmpdir(), "runner-protocol-eval-history-"),
  );
  const historyKey = `${validatedDestination.prefix}/history.json`;
  const mergedHistory = mergeProtocolEvalHistory(
    (await downloadJson(
      validatedDestination.bucket,
      historyKey,
      join(temporary, "history.json"),
    )) ?? emptyProtocolEvalHistory(),
    protocolEvalHistoryRecord(
      campaign,
      `${validatedDestination.publicBaseUrl}/${validatedDestination.prefix}`,
    ),
  );
  const history = await enrichProtocolEvalHistory(mergedHistory, {
    currentCampaign: campaign,
    loadCampaign: async (id) => {
      if (!SAFE_CAMPAIGN.test(id)) throw new Error("Unsafe historical campaign ID");
      return downloadJson(validatedDestination.bucket,
        `${validatedDestination.prefix}/campaigns/${id}/campaign.json`,
        join(temporary, `${id}.json`));
    },
  });
  const campaignPrefix = `${validatedDestination.prefix}/campaigns/${campaign.campaignId}`;
  const manifestKey = `${campaignPrefix}/bundle-manifest.json`;
  const existing = await downloadJson(
    validatedDestination.bucket,
    manifestKey,
    join(temporary, "existing-manifest.json"),
  );
  if (existing && existing.bundleDigest !== manifest.bundleDigest) {
    throw new Error(
      `Immutable campaign ${campaign.campaignId} already exists with a different digest`,
    );
  }
  if (!existing) {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Inspect s3://<bucket>/<prefix>/history.json and find the campaign entry with the non-conforming campaignId
  2. Remove or migrate that entry to a SAFE_CAMPAIGN-compliant ID, or reset history.json to a fresh v1 document
  3. Regenerate the offending campaign record through the current publisher so it gets a valid ID
  4. If it came from enrichment/analytics data, fix the upstream source of the ID

Example fix

// before (history.json)
{ "campaignId": "../../etc", ... }
// after (history.json)
{ "campaignId": "gha-12-1", ... }
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})?$/;
for (const c of history.campaigns)
  if (!SAFE_CAMPAIGN.test(c.campaignId)) throw new Error(`remote history has unsafe id: ${c.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 publishProtocolEvalHistory({ reportRoot, destination, viewerRoot });
} catch (e) {
  if (String(e.message).includes("Unsafe historical campaign ID")) {
    console.error("download history.json, fix/remove the malformed campaignId, and re-upload");
  }
  throw e;
}

Prevention

When it happens

Trigger: history.json in the bucket containing a campaign entry with a malformed campaignId (hand-edited, produced by a bug, or from an older schema); injection attempts via IDs containing slashes, dot-dot segments, or unusual characters stored in the remote history.

Common situations: A legacy history.json with differently-formatted IDs from before the SAFE_CAMPAIGN convention; manual edits to history.json; corrupted analytics/enrichment data feeding IDs into loadCampaign.

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/aa738a45dbc7c823. Report an issue: GitHub.