For the operator¶
Written so far, for what is built:
- Quickstart — install, use, data — the install on one page, the five tools an agent is given (one example each), and how documents, exports, databases and a folder or a bucket go in.
- Install inside your network — the customer package (
deploy/customer/): Limma, its store and queue, and the model inside your own environment; what leaves your network (nothing of your content; a licensed deployment's usage counts, unless you turn that off), the boot rules that make that true, updates, backups and rollback. - Licensing — where the license file goes, what happens at expiry (14 days
of grace, then read-only: questions and deletion keep working), exactly which fields a
check-in sends, and how to send the monthly file instead (
LIMMA_CHECKIN=off). - Data handling — where the demo window persists what it saw
(
LIMMA_DATA_DIR), persisting nothing (--no-record), the one-shot purge of records once committed, a pseudonymised load, and erasure: its proof by enumeration and what it leaves (the audit trail). - Operating the connect path — which databases and export files
limma_connectmay open (EPISTEM_CONNECT_ALLOWED_HOSTS,EPISTEM_CONNECT_ALLOWED_SQLITE_DIRS,EPISTEM_CONNECT_ALLOWED_EXPORT_DIRS), what a pull writes, generations andreplaces, loading a document-store export (amongoexportfile) under a stated mapping — what it quarantines and what that does to an answer, how long a second pull of the same export waits for the first (EPISTEM_CONNECT_LINE_WAIT_S), what a pull that died leaves — and the audit event of a correction.
The on-prem profile, its environment and its egress set are the install page above — updating, rolling back and backing up an install are its sections 5 to 7. The cloud and private-account profiles, a cold no-egress proof (a packet capture), the worker and what survives a restart, the audit log schema and replay, and sizing as measured are not written here yet. (The deployment Limma itself hosts has a runbook of its own — pinned deploys and their rollback — which is no part of an install inside your network.)
What else exists today and is documented elsewhere: the API boots with
uvicorn epistem.api.app:create_app --factory — shown in the
integrator quickstart.
Keys and roles¶
A caller is an API key. Keys are the entries of EPISTEM_API_KEYS, comma-separated, each
key:organization:role, optionally followed by :workspace_ids and :document_ids to
confine the key to those. THE IDS OF A SCOPE ARE JOINED WITH |, NEVER WITH A COMMA
(<key>:acme:viewer:<workspace 1>|<workspace 2>): the comma separates entries, so one
inside a scope ends the entry — and what follows it is read as an entry of its own. A key
alone is the legacy form: organization default, role admin — and key:organization
with the role left out is an admin of that organization too: an omitted role is the
widest one. Write the role. AN ON-PREM PROCESS REFUSES BOTH SHORT FORMS AT BOOT, by the
entry's position: there every entry has three fields or more, so the id a stray comma
cut loose is never a key.
| role | may |
|---|---|
viewer |
read: search, facts, region, ask, plan, precheck, state, and every GET |
analyst |
read and write: ingest, connect, corrections, uploads, catalogue edits, webhooks; derive, verify and attest; create a workspace |
service |
what an analyst may, except apply a pack (apply_pack) or create a workspace |
admin |
everything above, and delete a workspace |
- A key belongs to one organization and reaches that organization's workspaces only. A workspace-scoped key reaches the workspaces it names — on every route, wherever in the request the workspace's id travels — lists only those, and can neither create a workspace nor register, list or remove a webhook (a subscription is the organization's). The status of an ingestion job is for a caller who may reach the job's workspace.
- A key confined to documents reads those documents and changes no fact, whatever its
role. It may call the verbs that narrow to its documents and report what they could not
read — ask, facts, search, plan, precheck, derive, verify, attest, state, region,
documents, diff, generations, corrections, usage — and list, read and follow the documents
it holds. In every
block of those answers it is shown its own documents and no other's — who a document is
about, its page quality, its table account — and none of the workspace-wide
observations that are tied to no single document (look-alike names, merged rows).
Where an answer carries
sources_unreadable— state, precheck, derive and verify at its top, ask and facts inside their coverage block; the verb reference says of each verb what a confined key is answered, and where — it names the documents that were withheld. On facts it names one page of them (sources_unreadable_totalcounts them all;sources_unreadable_next_after, sent back assources_unreadable_afteronPOST /v1/public/facts, returns the next page). What it ASKS is recorded, as every caller's questions are: an answer record for ask and answer, an attestation for attest, an audit event for each call. It adds, corrects and deletes nothing. Every other operation answers 403document_scoped_key_refused: ingest, connect, corrections, deletes, uploads, the catalogue, backups, webhooks, feedback, the answer records and the/v1/contractsverbs among them. A route added later is refused to such a key until it is shown to narrow. - A scope that is written and names nothing defines no key.
key:acme:viewer:ws_1:— what…:ws_1:${DOCUMENT}becomes when the variable is unset — used to mean "every document"; a fourth field written and empty meant "every workspace". Each is now an entry that defines no key, as is one with a sixth field: the boot of a production or on-prem process refuses it by its position, and in development it is logged and authenticates nothing. A key that is not confined simply has no such field. - A key that may only read changes nothing. Every route whose method can change state
asks for
write— one rule for all of them, including a route added later — except the questions that travel in aPOSTbody (search, facts, ask, plan, precheck, a workspace'sanswerandquery, an audit replay, and a message to the MCP endpointPOST /mcp, each of whose tool calls is then judged by its own verb — see MCP over HTTP), which ask forread, and derive / verify / attest, which ask forverify. A route may ask for MORE than that floor — applying a pack asks formanage_catalogue, deleting a workspace formanage_workspaces— and the verb reference states what each verb's own door asks. A viewer key callingingestis refused 403 whatever it sends. - An entry that is not a key defines no key. No key; a key, an organization or a
scope id holding a space, a tab, a quote, a backtick, an angle bracket, a backslash, or
any character that is not printable ASCII (a paste can carry an invisible one, and a
value pasted with what a page wraps it in is another value); no organization; or a
role that is not one of the four (
Adminandviwerare not): the entry authenticates nothing, and a production or on-prem process refuses to start, naming the entry by its position — never by its text. The same refusal for a list with no usable key at all (unset, or only separators). A key the list defines twice with different grants authenticates nothing either — also where keys are read from the store: the conflict is the answer, and the store is not asked about that value. - With no entry, no key is asked for — in development only (
LIMMA_ENVunset ordev). In every other environment a list with no entry is a door nothing opens: 401 without a key, 403 with one. - A caller is refused before what it sent is read. No key, a key that names nobody, a key that may not do what the call asks: 401 or 403 before the request's body is read, on every route. Every body also has a size limit by what its route takes (413 over it). The limits, and the one bounded exception for the audit record of a refused call, are on the install page.
- A workspace id that names nothing is 404, said once, for every workspace route.
Every route under
/api/workspaces/{id}/and/v1/workspaces/{id}/answers a caller it has named with 404Workspace not foundwhere the id names no workspace — before the route looks at what the key may do there. A caller with no key is told 401 first, so the answer does not say which ids exist to somebody who is nobody. (A workspace that exists in another organization is 403.) - A webhook subscription belongs to the organization of the key that registered it. It
is sent that organization's workspaces' events; only that organization's unscoped keys
register, list or remove it. An event whose workspace can no longer be read (it was
deleted before the event was sent, or the event names none) is sent to nobody — and
DELETING A WORKSPACE SENDS NO EVENT of its own: a subscriber is told when a document
is deleted (
source_deleted), never that the workspace, with the documents in it, is gone. A subscription with no organization — a row from before organizations were recorded — is sent only the events of a workspace that has none either. - A key that is presented and names no key never lets a request through on the
strength of something else it carries. On every operation that changes state, and
on every route that reads a workspace's content, a request with a session and an
X-API-Keyvalue (or an?api_key=) that names no key answers 401; the session beside it is not consulted. At the agent verbs a wrong key answers 403, as it always did — also when a client sends the same key a second time asAuthorization: Bearer(the Python client does): one credential sent twice is answered as it is sent once. (A READ that serves a signed-in user about their OWN account or organization — the workspace list, your account and its sessions, your organization's members and invitations, the session-read audit trail and the diagnosis of a workspace of yours — answers the session's own user exactly as it would without the header, and takes nothing from it.) - Accounts. On-prem there are none: the routes under
/v1/auth(sign-up, login, invitations, sessions) answer 404, so do the routes that serve only a signed-in user (/v1/api-keys,/v1/audit/{workspace_id}and its export), and a session row in the store authenticates nobody. Elsewhere outside development, self-registration (POST /v1/auth/signup) answers 403 unless the operator setLIMMA_ALLOW_SIGNUP=1. - The health routes answer anybody, and tell nobody more than a probe reads. Without
a key
/healthand/health/modelanswer a closed list of fields — the install page names every one of them, in one place (what you get) — and/health/auditone boolean. With any key (any organization's, any role's) they add each worker and the job it is on — a job is namedingest-<document id>, or by a random id when it came through the ingestion webhook: an id that opens nothing to a key that may not read the document — the queue, hosts, model names and the text of any error.