{"record":{"id":"348e55c1786cb580","repo":"paperclipai/paperclip","slug":"actoruserid-userid-has-viewer-read-only-acc","errorCode":null,"errorMessage":"actorUserId \"${userId}\" has viewer (read-only) access and cannot take this write action","messagePattern":"actorUserId \"(.+?)\" has viewer \\(read-only\\) access and cannot take this write action","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/plugin-host-services.ts","lineNumber":908,"sourceCode":"    companyId: string,\n    userId: string,\n    { allowViewer = false }: { allowViewer?: boolean } = {},\n  ): Promise<void> => {\n    const [membership] = await db\n      .select({ id: companyMemberships.id, membershipRole: companyMemberships.membershipRole })\n      .from(companyMemberships)\n      .where(and(\n        eq(companyMemberships.companyId, companyId),\n        eq(companyMemberships.principalType, \"user\"),\n        eq(companyMemberships.principalId, userId),\n        eq(companyMemberships.status, \"active\"),\n      ))\n      .limit(1);\n    if (!membership) {\n      throw new Error(`actorUserId \"${userId}\" is not an active human member of this company`);\n    }\n    if (!allowViewer && membership.membershipRole === \"viewer\") {\n      throw new Error(`actorUserId \"${userId}\" has viewer (read-only) access and cannot take this write action`);\n    }\n  };\n\n  /**\n   * Wake the assignee of a continuation issue after a plugin-relayed board-user\n   * interaction resolution, mirroring the web app's board interaction routes\n   * (routes/issues.ts's queueResolvedInteractionContinuationWakeup) so a\n   * confirmation accepted/rejected from chat resumes the agent the same way it\n   * would from the web app. Deliberately narrower than the HTTP helper: it\n   * carries the core interaction context plus the plan-review continuation\n   * payload (the confirmation kind the gateway resolves), and skips the\n   * checkbox/tool-action/item-verdict extras that the gateway's yes/no decision\n   * cards never produce. Failure-tolerant: a wake failure is logged, never\n   * thrown back to the plugin (the decision itself already applied).\n   */\n  const queuePluginInteractionContinuationWakeup = (args: {\n    issue: { id: string; assigneeAgentId: string | null; status: string };\n    interaction: {","sourceCodeStart":890,"sourceCodeEnd":926,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/services/plugin-host-services.ts#L890-L926","documentation":"The actorUserId is an active member but has viewer (read-only) role, and the operation was called without { allowViewer: true }. The permission guard blocks viewer principals from write actions on behalf of board users.","triggerScenarios":"Thrown at server/src/services/plugin-host-services.ts:899 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Use an actorUserId with an editor/admin role for write actions; viewer-role users can only perform read operations. Ask a company admin to elevate the user's role if writes are required."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}