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 legacyis_quantitycheck). - Deploy lens: the tessera Helm chart values gain
resources: {cpu, memory}(stillresources: {}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
USERStable carries a per-userrealm_json; issue_jwtstamps arealmclaim (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 OIDCid_token) parses the claim intoAuthed.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 devrealmbar keeps working).
- the
- 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
identityis declared, the render emitsAUTHZ_GUARDS.tsvat 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 theroute_policytable. - 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_BACKENDmust be""/file(always) orlancedb(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:
tenantdimension (non-global root, seededacme);DocandDocLogare realm-scoped (realm: [tenant]— instance ids carry the tenant prefix). Users:admin(cross-tenant*),owner/viewer(acme),owner-globex(globex). - ABAC:
Doc.Retirecarrieswhere: 'subject.role == "admin"'— owner passes the role ladder + thedoc.publishpermission and is still denied by the attribute condition. - Rulesets: reactive
doc-published/doc-retired(event → dispatch → theDocLogaggregate, which has no REST surface — written only by the rulesets) and callabledoc-review-policy(decision table over the request body atPOST /api/rulesets/doc-review-policy). - Deploy resources:
li7declares500m / 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
memvidcapacity self-grant (9b7ced46d) does not apply (mosaic uses sqlite/lancedb/opensearch/qdrant/clickhouse, not memvid).