Unclassified

Runtime Profile, Entry Channel and Navigation Contract

Date: 2026-07-30
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.
Depends on: ARCHITECTURE_LOCK.md (12 kernel terms, frozen runtime sequence), CODEBASE_EXCAVATION.md (current drift baseline).


1. Why this document exists

Three different things in this repository are called "profile":

SourceValuesAxis it actually encodes
packages/types/src/handoff.ts:60, services/intelligence/src/tenant.ts:8customer_zero | externalCapability exposure / trust tier
CUSTOMER_ZERO_TO_EXTERNAL_PRODUCT_AND_MARKETPLACE_SPEC.md §3cz-plugin, external-plugin, cz-mcp-workbench, external-mcp-workbenchAudience × entry channel (2×2)
Product direction (2026-07-30)development | customer_zero | externalOperating context

These 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.

Resolution: stop storing the product of the axes. Store the axes.


2. The three axes

2.1 Profile — which capabilities exist, and who may administer

type Profile = "development" | "customer_zero" | "external";

Replaces 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.

2.2 Entry channel — where identity comes from, and where UI renders

type EntryChannel = "connected_tool" | "mcp_workbench" | "plugin_host";
ChannelIdentity sourceUI surfaceConnector origin
connected_toolOAuth with a provider the user already holds (Notion, Vercel, GitHub, Coda)IntegrateWise browser WorkbenchThe authenticating provider is the first connection
mcp_workbenchIW OAuth via MCP registry/marketplaceIntegrateWise browser WorkbenchChosen in the Workbench after landing
plugin_hostHost installation grant (ChatGPT, Claude, Perplexity)Embedded surface inside the hostInherited from the host's existing connections

2.3 Surface — infrastructure isolation only

type Surface = "dev" | "test" | "prod";

Already 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.

2.4 The derived view

The frozen spec's four profiles are profile × entry_channel computed, never stored:

profileentry_channelFrozen spec ID
customer_zeroplugin_hostcz-plugin
externalplugin_hostexternal-plugin
customer_zeromcp_workbenchcz-mcp-workbench
externalmcp_workbenchexternal-mcp-workbench
developmentany(no ID in the frozen spec — this is the gap)
customer_zero / externalconnected_tool(no ID in the frozen spec — this is the second gap)

Keeping 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.


3. Profile-gated exposure — five rows, not twenty-two

Of the platform modules, only these vary by profile:

Moduledevelopmentcustomer_zeroexternal
MCP Poolfull administrationfull administrationnot exposed
Capability Managerall + experimentalall governedtenant-permitted subset
Administrationfullfulltenant-scoped
Billingbypassedinternal accountinglive metering
Deploymentsfullfullnot exposed

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.

Those 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.

This 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.

3.1 On MCP versus MCP Pool

The direction "MCP is for external users while MCP Pool is for development and Customer Zero" does not hold as stated, for two reasons.

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.

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.

Correct statement:

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.


4. One onboarding state machine, three entry channels

services/marketplace-installation/src/index.ts already implements the correct sequence:

initiated → identity_required → workspace_required → consent_required
         → binding → hydrating → active
                              ↘ degraded | revoked | failed

Every entry channel enters this same machine. Only two states are channel-dependent:

Stateconnected_toolmcp_workbenchplugin_host
identity_requiredProvider OAuth (Notion / Vercel / GitHub / Coda)IW OAuth from registryHost installation grant
workspace_requiredTenant from email domain or new-tenant creationTenant from IW identityHost account/team → tenant binding

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.

4.1 Channel class versus provider

InstallationChannel currently flattens three classes into six provider names:

// current — conflates class with provider
type InstallationChannel = "vercel" | "chatgpt" | "github" | "notion" | "coda" | "mcp-directory";

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.

// proposed — orthogonal
interface InstallationOrigin {
  channel: EntryChannel; // connected_tool | mcp_workbench | plugin_host
  provider: string; // "notion" | "chatgpt" | "perplexity" | "claude" | …
  external_installation_id: string;
}

Adding Perplexity becomes catalogue data, not a type change.

4.2 What was fixed in this change

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.

Consequence: 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.

After:

  • Env.MARKETPLACE_INSTALLATION?: Fetcher added to services/gateway/src/index.ts.
  • TARGET_TO_SERVICE.MARKETPLACE_INSTALLATION added for circuit-breaker tracking.
  • /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.
  • 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.
  • Bindings added to all three surfaces with correct isolation:
    integratewise-marketplace-installation (top-level) · -test (env.test) · -prod (env.prod).

