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
- Mint a fresh assertion with a new requestId for each cloud-control request
- If retrying, request a new assertion from the control plane before re-sending
- Synchronize clocks (NTP) and ensure assertion lifetimes exceed plausible retry windows only via new requestIds, not reuse
- 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
- Always mint a new assertion (new requestId) per request; never cache one across calls
- Configure retry layers (proxies, LBs) to re-request an assertion, not replay it
- Keep clocks NTP-synced so exp/skew logic behaves predictably
- Log requestId on failure to confirm whether a duplicate send happened
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
- Cloud control assertion is expired or has an invalid…
- Cloud control assertion request id is invalid
- A trusted viewer build is required for public chat reports
- Access denied
- ACPX runtime executable changed while it was verified
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)