ruvnet/ruflo · error

Cannot compose capabilities with different scopes: "${cap1.s

Error message

Cannot compose capabilities with different scopes: "${cap1.scope}" vs "${cap2.scope}"

What it means

CapabilityAuthority.compose() intersects two capabilities (actions intersected, constraints unioned, tighter expiry), which is only meaningful when both describe the same target. It throws when `cap1.scope !== cap2.scope` because intersecting capabilities over different scopes (e.g. 'global' vs 'workspace') has no well-defined result. Scope must match exactly before the resource check even runs.

Source

Thrown at v3/@claude-flow/guidance/src/capabilities.ts:451

    }

    return chain;
  }

  /**
   * Compose two capabilities via intersection.
   *
   * - Actions = intersection of both action sets
   * - Constraints = union of both constraint sets
   * - Expiry = the tighter (earlier) of the two
   * - Delegatable = true only if both are delegatable
   * - Scope and resource must match; throws if they differ
   *
   * @throws Error if scope or resource do not match
   */
  compose(cap1: Capability, cap2: Capability): Capability {
    if (cap1.scope !== cap2.scope) {
      throw new Error(
        `Cannot compose capabilities with different scopes: "${cap1.scope}" vs "${cap2.scope}"`
      );
    }
    if (cap1.resource !== cap2.resource) {
      throw new Error(
        `Cannot compose capabilities with different resources: "${cap1.resource}" vs "${cap2.resource}"`
      );
    }

    // Actions: intersection
    const actionSet1 = new Set(cap1.actions);
    const intersectedActions = cap2.actions.filter(a => actionSet1.has(a));

    // Constraints: union
    const combinedConstraints = [...cap1.constraints, ...cap2.constraints];

    // Expiry: tightest
    let expiresAt: number | null = null;

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Only compose capabilities minted with the identical scope string
  2. Re-grant one of the two under the other's scope, then compose
  3. Filter capability pools by `cap.scope === targetScope` before composing
  4. Centralize scope names as constants so naming cannot drift

Example fix

// before
const merged = authority.compose(globalCap, workspaceCap); // scopes differ → throws

// after
const pairs = caps.filter(c => c.scope === 'workspace');
const merged = pairs.length === 2 ? authority.compose(pairs[0], pairs[1]) : mintFresh();
Defensive patterns

Strategy: validation

Validate before calling

if (cap1.scope !== cap2.scope) {
  // different scopes — skip compose or re-grant one side
}

Type guard

const sameScope = (a: Capability, b: Capability): boolean => a.scope === b.scope;

Prevention

When it happens

Trigger: compose(capA with scope 'global', capB with scope 'workspace'); composing capabilities minted for different environments or tenants; generic helper code that pairs any two capabilities from a pool without filtering by scope.

Common situations: Capabilities issued per-namespace/per-tenant being mixed in a shared pool; refactors that change the scope naming scheme ('ws' vs 'workspace') so older stored capabilities no longer match; multi-environment configs mixing staging and production caps.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/816ed6e7708b858b. Report an issue: GitHub.