ADR 0029 — App variants, OpenAPI proxy apps, domain-service surfaces, and Mosaic Aide
This ADR lands the “Mosaic beyond EE” gap program:
- Mosaic Aide — an out-of-the-box named assistant for every app that has
a
vector_db(knowledge + MCP + voice support). - Modern OpenAPI proxy apps —
mosaic import openapinow emits a tessera app that delegates every imported operation to the upstream, with optional cache and knowledge-capture interceptors, and exposes the same operations as MCP tools. - App variants / composites —
app.variant+app.variantsare named, composable overlays (surface, realm, feature, and part-instance composition). - Domain-service surface derivation — a domain
services[].delegate(Aggregate.Command) derives its own REST endpoint and MCP tool. - Dashboard UI — the web lens gains a
/dashboardpage (KPI cards, event mix, recent activity) on top of the existing list/detail/events/ workflow-DAG pages.
1. Mosaic Aide
Context
EE’s “Eezy assistant” is a product-level assistant that wraps the app’s
knowledge, tools, and voice. Mosaic already generated a strong RAG assistant
(POST /api/assistant/chat, /api/aide-less page, assistant_chat MCP tool)
and a separate /voice widget, but the assistant had no stable product name
and no first-class profile/page surface.
Decision
When an app declares at least one vector_db, the app lens emits:
| surface | behavior |
|---|---|
GET /api/aide/profile | returns Mosaic Aide, the active model name, and voice: true |
POST /api/aide/chat | aliases the existing grounded assistant (assistant_chat) |
GET /aide | a self-contained assistant page (conversation id in localStorage, page context omitted for now) |
aide_chat MCP tool | a model-facing tool over /api/aide/chat (query, conversation/page/collection/top_k/stream) |
Mosaic Aide is the assistant brand. It reuses the existing assistant runtime
(history, page grounding, citations, vectordb RAG, no-LLM grounded fallback)
and the existing voice surface; it is not a second engine.
The route is app-lens only (the web lens keeps its own /api/chat surface and
does not emit the Aide page route).
2. Modern OpenAPI proxy apps
Context
ADR 0006 made the legacy kind: proxy tessera a byte pass-through (no
enrichment). That was the right boundary for the legacy service/proxy path,
but the user request asks for an imported OpenAPI service to become a Mosaic
app: delegate calls, add interceptors (cache, knowledge), and expose MCP tools
for every endpoint with the app’s assistant available.
Decision
mosaic import openapi now emits a modern tessera app (not a legacy
kind: proxy document):
app:
name: petstore
parts: [openapi-proxy, metrics]
parts:
- name: openapi-proxy
contributions:
- slot: rest.endpoints
items:
get_pet:
mount: /pets/{petId}
method: get
auth: none
source: "proxy get_pet"
upstream: "https://upstream.example.com"
cache: 60s
knowledge: upstream-kb
- slot: mcp.tools
items:
get_pet:
about: "Call the upstream get_pet operation"
source: "proxy get_pet"
vector_dbs:
- name: upstream-kb
about: "Responses captured by the Mosaic proxy."
docs: []
Planner rules:
source: proxy <endpoint>(or baresource: proxyusing the endpoint name) resolves to aProxyEndpointPlan(upstream,upstream_path,cache_secs,knowledge).upstreammust be anhttp(s)base URL;upstream_pathdefaults to the endpointmountand must start with/.cacheis an interval (0sdisables it); it applies toGETforwards.knowledgemust name a declaredvector_db; when set and the upstream response is successful, the response body is ingested into that collection (vector_ingest).- A proxy endpoint cannot be a websocket relay.
- An MCP tool
source: proxy <endpoint>must reference a declared proxy endpoint (mcp-proxy-unknownotherwise).
Generated runtime:
- The proxy handler reads the raw
axum::extract::Request, substitutes{param}path segments from the concrete request path, preserves the query string, and forwards the method/body/headers to the upstream. - It skips hop-by-hop / client-shaping headers (
host,content-length,connection,accept-encoding) and addsx-proxied-by: mosaic+x-cache: HIT|MISS|BYPASS. - The TTL cache is an in-process
BTreeMapkeyed byMETHOD upstream_path?query. - Knowledge capture is optional and only emitted when the app has vectordbs.
MCP tools are now method-aware:
Toolcarriesmethod(defaultPOSTfor CQRS/workflow/ruleset tools).tools/callsubstitutes{name}path arguments into the mount and sends remaining arguments as the query string.- Body is sent only for
POST/PUT/PATCH.
This intentionally extends ADR 0006 for the modern tessera proxy path; the
legacy kind: proxy document remains byte pass-through.
3. App variants / composites
Context
The user asked for app/composite variants covering realm variants (no realms,
tenant, tenant+workspace, workspace-only) and surface variants (REST, REST+MCP,
REST+CLI, UI, etc.). Mosaic already had realm_profiles /
active_realm_profile, parts, part instances, MCP surface filters, UI layout /
prefix — but no named way to compose them.
Decision
app.variants is a map of named overlays, and app.variant selects the active
one (default: default when present, otherwise no overlay):
app:
name: full-stack
variant: tenant-mcp
variants:
base:
parts: [shared]
rest-only:
extends: [base]
exclude: [mcp-tools, ui-scenes]
tenant-mcp:
extends: [base]
parts: [mcp-tools]
realm_profile: tenant
mcp_expose: [place_order, list_orders]
workspace-ui:
realm_profile: workspace
layout: sidebar
ui_prefix: "/_"
A variant may set:
extends: [name…]— composes other variants (laterextendsentries are lower precedence than the current variant; cycles are a plan error).parts— part instances to include.exclude— part instances to remove (wins overparts).realm_profile— activates a declaredrealm_profilesentry.features— extra app features.mcp_hide/mcp_expose— MCP surface filter for the variant.layout/ui_prefix— UI overrides.
The planner applies the merged variant before parts, realms, MCP filtering,
and UI rendering. AppPlan.active_variant records the resolved name.
This gives both requested axes without changing the existing parts/realm/MCP machinery:
- realm variants = variants that set different
realm_profilevalues. - surface variants = variants that include/exclude the parts contributing the desired surfaces and/or adjust the MCP filter.
4. Domain-service surface derivation
Context
domain.services existed as documentation-grade DDD metadata (delegate: "Aggregate.Command"), but it did not derive runtime surfaces. EE derives
domain-service bridges; Mosaic’s equivalent is to make an explicit service
boundary a first-class REST + MCP surface.
Decision
A domain service with delegate: "Aggregate.Command" now derives:
POST /api/domains/{domain-kebab}/{service-kebab}(unless an authored endpoint already claims that mount).- An MCP tool named after the service (unless claimed; collisions warn
surface-collision).
The derived endpoint resolves through the same resolve_endpoint path as
authored endpoints, so the command’s authz (min_role / permission /
where) applies.
5. Dashboard UI
Context
EE ships KPI/dashboard scaffolding and CQRS ops views. Mosaic had list/detail pages, workflow DAG, projection charts, and an event log, but no top-level KPI dashboard.
Decision
The web lens now emits a /dashboard route + nav entry:
- KPI cards: aggregates, commands, events, workflows, endpoints, CLI commands, schedules, flags, knowledge bases.
- Event mix: events by aggregate from the live event log.
- Recent activity: the newest events.
In SSR modes the page reads the in-process store; in CSR it fetches
GET /api/events/list. The KPI counts are build-time DSL facts (deterministic
golden output).
Consequences
- Every knowledge-backed app has a stable assistant brand (Mosaic Aide) with knowledge, MCP, and voice support out of the box.
- Imported OpenAPI services become Mosaic apps: REST pass-through + MCP tools + optional cache/knowledge + the app’s assistant, instead of an opaque proxy tessera.
- Variants make surface/realm composition declarative and composable without duplicating apps.
- Domain services become addressable surfaces, not just docs.
- The dashboard is a read-only KPI/ops view. The full CQRS replay scrubber and interactive aggregate-state explorer remain follow-ups (the event log and aggregate pages already exist).
- The proxy TTL cache is process-local; multi-replica deployments need an external cache for shared state (a follow-up).
- Variants are plan-time static; a single build picks one active variant. Runtime variant switching is out of scope.