{"record":{"id":"d2cc3d8646066c02","repo":"paperclipai/paperclip","slug":"warm-transition-recovery-requires-its-explicit-one","errorCode":null,"errorMessage":"Warm transition recovery requires its explicit one-use bootstrap capability.","messagePattern":"Warm transition recovery requires its explicit one-use bootstrap capability\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":1731,"sourceCode":"      throw new Error(\"Durable PRP run attachment template is invalid.\");\n    }\n    const existing = this.#store.state.runAttachTemplate;\n    if (\n      existing !== undefined &&\n      existing !== null &&\n      canonicalJson(existing) !== canonicalJson(runAttachTemplate)\n    ) {\n      throw new Error(\"Durable PRP run attachment template conflicts.\");\n    }\n    if (existing !== undefined && existing !== null) return;\n    this.#store.state.runAttachTemplate = structuredClone(runAttachTemplate);\n    this.#store.save();\n  }\n\n  issueBootstrapTicket(ttlMs = 5_000): string {\n    this.#store.assertWritable();\n    if (this.#store.state.warmTransition) {\n      throw new Error(\n        \"Warm transition recovery requires its explicit one-use bootstrap capability.\",\n      );\n    }\n    if (!Number.isInteger(ttlMs) || ttlMs < 1_000 || ttlMs > 60_000) {\n      throw new Error(\"Durable PRP bootstrap TTL is invalid.\");\n    }\n    this.#pruneCredentials();\n    const ticket = `bootstrap_${randomUUID()}`;\n    const material = credentialMaterial(ticket);\n    const expiresAtUnixMs = Date.now() + ttlMs;\n    this.#store.state.tickets[material.credentialId] = {\n      recordId: `bootstrap_ticket_${randomUUID()}`,\n      credentialId: material.credentialId,\n      authKeyDigest: `sha256:${material.authKey.toString(\"hex\")}`,\n      identity: structuredClone(this.#identity),\n      runnerVersion: this.#expectedRunnerVersion,\n      runnerDigest: this.#expectedRunnerDigest,\n      expiresAt: new Date(expiresAtUnixMs).toISOString(),","sourceCodeStart":1713,"sourceCodeEnd":1749,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L1713-L1749","documentation":"issueBootstrapTicket() refuses to mint a fresh bootstrap capability while a warmTransition is present in the durable store. During warm-transition recovery, the only sanctioned entry is the explicit one-use bootstrap capability tied to that transition; issuing a new generic ticket would bypass the authenticated handoff, so the call is rejected.","triggerScenarios":"Calling issueBootstrapTicket() while store.state.warmTransition is set — e.g. an operator restarts the runner and the bootstrap code unconditionally issues a new ticket instead of using the transition's existing capability.","commonSituations":"Crash/restart during a warm transition leaves the transition state behind and generic bootstrap logic runs; a second runner instance tries to bootstrap into a run mid-handoff; recovery scripts don't check for an active transition.","solutions":["Use the explicit one-use bootstrap capability issued for the warm transition instead of calling issueBootstrapTicket().","Check store.state.warmTransition before bootstrapping and branch to the transition-recovery path when present.","If the transition is abandoned and truly unrecoverable, clear the warmTransition state through the documented recovery/reset flow first, then issue a ticket.","Do not delete transition state ad hoc (e.g. by editing the store) — it exists to authenticate the new identity."],"exampleFix":"// before\nconst ticket = controlPlane.issueBootstrapTicket(); // throws mid-transition\n// after\nif (store.state.warmTransition) {\n  await recoverViaTransitionCapability();\n} else {\n  const ticket = controlPlane.issueBootstrapTicket();\n}","handlingStrategy":"type-guard","validationCode":"if (store.state.warmTransition) {\n  await recoverViaTransitionCapability(); // use the transition's one-use bootstrap capability\n} else {\n  controlPlane.issueBootstrapTicket();\n}","typeGuard":"function inWarmTransition(state: { warmTransition?: unknown }): boolean {\n  return state.warmTransition != null;\n}","tryCatchPattern":"try {\n  ticket = controlPlane.issueBootstrapTicket();\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"one-use bootstrap capability\")) {\n    ticket = await useTransitionBootstrapCapability(); // sanctioned path\n  } else throw err;\n}","preventionTips":["Always branch bootstrap logic on the presence of warmTransition state.","Persist and reuse the transition's bootstrap capability across restarts.","Never clear transition state to unblock ticket issuance; use the documented recovery flow."],"tags":["security","warm-transition","bootstrap","state-machine"],"backgroundTag":"invalid-state-transition","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"}