paperclipai/paperclip · error
invalid_cloud_control_assertion
invalid_cloud_control_assertion
Error message
invalid_cloud_control_assertion
What it means
cloudControlMiddleware verified the request targets the correct endpoint, but verifyCloudControlAssertion rejected the compact JWS assertion — bad signature, expired token, wrong expected action, malformed JWS, or untrusted key. The middleware logs the rejection (without echoing the token) and returns 401 with code invalid_cloud_control_assertion, refusing to grant the 'paperclip-cloud' board actor.
Solutions
- Regenerate a fresh assertion from Paperclip Cloud and retry (rules out expiry)
- Ensure the assertion's action claim matches the HTTP method used (per ACTION_BY_METHOD)
- Verify instance/cloud clocks are synchronized (NTP) and the cloud signing keys are current on the instance
- Inspect server logs for the 'Rejected Cloud control assertion' warning to see the underlying verification error
Example fix
// before: reusing an old cached assertion
const assertion = cachedAssertion;
// after: mint per-request
const assertion = signCloudControlAssertion({ action: expectedAction, ttlSeconds: 60 }); Defensive patterns
Strategy: retry
Validate before calling
// before sending, mint a fresh, short-lived assertion for the exact action
const assertion = signCloudControlAssertion({ action: expectedAction, issuedAt: Date.now(), ttlSeconds: 60 });
if (decodeJwt(assertion).exp * 1000 < Date.now()) throw new Error("assertion already expired"); Try / catch
let res = await send(assertion);
if (res.status === 401 && res.body?.error === "invalid_cloud_control_assertion") {
res = await send(signCloudControlAssertion({ action: expectedAction })); // one fresh-token retry
} Prevention
- Mint a new assertion per request with a short TTL
- Keep clocks synchronized (NTP) between cloud and instance
- Ensure the action claim matches the HTTP method used
- Re-sync cloud signing keys on the instance after rotation
When it happens
Trigger: An assertion sent to /api/instance/task-drain fails verifyCloudControlAssertion: signature mismatch, assertion expired, assertion's action claim does not match the expectedAction for the HTTP method, or the token is structurally invalid.
Common situations: Clock skew between cloud and instance causing expiry; the cloud signed the assertion for one action but the request used another method; key rotation on the cloud side not yet picked up by the instance; truncated/mangled assertion in a proxy or header.
Related errors
- invalid_cloud_runtime_identity
- cloud_control_wrong_endpoint
- Cloud runtime identity assertion is expired or has an…
- conversation_turn_cancelled
- Cloud control assertion does not authorize this action
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/62c8a211d46e2d53.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/middleware/cloud-control.ts:47
return (req, res, next) => {
const assertion = req.get(CLOUD_CONTROL_HEADER)?.trim();
if (!assertion) {
next();
return;
}
const expectedAction = ACTION_BY_METHOD[req.method];
// Express's non-strict routing treats a trailing slash as the same
// route; the endpoint check must agree with it.
const normalizedPath = req.path.length > 1 && req.path.endsWith("/") ? req.path.slice(0, -1) : req.path;
if (normalizedPath !== "/api/instance/task-drain" || !expectedAction) {
res.status(400).json({ error: "cloud_control_wrong_endpoint" });
return;
}
try {
verifyCloudControlAssertion({ compactJws: assertion, expectedAction });
} catch (error) {
logger.warn({ err: error }, "Rejected Cloud control assertion");
res.status(401).json({ error: "invalid_cloud_control_assertion" });
return;
}
req.actor = {
type: "board",
userId: "paperclip-cloud",
userName: "Paperclip Cloud",
userEmail: null,
isInstanceAdmin: true,
source: "cloud_control",
};
next();
};
}
View on GitHub (pinned to 3f1d897a7c)