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

  1. Mint a fresh assertion whose action claim exactly equals the expectedAction of the endpoint you are calling (one assertion per action)
  2. Check spelling/case of the action string against the CLOUD_CONTROL_ACTIONS allowlist in cloud-runtime-identity.ts
  3. If a genuinely new action is needed, add it to CLOUD_CONTROL_ACTIONS in code and redeploy, then mint tokens for it
  4. 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

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


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 a

View on GitHub (pinned to 3f1d897a7c)