paperclipai/paperclip · error · WatchdogDecisionApplicationError

not_authorized

not_authorized

Error message

Only the board or the assigned recovery owner can record watchdog decisions

What it means

Watchdog decisions may only be recorded by the board or by the evaluation issue's assigned recovery owner (the agent whose assigneeAgentId matches the actor's agentId on a live stale-active-run evaluation issue bound to the run). When neither condition holds, the use case throws code not_authorized. This enforces the single-assignee recovery-owner model.

Source

Thrown at server/src/modules/active-run-watchdog/application/use-cases.ts:163

    if (input.actor.type === "agent" && !evaluationIssue) {
      throw new WatchdogDecisionApplicationError(
        "evaluation_issue_required",
        "Agent watchdog decisions require the target evaluation issue",
      );
    }

    const boardActor = input.actor.type === "board";
    const assignedRecoveryOwner =
      input.actor.type === "agent" &&
      Boolean(input.actor.agentId) &&
      evaluationIssue !== null &&
      evaluationIssue.originKind === STALE_ACTIVE_RUN_EVALUATION_ORIGIN_KIND &&
      evaluationIssue.originId === run.id &&
      evaluationIssue.hiddenAt === null &&
      !["done", "cancelled"].includes(evaluationIssue.status) &&
      evaluationIssue.assigneeAgentId === input.actor.agentId;
    if (!boardActor && !assignedRecoveryOwner) {
      throw new WatchdogDecisionApplicationError(
        "not_authorized",
        "Only the board or the assigned recovery owner can record watchdog decisions",
      );
    }

    if (evaluationIssue && (
      evaluationIssue.originKind !== STALE_ACTIVE_RUN_EVALUATION_ORIGIN_KIND ||
      evaluationIssue.originId !== run.id
    )) {
      throw new WatchdogDecisionApplicationError(
        "evaluation_issue_mismatch",
        "Watchdog decision evaluation issue is not bound to the target run",
      );
    }

    const createdByRunId = input.actor.type === "agent"
      ? input.actor.runId ?? input.createdByRunId ?? null
      : input.actor.type === "board"

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the evaluation issue's assigneeAgentId equals the acting agent's ID before recording
  2. Use a board actor (board operator context) for decisions not tied to the assigned recovery owner
  3. If the assigned agent should change, reassign the evaluation issue first, then record the decision as the new assignee
  4. Confirm the issue is still live: hiddenAt null and status not done/cancelled
  5. Do not attempt to bypass with a different company or actor context — company scoping is enforced upstream

Example fix

// before (agent not assigned)
await recordWatchdogDecision({ actor: { type: 'agent', agentId: helperAgentId }, ... });
// after
const issue = await getEvaluationIssue(companyId, evaluationIssueId);
const actor = issue.assigneeAgentId === helperAgentId
  ? { type: 'agent', agentId: helperAgentId }
  : { type: 'board' };
await recordWatchdogDecision({ actor, ... });
Defensive patterns

Strategy: validation

Validate before calling

const authorized = actor.type === 'board' || (issue && issue.assigneeAgentId === actor.agentId && issue.hiddenAt === null && !['done','cancelled'].includes(issue.status)); if (!authorized) throw new Error('only board or assigned recovery owner may record this decision');

Type guard

function isRecoveryOwner(issue: { assigneeAgentId?: string | null; hiddenAt: Date | null; status: string }, agentId: string) { return issue.assigneeAgentId === agentId && issue.hiddenAt === null && !['done','cancelled'].includes(issue.status); }

Try / catch

try { await recordWatchdogDecision(input); } catch (e) { if (e.code === 'not_authorized') { /* fall back to board actor or reassign issue */ } else throw e; }

Prevention

When it happens

Trigger: An agent records a decision on a run whose stale-active-run evaluation issue is assigned to a different agent, or where the issue's originKind/originId don't match, the issue is hidden, done, or cancelled — making assignedRecoveryOwner false — while the actor is not a board actor.

Common situations: A backup agent answering recovery prompts intended for the primary assignee; agents acting after the evaluation issue was closed (done/cancelled) or hidden; automation using a service agent ID that is not the assignee; calling with board-only semantics using an agent key.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/5d0a0fd0d10c2e28. Report an issue: GitHub.