{"slug":"business/v2-provider-fabric","title":"IntegrateWise Continuity Bridge v2.0 — Provider Fabric and Adapter Pattern","section":"business","sectionLabel":"Business","status":"unclassified","type":"document","path":"business/v2-provider-fabric.mdx","bytes":115068,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n> **Canonical Reference:** [CANONICAL_PLATFORM_ARCHITECTURE.md](../architecture/CANONICAL_PLATFORM_ARCHITECTURE.md)  \n> _This document is downstream of the canonical architecture. Refer to the canonical source for current truth._\n\n**Status:** STAGE 1/2 DEEP-DIVE — DRAFT FOR CANONICAL v2.0  \n**Domain:** Provider Fabric and Adapter Pattern — Outbound adapters (Nango/MCP/Native), Inbound sync adapters, Credential wall, Tool-to-tool communication protocol, Entity360 injection, Provider health, Circuit breaker  \n**Date:** 2026-07-03  \n**Version:** 2.0.0-DRAFT  \n**Supersedes:** v1.0.1-FINAL §5 (Adapter Pattern), §8.2 (Outbound MCP Pool)  \n**Author:** Architecture Specialist — Provider Fabric Domain\n\n---\n\n## Table of Contents\n\n1. [Domain Charter](#1-domain-charter)\n2. [Conceptual Position in the 6-Layer Stack](#2-conceptual-position-in-the-6-layer-stack)\n3. [Architecture Overview](#3-architecture-overview)\n4. [Adapter Taxonomy and Interface Contracts](#4-adapter-taxonomy-and-interface-contracts)\n5. [Outbound Execution Flow (act Service)](#5-outbound-execution-flow-act-service)\n6. [Inbound Sync Flow (connector-sync Service)](#6-inbound-sync-flow-connector-sync-service)\n7. [Credential Wall](#7-credential-wall)\n8. [Entity360 Injection Protocol](#8-entity360-injection-protocol)\n9. [Tool-to-Tool Communication Protocol (T2T)](#9-tool-to-tool-communication-protocol-t2t)\n10. [Provider Health Monitoring](#10-provider-health-monitoring)\n11. [Circuit Breaker](#11-circuit-breaker)\n12. [Security Boundaries](#12-security-boundaries)\n13. [Integration Points with Adjacent Domains](#13-integration-points-with-adjacent-domains)\n14. [Error Handling, Retries, and Dead-Letter Behavior](#14-error-handling-retries-and-dead-letter-behavior)\n15. [Physical Deployment Map](#15-physical-deployment-map)\n16. [Operational Runbooks](#16-operational-runbooks)\n\n---\n\n## 1. Domain Charter\n\nThe Provider Fabric is Layer 6 of the IntegrateWise Continuity Bridge — the **execution boundary** between the platform's continuity kernel and the external universe of SaaS providers, APIs, databases, queues, and agent runtimes. Every byte that crosses this boundary is mediated by an adapter, governed by a credential wall, enriched by Entity360 context, and monitored by health probes and circuit breakers.\n\n**Core responsibility:** The Provider Fabric makes the platform **provider-agnostic** at the Continuity Layer. Swapping Salesforce for HubSpot, or Nango for direct OAuth, or MCP for REST, is a configuration change — never a consumer-facing change.\n\n**Doctrine:** _Truth you own. AI you rent. Providers you adapt. The Continuity Layer is the moat._\n\n**Key invariant:** No service outside the Provider Fabric ever holds a provider credential, constructs a provider API call, or parses a provider-specific response schema. The Continuity Layer (Spine, Memory, Knowledge) is entirely provider-agnostic.\n\n---\n\n## 2. Conceptual Position in the 6-Layer Stack\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ LAYER 1 — IDENTITY                                                  │\n│  JWT issuance, tenant resolution, RBAC, SSO, API Keys               │\n├─────────────────────────────────────────────────────────────────────┤\n│ LAYER 2 — INGRESS                                                   │\n│  Gateway, webhook-ingress, inbound MCP pool, rate-limiting          │\n├─────────────────────────────────────────────────────────────────────┤\n│ LAYER 3 — CAPABILITY                                                │\n│  Capability engine, agent runtime, think, knowledge, intelligence   │\n│  OODA: OBSERVE → ORIENT (Context Assembly)                          │\n├─────────────────────────────────────────────────────────────────────┤\n│ LAYER 4 — CONTINUITY                                                │\n│  Adaptive Spine, Shared Memory, Continuity Engine, Twin (AMBIENT)   │\n│  OODA: DECIDE → ACT (governance-gated)                              │\n│  ████████████████ THE MOAT ████████████████                         │\n├─────────────────────────────────────────────────────────────────────┤\n│ LAYER 5 — GOVERNANCE                                                │\n│  Confidence gates (0.70/0.85), HITL, audit, policy registry         │\n├─────────────────────────────────────────────────────────────────────┤\n│ LAYER 6 — PROVIDER FABRIC  ◄── YOU ARE HERE                         │\n│  Adapter pattern, credential wall, Entity360 injection,             │\n│  provider health, circuit breaker, T2T protocol                     │\n│  Outbound: act → adapters → providers                               │\n│  Inbound:  providers → adapters → normalizer → pipeline → spine     │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n**North-South axis:** The Provider Fabric is the **South plane** — all egress and all inbound sync converge here. The North plane (Ingress) and South plane (Provider Fabric) never communicate directly; all coordination happens through the Spine (Layer 4) and the Governance Layer (Layer 5).\n\n---\n\n## 3. Architecture Overview\n\n### 3.1 The Adapter Pattern at v2.0\n\nThe Integration Manager is **not a separate service** in v2.0. It is a **pattern** — a set of adapter modules co-located inside the services that actually touch external providers. This is the canonical implementation:\n\n```\n┌──────────────────────────────────────────────────────────────────────┐\n│                         OUTBOUND PATH                                │\n│                                                                      │\n│   Continuity Layer                                                   │\n│   ┌──────────────┐                                                   │\n│   │   GOVERN     │─── approved action ───► ACT_QUEUE (Cloudflare)   │\n│   └──────────────┘                                                   │\n│          │                                                           │\n│          ▼                                                           │\n│   ┌──────────────────────────────────────────────────────────────┐   │\n│   │                    iw-act  (Cloudflare Worker)                │   │\n│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐   │   │\n│   │  │  execute.ts │─►│ adapter sel │─►│ credentials.ts      │   │   │\n│   │  │  (dequeue   │  │ (outbound   │  │ (credential wall)   │   │   │\n│   │  │   ACT_QUEUE)│  │  adapter    │  │ CF Secrets / KV     │   │   │\n│   │  └─────────────┘  │  factory)   │  │ Nango vault         │   │   │\n│   │                   └─────────────┘  └─────────────────────┘   │   │\n│   │                          │                    │              │   │\n│   │                   ┌──────┴──────┐              │              │   │\n│   │                   ▼             ▼             ▼              │   │\n│   │            ┌──────────┐  ┌──────────┐  ┌──────────┐         │   │\n│   │            │  nango   │  │   mcp    │  │  native  │         │   │\n│   │            │ adapter  │  │ outbound │  │ adapter  │         │   │\n│   │            │ (OAuth   │  │ adapter  │  │ (direct  │         │   │\n│   │            │  mature) │  │ (AI-nat) │  │  REST)   │         │   │\n│   │            └──────────┘  └──────────┘  └──────────┘         │   │\n│   │                   │             │             │              │   │\n│   │                   └─────────────┴─────────────┘              │   │\n│   │                                 │                            │   │\n│   │                          ┌──────┴──────┐                     │   │\n│   │                          ▼             ▼                     │   │\n│   │                   ┌──────────┐   ┌──────────┐               │   │\n│   │                   │ context- │   │ circuit- │               │   │\n│   │                   │ inject.ts│   │ breaker  │               │   │\n│   │                   │(Entity360│   │ (per     │               │   │\n│   │                   │injection)│   │provider) │               │   │\n│   │                   └──────────┘   └──────────┘               │   │\n│   │                          │                                  │   │\n│   └──────────────────────────┼──────────────────────────────────┘   │\n│                              │                                       │\n│                              ▼                                       │\n│   ┌──────────────────────────────────────────────────────────────┐   │\n│   │              EXTERNAL PROVIDER FABRIC                         │   │\n│   │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐     │   │\n│   │  │Salesf. │ │HubSpot │ │ Slack  │ │ GitHub │ │ Stripe │ ... │   │\n│   │  │ MCP/   │ │ Nango  │ │ Nango  │ │ MCP/   │ │ Native │     │   │\n│   │  │ REST   │ │ OAuth  │ │ OAuth  │ │ REST   │ │ REST   │     │   │\n│   │  └────────┘ └────────┘ └────────┘ └────────┘ └────────┘     │   │\n│   └──────────────────────────────────────────────────────────────┘   │\n└──────────────────────────────────────────────────────────────────────┘\n\n┌──────────────────────────────────────────────────────────────────────┐\n│                         INBOUND PATH                                 │\n│                                                                      │\n│   EXTERNAL PROVIDERS                                                 │\n│   ┌────────┐ ┌────────┐ ┌────────┐                                  │\n│   │Salesf. │ │HubSpot │ │ Stripe │  webhooks / sync polls           │\n│   │webhook │ │ webhook│ │webhook │ ───────────────────────────►     │\n│   └────────┘ └────────┘ └────────┘                                  │\n│                              │                                       │\n│                              ▼                                       │\n│   ┌──────────────────────────────────────────────────────────────┐   │\n│   │              webhook-ingress  (Cloudflare Worker)             │   │\n│   │  JWT verify (provider signature) → tenant routing             │   │\n│   └──────────────────────────────────────────────────────────────┘   │\n│                              │                                       │\n│                              ▼                                       │\n│   ┌──────────────────────────────────────────────────────────────┐   │\n│   │              iw-connector-sync  (Cloudflare Worker)           │   │\n│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐   │   │\n│   │  │   sync.ts   │─►│ sync adapter│─►│ credentials.ts      │   │   │\n│   │  │ (cron/      │  │ (inbound    │  │ (same credential    │   │   │\n│   │  │  webhook    │  │  adapter)   │  │  wall as outbound)  │   │   │\n│   │  │  triggered) │  └─────────────┘  └─────────────────────┘   │   │\n│   │  └─────────────┘                                              │   │\n│   │                              │                                │   │\n│   │                              ▼                                │   │\n│   │   ┌────────────────────────────────────────────────────┐     │   │\n│   │   │  NORMALIZER_QUEUE → iw-normalizer (8-stage NA0-5) │     │   │\n│   │   └────────────────────────────────────────────────────┘     │   │\n│   │                              │                                │   │\n│   │                              ▼                                │   │\n│   │   ┌────────────────────────────────────────────────────┐     │   │\n│   │   │  PIPELINE_QUEUE → iw-pipeline (only spine writer)  │     │   │\n│   │   └────────────────────────────────────────────────────┘     │   │\n│   └──────────────────────────────────────────────────────────────┘   │\n└──────────────────────────────────────────────────────────────────────┘\n```\n\n### 3.2 Adapter Philosophy\n\n| Principle              | Implementation                                                                              |\n| ---------------------- | ------------------------------------------------------------------------------------------- |\n| **Co-location**        | Adapters live inside the service that uses them (`act`, `connector`, `connector-sync`)      |\n| **Zero extra hop**     | `act` → adapter → provider. No intermediate orchestrator.                                   |\n| **Interface contract** | `OutboundAdapter` and `InboundSyncAdapter` interfaces are the only contracts.               |\n| **Swappability**       | Change adapter registration in a map. No consumer code changes.                             |\n| **Tenant isolation**   | Every adapter instance is scoped to one tenant. No shared credential state.                 |\n| **Observability**      | Every adapter call emits a `provider_call` telemetry event with latency, status, tenant_id. |\n\n---\n\n## 4. Adapter Taxonomy and Interface Contracts\n\n### 4.1 Core TypeScript Interfaces\n\n```typescript\n// packages/connectors/src/types.ts\n// Shared adapter contract — consumed by act, connector-sync, and connector\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.1 Outbound Adapter Contract (execution)\n// ─────────────────────────────────────────────────────────────\n\nexport interface OutboundAdapter {\n  /** Execute a capability against a provider */\n  execute(request: OutboundExecutionRequest): Promise<OutboundExecutionResult>;\n\n  /** Health check — used by provider health monitor */\n  healthCheck(credentials: Credentials): Promise<ProviderHealthStatus>;\n\n  /** Optional: batch execution for bulk operations */\n  executeBatch?(requests: OutboundExecutionRequest[]): Promise<OutboundExecutionResult[]>;\n}\n\nexport interface OutboundExecutionRequest {\n  /** Capability path: \"salesforce.opportunity.update\" */\n  capability: string;\n\n  /** Tenant identifier — every call is tenant-scoped */\n  tenantId: string;\n\n  /** Operation payload */\n  payload: Record<string, unknown>;\n\n  /** Entity360 context injected by the Continuity Layer */\n  entityContext?: Entity360Context;\n\n  /** Correlation ID for distributed tracing */\n  correlationId: string;\n\n  /** Governance approval token (for writes) */\n  approvalToken?: string;\n\n  /** Timeout override (ms). Default: 30000 */\n  timeoutMs?: number;\n\n  /** Idempotency key for safe retries */\n  idempotencyKey: string;\n}\n\nexport interface OutboundExecutionResult {\n  /** Success / error / partial */\n  status: \"success\" | \"error\" | \"partial\";\n\n  /** Provider-native response (normalized later by pipeline) */\n  rawResponse: Record<string, unknown>;\n\n  /** Canonical entity IDs created/updated (for Spine writeback) */\n  affectedEntities?: AffectedEntity[];\n\n  /** Metadata for audit and telemetry */\n  metadata: {\n    provider: string;\n    adapter: \"nango\" | \"mcp\" | \"native\";\n    latencyMs: number;\n    retryCount: number;\n    circuitBreakerState: CircuitBreakerState;\n    timestamp: number;\n  };\n\n  /** Error details if status !== 'success' */\n  error?: AdapterError;\n}\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.2 Inbound Sync Adapter Contract (sync / webhook)\n// ─────────────────────────────────────────────────────────────\n\nexport interface InboundSyncAdapter {\n  /** Fetch raw records since last sync timestamp */\n  fetch(request: InboundSyncRequest): Promise<InboundSyncResult>;\n\n  /** Webhook handler — parse provider-specific webhook to canonical event */\n  handleWebhook?(request: WebhookRequest): Promise<WebhookResult>;\n\n  /** Resolve provider-specific cursor for incremental sync */\n  getCursor?(credentials: Credentials): Promise<string | null>;\n}\n\nexport interface InboundSyncRequest {\n  tenantId: string;\n  connectorId: string;\n  provider: string;\n  credentials: Credentials;\n\n  /** Last successful sync timestamp */\n  since: Date;\n\n  /** Provider-specific cursor (for cursor-based pagination) */\n  cursor?: string | null;\n\n  /** Max records per page. Default: 500 */\n  pageSize?: number;\n\n  /** Correlation ID */\n  correlationId: string;\n}\n\nexport interface InboundSyncResult {\n  /** Raw provider records — NOT normalized */\n  rawRecords: RawRecord[];\n\n  /** Next cursor, or null if complete */\n  nextCursor: string | null;\n\n  /** Whether more pages exist */\n  hasMore: boolean;\n\n  /** Provider-specific sync state to persist */\n  syncState?: Record<string, unknown>;\n\n  /** Metadata */\n  metadata: {\n    provider: string;\n    adapter: string;\n    recordsFetched: number;\n    latencyMs: number;\n    timestamp: number;\n  };\n}\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.3 Credential Wall Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface Credentials {\n  /** Adapter type determines which adapter factory is used */\n  type: \"nango\" | \"mcp\" | \"native\";\n\n  tenantId: string;\n  provider: string;\n\n  /** Nango: connectionId = tenant_id in practice */\n  connectionId?: string;\n\n  /** Nango: cached access token (short-lived, rotated) */\n  accessToken?: string;\n\n  /** Nango: refresh token (stored in CF Secrets, never logged) */\n  refreshToken?: string;\n\n  /** Native: API key (stored in CF Secrets) */\n  apiKey?: string;\n\n  /** Native / MCP: endpoint URL */\n  endpointUrl?: string;\n\n  /** MCP: server URL for outbound MCP pool */\n  mcpServerUrl?: string;\n\n  /** MCP: tool whitelist for this tenant */\n  allowedTools?: string[];\n\n  /** OAuth: token expiry timestamp */\n  tokenExpiresAt?: number;\n\n  /** OAuth: scopes granted */\n  scopes?: string[];\n\n  /** Custom provider-specific config */\n  config?: Record<string, unknown>;\n}\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.4 Entity360 Context (injected into outbound calls)\n// ─────────────────────────────────────────────────────────────\n\nexport interface Entity360Context {\n  /** The primary entity this action targets */\n  primaryEntity: {\n    id: string; // SSOC stable UUID\n    type: string; // e.g., \"account\", \"contact\", \"deal\"\n    providerId: string; // Provider-native ID\n    displayName: string;\n  };\n\n  /** Related entities (up to 5, ranked by relevance) */\n  relatedEntities: RelatedEntity[];\n\n  /** Recent timeline events for this entity */\n  timeline: TimelineEvent[];\n\n  /** Memory context — what the system \"knows\" about this entity */\n  memory: {\n    lastInteraction: number; // timestamp\n    sentiment: number; // -1.0 to 1.0\n    keyTopics: string[];\n    openTasks: number;\n  };\n\n  /** Tenant-specific projection context (e.g., CS lens, Sales lens) */\n  projectionContext?: Record<string, unknown>;\n}\n\nexport interface RelatedEntity {\n  id: string;\n  type: string;\n  relationship: string; // e.g., \"primary_contact\", \"parent_account\"\n  displayName: string;\n}\n\nexport interface TimelineEvent {\n  timestamp: number;\n  type: \"email\" | \"call\" | \"meeting\" | \"note\" | \"task\" | \"sync\";\n  summary: string;\n  actor: string; // user ID or \"system\"\n}\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.5 Error and Health Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface AdapterError {\n  code:\n    | \"PROVIDER_UNAVAILABLE\" // 5xx from provider\n    | \"AUTH_EXPIRED\" // 401/403\n    | \"RATE_LIMITED\" // 429\n    | \"INVALID_PAYLOAD\" // 400 from provider\n    | \"TIMEOUT\" // Adapter timeout\n    | \"CIRCUIT_OPEN\" // Circuit breaker blocked call\n    | \"NOT_FOUND\" // 404\n    | \"PERMISSION_DENIED\" // Scope insufficient\n    | \"UNKNOWN\"; // Catch-all\n  message: string;\n  providerErrorCode?: string;\n  providerErrorMessage?: string;\n  retryable: boolean;\n  suggestedRetryAfterMs?: number;\n}\n\nexport type CircuitBreakerState = \"CLOSED\" | \"OPEN\" | \"HALF_OPEN\";\n\nexport interface ProviderHealthStatus {\n  provider: string;\n  tenantId: string;\n  state: CircuitBreakerState;\n  lastCheckAt: number;\n  consecutiveFailures: number;\n  latencyP50: number;\n  latencyP99: number;\n  errorRate: number; // 0.0 - 1.0 over last 5 min\n  nextRetryAt?: number; // Only when OPEN\n}\n\n// ─────────────────────────────────────────────────────────────\n// 4.1.6 Affected Entity (for Spine writeback)\n// ─────────────────────────────────────────────────────────────\n\nexport interface AffectedEntity {\n  /** Canonical SSOC ID (assigned by normalizer) */\n  canonicalId: string;\n\n  /** Provider-native ID */\n  providerId: string;\n\n  /** Entity type in canonical schema */\n  type: string;\n\n  /** Operation performed */\n  operation: \"created\" | \"updated\" | \"deleted\" | \"noop\";\n\n  /** Provider name */\n  provider: string;\n}\n```\n\n### 4.2 Adapter Registry Maps\n\n```typescript\n// packages/connectors/src/registry.ts\n// Adapter registration — the ONLY place provider → adapter mapping is defined.\n// Changing a provider from Nango to MCP is a one-line change here.\n\n\n// ─────────────────────────────────────────────────────────────\n// OUTBOUND ADAPTER REGISTRY\n// ─────────────────────────────────────────────────────────────\n\n  string,\n  {\n    adapter: new () => OutboundAdapter;\n    credentialType: \"nango\" | \"mcp\" | \"native\";\n    defaultTimeoutMs: number;\n    supportsBatch: boolean;\n    circuitBreaker: {\n      failureThreshold: number;\n      recoveryTimeoutMs: number;\n      halfOpenMaxCalls: number;\n    };\n  }\n> = {\n  // Nango-powered OAuth providers (mature SaaS)\n  salesforce: {\n    adapter: NangoAdapter,\n    credentialType: \"nango\",\n    defaultTimeoutMs: 30000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30000, halfOpenMaxCalls: 3 },\n  },\n  hubspot: {\n    adapter: NangoAdapter,\n    credentialType: \"nango\",\n    defaultTimeoutMs: 30000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30000, halfOpenMaxCalls: 3 },\n  },\n  slack: {\n    adapter: NangoAdapter,\n    credentialType: \"nango\",\n    defaultTimeoutMs: 15000,\n    supportsBatch: false, // Slack API is chat-oriented\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 20000, halfOpenMaxCalls: 2 },\n  },\n  notion: {\n    adapter: NangoAdapter,\n    credentialType: \"nango\",\n    defaultTimeoutMs: 20000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30000, halfOpenMaxCalls: 3 },\n  },\n\n  // MCP-native providers (AI-first tool APIs)\n  github: {\n    adapter: MCPOutboundAdapter,\n    credentialType: \"mcp\",\n    defaultTimeoutMs: 30000,\n    supportsBatch: false,\n    circuitBreaker: { failureThreshold: 3, recoveryTimeoutMs: 60000, halfOpenMaxCalls: 1 },\n  },\n  linear: {\n    adapter: MCPOutboundAdapter,\n    credentialType: \"mcp\",\n    defaultTimeoutMs: 20000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 3, recoveryTimeoutMs: 60000, halfOpenMaxCalls: 1 },\n  },\n  figma: {\n    adapter: MCPOutboundAdapter,\n    credentialType: \"mcp\",\n    defaultTimeoutMs: 20000,\n    supportsBatch: false,\n    circuitBreaker: { failureThreshold: 3, recoveryTimeoutMs: 60000, halfOpenMaxCalls: 1 },\n  },\n\n  // Native REST providers (direct HTTP, no abstraction)\n  stripe: {\n    adapter: NativeAdapter,\n    credentialType: \"native\",\n    defaultTimeoutMs: 30000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30000, halfOpenMaxCalls: 3 },\n  },\n  aws: {\n    adapter: NativeAdapter,\n    credentialType: \"native\",\n    defaultTimeoutMs: 60000,\n    supportsBatch: true,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 60000, halfOpenMaxCalls: 3 },\n  },\n  custom: {\n    adapter: NativeAdapter,\n    credentialType: \"native\",\n    defaultTimeoutMs: 30000,\n    supportsBatch: false,\n    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30000, halfOpenMaxCalls: 3 },\n  },\n};\n\n// ─────────────────────────────────────────────────────────────\n// INBOUND SYNC ADAPTER REGISTRY\n// ─────────────────────────────────────────────────────────────\n\n  string,\n  {\n    adapter: new () => InboundSyncAdapter;\n    syncMode: \"poll\" | \"webhook\" | \"hybrid\";\n    defaultIntervalMinutes: number;\n    supportsIncremental: boolean;\n  }\n> = {\n  salesforce: {\n    adapter: SalesforceSyncAdapter,\n    syncMode: \"hybrid\", // Webhook for realtime + poll for backup\n    defaultIntervalMinutes: 360, // 6 hours creamy sync\n    supportsIncremental: true,\n  },\n  hubspot: {\n    adapter: HubSpotSyncAdapter,\n    syncMode: \"hybrid\",\n    defaultIntervalMinutes: 360,\n    supportsIncremental: true,\n  },\n  slack: {\n    adapter: SlackSyncAdapter,\n    syncMode: \"webhook\", // Slack is event-driven\n    defaultIntervalMinutes: 0, // No polling\n    supportsIncremental: false,\n  },\n  stripe: {\n    adapter: StripeSyncAdapter,\n    syncMode: \"webhook\",\n    defaultIntervalMinutes: 0,\n    supportsIncremental: false,\n  },\n};\n```\n\n---\n\n## 5. Outbound Execution Flow (act Service)\n\n### 5.1 Service Structure\n\n```\nservices/act/\n├── src/\n│   ├── index.ts                    # Worker entry — ACT_QUEUE consumer\n│   ├── execute.ts                  # Main execution orchestrator\n│   ├── batch-execute.ts            # Batch execution (bulk ops)\n│   ├── adapters/\n│   │   ├── index.ts                # Adapter factory + registry\n│   │   ├── nango.ts                # Nango OAuth adapter\n│   │   ├── mcp-outbound.ts         # MCP outbound adapter\n│   │   ├── native.ts               # Native REST adapter\n│   │   ├── credentials.ts          # Credential wall (CF Secrets / KV / Nango)\n│   │   ├── context-inject.ts       # Entity360 injection\n│   │   └── health.ts               # Per-provider health checks\n│   ├── circuit-breaker/\n│   │   ├── breaker.ts              # Circuit breaker state machine\n│   │   ├── store.ts                # KV-backed breaker state\n│   │   └── monitor.ts              # Background health probe\n│   ├── telemetry.ts                # Provider call telemetry\n│   └── types.ts                    # Service-local types\n├── wrangler.toml\n└── package.json\n```\n\n### 5.2 Execution Flow Diagram\n\n```\nACT_QUEUE (Cloudflare Queues)\n    │\n    ▼\n┌─────────────────────────────────────────────────────────────┐\n│  iw-act  (Cloudflare Worker)                                 │\n│                                                              │\n│  1. DEQUEUE                                                  │\n│     │ Parse message: { capability, tenantId, payload,        │\n│     │   correlationId, idempotencyKey, approvalToken }       │\n│     ▼                                                        │\n│  2. IDEMPOTENCY CHECK                                        │\n│     │ Query KV: idempotencyKey exists?                       │\n│     │ Yes → return cached result                             │\n│     │ No → proceed                                           │\n│     ▼                                                        │\n│  3. CREDENTIAL WALL                                          │\n│     │ credentials.ts → getCredentials(provider, tenantId)    │\n│     │                                                        │\n│     │   CF Secrets ──► decrypt API key / refresh token       │\n│     │   Nango API ───► fetch access token (cached 5 min)     │\n│     │   KV cache ────► short-lived token cache               │\n│     │                                                        │\n│     │ Security: NEVER log tokens. NEVER return to caller.    │\n│     ▼                                                        │\n│  4. CIRCUIT BREAKER CHECK                                    │\n│     │ breaker.ts → getState(provider, tenantId)              │\n│     │                                                        │\n│     │   CLOSED   → proceed                                   │\n│     │   OPEN     → reject with CIRCUIT_OPEN error            │\n│     │   HALF_OPEN→ allow limited probes, then decide         │\n│     ▼                                                        │\n│  5. ENTITY360 INJECTION                                      │\n│     │ context-inject.ts → assembleEntity360(tenantId,        │\n│     │   payload.entity_id)                                   │\n│     │                                                        │\n│     │   Spine query ──► entity by SSOC ID                    │\n│     │   Memory query ──► recent interactions                 │\n│     │   Relation query ──► linked entities                   │\n│     │   Timeline query ──► recent events                     │\n│     │                                                        │\n│     │ Result: Entity360Context enriched payload              │\n│     ▼                                                        │\n│  6. ADAPTER SELECTION + EXECUTION                            │\n│     │ adapters/index.ts → selectAdapter(provider)            │\n│     │                                                        │\n│     │   nango.ts      ──► Nango proxy API                    │\n│     │   mcp-outbound.ts ──► MCP client (SSE/stdio)           │\n│     │   native.ts     ──► fetch() with custom headers        │\n│     │                                                        │\n│     │ Each adapter implements OutboundAdapter interface.     │\n│     ▼                                                        │\n│  7. TELEMETRY EMIT                                           │\n│     │ telemetry.ts → emitProviderCall({...})                 │\n│     │   → Cloudflare Workers Analytics                       │\n│     │   → telemetry service (async)                          │\n│     ▼                                                        │\n│  8. CIRCUIT BREAKER UPDATE                                   │\n│     │ On success: reset failure count                        │\n│     │ On failure: increment, check threshold, maybe OPEN     │\n│     ▼                                                        │\n│  9. SPINE WRITEBACK                                          │\n│     │ enqueueToPipelineQueue({ type: 'execution_result',     │\n│     │   tenantId, capability, result, affectedEntities })    │\n│     │                                                        │\n│     │ ONLY pipeline writes to Spine. act NEVER writes D1.    │\n│     ▼                                                        │\n│  10. IDEMPOTENCY CACHE                                       │\n│     │ KV.set(idempotencyKey, result, { expirationTtl: 86400 })│\n│     │                                                        │\n│     └────────────────────────────────────────────────────────►│\n│                                                              │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### 5.3 execute.ts — Canonical Implementation\n\n```typescript\n// services/act/src/execute.ts\n\n\nconst IDEMPOTENCY_TTL = 86400; // 24 hours\n\nexport async function executeCapability(\n  message: ActQueueMessage\n): Promise<OutboundExecutionResult> {\n  const startTime = Date.now();\n  const { capability, tenantId, payload, correlationId, idempotencyKey } = message;\n  const [provider, resource, operation] = capability.split(\".\");\n\n  // ── 1. IDEMPOTENCY ──────────────────────────────────────────\n  const cached = await kv.get<OutboundExecutionResult>(`idemp:${idempotencyKey}`);\n  if (cached) {\n    return {\n      ...cached,\n      metadata: { ...cached.metadata, fromCache: true },\n    } as OutboundExecutionResult;\n  }\n\n  try {\n    // ── 2. CREDENTIAL WALL ────────────────────────────────────\n    const credentials = await getCredentials(provider, tenantId);\n    if (!credentials) {\n      throw adapterError(\"AUTH_EXPIRED\", `No credentials for ${provider}/${tenantId}`, false);\n    }\n\n    // ── 3. CIRCUIT BREAKER ────────────────────────────────────\n    const breakerState = await checkCircuitBreaker(provider, tenantId);\n    if (breakerState === \"OPEN\") {\n      throw adapterError(\"CIRCUIT_OPEN\", `Circuit breaker OPEN for ${provider}/${tenantId}`, false);\n    }\n\n    // ── 4. ENTITY360 INJECTION ────────────────────────────────\n    const entityContext = payload.entity_id\n      ? await assembleEntity360(tenantId, payload.entity_id as string)\n      : undefined;\n\n    // ── 5. ADAPTER EXECUTION ──────────────────────────────────\n    const adapter = selectAdapter(provider);\n    const request: OutboundExecutionRequest = {\n      capability,\n      tenantId,\n      payload,\n      entityContext,\n      correlationId,\n      approvalToken: message.approvalToken,\n      idempotencyKey,\n    };\n\n    const result = await adapter.execute(request);\n\n    // ── 6. TELEMETRY ──────────────────────────────────────────\n    await emitProviderCall({\n      tenantId,\n      provider,\n      capability,\n      latencyMs: Date.now() - startTime,\n      status: result.status,\n      correlationId,\n    });\n\n    // ── 7. CIRCUIT BREAKER UPDATE ─────────────────────────────\n    await recordResult(provider, tenantId, result.status === \"success\");\n\n    // ── 8. SPINE WRITEBACK ────────────────────────────────────\n    await enqueueToPipelineQueue({\n      type: \"execution_result\",\n      tenantId,\n      capability,\n      result,\n      timestamp: Date.now(),\n      correlationId,\n    });\n\n    // ── 9. IDEMPOTENCY CACHE ──────────────────────────────────\n    await kv.put(`idemp:${idempotencyKey}`, JSON.stringify(result), {\n      expirationTtl: IDEMPOTENCY_TTL,\n    });\n\n    return result;\n  } catch (err) {\n    // ── ERROR PATH ────────────────────────────────────────────\n    const adapterErr = normalizeError(err);\n\n    await recordResult(provider, tenantId, false);\n\n    await emitProviderCall({\n      tenantId,\n      provider,\n      capability,\n      latencyMs: Date.now() - startTime,\n      status: \"error\",\n      errorCode: adapterErr.code,\n      correlationId,\n    });\n\n    // Even errors write to pipeline for audit completeness\n    await enqueueToPipelineQueue({\n      type: \"execution_error\",\n      tenantId,\n      capability,\n      error: adapterErr,\n      timestamp: Date.now(),\n      correlationId,\n    });\n\n    throw adapterErr;\n  }\n}\n```\n\n---\n\n## 6. Inbound Sync Flow (connector-sync Service)\n\n### 6.1 Service Structure\n\n```\nservices/connector-sync/\n├── src/\n│   ├── index.ts                    # Worker entry — cron + queue\n│   ├── sync.ts                     # Sync orchestrator\n│   ├── webhook-handler.ts          # Webhook → sync adapter routing\n│   ├── adapters/\n│   │   ├── index.ts                # Sync adapter factory\n│   │   ├── salesforce-sync.ts      # SOQL + bulk API sync\n│   │   ├── hubspot-sync.ts         // HubSpot CRM sync\n│   │   ├── slack-sync.ts           // Conversations + users sync\n│   │   └── stripe-sync.ts          // Events API sync\n│   ├── scheduler.ts                // Cron schedule manager\n│   ├── state.ts                    // Sync cursor/state persistence\n│   └── types.ts\n├── wrangler.toml\n└── package.json\n```\n\n### 6.2 Sync Flow Diagram\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                        INBOUND SYNC FLOW                            │\n│                                                                     │\n│   TRIGGER SOURCES                                                   │\n│   ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐    │\n│   │  Cloudflare │  │  Nango      │  │  Provider webhook       │    │\n│   │  Cron (6h)  │  │  webhook    │  │  (Salesforce, Stripe)   │    │\n│   └──────┬──────┘  └──────┬──────┘  └────────────┬────────────┘    │\n│          │                │                      │                 │\n│          └────────────────┼──────────────────────┘                 │\n│                           ▼                                         │\n│   ┌───────────────────────────────────────────────────────────┐    │\n│   │           CONNECTOR_SYNC_QUEUE                            │    │\n│   │  { tenantId, connectorId, provider, trigger, timestamp }  │    │\n│   └────────────────────────┬──────────────────────────────────┘    │\n│                            │                                        │\n│                            ▼                                        │\n│   ┌───────────────────────────────────────────────────────────┐    │\n│   │  iw-connector-sync  (Cloudflare Worker)                   │    │\n│   │                                                           │    │\n│   │  1. DEQUEUE                                               │    │\n│   │     │ Load connector config from D1                       │    │\n│   │     │   WHERE tenant_id = ? AND connector_id = ?          │    │\n│   │     ▼                                                     │    │\n│   │  2. RESOLVE CREDENTIALS                                   │    │\n│   │     │ Same credential wall as outbound (getCredentials)   │    │\n│   │     │ Connection ID = tenant_id (isolated)                │    │\n│   │     ▼                                                     │    │\n│   │  3. LOAD SYNC STATE                                       │    │\n│   │     │ KV: sync_state:{tenantId}:{connectorId}             │    │\n│   │     │   { lastSyncAt, cursor, recordCount }               │    │\n│   │     ▼                                                     │    │\n│   │  4. SELECT SYNC ADAPTER                                   │    │\n│   │     │ Factory looks up inboundSyncAdapterRegistry         │    │\n│   │     │   SalesforceSyncAdapter, HubSpotSyncAdapter, etc.   │    │\n│   │     ▼                                                     │    │\n│   │  5. FETCH RAW RECORDS                                     │    │\n│   │     │ Adapter calls provider API                          │    │\n│   │     │   Salesforce → SOQL REST API                        │    │\n│   │     │   HubSpot    → CRM API v3                           │    │\n│   │     │   Slack      → Conversations API                    │    │\n│   │     │   Stripe     → Events API                           │    │\n│   │     ▼                                                     │    │\n│   │  6. PERSIST STATE                                         │    │\n│   │     │ Save cursor, timestamp to KV                        │    │\n│   │     ▼                                                     │    │\n│   │  7. ENQUEUE TO NORMALIZER                                 │    │\n│   │     │ NORMALIZER_QUEUE: { tenantId, provider, records }   │    │\n│   │     │                                                     │    │\n│   │     │ Security: Records are RAW — no parsing here.        │    │\n│   │     │   Normalizer owns schema transformation.            │    │\n│   │     ▼                                                     │    │\n│   │  8. EMIT SYNC TELEMETRY                                   │    │\n│   │     │ Records fetched, latency, provider, tenant          │    │\n│   │     │                                                     │    │\n│   └───────────────────────────────────────────────────────────┘    │\n│                            │                                        │\n│                            ▼                                        │\n│   ┌───────────────────────────────────────────────────────────┐    │\n│   │  iw-normalizer  (8-stage pipeline: NA0 → NA5)             │    │\n│   │                                                           │    │\n│   │  NA0: Schema detection     ──► detect provider schema     │    │\n│   │  NA1: Canonical transform  ──► map to canonical types     │    │\n│   │  NA2: SSOC resolution      ──► stable UUID assignment     │    │\n│   │  NA3: Lineage tracking     ──► provenance + audit trail   │    │\n│   │  NA4: Relation binding     ──► link to existing entities  │    │\n│   │  NA5: Spine publish        ──► emit to PIPELINE_QUEUE     │    │\n│   └───────────────────────────────────────────────────────────┘    │\n│                            │                                        │\n│                            ▼                                        │\n│   ┌───────────────────────────────────────────────────────────┐    │\n│   │  iw-pipeline  (ONLY writer to D1 Spine)                   │    │\n│   │                                                           │    │\n│   │  INSERT entities, relationships                           │    │\n│   │  WRITE spine_audit_log                                    │    │\n│   │  WRITE sync_job_log                                       │    │\n│   │                                                           │    │\n│   │  Invariant: pipeline is the SOLE writer.                  │    │\n│   │    No other service writes to D1 entities table.          │    │\n│   └───────────────────────────────────────────────────────────┘    │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### 6.3 Webhook-to-Sync Routing\n\nWhen a provider webhook arrives at `webhook-ingress`, it is **not** directly processed. It is routed through the same adapter pattern:\n\n```typescript\n// services/webhook-ingress/src/index.ts\n\nexport async function handleWebhook(request: Request): Promise<Response> {\n  // 1. Verify provider signature (Salesforce HMAC, Stripe sig, etc.)\n  const provider = extractProvider(request);\n  const signature = request.headers.get(\"X-Webhook-Signature\");\n  if (!verifyWebhookSignature(provider, signature, await request.text())) {\n    return new Response(\"Invalid signature\", { status: 401 });\n  }\n\n  // 2. Extract tenant from webhook metadata (never from URL param)\n  const tenantId = await resolveTenantFromWebhook(provider, request);\n  if (!tenantId) {\n    return new Response(\"Unknown tenant\", { status: 404 });\n  }\n\n  // 3. Enqueue to connector-sync — do NOT process inline\n  //    (webhook ingress must be fast; sync processing is async)\n  await enqueueToConnectorSyncQueue({\n    type: \"webhook\",\n    tenantId,\n    provider,\n    payload: await request.json(),\n    receivedAt: Date.now(),\n    correlationId: crypto.randomUUID(),\n  });\n\n  return new Response(\"Accepted\", { status: 202 });\n}\n```\n\n---\n\n## 7. Credential Wall\n\n### 7.1 Design Principle\n\nThe Credential Wall is the **single point of secret retrieval** for the entire Provider Fabric. No adapter fetches credentials directly. No service caches credentials outside the wall. Every credential access is logged, audited, and tenant-isolated.\n\n### 7.2 Storage Hierarchy\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                  CREDENTIAL WALL ARCHITECTURE                 │\n│                                                             │\n│   TIER 1: Cloudflare Secrets (Encrypted at rest)            │\n│   ┌─────────────────────────────────────────────────────┐   │\n│   │  Secret Name Pattern:                               │   │\n│   │    PROVIDER_{PROVIDER}_{TENANT_ID}_API_KEY         │   │\n│   │    PROVIDER_{PROVIDER}_{TENANT_ID}_REFRESH_TOKEN   │   │\n│   │    PROVIDER_{PROVIDER}_{TENANT_ID}_CLIENT_SECRET   │   │\n│   │                                                     │   │\n│   │  Examples:                                          │   │\n│   │    PROVIDER_SALESFORCE_T_abc123_API_KEY            │   │\n│   │    PROVIDER_STRIPE_T_abc123_API_KEY                │   │\n│   │    PROVIDER_GITHUB_T_abc123_TOKEN                  │   │\n│   │                                                     │   │\n│   │  Access: Only via env.getSecret() in Workers       │   │\n│   │  Rotation: Admin CLI or Nango auto-refresh         │   │\n│   └─────────────────────────────────────────────────────┘   │\n│                            │                                │\n│   TIER 2: Nango Vault (OAuth token storage)                 │\n│   ┌─────────────────────────────────────────────────────┐   │\n│   │  connectionId = tenant_id  (by convention)          │   │\n│   │                                                     │   │\n│   │  Nango stores:                                      │   │\n│   │    - access_token  (short-lived, ~1 hour)           │   │\n│   │    - refresh_token  (long-lived, rotated by Nango)  │   │\n│   │    - expires_at                                     │   │\n│   │    - connection_config (scopes, metadata)           │   │\n│   │                                                     │   │\n│   │  Access: Nango REST API with NANGO_SECRET_KEY       │   │\n│   └─────────────────────────────────────────────────────┘   │\n│                            │                                │\n│   TIER 3: KV Cache (Performance, NOT security)              │\n│   ┌─────────────────────────────────────────────────────┐   │\n│   │  Key: cred_cache:{provider}:{tenantId}              │   │\n│   │  TTL: 300 seconds (5 min)                           │   │\n│   │  Content: Decrypted access token (ephemeral)        │   │\n│   │                                                     │   │\n│   │  WARNING: KV is NOT encrypted. Tokens are short-    │   │\n│   │  lived and rotate frequently. Never cache refresh   │   │\n│   │  tokens in KV.                                      │   │\n│   └─────────────────────────────────────────────────────┘   │\n│                            │                                │\n│                            ▼                                │\n│   ┌─────────────────────────────────────────────────────┐   │\n│   │              getCredentials() FLOW                  │   │\n│   │                                                     │   │\n│   │  1. Check KV cache first (fast path)                │   │\n│   │     Hit → return cached token (check expiry)        │   │\n│   │                                                     │   │\n│   │  2. Check credential type from connector config     │   │\n│   │     nango  → call Nango API for access token        │   │\n│   │     native → fetch from CF Secrets                  │   │\n│   │     mcp    → fetch endpoint + token from Secrets    │   │\n│   │                                                     │   │\n│   │  3. If token expired and refreshable:               │   │\n│   │     Nango → auto-refresh (Nango handles this)       │   │\n│   │     Native → use refresh_token → new access_token   │   │\n│   │                                                     │   │\n│   │  4. Cache in KV (5 min TTL)                         │   │\n│   │                                                     │   │\n│   │  5. Return credentials to adapter                   │   │\n│   │                                                     │   │\n│   │  AUDIT: Every retrieval logged to                   │   │\n│   │    spine_audit_log with action: CREDENTIAL_ACCESS   │   │\n│   └─────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### 7.3 getCredentials Implementation\n\n```typescript\n// services/act/src/adapters/credentials.ts\n// services/connector-sync/src/adapters/credentials.ts\n// (shared via packages/connectors, but implemented per-service)\n\n\nconst KV_CACHE_TTL = 300; // 5 minutes\n\nexport async function getCredentials(\n  provider: string,\n  tenantId: string\n): Promise<Credentials | null> {\n  // 1. Check KV cache\n  const cacheKey = `cred_cache:${provider}:${tenantId}`;\n  const cached = await kv.get<Credentials>(cacheKey);\n  if (cached && !isExpired(cached)) {\n    return cached;\n  }\n\n  // 2. Load connector config to determine credential type\n  const config = await getConnectorConfig(provider, tenantId);\n  if (!config) return null;\n\n  let credentials: Credentials;\n\n  switch (config.credentialType) {\n    case \"nango\": {\n      const nangoCreds = await fetchNangoCredentials(provider, tenantId);\n      credentials = {\n        type: \"nango\",\n        tenantId,\n        provider,\n        connectionId: tenantId, // Convention: connectionId = tenantId\n        accessToken: nangoCreds.access_token,\n        refreshToken: undefined, // Nango manages refresh; we never see it\n        tokenExpiresAt: nangoCreds.expires_at,\n        scopes: nangoCreds.scopes,\n      };\n      break;\n    }\n\n    case \"native\": {\n      const apiKey = await env.getSecret(`PROVIDER_${provider.toUpperCase()}_${tenantId}_API_KEY`);\n      const endpointUrl = config.endpointUrl;\n      credentials = {\n        type: \"native\",\n        tenantId,\n        provider,\n        apiKey,\n        endpointUrl,\n      };\n      break;\n    }\n\n    case \"mcp\": {\n      const mcpToken = await env.getSecret(\n        `PROVIDER_${provider.toUpperCase()}_${tenantId}_MCP_TOKEN`\n      );\n      const mcpServerUrl = config.mcpServerUrl;\n      const allowedTools = config.allowedTools;\n      credentials = {\n        type: \"mcp\",\n        tenantId,\n        provider,\n        mcpServerUrl,\n        accessToken: mcpToken,\n        allowedTools,\n      };\n      break;\n    }\n\n    default:\n      return null;\n  }\n\n  // 3. Cache in KV (ephemeral — token is short-lived anyway)\n  await kv.put(cacheKey, JSON.stringify(credentials), {\n    expirationTtl: KV_CACHE_TTL,\n  });\n\n  // 4. Audit log\n  await logCredentialAccess(provider, tenantId, config.credentialType);\n\n  return credentials;\n}\n\nfunction isExpired(creds: Credentials): boolean {\n  if (!creds.tokenExpiresAt) return false;\n  return Date.now() >= creds.tokenExpiresAt - 60000; // 1 min buffer\n}\n```\n\n---\n\n## 8. Entity360 Injection Protocol\n\n### 8.1 Purpose\n\nEvery outbound call carries **context** from the Continuity Layer. This is not just \"pass the entity ID.\" It is a structured, ranked, truncated context package that lets the provider adapter (or the provider itself, if MCP-native) make semantically richer decisions.\n\n### 8.2 Assembly Flow\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                 ENTITY360 ASSEMBLY PIPELINE                         │\n│                                                                     │\n│   INPUT: tenantId, entityId (SSOC UUID)                             │\n│                                                                     │\n│   STEP 1: PRIMARY ENTITY RESOLUTION                                 │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Query Spine (via service binding to spine-v2)              │   │\n│   │    SELECT * FROM entities                                   │   │\n│   │    WHERE tenant_id = ? AND ssoc_id = ?                      │   │\n│   │                                                             │   │\n│   │  Result: { id, type, provider_ids, traits, metadata }       │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│   STEP 2: RELATED ENTITIES (max 5)                                  │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Query relationships table                                  │   │\n│   │    SELECT * FROM relationships                              │   │\n│   │    WHERE tenant_id = ?                                      │   │\n│   │      AND (source_id = ? OR target_id = ?)                   │   │\n│   │    ORDER BY strength DESC                                   │   │\n│   │    LIMIT 5                                                  │   │\n│   │                                                             │   │\n│   │  Resolve each related entity's display name + type          │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│   STEP 3: TIMELINE (last 10 events)                                 │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Query memory / audit log                                   │   │\n│   │    SELECT * FROM spine_audit_log                            │   │\n│   │    WHERE tenant_id = ? AND entity_id = ?                    │   │\n│   │    ORDER BY timestamp DESC                                  │   │\n│   │    LIMIT 10                                                 │   │\n│   │                                                             │   │\n│   │  Normalize to TimelineEvent[]                               │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│   STEP 4: MEMORY CONTEXT                                            │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Query continuity service (memory tier)                     │   │\n│   │    { lastInteraction, sentiment, keyTopics, openTasks }     │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│   STEP 5: CONTEXT WINDOW OPTIMIZATION                               │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Truncate timeline to fit within budget                     │   │\n│   │  Rank related entities by relevance score                   │   │\n│   │  Serialize to compact JSON                                  │   │\n│   │                                                             │   │\n│   │  Budget: max 8KB per outbound call (configurable)           │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│                              ▼                                      │\n│   OUTPUT: Entity360Context object                                   │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### 8.3 context-inject.ts Implementation\n\n```typescript\n// services/act/src/adapters/context-inject.ts\n\n\nconst MAX_RELATED_ENTITIES = 5;\nconst MAX_TIMELINE_EVENTS = 10;\nconst CONTEXT_BUDGET_BYTES = 8192; // 8KB\n\nexport async function assembleEntity360(\n  tenantId: string,\n  entityId: string\n): Promise<Entity360Context | undefined> {\n  // Query spine-v2 via service binding\n  const spine = getSpineBinding();\n\n  const [entity, relations, timeline, memory] = await Promise.all([\n    spine.getEntity(tenantId, entityId),\n    spine.getRelatedEntities(tenantId, entityId, MAX_RELATED_ENTITIES),\n    spine.getEntityTimeline(tenantId, entityId, MAX_TIMELINE_EVENTS),\n    getMemoryContext(tenantId, entityId),\n  ]);\n\n  if (!entity) return undefined;\n\n  const context: Entity360Context = {\n    primaryEntity: {\n      id: entity.ssoc_id,\n      type: entity.type,\n      providerId: entity.provider_ids?.[0] ?? entity.id,\n      displayName: entity.traits?.display_name ?? entity.traits?.name ?? \"Unknown\",\n    },\n    relatedEntities: relations.map((r: any) => ({\n      id: r.target_id === entityId ? r.source_id : r.target_id,\n      type: r.target_type,\n      relationship: r.relationship_type,\n      displayName: r.target_name ?? \"Unknown\",\n    })),\n    timeline: timeline.map((t: any) => ({\n      timestamp: t.timestamp,\n      type: t.event_type,\n      summary: t.summary,\n      actor: t.actor_id ?? \"system\",\n    })),\n    memory: memory ?? {\n      lastInteraction: 0,\n      sentiment: 0,\n      keyTopics: [],\n      openTasks: 0,\n    },\n  };\n\n  // Budget enforcement\n  const serialized = JSON.stringify(context);\n  if (serialized.length > CONTEXT_BUDGET_BYTES) {\n    return truncateContext(context, CONTEXT_BUDGET_BYTES);\n  }\n\n  return context;\n}\n\nfunction truncateContext(ctx: Entity360Context, budget: number): Entity360Context {\n  // Truncate timeline first (least critical)\n  while (JSON.stringify(ctx).length > budget && ctx.timeline.length > 3) {\n    ctx.timeline.pop();\n  }\n  // Then related entities\n  while (JSON.stringify(ctx).length > budget && ctx.relatedEntities.length > 2) {\n    ctx.relatedEntities.pop();\n  }\n  // Last resort: truncate summaries\n  if (JSON.stringify(ctx).length > budget) {\n    ctx.timeline = ctx.timeline.map((t) => ({\n      ...t,\n      summary: t.summary.substring(0, 100),\n    }));\n  }\n  return ctx;\n}\n```\n\n### 8.4 MCP-Native Context Passing\n\nFor MCP outbound adapters, Entity360 is injected into the MCP `tool` call as a structured `_context` parameter:\n\n```typescript\n// services/act/src/adapters/mcp-outbound.ts\n\nasync function execute(request: OutboundExecutionRequest): Promise<OutboundExecutionResult> {\n  const { capability, payload, entityContext, credentials } = request;\n\n  // Build MCP tool call\n  const toolCall = {\n    name: capability,\n    arguments: {\n      ...payload,\n      // Entity360 injected as structured context\n      _iw_context: entityContext\n        ? {\n            entity: entityContext.primaryEntity,\n            related: entityContext.relatedEntities,\n            memory: entityContext.memory,\n            // Timeline omitted for MCP (too large); summary only\n            summary: entityContext.timeline\n              .slice(0, 3)\n              .map((t) => t.summary)\n              .join(\"; \"),\n          }\n        : undefined,\n    },\n  };\n\n  // Call provider MCP server\n  const mcpClient = new MCPClient({\n    serverUrl: credentials.mcpServerUrl!,\n    authToken: credentials.accessToken!,\n  });\n\n  const result = await mcpClient.callTool(toolCall);\n  return normalizeMCPResult(result, capability);\n}\n```\n\n---\n\n## 9. Tool-to-Tool Communication Protocol (T2T)\n\n### 9.1 Purpose\n\nThe Tool-to-Tool (T2T) protocol enables **providers to communicate with each other through the Bridge**, not peer-to-peer. When Salesforce needs to notify Slack, or GitHub needs to update Linear, the request flows through the Provider Fabric — mediated, governed, and audited.\n\n### 9.2 Why Not Peer-to-Peer?\n\n| Peer-to-Peer        | T2T via Bridge                                |\n| ------------------- | --------------------------------------------- |\n| No governance       | Governed by confidence gate                   |\n| No audit trail      | Full `spine_audit_log` + `outbound_mcp_calls` |\n| No context sharing  | Entity360 injected automatically              |\n| Credential sprawl   | Single credential wall                        |\n| No tenant isolation | `WHERE tenant_id = ?` on every hop            |\n\n### 9.3 T2T Flow\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                    TOOL-TO-TOOL PROTOCOL                            │\n│                                                                     │\n│   SCENARIO: Salesforce opportunity closes → Slack notification      │\n│                                                                     │\n│   Salesforce                                                        │\n│       │ webhook: \"opportunity.closed_won\"                           │\n│       ▼                                                             │\n│   ┌───────────────────┐                                             │\n│   │ webhook-ingress   │  verify signature, route to tenant          │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ connector-sync    │  SalesforceSyncAdapter.handleWebhook()      │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ normalizer        │  NA0-NA5: normalize to canonical event      │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ pipeline          │  WRITE Spine: event entity + relationships   │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ twin-orchestrator │  AMBIENT Twin detects pattern:              │\n│   │  (AMBIENT)        │  \"closed_won → notify channel\"              │\n│   └─────────┬─────────┘                                             │\n│             │ Proposes: send_slack_notification                     │\n│             │ Confidence: 0.92                                      │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ govern            │  ≥0.85 → AUTO-APPROVE                       │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ ACT_QUEUE         │  { capability: \"slack.chat.postMessage\",    │\n│   └─────────┬─────────┘    payload: { channel, text, blocks } }     │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ act               │  NangoAdapter.execute()                     │\n│   │                   │  Entity360 injected (account context)       │\n│   │                   │  Credential wall: Slack token for tenant    │\n│   └─────────┬─────────┘                                             │\n│             ▼                                                       │\n│   ┌───────────────────┐                                             │\n│   │ Slack API         │  Message posted                             │\n│   └───────────────────┘                                             │\n│                                                                     │\n│   RESULT:                                                           │\n│   - Slack message sent with account context                         │\n│   - Full audit trail in spine_audit_log                             │\n│   - No direct Salesforce → Slack connection ever existed            │\n│   - If Slack is down: circuit breaker OPEN, retry later             │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### 9.4 T2T Message Schema\n\n```typescript\n// packages/connectors/src/t2t.ts\n\nexport interface T2TRequest {\n  /** The source provider that triggered this chain */\n  sourceProvider: string;\n\n  /** The target capability to invoke */\n  targetCapability: string;\n\n  /** Tenant scope */\n  tenantId: string;\n\n  /** Canonical entity ID that triggered the chain */\n  triggerEntityId: string;\n\n  /** The full Entity360 context from the source */\n  sourceContext: Entity360Context;\n\n  /** Proposed payload for the target capability */\n  proposedPayload: Record<string, unknown>;\n\n  /** Why this T2T was proposed (Twin reasoning trace) */\n  reasoningTrace: string;\n\n  /** Governance approval token (pre-approved by govern) */\n  approvalToken: string;\n\n  /** Max chain depth to prevent infinite loops */\n  maxChainDepth: number;\n\n  /** Current depth in the chain */\n  currentDepth: number;\n}\n\nexport interface T2TResult {\n  /** Whether the target call succeeded */\n  status: \"success\" | \"error\" | \"rejected\";\n\n  /** Target call result */\n  targetResult?: OutboundExecutionResult;\n\n  /** Next T2T requests (if chain continues) */\n  nextSteps?: T2TRequest[];\n\n  /** Chain depth consumed */\n  chainDepthConsumed: number;\n\n  /** Full audit record */\n  auditRecord: {\n    tenantId: string;\n    sourceProvider: string;\n    targetCapability: string;\n    triggerEntityId: string;\n    timestamp: number;\n    correlationId: string;\n  };\n}\n\n/** Max chain depth to prevent infinite T2T loops */\n\n/** T2T chain registry — tracks in-flight chains to detect cycles */\nexport interface T2TChainRegistry {\n  /** Start a new chain or extend an existing one */\n  registerStep(\n    correlationId: string,\n    step: { from: string; to: string; timestamp: number }\n  ): Promise<boolean>; // returns false if cycle detected\n\n  /** Close a chain */\n  closeChain(correlationId: string): Promise<void>;\n}\n```\n\n### 9.5 T2T Governance Rules\n\n1. **Chain depth limit:** Max 3 hops. A → B → C. No further.\n2. **Cycle detection:** `salesforce → slack → salesforce` is blocked by T2TChainRegistry.\n3. **Capability whitelist:** Only capabilities in `t2t_allowed_capabilities` config can be T2T targets.\n4. **Approval required:** Every T2T hop re-evaluates confidence. Auto-approve only if ≥0.85.\n5. **Audit completeness:** Every hop logs to `spine_audit_log` with `via: t2t`.\n6. **Tenant isolation:** T2T cannot cross tenant boundaries. Ever.\n\n---\n\n## 10. Provider Health Monitoring\n\n### 10.1 Health Check Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                PROVIDER HEALTH MONITORING                           │\n│                                                                     │\n│   HEALTH PROBE SCHEDULER  (inside act service)                      │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  Cron: Every 60 seconds per active provider/tenant pair     │   │\n│   │                                                             │   │\n│   │  For each (provider, tenantId) with active connection:      │   │\n│   │    1. Call adapter.healthCheck(credentials)                │   │\n│   │    2. Record latency, status code, error (if any)          │   │\n│   │    3. Update KV: health:{provider}:{tenantId}              │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│                              ▼                                      │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  HEALTH STATE MACHINE  (per provider/tenant)                │   │\n│   │                                                             │   │\n│   │  State: CLOSED  ──► healthy, traffic allowed                │   │\n│   │         OPEN    ──► unhealthy, all calls rejected fast      │   │\n│   │         HALF_OPEN ──► probing with limited traffic          │   │\n│   │                                                             │   │\n│   │  Transitions:                                               │   │\n│   │    CLOSED → OPEN:     consecutiveFailures >= threshold      │   │\n│   │    OPEN → HALF_OPEN:  recoveryTimeoutMs elapsed             │   │\n│   │    HALF_OPEN → CLOSED: halfOpenMaxCalls consecutive success │   │\n│   │    HALF_OPEN → OPEN:   any failure during probe             │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n│                              │                                      │\n│                              ▼                                      │\n│   ┌─────────────────────────────────────────────────────────────┐   │\n│   │  HEALTH DASHBOARD DATA  (exposed via intelligence service)  │   │\n│   │                                                             │   │\n│   │  GET /api/v1/health/providers                               │   │\n│   │  Returns per-tenant provider health:                        │   │\n│   │    [                                                        │   │\n│   │      { provider: \"salesforce\", state: \"CLOSED\",             │   │\n│   │        latencyP50: 234, latencyP99: 1200,                   │   │\n│   │        errorRate: 0.01, lastCheckAt: 1234567890 },          │   │\n│   │      { provider: \"slack\", state: \"OPEN\",                    │   │\n│   │        nextRetryAt: 1234570890 }                            │   │\n│   │    ]                                                        │   │\n│   └─────────────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### 10.2 Health Check Implementation\n\n```typescript\n// services/act/src/circuit-breaker/monitor.ts\n\n\nconst HEALTH_CHECK_INTERVAL_MS = 60000;\n\nexport async function runHealthChecks(): Promise<void> {\n  // Load all active provider/tenant pairs from D1\n  const connections = await db\n    .selectFrom(\"connector_connections\")\n    .select([\"provider\", \"tenant_id\"])\n    .where(\"status\", \"=\", \"active\")\n    .execute();\n\n  for (const conn of connections) {\n    await checkProviderHealth(conn.provider, conn.tenant_id);\n  }\n}\n\nasync function checkProviderHealth(\n  provider: string,\n  tenantId: string\n): Promise<ProviderHealthStatus> {\n  const start = Date.now();\n\n  try {\n    // Fetch credentials via credential wall\n    const credentials = await getCredentials(provider, tenantId);\n    if (!credentials) {\n      return buildHealthStatus(provider, tenantId, \"OPEN\", 0, 1);\n    }\n\n    // Run adapter-specific health check\n    const adapterConfig = outboundAdapterRegistry[provider];\n    const AdapterClass = adapterConfig?.adapter;\n    if (!AdapterClass) {\n      return buildHealthStatus(provider, tenantId, \"OPEN\", 0, 1);\n    }\n\n    const adapter = new AdapterClass();\n    const health = await adapter.healthCheck(credentials);\n\n    const latencyMs = Date.now() - start;\n\n    // Update rolling metrics in KV\n    await updateHealthMetrics(provider, tenantId, {\n      latencyMs,\n      success: health.state !== \"OPEN\",\n    });\n\n    return health;\n  } catch (err) {\n    const latencyMs = Date.now() - start;\n    await updateHealthMetrics(provider, tenantId, {\n      latencyMs,\n      success: false,\n      error: (err as Error).message,\n    });\n\n    return buildHealthStatus(provider, tenantId, \"OPEN\", latencyMs, 1);\n  }\n}\n\nfunction buildHealthStatus(\n  provider: string,\n  tenantId: string,\n  state: CircuitBreakerState,\n  latencyMs: number,\n  consecutiveFailures: number\n): ProviderHealthStatus {\n  return {\n    provider,\n    tenantId,\n    state,\n    lastCheckAt: Date.now(),\n    consecutiveFailures,\n    latencyP50: latencyMs,\n    latencyP99: latencyMs,\n    errorRate: state === \"OPEN\" ? 1.0 : 0.0,\n  };\n}\n```\n\n---\n\n## 11. Circuit Breaker\n\n### 11.1 State Machine\n\n```\n                    ┌─────────────────────────────────────────┐\n                    │                                         │\n                    │   ┌─────────┐      success              │\n           ┌────────┼──►│  CLOSED │◄────────────────────┐     │\n           │        │   └────┬────┘                     │     │\n           │        │        │ failure                   │     │\n           │        │        ▼                           │     │\n           │        │   ┌─────────┐  failure >= threshold│     │\n  recovery │        │   │  COUNT  │──────────────────────┼─────┼──►┌──────┐\n  timeout  │        │   │ FAILURES│                      │     │   │ OPEN │\n  elapsed  │        │   └─────────┘                      │     │   └──┬───┘\n           │        │                                    │     │      │\n           │        └────────────────────────────────────┘     │      │\n           │                                                   │      │\n           │         ┌─────────────────────────────────────────┘      │\n           │         │                                                │\n           │    ┌────┴────┐    halfOpenMaxCalls                      │\n           └───┄│HALF_OPEN│◄─────────────────────────────────────────┘\n                └────┬────┘\n                     │\n        ┌────────────┼────────────┐\n        │ success    │    failure │\n        ▼            │            ▼\n    ┌───────┐        │        ┌──────┐\n    │ CLOSED│        │        │ OPEN │\n    └───────┘        │        └──────┘\n                     │\n```\n\n### 11.2 KV-Backed State Storage\n\n```typescript\n// services/act/src/circuit-breaker/store.ts\n\ninterface CircuitBreakerStateData {\n  state: CircuitBreakerState;\n  failures: number;\n  lastFailureAt: number;\n  lastSuccessAt: number;\n  openedAt?: number;\n  halfOpenCalls: number;\n  halfOpenSuccesses: number;\n}\n\nconst CB_KEY_PREFIX = \"cb:\";\n\nexport async function getBreakerState(\n  provider: string,\n  tenantId: string\n): Promise<CircuitBreakerStateData> {\n  const key = `${CB_KEY_PREFIX}${provider}:${tenantId}`;\n  const stored = await kv.get<CircuitBreakerStateData>(key);\n\n  if (!stored) {\n    return {\n      state: \"CLOSED\",\n      failures: 0,\n      lastFailureAt: 0,\n      lastSuccessAt: 0,\n      halfOpenCalls: 0,\n      halfOpenSuccesses: 0,\n    };\n  }\n\n  return stored;\n}\n\nexport async function setBreakerState(\n  provider: string,\n  tenantId: string,\n  state: CircuitBreakerStateData\n): Promise<void> {\n  const key = `${CB_KEY_PREFIX}${provider}:${tenantId}`;\n  await kv.put(key, JSON.stringify(state), {\n    // State persists for 24 hours to survive deploys\n    expirationTtl: 86400,\n  });\n}\n```\n\n### 11.3 breaker.ts — Core Logic\n\n```typescript\n// services/act/src/circuit-breaker/breaker.ts\n\n\nexport async function checkCircuitBreaker(\n  provider: string,\n  tenantId: string\n): Promise<CircuitBreakerState> {\n  const config = outboundAdapterRegistry[provider]?.circuitBreaker;\n  if (!config) return \"CLOSED\"; // No config = no breaker\n\n  const state = await getBreakerState(provider, tenantId);\n\n  switch (state.state) {\n    case \"CLOSED\":\n      return \"CLOSED\";\n\n    case \"OPEN\":\n      // Check if recovery timeout has elapsed\n      if (state.openedAt && Date.now() - state.openedAt >= config.recoveryTimeoutMs) {\n        // Transition to HALF_OPEN\n        await setBreakerState(provider, tenantId, {\n          ...state,\n          state: \"HALF_OPEN\",\n          halfOpenCalls: 0,\n          halfOpenSuccesses: 0,\n        });\n        return \"HALF_OPEN\";\n      }\n      return \"OPEN\";\n\n    case \"HALF_OPEN\":\n      // Allow limited calls through\n      if (state.halfOpenCalls >= config.halfOpenMaxCalls) {\n        return \"OPEN\"; // Too many probes, back to OPEN\n      }\n      return \"HALF_OPEN\";\n  }\n}\n\nexport async function recordResult(\n  provider: string,\n  tenantId: string,\n  success: boolean\n): Promise<void> {\n  const config = outboundAdapterRegistry[provider]?.circuitBreaker;\n  if (!config) return;\n\n  const state = await getBreakerState(provider, tenantId);\n\n  if (success) {\n    // Success path\n    if (state.state === \"HALF_OPEN\") {\n      const newSuccesses = state.halfOpenSuccesses + 1;\n      if (newSuccesses >= config.halfOpenMaxCalls) {\n        // Fully recovered\n        await setBreakerState(provider, tenantId, {\n          state: \"CLOSED\",\n          failures: 0,\n          lastFailureAt: 0,\n          lastSuccessAt: Date.now(),\n          halfOpenCalls: 0,\n          halfOpenSuccesses: 0,\n        });\n      } else {\n        await setBreakerState(provider, tenantId, {\n          ...state,\n          halfOpenSuccesses: newSuccesses,\n          halfOpenCalls: state.halfOpenCalls + 1,\n        });\n      }\n    } else {\n      await setBreakerState(provider, tenantId, {\n        ...state,\n        failures: 0,\n        lastSuccessAt: Date.now(),\n      });\n    }\n  } else {\n    // Failure path\n    const newFailures = state.failures + 1;\n\n    if (state.state === \"HALF_OPEN\") {\n      // Any failure in HALF_OPEN → immediately OPEN\n      await setBreakerState(provider, tenantId, {\n        ...state,\n        state: \"OPEN\",\n        failures: newFailures,\n        lastFailureAt: Date.now(),\n        openedAt: Date.now(),\n      });\n    } else if (newFailures >= config.failureThreshold) {\n      // Threshold crossed → OPEN\n      await setBreakerState(provider, tenantId, {\n        ...state,\n        state: \"OPEN\",\n        failures: newFailures,\n        lastFailureAt: Date.now(),\n        openedAt: Date.now(),\n      });\n    } else {\n      await setBreakerState(provider, tenantId, {\n        ...state,\n        failures: newFailures,\n        lastFailureAt: Date.now(),\n      });\n    }\n  }\n}\n```\n\n---\n\n## 12. Security Boundaries\n\n### 12.1 Trust Zones\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│                         TRUST ZONES                                     │\n│                                                                         │\n│  ┌─────────────────────────────────────────────────────────────────┐   │\n│  │ ZONE 0: PUBLIC INTERNET                                         │   │\n│  │  Untrusted. All traffic TLS-terminated at Cloudflare edge.      │   │\n│  │  WAF rules apply. Rate limiting enforced.                       │   │\n│  └─────────────────────────────────────────────────────────────────┘   │\n│                              │                                          │\n│                              ▼                                          │\n│  ┌─────────────────────────────────────────────────────────────────┐   │\n│  │ ZONE 1: INGRESS (gateway, webhook-ingress, mcp-connector)       │   │\n│  │  JWT validation. Tenant extraction from JWT claim ONLY.         │   │\n│  │  No provider credentials in this zone.                          │   │\n│  └─────────────────────────────────────────────────────────────────┘   │\n│                              │                                          │\n│                              ▼                                          │\n│  ┌─────────────────────────────────────────────────────────────────┐   │\n│  │ ZONE 2: CONTINUITY KERNEL (Spine, Memory, Twin, Govern)         │   │\n│  │  Tenant-isolated via WHERE tenant_id = ? on every query.        │   │\n│  │  No provider credentials. No external API calls.                │   │\n│  │  This is THE MOAT.                                              │   │\n│  └─────────────────────────────────────────────────────────────────┘   │\n│                              │                                          │\n│                              ▼                                          │\n│  ┌─────────────────────────────────────────────────────────────────┐   │\n│  │ ZONE 3: PROVIDER FABRIC (act, connector, connector-sync)        │   │\n│  │  CREDENTIAL WALL: Only zone that reads CF Secrets / Nango.     │   │\n│  │  Adapter modules execute external calls.                        │   │\n│  │  Circuit breaker blocks failing providers.                      │   │\n│  │  Every call audited. Every call tenant-scoped.                  │   │\n│  └─────────────────────────────────────────────────────────────────┘   │\n│                              │                                          │\n│                              ▼                                          │\n│  ┌─────────────────────────────────────────────────────────────────┐   │\n│  │ ZONE 4: EXTERNAL PROVIDERS (Salesforce, Slack, GitHub, ...)     │   │\n│  │  Untrusted. Ephemeral connections. No persistent state.         │   │\n│  │  Provider signatures verified on inbound.                       │   │\n│  └─────────────────────────────────────────────────────────────────┘   │\n│                                                                         │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n### 12.2 Security Rules (P0)\n\n| #   | Rule                              | Enforcement                                                                          | Violation         |\n| --- | --------------------------------- | ------------------------------------------------------------------------------------ | ----------------- |\n| 1   | No credential outside Zone 3      | CF Secrets bindings only on `act`, `connector`, `connector-sync`                     | Security incident |\n| 2   | Tenant isolation at adapter layer | Every adapter call includes `tenantId`; adapter resolves credentials per-tenant only | 403               |\n| 3   | No credential logging             | Tokens, API keys, refresh tokens NEVER logged or returned                            | Security incident |\n| 4   | Read-only default for MCP         | Inbound MCP pool default `scope: read`. Write requires `scope: write` + governance   | 403               |\n| 5   | Tool whitelist per tenant         | MCP outbound only calls tools in `allowedTools` config                               | 403               |\n| 6   | Circuit breaker blocks abuse      | Failed auth repeated 5x → circuit OPEN → no more credential exposure                 | Automatic         |\n| 7   | Idempotency prevents replay       | Every outbound call carries `idempotencyKey`                                         | Duplicate ignored |\n| 8   | Webhook signature verification    | Every inbound webhook verified against provider secret                               | 401 if invalid    |\n| 9   | Approval token for writes         | Every write capability requires governance `approvalToken`                           | 403               |\n| 10  | Audit completeness                | Every provider call logged to `spine_audit_log` + `outbound_mcp_calls`               | Halt operation    |\n\n---\n\n## 13. Integration Points with Adjacent Domains\n\n### 13.1 Northbound: Continuity Layer (Spine, Memory, Twin)\n\n| Direction         | Integration                     | Contract                                                 |\n| ----------------- | ------------------------------- | -------------------------------------------------------- |\n| **Inbound**       | `connector-sync` → `normalizer` | `NORMALIZER_QUEUE` — raw records + tenantId + provider   |\n| **Inbound**       | `normalizer` → `pipeline`       | `PIPELINE_QUEUE` — canonical entities + relationships    |\n| **Outbound**      | `govern` → `act`                | `ACT_QUEUE` — approved action + payload + Entity360      |\n| **Bidirectional** | `act` ↔ `spine-v2`              | Service binding — Entity360 assembly reads Spine         |\n| **Bidirectional** | `act` ↔ `continuity`            | Service binding — memory retrieval for context injection |\n\n### 13.2 East/West: Governance Layer\n\n| Direction    | Integration               | Contract                                              |\n| ------------ | ------------------------- | ----------------------------------------------------- |\n| **Inbound**  | `govern` → `act`          | `ACT_QUEUE` messages include `approvalToken`          |\n| **Outbound** | `act` → `govern`          | On failure, `governance_audit_log` updated with error |\n| **Inbound**  | `govern` policy → adapter | `allowedTools` per tenant restricts MCP outbound      |\n\n### 13.3 Southbound: External Providers\n\n| Provider   | Adapter              | Protocol        | Credential Type         |\n| ---------- | -------------------- | --------------- | ----------------------- |\n| Salesforce | `NangoAdapter`       | REST / Bulk API | Nango OAuth             |\n| HubSpot    | `NangoAdapter`       | REST API v3     | Nango OAuth             |\n| Slack      | `NangoAdapter`       | Web API         | Nango OAuth             |\n| GitHub     | `MCPOutboundAdapter` | MCP (SSE)       | MCP token               |\n| Linear     | `MCPOutboundAdapter` | MCP (SSE)       | MCP token               |\n| Figma      | `MCPOutboundAdapter` | MCP (SSE)       | MCP token               |\n| Stripe     | `NativeAdapter`      | REST API        | API key (CF Secrets)    |\n| AWS        | `NativeAdapter`      | SDK / REST      | Access key (CF Secrets) |\n| Notion     | `NangoAdapter`       | REST API        | Nango OAuth             |\n\n### 13.4 Registry Integration\n\nThe Provider Fabric reads from and writes to the **Registry Layer** (Layer 13):\n\n```typescript\n// Provider registration at tenant onboarding\ninterface ProviderRegistration {\n  provider: string;\n  tenantId: string;\n  credentialType: \"nango\" | \"mcp\" | \"native\";\n  status: \"pending\" | \"active\" | \"error\" | \"disconnected\";\n  connectedAt: number;\n  lastSyncAt?: number;\n  capabilities: string[]; // e.g., [\"salesforce.opportunity.read\", ...]\n  allowedTools?: string[]; // For MCP providers\n  syncConfig?: {\n    mode: \"poll\" | \"webhook\" | \"hybrid\";\n    intervalMinutes: number;\n    supportsIncremental: boolean;\n  };\n}\n\n// Registered in D1 `provider_registry` table\n// Read by Discovery to build per-tenant capability catalog\n// Read by adapter factory to select correct adapter\n```\n\n---\n\n## 14. Error Handling, Retries, and Dead-Letter Behavior\n\n### 14.1 Retry Matrix\n\n| Error Code             | Retryable  | Retry Strategy        | Max Retries | Backoff                     |\n| ---------------------- | ---------- | --------------------- | ----------- | --------------------------- |\n| `PROVIDER_UNAVAILABLE` | Yes        | Exponential           | 3           | 1s → 2s → 4s                |\n| `RATE_LIMITED`         | Yes        | Fixed delay           | 5           | `Retry-After` header or 60s |\n| `TIMEOUT`              | Yes        | Exponential           | 3           | 2s → 4s → 8s                |\n| `AUTH_EXPIRED`         | Yes (once) | Refresh token → retry | 1           | Immediate                   |\n| `CIRCUIT_OPEN`         | No         | Reject immediately    | 0           | N/A                         |\n| `INVALID_PAYLOAD`      | No         | Log + discard         | 0           | N/A                         |\n| `PERMISSION_DENIED`    | No         | Log + alert admin     | 0           | N/A                         |\n| `NOT_FOUND`            | No         | Log + discard         | 0           | N/A                         |\n\n### 14.2 Dead-Letter Queue (DLQ)\n\n```\nACT_QUEUE_DLQ\n├── Message format: original message + error history + retry count\n├── Trigger: max retries exceeded OR non-retryable error\n├── Processing: telemetry service polls DLQ every 5 minutes\n├── Actions:\n│   ├── Alert admin via `admin` service (P1)\n│   ├── Write to `spine_audit_log` as `execution_error`\n│   ├── If `AUTH_EXPIRED` → trigger credential refresh alert\n│   └── If `PERMISSION_DENIED` → disable connector, notify tenant owner\n└── Retention: 7 days in DLQ, then archived to R2\n```\n\n### 14.3 DLQ Handler\n\n```typescript\n// services/act/src/dlq.ts\n\nexport async function processDLQ(\n  message: ActQueueMessage & { errorHistory: AdapterError[] }\n): Promise<void> {\n  const lastError = message.errorHistory[message.errorHistory.length - 1];\n\n  // Always audit\n  await enqueueToPipelineQueue({\n    type: \"execution_error_dlq\",\n    tenantId: message.tenantId,\n    capability: message.capability,\n    error: lastError,\n    retryCount: message.errorHistory.length,\n    timestamp: Date.now(),\n  });\n\n  switch (lastError.code) {\n    case \"AUTH_EXPIRED\":\n      // Alert: credential needs refresh\n      await alertAdmin({\n        type: \"credential_expired\",\n        tenantId: message.tenantId,\n        provider: message.capability.split(\".\")[0],\n        severity: \"high\",\n      });\n      break;\n\n    case \"PERMISSION_DENIED\":\n      // Auto-disable connector\n      await disableConnector(message.tenantId, message.capability.split(\".\")[0]);\n      await notifyTenantOwner(message.tenantId, {\n        type: \"connector_disabled\",\n        reason: \"permission_denied\",\n        provider: message.capability.split(\".\")[0],\n      });\n      break;\n\n    case \"RATE_LIMITED\":\n      // Auto-backoff: increase circuit breaker recovery time\n      await increaseBreakerRecoveryTimeout(message.capability.split(\".\")[0], message.tenantId);\n      break;\n\n    default:\n      // Log and archive\n      await archiveToR2(message);\n  }\n}\n```\n\n---\n\n## 15. Physical Deployment Map\n\n### 15.1 Cloudflare Workers Topology\n\n| Service           | Worker Name          | Bindings                           | Queues                                     | D1           | KV                                      |\n| ----------------- | -------------------- | ---------------------------------- | ------------------------------------------ | ------------ | --------------------------------------- |\n| `act`             | `iw-act`             | `SPINE_V2`, `CONTINUITY`, `GOVERN` | `ACT_QUEUE`                                | —            | `cred_cache`, `idempotency`, `cb_state` |\n| `connector`       | `iw-connector`       | `TENANTS`                          | —                                          | `connectors` | `oauth_state`                           |\n| `connector-sync`  | `iw-connector-sync`  | `SPINE_V2`, `NORMALIZER`           | `CONNECTOR_SYNC_QUEUE`, `NORMALIZER_QUEUE` | `sync_jobs`  | `sync_state`                            |\n| `webhook-ingress` | `iw-webhook-ingress` | `CONNECTOR_SYNC`                   | `CONNECTOR_SYNC_QUEUE`                     | —            | —                                       |\n\n### 15.2 Secret Bindings\n\n```toml\n# services/act/wrangler.toml\n[name]\nname = \"iw-act\"\n\n[[env.production.vars]]\nNANGO_SECRET_KEY = \"secret_key_from_cf_secrets\"\n\n[[env.production.secrets]]\n# Provider credentials pattern: PROVIDER_{PROVIDER}_{TENANT}_API_KEY\n# Managed by admin CLI, not in wrangler.toml\n\n[[env.production.kv_namespaces]]\nbinding = \"CREDENTIAL_CACHE\"\nid = \"cred_cache_kv_namespace_id\"\n\n[[env.production.kv_namespaces]]\nbinding = \"IDEMPOTENCY_STORE\"\nid = \"idempotency_kv_namespace_id\"\n\n[[env.production.kv_namespaces]]\nbinding = \"CIRCUIT_BREAKER\"\nid = \"circuit_breaker_kv_namespace_id\"\n\n[[queues.producers]]\nbinding = \"ACT_QUEUE\"\nqueue = \"act-queue\"\n\n[[queues.producers]]\nbinding = \"PIPELINE_QUEUE\"\nqueue = \"pipeline-queue\"\n```\n\n---\n\n## 16. Operational Runbooks\n\n### 16.1 Adding a New Provider\n\n```\n1. Add adapter implementation:\n   services/act/src/adapters/{provider}.ts\n   services/connector-sync/src/adapters/sync/{provider}-sync.ts\n\n2. Register in registry:\n   packages/connectors/src/registry.ts\n\n3. Add credential schema:\n   packages/connectors/src/types.ts (Credentials interface)\n\n4. Configure circuit breaker:\n   outboundAdapterRegistry[provider] = { ... }\n\n5. Add health check endpoint in adapter:\n   adapter.healthCheck(credentials)\n\n6. Deploy and test:\n   pnpm --filter act deploy\n   pnpm test:provider {provider}\n\n7. Update Discovery:\n   agent-registry service picks up new capabilities automatically\n```\n\n### 16.2 Rotating Credentials\n\n```\n1. Admin CLI:\n   iw-admin credentials rotate --provider salesforce --tenant T_abc123\n\n2. CLI actions:\n   a. Generate new connection in Nango (or new API key)\n   b. Write to CF Secrets: PROVIDER_SALESFORCE_T_abc123_API_KEY\n   c. Invalidate KV cache: cred_cache:salesforce:T_abc123\n   d. Test: adapter.healthCheck(newCreds)\n   e. If test passes: update connector_connections.status = 'active'\n   f. If test fails: rollback, alert admin\n\n3. No downtime: KV cache miss triggers credential wall re-fetch\n```\n\n### 16.3 Circuit Breaker Manual Override\n\n```\n# Force circuit breaker state (emergency only)\n# Requires admin JWT with scope: admin:provider\n\nPOST /api/v1/admin/circuit-breaker\n{\n  \"provider\": \"salesforce\",\n  \"tenantId\": \"T_abc123\",\n  \"action\": \"force_close\",  // or \"force_open\"\n  \"reason\": \"Emergency maintenance window\"\n}\n\n# Audit: logged to governance_audit_log with admin identity\n```\n\n### 16.4 Provider Health Dashboard Query\n\n```sql\n-- Run in D1 console or via intelligence service\nSELECT\n  provider,\n  tenant_id,\n  state,\n  COUNT(*) as tenant_count,\n  AVG(latency_p50) as avg_latency,\n  SUM(CASE WHEN state = 'OPEN' THEN 1 ELSE 0 END) as open_count\nFROM provider_health_status\nWHERE last_check_at > datetime('now', '-1 hour')\nGROUP BY provider, state\nORDER BY open_count DESC;\n```\n\n---\n\n## Appendix A: Queue Message Schemas\n\n### A.1 ACT_QUEUE\n\n```typescript\ninterface ActQueueMessage {\n  type: \"execute_capability\" | \"execute_batch\" | \"t2t_request\";\n  capability: string;\n  tenantId: string;\n  payload: Record<string, unknown>;\n  correlationId: string;\n  idempotencyKey: string;\n  approvalToken?: string;\n  entityId?: string; // For Entity360 injection\n  t2tContext?: T2TRequest; // For tool-to-tool chains\n}\n```\n\n### A.2 PIPELINE_QUEUE (from act)\n\n```typescript\ninterface PipelineQueueMessage {\n  type: \"execution_result\" | \"execution_error\" | \"execution_error_dlq\";\n  tenantId: string;\n  capability: string;\n  result?: OutboundExecutionResult;\n  error?: AdapterError;\n  affectedEntities?: AffectedEntity[];\n  timestamp: number;\n  correlationId: string;\n}\n```\n\n### A.3 CONNECTOR_SYNC_QUEUE\n\n```typescript\ninterface ConnectorSyncQueueMessage {\n  type: \"scheduled_sync\" | \"webhook\" | \"creamy_sync\";\n  tenantId: string;\n  connectorId: string;\n  provider: string;\n  trigger: \"cron\" | \"webhook\" | \"manual\";\n  payload?: Record<string, unknown>; // For webhooks\n  correlationId: string;\n}\n```\n\n### A.4 NORMALIZER_QUEUE\n\n```typescript\ninterface NormalizerQueueMessage {\n  tenantId: string;\n  provider: string;\n  records: RawRecord[];\n  syncJobId: string;\n  timestamp: number;\n  correlationId: string;\n}\n```\n\n---\n\n## Appendix B: Change Log from v1.0.1\n\n| Change                | v1.0.1                              | v2.0.0 (this doc)                                                     |\n| --------------------- | ----------------------------------- | --------------------------------------------------------------------- |\n| Integration Manager   | Separate service (incorrectly)      | Pattern inside `act` + `connector-sync`                               |\n| Adapter location      | `integration-manager/src/adapters/` | `services/act/src/adapters/`, `services/connector-sync/src/adapters/` |\n| Entity360             | Mentioned but not specified         | Full assembly pipeline, budget enforcement, MCP injection             |\n| T2T Protocol          | Not defined                         | Formal protocol with chain limits, cycle detection, governance        |\n| Circuit breaker       | Mentioned                           | Full state machine, KV storage, per-provider/tenant configuration     |\n| Credential wall       | `getCredentials()` sketch           | Full tiered storage (Secrets → Nango → KV), audit logging             |\n| Health monitoring     | Not defined                         | Per-provider/tenant health checks, metrics, dashboard API             |\n| DLQ                   | Not defined                         | Retry matrix, DLQ handler, auto-alerts                                |\n| Security boundaries   | P0 list                             | Full trust zone model with 4 zones                                    |\n| TypeScript interfaces | Basic `OutboundAdapter`             | Complete type system: 15+ interfaces, registry maps, error taxonomy   |\n\n---\n\n**END OF SPECIFICATION**\n\n_IntegrateWise Continuity Bridge v2.0 — Provider Fabric and Adapter Pattern. This document is a Stage 1/2 deep-dive feeding into the canonical v2.0 architecture document. All interfaces, flows, and schemas are production-intended and must be validated against the live repo before implementation._\n"}