feat(p9): route OutboundService through the Capability Broker

Task 9.3: OutboundService now delegates the execute step to CapabilityBroker
(policy-gated + obligation-enforced) instead of calling a provider directly, while
keeping idempotency + rate-limit + the command/attempt ledger. BLOCKED obligations
→ command BLOCKED; a fail-closed throw marks the command FAILED then propagates.
AdaptersModule imports CapabilityModule; P4/P6/P8 egress now flows through the
broker unchanged. Updated 6 spec constructions; added a DENY_OUTBOUND test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 20:04:11 +05:30
parent c50a501f32
commit 28e517fc1b
7 changed files with 50 additions and 23 deletions
@@ -7,6 +7,8 @@ import { signedFixture, emailFixture } from '@insignia/iios-adapter-sdk';
import { AdapterRegistry } from './adapter.registry';
import { InboundService } from './inbound.service';
import { OutboundService } from './outbound.service';
import { CapabilityBroker } from '../capability/capability.broker';
import { CapabilityProviderRegistry } from '../capability/capability.registry';
import { RawEventProjector } from './raw-event.projector';
import { IngestService } from '../interactions/ingest.service';
import { ActorResolver } from '../identity/actor.resolver';
@@ -77,7 +79,7 @@ describe('Adapter inbound (P5)', () => {
describe('Adapter outbound (P5)', () => {
const outbound = (limit?: number): OutboundService => {
const s = new OutboundService(asService, registry());
const s = new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()));
if (limit) s.limit = limit;
return s;
};
@@ -104,4 +106,13 @@ describe('Adapter outbound (P5)', () => {
expect(b.id).toBe(a.id);
expect(await prisma.iiosOutboundCommand.count()).toBe(1);
});
it('broker obligation DENY_OUTBOUND → command BLOCKED, no send (P9)', async () => {
const ports = makeFakePorts();
ports.opa.setDecision({ allow: true, obligations: [{ kind: 'DENY_OUTBOUND', reason: 'recipient opted out' }] });
const svc = new OutboundService(asService, new CapabilityBroker(ports, new CapabilityProviderRegistry()));
const cmd = await svc.send('WEBHOOK', 'user@x', { text: 'hi' }, 'blk-1');
expect(cmd.status).toBe('BLOCKED');
expect(cmd.providerRef).toBe('blocked');
});
});