paperclipai/paperclip · error
Cloud control assertion does not authorize this action
Error message
Cloud control assertion does not authorize this action
What it means
verifyCloudControlAssertion enforces least privilege: the payload's action claim must be a member of the CLOUD_CONTROL_ACTIONS allowlist and must exactly equal the expectedAction the endpoint demands. This error means the assertion is otherwise valid but does not authorize the operation being attempted.
Solutions
- Mint a fresh assertion whose action claim exactly equals the expectedAction of the endpoint you are calling (one assertion per action)
- Check spelling/case of the action string against the CLOUD_CONTROL_ACTIONS allowlist in cloud-runtime-identity.ts
- If a genuinely new action is needed, add it to CLOUD_CONTROL_ACTIONS in code and redeploy, then mint tokens for it
- Stop replaying a single broad assertion across endpoints; generate per-request assertions
Example fix
// before
const assertion = mintAssertion({ sub: stackId, action: "logs", requestId });
await control("restart", assertion);
// after
const assertion = mintAssertion({ sub: stackId, action: "restart", requestId: crypto.randomUUID() });
await control("restart", assertion); Defensive patterns
Strategy: validation
Validate before calling
const ACTIONS = [/* mirror of CLOUD_CONTROL_ACTIONS */];
function authorizeBeforeMint(action, expectedAction) {
if (!ACTIONS.includes(action)) throw new Error(`action ${action} not in allowlist`);
if (action !== expectedAction) throw new Error(`mint action ${action} but endpoint expects ${expectedAction}`);
}
authorizeBeforeMint(minted.action, "restart"); Try / catch
try {
return verifyCloudControlAssertion({ compactJws: token, expectedAction });
} catch (e) {
if (e.message === "Cloud control assertion does not authorize this action") {
return respond(403, "insufficient action scope"); // mint a fresh assertion for this specific action
}
throw e;
} Prevention
- Mint one assertion per request/action; never reuse a token across different control endpoints
- Import the action allowlist from shared constants instead of hardcoding strings
- Add an integration test per (action, endpoint) pair
When it happens
Trigger: Calling an endpoint expecting action "restart" with an assertion minted for "logs" or "status"; the action claim contains a value not in CLOUD_CONTROL_ACTIONS (e.g. a typo'd or invented action); an assertion reused across different control endpoints with different expectedAction values.
Common situations: A control client caches one assertion and reuses it for multiple operations; an action name changed server-side while clients still mint the old name; case/spacing differences ("Restart" vs "restart"); a new control operation was added but not yet added to CLOUD_CONTROL_ACTIONS.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Cloud control assertion is expired or has an invalid…
- Cloud control assertion is not a compact JWS
- Cloud control assertion request id is invalid
- Cloud control assertion stack does not match this instance
- Cloud control claims are incomplete
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/2acc3d102073380a.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/cloud-runtime-identity.ts:555
|| payload.aud !== CLOUD_CONTROL_AUDIENCE
|| typeof payload.sub !== "string"
|| typeof payload.action !== "string"
|| typeof payload.requestId !== "string"
|| typeof payload.iat !== "number"
|| !Number.isInteger(payload.iat)
|| typeof payload.exp !== "number"
|| !Number.isInteger(payload.exp)
) {
throw new Error("Cloud control claims are incomplete");
}
if (!configuredStackId || payload.sub !== configuredStackId) {
throw new Error("Cloud control assertion stack does not match this instance");
}
if (
!(CLOUD_CONTROL_ACTIONS as readonly string[]).includes(payload.action)
|| payload.action !== input.expectedAction
) {
throw new Error("Cloud control assertion does not authorize this action");
}
if (
!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 aView on GitHub (pinned to 3f1d897a7c)