Repository navigation
Expand file tree
/
Copy paththreat-model.json
More file actions
2465 lines (2465 loc) · 150 KB
/
Copy paththreat-model.json
File metadata and controls
2465 lines (2465 loc) · 150 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<
10BC8
div data-line-number="302" class="react-line-number react-code-text" style="padding-right:16px">302303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"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)",