Scopes — per-métier boundaries for ask¶
Every lm ask example passes --scope. This page defines what a scope actually is, how one
comes to exist, and what it guarantees.
A scope is a connector tag¶
There is no separate scope object to create or administer. A scope is a tag that rode in on a connector manifest:
spec:
target:
tags: [clients] # ← this IS the scope
When the connector runs, its chunks and tables carry the tag; the set of scopes in a tenant is
simply the set of tags seen across its ingested data. metadata.tags on the manifest merge into
the same set.
Using scopes¶
lm scopes # list scopes in this tenant (chunks, connectors, last indexed)
lm ask --scope clients "how many customers in Lyon?"
lm scope # show your default scope (fallback: 'system')
lm scope set clients # set your default; empty string clears it
In the TUI, :scopes lists them; Enter sets the active scope (shown in the header, used by
:ask).
What a scope guarantees — and what it doesn't¶
- Tenant isolation is beneath scopes, enforced by PostgreSQL Row-Level Security. A scope never crosses a tenant.
- Within a tenant, the scope is the métier boundary on analytic SQL: the leak guard refuses any generated query that reads a table not declared in the scope (see the guard page for the refusal message).
- By default a scope is a working boundary, not a security boundary between users: a user with no scope grants may ask any scope — scopes keep answers faithful to a métier's data. To make it a need-to-know boundary too, bind the user (next section).
Binding users to scopes (opt-in)¶
A grant is a Keycloak realm role named scope:<tag> — nothing else to create or administer.
- A user with no
scope:*roles is unscoped: tenant-wide access, exactly the default behavior above. (This is why admins need no special case — they simply carry no grants.) - A user with one or more
scope:*roles is scope-bound: - asking or searching a scope they don't hold → 403 scope not granted;
--scope all(or none, on search/chat) is rewritten onto their granted set — never unfiltered;- analytic
askrequires them to name a granted scope (no silentsystemfallback); lm scopeslists only their métiers.
The boundary covers the whole read surface, not just the chat: kind:View
render/detail/generate, saved-view discovery (GET /api/views), and the fused-entity REST
read all answer the same uniform 403 {"error":"scope not granted"} outside the granted
set. End users reach the chat through their own door, POST /api/rag/chat (lm_user) —
/api/platform/rag/chat stays the integrator surface.
The same table can live in several métiers: tag the connector tags: [achat, compta] and a
scope:achat user sees it while a scope:stock user does not.
Scoping and the ladder¶
The same tag scopes the whole ladder: a kind: View renders a scope's table, a kind: Process
watches a scope's raw ingest table, and a kind: Workflow declares a
lifecycle whose corpus chunk is tagged with that same scope — so a bound user retrieves the
declared graph only for the métiers they hold. One tag, one métier, end to end.
See also¶
- Ask honesty — the five guards
- CSV / JDBC /
REST manifests — where
target.tagslives.