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":
| Source | Values | Axis it actually encodes |
|---|---|---|
packages/types/src/handoff.ts:60, services/intelligence/src/tenant.ts:8 | customer_zero | external | Capability exposure / trust tier |
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) |
| Product direction (2026-07-30) | development | customer_zero | external | Operating 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";
| Channel | Identity source | UI surface | Connector origin |
|---|---|---|---|
connected_tool | OAuth with a provider the user already holds (Notion, Vercel, GitHub, Coda) | IntegrateWise browser Workbench | The authenticating provider is the first connection |
mcp_workbench | IW OAuth via MCP registry/marketplace | IntegrateWise browser Workbench | Chosen in the Workbench after landing |
plugin_host | Host installation grant (ChatGPT, Claude, Perplexity) | Embedded surface inside the host | Inherited 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:
| profile | entry_channel | Frozen spec ID |
|---|---|---|
customer_zero | plugin_host | cz-plugin |
external | plugin_host | external-plugin |
customer_zero | mcp_workbench | cz-mcp-workbench |
external | mcp_workbench | external-mcp-workbench |
development | any | (no ID in the frozen spec — this is the gap) |
customer_zero / external | connected_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:
| Module | development | customer_zero | external |
|---|---|---|---|
| MCP Pool | full administration | full administration | not exposed |
| Capability Manager | all + experimental | all governed | tenant-permitted subset |
| Administration | full | full | tenant-scoped |
| Billing | bypassed | internal accounting | live metering |
| Deployments | full | full | not 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
developmentandcustomer_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:
| State | connected_tool | mcp_workbench | plugin_host |
|---|---|---|---|
identity_required | Provider OAuth (Notion / Vercel / GitHub / Coda) | IW OAuth from registry | Host installation grant |
workspace_required | Tenant from email domain or new-tenant creation | Tenant from IW identity | Host 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?: Fetcheradded toservices/gateway/src/index.ts.TARGET_TO_SERVICE.MARKETPLACE_INSTALLATIONadded 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/installationsprefix, so no rewrite is required), carrying the Gateway-injectedx-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:
- 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.
- Inherited connections are still Spine-mediated. Host connector data enters through the same
hydratingpath. 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
| Definition | Items | Note |
|---|---|---|
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 |
apps/web/src/config/os-shell-registry.ts | Inbox, Today, Accounts, Contacts, Deals, Tasks, Connectors, Knowledge, Brainstorm, Goals, Accelerators, Reports, Workflows | Flat, entity-oriented |
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 |
apps/web/src/components/l1/domains/domain-sidebar.tsx | Domain-scoped | |
packages/workbench-config/src/roles.ts | 12 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":
Platformis 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
| Channel | Navigation |
|---|---|
connected_tool | Full sidebar |
mcp_workbench | Full sidebar |
plugin_host | Constrained. 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:
| Concern | Owner | Write authority |
|---|---|---|
| Operational memory, operational DB, agent memory, continuity, active context | This repo | Writes freely; this is its truth |
| Canonical Spine entities | Platform repo | This repo proposes; promotion is governed |
| Projections | This repo | Derived — 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
- ✅ Bind
marketplace-installationand route the Gateway to it. Done in this change — see §4.2. - Fold
services/gateway/src/onboarding.ts(425 LOC) into the state machine as theconnected_toolchannel. It is currently a second, parallel onboarding funnel with its owncompleteOnboarding(); role and connector selection becomeconsent_requiredpayload. Until this lands,connected_toolandmcp_workbenchhydrate differently. - Split
InstallationChannelintochannel+provider(§4.1). - Rename
TenantClass→Profileinpackages/types, adddevelopment, and move the five-row exposure table behind the Gateway capability manifest. - Consolidate the five navigation definitions into
packages/workbench-config; fix the"L1"group label; add the Industry axis as schema-derived. - Decide the
plugin_hostnavigation reduction (§6.4). - 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
| # | Decision | Blocks |
|---|---|---|
| D1 | Is plugin_host a peer surface or a reduced companion to the browser Workbench? | §6.4, plugin channel implementation |
| D2 | Does Platform become one gated destination, or 22 sidebar entries? | §6.3, navigation consolidation |
| D3 | How is the Industry axis derived — tenant Spine schema, or explicit config? | §6.2, workbench-config shape |
| D4 | Which Spine writer survives, services/spine or services/pipeline/src/spine? | §7, the entire repo split |
| D5 | Does development get its own tenant, or is it customer_zero on test? | §2.3, profile resolution |