2223d7a891
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>
58 lines
1.9 KiB
TypeScript
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>;
|
|
}
|