feat(p9): Capability Broker — governed egress + fail-closed gate + obligations
Tasks 9.1+9.2: contracts (CapabilityRequest/Result/Provider, PolicyObligation) + service CapabilityBroker that fails closed via opa (PolicyDeniedError, nothing sent), enforces obligations before any provider call (DENY_OUTBOUND/REVIEW → BLOCKED; MASK/REDACT → mask payload field), then routes to a CapabilityProviderRegistry (SandboxProvider default; env-gated HttpProvider overrides per channel; unknown channel fails closed). 5 tests: allow/deny/block/redact/unknown-channel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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;
|
||||
dataClass?: DataClass;
|
||||
}
|
||||
|
||||
export interface CapabilityResult {
|
||||
providerRef: string;
|
||||
outcome: CapabilityOutcome;
|
||||
errorCode?: string;
|
||||
latencyMs?: number;
|
||||
}
|
||||
|
||||
/** 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user