Surface Approval Adapters
Brain surface adapters deliver agent proposals to the places where operators already work: Slack, Microsoft Teams, and email. They are approval surfaces, not execution rails.
Current Code
The implementation lives in packages/surfaces, core bindings live in packages/core, and the inbound webhook process lives in services/surface-gateway.
Proposal schema and hashing
packages/surfaces/src/proposal
Canonical proposal validation and content hash generation
Dispatch pipeline
packages/surfaces/src/core/dispatcher.ts
Validate, hash once, deliver, and persist delivered refs through a callback
Approval pipeline
packages/surfaces/src/core/approval.ts
Expiry, identity, policy, idempotency, audit, approval signature, execution handoff, surface update
Surface renderers
packages/surfaces/src/surfaces
Slack Block Kit, Teams Adaptive Cards, and email templates
Inbound helpers
packages/surfaces/src/http
Slack signature validation, email token validation, Teams verifier seam
Live clients
packages/surfaces/src/clients
Slack Web API, generic HTTP email provider, and Bot Framework Teams client
Core bindings
packages/core/src/bindings
Adapter layer from Brain services into surface ports
Webhook deployable
services/surface-gateway
Fastify routes, DB adapters, live client wiring, and process isolation
@brain/surfaces does not import @brain/core. The root check-surface-acyclic script enforces the dependency direction.
Runtime Flow
Agent finding
-> Proposal factory
-> Dispatcher
-> Slack, Teams, or email adapter
-> Human approve or hold action
-> Inbound HTTP helper
-> ApprovalService.handle
-> @brain/core ports
-> post-audit approval signature
-> execution approval handoffThe dispatcher computes the proposal content hash once at emit time. That hash is the value later recorded in audit, so the audit record proves what was shown to the approver.
Approval Safety Model
All surfaces share the same approval pipeline:
Reject expired proposals.
Resolve the surface identity to a tenant-scoped Brain actor.
Re-check authority at click time through policy.
Write audit before any quorum-changing approval signature.
Record the approval signature and read post-write quorum.
Claim the terminal decision only after approval quorum is met, or for rejection.
Enqueue execution only for approved proposals after audit, signature, quorum, and claim.
Update the original surface message on a best-effort basis.
Slack, Teams, and email are therefore input channels to the same policy and audit path. A surface button cannot become a direct money movement path.
Deployment Notes
services/surface-gateway hosts the framework-neutral handlers as a separate Fastify v5 process:
POST /surfaces/slack/interactions
Slack interactivity with raw-body signature verification and retry dedupe
GET /surfaces/email/approve
Confirmation page for signed email approval links
HEAD /surfaces/email/approve
Link preview and health-safe email route check
POST /surfaces/email/approve
Email approval confirmation with signed-token validation
POST /surfaces/email/recipients/verify/start
Admin-gated recipient verification email initiation
GET /surfaces/email/verify
Confirmation page for signed recipient verification links
HEAD /surfaces/email/verify
Link preview-safe recipient verification route check
POST /surfaces/email/verify
Recipient verification confirmation
POST /surfaces/email/routes
Admin-gated agent to verified-recipient routing config
POST /surfaces/email/domains
Admin-gated tenant sender-domain DNS verification
POST /surfaces/email/domains/reverify
Admin-gated re-check for tenant sender-domain DNS
POST /surfaces/email/events
ESP bounce and complaint events with webhook signature verification
POST /surfaces/teams/messages
Bot Framework verified Teams submit activities
POST /surfaces/teams/install
Admin-gated Brain tenant to Azure AD tenant mapping
POST /surfaces/teams/revoke
Admin-gated Teams installation revocation
POST /surfaces/smoke/proposals
Explicitly gated smoke dispatch for release candidates
GET /healthz
Process health check
The gateway owns only surface persistence:
surface_external_identitiessurface_proposalssurface_delivered_refssurface_decisionssurface_slack_retriessurface_slack_installationssurface_slack_install_noncessurface_email_recipientssurface_email_routessurface_email_domainssurface_teams_conversation_refssurface_teams_installations
Slack installations are resolved by verified Slack workspace id before tenant state is accepted. Teams installations are resolved by authenticated Azure AD tenant id before the card's Brain tenant is trusted. Teams conversation references are recorded only after that mapping succeeds, so proactive sends use <brainTenantId>:<conversationId> references tied to an active install. Email recipients must be verified before routing or identity resolution accepts them. Email GET and HEAD routes never mutate state; only POST confirmation verifies an address or applies an approval. ESP bounce and complaint events mark recipients disabled so future dispatch skips them. Tenant custom senders are only used after Brain verifies SPF, DKIM, and DMARC through DNS.
Surface onboarding routes require a Brain bearer JWT with surfaces:admin. Slack OAuth install, Teams install and revoke, and email recipient, route, and domain onboarding all derive the Brain tenant from the principal. Slack and ESP event webhooks stay on provider HMAC signatures because they are machine callbacks, not tenant-admin onboarding calls.
The production DB role is brain_surface_gateway. It is tenant-scoped, has no BYPASSRLS, and has no Ledger or execution outbox grants. Decisions delegate to the active Policy document at click time, write shared Audit with deterministic idempotency keys, and use the existing execution approval path for handoff.
Last updated
