Mosaic one model, many lenses

ADR 0029 — App variants, OpenAPI proxy apps, domain-service surfaces, and Mosaic Aide

This ADR lands the “Mosaic beyond EE” gap program:

  1. Mosaic Aide — an out-of-the-box named assistant for every app that has a vector_db (knowledge + MCP + voice support).
  2. Modern OpenAPI proxy apps — mosaic import openapi now 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.
  3. App variants / composites — app.variant + app.variants are named, composable overlays (surface, realm, feature, and part-instance composition).
  4. Domain-service surface derivation — a domain services[].delegate (Aggregate.Command) derives its own REST endpoint and MCP tool.
  5. Dashboard UI — the web lens gains a /dashboard page (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:

surfacebehavior
GET /api/aide/profilereturns Mosaic Aide, the active model name, and voice: true
POST /api/aide/chataliases the existing grounded assistant (assistant_chat)
GET /aidea self-contained assistant page (conversation id in localStorage, page context omitted for now)
aide_chat MCP toola 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 bare source: proxy using the endpoint name) resolves to a ProxyEndpointPlan (upstream, upstream_path, cache_secs, knowledge).
  • upstream must be an http(s) base URL; upstream_path defaults to the endpoint mount and must start with /.
  • cache is an interval (0s disables it); it applies to GET forwards.
  • knowledge must name a declared vector_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-unknown otherwise).

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 adds x-proxied-by: mosaic + x-cache: HIT|MISS|BYPASS.
  • The TTL cache is an in-process BTreeMap keyed by METHOD upstream_path?query.
  • Knowledge capture is optional and only emitted when the app has vectordbs.

MCP tools are now method-aware:

  • Tool carries method (default POST for CQRS/workflow/ruleset tools).
  • tools/call substitutes {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 (later extends entries are lower precedence than the current variant; cycles are a plan error).
  • parts — part instances to include.
  • exclude — part instances to remove (wins over parts).
  • realm_profile — activates a declared realm_profiles entry.
  • 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_profile values.
  • 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.