ltcai 11.5.0 → 11.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +86 -47
  2. package/docs/CHANGELOG.md +76 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
  6. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/WORKFLOW_DESIGNER.md +1 -1
  12. package/docs/architecture.md +4 -4
  13. package/docs/kg-schema.md +1 -1
  14. package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
  15. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/graph/proactive.py +1 -1
  18. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  19. package/lattice_brain/graph/vector_index/base.py +0 -3
  20. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  21. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  22. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  23. package/lattice_brain/ingestion_jobs.py +0 -3
  24. package/lattice_brain/memory.py +0 -27
  25. package/lattice_brain/portability/sharing.py +0 -4
  26. package/lattice_brain/quality.py +0 -49
  27. package/lattice_brain/runtime/agent_runtime.py +1 -1
  28. package/lattice_brain/runtime/hooks.py +0 -13
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/lattice_brain/sealed_box.py +0 -4
  31. package/latticeai/__init__.py +1 -1
  32. package/latticeai/api/admin.py +12 -12
  33. package/latticeai/api/agent_worker_seam.py +389 -0
  34. package/latticeai/api/auth.py +25 -2
  35. package/latticeai/api/chat.py +3 -3
  36. package/latticeai/api/chat_agent_http.py +3 -12
  37. package/latticeai/api/chat_helpers.py +10 -13
  38. package/latticeai/api/computer_use.py +31 -42
  39. package/latticeai/api/health.py +21 -1
  40. package/latticeai/api/local_files.py +14 -0
  41. package/latticeai/api/permissions.py +38 -12
  42. package/latticeai/api/search.py +24 -0
  43. package/latticeai/api/static_routes.py +4 -1
  44. package/latticeai/cli/runtime.py +7 -3
  45. package/latticeai/core/config.py +47 -3
  46. package/latticeai/core/csrf.py +28 -2
  47. package/latticeai/core/embedding_providers/__init__.py +3 -5
  48. package/latticeai/core/embedding_providers/base.py +1 -1
  49. package/latticeai/core/embedding_providers/text.py +1 -1
  50. package/latticeai/core/enterprise.py +1 -4
  51. package/latticeai/core/http_origin.py +146 -0
  52. package/latticeai/core/invitations.py +3 -2
  53. package/latticeai/core/io_utils.py +2 -11
  54. package/latticeai/core/legacy_compatibility.py +1 -1
  55. package/latticeai/core/marketplace.py +1 -1
  56. package/latticeai/core/messages.py +40 -0
  57. package/latticeai/core/model_compat.py +2 -8
  58. package/latticeai/core/module_probe.py +37 -0
  59. package/latticeai/core/run_explain.py +5 -2
  60. package/latticeai/core/run_store.py +2 -2
  61. package/latticeai/core/security.py +13 -0
  62. package/latticeai/core/sessions.py +3 -2
  63. package/latticeai/core/sse.py +25 -0
  64. package/latticeai/core/users.py +0 -9
  65. package/latticeai/core/workspace_graph_trace.py +2 -8
  66. package/latticeai/core/workspace_os.py +0 -10
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_utils.py +34 -0
  69. package/latticeai/core/workspace_review_items.py +2 -8
  70. package/latticeai/core/workspace_runs.py +2 -8
  71. package/latticeai/core/workspace_skills.py +2 -7
  72. package/latticeai/models/router/documents.py +0 -35
  73. package/latticeai/models/router/registry.py +0 -4
  74. package/latticeai/runtime/access_runtime.py +16 -4
  75. package/latticeai/runtime/build_phases/features.py +25 -2
  76. package/latticeai/runtime/build_phases/web.py +2 -0
  77. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  78. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  79. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  80. package/latticeai/runtime/router_registration.py +4 -0
  81. package/latticeai/runtime/runtime_context.py +1 -13
  82. package/latticeai/runtime/service_singletons.py +55 -0
  83. package/latticeai/services/architecture_readiness.py +1 -1
  84. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  85. package/latticeai/services/change_proposals.py +2 -36
  86. package/latticeai/services/chronicle.py +4 -6
  87. package/latticeai/services/command_center.py +4 -4
  88. package/latticeai/services/evidence_actions.py +5 -2
  89. package/latticeai/services/hybrid_chat.py +2 -2
  90. package/latticeai/services/hybrid_policy.py +8 -57
  91. package/latticeai/services/mode_store.py +132 -0
  92. package/latticeai/services/model_capability_registry.py +0 -15
  93. package/latticeai/services/model_catalog.py +0 -7
  94. package/latticeai/services/model_loading.py +3 -2
  95. package/latticeai/services/model_runtime/__init__.py +0 -57
  96. package/latticeai/services/model_runtime/engines.py +27 -128
  97. package/latticeai/services/model_runtime/loading.py +2 -2
  98. package/latticeai/services/network_boundary_service.py +7 -44
  99. package/latticeai/services/permission_mode_service.py +8 -56
  100. package/latticeai/services/product_readiness.py +1 -1
  101. package/latticeai/services/setup_detection.py +67 -1
  102. package/latticeai/services/tool_dispatch.py +0 -4
  103. package/latticeai/services/upload_service.py +7 -20
  104. package/latticeai/services/workspace_service.py +0 -6
  105. package/latticeai/setup/auto_setup.py +15 -26
  106. package/latticeai/setup/wizard/catalog.py +5 -2
  107. package/latticeai/setup/wizard/detect.py +6 -27
  108. package/latticeai/setup/wizard/paths.py +2 -5
  109. package/latticeai/setup/wizard/plans.py +2 -2
  110. package/package.json +2 -4
  111. package/scripts/bump_version.py +14 -1
  112. package/scripts/check_current_release_docs.mjs +1 -1
  113. package/scripts/check_legacy_debt.mjs +1 -1
  114. package/scripts/check_server_i18n.mjs +1 -0
  115. package/scripts/generate_agent_loop_fixtures.py +994 -0
  116. package/scripts/generate_rust_parity_fixtures.py +72 -161
  117. package/scripts/parity_fixture_corpus_context.py +162 -0
  118. package/scripts/parity_fixture_corpus_docgen.py +341 -0
  119. package/scripts/release_screen_claims.json +20 -0
  120. package/src-tauri/Cargo.lock +13 -7
  121. package/src-tauri/Cargo.toml +1 -1
  122. package/src-tauri/src/backend.rs +76 -2
  123. package/src-tauri/src/main.rs +13 -3
  124. package/src-tauri/tauri.conf.json +2 -2
  125. package/static/app/asset-manifest.json +41 -41
  126. package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
  127. package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
  128. package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
  129. package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
  130. package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
  131. package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
  132. package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
  133. package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
  134. package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
  135. package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
  136. package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
  137. package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
  138. package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
  139. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  140. package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
  141. package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
  142. package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
  143. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  144. package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
  145. package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
  146. package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
  147. package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
  148. package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
  149. package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
  150. package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
  151. package/static/app/assets/index-DxmOfNRi.css +2 -0
  152. package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
  153. package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
  154. package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
  155. package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
  156. package/static/app/assets/search-BcHqkjoy.js +1 -0
  157. package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
  158. package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
  159. package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
  160. package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
  161. package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
  162. package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
  163. package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
  164. package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
  165. package/static/app/index.html +4 -4
  166. package/static/sw.js +1 -1
  167. package/desktop/electron/README.md +0 -9
  168. package/desktop/electron/main.cjs +0 -58
  169. package/desktop/electron/preload.cjs +0 -5
  170. package/latticeai/core/graph_curator.py +0 -11
  171. package/latticeai/core/hooks.py +0 -11
  172. package/latticeai/core/local_embeddings.py +0 -104
  173. package/latticeai/core/multi_agent.py +0 -11
  174. package/latticeai/core/workflow_engine.py +0 -11
  175. package/latticeai/services/ingestion.py +0 -11
  176. package/latticeai/services/kg_portability.py +0 -11
  177. package/latticeai/services/multimodal_streaming.py +0 -129
  178. package/scripts/measure_brain_home_fill.mjs +0 -141
  179. package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
  180. package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
  181. package/static/app/assets/index-BLPb5lmE.css +0 -2
  182. package/static/app/assets/search-Cj_TKk_2.js +0 -1
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """The document-generation half of the Python↔Rust parity corpus (v11.5.1).
3
+
4
+ ``scripts/generate_rust_parity_fixtures.py`` owns the store and the goldens;
5
+ this module owns the rows and specs the **document-generation** ports need, so
6
+ neither file has to grow past the tree's file-size ceiling. Nothing here runs
7
+ anything — it is data plus one normalization helper.
8
+
9
+ Three things are shaped on purpose:
10
+
11
+ * **Every allow-listed node type appears.** ``search_for_document_generation``
12
+ admits fifteen types and boosts four of them; a corpus that only carried
13
+ ``Document`` would prove the boost and the filter equally badly.
14
+ * **The Self-Model subgraph is real.** ``summary_for_prompt`` reads the legacy
15
+ ``nodes`` table for ``self:%`` ids, so the profile rows are written through
16
+ the same write door as everything else, including the root the read excludes
17
+ and a ``self:`` row whose metadata carries no recognised kind (dropped).
18
+ * **``updated_at`` is spread against the frozen clock**, including one stamp
19
+ that does not parse — the recency term's ``0.0`` branch is otherwise
20
+ unreachable from a fixture whose every row is well formed.
21
+
22
+ :func:`normalize_multi_hop` is the one deliberate deviation, and it is a
23
+ *golden* normalization rather than a port divergence: see its docstring.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Dict, List, Optional, Tuple
29
+
30
+ WS_ALPHA = "ws-alpha"
31
+ WS_BETA = "ws-beta"
32
+
33
+ # ── the document-generation corpus ───────────────────────────────────────────
34
+ # (node_id, type, title, summary, metadata, workspace_id, updated_at)
35
+ DOCGEN_NODES: List[Tuple[str, str, str, str, Dict[str, Any], Optional[str], str]] = [
36
+ # ── the Self-Model subgraph (read by `summary_for_prompt`) ──────────────
37
+ # `self:root` is excluded by id, and `self:unknown:*` carries no
38
+ # `self_model_kind`, so `_row_to_fact` drops it — both are read branches
39
+ # that only exist if the fixture actually contains them.
40
+ ("self:root", "Self", "나", "Brain이 사용자에 대해 알고 있는 사실의 뿌리입니다.",
41
+ {"self_model": True, "self_model_kind": "root"}, None, "2026-07-01T09:00:00"),
42
+ ("self:decision:a1", "Decision", "문서는 한국어로 쓴다",
43
+ "문서는 한국어로 쓴다",
44
+ {"self_model": True, "self_model_kind": "decision", "origin": "user",
45
+ "confidence": 0.9, "signal": "ko_decision_marker", "workspace_id": WS_ALPHA},
46
+ WS_ALPHA, "2026-07-02T09:00:00"),
47
+ ("self:habit:b2", "Habit", "매주 월요일 회의록을 정리한다",
48
+ "매주 월요일 회의록을 정리한다",
49
+ {"self_model": True, "self_model_kind": "habit", "origin": "extraction",
50
+ "confidence": 0.6, "signal": "ko_habit", "workspace_id": None},
51
+ None, "2026-07-03T09:00:00"),
52
+ ("self:preference:c3", "Preference", "설명은 짧고 근거를 먼저",
53
+ "설명은 짧고 근거를 먼저",
54
+ {"self_model": True, "self_model_kind": "preference", "origin": "user",
55
+ "confidence": 0.8, "signal": "ko_prefer", "workspace_id": None},
56
+ None, "2026-07-04T09:00:00"),
57
+ ("self:relationship:d4", "Relationship", "박민서 님과 빌드 파이프라인을 함께 본다",
58
+ "박민서 님과 빌드 파이프라인을 함께 본다",
59
+ {"self_model": True, "self_model_kind": "relationship", "origin": "extraction",
60
+ "confidence": 0.5, "signal": "ko_with", "workspace_id": WS_BETA},
61
+ WS_BETA, "2026-07-05T09:00:00"),
62
+ ("self:trait:e5", "Self", "검색 품질 담당자",
63
+ "검색 품질 담당자",
64
+ {"self_model": True, "self_model_kind": "trait", "origin": "user",
65
+ "confidence": 0.7, "signal": "ko_role", "workspace_id": None},
66
+ None, "2026-07-06T09:00:00"),
67
+ ("self:unknown:f6", "Concept", "kind가 없는 self 노드",
68
+ "self_model_kind가 없어 프로필에서 제외되는 행입니다.",
69
+ {"self_model": True}, None, "2026-07-07T09:00:00"),
70
+ # ── the proposal cluster: one hub document with more qualifying
71
+ # neighbours than the eight the related-concept join keeps ───────────
72
+ ("doc:proposal", "Document", "제안서 초안: 검색 품질 개선",
73
+ "검색 품질 개선 제안서 초안입니다. 근거는 주간 회의 기록과 분기 리뷰 발표자료입니다.",
74
+ {"relative_path": "docs/proposal.md", "filename": "proposal.md", "ext": ".md"},
75
+ WS_ALPHA, "2026-07-30T09:00:00"),
76
+ ("doc:release-notes", "Document", "Release notes for the retrieval port",
77
+ "Release notes describing the ranking port and the parity harness.",
78
+ {"source": "release"}, None, "2026-07-28T09:00:00"),
79
+ ("file:meeting-minutes", "File", "meeting-minutes.md",
80
+ "회의록: 랭킹 개선과 온보딩 체크리스트를 확정했습니다.",
81
+ {"filename": "meeting-minutes.md", "relative_path": "notes/meeting-minutes.md"},
82
+ WS_ALPHA, "2026-08-01T11:00:00"),
83
+ ("deck:proposal", "SlideDeck", "제안 발표자료",
84
+ "제안서 초안을 발표용으로 정리한 자료입니다.",
85
+ {"filename": "proposal.pptx", "ext": ".pptx"}, WS_BETA, "2026-07-29T09:00:00"),
86
+ ("chat:proposal", "Chat", "제안서 관련 대화",
87
+ "제안서 초안을 어떻게 쓸지 논의한 대화입니다.",
88
+ {"conversation_id": "conv-f"}, WS_ALPHA, "2026-07-26T09:00:00"),
89
+ ("task:proposal-review", "Task", "제안서 검토",
90
+ "제안서 초안을 검토하고 근거를 확인합니다.", {"status": "open"},
91
+ WS_ALPHA, "2026-07-27T09:00:00"),
92
+ ("dec:doc-format", "Decision", "문서 형식은 마크다운으로 통일",
93
+ "문서 생성 결과는 마크다운으로 통일하기로 했습니다.", {},
94
+ WS_ALPHA, "2026-07-22T09:00:00"),
95
+ ("feature:doc-generation", "Feature", "문서 생성",
96
+ "질문에서 문서를 만들어 주는 기능입니다.", {}, WS_ALPHA, "2026-07-25T09:00:00"),
97
+ ("concept:proposal", "Concept", "제안서",
98
+ "제안서는 결정을 요청하는 문서입니다.", {}, None, "2026-07-24T09:00:00"),
99
+ ("concept:evidence", "Concept", "근거",
100
+ "근거는 주장을 뒷받침하는 자료입니다.", {}, WS_ALPHA, "2026-07-23T09:00:00"),
101
+ ("page:proposal-outline", "Page", "제안서 목차",
102
+ "제안서 목차 페이지입니다.", {}, None, "2026-07-21T09:00:00"),
103
+ ("slide:proposal-1", "Slide", "제안 슬라이드 1",
104
+ "제안서 요약 슬라이드입니다.", {}, WS_BETA, "2026-07-19T09:00:00"),
105
+ ("sheet:proposal-budget", "Spreadsheet", "proposal-budget.xlsx",
106
+ "제안서 예산 표입니다.", {"filename": "proposal-budget.xlsx", "ext": ".xlsx"},
107
+ WS_ALPHA, "2026-07-17T09:00:00"),
108
+ ("img:proposal-sketch", "Image", "proposal-sketch.png",
109
+ "제안서 구조 스케치입니다.", {"filename": "proposal-sketch.png", "ext": ".png"},
110
+ WS_BETA, "2026-07-16T09:00:00"),
111
+ ("imgtext:proposal-ocr", "ImageText", "제안서 스케치 OCR",
112
+ "제안서 구조: 배경, 근거, 결론.",
113
+ {"filename": "proposal-sketch.png", "ocr": True}, WS_BETA, "2026-07-16T09:01:00"),
114
+ ("audio:proposal-call", "Audio", "proposal-call.m4a",
115
+ "제안서 관련 통화 녹음입니다.", {"filename": "proposal-call.m4a", "ext": ".m4a"},
116
+ WS_ALPHA, "2026-07-15T09:00:00"),
117
+ ("code:doc-builder", "CodeFile", "context_builder.py",
118
+ "문서 생성 컨텍스트를 조합하는 코드입니다. 제안서 생성도 여기를 지납니다.",
119
+ {"filename": "context_builder.py",
120
+ "relative_path": "latticeai/core/context_builder.py"},
121
+ WS_ALPHA, "2026-07-14T09:00:00"),
122
+ # The recency term's `0.0` branch: a stamp `datetime.fromisoformat` refuses.
123
+ ("doc:undated", "Document", "Undated 제안서 memo",
124
+ "An undated memo about 제안서; its stamp does not parse.", {},
125
+ None, "not-a-timestamp"),
126
+ ]
127
+
128
+ # (from, to, type, weight, created_at) — appended to the base edge set.
129
+ DOCGEN_EDGES: List[Tuple[str, str, str, float, str]] = [
130
+ ("doc:proposal", "concept:proposal", "mentions", 0.92, "2026-07-30T00:00:00"),
131
+ ("doc:proposal", "concept:evidence", "mentions", 0.91, "2026-07-30T00:01:00"),
132
+ ("doc:proposal", "feature:doc-generation", "relates_to", 0.90, "2026-07-30T00:02:00"),
133
+ ("doc:proposal", "dec:doc-format", "relates_to", 0.89, "2026-07-30T00:03:00"),
134
+ ("doc:proposal", "task:proposal-review", "relates_to", 0.88, "2026-07-30T00:04:00"),
135
+ ("doc:proposal", "concept:retrieval", "mentions", 0.87, "2026-07-30T00:05:00"),
136
+ ("doc:proposal", "concept:ranking", "mentions", 0.86, "2026-07-30T00:06:00"),
137
+ ("doc:proposal", "feature:command-palette", "mentions", 0.85, "2026-07-30T00:07:00"),
138
+ ("doc:proposal", "task:parity-harness", "relates_to", 0.84, "2026-07-30T00:08:00"),
139
+ ("doc:proposal", "page:proposal-outline", "contains", 0.83, "2026-07-30T00:09:00"),
140
+ ("deck:proposal", "doc:proposal", "relates_to", 0.82, "2026-07-29T00:00:00"),
141
+ ("deck:proposal", "slide:proposal-1", "contains", 0.81, "2026-07-29T00:01:00"),
142
+ ("file:meeting-minutes", "dec:doc-format", "relates_to", 0.80, "2026-07-28T00:00:00"),
143
+ ("file:meeting-minutes", "meeting:weekly", "relates_to", 0.79, "2026-07-28T00:01:00"),
144
+ ("chat:proposal", "doc:proposal", "mentions", 0.78, "2026-07-27T00:00:00"),
145
+ ("img:proposal-sketch", "imgtext:proposal-ocr", "relates_to", 0.77, "2026-07-26T00:00:00"),
146
+ ("imgtext:proposal-ocr", "concept:proposal", "mentions", 0.76, "2026-07-26T00:01:00"),
147
+ ("audio:proposal-call", "task:proposal-review", "mentions", 0.75, "2026-07-25T00:00:00"),
148
+ ("code:doc-builder", "feature:doc-generation", "relates_to", 0.74, "2026-07-24T00:00:00"),
149
+ ("sheet:proposal-budget", "doc:proposal", "relates_to", 0.73, "2026-07-23T00:00:00"),
150
+ ("doc:release-notes", "concept:ranking", "mentions", 0.72, "2026-07-22T00:00:00"),
151
+ ("doc:undated", "concept:proposal", "mentions", 0.71, "2026-07-21T00:00:00"),
152
+ # The Self-Model's own shape: every fact points at the root.
153
+ ("self:decision:a1", "self:root", "PART_OF", 1.0, "2026-07-02T00:00:00"),
154
+ ("self:habit:b2", "self:root", "PART_OF", 1.0, "2026-07-03T00:00:00"),
155
+ ("self:preference:c3", "self:root", "PART_OF", 1.0, "2026-07-04T00:00:00"),
156
+ ("self:relationship:d4", "self:root", "PART_OF", 1.0, "2026-07-05T00:00:00"),
157
+ ("self:trait:e5", "self:root", "PART_OF", 1.0, "2026-07-06T00:00:00"),
158
+ ("self:unknown:f6", "self:root", "PART_OF", 1.0, "2026-07-07T00:00:00"),
159
+ ]
160
+
161
+ # ── suite specs ──────────────────────────────────────────────────────────────
162
+ DOCGEN_SEARCH_SPECS: List[Dict[str, Any]] = [
163
+ {"key": "ko_proposal", "query": "제안서 초안"},
164
+ {"key": "ko_meeting", "query": "회의 결정 사항"},
165
+ {"key": "en_ranking", "query": "hybrid retrieval ranking"},
166
+ {"key": "ko_sketch", "query": "스케치"},
167
+ {"key": "ko_self_model", "query": "문서는 한국어로"},
168
+ {"key": "en_undated", "query": "Undated"},
169
+ {"key": "hub_deck", "query": "제안 발표자료"},
170
+ {"key": "tie", "query": "Tie candidate"},
171
+ {"key": "no_hit", "query": "zzqq wumpus nonsense"},
172
+ {"key": "empty_query", "query": ""},
173
+ {"key": "blank_query", "query": " "},
174
+ {"key": "limit_one", "query": "제안서", "limit": 1},
175
+ {"key": "limit_zero", "query": "제안서", "limit": 0},
176
+ {"key": "limit_over", "query": "제안서", "limit": 900},
177
+ {"key": "scoped_alpha", "query": "제안서", "allowed": [WS_ALPHA]},
178
+ {"key": "scoped_alpha_legacy", "query": "제안서", "allowed": [WS_ALPHA], "legacy": True},
179
+ {"key": "scoped_beta", "query": "제안서", "allowed": [WS_BETA]},
180
+ {"key": "scoped_empty", "query": "제안서", "allowed": []},
181
+ ]
182
+
183
+ MULTI_HOP_SPECS: List[Dict[str, Any]] = [
184
+ *[{"key": f"hub_h{hops}", "node_ids": ["doc:proposal"], "max_hops": hops}
185
+ for hops in (0, 1, 2, 3)],
186
+ {"key": "hub_hneg", "node_ids": ["doc:proposal"], "max_hops": -1},
187
+ {"key": "multi_seed", "node_ids": ["doc:proposal", "dec:fusion-alpha"], "max_hops": 2},
188
+ {"key": "leaf", "node_ids": ["slide:proposal-1"], "max_hops": 2},
189
+ {"key": "isolated", "node_ids": ["tie:c"], "max_hops": 2},
190
+ {"key": "self_root", "node_ids": ["self:root"], "max_hops": 2},
191
+ {"key": "missing_seed", "node_ids": ["nope:missing"], "max_hops": 2},
192
+ {"key": "no_seeds", "node_ids": [], "max_hops": 2},
193
+ {"key": "scoped_alpha", "node_ids": ["doc:proposal"], "max_hops": 2,
194
+ "allowed": [WS_ALPHA]},
195
+ {"key": "scoped_alpha_legacy", "node_ids": ["doc:proposal"], "max_hops": 2,
196
+ "allowed": [WS_ALPHA], "legacy": True},
197
+ {"key": "scoped_beta", "node_ids": ["deck:proposal"], "max_hops": 2,
198
+ "allowed": [WS_BETA]},
199
+ {"key": "scoped_empty", "node_ids": ["doc:proposal"], "max_hops": 2, "allowed": []},
200
+ ]
201
+
202
+ CONTEXT_DOCUMENT_SPECS: List[Dict[str, Any]] = [
203
+ {"key": "default", "query": "제안서 초안"},
204
+ {"key": "no_self_model", "query": "제안서 초안", "include_self_model": False},
205
+ {"key": "self_model_tokens_small", "query": "제안서 초안", "self_model_tokens": 12},
206
+ {"key": "budget_240", "query": "제안서 초안", "budget": 240},
207
+ {"key": "budget_120", "query": "제안서 초안", "budget": 120},
208
+ {"key": "budget_40", "query": "제안서 초안", "budget": 40},
209
+ {"key": "budget_one", "query": "제안서 초안", "budget": 1},
210
+ {"key": "budget_zero", "query": "제안서 초안", "budget": 0},
211
+ {"key": "hops_zero", "query": "제안서 초안", "max_hops": 0},
212
+ {"key": "hops_three", "query": "제안서 초안", "max_hops": 3},
213
+ {"key": "results_two", "query": "제안서 초안", "max_results": 2},
214
+ {"key": "ko_meeting", "query": "회의 결정 사항"},
215
+ {"key": "en_ranking", "query": "hybrid retrieval ranking"},
216
+ {"key": "ko_sketch", "query": "스케치"},
217
+ {"key": "fallback_lexical", "query": "박민서 님"},
218
+ {"key": "fallback_empty", "query": "zzqq wumpus nonsense"},
219
+ {"key": "empty_query", "query": ""},
220
+ {"key": "scoped_alpha", "query": "제안서 초안", "allowed": [WS_ALPHA]},
221
+ {"key": "scoped_alpha_legacy", "query": "제안서 초안", "allowed": [WS_ALPHA],
222
+ "legacy": True},
223
+ {"key": "scoped_empty", "query": "제안서 초안", "allowed": []},
224
+ ]
225
+
226
+
227
+ def normalize_multi_hop(payload: Dict[str, Any]) -> Dict[str, Any]:
228
+ """Order a ``multi_hop_context`` answer deterministically.
229
+
230
+ **This is a golden normalization, not a port divergence.** Python's
231
+ traversal iterates ``frontier``, a ``set`` — so the order in which the
232
+ nodes of one hop are appended (and therefore the order their incident
233
+ edges are appended) depends on string hashing, which CPython randomizes
234
+ per process unless ``PYTHONHASHSEED`` is pinned. The *contents* are fully
235
+ determined (which nodes are visited, which edges are collected, and every
236
+ node's hop label); only the sequence is not, so the live response ordering
237
+ is unspecified and there is nothing for a port to reproduce.
238
+
239
+ Both sides therefore sort before comparing: nodes by ``(hop, id)``, edges
240
+ by ``(from, to, type, weight)``. The Rust port emits that order natively —
241
+ it is deterministic where Python is not, which is a strictly stronger
242
+ promise and not a different answer.
243
+ """
244
+ return {
245
+ "nodes": sorted(payload["nodes"], key=lambda node: (node["hop"], node["id"])),
246
+ "edges": sorted(
247
+ payload["edges"],
248
+ key=lambda edge: (edge["from"], edge["to"], edge["type"], edge["weight"]),
249
+ ),
250
+ }
251
+
252
+
253
+ class OrderedMultiHopStore:
254
+ """The store, with ``multi_hop_context`` ordered deterministically.
255
+
256
+ ``retrieve_context_for_generation`` feeds the traversal's node list into
257
+ ``extra_nodes``, and the concept section renders the first eight of it — so
258
+ Python's unordered ``frontier`` iteration reaches the *rendered markdown*,
259
+ not only the traversal payload. Both sides therefore see the normalized
260
+ order (see :func:`normalize_multi_hop`); every other attribute is forwarded
261
+ to the real store untouched.
262
+ """
263
+
264
+ def __init__(self, store: Any):
265
+ self._store = store
266
+
267
+ def __getattr__(self, name: str) -> Any:
268
+ return getattr(self._store, name)
269
+
270
+ def multi_hop_context(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
271
+ return normalize_multi_hop(self._store.multi_hop_context(*args, **kwargs))
272
+
273
+
274
+ def _scope(spec: Dict[str, Any]) -> Dict[str, Any]:
275
+ """The graph-layer scope every document-generation read takes."""
276
+ allowed = spec.get("allowed")
277
+ return {
278
+ "allowed_workspaces": None if allowed is None else set(allowed),
279
+ "include_legacy_global": spec.get("legacy", False),
280
+ }
281
+
282
+
283
+ def run_docgen_search(harness: Any, spec: Dict[str, Any]) -> List[Dict[str, Any]]:
284
+ """``KnowledgeGraphStore.search_for_document_generation`` for one spec."""
285
+ return harness.store.search_for_document_generation(
286
+ spec["query"], limit=spec.get("limit", 10), **_scope(spec)
287
+ )
288
+
289
+
290
+ def run_multi_hop(harness: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
291
+ """``multi_hop_context`` for one spec, in the normalized order."""
292
+ return normalize_multi_hop(
293
+ harness.store.multi_hop_context(
294
+ spec["node_ids"], max_hops=spec.get("max_hops", 2), **_scope(spec)
295
+ )
296
+ )
297
+
298
+
299
+ def run_context_document(harness: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
300
+ """``retrieve_context_for_generation`` plus the footnote over its sources.
301
+
302
+ The footnote is a second public entry point over the same ``sources`` list;
303
+ carrying it here proves that port too, without a suite of its own.
304
+ """
305
+ from lattice_brain.self_model import DEFAULT_SUMMARY_TOKENS
306
+ from latticeai.core.context_builder import (
307
+ DEFAULT_DOCUMENT_CONTEXT_BUDGET,
308
+ format_sources_footnote,
309
+ retrieve_context_for_generation,
310
+ )
311
+
312
+ context = retrieve_context_for_generation(
313
+ OrderedMultiHopStore(harness.store),
314
+ spec["query"],
315
+ max_results=spec.get("max_results", 10),
316
+ max_hops=spec.get("max_hops", 2),
317
+ budget=spec.get("budget", DEFAULT_DOCUMENT_CONTEXT_BUDGET),
318
+ include_self_model=spec.get("include_self_model", True),
319
+ self_model_tokens=spec.get("self_model_tokens", DEFAULT_SUMMARY_TOKENS),
320
+ **_scope(spec),
321
+ )
322
+ return {
323
+ "context": context,
324
+ "sources_footnote": format_sources_footnote(context["sources"]),
325
+ }
326
+
327
+
328
+ #: The three document-generation suites, as the generator merges them into
329
+ #: :data:`SUITES` / :data:`SUITE_RUNNERS`. Keeping the pair side by side is what
330
+ #: stops a spec list from arriving without the runner that answers it.
331
+ DOCGEN_SUITES: Dict[str, List[Dict[str, Any]]] = {
332
+ "docgen_search": DOCGEN_SEARCH_SPECS,
333
+ "multi_hop": MULTI_HOP_SPECS,
334
+ "context_document": CONTEXT_DOCUMENT_SPECS,
335
+ }
336
+
337
+ DOCGEN_RUNNERS: Dict[str, Any] = {
338
+ "docgen_search": run_docgen_search,
339
+ "multi_hop": run_multi_hop,
340
+ "context_document": run_context_document,
341
+ }
@@ -21,6 +21,26 @@
21
21
  "specific, not less."
22
22
  ],
23
23
 
24
+ "11.5.2": {
25
+ "_why": [
26
+ "Tight Ship release: deletions, consolidation and front-door fixes —",
27
+ "backend only. The frontend bundle is untouched; no captured screen",
28
+ "changes on purpose. All thirteen measured but not gated. Known",
29
+ "jitter: 05-memory-graph moves ~2-3% between captures from the",
30
+ "cytoscape layout (11.3.0-11.5.1 observed)."
31
+ ]
32
+ },
33
+
34
+ "11.5.1": {
35
+ "_why": [
36
+ "Rust Full Loop release: agent-loop orchestrator port, worker seams,",
37
+ "doc-gen context builder — backend only. The frontend bundle is",
38
+ "untouched; no captured screen changes on purpose. All thirteen",
39
+ "measured but not gated. Known jitter: 05-memory-graph moves ~2-3%",
40
+ "between captures from the cytoscape layout (11.3.0-11.5.0 observed)."
41
+ ]
42
+ },
43
+
24
44
  "11.5.0": {
25
45
  "_why": [
26
46
  "Rust Complete release: backend/desktop-topology architecture only",
@@ -1730,17 +1730,23 @@ dependencies = [
1730
1730
 
1731
1731
  [[package]]
1732
1732
  name = "lattice-agent"
1733
- version = "11.5.0"
1733
+ version = "11.5.2"
1734
1734
  dependencies = [
1735
1735
  "axum",
1736
+ "base64 0.22.1",
1737
+ "fancy-regex",
1738
+ "futures-core",
1739
+ "getrandom 0.4.3",
1740
+ "reqwest 0.12.28",
1736
1741
  "serde",
1737
1742
  "serde_json",
1743
+ "sha2",
1738
1744
  "tokio",
1739
1745
  ]
1740
1746
 
1741
1747
  [[package]]
1742
1748
  name = "lattice-ai-desktop"
1743
- version = "11.5.0"
1749
+ version = "11.5.2"
1744
1750
  dependencies = [
1745
1751
  "lattice-host",
1746
1752
  "lattice-jobs",
@@ -1755,7 +1761,7 @@ dependencies = [
1755
1761
 
1756
1762
  [[package]]
1757
1763
  name = "lattice-core"
1758
- version = "11.5.0"
1764
+ version = "11.5.2"
1759
1765
  dependencies = [
1760
1766
  "blake2",
1761
1767
  "rusqlite",
@@ -1765,7 +1771,7 @@ dependencies = [
1765
1771
 
1766
1772
  [[package]]
1767
1773
  name = "lattice-host"
1768
- version = "11.5.0"
1774
+ version = "11.5.2"
1769
1775
  dependencies = [
1770
1776
  "axum",
1771
1777
  "lattice-agent",
@@ -1782,7 +1788,7 @@ dependencies = [
1782
1788
 
1783
1789
  [[package]]
1784
1790
  name = "lattice-ingest"
1785
- version = "11.5.0"
1791
+ version = "11.5.2"
1786
1792
  dependencies = [
1787
1793
  "axum",
1788
1794
  "lattice-core",
@@ -1795,7 +1801,7 @@ dependencies = [
1795
1801
 
1796
1802
  [[package]]
1797
1803
  name = "lattice-jobs"
1798
- version = "11.5.0"
1804
+ version = "11.5.2"
1799
1805
  dependencies = [
1800
1806
  "axum",
1801
1807
  "lattice-core",
@@ -1807,7 +1813,7 @@ dependencies = [
1807
1813
 
1808
1814
  [[package]]
1809
1815
  name = "lattice-retrieval"
1810
- version = "11.5.0"
1816
+ version = "11.5.2"
1811
1817
  dependencies = [
1812
1818
  "axum",
1813
1819
  "fancy-regex",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "11.5.0"
3
+ version = "11.5.2"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -72,6 +72,9 @@ pub struct BackendStatus {
72
72
  /// Whether the background jobs timer is running in this process
73
73
  /// (11.5.0, additive).
74
74
  pub jobs_running: bool,
75
+ /// Why the front door is not there, when this shell was supposed to serve
76
+ /// one and could not bind it (11.5.2, additive). `null` normally.
77
+ pub gateway_error: Option<String>,
75
78
  }
76
79
 
77
80
  /// The gateway this shell is serving, and how to stop it.
@@ -89,6 +92,11 @@ pub struct DesktopBackend {
89
92
  gateway: Mutex<Option<RunningGateway>>,
90
93
  /// The jobs scheduler, mounted on `/host/jobs` in the gateway topology.
91
94
  jobs: Option<Arc<lattice_jobs::Scheduler>>,
95
+ /// Why the front door is not there, when it could not be bound.
96
+ ///
97
+ /// `None` means "nothing went wrong" — including in the topologies that
98
+ /// serve no gateway at all, where the worker *is* the front door.
99
+ gateway_error: Mutex<Option<String>>,
92
100
  }
93
101
 
94
102
  impl DesktopBackend {
@@ -116,6 +124,7 @@ impl DesktopBackend {
116
124
  health_deadline,
117
125
  gateway: Mutex::new(None),
118
126
  jobs,
127
+ gateway_error: Mutex::new(None),
119
128
  })
120
129
  }
121
130
 
@@ -139,6 +148,18 @@ impl DesktopBackend {
139
148
  self.plan.topology
140
149
  }
141
150
 
151
+ /// Why the front door is unavailable, when it is.
152
+ ///
153
+ /// `Some` only when this shell was supposed to serve the gateway and could
154
+ /// not bind it — in which case [`Self::app_url`] names a port nothing will
155
+ /// ever listen on, and `main::boot` must not navigate the window into it.
156
+ pub fn gateway_error(&self) -> Option<String> {
157
+ self.gateway_error
158
+ .lock()
159
+ .expect("gateway error lock")
160
+ .clone()
161
+ }
162
+
142
163
  /// Start the front door, then the worker.
143
164
  ///
144
165
  /// The gateway comes up first on purpose: it is what the webview polls, and
@@ -171,10 +192,16 @@ impl DesktopBackend {
171
192
  let listener = match bind_loopback(addr).await {
172
193
  Ok(listener) => listener,
173
194
  Err(err) => {
174
- eprintln!(
175
- "lattice-ai-desktop: the gateway could not bind {addr}: {err} — \
195
+ let message = format!(
196
+ "the gateway could not bind {addr}: {err} — \
176
197
  set LATTICEAI_DESKTOP_DIRECT=1 to run the worker as the front door"
177
198
  );
199
+ eprintln!("lattice-ai-desktop: {message}");
200
+ // Recorded, not only logged: `plan.origin` names the port that
201
+ // just failed to bind, so navigating the window there would
202
+ // land on a connection refused with no explanation. The window
203
+ // stays on the bundled shell and reads this instead.
204
+ *self.gateway_error.lock().expect("gateway error lock") = Some(message);
178
205
  return;
179
206
  }
180
207
  };
@@ -310,6 +337,7 @@ impl DesktopBackend {
310
337
  .as_ref()
311
338
  .map(|scheduler| scheduler.is_running())
312
339
  .unwrap_or(false),
340
+ gateway_error: self.gateway_error(),
313
341
  }
314
342
  }
315
343
 
@@ -457,4 +485,50 @@ mod tests {
457
485
  shell.stop_gateway();
458
486
  shell.stop_gateway();
459
487
  }
488
+
489
+ /// A shell that never had to bind a front door has nothing to report, and
490
+ /// its origin is navigable.
491
+ #[test]
492
+ fn a_healthy_shell_names_no_gateway_error() {
493
+ for probe in [
494
+ StaticProbe::new().with_env(PORT_ENV, "41844"),
495
+ StaticProbe::new().with_env(DIRECT_ENV, "1"),
496
+ ] {
497
+ let shell = backend(probe);
498
+ assert_eq!(shell.gateway_error(), None);
499
+ let value =
500
+ serde_json::to_value(shell.render(&WorkerStatus::idle(4825, true))).expect("json");
501
+ assert!(value["gateway_error"].is_null());
502
+ }
503
+ }
504
+
505
+ /// Binding the front door is the one failure that makes `app_url` name a
506
+ /// port nothing will ever answer on. The window must read the reason rather
507
+ /// than navigate into it.
508
+ #[tokio::test]
509
+ async fn a_gateway_that_cannot_bind_is_reported_and_not_navigated_to() {
510
+ // Hold the port the shell will try to bind.
511
+ let occupied = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
512
+ let port = occupied.local_addr().expect("addr").port();
513
+ let shell = backend(StaticProbe::new().with_env(PORT_ENV, &port.to_string()));
514
+ assert_eq!(shell.topology(), Topology::Gateway);
515
+
516
+ shell.start_gateway().await;
517
+
518
+ let reason = shell.gateway_error().expect("the failure is recorded");
519
+ assert!(reason.contains("could not bind"), "{reason}");
520
+ assert!(
521
+ reason.contains("LATTICEAI_DESKTOP_DIRECT"),
522
+ "the way out is named: {reason}"
523
+ );
524
+ assert!(
525
+ shell.app_url().contains(&port.to_string()),
526
+ "app_url still names the port nothing bound — which is why boot must \
527
+ read gateway_error instead of navigating"
528
+ );
529
+ let value =
530
+ serde_json::to_value(shell.render(&WorkerStatus::idle(port, true))).expect("serialise");
531
+ assert_eq!(value["gateway_error"], reason);
532
+ drop(occupied);
533
+ }
460
534
  }
@@ -82,9 +82,19 @@ fn boot(app: &tauri::App) {
82
82
  // Waiting in both places would double the worst case.
83
83
  shell.wait_until_serving().await;
84
84
  }
85
- // Navigate either way, as the shell always has: a window showing the
86
- // browser's own "cannot connect" is more use than a window showing
87
- // nothing while a worker that will never arrive is waited for.
85
+ // One reason not to navigate, and it is not "the worker is slow": if
86
+ // this shell was supposed to serve the front door and could not bind
87
+ // it, `app_url` names a port nothing will ever listen on, and sending
88
+ // the window there replaces a readable failure with a blank "cannot
89
+ // connect". The bundled shell stays up and reads `gateway_error` out of
90
+ // `desktopBackendStatus` instead.
91
+ if let Some(reason) = shell.gateway_error() {
92
+ eprintln!("lattice-ai-desktop: staying on the bundled shell — {reason}");
93
+ return;
94
+ }
95
+ // Otherwise navigate either way, as the shell always has: a window
96
+ // showing the browser's own "cannot connect" is more use than a window
97
+ // showing nothing while a worker that will never arrive is waited for.
88
98
  if let Ok(url) = tauri::Url::parse(&shell.app_url()) {
89
99
  let _ = window.navigate(url);
90
100
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "11.5.0",
4
+ "version": "11.5.2",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "security": {
25
- "csp": "default-src 'self' asset: tauri: http://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' ipc: http://127.0.0.1:* ws://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'",
25
+ "csp": "default-src 'self' asset: tauri: http://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' ipc: http://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'",
26
26
  "devCsp": "default-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'"
27
27
  }
28
28
  },