/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.


5. The plugin-host runtime

The plugin_host channel differs from the other two in a way that is easy to underestimate.

5.1 The host supplies the agent

In plugin_host, IntegrateWise does not ship inference. ChatGPT, Claude or Perplexity is the reasoning runtime. IntegrateWise supplies:

  • the tool surface (public MCP tools),
  • the truth (Spine),
  • the memory (operational + agent memory, continuity),
  • the governance gate (approval before any external mutation),
  • the embedded UI (host-rendered resource).

The 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.

5.2 Host connector inheritance

The host arrives already connected to N applications. Those connections become a readable ecosystem in the Workbench without IntegrateWise re-authorising each one.

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."

Two rules that must hold:

  1. 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.
  2. 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.

5.3 Cross-host bridging is a Spine function, not a host function

When 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.

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.

The 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.


6. Navigation contract

6.1 Current state: five competing definitions

DefinitionItemsNote
apps/web/src/components/l1/navigation/global-nav-bar.tsxHome, My Work, Contexts, Meetings, Knowledge, Approvals, Timeline, Connections, Contextual guidance, Settings, Help, Administration7 primary + 5 secondary
apps/web/src/config/os-shell-registry.tsInbox, Today, Accounts, Contacts, Deals, Tasks, Connectors, Knowledge, Brainstorm, Goals, Accelerators, Reports, WorkflowsFlat, entity-oriented
apps/web/src/components/l1/workspace/workspace-config.tsRole-scoped groupsContains a nav group literally labelled "L1" (line 163) — user-visible L-layer leak
apps/web/src/components/l1/domains/domain-sidebar.tsxDomain-scoped
packages/workbench-config/src/roles.ts12 roles + PLATFORM_MODULES (6 items)Closest to the target model

The proposed sidebar would be a sixth. It should instead replace all five, with packages/workbench-config as the single home.

6.2 Target structure

Home
My Work            Inbox · Tasks · My Queue · Approvals · Notifications
                   Recent Work · Favorites
Communications     Activity Feed · Email · Calendar · Meetings · Chat
                   Calls · Documents Shared
Personal           Notes · Knowledge Base · Brainstorming Sessions · Activity
Work               ← resolved by Role × Department × Industry
Platform           ← resolved by Profile (§3)

Three observations.

The Twin is correctly absent. No sidebar entry, consistent with "Twin is a contextual capability, not a persistent panel or sidebar destination." Preserve this.

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.

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.

6.3 The Platform group contradicts the existing config comment

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.

Both positions are defensible; they cannot both be encoded. The reconciliation that preserves "the Workbench is one surface":

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.

This 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.

6.4 Navigation per entry channel

ChannelNavigation
connected_toolFull sidebar
mcp_workbenchFull sidebar
plugin_hostConstrained. The host owns the frame.

The 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.

This 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.


7. Repo boundary: operational versus canonical

Stated direction: this repository owns operational memory, the operational DB and agent memory; the platform repository owns core concepts; operational → canonical synchronisation runs between them.

This 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.

The boundary is only safe if it is drawn by write authority, not by proximity:

ConcernOwnerWrite authority
Operational memory, operational DB, agent memory, continuity, active contextThis repoWrites freely; this is its truth
Canonical Spine entitiesPlatform repoThis repo proposes; promotion is governed
ProjectionsThis repoDerived — never a write target

The 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.

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.


8. Implementation order

  1. ✅ Bind marketplace-installation and route the Gateway to it. Done in this change — see §4.2.
  2. 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.
  3. Split InstallationChannel into channel + provider (§4.1).
  4. Rename TenantClass → Profile in packages/types, add development, and move the five-row exposure table behind the Gateway capability manifest.
  5. Consolidate the five navigation definitions into packages/workbench-config; fix the "L1" group label; add the Industry axis as schema-derived.
  6. Decide the plugin_host navigation reduction (§6.4).
  7. Resolve §S8, then draw the repo boundary (§7).

Steps 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.


9. Open decisions

#DecisionBlocks
D1Is plugin_host a peer surface or a reduced companion to the browser Workbench?§6.4, plugin channel implementation
D2Does Platform become one gated destination, or 22 sidebar entries?§6.3, navigation consolidation
D3How is the Industry axis derived — tenant Spine schema, or explicit config?§6.2, workbench-config shape
D4Which Spine writer survives, services/spine or services/pipeline/src/spine?§7, the entire repo split
D5Does development get its own tenant, or is it customer_zero on test?§2.3, profile resolution