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>
This commit is contained in:
@@ -27,21 +27,34 @@ export class CapabilityBroker {
|
||||
target: req.target,
|
||||
});
|
||||
|
||||
// 2. Enforce obligations BEFORE touching a provider.
|
||||
// 2. Enforce policy 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 };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Consent gate (CMP). Authorization (OPA) is not consent — a DENY here means the
|
||||
// recipient did not consent to this purpose, so nothing is sent. Checked at send
|
||||
// time, not enqueue time. NOT_REQUIRED/ALLOW proceed; ALLOW carries a receipt.
|
||||
const consent = await this.ports.cmp.checkPurpose({
|
||||
purpose: req.purpose ?? 'outbound_message',
|
||||
scopeId: req.scopeId,
|
||||
subject: req.target,
|
||||
});
|
||||
if (consent.status === 'DENY') {
|
||||
return { providerRef: 'blocked', outcome: 'BLOCKED', errorCode: 'CONSENT_DENIED' };
|
||||
}
|
||||
|
||||
const payload = this.applyMasks(req.payload, decision.obligations);
|
||||
|
||||
// 3. Route to the channel's provider (unknown channel fails closed in the registry).
|
||||
// 4. 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.
|
||||
// 5. Normalize the provider's outcome + record the consent receipt for provenance.
|
||||
const outcome = result.outcome === 'SENT' ? 'SENT' : result.outcome === 'RATE_LIMITED' ? 'RATE_LIMITED' : 'FAILED';
|
||||
return { providerRef: result.providerRef, outcome, errorCode: result.errorCode, latencyMs: result.latencyMs };
|
||||
return { providerRef: result.providerRef, outcome, errorCode: result.errorCode, latencyMs: result.latencyMs, consentReceiptRef: consent.receiptRef };
|
||||
}
|
||||
|
||||
private applyMasks(
|
||||
|
||||
Reference in New Issue
Block a user