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
@@ -11,7 +11,8 @@ import { RouteService } from './route.service';
import { RouteProjector } from './route.projector';
import { IngestService } from '../interactions/ingest.service';
import { OutboundService } from '../adapters/outbound.service';
import { AdapterRegistry } from '../adapters/adapter.registry';
import { CapabilityBroker } from '../capability/capability.broker';
import { CapabilityProviderRegistry } from '../capability/capability.registry';
import { OutboxBus } from '../outbox/outbox.bus';
import { IIOS_EVENTS, type CloudEvent } from '@insignia/iios-contracts';
import type { PrismaService } from '../prisma/prisma.service';
@@ -103,7 +104,7 @@ describe('Route simulator (P6, preview-first)', () => {
describe('RouteService approve/deny → execute (P6)', () => {
const admin: MessagePrincipal = { userId: 'admin', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Admin' };
const svc = () => new RouteService(asService, new RouteSimulator(asService, new RuleEngine()), new OutboundService(asService, new AdapterRegistry()), actors);
const svc = () => new RouteService(asService, new RouteSimulator(asService, new RuleEngine()), new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry())), actors);
it('approving a REVIEW decision executes the forward to the sandbox', async () => {
const { interactionId, scopeId } = await makeInteraction('party at 9 PM');
@@ -138,7 +139,7 @@ describe('RouteService approve/deny → execute (P6)', () => {
});
describe('RouteProjector — AUTOMATIC forwarding (P6)', () => {
const routeSvc = () => new RouteService(asService, new RouteSimulator(asService, new RuleEngine()), new OutboundService(asService, new AdapterRegistry()), actors);
const routeSvc = () => new RouteService(asService, new RouteSimulator(asService, new RuleEngine()), new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry())), actors);
const projector = () => new RouteProjector(asService, new OutboxBus(), routeSvc());
// Ingest an inbound (channel-bearing) interaction — routable, unlike native DMs.