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,57 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { CapabilityRequest, CapabilityResult, IiosPlatformPorts } from '@insignia/iios-contracts';
|
||||
import { PLATFORM_PORTS } from '../platform/platform-ports';
|
||||
import { decideOrThrow } from '../platform/fail-closed';
|
||||
import { CapabilityProviderRegistry } from './capability.registry';
|
||||
|
||||
/**
|
||||
* The single governed egress boundary (P9). Every outbound action flows through
|
||||
* here: it fails closed on policy (nothing sent unless explicitly allowed),
|
||||
* enforces the policy's obligations BEFORE a provider is ever called, then routes
|
||||
* to the channel's provider. It is the concrete realization of the `capability`
|
||||
* platform port — a real broker replacing the permissive P1 stub.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CapabilityBroker {
|
||||
constructor(
|
||||
@Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
|
||||
private readonly registry: CapabilityProviderRegistry,
|
||||
) {}
|
||||
|
||||
async invoke(req: CapabilityRequest): Promise<CapabilityResult> {
|
||||
// 1. Fail-closed policy gate. Deny/timeout ⇒ PolicyDeniedError, nothing sent.
|
||||
const decision = await decideOrThrow(this.ports, {
|
||||
action: `iios.capability.${req.capability}`,
|
||||
scopeId: req.scopeId,
|
||||
channelType: req.channelType,
|
||||
target: req.target,
|
||||
});
|
||||
|
||||
// 2. Enforce obligations BEFORE touching a provider.
|
||||
for (const ob of decision.obligations) {
|
||||
if (ob.kind === 'DENY_OUTBOUND' || ob.kind === 'REVIEW') {
|
||||
return { providerRef: 'blocked', outcome: 'BLOCKED', errorCode: ob.kind };
|
||||
}
|
||||
}
|
||||
const payload = this.applyMasks(req.payload, decision.obligations);
|
||||
|
||||
// 3. Route to the channel's provider (unknown channel fails closed in the registry).
|
||||
const provider = this.registry.forChannel(req.channelType);
|
||||
const result = await provider.send({ ...req, payload });
|
||||
|
||||
// 4. Normalize the provider's outcome.
|
||||
const outcome = result.outcome === 'SENT' ? 'SENT' : result.outcome === 'RATE_LIMITED' ? 'RATE_LIMITED' : 'FAILED';
|
||||
return { providerRef: result.providerRef, outcome, errorCode: result.errorCode, latencyMs: result.latencyMs };
|
||||
}
|
||||
|
||||
private applyMasks(
|
||||
payload: Record<string, unknown>,
|
||||
obligations: { kind: string; target?: string }[],
|
||||
): Record<string, unknown> {
|
||||
const masks = obligations.filter((o) => (o.kind === 'MASK' || o.kind === 'REDACT') && o.target);
|
||||
if (masks.length === 0) return payload;
|
||||
const out = { ...payload };
|
||||
for (const m of masks) out[m.target as string] = '[REDACTED]';
|
||||
return out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user