ruvnet/ruflo · error · ClaimOperationError

UNAUTHORIZED

UNAUTHORIZED

Error message

Claimant ${claimant.name} does not own the claim on issue ${issueId}

What it means

Thrown by ClaimService.release when a claim exists but claim.claimant.id !== claimant.id. The release operation is owner-only at the service layer; this guards against a non-holder releasing someone else's claim. Code is UNAUTHORIZED.

Source

Thrown at v3/@claude-flow/claims/src/application/claim-service.ts:252

    // Emit event
    const event = createClaimCreatedEvent(claimId, issueId, claimant);
    await this.eventStore.append(event);

    return { success: true, claim };
  }

  async release(issueId: string, claimant: Claimant): Promise<void> {
    const claim = await this.claimRepository.findByIssueId(issueId);

    // Validate claim exists
    if (!claim) {
      throw new ClaimOperationError('NOT_CLAIMED', `Issue ${issueId} is not claimed`);
    }

    // Validate claimant owns the claim
    if (claim.claimant.id !== claimant.id) {
      throw new ClaimOperationError(
        'UNAUTHORIZED',
        `Claimant ${claimant.name} does not own the claim on issue ${issueId}`
      );
    }

    // Check for pending handoffs
    const pendingHandoff = claim.handoffChain?.find((h) => h.status === 'pending');
    if (pendingHandoff) {
      throw new ClaimOperationError(
        'HANDOFF_PENDING',
        `Cannot release claim with pending handoff to ${pendingHandoff.to.name}`
      );
    }

    // Update claim status
    const previousStatus = claim.status;
    claim.status = 'released';
    claim.lastActivityAt = new Date();

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Pass the Claimant whose id equals claim.claimant.id (look it up first).
  2. For delegated release, extend the service with an authorized override rather than bypassing the check.
  3. Use requestHandoff to transfer ownership to the new claimant.

Example fix

// before
await service.release(issueId, { id: 'agent-v2', name: 'Agent', type: 'agent' });
// throws UNAUTHORIZED; claim held by 'agent-v1'

// after
const claim = await repo.findByIssueId(issueId);
if (claim) await service.release(issueId, claim.claimant);
Defensive patterns

Strategy: validation

Validate before calling

const claim = await claimRepo.findByIssueId(issueId);
if (!claim || claim.claimant.id !== claimant.id) throw new Error('not the holder');

Type guard

null

Try / catch

try { await service.release(issueId, claimant); } catch (e) {
  if (e instanceof ClaimOperationError && e.code === 'UNAUTHORIZED') { /* re-fetch holder */ } else throw e;
}

Prevention

When it happens

Trigger: A claimant other than the holder calling release; the holder's identity object has a different id than the one stored on the claim.

Common situations: Agent restart produced a new id; a supervisor trying to release on behalf of a worker without using handoff; passing a Claimant with a typo'd id.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/bbc1fbed5cfd371d. Report an issue: GitHub.