{"slug":"architecture/RUNTIME_PROFILE_AND_ENTRY_CHANNEL_CONTRACT","title":"Runtime Profile, Entry Channel and Navigation Contract","section":"architecture","sectionLabel":"Architecture","status":"unclassified","type":"document","path":"architecture/RUNTIME_PROFILE_AND_ENTRY_CHANNEL_CONTRACT.mdx","bytes":23593,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n**Date:** 2026-07-30\n**Status:** Proposed — supersedes the stored 4-profile enum in `CUSTOMER_ZERO_TO_EXTERNAL_PRODUCT_AND_MARKETPLACE_SPEC.md §3` by decomposing it, not by replacing it.\n**Depends on:** `ARCHITECTURE_LOCK.md` (12 kernel terms, frozen runtime sequence), `CODEBASE_EXCAVATION.md` (current drift baseline).\n\n---\n\n## 1. Why this document exists\n\nThree different things in this repository are called \"profile\":\n\n| Source                                                                      | Values                                                                       | Axis it actually encodes           |\n| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------- |\n| `packages/types/src/handoff.ts:60`, `services/intelligence/src/tenant.ts:8` | `customer_zero` \\| `external`                                                | Capability exposure / trust tier   |\n| `CUSTOMER_ZERO_TO_EXTERNAL_PRODUCT_AND_MARKETPLACE_SPEC.md §3`              | `cz-plugin`, `external-plugin`, `cz-mcp-workbench`, `external-mcp-workbench` | Audience **×** entry channel (2×2) |\n| Product direction (2026-07-30)                                              | `development` \\| `customer_zero` \\| `external`                               | Operating context                  |\n\nThese are not three revisions of one idea. They are three orthogonal axes collapsed into one word. Choosing \"3\" over \"4\" does not resolve the collision — it selects a different axis and silently discards the other two.\n\n**Resolution: stop storing the product of the axes. Store the axes.**\n\n---\n\n## 2. The three axes\n\n### 2.1 Profile — _which capabilities exist, and who may administer_\n\n```ts\ntype Profile = \"development\" | \"customer_zero\" | \"external\";\n```\n\nReplaces `TenantClass`. `TenantClass` was never a property of a tenant; it was a property of the runtime the tenant is served by. The rename is the point.\n\n### 2.2 Entry channel — _where identity comes from, and where UI renders_\n\n```ts\ntype EntryChannel = \"connected_tool\" | \"mcp_workbench\" | \"plugin_host\";\n```\n\n| Channel          | Identity source                                                             | UI surface                           | Connector origin                                    |\n| ---------------- | --------------------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------- |\n| `connected_tool` | OAuth with a provider the user already holds (Notion, Vercel, GitHub, Coda) | IntegrateWise browser Workbench      | The authenticating provider is the first connection |\n| `mcp_workbench`  | IW OAuth via MCP registry/marketplace                                       | IntegrateWise browser Workbench      | Chosen in the Workbench after landing               |\n| `plugin_host`    | Host installation grant (ChatGPT, Claude, Perplexity)                       | Embedded surface **inside the host** | **Inherited from the host's existing connections**  |\n\n### 2.3 Surface — _infrastructure isolation only_\n\n```ts\ntype Surface = \"dev\" | \"test\" | \"prod\";\n```\n\nAlready correct in `wrangler.toml` for the core nine. Carries no product semantics. A `development` profile normally runs on `test`, but the axes are independent: Customer Zero on `test` is a valid and useful combination.\n\n### 2.4 The derived view\n\nThe frozen spec's four profiles are `profile × entry_channel` **computed**, never stored:\n\n| profile                      | entry_channel    | Frozen spec ID                                        |\n| ---------------------------- | ---------------- | ----------------------------------------------------- |\n| `customer_zero`              | `plugin_host`    | `cz-plugin`                                           |\n| `external`                   | `plugin_host`    | `external-plugin`                                     |\n| `customer_zero`              | `mcp_workbench`  | `cz-mcp-workbench`                                    |\n| `external`                   | `mcp_workbench`  | `external-mcp-workbench`                              |\n| `development`                | any              | _(no ID in the frozen spec — this is the gap)_        |\n| `customer_zero` / `external` | `connected_tool` | _(no ID in the frozen spec — this is the second gap)_ |\n\nKeeping the 2×2 as a derived label preserves the frozen spec's vocabulary in reports and audit records while making it structurally impossible for the label and the underlying state to drift apart.\n\n---\n\n## 3. Profile-gated exposure — five rows, not twenty-two\n\nOf the platform modules, only these vary by profile:\n\n| Module             | `development`       | `customer_zero`     | `external`              |\n| ------------------ | ------------------- | ------------------- | ----------------------- |\n| MCP Pool           | full administration | full administration | **not exposed**         |\n| Capability Manager | all + experimental  | all governed        | tenant-permitted subset |\n| Administration     | full                | full                | tenant-scoped           |\n| Billing            | bypassed            | internal accounting | live metering           |\n| Deployments        | full                | full                | **not exposed**         |\n\n**Everything else is identical across all three profiles**: Identity, Integration Manager, Connector Marketplace, Activation Hub, AI Models, Knowledge, Memory & Continuity, Automation, Pipelines, Governance, Observability, Organizations, Users, Roles, Permissions, Audit, API Keys, Security.\n\nThose modules vary by **tenant scope**, which they already do through existing RBAC. Adding profile branches to them invents variance that does not exist and creates eighteen new drift sites.\n\nThis table is resolved **server-side at the Gateway** and returned in the capability manifest. The frontend never computes it, and never receives the definitions of modules the profile excludes.\n\n### 3.1 On MCP versus MCP Pool\n\nThe direction \"MCP is for external users while MCP Pool is for development and Customer Zero\" does not hold as stated, for two reasons.\n\n**It breaks the parity rule.** `CUSTOMER_ZERO_TO_EXTERNAL_PRODUCT_AND_MARKETPLACE_SPEC.md §3.1` requires Customer Zero to exercise the same MCP tool names, schemas, responses and discovery envelope external users receive. If Customer Zero runs on Pool while external runs on the public MCP surface, Customer Zero stops being a proof of the external contract — which is its entire purpose.\n\n**They are not the same layer.** `services/mcp-pool/src/index.ts` exposes `register`, `unregister`, `capabilities`, `health`, `invoke`, and binds `PROVIDER_MANAGEMENT`, `INTEGRATION_MANAGEMENT`, `TASK_MANAGEMENT`, `SPINE`. It is a **control plane for internal operator MCPs**. It is not a customer-facing tool surface and therefore cannot substitute for one.\n\n**Correct statement:**\n\n> The public MCP surface is byte-identical across all three profiles. MCP Pool is an operator control plane whose _administration UI_ is exposed only in `development` and `customer_zero`. Pool is never a tool surface, and never a per-profile alternative to MCP.\n\n---\n\n## 4. One onboarding state machine, three entry channels\n\n`services/marketplace-installation/src/index.ts` already implements the correct sequence:\n\n```\ninitiated → identity_required → workspace_required → consent_required\n         → binding → hydrating → active\n                              ↘ degraded | revoked | failed\n```\n\nEvery entry channel enters this same machine. Only two states are channel-dependent:\n\n| State                | `connected_tool`                                 | `mcp_workbench`         | `plugin_host`                      |\n| -------------------- | ------------------------------------------------ | ----------------------- | ---------------------------------- |\n| `identity_required`  | Provider OAuth (Notion / Vercel / GitHub / Coda) | IW OAuth from registry  | Host installation grant            |\n| `workspace_required` | Tenant from email domain or new-tenant creation  | Tenant from IW identity | Host account/team → tenant binding |\n\n`consent_required`, `binding`, `hydrating` and `active` are **identical for all channels**. This is the invariant that matters: the Spine write path, hydration, projection composition and governance do not fork per channel. Fork them and you get three onboarding funnels that hydrate differently — which is the current state.\n\n### 4.1 Channel class versus provider\n\n`InstallationChannel` currently flattens three classes into six provider names:\n\n```ts\n// current — conflates class with provider\ntype InstallationChannel = \"vercel\" | \"chatgpt\" | \"github\" | \"notion\" | \"coda\" | \"mcp-directory\";\n```\n\n`vercel`/`github`/`notion`/`coda` are `connected_tool` providers; `chatgpt` is a `plugin_host`; `mcp-directory` is an `mcp_workbench` registry. Routing logic that switches on this union must be edited every time a provider is added.\n\n```ts\n// proposed — orthogonal\ninterface InstallationOrigin {\n  channel: EntryChannel; // connected_tool | mcp_workbench | plugin_host\n  provider: string; // \"notion\" | \"chatgpt\" | \"perplexity\" | \"claude\" | …\n  external_installation_id: string;\n}\n```\n\nAdding Perplexity becomes catalogue data, not a type change.\n\n### 4.2 What was fixed in this change\n\n**Before:** `services/gateway/src/index.ts` handled `/api/v1/marketplace/installations` by rewriting the path to `/api/v1/spine/app_installs` and proxying to `SPINE` as generic table CRUD. `services/marketplace-installation` had **zero inbound service bindings** and was unreachable.\n\nConsequence: on the live path there was no state machine. `consent_required` was never enforced, and `binding → hydrating` was never gated. **Consent is a governance gate, and it was unenforced on every marketplace onboarding path.**\n\n**After:**\n\n- `Env.MARKETPLACE_INSTALLATION?: Fetcher` added to `services/gateway/src/index.ts`.\n- `TARGET_TO_SERVICE.MARKETPLACE_INSTALLATION` added for circuit-breaker tracking.\n- `/api/v1/marketplace/installations*` forwards to the state machine with the path unchanged (the service registers the full `/api/v1/marketplace/installations` prefix, so no rewrite is required), carrying the Gateway-injected `x-tenant-id`.\n- A missing binding now returns **503 with an explicit reason** rather than silently degrading to Spine table writes. Failing closed on a governance gate is the correct behaviour.\n- Bindings added to all three surfaces with correct isolation:\n  `integratewise-marketplace-installation` (top-level) · `-test` (`env.test`) · `-prod` (`env.prod`).\n\n`/api/v1/marketplace/apps` is unchanged and still reads `spine/marketplace_apps` — correct, because the catalogue **is** Spine data. Only installation _state_ needed the state machine.\n\n---\n\n## 5. The plugin-host runtime\n\nThe `plugin_host` channel differs from the other two in a way that is easy to underestimate.\n\n### 5.1 The host supplies the agent\n\nIn `plugin_host`, IntegrateWise does **not** ship inference. ChatGPT, Claude or Perplexity is the reasoning runtime. IntegrateWise supplies:\n\n- the **tool surface** (public MCP tools),\n- the **truth** (Spine),\n- the **memory** (operational + agent memory, continuity),\n- the **governance gate** (approval before any external mutation),\n- the **embedded UI** (host-rendered resource).\n\nThe Twin's silence law is unchanged, but its enforcement point moves: in `mcp_workbench` the Twin is IntegrateWise's own runtime; in `plugin_host` the host's model is doing the reasoning, so **the approval boundary must be enforced server-side at the Gateway, not in UI**. A host model cannot be trusted to respect a client-side gate. This is already the correct design (`Governance is a hard gate. No approval token means no external mutation`) — the plugin channel is simply the case that proves why it must be server-side.\n\n### 5.2 Host connector inheritance\n\nThe host arrives already connected to N applications. Those connections become a readable ecosystem in the Workbench without IntegrateWise re-authorising each one.\n\n`services/gateway/src/connected-ecosystem.ts` is the right facade for this and is already correctly scoped — read-only, and deliberately excluding \"connection identifiers, remote endpoints, credentials, and raw provider metadata.\"\n\nTwo rules that must hold:\n\n1. **Inherited connections are readable, not writable.** A host-granted connection confers the host's read scope. Any _mutation_ through it is a capability execution and passes the governance gate like any other.\n2. **Inherited connections are still Spine-mediated.** Host connector data enters through the same `hydrating` path. The Workbench never reads a provider directly because the host happens to hold the token.\n\n### 5.3 Cross-host bridging is a Spine function, not a host function\n\nWhen the ChatGPT-side agent needs data from a tool that is connected to Claude but not to ChatGPT, the exchange goes **through the Spine**, not host-to-host.\n\n`services/mcp-connector/src/continuity-bridge.ts` (1,312 LOC) is already this primitive — `active-context.ts`, `context-boundary.ts`, `recordContextExchange`, `targetPermitsCapability`, `projectBoundaryContext`. It is the correct place, and it already enforces scope on the exchange.\n\nThe architectural claim worth stating plainly: **Spine is the interop substrate between hosts.** There is no agent-to-agent protocol between ChatGPT and Claude. Each host talks to the Spine; continuity and context boundaries make the exchange legible and auditable. This is a significant differentiator — it means adding a fourth host is a catalogue entry, not an N×N integration.\n\n---\n\n## 6. Navigation contract\n\n### 6.1 Current state: five competing definitions\n\n| Definition                                                 | Items                                                                                                                               | Note                                                                                      |\n| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |\n| `apps/web/src/components/l1/navigation/global-nav-bar.tsx` | Home, My Work, Contexts, Meetings, Knowledge, Approvals, Timeline, Connections, Contextual guidance, Settings, Help, Administration | 7 primary + 5 secondary                                                                   |\n| `apps/web/src/config/os-shell-registry.ts`                 | Inbox, Today, Accounts, Contacts, Deals, Tasks, Connectors, Knowledge, Brainstorm, Goals, Accelerators, Reports, Workflows          | Flat, entity-oriented                                                                     |\n| `apps/web/src/components/l1/workspace/workspace-config.ts` | Role-scoped groups                                                                                                                  | **Contains a nav group literally labelled `\"L1\"`** (line 163) — user-visible L-layer leak |\n| `apps/web/src/components/l1/domains/domain-sidebar.tsx`    | Domain-scoped                                                                                                                       |                                                                                           |\n| `packages/workbench-config/src/roles.ts`                   | 12 roles + `PLATFORM_MODULES` (6 items)                                                                                             | Closest to the target model                                                               |\n\nThe proposed sidebar would be a sixth. It should instead **replace** all five, with `packages/workbench-config` as the single home.\n\n### 6.2 Target structure\n\n```\nHome\nMy Work            Inbox · Tasks · My Queue · Approvals · Notifications\n                   Recent Work · Favorites\nCommunications     Activity Feed · Email · Calendar · Meetings · Chat\n                   Calls · Documents Shared\nPersonal           Notes · Knowledge Base · Brainstorming Sessions · Activity\nWork               ← resolved by Role × Department × Industry\nPlatform           ← resolved by Profile (§3)\n```\n\nThree observations.\n\n**The Twin is correctly absent.** No sidebar entry, consistent with _\"Twin is a contextual capability, not a persistent panel or sidebar destination.\"_ Preserve this.\n\n**`Work` and `Platform` are the only computed groups.** The first four groups are static and identical for every user, role and profile. `Work` resolves through `Role × Department × Industry`; `Platform` resolves through the five-row profile table. Everything else is a constant — which is exactly what makes this structure durable.\n\n**Industry is a new axis.** `packages/workbench-config` currently models `RoleId × ModuleId`. `Role × Department × Industry` is a three-dimensional resolution and 12 roles × ~12 departments × N industries cannot be enumerated as static config. This needs to be **derived from the tenant's activated Spine schema**, not hand-written. The existing `RoleConfig.spineProjection` field is the hook.\n\n### 6.3 The `Platform` group contradicts the existing config comment\n\n`packages/workbench-config/src/roles.ts:14–16` states that platform modules are _\"NOT sidebar destinations that open separate 'workspaces'.\"_ The proposed sidebar makes 22 of them sidebar destinations.\n\nBoth positions are defensible; they cannot both be encoded. The reconciliation that preserves _\"the Workbench is one surface\"_:\n\n> `Platform` is a **single sidebar destination** that opens a settings-style module index **within** the Workbench shell — not 22 top-level entries, and not 22 separate workspaces. The module list inside it is profile-gated per §3.\n\nThis keeps the Workbench one surface, keeps platform administration reachable, and keeps the sidebar scannable. A 40-entry sidebar is also not renderable in a `plugin_host` panel (§6.4), which forces the same conclusion independently.\n\n### 6.4 Navigation per entry channel\n\n| Channel          | Navigation                                |\n| ---------------- | ----------------------------------------- |\n| `connected_tool` | Full sidebar                              |\n| `mcp_workbench`  | Full sidebar                              |\n| `plugin_host`    | **Constrained.** The host owns the frame. |\n\nThe embedded plugin surface cannot render six groups and ~40 items. It needs a documented reduction — a defensible default is `Home`, `My Work`, `Approvals`, and the active `Work` view, with everything else reachable by deep link into the full Workbench.\n\nThis is a genuine open design question, not a detail: it determines whether `plugin_host` is a _peer surface_ to the browser Workbench or a _reduced companion_ to it. The answer changes what \"one Workbench\" means. **Decision required.**\n\n---\n\n## 7. Repo boundary: operational versus canonical\n\nStated direction: this repository owns operational memory, the operational DB and agent memory; the platform repository owns core concepts; operational → canonical synchronisation runs between them.\n\nThis needs care, because `CLAUDE.md` states _\"Spine is the single source of operational truth\"_ — and `CODEBASE_EXCAVATION.md` §S8 already found **two live Spine writers** with conflicting SSOT claims (`services/pipeline/src/index.ts:6` versus `services/spine/src/index.ts:4`, plus `services/billing` asserting Supabase). Splitting Spine across two repositories before resolving that would make a two-way ambiguity a four-way one.\n\nThe boundary is only safe if it is drawn by **write authority**, not by proximity:\n\n| Concern                                                                      | Owner             | Write authority                               |\n| ---------------------------------------------------------------------------- | ----------------- | --------------------------------------------- |\n| Operational memory, operational DB, agent memory, continuity, active context | **This repo**     | Writes freely; this is its truth              |\n| Canonical Spine entities                                                     | **Platform repo** | This repo **proposes**; promotion is governed |\n| Projections                                                                  | This repo         | Derived — never a write target                |\n\nThe synchronisation direction must be one-way with a gate: operational state promotes to canonical through the existing promotion path (`Continuity Bridge → Promotion` in the frozen runtime sequence), never a bidirectional merge. `packages/spine-schema` and `contracts/shared-workbench/v1.ts` are the shared contract surface; both must be versioned before the split, because after it they become a cross-repo API.\n\n**Prerequisite:** resolve §S8 first. Retire `services/pipeline/src/spine/` (self-labelled legacy, Supabase-backed) so that exactly one Spine writer exists, _then_ draw the repo boundary. Drawing it first would encode the ambiguity into the contract.\n\n---\n\n## 8. Implementation order\n\n1. ✅ **Bind `marketplace-installation` and route the Gateway to it.** Done in this change — see §4.2.\n2. **Fold `services/gateway/src/onboarding.ts` (425 LOC) into the state machine** as the `connected_tool` channel. It is currently a second, parallel onboarding funnel with its own `completeOnboarding()`; role and connector selection become `consent_required` payload. Until this lands, `connected_tool` and `mcp_workbench` hydrate differently.\n3. **Split `InstallationChannel` into `channel` + `provider`** (§4.1).\n4. **Rename `TenantClass` → `Profile` in `packages/types`, add `development`**, and move the five-row exposure table behind the Gateway capability manifest.\n5. **Consolidate the five navigation definitions** into `packages/workbench-config`; fix the `\"L1\"` group label; add the Industry axis as schema-derived.\n6. **Decide the `plugin_host` navigation reduction** (§6.4).\n7. **Resolve §S8, then draw the repo boundary** (§7).\n\nSteps 1–3 are prerequisites for the plugin-host channel existing at all: today it has no implementation, and the state machine it would enter was unreachable until step 1.\n\n---\n\n## 9. Open decisions\n\n| #   | Decision                                                                         | Blocks                              |\n| --- | -------------------------------------------------------------------------------- | ----------------------------------- |\n| D1  | Is `plugin_host` a peer surface or a reduced companion to the browser Workbench? | §6.4, plugin channel implementation |\n| D2  | Does `Platform` become one gated destination, or 22 sidebar entries?             | §6.3, navigation consolidation      |\n| D3  | How is the Industry axis derived — tenant Spine schema, or explicit config?      | §6.2, `workbench-config` shape      |\n| D4  | Which Spine writer survives, `services/spine` or `services/pipeline/src/spine`?  | §7, the entire repo split           |\n| D5  | Does `development` get its own tenant, or is it `customer_zero` on `test`?       | §2.3, profile resolution            |\n"}