8000 lense/threat-model.json at v0.19.0 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
2465 lines (2465 loc) · 150 KB

threat-model.json

File metadata and controls

2465 lines (2465 loc) · 150 KB
{
"spec_version": 1,
"repository": "https://github.com/adeelahmad/lense",
"commit": "5f799f2ff8bb8a762d8151d9b10b535a17878b55",
"date": "2026-10-05",
"scope_subpath": null,
"description": "Lens is a self-hosted archive for documents, recordings, photos and messages: it reads, transcribes, indexes and publishes them, and people search and chat over them. One operator (the instance admin) runs an instance for their own people: a FastAPI API, a Next.js web app that proxies the API and holds the sign-in session, and background workers, around one SurrealDB database, with content split into namespaces and per-namespace roles. Exposing an instance to the internet behind HTTPS (reverse proxy or Cloudflare tunnel) is supported. Only the instance admin (equivalent to host code execution) and the host operator are trusted; everyone else is trusted only as far as their role.",
"confidence": {
"documented": 269,
"inferred": 99
},
"components": [
{
"name": "web",
"entry_points": [
"every request"
],
"touches": [
"network"
],
"in_scope": true,
"provenance": "inferred",
"source": "open question Q34"
},
{
"name": "identity",
"entry_points": [
"every /api/v1/* route",
"passkey origin, OAuth discovery, external-login redirect",
"POST /auth/login, /iiif/auth/access form",
"/auth/setup, /auth/passkey/setup*, /auth/setup/no-passkey",
"/auth/refresh, /auth/ticket, /auth/signin-link/*, /auth/password/*, WebAuthn flows",
"/oauth/register, /authorize, /token, /revoke",
"/auth/external/{key}/*",
"POST /sources/rclone-signin, /sources/rclone-signin/{id} (signing a source in through rclone)",
"/mcp"
],
"touches": [
"network"
],
"in_scope": true,
"provenance": "documented",
"source": "docs/authentication.md \"one-time **setup code**\""
},
{
"name": "authz",
"entry_points": [
"IP groups, throttles",
"namespace, recording and child-object routes",
"routes not read one by one (about 370)",
"media, report, embed, IIIF content",
"/iiif/auth/token, /probe, /logout",
"workers and DB peers",
"host control inbox and the monitoring scrape token",
"admin machine setup requests",
"admin optional container requests",
"entity and topic curation tools",
"lens help tool",
"microsandbox program requests",
"POST /circles and /circles/shares, MCP save_circle and share_with_circle, the assistant's and automations' share_with_circle (circles and sharing with them)",
"POST /circles/when-free, MCP when_free, the assistant's when_free (when people in your circles are free)"
],
"touches": [
"filesystem"
],
"in_scope": true,
"provenance": "documented",
"source": "SECURITY.md \"What counts\""
},
{
"name": "outbound",
"entry_points": [
"watched folders, storage sources, IMAP, Gmail, iCal, news feeds",
"Google Calendar sources",
"News feed sources",
"Connect with Google",
"POST /imports/web, extension http tools, workflow HTTP call nodes, notification targets",
"admin settings (model, embeddings, speech, decisions, Fedora, bridge, telemetry, IIIF import, rclone remotes, IMAP, Gmail sign-in, iCal, news feeds, MQTT bridge, OIDC issuer, web search, LanguageTool server)",
"replies from remote servers",
".env, archive.yaml, LENS_*, ARCHIVE_SECRET_KEY, RCLONE_BINARY"
],
"touches": [
"filesystem",
"network",
"env",
"child_processes"
],
"in_scope": true,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "ingest",
"entry_points": [
"/uploads, /import, /recordings/{rid}/files",
"POST /hooks/import",
"POST /ap/users/<name>/inbox, GET /ap/users/<name>, /.well-known/webfinger (circles with other servers)"
],
"touches": [
"filesystem",
"child_processes"
],
"in_scope": true,
"provenance": "inferred",
"source": "open question Q34"
},
{
"name": "query",
"entry_points": [
"/recordings, /search, /public/search, chat retrieval",
"/graph/query, MCP graph_query",
"/namespaces/{name}/sparql",
"/namespaces/{name}/rdf/import"
],
"touches": [
"global_state"
],
"in_scope": true,
"provenance": "inferred",
"source": "open question Q34"
},
{
"name": "assistant",
"entry_points": [
"chat, /graph/ask, MCP tools",
"extensions (prompt, graph and http tools, hooks), custom nodes including template nodes",
"MCP servers: POST /extensions/mcp/read and /mcp/test, GET /extensions/mcp/presets, saving an extension of kind mcp, PATCH /extensions/{id}/tools, MCP mcp_servers and add_mcp_server",
"Python code tools, shipped and admin-created templates, workflows, pipelines, routines",
"Lens extension side panel",
"automations",
"the assistant's web_search and web_read",
"the assistant's connection tools",
"POST /circles/ask, MCP ask_their_agent, the assistant's ask_their_agent (asking someone's agent a question)"
],
"touches": [
"network",
"child_processes"
],
"in_scope": true,
"provenance": "inferred",
"source": "open question Q34"
},
{
"name": "optional",
"entry_points": [
"POST /automations/hooks/{token} (an automation's webhook)",
"GET /feeds/public/{token}, /feeds/public/{token}/items/{item}/audio (a published feed, or a saved collection's podcast feed)",
"automations' params and prompts, POST /automations/preview and /automations/variables",
"MQTT 1883, syslog 5514, POST /sensors/push/{token}/{stream}",
"SMB share (port 1445 on the host, 4445 in the worker)",
"POST /api/v1/mesh/join (a machine joining the mesh)",
"/api/v1/mesh/node/config, /node/renew, /node/beat (a node's own calls, with its node token)",
"GET /api/v1/mesh/node/worker, /node/media/{id} (a worker node's database user and its jobs' files, with its node token)",
"PUT /api/v1/mesh/node/sandbox (a compute node's sandbox gateway's client certificate, with its node token), and Lens's calls to that gateway on the node's mesh address, port 7443",
"POST /api/v1/mesh/tokens, PATCH and DELETE /api/v1/mesh/nodes/{id}, PUT /api/v1/settings/mesh, MCP and assistant mesh tools",
"UDP 4242 on the primary and ports 8000 and 8080 on its mesh address (the mesh container, while the mesh is on)"
],
"touches": [
"network",
"child_processes",
"global_state"
],
"in_scope": true,
"provenance": "inferred",
"source": "open question Q34"
}
],
"out_of_scope": [
{
"item": "ci-tooling-and-samples",
"reason": "examples/ (source clone only), overrides/ (mkdocs theme), ci/, .github/, .pre-commit* (CI, in no image) and prod-frontend-deploy.yml (stale Vercel workflow) are unsupported paths built into no running service (§1.3)",
"provenance": "inferred",
"source": "open question Q34"
},
{
"item": "inert-backend-files",
"reason": "fastapi_backend/tests/, watcher.py and commands/ are copied into the backend image but never started by the production command; a report must show production code reaching them (§1.3)",
"provenance": "inferred",
"source": "open question Q34"
}
],
"trust_boundaries": [
{
"component": "web",
"boundary": "B1, network to web app: everything a browser, app or agent sends is untrusted; the web app forwards /api, /mcp, /iiif, /embed and /s to the API, which authorizes every call.",
"provenance": "inferred",
"reachability_precondition": "reachable from a browser request and changes what the API decides or what another person's browser runs",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "identity",
"boundary": "B2, request to role: the API turns a bearer token, signed link, share link, IIIF cookie or vouched-for address into a role per namespace.",
"provenance": "inferred",
"reachability_precondition": "reachable with no account, or with an account, and yields a session, token or admin the attacker was not given; setup-code findings apply only while no account exists",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "authz",
"boundary": "B2, request to role: the main boundary; namespace isolation lives here, in application code. B5: workers, the API and the web app trust the database and each other.",
"provenance": "inferred",
"reachability_precondition": "lets a principal read or change more than its role and the asset's access level allow, or open media without a link the server signed for it",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "outbound",
"boundary": "B4, Lens to the outside: fetches whose URL a non-admin sets, and iCal and news feeds, go through netguard; other admin-set URLs do not; the telemetry endpoint is only shape-checked (P-ADMIN-GUARD).",
"provenance": "inferred",
"reachability_precondition": "a URL a non-admin sets, or a guarded admin operand, reaches a non-public address; or a stored secret reaches anyone but the host the admin set",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "ingest",
"boundary": "B3, content to parser: uploaded and fetched bytes cross into ffmpeg, LibreOffice, Chromium, poppler and Tesseract.",
"provenance": "inferred",
"reachability_precondition": "reachable from bytes an editor uploads or a watched source delivers, and the fault is in how Lens runs the tool (arguments, environment, timeouts, sandbox flags)",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "query",
"boundary": "B2, request to role: query text from a non-admin is untrusted and runs only over the caller's readable namespaces.",
"provenance": "inferred",
"reachability_precondition": "query text from a non-admin reads outside the caller's namespaces, writes, fetches, or reaches SurrealQL",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
10BC8
"component": "assistant",
"boundary": "B3, content to parser: uploaded and fetched bytes cross into the LLM prompt; B2 bounds the tools to the person's role.",
"provenance": "inferred",
"reachability_precondition": "a non-admin (or content steering the model for one) exceeds the person's reads, changes data without approval outside notes and admin setup, or runs code on the host",
"source": "threat-model.md §1.4 (untagged boundary list)"
},
{
"component": "optional",
"boundary": "B6, devices: sensor listeners take traffic from the LAN (or wider, per sensors.bind) once an admin enables them.",
"provenance": "inferred",
"reachability_precondition": "the service is turned on by an admin, and the attacker lacks the login, network or token the feature requires",
"source": "threat-model.md §1.4 (untagged boundary list)"
}
],
"entry_points": [
{
"entry_point": "every /api/v1/* route",
"parameter": "Authorization: Bearer (web JWT, la_ API token, lo_ app token)",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "holder keeps it secret; Lens checks signature or hash, expiry, account, session",
"provenance": "documented",
"source": "docs/authentication.md \"Every request checks that the account is still active\""
},
{
"entry_point": "every request",
"parameter": "Host header",
"attacker_controllable": "yes",
"component": "web",
"caller_must_enforce": "operator lists served hosts",
"provenance": "documented",
"source": "docs/configuration.md \"checks the `Host` header\""
},
{
"entry_point": "IP groups, throttles",
"parameter": "X-Forwarded-For, peer address",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "a reverse proxy sets the header; trusted_proxies lists exactly the front hops",
"provenance": "documented",
"source": "docs/configuration.md \"Without it, a visitor can claim any address\""
},
{
"entry_point": "passkey origin, OAuth discovery, external-login redirect",
"parameter": "X-Forwarded-Host, X-Forwarded-Proto",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "TRUST_PROXY_HEADERS only behind a real proxy",
"provenance": "documented",
"source": "docs/authentication.md \"anyone could choose the address\""
},
{
"entry_point": "POST /auth/login, /iiif/auth/access form",
"parameter": "email, password",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "none — P-PASSWORDS-OFF (passwords work only where auth.passwords is on)",
"provenance": "documented",
"source": "docs/authentication.md \"With passwords off, password sign-in\""
},
{
"entry_point": "/auth/setup, /auth/passkey/setup*, /auth/setup/no-passkey",
"parameter": "setup code, email, name",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "keep the log private until the first admin exists",
"provenance": "documented",
"source": "docs/configuration.md \"so a stranger who finds a new\""
},
{
"entry_point": "/auth/refresh, /auth/ticket, /auth/signin-link/*, /auth/password/*, WebAuthn flows",
"parameter": "rotating and one-time secrets",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "holder keeps them secret",
"provenance": "documented",
"source": "docs/authentication.md \"A link lasts three days\""
},
{
"entry_point": "POST /auth/passkeys/elsewhere",
"parameter": "an address to take your sign-in to",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "signed in themselves, not API tokens or apps; only an address Lens opens at; 15 minutes, once, other links left alone",
"provenance": "documented",
"source": "docs/authentication.md \"Lens opens at another address\""
},
{
"entry_point": "/oauth/register, /authorize, /token, /revoke",
"parameter": "redirect URIs, client metadata, code, verifier, refresh token",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "the app keeps state and its verifier; the person consents",
"provenance": "documented",
"source": "docs/authentication.md \"Apps register themselves\""
},
{
"entry_point": "/auth/external/{key}/*",
"parameter": "next, state, code, provider profile",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "admin chooses providers",
"provenance": "documented",
"source": "docs/authentication.md \"when the provider vouches for the email\""
},
{
"entry_point": "POST /sources/rclone-signin, /sources/rclone-signin/{id} (signing a source in through rclone)",
"parameter": "type, params, address (state, code, error), the token rclone prints",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "the person pastes only the address their own sign-in ended on; the host keeps 127.0.0.1:53682 to itself",
"provenance": "documented",
"source": "docs/processing.md \"Lens passes only that address's state, code and\""
},
{
"entry_point": "/mcp",
"parameter": "bearer, Origin, JSON-RPC body",
"attacker_controllable": "yes",
"component": "identity",
"caller_must_enforce": "none — P-OAUTH",
"provenance": "documented",
"source": "docs/mcp.md \"A token an app asked for another server\""
},
{
"entry_point": "namespace, recording and child-object routes",
"parameter": "namespace name, rid, child ids (sequential integers)",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "none — P-NS-ISOLATION (Lens checks the role in each handler)",
"provenance": "documented",
"source": "docs/api.md \"Namespaces you can't read answer 404\""
},
{
"entry_point": "routes not read one by one (about 370)",
"parameter": "per route",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "follow the gate pattern",
"provenance": "inferred",
"source": "open question Q8"
},
{
"entry_point": "media, report, embed, IIIF content",
"parameter": "exp, sig, full, ?s= share token, /s/<code> short code",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "holder keeps the link private",
"provenance": "documented",
"source": "SECURITY.md \"only the server's own signed links should work\""
},
{
"entry_point": "/iiif/auth/token, /probe, /logout",
"parameter": "origin, messageId, IIIF cookie, probe token",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "operator sets iiif.allowed_origins",
"provenance": "documented",
"source": "docs/iiif.md \"limits which viewer sites can get tokens\""
},
{
"entry_point": "/recordings, /search, /public/search, chat retrieval",
"parameter": "query text, filters, sort, limit, offset",
"attacker_controllable": "yes",
"component": "query",
"caller_must_enforce": "none — P-NO-INJECTION",
"provenance": "documented",
"source": "SECURITY.md \"injection into the database queries\""
},
{
"entry_point": "/graph/query, MCP graph_query",
"parameter": "Cypher text, params",
"attacker_controllable": "yes",
"component": "query",
"caller_must_enforce": "none — P-QUERY",
"provenance": "documented",
"source": "docs/graph.md \"3 million steps\""
},
{
"entry_point": "/namespaces/{name}/sparql",
"parameter": "SPARQL text",
"attacker_controllable": "yes",
"component": "query",
"caller_must_enforce": "none — P-NO-FETCH",
"provenance": "documented",
"source": "routes/rdf.py sparql docstring \"SERVICE and FROM aren't allowed\""
},
{
"entry_point": "/namespaces/{name}/rdf/import",
"parameter": "Turtle, N-Triples, JSON-LD",
"attacker_controllable": "yes",
"component": "query",
"caller_must_enforce": "none — P-NO-FETCH",
"provenance": "documented",
"source": "rdf_import.py docstring \"RDF/XML isn't read\""
},
{
"entry_point": "/uploads, /import, /recordings/{rid}/files",
"parameter": "file bytes, name, extension, size",
"attacker_controllable": "yes",
"component": "ingest",
"caller_must_enforce": "none — P-UPLOAD",
"provenance": "documented",
"source": "docs/configuration.md \"Uploads accept only the types\""
},
{
"entry_point": "POST /hooks/import",
"parameter": "bearer (a hook's lih_ token, or a person's la_, lo_ or web token with ?namespace=), file bytes, URL, text",
"attacker_controllable": "yes",
"component": "ingest",
"caller_must_enforce": "none — P-UPLOAD; the address form /hooks/import/{token} takes hook tokens only, so a person's token never sits in a logged path; ?for= hands a PDF back only for a document the same hook brought in that still waits for one (hand_back, off by default), and it becomes that document's rendition",
"provenance": "documented",
"source": "docs/api.md \"the address form takes hook tokens only\""
},
{
"entry_point": "watched folders, storage sources, IMAP, Gmail, iCal, news feeds",
"parameter": "remote file names, bytes, mail, events, feed posts",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "admin creates sources",
"provenance": "inferred",
"source": "open question Q17"
},
{
"entry_point": "Google Calendar sources",
"parameter": "service account key file or connected account's refresh token, calendar ids, impersonate; the events Google returns",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "admin creates sources; read-only calendar.readonly scope; Google's own token endpoint and API address (a key file's token_uri is ignored); event text escaped into its .ics without control characters",
"provenance": "documented",
"source": "docs/processing.md \"The token endpoint is always Google's own\""
},
{
"entry_point": "News feed sources",
"parameter": "the feed's address (or a site's page that names its feed), user and password; the feed's XML and its posts' HTML",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "admins add them (web app, API, MCP subscribe_feed with a write-scope token, or the assistant's tool, which asks first outside a setup conversation or once it has read outside text); fetched as iCal feeds are; feedparser reads the XML with external entities off and drops scripts, styles and event handlers from a post's HTML; each post kept as an Atom entry Lens writes, without the feed's address; post text is marked untrusted in prompts",
"provenance": "documented",
&q 10BC8 uot;source": "docs/processing.md \"News feeds (RSS and Atom)\""
},
{
"entry_point": "Connect with Google",
"parameter": "OAuth client id and secret; on the way back state, code and the email in Google's id token",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "admin only, from the web app (API tokens refused), still an admin on return; single-use hashed state bound to the starting browser (cookie), 10 minutes, PKCE S256; client secret sealed on the flow, refresh token on the source; kept only when the source's scope was granted (read only, but all of Gmail over IMAP, all of Drive for Drive set to full access); Drive keeps its client's secret sealed too; returns only to the starting web origin",
"provenance": "documented",
"source": "docs/processing.md \"the consent itself is a person's\""
},
{
"entry_point": "POST /imports/web, extension http tools, workflow HTTP call nodes, notification targets",
"parameter": "URL",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "none — P-SSRF-GUARD",
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"entry_point": "the assistant's web_search and web_read",
"parameter": "a search query and a page address, both chosen by the model; the results and pages that come back",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — queries go only to the admin's provider; pages are fetched through netguard like web import, 10 MB at most (P-SSRF-GUARD); once the answer has read outside text, a query or address neither the person nor a tool gave it asks first (P-INJECTION-EGRESS); results and pages reach the model marked as data; the ledger keeps the provider or host, never the query or page",
"provenance": "documented",
"source": "docs/assistant.md \"a search or a page read sends words out of Lens\""
},
{
"entry_point": "the assistant's connection tools",
"parameter": "a connection's id or name, a Gmail search, a message id, a time span, a path; the mail, events and files that come back; the message, event or text file the model would send or write",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — admins only, and only the tools for the connections there are; reading changes nothing in the account; a file is read up to 20 MB and a message's or file's text up to 11,000 characters; mail, events and files reach the model marked as data, and once read, tools that could carry it out of Lens ask first (P-INJECTION-EGRESS); bringing anything into a namespace waits for the person's yes; sending mail, saving a draft, adding an event and writing a file always wait for the person's yes, even in a setup conversation, with all of it shown (and a note when the answer read outside text), and the model is told never to act on what it read; sending and adding work only on a Gmail or Google Calendar connection an admin let do so (write, which asks Google for gmail.compose or calendar.events), checked again when approved; at most 50 addresses a field, 50,000 characters a message and 200,000 a file; a file is replaced only when asked and approved, and Google Drive connected read only is never written; each change is in the audit log; the ledger keeps what was called, never what came back",
"provenance": "documented",
"source": "docs/assistant.md \"Mail, calendars and files\""
},
{
"entry_point": "email replies",
"parameter": "an imported email's headers and text (From, Reply-To, Message-ID, References, the body), read to draft a reply; the reply sent, or kept in Gmail's drafts, through the Gmail connection the email came in on",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "drafting needs the right to process the email; the email reaches the model marked untrusted and the system prompt says not to follow it; only the email's own namespace is searched for excerpts; a web address in the draft that neither the email nor the excerpts hold is broken up, and a citation to an excerpt the model wasn't given is taken out; nothing is sent on its own: sending (or keeping it in Gmail's drafts) is an admin's own pick, signed in to Lens, after an Undo window, on a Gmail connection an admin let send (write), checked when it goes; an API key, an app or an agent can only ask (MCP `send_reply`), which waits in Approvals showing all of the message and sends the text that was approved, never a later edit; a draft with a [to confirm: …] mark left isn't sent; the recipient (the address Reply-To names) is shown and editable before anything goes; the reply goes without its citations; each send is in the audit log",
"provenance": "documented",
"source": "docs/email-replies.md \"Lens never sends a reply on its own\""
},
{
"entry_point": "event proposals from dates said",
"parameter": "a recording, a line and which of its dates; the fields to use instead of the prefill (title, start, end, where, description, attendees, invite, connection, calendar); the line's text and an email's addresses that fill the rest",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none; admins only, for an asset they can read, and only with a Google Calendar connection an admin let add events; the proposal is an approval of add_event with all of the event shown (the line, the time read from it, who is invited and that they aren't emailed unless invite is true), and nothing is added or sent until the person approves it signed in to Lens: an API key, an app, MCP and the assistant can only decline it; declining does nothing; the connection is checked again when approved; no model is called; at most 20 addresses are prefilled and 50 given; each proposal and each addition is in the audit log (calendar.propose, calendar.add)",
"provenance": "documented",
"source": "docs/processing.md \"Propose an event\""
},
{
"entry_point": "admin settings (model, embeddings, speech, decisions, Fedora, bridge, telemetry, IIIF import, rclone remotes, IMAP, Gmail sign-in, iCal, news feeds, MQTT bridge, OIDC issuer, web search, LanguageTool server)",
"parameter": "URL, credentials, api_key_env",
"attacker_controllable": "no",
"component": "outbound",
"caller_must_enforce": "admin is trusted",
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"entry_point": "replies from remote servers",
"parameter": "web page HTML and scripts, IIIF manifests, model replies, OIDC discovery, calendars and their redirects",
"attacker_controllable": "yes",
"component": "outbound",
"caller_must_enforce": "treat as untrusted",
"provenance": "inferred",
"source": "open question Q17"
},
{
"entry_point": "chat, /graph/ask, MCP tools",
"parameter": "prompt text; model-chosen tool calls",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — P-RETRIEVAL; what people say to Lens reaches automations as outside text (chat.message, lens.untrusted)",
"provenance": "documented",
"source": "docs/assistant.md \"limited to what the person can read\""
},
{
"entry_point": "Lens extension side panel",
"parameter": "answers shown there (model text that can repeat third-party text), the sign-in coming back",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none; answers render as Markdown cleaned by DOMPurify (no scripts, images or forms; only http and https links), and the sign-in checks its state and PKCE",
"provenance": "documented",
"source": "extensions/browser/src/core/assistant.js answerHtml docstring \"cleaned of anything that could run or fetch on its own\""
},
{
"entry_point": "extensions (prompt, graph and http tools, hooks), custom nodes including template nodes",
"parameter": "tool spec, graph JSON, template text",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "person enables only extensions they trust",
"provenance": "documented",
"source": "docs/assistant.md \"other people's run only when an admin shared them\""
},
{
"entry_point": "MCP servers: POST /extensions/mcp/read and /mcp/test, GET /extensions/mcp/presets, saving an extension of kind mcp, PATCH /extensions/{id}/tools, MCP mcp_servers and add_mcp_server",
"parameter": "config text (JSON or YAML), a server's address and headers, or a command, its arguments and env; a tool name and its switches",
"attacker_controllable": "no",
"component": "assistant",
"caller_must_enforce": "admin is trusted with the address and the command; reading a config parses text only; a command runs only in the MCP runner; credential values are sealed to the extension and where they go and shown as ••••; a tool switched off isn't offered, and every MCP tool asks first after outside text",
"provenance": "documented",
"source": "docs/assistant.md \"Adding an MCP server\""
},
{
"entry_point": "Python code tools, shipped and admin-created templates, workflows, pipelines, routines",
"parameter": "Python, Jinja source, regexes",
"attacker_controllable": "no",
"component": "assistant",
"caller_must_enforce": "admin is trusted",
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"entry_point": "POST /automations/hooks/{token} (an automation's webhook)",
"parameter": "body (JSON, a form's text fields or text), query, Standard Webhooks headers, token in the path",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "the person keeps the address and secret to the services that call it; Lens keeps the token hashed, caps size and rate, checks signatures sent, and treats what was sent as outside text",
"provenance": "documented",
"source": "docs/automations.md \"No webhook exists until an automation with such a trigger is saved\"",
"x-tests": [
"fastapi_backend/tests/api/test_automation_hooks.py::test_a_hook_is_made_for_a_trigger_and_only_its_hash_kept",
"fastapi_backend/tests/api/test_automation_hooks.py::test_a_call_is_a_webhook_received_activity",
"fastapi_backend/tests/api/test_automation_hooks.py::test_size_and_rate_caps",
"fastapi_backend/tests/api/test_automation_hooks.py::test_signed_calls_and_retries",
"fastapi_backend/tests/api/test_automations.py::test_a_webhook_trigger_gets_its_address_when_saved_and_loses_it_when_dropped"
]
},
{
"entry_point": "GET /feeds/public/{token}, /feeds/public/{token}/items/{item}/audio (a published feed, or a saved collection's podcast feed)",
"parameter": "token in the path, Range, If-None-Match",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "the person shares the address as a password; Lens finds the token by its hash, stops the old address when a new one is made, and lists only what the namespace's readers can read, or for a collection's feed what its person may read now",
"provenance": "documented",
"source": "docs/feeds.md \"Private by default\""
},
{
"entry_point": "POST /circles and /circles/shares, MCP save_circle and share_with_circle, the assistant's and automations' share_with_circle (circles and sharing with them)",
"parameter": "circle members, an activity or resource id, a note, with_item",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "members must be people the sharer may add; Lens checks the sharer can read the item, refuses vaults, caps the note, and needs publish to give the item; the assistant asks first",
"provenance": "documented",
"source": "docs/circles.md \"What keeps it safe\""
},
{
"entry_point": "POST /circles/when-free, MCP when_free, the assistant's when_free (when people in your circles are free)",
"parameter": "people, a window, a time zone, minutes, the hours of the day",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "the person asked must have let a circle with the asker see; Lens answers only start and end times, caps the window and the people, and records each ask in the asked person's timeline",
"provenance": "documented",
"source": "docs/circles.md \"When they're free\""
},
{
"entry_point": "POST /circles/ask, MCP ask_their_agent, the assistant's ask_their_agent (asking someone's agent a question)",
"parameter": "the person asked, a question",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "the person asked must have let a circle with the asker ask and named the namespaces; Lens answers from those only, wraps the question as outside text, and records the question and answer in their timeline",
"provenance": "documented",
"source": "docs/circles.md \"Ask their agent\""
},
{
"entry_point": "POST /ap/users/<name>/inbox, GET /ap/users/<name>, /.well-known/webfinger (circles with other servers)",
"parameter": "an ActivityPub activity and its HTTP Signature, a handle",
"attacker_controllable": "yes",
"component": "ingest",
"caller_must_enforce": "Lens answers 404 until an admin and the person turn it on, and takes a Note only signed by someone in one of their circles with the key Lens kept, recent, its digest matching; its text is outside text",
"provenance": "documented",
"source": "docs/federation.md \"What travels\""
},
{
"entry_point": "automations' params and prompts, POST /automations/preview and /automations/variables",
"parameter": "template text, a draft automation, an activity id",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "Lens renders them in the Jinja sandbox (2 MB of output, guarded * and **; Q23); previews read only activities, namespaces and automations the person may see, and prompts mark outside text as data",
"provenance": "documented",
"source": "docs/automations.md \"The variable picker and preview\""
},
{
"entry_point": "MQTT 1883, syslog 5514, POST /sensors/push/{token}/{stream}",
"parameter": "payloads, hub logins, token in the path",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "admin enables; operator firewalls; readings, alert patterns and new devices reach automations as outside text (lens.untrusted)",
"provenance": "documented",
"source": "docs/sensors.md \"Sensors are off until an admin turns them on\""
},
{
"entry_point": "NFS share (port 12049 on the host, 2049 in the worker)",
"parameter": "NFS and MOUNT RPCs, client address",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "admin enables and picks the account; operator firewalls",
"provenance": "documented",
"source": "docs/file-shares.md \"NFS has no login\""
},
{
"entry_point": "SMB share (port 1445 on the host, 4445 in the worker)",
"parameter": "SMB2 requests, NTLM sign-in with a file access key, client address",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "admin enables; operator firewalls",
"provenance": "documented",
"source": "docs/smb.md \"What keeps it safe\""
},
{
"entry_point": "web app /mqtt and port 9883 (MQTT over WebSockets, relayed by lens-relay.js ahead of Next)",
"parameter": "the WebSocket handshake, MQTT packets in binary messages, hub logins",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "admin enables sensors.mqtt_web; a hub login on every connection, never anonymous; failed logins throttled per visitor from X-Forwarded-For as trusted proxies report it",
"provenance": "documented",
"source": "docs/sensors.md \"At the web app's address\""
},
{
"entry_point": "web app /s3 and /dav (file shares relayed by lens-relay.js, ahead of Next)",
"parameter": "any method, path, Host and body, passed to the share servers with X-Forwarded-Prefix and X-Forwarded-For",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "admin enables shares.enabled and shares.remote; per-person access keys, files added imported as at the ports (shares.add_files); failed WebDAV sign-ins throttled per visitor from X-Forwarded-For as trusted proxies report it",
"provenance": "documented",
"source": "docs/file-tree.md \"At the web app's address\""
},
{
"entry_point": ".env, archive.yaml, LENS_*, ARCHIVE_SECRET_KEY, RCLONE_BINARY",
"parameter": "process configuration",
"attacker_controllable": "no",
"component": "outbound",
"caller_must_enforce": "keep unreadable to others",
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"entry_point": "workers and DB peers",
"parameter": "DB credential, shared database rows",
"attacker_controllable": "no",
"component": "authz",
"caller_must_enforce": "run peers on trusted hosts",
"provenance": "inferred",
"source": "open question Q9"
},
{
"entry_point": "host control inbox and the monitoring scrape token",
"parameter": "inbox/upgrade request (release ref, empty for the version running, with https=0|1, public=0|1 and target=full|lean; or action=services with profile=mcp|browser|monitoring and turn=on|off, or an older Lens's monitoring=on|off); /metrics scrape token and .lens-control/prometheus-token",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "the agent installs only a release tag or the install's branch, changes only the HTTPS ports, the web app's network reach and the edition (other values dropped), and only toggles the mcp, browser and monitoring profiles (any other refused), so a writer can at most restart Lens with those choices or open it to the network; the token is compared in constant time and grants metrics only",
"provenance": "documented",
"source": "docs/updates.md, docs/prometheus.md"
},
{
"entry_point": "admin machine setup requests",
"parameter": "PUT /api/v1/admin/install {public, target}, POST /api/v1/settings/https/ports {open}, MCP set_install_settings",
"attacker_controllable": "no",
"component": "authz",
"caller_must_enforce": "admin is trusted; audited (lens.install, https.ports); reaches the machine only as an inbox request the agent checks again",
"provenance": "documented",
"source": "docs/updates.md \"Setting the machine up again\""
},
{
"entry_point": "admin optional container requests",
"parameter": "PUT /api/v1/admin/services/{profile} {on}, MCP set_service: profile mcp|browser|monitoring",
"attacker_controllable": "no",
"component": "authz",
"caller_must_enforce": "admin is trusted; audited (lens.services, lens.monitoring); unknown profiles are a 404; one change at a time; reaches the machine only as an inbox request the agent checks again",
"provenance": "documented",
"source": "docs/assistant.md \"Optional containers\""
},
{
"entry_point": "admin auto scaling",
"parameter": "/api/v1/scaling (providers, groups, units, tick), PUT /api/v1/settings/scaling, MCP and assistant scaling tools: provider credentials, a group's terms and rules, a unit id",
"attacker_controllable": "no",
"component": "authz",
"caller_must_enforce": "admin is trusted; off until turned on; audited (scaling.*) and in the activity ledger; credentials sealed and shown only as set; a group launches nothing until a person signed in to the web app approves its terms (P-SCALING)",
"provenance": "documented",
"source": "docs/scaling.md \"The person approves\""
},
{
"entry_point": "entity and topic curation tools",
"parameter": "MCP edit_entity, edit_topic, merge_topics, delete_topic, undo_topic_change, undo_graph_change; assistant propose_topic_change: entity and topic ids, names, labels, definitions",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "none — the curate permission in each namespace touched and a write-capable principal (the web app's own route functions); the assistant's changes wait for the person's approval; merges and deletes are kept to undo, and audited",
"provenance": "documented",
"source": "docs/mcp.md \"curate entities and topics as an editor does\""
},
{
"entry_point": "automations",
"parameter": "/api/v1/automations (save, try, run now, decide, draft from a sentence), the MCP and assistant automation tools: triggers, where and If match trees, the event workflow graph (action params are Jinja templates), policy, limits, an activity id; the text of the activities an automation runs on",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — it runs as its maker with their permissions at run time (P-AUTOMATION-ACTOR); actions reaching outside Lens ask first; outside text marks intents untrusted and breaks up links (P-INJECTION-EGRESS); chains and bursts are bounded (P-AUTOMATION-LOOP); `matches` patterns refuse nested repeats and repeated alternatives that start alike, read at most 20,000 characters and give up after 50 ms",
"provenance": "documented",
"source": "docs/automations.md \"Running automations\"",
"x-tests": [
"fastapi_backend/tests/domain/test_match_ops.py::test_checks_refuse_values_a_test_cant_use",
"fastapi_backend/tests/domain/test_match_ops.py::test_a_pattern_reads_a_long_value_only_so_far_and_gives_up_when_slow"
]
},
{
"entry_point": "admin sandbox gateway",
"parameter": "PUT /api/v1/admin/sandbox {on}, MCP set_sandbox_gateway",
"attacker_controllable": "no",
"component": "authz",
"caller_must_enforce": "admin is trusted; audited (lens.sandbox); refused when preflight finds the machine can't run the gateway; reaches the machine only as an inbox request (action=services, profile=sandbox) the agent checks again; off deletes every sandbox first",
"provenance": "documented",
"source": "docs/assistant.md \"The gateway\""
},
{
"entry_point": "import runs",
"parameter": "GET /api/v1/import/runs, POST /api/v1/import/runs/{id}/undo; assistant import_runs, undo_import: a run id",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "none — a run is seen only by those (404 otherwise); undo deletes each asset through deletion.delete and needs the remove permission (or a collection admin role) on every one, else nothing is deleted (403); the assistant's undo always waits for the person's approval; audited per asset (recording.delete) and for the run (import.undo)",
"provenance": "documented",
"source": "docs/api.md \"Import runs\""
},
{
"entry_point": "code sandbox",
"parameter": "assistant run_python, run_bash, sandbox_files, sandbox_nodes, share_sandbox_file and import_sandbox_file: code the model writes, attachment ids, a path in the sandbox, a compute node's id or name; sandbox.* settings (admins)",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — off by default; the code runs in the conversation's own OpenShell sandbox with none of Lens's data or keys and no network unless sandbox.network allows it (P-SANDBOX); with the network on, a run after outside text asks first (P-INJECTION-EGRESS); files in only from the person's attachments, out only through an approved import or as a download for that person; with compute nodes registered by the mesh, a run may go to a ready compute node's own gateway, reached only over the mesh, with the same sandbox settings, and only where the model passed node or the conversation's sandbox already is",
"provenance": "documented",
"source": "docs/assistant.md \"Code sandbox\""
},
{
"entry_point": "code sandbox shared files",
"parameter": "GET /api/v1/chats/{cid}/files/{fid}: a file share_sandbox_file copied out of the conversation's sandbox; its bytes come from code the model wrote",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — served only to the conversation's person (404 otherwise, or for another conversation's id) as an attachment with nosniff and a sandbox content policy, never rendered; random 96-bit id checked against the conversation; kept AES-256-GCM encrypted with a key derived from the server's secret, bound to the file and conversation; at most 20 a conversation, 25 MB each; deleted with the conversation or the account",
"provenance": "documented",
"source": "docs/assistant.md \"Code sandbox\""
},
{
"entry_point": "lens help tool",
"parameter": "assistant and MCP lens_help: a question's words, a docs page's name",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "none — it only reads: docs from a fixed folder (a page picked by name from the indexed ones, never opened by path) in an in-memory index; this server's facts carry no keys, tokens or model server addresses, and the settings pages only for admins",
"provenance": "documented",
"source": "docs/assistant.md \"Questions about Lens itself\""
},
{
"entry_point": "deep research",
"parameter": "/research <goal>, research: true, POST /chats/{cid}/research, /research/{id} (revise, stop, resume, note, podcast), start_research (assistant and MCP), research_status (MCP): a goal, answers to follow-up questions, plan edits; research.* settings (admins)",
"attacker_controllable": "yes",
"component": "assistant",
"caller_must_enforce": "none — the plan waits for the person's own approval (P-RESEARCH); the run uses only its sources' read tools, as the person, within its tool calls, minutes and budgets; its web searches and page reads don't ask again, bounded by the plan and the ledger; one run per person at a time; the report's claims are checked against what they cite",
"provenance": "documented",
"source": "docs/assistant.md \"Deep research\""
},
{
"entry_point": "microsandbox program requests",
"parameter": "ask/NAME.want in /data/.lens/programs: a program's name",
"attacker_controllable": "yes",
"component": "authz",
"caller_must_enforce": "the start script installs only libreoffice or chromium, each a fixed list of signed Debian packages, answers in a folder only root writes, and reads nothing but the name",
"provenance": "documented",
"source": "docs/microsandbox.md"
},
{
"entry_point": "POST /api/v1/mesh/join (a machine joining the mesh)",
"parameter": "a join token's id, a nonce, the machine's name and Nebula public key, an HMAC of those keyed with the token's secret, facts about the machine",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "no sign-in: the token's secret never crosses the wire, the HMAC is checked in constant time, wrong proofs throttled per address (8 in 15 minutes), a token is spent on use and expires, and the answer is sealed with a key derived from the secret",
"provenance": "documented",
"source": "docs/mesh.md \"Joining\""
},
{
"entry_point": "/api/v1/mesh/node/config, /node/renew, /node/beat (a node's own calls, with its node token)",
"parameter": "a public key to certify; a status report: whether Nebula runs, its peers, its machine, its log lines",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "the node token is 256 random bits kept only as a hash; a node gets its own config and a certificate for its own address and groups only; reports are capped; they arrive at the primary's mesh address, which the API's host check admits for these paths alone while the mesh is on",
"provenance": "documented",
"source": "docs/mesh.md \"After joining\""
},
{
"entry_point": "GET /api/v1/mesh/node/worker, /node/media/{id} (a worker node's database user and its jobs' files, with its node token)",
"parameter": "a recording id; the node's role and namespaces",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "worker nodes only (403 for others); the database user is the node's own, of Lens's database only, never the server's root, removed with the node and made again with a new password on an undo; files are those of the namespaces the node serves, never a vault's, sent plain over the mesh; the archive's secret never leaves the primary",
"provenance": "documented",
"source": "docs/mesh.md \"Worker nodes\""
},
{
"entry_point": "PUT /api/v1/mesh/node/sandbox (a compute node's sandbox gateway's client certificate, with its node token), and Lens's calls to that gateway on the node's mesh address, port 7443",
"parameter": "a CA certificate, a client certificate and its key, PEM",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "compute nodes only (403 for others); each part capped and loaded as a certificate or key (400 otherwise); the key kept sealed like a setting's secret; Lens reaches the gateway over the mesh only (7443 open to the primary only, relayed to the gateway by the node's mesh container), with mutual TLS checked against the name the gateway's certificate was made for; the node gets nothing of Lens's",
"provenance": "documented",
"source": "docs/mesh.md \"Compute nodes\""
},
{
"entry_point": "POST /api/v1/mesh/tokens, PATCH and DELETE /api/v1/mesh/nodes/{id}, PUT /api/v1/settings/mesh, MCP and assistant mesh tools",
"parameter": "a node's role, name, namespaces, token hours and uses; the mesh's network, port, hosts, cipher",
"attacker_controllable": "no",
"component": "optional",
"caller_must_enforce": "admin is trusted; audited (mesh.token.create, mesh.join, mesh.node.*); the network can't change once machines have addresses in it; the assistant's adding and removing a node always wait for the person's approval; an install line (it holds a join token's secret) goes only to the MCP agent that asked or to the person who approved, once, and is never kept in an approval, the audit log or the model's context; the read tools never show a token's secret",
"provenance": "documented",
"source": "docs/mesh.md \"Settings\""
},
{
"entry_point": "UDP 4242 on the primary and ports 8000 and 8080 on its mesh address (the mesh container, while the mesh is on)",
"parameter": "Nebula handshakes and tunnelled packets from anyone reaching the port; on the mesh, TCP to the database and the API",
"attacker_controllable": "yes",
"component": "optional",
"caller_must_enforce": "Nebula drops what no certificate of the mesh's CA signs and anything on the blocklist; its firewall, rendered by Lens, lets only lens-worker reach 8000 and lens-node 8080; the relays listen on the mesh address only; the container is the only one holding NET_ADMIN, makes its own key pair (the private key stays in its own volume) and runs only Nebula's release with the checksum kept in Lens",
"provenance": "documented",
"source": "docs/mesh.md \"On this machine\""
}
],
"environment": {
"assumes": [
"Host is Linux containers, an LXC on Proxmox, a NAS package, or a native install on Linux or macOS; the host and its root user are trusted.",
"One SurrealDB database holds every namespace, with optimistic, retried transactions; the embedded engine is for trying Lens and small archives.",
"Expiry uses wall-clock time with no skew handling.",
"Several API processes and workers may run at once; throttle counters and the setup code live in each process's memory.",
"Child processes (rclone, ffmpeg, LibreOffice, Chromium, Tesseract, poppler, cloudflared, llama-server, code tools, uv/pip) run as argument lists.",
"Listening sockets: API and web app; a loopback netguard proxy during captures; MQTT 1883 and syslog 5514 when sensors are on, MQTT over WebSockets on 9883 with sensors.mqtt_web; llama-server when enabled.",
"Files live under data_dir, including secret.key when ARCHIVE_SECRET_KEY is unset; the server log carries the setup code and, without mail, sign-in links."
],
"does_not": [
"make outbound connections except to configured sources, model servers, mail, notification and telemetry targets, components.auto installs and model downloads, and the llama.cpp/cloudflared/hf: downloads when those features are on",
"send telemetry by default",
"check for updates"
],
"provenance": "inferred",
"source": "open question Q19"
},
"build_variants": [
{
"name": "docker-compose.yml, make dev",
"default": "dev secrets, web on 0.0.0.0:3000",
"effect": "voids P-AUTH-REQ; voids P-MEDIA-LINK; narrows P-HARDENED",
"discouraged": false,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "docker-compose.prod.yml, install.sh, Cloudron, Synology, QNAP, Proxmox",
"default": "random secrets (Makefile .env writes SURREAL_PASS=root)",
"effect": "preserves P-HARDENED; preserves P-IPGROUP",
"discouraged": false,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "Proxmox LXC service user",
"default": "systemd units have no User=, so Lens runs as root inside the unprivileged LXC; the SurrealDB container runs as root",
"effect": "narrows P-NO-CODE",
"discouraged": false,
"provenance": "inferred",
"source": "open question Q33"
},
{
"name": "install.sh on a headless host, NAS packages",
"default": "HTTP on all interfaces, ?code= setup URL printed",
"effect": "narrows P-SETUP-ONCE",
"discouraged": true,
"provenance": "inferred",
"source": "open question Q31"
},
{
"name": "Plain HTTP",
"default": "on",
"effect": "voids P-SETUP-ONCE; narrows P-AUTH-REQ",
"discouraged": true,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "SurrealDB root/root",
"default": "on in env defaults",
"effect": "voids P-NS-ISOLATION",
"discouraged": true,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "Image build context (.dockerignore per image)",
"default": ".dockerignore per image",
"effect": "narrows P-SECRETS",
"discouraged": false,
"provenance": "inferred",
"source": "open question Q44"
},
{
"name": "/docs and /openapi.json (OPENAPI_URL)",
"default": "on",
"effect": "no effect on claimed properties",
"discouraged": false,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "tokens.oauth_enabled",
"default": "on",
"effect": "preserves P-OAUTH",
"discouraged": false,
"provenance": "documented",
"source": "maintainer ruling, 2026-10"
},
{
"name": "iiif.allowed_origins",
"default": "[\"*\"]",
"effect": "narrows P-ACCESS-PARTS",
"discouraged": false,
"provenance": "inferred",
"source": "open question Q1"
},
{
"name": "server.trusted_proxies, LENS_TRUSTED_PROXY_HOSTS",
"default": "127.0.0.0/8, ::1; compose names its containers",
"effect": "narrows P-IPGROUP; narrows P-THROTTLE",
"discouraged": false,
"provenance": "documented",
"source": "docs/configuration.md \"Trusted proxies\""
},
{
"name": "TRUST_PROXY_HEADERS (web app)",
0