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:
2026-07-01 19:58:25 +05:30
parent e11964b8bc
commit c50a501f32
9 changed files with 455 additions and 0 deletions
+53
View File
@@ -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>;
}
+1
View File
@@ -3,6 +3,7 @@ export * from './enums';
export * from './ingest';
export * from './ports';
export * from './inference';
export * from './capability';
export * from './events';
export * from './commands';
export * from './errors';