tasq/node_modules/@claude-flow/guidance/dist/capabilities.js

485 lines
18 KiB
JavaScript

/**
* Capability Algebra
*
* All permissions become typed objects that can be composed, restricted,
* delegated, revoked, and reasoned about. Supports delegation chains,
* attestations, constraint evaluation, and set-theoretic composition
* (intersection for actions, union for constraints).
*
* @module @claude-flow/guidance/capabilities
*/
import { randomUUID } from 'node:crypto';
// ============================================================================
// Capability Algebra
// ============================================================================
/**
* Capability Algebra
*
* Manages the lifecycle of typed capabilities: granting, restricting,
* delegating, revoking, attesting, checking, and composing permissions.
* All mutations produce new capability objects; the original is never
* modified in place (except for revocation which is a state change).
*/
export class CapabilityAlgebra {
/** All capabilities indexed by ID */
capabilities = new Map();
/** Index: agentId -> set of capability IDs */
agentIndex = new Map();
/** Index: parentCapabilityId -> set of child capability IDs */
delegationIndex = new Map();
// =========================================================================
// Public API
// =========================================================================
/**
* Grant a new root capability.
*
* Creates a capability with no parent (it is a root grant from an
* authority to an agent).
*/
grant(params) {
const capability = {
id: randomUUID(),
scope: params.scope,
resource: params.resource,
actions: [...params.actions],
constraints: params.constraints ? [...params.constraints] : [],
grantedBy: params.grantedBy,
grantedTo: params.grantedTo,
grantedAt: Date.now(),
expiresAt: params.expiresAt ?? null,
delegatable: params.delegatable ?? false,
revoked: false,
revokedAt: null,
attestations: [],
parentCapabilityId: null,
};
this.store(capability);
return capability;
}
/**
* Restrict a capability, producing a new capability with tighter constraints.
*
* Restrictions can only narrow permissions, never widen them:
* - Actions can only be removed, never added
* - Constraints can only be added, never removed
* - Expiry can only be shortened, never extended
* - Delegatable can only be set to false, never promoted to true
*/
restrict(capability, restrictions) {
const restricted = {
...capability,
id: randomUUID(),
grantedAt: Date.now(),
attestations: [],
parentCapabilityId: capability.id,
};
// Actions: only allow narrowing (intersection with original)
if (restrictions.actions) {
const originalSet = new Set(capability.actions);
restricted.actions = restrictions.actions.filter(a => originalSet.has(a));
}
// Constraints: only allow adding more (union)
if (restrictions.constraints) {
restricted.constraints = [
...capability.constraints,
...restrictions.constraints,
];
}
// Expiry: only allow shortening (pick earlier)
if (restrictions.expiresAt !== undefined) {
if (restrictions.expiresAt !== null) {
if (capability.expiresAt === null) {
restricted.expiresAt = restrictions.expiresAt;
}
else {
restricted.expiresAt = Math.min(capability.expiresAt, restrictions.expiresAt);
}
}
// If restriction tries to set null (no expiry) but original has expiry, keep original
}
// Delegatable: can only be downgraded to false
if (restrictions.delegatable !== undefined) {
if (!restrictions.delegatable) {
restricted.delegatable = false;
}
// Cannot promote to delegatable if original is not
}
this.store(restricted);
return restricted;
}
/**
* Delegate a capability to another agent.
*
* Creates a child capability with the new grantedTo agent. The parent
* capability must have delegatable=true. Optional further restrictions
* can be applied during delegation.
*
* @throws Error if the capability is not delegatable
*/
delegate(capability, toAgentId, restrictions) {
if (!capability.delegatable) {
throw new Error(`Capability ${capability.id} is not delegatable`);
}
if (capability.revoked) {
throw new Error(`Cannot delegate revoked capability ${capability.id}`);
}
if (capability.expiresAt !== null && capability.expiresAt <= Date.now()) {
throw new Error(`Cannot delegate expired capability ${capability.id}`);
}
const delegated = {
...capability,
id: randomUUID(),
grantedBy: capability.grantedTo,
grantedTo: toAgentId,
grantedAt: Date.now(),
attestations: [],
parentCapabilityId: capability.id,
};
// Apply optional further restrictions
if (restrictions?.actions) {
const originalSet = new Set(capability.actions);
delegated.actions = restrictions.actions.filter(a => originalSet.has(a));
}
if (restrictions?.constraints) {
delegated.constraints = [
...capability.constraints,
...restrictions.constraints,
];
}
if (restrictions?.expiresAt !== undefined && restrictions.expiresAt !== null) {
if (capability.expiresAt === null) {
delegated.expiresAt = restrictions.expiresAt;
}
else {
delegated.expiresAt = Math.min(capability.expiresAt, restrictions.expiresAt);
}
}
if (restrictions?.delegatable === false) {
delegated.delegatable = false;
}
this.store(delegated);
// Track delegation relationship
const children = this.delegationIndex.get(capability.id) ?? new Set();
children.add(delegated.id);
this.delegationIndex.set(capability.id, children);
return delegated;
}
/**
* Expire a capability immediately by setting expiresAt to now.
*/
expire(capabilityId) {
const capability = this.capabilities.get(capabilityId);
if (!capability)
return;
capability.expiresAt = Date.now();
}
/**
* Revoke a capability and cascade revocation to all delegated children.
*/
revoke(capabilityId, _reason) {
const capability = this.capabilities.get(capabilityId);
if (!capability)
return;
capability.revoked = true;
capability.revokedAt = Date.now();
this.cascadeRevoke(capabilityId);
}
/**
* Add an attestation to a capability.
*/
attest(capabilityId, attestation) {
const capability = this.capabilities.get(capabilityId);
if (!capability)
return;
capability.attestations.push({
...attestation,
attestedAt: Date.now(),
});
}
/**
* Check whether an agent is allowed to perform an action on a resource.
*
* Finds all non-revoked, non-expired capabilities for the agent that
* match the requested scope and resource, checks if the requested action
* is allowed, and verifies all constraints are satisfied.
*/
check(agentId, scope, resource, action, context) {
const agentCapIds = this.agentIndex.get(agentId);
if (!agentCapIds || agentCapIds.size === 0) {
return {
allowed: false,
capabilities: [],
reason: `No capabilities found for agent "${agentId}"`,
constraints: [],
};
}
const now = Date.now();
const matchingCapabilities = [];
const activeConstraints = [];
for (const capId of agentCapIds) {
const cap = this.capabilities.get(capId);
if (!cap)
continue;
// Skip revoked
if (cap.revoked)
continue;
// Skip expired
if (cap.expiresAt !== null && cap.expiresAt <= now)
continue;
// Match scope and resource
if (cap.scope !== scope)
continue;
if (cap.resource !== resource && cap.resource !== '*')
continue;
// Check action
if (!cap.actions.includes(action) && !cap.actions.includes('*'))
continue;
// Check constraints
if (!this.satisfiesConstraints(cap, context))
continue;
matchingCapabilities.push(cap);
activeConstraints.push(...cap.constraints);
}
if (matchingCapabilities.length === 0) {
return {
allowed: false,
capabilities: [],
reason: `No matching capability for agent "${agentId}" to "${action}" on ${scope}:${resource}`,
constraints: [],
};
}
return {
allowed: true,
capabilities: matchingCapabilities,
reason: `Allowed by ${matchingCapabilities.length} capability(ies)`,
constraints: activeConstraints,
};
}
/**
* Get all capabilities granted to a specific agent.
*/
getCapabilities(agentId) {
const capIds = this.agentIndex.get(agentId);
if (!capIds)
return [];
const result = [];
for (const id of capIds) {
const cap = this.capabilities.get(id);
if (cap)
result.push(cap);
}
return result;
}
/**
* Get a capability by ID.
*/
getCapability(id) {
return this.capabilities.get(id);
}
/**
* Get the full delegation chain from root to the given capability.
*
* Returns an array ordered from the root ancestor to the given capability.
*/
getDelegationChain(capabilityId) {
const chain = [];
let current = this.capabilities.get(capabilityId);
while (current) {
chain.unshift(current);
if (current.parentCapabilityId === null)
break;
current = this.capabilities.get(current.parentCapabilityId);
}
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, cap2) {
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 = null;
if (cap1.expiresAt !== null && cap2.expiresAt !== null) {
expiresAt = Math.min(cap1.expiresAt, cap2.expiresAt);
}
else if (cap1.expiresAt !== null) {
expiresAt = cap1.expiresAt;
}
else if (cap2.expiresAt !== null) {
expiresAt = cap2.expiresAt;
}
const composed = {
id: randomUUID(),
scope: cap1.scope,
resource: cap1.resource,
actions: intersectedActions,
constraints: combinedConstraints,
grantedBy: cap1.grantedBy,
grantedTo: cap1.grantedTo,
grantedAt: Date.now(),
expiresAt,
delegatable: cap1.delegatable && cap2.delegatable,
revoked: false,
revokedAt: null,
attestations: [],
parentCapabilityId: null,
};
this.store(composed);
return composed;
}
/**
* Check if inner's permission set is a subset of outer's.
*
* Returns true if:
* - inner.scope === outer.scope
* - inner.resource === outer.resource
* - Every action in inner is present in outer
* - inner.expiresAt is <= outer.expiresAt (or outer has no expiry)
*/
isSubset(inner, outer) {
if (inner.scope !== outer.scope)
return false;
if (inner.resource !== outer.resource)
return false;
const outerActions = new Set(outer.actions);
for (const action of inner.actions) {
if (!outerActions.has(action))
return false;
}
// Expiry: inner must expire no later than outer (or outer has no expiry)
if (outer.expiresAt !== null) {
if (inner.expiresAt === null)
return false; // inner never expires but outer does
if (inner.expiresAt > outer.expiresAt)
return false;
}
return true;
}
// =========================================================================
// Private Methods
// =========================================================================
/**
* Evaluate whether all constraints on a capability are satisfied.
*/
satisfiesConstraints(capability, context) {
for (const constraint of capability.constraints) {
switch (constraint.type) {
case 'time-window': {
const now = Date.now();
const start = constraint.params['start'];
const end = constraint.params['end'];
if (start !== undefined && now < start)
return false;
if (end !== undefined && now > end)
return false;
break;
}
case 'rate-limit': {
// Rate-limit constraints are informational; enforcement is external.
// If context provides current usage, check it.
if (context) {
const max = constraint.params['max'];
const current = context['currentUsage'];
if (max !== undefined && current !== undefined && current >= max) {
return false;
}
}
break;
}
case 'budget': {
if (context) {
const limit = constraint.params['limit'];
const used = context['budgetUsed'];
if (limit !== undefined && used !== undefined && used >= limit) {
return false;
}
}
break;
}
case 'condition': {
// Condition constraints require a truthy context value at the specified key
const key = constraint.params['key'];
const expectedValue = constraint.params['value'];
if (key && context) {
if (expectedValue !== undefined) {
if (context[key] !== expectedValue)
return false;
}
else {
if (!context[key])
return false;
}
}
break;
}
case 'scope-restriction': {
// Scope restrictions limit to specific sub-resources
const allowedPattern = constraint.params['pattern'];
if (allowedPattern && context) {
const targetResource = context['targetResource'];
if (targetResource && !targetResource.startsWith(allowedPattern)) {
return false;
}
}
break;
}
}
}
return true;
}
/**
* Cascade revocation to all delegated children of a capability.
*/
cascadeRevoke(capabilityId) {
const children = this.delegationIndex.get(capabilityId);
if (!children)
return;
const now = Date.now();
for (const childId of children) {
const child = this.capabilities.get(childId);
if (child && !child.revoked) {
child.revoked = true;
child.revokedAt = now;
// Recurse into grandchildren
this.cascadeRevoke(childId);
}
}
}
/**
* Store a capability and update indices.
*/
store(capability) {
this.capabilities.set(capability.id, capability);
const agentCaps = this.agentIndex.get(capability.grantedTo) ?? new Set();
agentCaps.add(capability.id);
this.agentIndex.set(capability.grantedTo, agentCaps);
}
}
// ============================================================================
// Factory
// ============================================================================
/**
* Create a CapabilityAlgebra instance
*/
export function createCapabilityAlgebra() {
return new CapabilityAlgebra();
}
//# sourceMappingURL=capabilities.js.map