paperclipai/paperclip · error

Cloud control assertion has already been used

Error message

Cloud control assertion has already been used

What it means

verifyCloudControlAssertion validates a signed cloud-control assertion (claims + lifetime + requestId). After all semantic checks pass, it calls consumeControlRequestId to atomically burn the assertion's request id with an expiry of payload.exp + MAX_CLOCK_SKEW_SECONDS. If that id was already consumed (replay), it throws 'Cloud control assertion has already been used' — a replay-protection guard so each assertion authorizes exactly one request.

Solutions

  1. Mint a fresh assertion with a new requestId for each cloud-control request
  2. If retrying, request a new assertion from the control plane before re-sending
  3. Synchronize clocks (NTP) and ensure assertion lifetimes exceed plausible retry windows only via new requestIds, not reuse
  4. Check that no middleware/duplex layer double-invokes cloudControlMiddleware for one request

Example fix

// before
await fetch(url, { headers: { authorization: `Bearer ${cachedAssertion}` } });
await fetch(url, { headers: { authorization: `Bearer ${cachedAssertion}` } }); // replay
// after
await fetch(url, { headers: { authorization: `Bearer ${await mintAssertion()}` } }); // fresh requestId per call
Defensive patterns

Strategy: try-catch

Validate before calling

// client-side: track used requestIds you generated
if (usedRequestIds.has(assertion.requestId)) {
  assertion = await mintAssertion(); // mint fresh before sending
}

Type guard

function isAssertionReplayError(e: unknown): e is Error {
  return e instanceof Error && /assertion has already been used/.test(e.message);
}

Try / catch

try {
  await cloudControlCall(assertion);
} catch (e) {
  if (isAssertionReplayError(e)) {
    const fresh = await mintAssertion();
    return cloudControlCall(fresh); // do NOT retry the consumed one
  }
  throw e;
}

Prevention

When it happens

Trigger: Reusing the same signed assertion for a second cloud-control API call; a client retrying a request with the identical assertion after a first (even successful or failed-late) attempt already consumed the requestId; two concurrent requests sharing one assertion racing on consumeControlRequestId.

Common situations: Client-side retry logic that replays the same assertion instead of minting a fresh one; load balancer/proxy retries duplicating a request; clocks far out of skew causing clients to believe an assertion is still valid; test harnesses caching a single assertion across multiple calls.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/f24520f37c410c0f. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/cloud-runtime-identity.ts:576

    !payload.requestId
    || payload.requestId.trim() !== payload.requestId
    || payload.requestId.length > 256
  ) {
    throw new Error("Cloud control assertion request id is invalid");
  }
  if (
    payload.exp <= nowSeconds
    || payload.iat > nowSeconds + MAX_CLOCK_SKEW_SECONDS
    || payload.exp <= payload.iat
    || payload.exp - payload.iat > CLOUD_CONTROL_MAX_LIFETIME_SECONDS
  ) {
    throw new Error("Cloud control assertion is expired or has an invalid lifetime");
  }
  // Consumed LAST, only after every other check passed: a rejected
  // assertion must not burn its request id, or an attacker could deny a
  // legitimate call by replaying a mangled copy of it first.
  if (!consumeControlRequestId(payload.requestId, payload.exp + MAX_CLOCK_SKEW_SECONDS, now.getTime())) {
    throw new Error("Cloud control assertion has already been used");
  }
  return payload as CloudControlClaims;
}

View on GitHub (pinned to 3f1d897a7c)