Skip to content

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_connect may open (EPISTEM_CONNECT_ALLOWED_HOSTS, EPISTEM_CONNECT_ALLOWED_SQLITE_DIRS, EPISTEM_CONNECT_ALLOWED_EXPORT_DIRS), what a pull writes, generations and replaces, loading a document-store export (a mongoexport file) 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_total counts them all; sources_unreadable_next_after, sent back as sources_unreadable_after on POST /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 403 document_scoped_key_refused: ingest, connect, corrections, deletes, uploads, the catalogue, backups, webhooks, feedback, the answer records and the /v1/contracts verbs 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 a POST body (search, facts, ask, plan, precheck, a workspace's answer and query, an audit replay, and a message to the MCP endpoint POST /mcp, each of whose tool calls is then judged by its own verb — see MCP over HTTP), which ask for read, and derive / verify / attest, which ask for verify. A route may ask for MORE than that floor — applying a pack asks for manage_catalogue, deleting a workspace for manage_workspaces — and the verb reference states what each verb's own door asks. A viewer key calling ingest is 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 (Admin and viwer are 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_ENV unset or dev). 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 404 Workspace not found where 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-Key value (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 as Authorization: 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 set LIMMA_ALLOW_SIGNUP=1.
  • The health routes answer anybody, and tell nobody more than a probe reads. Without a key /health and /health/model answer a closed list of fields — the install page names every one of them, in one place (what you get) — and /health/audit one boolean. With any key (any organization's, any role's) they add each worker and the job it is on — a job is named ingest-<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.