paperclipai/paperclip · error

Warm transition bootstrap snapshot proof is not authorized.

Error message

Warm transition bootstrap snapshot proof is not authorized.

What it means

This error is thrown by the warm-transition bootstrap authorization check in the durable PRP control plane. Before issuing a bootstrap snapshot for a warm run transition, the code verifies that a proof exists, that the requested transitionId matches the transitionId embedded in the proof's transition receipt, and that the requested TTL is an integer between 1 and 60 seconds. If any precondition fails, the bootstrap request is rejected as unauthorized.

Source

Thrown at packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts:1784

    this.#store.assertWritable();
    const pending = input.runnerState.warmTransition;
    const proof = warmTransitionRecoveryProof({
      controlPlaneState: this.#store.state,
      runnerState: input.runnerState,
      expectedNewIdentity: (isRecord(pending) && isRecord(pending.receipt)
        ? pending.receipt.newIdentity
        : null) as DurableRecoveryIdentity,
      expectedRunnerVersion: this.#expectedRunnerVersion,
      expectedRunnerDigest: this.#expectedRunnerDigest,
    });
    if (
      !proof ||
      input.transitionId !== proof.transition.receipt.transitionId ||
      !Number.isInteger(ttlMs) ||
      ttlMs < 1_000 ||
      ttlMs > 60_000
    ) {
      throw new Error(
        "Warm transition bootstrap snapshot proof is not authorized.",
      );
    }
    const { transition, original, requested } = proof;
    const ticket = `bootstrap_${randomUUID()}`;
    const material = credentialMaterial(ticket);
    const expiresAtUnixMs = Math.min(
      Date.now() + ttlMs,
      original.expiresAtUnixMs,
    );
    const candidate = structuredClone(this.#store.state);
    candidate.schema = transitionCoreStateSchema;
    candidate.warmTransition = structuredClone(transition);
    candidate.tickets[material.credentialId] = {
      recordId: `bootstrap_ticket_${randomUUID()}`,
      credentialId: material.credentialId,
      authKeyDigest: `sha256:${material.authKey.toString("hex")}`,
      identity: structuredClone(requested),

View on GitHub (pinned to 01ad858492)

Solutions

  1. Ensure the proof object passed to the bootstrap call is the exact proof issued for the same transitionId, not one from an earlier transition.
  2. Set ttlMs to an integer number of milliseconds in the 1000-60000 range (1-60 seconds).
  3. Re-fetch/rebuild the transition proof if the transition was re-created after a restart or retry.
  4. Log proof.transition.receipt.transitionId and input.transitionId side by side to find the mismatch.

Example fix

// before
await controlPlane.warmTransitionBootstrap({ transitionId: oldId, proof: staleProof, ttlMs: 120000 });
// after
await controlPlane.warmTransitionBootstrap({ transitionId: proof.transition.receipt.transitionId, proof, ttlMs: 30000 });
Defensive patterns

Strategy: validation

Validate before calling

function canBootstrap(input) {
  const ttlMs = input.ttlMs;
  return Boolean(input.proof) &&
    input.transitionId === input.proof.transition.receipt.transitionId &&
    Number.isInteger(ttlMs) && ttlMs >= 1000 && ttlMs <= 60000;
}
if (!canBootstrap(input)) throw new Error("invalid warm transition bootstrap input");

Type guard

function hasValidProof(input) {
  return input.proof != null && input.transitionId === input.proof.transition.receipt.transitionId;
}

Try / catch

try {
  await controlPlane.warmTransitionBootstrap(input);
} catch (err) {
  if (err.message.includes("not authorized")) {
    // refetch proof and re-validate ttlMs
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the warm-transition bootstrap path with proof=null/undefined, with a transitionId that differs from proof.transition.receipt.transitionId, or with ttlMs that is not an integer or is <1000 or >60000.

Common situations: Passing a stale proof from a previous transition, constructing the proof manually with a mismatched transitionId, misconfiguring TTL in milliseconds vs seconds (e.g. passing 120000 thinking it is seconds), or a race where a newer transition invalidated the cached proof.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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