Mosaic one model, many lenses

ADR 0026 — EE learn batch: deploy resources, token-borne realms, guard baseline, backend boot check

From a full comparison of the EE mono (DSL + codegen framework) and the generated ee-pages portal against mosaic, plus the latest EE commits (a96bb8fe3, 8f2cdb4, b0f6521, d59df40, 5fa7bc9, 41011d028, b593e7a4b, 9b7ced46d). This ADR covers the four codegen improvements landed from that review; ADR 0024/0025 cover the earlier batches.

Already in mosaic (no work): DSL-first/codegen-first doctrine (tessera is the single source of truth; every surface is a projection over it), the single-collection source-scoped search (5fa7bc9 equivalent — mosaic sources carry a source filter on one collection), DSL-driven RAG config (d59df40 equivalent — retrieval.mode/top_k + chunk in the tessera), the native RuleSet DSL (41011d028/b593e7a4b equivalent — mosaic has reactive + callable + dynamic rulesets), on_create auto-grants, and the 4-crate generated layout (app, web, web/ui, knowledge-engine — the crate-count discipline).

1. deploy.resources { cpu, memory } — pod sizing in the tessera (EE a96bb8fe3)

EE learn: ee-pages was OOMKilled mid-RAG-indexing at the chart's hardcoded 512Mi limit with no DSL path to fix it. The fix added deploy: { resources: { cpu, memory } }; a declared quantity tunes both requests and limits (Guaranteed QoS), per-field fallback to chart defaults, and omitting the block renders byte-identically.

Mosaic implementation.

  • Tessera: deploys: [{ resources: { cpu: "500m", memory: "512Mi" } }] (AppDeploy.resources: Option<ResourceSpec> — the shared spec type). Both fields required (a partial spec is a parse error, not a silent default). Fail-closed validation on Kubernetes quantities (bad-deploy-resource-quantity, the legacy is_quantity check).
  • Deploy lens: the tessera Helm chart values gain resources: {cpu, memory} (still resources: {} when omitted — byte-identical); the deployment template emits the requests+limits block under {{- if .Values.resources.cpu }}.
  • The li7 spec (the public portal) declares 500m / 2Gi — its boot-time RAG index of the books corpus is exactly the EE failure mode.

2. Realms from the verified token — never from the request (EE ScopedCaller)

EE learn (8f2cdb4 tenant-less/app-identity rework + the ScopedCaller contract): scope/tenant values are resolved from session/ JWT state, never from the request body or headers — a caller may not claim a tenancy by sending a header. Mosaic's realm plumbing read x-realm-{dim} straight from the request headers: any caller could claim any tenant.

Mosaic implementation.

  • Tessera: identity.users.<name>.realm: { dim: value } — the user's declared realm assignments (UserDecl.realm).
  • Codegen (shared auth_handler, so app + web get it):
    • the USERS table carries a per-user realm_json;
    • issue_jwt stamps a realm claim (JSON object) when the user has one — the login is the only mint site, so every local token carries it;
    • verify_token (local JWT and OIDC id_token) parses the claim into Authed.realm;
    • the default-deny middleware re-stamps x-realm-{dim} from the verified claim over any client-declared header — downstream scope resolution (caller_realm/realm_filter) is unchanged but can no longer be spoofed. Realm-less tokens (no claim) keep the header/env/seed resolution (the dev realm bar keeps working).
  • The e2e harness signs scenario tokens with the declared user's realm claim (E2ePlan.users), so token-realm behavior is testable without a live login.

realm: { tenant: "*" } is the cross-tenant principal: realm_filter treats the * value as a wildcard (no prefix narrowing).

3. AUTHZ_GUARDS.tsv — the guard baseline as a generated artifact (EE route-guard-baseline.tsv)

EE learn: every authorized route is inventoried in a machine-generated TSV (block/mechanism/method/mount/guard), checked in; a CI drift check treats losing a guard as a regression ("adding routes is fine, moving a guard is reported, losing one is the failure").

Mosaic implementation.

  • App lens: when identity is declared, the render emits AUTHZ_GUARDS.tsv at the project root — one line per (mount, method) with its default authorization decision (action/resource/anonymous, - = public), generated from the same route inventory that feeds the router and the route_policy table.
  • Conformance: the file is pinned under golden/ (a lost guard is a byte diff) and a semantic test (authz_baseline_covers_declared_endpoints) asserts every declared rest endpoint appears with its auth class — required → non-anonymous decision, optional → anonymous admitted, none → public. A codegen change that silently re-classifies a route fails the suite even when the TSV bytes happen to match.

4. MOSAIC_VECTOR_BACKEND boot check — no silent degradation (EE b0f6521)

EE learn: EE's vectordb registry rejects any backend name absent from the compiled-in factory list at boot ("unsupported vector backend qdrant — supported values in this build: …") — a deploy env typo must fail loudly, not degrade to a weaker store.

Mosaic implementation.

  • Generated check_vector_backend_env() (app + web binaries, whenever the app has a vectordb): MOSAIC_VECTOR_BACKEND must be ""/file (always) or lancedb (only in builds with the lancedb feature — the compiled-in list is printed in the error); anything else exits the boot with code 2. Previously any unknown value silently degraded to the ephemeral file store — invisible data loss across pod restarts.

5. mosaic-pages as the reference app (all features exercised)

The portal tessera now exercises the full platform surface:

  • Realms: tenant dimension (non-global root, seeded acme); Doc and DocLog are realm-scoped (realm: [tenant] — instance ids carry the tenant prefix). Users: admin (cross-tenant *), owner/viewer (acme), owner-globex (globex).
  • ABAC: Doc.Retire carries where: 'subject.role == "admin"' — owner passes the role ladder + the doc.publish permission and is still denied by the attribute condition.
  • Rulesets: reactive doc-published / doc-retired (event → dispatch → the DocLog aggregate, which has no REST surface — written only by the rulesets) and callable doc-review-policy (decision table over the request body at POST /api/rulesets/doc-review-policy).
  • Deploy resources: li7 declares 500m / 2Gi.
  • e2e (26 scenarios): tenant isolation, token-realm-beats-header, cross-tenant admin, seeded-tenant anonymous reads, the ABAC deny, rule-set audit rows, and the decision table.

Notes

  • EE's live permission model (per-request IAM lookup instead of role-in-token) and its OIDC realms are deliberately not ported: mosaic's model is role-in-token + resource-scoped grants + ABAC, which covers the portal's needs; EE's realm is an OIDC-issuer concept, mosaic's realm is the tenancy dimension.
  • EE's memvid capacity self-grant (9b7ced46d) does not apply (mosaic uses sqlite/lancedb/opensearch/qdrant/clickhouse, not memvid).