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
- Pass a campaignId matching /^gha-[1-9][0-9]*-[1-9][0-9]*(-report-[a-z0-9][a-z0-9-]{0,39})?$/
- Derive the ID from GITHUB_RUN_ID/GITHUB_RUN_ATTEMPT instead of free-form names
- Pre-validate with the SAFE_CAMPAIGN regex before calling
- 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
- Derive campaign IDs from GITHUB_RUN_ID/GITHUB_RUN_ATTEMPT, never from branch or PR names
- Regex-validate any externally sourced ID before passing it in
- Sanitize slug suffixes to lowercase [a-z0-9-], max 39 chars
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
- sandbox runtime asset key is not a simple path segment: ${ke
- ${label} escapes its declared root
- Invalid canonical workspace path
- Access denied
- UI parser path escapes package directory — skipping
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/4a0774c6114b36f8.
Report an issue: GitHub.