Files
iios/packages/iios-contracts/src/capability.ts
T
maaz519 2223d7a891 feat(p9): CMP consent gate in the Capability Broker
Task S2.1: the broker now checks consent (cmp.checkPurpose) after the OPA gate +
obligations and before the provider — a DENY blocks the send (BLOCKED/CONSENT_DENIED,
provider never called); ALLOW/NOT_REQUIRED proceed, ALLOW carrying the consent
receipt. Contracts gain CapabilityRequest.purpose + CapabilityResult.consentReceiptRef.
FakeCmp is now purpose-aware (denyPurpose/allowOnly). 5 new tests: allow+receipt,
deny→blocked, not-required, purpose-aware, OPA-before-CMP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:23:49 +05:30

58 lines
1.9 KiB
TypeScript

/**
* Capability Broker contract (P9) — the single governed egress boundary.
*
* IIOS prepares idempotent outbound commands; the broker gates them through
* policy, enforces the returned obligations, and routes to a provider. In dev the
* provider is a sandbox (no network); a real provider is a binding/config swap.
* This is the concrete realization of the `capability` platform port.
*/
import type { DataClass } from './events';
import type { PolicyDecision } from './ports';
/** One obligation the broker must enforce before/at egress. */
export type PolicyObligation = PolicyDecision['obligations'][number];
export type CapabilityOutcome = 'SENT' | 'BLOCKED' | 'FAILED' | 'RATE_LIMITED';
export interface CapabilityRequest {
/** Dotted capability name, e.g. "channel.send". */
capability: string;
scopeId?: string;
channelType: string;
target: string;
payload: Record<string, unknown>;
idempotencyKey: string;
/** CMP purpose code for the consent check (e.g. 'outbound_message', 'marketing'). */
purpose?: string;
dataClass?: DataClass;
}
export interface CapabilityResult {
providerRef: string;
outcome: CapabilityOutcome;
errorCode?: string;
latencyMs?: number;
/** The CMP consent receipt this egress went out under (provenance). */
consentReceiptRef?: string;
}
/** What a provider reports; the broker normalizes `outcome` into CapabilityResult. */
export interface ProviderResult {
providerRef: string;
outcome: string;
errorCode?: string;
latencyMs?: number;
}
/**
* A pluggable egress provider. Sandbox (no network) by default; real providers
* (WhatsApp/SMTP/HTTP webhook) implement the same shape. Must be idempotent and
* must surface transport errors as FAILED rather than throwing.
*/
export interface CapabilityProvider {
readonly name: string;
readonly channelTypes: string[];
readonly capabilities: { canSend: boolean; maxPayloadBytes?: number };
send(req: CapabilityRequest): Promise<ProviderResult>;
}