ltcai 11.1.0 → 11.4.0

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 (289) hide show
  1. package/README.md +47 -54
  2. package/docs/CHANGELOG.md +94 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
  6. package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -2
  10. package/docs/PERMISSION_MODE.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/architecture.md +6 -2
  14. package/docs/kg-schema.md +2 -2
  15. package/docs/v11.3.0_PLAN.md +202 -0
  16. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  17. package/lattice_brain/__init__.py +1 -1
  18. package/lattice_brain/gates.py +125 -0
  19. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  20. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  21. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  22. package/lattice_brain/graph/_kg_common/text.py +479 -0
  23. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  24. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  25. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  26. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  27. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  28. package/lattice_brain/graph/fusion.py +35 -4
  29. package/lattice_brain/graph/projection/__init__.py +42 -0
  30. package/lattice_brain/graph/projection/curation.py +500 -0
  31. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
  32. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  33. package/lattice_brain/graph/retrieval/context.py +197 -0
  34. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  35. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  36. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  37. package/lattice_brain/graph/retrieval/signals.py +95 -0
  38. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  39. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  40. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  41. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  42. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  43. package/lattice_brain/graph/schema.py +9 -0
  44. package/lattice_brain/graph/store.py +9 -0
  45. package/lattice_brain/graph/vector_index/selector.py +32 -2
  46. package/lattice_brain/ingestion/__init__.py +130 -0
  47. package/lattice_brain/ingestion/_contract.py +90 -0
  48. package/lattice_brain/ingestion/constants.py +127 -0
  49. package/lattice_brain/ingestion/folder_scan.py +57 -0
  50. package/lattice_brain/ingestion/folders.py +258 -0
  51. package/lattice_brain/ingestion/hashing.py +26 -0
  52. package/lattice_brain/ingestion/jobs_api.py +107 -0
  53. package/lattice_brain/ingestion/models.py +80 -0
  54. package/lattice_brain/ingestion/pipeline.py +486 -0
  55. package/lattice_brain/ingestion/quality.py +209 -0
  56. package/lattice_brain/ingestion/routing.py +295 -0
  57. package/lattice_brain/multimodal/__init__.py +164 -0
  58. package/lattice_brain/multimodal/audio.py +77 -0
  59. package/lattice_brain/multimodal/common.py +118 -0
  60. package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
  61. package/lattice_brain/multimodal/ports.py +169 -0
  62. package/lattice_brain/multimodal/video.py +410 -0
  63. package/lattice_brain/portability/__init__.py +90 -0
  64. package/lattice_brain/portability/_contract.py +42 -0
  65. package/lattice_brain/portability/backups.py +338 -0
  66. package/lattice_brain/portability/bundles.py +136 -0
  67. package/lattice_brain/portability/constants.py +93 -0
  68. package/lattice_brain/portability/fsops.py +138 -0
  69. package/lattice_brain/portability/service.py +41 -0
  70. package/lattice_brain/portability/sharing.py +714 -0
  71. package/lattice_brain/runtime/__init__.py +1 -1
  72. package/lattice_brain/runtime/multi_agent.py +1 -1
  73. package/lattice_brain/sealed_box.py +244 -0
  74. package/lattice_brain/synthesis.py +24 -1
  75. package/latticeai/__init__.py +1 -1
  76. package/latticeai/api/brain_intelligence.py +4 -0
  77. package/latticeai/api/chat.py +11 -0
  78. package/latticeai/api/chat_helpers.py +16 -3
  79. package/latticeai/api/chat_hybrid.py +32 -1
  80. package/latticeai/api/chronicle.py +63 -0
  81. package/latticeai/api/features.py +70 -0
  82. package/latticeai/api/local_files.py +102 -0
  83. package/latticeai/api/portability.py +39 -4
  84. package/latticeai/api/review_queue.py +126 -0
  85. package/latticeai/api/search.py +16 -2
  86. package/latticeai/core/agent/__init__.py +93 -0
  87. package/latticeai/core/agent/_contract.py +79 -0
  88. package/latticeai/core/agent/context.py +57 -0
  89. package/latticeai/core/agent/deps.py +125 -0
  90. package/latticeai/core/agent/execution.py +622 -0
  91. package/latticeai/core/agent/planning.py +145 -0
  92. package/latticeai/core/agent/recovery.py +157 -0
  93. package/latticeai/core/agent/runtime.py +210 -0
  94. package/latticeai/core/agent/verification.py +231 -0
  95. package/latticeai/core/config.py +4 -1
  96. package/latticeai/core/context_builder.py +6 -3
  97. package/latticeai/core/embedding_providers/__init__.py +151 -0
  98. package/latticeai/core/embedding_providers/base.py +199 -0
  99. package/latticeai/core/embedding_providers/captions.py +162 -0
  100. package/latticeai/core/embedding_providers/profiles.py +126 -0
  101. package/latticeai/core/embedding_providers/text.py +350 -0
  102. package/latticeai/core/embedding_providers/vision.py +352 -0
  103. package/latticeai/core/file_generation/__init__.py +115 -0
  104. package/latticeai/core/file_generation/bundles.py +76 -0
  105. package/latticeai/core/file_generation/extraction.py +154 -0
  106. package/latticeai/core/file_generation/inference.py +235 -0
  107. package/latticeai/core/file_generation/orchestration.py +152 -0
  108. package/latticeai/core/file_generation/prompting.py +117 -0
  109. package/latticeai/core/file_generation/repair.py +114 -0
  110. package/latticeai/core/file_generation/sanitize.py +61 -0
  111. package/latticeai/core/file_generation/validation.py +201 -0
  112. package/latticeai/core/legacy_compatibility.py +1 -1
  113. package/latticeai/core/marketplace.py +1 -1
  114. package/latticeai/core/messages.py +152 -0
  115. package/latticeai/core/model_compat.py +73 -2
  116. package/latticeai/core/workspace_os_constants.py +1 -1
  117. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  118. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  119. package/latticeai/integrations/telegram_bot/config.py +86 -0
  120. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  121. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  122. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  123. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  124. package/latticeai/models/model_providers.py +12 -4
  125. package/latticeai/models/router/__init__.py +88 -0
  126. package/latticeai/models/router/_contract.py +66 -0
  127. package/latticeai/models/router/branding.py +56 -0
  128. package/latticeai/models/router/catalog.py +69 -0
  129. package/latticeai/models/router/documents.py +199 -0
  130. package/latticeai/models/router/errors.py +37 -0
  131. package/latticeai/models/router/generation.py +258 -0
  132. package/latticeai/models/router/loading.py +291 -0
  133. package/latticeai/models/router/local_models.py +85 -0
  134. package/latticeai/models/router/registry.py +147 -0
  135. package/latticeai/runtime/build_phases/__init__.py +82 -0
  136. package/latticeai/runtime/build_phases/features.py +407 -0
  137. package/latticeai/runtime/build_phases/foundation.py +555 -0
  138. package/latticeai/runtime/build_phases/web.py +492 -0
  139. package/latticeai/runtime/chat_wiring.py +4 -0
  140. package/latticeai/runtime/feature_toggle_wiring.py +163 -0
  141. package/latticeai/runtime/router_registration.py +11 -0
  142. package/latticeai/runtime/runtime_context.py +1 -0
  143. package/latticeai/services/app_context.py +8 -0
  144. package/latticeai/services/architecture_readiness.py +48 -19
  145. package/latticeai/services/automation_intelligence.py +22 -2
  146. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  147. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  148. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  149. package/latticeai/services/brain_intelligence/constants.py +47 -0
  150. package/latticeai/services/brain_intelligence/digest.py +258 -0
  151. package/latticeai/services/brain_intelligence/health.py +331 -0
  152. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  153. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  154. package/latticeai/services/brain_intelligence/service.py +48 -0
  155. package/latticeai/services/chronicle.py +557 -0
  156. package/latticeai/services/command_center.py +10 -4
  157. package/latticeai/services/feature_toggles.py +502 -0
  158. package/latticeai/services/folder_watch.py +122 -1
  159. package/latticeai/services/hybrid_chat.py +56 -5
  160. package/latticeai/services/interop_bridges.py +978 -0
  161. package/latticeai/services/memory_service/__init__.py +52 -0
  162. package/latticeai/services/memory_service/_contract.py +100 -0
  163. package/latticeai/services/memory_service/brief.py +431 -0
  164. package/latticeai/services/memory_service/constants.py +57 -0
  165. package/latticeai/services/memory_service/maintenance.py +138 -0
  166. package/latticeai/services/memory_service/manager.py +186 -0
  167. package/latticeai/services/memory_service/proof.py +136 -0
  168. package/latticeai/services/memory_service/recall.py +225 -0
  169. package/latticeai/services/memory_service/service.py +48 -0
  170. package/latticeai/services/memory_service/stores.py +110 -0
  171. package/latticeai/services/model_capability_registry.py +434 -261
  172. package/latticeai/services/model_catalog.py +95 -61
  173. package/latticeai/services/model_recommendation.py +18 -11
  174. package/latticeai/services/model_runtime/__init__.py +322 -0
  175. package/latticeai/services/model_runtime/cloud.py +87 -0
  176. package/latticeai/services/model_runtime/download.py +282 -0
  177. package/latticeai/services/model_runtime/engines.py +341 -0
  178. package/latticeai/services/model_runtime/loading.py +178 -0
  179. package/latticeai/services/model_runtime/service.py +129 -0
  180. package/latticeai/services/model_runtime/state.py +131 -0
  181. package/latticeai/services/model_runtime/status.py +255 -0
  182. package/latticeai/services/multimodal_ports.py +26 -1
  183. package/latticeai/services/obsidian_bridge.py +16 -25
  184. package/latticeai/services/product_readiness.py +15 -7
  185. package/latticeai/services/search_service.py +149 -2
  186. package/latticeai/services/tool_dispatch.py +4 -0
  187. package/latticeai/setup/auto_setup.py +27 -30
  188. package/latticeai/setup/wizard/__init__.py +126 -0
  189. package/latticeai/setup/wizard/catalog.py +172 -0
  190. package/latticeai/setup/wizard/detect.py +323 -0
  191. package/latticeai/setup/wizard/install.py +348 -0
  192. package/latticeai/setup/wizard/paths.py +168 -0
  193. package/latticeai/setup/wizard/plans.py +74 -0
  194. package/latticeai/setup/wizard/recommend.py +320 -0
  195. package/package.json +6 -2
  196. package/scripts/bump_version.py +14 -0
  197. package/scripts/capture_release_evidence.mjs +33 -21
  198. package/scripts/check_current_release_docs.mjs +1 -1
  199. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  200. package/scripts/check_max_file_lines.mjs +102 -0
  201. package/scripts/check_release_evidence_bound.mjs +30 -15
  202. package/scripts/check_screenshot_pixel_delta.py +34 -4
  203. package/scripts/check_server_i18n.mjs +2 -0
  204. package/scripts/generate_rust_parity_fixtures.py +562 -0
  205. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  206. package/scripts/release_screen_claims.json +44 -2
  207. package/scripts/verify_hf_model_registry.py +253 -218
  208. package/src-tauri/Cargo.lock +361 -3
  209. package/src-tauri/Cargo.toml +6 -1
  210. package/src-tauri/src/backend.rs +349 -0
  211. package/src-tauri/src/folder.rs +33 -0
  212. package/src-tauri/src/main.rs +97 -399
  213. package/src-tauri/tauri.conf.json +1 -1
  214. package/static/app/asset-manifest.json +41 -37
  215. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  216. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  217. package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
  218. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  219. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  220. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  221. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  222. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  223. package/static/app/assets/Library-DwO3yZST.js +1 -0
  224. package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
  225. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  226. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  227. package/static/app/assets/System-DW8F-2xL.js +1 -0
  228. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  229. package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
  230. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  231. package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
  232. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  233. package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
  234. package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
  235. package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
  236. package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
  237. package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
  238. package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
  239. package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
  240. package/static/app/assets/index-_u5iUHDr.js +10 -0
  241. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  242. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  243. package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
  244. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  245. package/static/app/assets/search-BybIWPNd.js +1 -0
  246. package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
  247. package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
  248. package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
  249. package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
  250. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  251. package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
  252. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  253. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  254. package/static/app/index.html +4 -4
  255. package/static/sw.js +1 -1
  256. package/lattice_brain/graph/_kg_common.py +0 -1331
  257. package/lattice_brain/graph/discovery_index.py +0 -1141
  258. package/lattice_brain/graph/retrieval.py +0 -1120
  259. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  260. package/lattice_brain/ingestion.py +0 -1377
  261. package/lattice_brain/portability.py +0 -1210
  262. package/latticeai/core/agent.py +0 -1412
  263. package/latticeai/core/embedding_providers.py +0 -1196
  264. package/latticeai/core/file_generation.py +0 -1047
  265. package/latticeai/integrations/telegram_bot.py +0 -1390
  266. package/latticeai/models/router.py +0 -1007
  267. package/latticeai/runtime/build_phases.py +0 -1422
  268. package/latticeai/services/brain_intelligence.py +0 -967
  269. package/latticeai/services/memory_service.py +0 -1177
  270. package/latticeai/services/model_runtime.py +0 -1281
  271. package/latticeai/setup/wizard.py +0 -1277
  272. package/static/app/assets/Act-D0HWqtn0.js +0 -1
  273. package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
  274. package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
  275. package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
  276. package/static/app/assets/Capture-CT8v1StE.js +0 -1
  277. package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
  278. package/static/app/assets/Library-DDoxFE5c.js +0 -1
  279. package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
  280. package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
  281. package/static/app/assets/System-BRllvYXd.js +0 -1
  282. package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
  283. package/static/app/assets/brain-uMb_5hnO.js +0 -1
  284. package/static/app/assets/index-0AvoEBzJ.js +0 -10
  285. package/static/app/assets/input-B_5ZJ9oy.js +0 -1
  286. package/static/app/assets/primitives-CVwew78r.js +0 -1
  287. package/static/app/assets/search-DkhnOKZt.js +0 -1
  288. package/static/app/assets/utils-DcDMoZIe.js +0 -4
  289. package/static/app/assets/workspace-LtRRSKTf.js +0 -1
@@ -0,0 +1,492 @@
1
+ """Phases 5-8: domain singletons, the web app, services, foundation routes.
2
+
3
+ The middle of the build order. ``phase_domain`` runs before ``phase_web``
4
+ because the lifespan, the static status routes and the model runtime service
5
+ are all wired against the router it builds; ``phase_services`` runs after
6
+ ``phase_web`` because the AppContext carries handles the web phase produces.
7
+ Split out of the single ``build_phases`` module in v11.3.0.
8
+
9
+ Every heavy import lives *inside* a phase, never at module scope.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Any, Dict, Optional
16
+
17
+ from latticeai.runtime.runtime_context import RuntimeContext
18
+
19
+
20
+ # ── phase 5: domain singletons (must precede the web app) ────────────────────
21
+ def phase_domain(ctx: RuntimeContext) -> None:
22
+ """The model router, the garden, and the chat service.
23
+
24
+ Separated from :func:`phase_services` because ``phase_web`` needs the model
25
+ router: the lifespan hooks, the static status routes, and the model runtime
26
+ service are all wired against it.
27
+ """
28
+ ctx.enter("domain")
29
+
30
+ from lattice_brain.graph.runtime import set_llm_router
31
+ from latticeai.models.router import LLMRouter
32
+ from latticeai.services.chat_service import ChatService
33
+ from latticeai.services.p_reinforce import PReinforceGardener
34
+ from latticeai.services.tool_dispatch import configure_tool_dispatch
35
+
36
+ model_router = LLMRouter()
37
+ set_llm_router(model_router)
38
+ configure_tool_dispatch(load_users=ctx.load_users, get_user_role=ctx.get_user_role)
39
+ ctx.set(model_router=model_router)
40
+
41
+ # v4 garden absorption: the vault is the user-owned markdown mirror and the
42
+ # brain is authoritative. Existing notes import idempotently (content-hash
43
+ # dedup), and garden context queries the brain rather than rescanning.
44
+ gardener = PReinforceGardener(
45
+ ingestion_pipeline=ctx.INGESTION_PIPELINE if ctx.ENABLE_GRAPH else None,
46
+ knowledge_graph=ctx.KNOWLEDGE_GRAPH,
47
+ )
48
+ if ctx.ENABLE_GRAPH:
49
+ try:
50
+ garden_import = gardener.import_vault()
51
+ if garden_import.get("failed"):
52
+ logging.warning(
53
+ "garden vault import: %s notes failed to ingest",
54
+ garden_import["failed"],
55
+ )
56
+ except Exception as exc:
57
+ logging.warning("garden vault import skipped: %s", exc)
58
+ ctx.set(gardener=gardener)
59
+
60
+ # The chat service owns persistence and trace behaviour once its user-key
61
+ # dependencies exist.
62
+ ctx.set(
63
+ CHAT_SERVICE=ChatService(
64
+ store=ctx.WORKSPACE_OS,
65
+ get_history=ctx.get_history,
66
+ save_to_history=ctx.save_to_history,
67
+ get_history_user=ctx.get_history_user,
68
+ )
69
+ )
70
+
71
+
72
+ def self_model_port(workspace_graph: Any) -> Any:
73
+ """The agent loop's Self-Model port (v11.2.0).
74
+
75
+ 11.1.0 built ``executor_prompt_for(self_model_summary=…)`` and had nothing
76
+ to pass it; this is what the composition root passes. ``summary_for_prompt``
77
+ never raises and answers ``""`` for a Brain that knows nothing about its
78
+ owner — which produces exactly the pre-11.2.0 prompt bytes.
79
+
80
+ A module-level factory rather than a closure inside ``phase_services``
81
+ because a port that cannot be called on its own cannot be tested on its own.
82
+ """
83
+
84
+ def _summary(*, user_email: Any = None, workspace_id: Any = None) -> str:
85
+ from lattice_brain.self_model import summary_for_prompt
86
+
87
+ graph = workspace_graph()
88
+ if graph is None:
89
+ return ""
90
+ return summary_for_prompt(graph, workspace_id=workspace_id)
91
+
92
+ return _summary
93
+
94
+
95
+ # ── phase 6b: retrieval, agent runtime, and the typed AppContext ─────────────
96
+ def phase_services(ctx: RuntimeContext) -> None:
97
+ """Retrieval/context assembly, the chat agent runtime, and the AppContext.
98
+
99
+ Runs after ``phase_web`` because the AppContext carries handles the web
100
+ phase produces (``ui_file_response``, ``local_sysinfo``, ``graph_stats``)
101
+ and the telegram mirror needs the lifespan's ``_spawn``.
102
+ """
103
+ ctx.enter("services")
104
+
105
+ from latticeai.api.chat import build_recent_chat_context
106
+ from latticeai.core.enterprise import capability_registry
107
+ from latticeai.core.mcp_registry import (
108
+ SKILLS_DIR,
109
+ _fetch_skills_marketplace,
110
+ install_skill,
111
+ )
112
+ from latticeai.core.workspace_os import remove_skill_directory
113
+ from latticeai.runtime.chat_wiring import (
114
+ build_chat_agent_runtime_from_context,
115
+ maybe_build_telegram_chat_mirror,
116
+ )
117
+ from latticeai.runtime.context_runtime import build_context_runtime
118
+ from latticeai.services.app_context import AppContext
119
+ from latticeai.services.tool_dispatch import build_agent_runtime
120
+ from latticeai.setup.wizard import get_recommendations, scan_environment
121
+ from latticeai.tools import execute_tool, knowledge_save
122
+
123
+ gardener = ctx.gardener
124
+ model_router = ctx.model_router
125
+
126
+ def _workspace_settings_payload() -> Dict:
127
+ return {
128
+ "mode": ctx.APP_MODE,
129
+ "host": ctx.DEFAULT_HOST,
130
+ "port": ctx.DEFAULT_PORT,
131
+ "require_auth": ctx.REQUIRE_AUTH,
132
+ "enable_graph": ctx.ENABLE_GRAPH,
133
+ "allow_local_models": ctx.ALLOW_LOCAL_MODELS,
134
+ "static_dir": str(ctx.STATIC_DIR),
135
+ "data_dir": str(ctx.DATA_DIR),
136
+ }
137
+
138
+ def _workspace_models_payload() -> Dict:
139
+ return {
140
+ "current_model": ctx.model_router.current_model_id,
141
+ "loaded_models": ctx.model_router.loaded_model_ids,
142
+ "public_model": ctx.PUBLIC_MODEL,
143
+ "local_model": ctx.LOCAL_MODEL,
144
+ "local_draft_model": ctx.LOCAL_DRAFT_MODEL,
145
+ }
146
+
147
+ def _embedding_info() -> dict:
148
+ from latticeai.core.embedding_providers import (
149
+ PROVIDER_TYPES,
150
+ embedding_provider_profiles,
151
+ )
152
+
153
+ info = ctx.EMBEDDER.as_dict()
154
+ info["available_providers"] = list(PROVIDER_TYPES)
155
+ info["profile"] = ctx.CONFIG.embedding_profile or ""
156
+ info["profiles"] = embedding_provider_profiles()
157
+ return info
158
+
159
+ def _allowed_workspaces_for(user: Any) -> Any:
160
+ # No-auth local mode is single-user: no scoping. With auth, reads are
161
+ # scoped to the caller's memberships (legacy-global rows need an
162
+ # explicit maintenance-only opt-in).
163
+ if not ctx.REQUIRE_AUTH or not user:
164
+ return None
165
+ return ctx.PLATFORM.allowed_scopes(user)
166
+
167
+ def _recent_chat_context(
168
+ limit: int = 10,
169
+ include_image_missing_replies: bool = True,
170
+ user_email: Optional[str] = None,
171
+ conversation_id: Optional[str] = None,
172
+ workspace_id: Optional[str] = None,
173
+ ) -> str:
174
+ return build_recent_chat_context(
175
+ get_history=ctx.get_history,
176
+ limit=limit,
177
+ include_image_missing_replies=include_image_missing_replies,
178
+ user_email=user_email,
179
+ conversation_id=conversation_id,
180
+ workspace_id=workspace_id,
181
+ )
182
+
183
+ ctx.set(
184
+ _workspace_settings_payload=_workspace_settings_payload,
185
+ _workspace_models_payload=_workspace_models_payload,
186
+ _embedding_info=_embedding_info,
187
+ _allowed_workspaces_for=_allowed_workspaces_for,
188
+ _recent_chat_context=_recent_chat_context,
189
+ )
190
+
191
+ context_runtime = build_context_runtime(
192
+ graph_store=ctx._workspace_graph(),
193
+ ingestion_pipeline=ctx.INGESTION_PIPELINE,
194
+ memory_service=ctx.MEMORY_SERVICE,
195
+ gardener=gardener,
196
+ require_auth=ctx.REQUIRE_AUTH,
197
+ allowed_scopes_for_user=lambda user_email: ctx.PLATFORM.allowed_scopes(
198
+ user_email
199
+ ),
200
+ )
201
+ ctx.adopt(
202
+ context_runtime,
203
+ "SEARCH_SERVICE",
204
+ "BRAIN_MEMORY",
205
+ "CONTEXT_ASSEMBLER",
206
+ "ARTIFACT_LEDGER",
207
+ "_scoped_hybrid_search",
208
+ )
209
+
210
+ # Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy, so
211
+ # latticeai.api.chat never imports the 45KB telegram module.
212
+ ctx.set(
213
+ on_chat_message=maybe_build_telegram_chat_mirror(
214
+ enable_telegram=ctx.ENABLE_TELEGRAM,
215
+ spawn=ctx._spawn,
216
+ )
217
+ )
218
+
219
+ ctx.set(
220
+ CHAT_AGENT_RUNTIME=build_chat_agent_runtime_from_context(
221
+ build_agent_runtime=build_agent_runtime,
222
+ model_router=model_router,
223
+ execute_tool=execute_tool,
224
+ recent_chat_context=_recent_chat_context,
225
+ clear_history=ctx.clear_history,
226
+ knowledge_save=knowledge_save,
227
+ audit=ctx.append_audit_event,
228
+ hooks=ctx.HOOKS_REGISTRY,
229
+ brain_memory=ctx.BRAIN_MEMORY,
230
+ self_model_summary=self_model_port(
231
+ lambda: ctx.KNOWLEDGE_GRAPH if ctx.ENABLE_GRAPH else None
232
+ ),
233
+ )
234
+ )
235
+
236
+ # One typed context object replaces the historical 25-30-kwarg wiring.
237
+ ctx.set(
238
+ app_context=AppContext(
239
+ config=ctx.CONFIG,
240
+ data_dir=ctx.DATA_DIR,
241
+ static_dir=ctx.STATIC_DIR,
242
+ base_dir=ctx.BASE_DIR,
243
+ skills_dir=SKILLS_DIR,
244
+ model_router=model_router,
245
+ workspace_store=ctx.WORKSPACE_OS,
246
+ workspace_service=ctx.WORKSPACE_SERVICE,
247
+ knowledge_graph=ctx.KNOWLEDGE_GRAPH,
248
+ local_kg_watcher=ctx.LOCAL_KG_WATCHER,
249
+ chat_service=ctx.CHAT_SERVICE,
250
+ context_assembler=ctx.CONTEXT_ASSEMBLER,
251
+ artifact_ledger=ctx.ARTIFACT_LEDGER,
252
+ brain_memory=ctx.BRAIN_MEMORY,
253
+ ingestion_pipeline=ctx.INGESTION_PIPELINE if ctx.ENABLE_GRAPH else None,
254
+ chat_agent_runtime=ctx.CHAT_AGENT_RUNTIME,
255
+ gardener=gardener,
256
+ hooks=ctx.HOOKS_REGISTRY,
257
+ realtime_bus=ctx.REALTIME_BUS,
258
+ capability_registry=capability_registry,
259
+ require_user=ctx.require_user,
260
+ require_admin=ctx.require_admin,
261
+ get_current_user=ctx.get_current_user,
262
+ load_users=ctx.load_users,
263
+ get_user_role=ctx.get_user_role,
264
+ enforce_rate_limit=ctx.enforce_rate_limit,
265
+ allowed_workspaces_for=ctx._history_allowed_workspaces_for,
266
+ append_audit_event=ctx.append_audit_event,
267
+ get_audit_log=ctx.get_audit_log,
268
+ get_history=ctx.get_history,
269
+ get_history_user=ctx.get_history_user,
270
+ save_to_history=ctx.save_to_history,
271
+ clear_history=ctx.clear_history,
272
+ clear_conversation=ctx.clear_conversation,
273
+ group_history_conversations=ctx.group_history_conversations,
274
+ get_conversation_messages=ctx.get_conversation_messages,
275
+ conversation_title=ctx.conversation_title,
276
+ enable_graph=ctx.ENABLE_GRAPH,
277
+ require_graph=ctx._require_graph,
278
+ workspace_graph=ctx._workspace_graph,
279
+ graph_stats=ctx._graph_stats_safe,
280
+ # Provider, not a value: REVIEW_QUEUE lands two phases later.
281
+ review_queue=lambda: ctx.REVIEW_QUEUE,
282
+ workspace_models=_workspace_models_payload,
283
+ workspace_settings=_workspace_settings_payload,
284
+ scan_environment=scan_environment,
285
+ local_sysinfo=ctx.local_sysinfo,
286
+ get_recommendations=get_recommendations,
287
+ fetch_skills_marketplace=_fetch_skills_marketplace,
288
+ install_skill=install_skill,
289
+ remove_skill_directory=remove_skill_directory,
290
+ redact_secret_text=ctx.redact_secret_text,
291
+ ui_file_response=ctx.ui_file_response,
292
+ public_model=ctx.PUBLIC_MODEL,
293
+ local_model=ctx.LOCAL_MODEL or "",
294
+ on_chat_message=ctx.on_chat_message,
295
+ funnel_metrics=ctx.FUNNEL_METRICS,
296
+ )
297
+ )
298
+ ctx.app.state.context = ctx.app_context
299
+
300
+
301
+ # ── phase 6: web app and foundation routers ──────────────────────────────────
302
+ def phase_web(ctx: RuntimeContext) -> None:
303
+ """Lifespan, the FastAPI app, model runtime service, foundation routers."""
304
+ ctx.enter("web")
305
+
306
+ from latticeai.api.admin import create_admin_router
307
+ from latticeai.api.auth import create_auth_router
308
+ from latticeai.api.invitations import create_invitations_router
309
+ from latticeai.api.security_dashboard import (
310
+ create_security_router as _create_security_router,
311
+ )
312
+ from latticeai.api.static_routes import create_static_routes_router
313
+ from latticeai.core.policy import policy_matrix
314
+ from latticeai.core.product_hardening import build_product_hardening_status
315
+ from latticeai.core.security import hash_password
316
+ from latticeai.core.users import ensure_user_identity
317
+ from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
318
+ from latticeai.runtime.model_wiring import configure_model_runtime_from_context
319
+ from latticeai.runtime.router_registration import (
320
+ build_auth_admin_security_router_bundle,
321
+ build_static_routes_bundle,
322
+ )
323
+ from latticeai.runtime.web_runtime import build_web_runtime
324
+ from latticeai.services.model_runtime import (
325
+ LOCAL_SERVER_PROCESSES,
326
+ build_model_runtime,
327
+ )
328
+ from latticeai.tools import ensure_agent_root
329
+
330
+ lifespan_runtime = build_lifespan_runtime(
331
+ app_mode=ctx.APP_MODE,
332
+ enable_telegram=ctx.ENABLE_TELEGRAM,
333
+ autoload_models=ctx.AUTOLOAD_MODELS,
334
+ is_public_mode=ctx.IS_PUBLIC_MODE,
335
+ public_model=ctx.PUBLIC_MODEL,
336
+ allow_local_models=ctx.ALLOW_LOCAL_MODELS,
337
+ local_model=ctx.LOCAL_MODEL,
338
+ local_draft_model=ctx.LOCAL_DRAFT_MODEL,
339
+ model_idle_unload_seconds=ctx.MODEL_IDLE_UNLOAD_SECONDS,
340
+ model_router=ctx.model_router,
341
+ local_kg_watcher=ctx.LOCAL_KG_WATCHER,
342
+ local_server_processes=LOCAL_SERVER_PROCESSES,
343
+ logger=logging,
344
+ )
345
+ ctx.adopt(lifespan_runtime, "_spawn", "lifespan")
346
+
347
+ web_runtime = build_web_runtime(
348
+ app_mode=ctx.APP_MODE,
349
+ app_version=ctx.APP_VERSION,
350
+ lifespan=ctx.lifespan,
351
+ default_host=ctx.DEFAULT_HOST,
352
+ default_port=ctx.DEFAULT_PORT,
353
+ cors_extra_origins=ctx.CORS_EXTRA_ORIGINS,
354
+ cors_allow_network=ctx.CORS_ALLOW_NETWORK,
355
+ static_dir=ctx.STATIC_DIR,
356
+ csrf_trusted_origins=ctx.CSRF_TRUSTED_ORIGINS,
357
+ )
358
+ ctx.adopt(web_runtime, "app")
359
+ ensure_agent_root()
360
+
361
+ ctx.set(
362
+ model_runtime_service=configure_model_runtime_from_context(
363
+ build_model_runtime=build_model_runtime,
364
+ router=ctx.model_router,
365
+ APP_MODE=ctx.APP_MODE,
366
+ DEFAULT_HOST=ctx.DEFAULT_HOST,
367
+ DEFAULT_PORT=ctx.DEFAULT_PORT,
368
+ DATA_DIR=ctx.DATA_DIR,
369
+ BASE_DIR=ctx.BASE_DIR,
370
+ ENABLE_TELEGRAM=ctx.ENABLE_TELEGRAM,
371
+ ENABLE_GRAPH=ctx.ENABLE_GRAPH,
372
+ AUTOLOAD_MODELS=ctx.AUTOLOAD_MODELS,
373
+ MODEL_IDLE_UNLOAD_SECONDS=ctx.MODEL_IDLE_UNLOAD_SECONDS,
374
+ ALLOW_MODEL_DOWNLOADS=ctx.ALLOW_MODEL_DOWNLOADS,
375
+ MODEL_DOWNLOAD_TIMEOUT=ctx.MODEL_DOWNLOAD_TIMEOUT,
376
+ ALLOW_LOCAL_MODELS=ctx.ALLOW_LOCAL_MODELS,
377
+ REQUIRE_AUTH=ctx.REQUIRE_AUTH,
378
+ INVITE_GATE_ENABLED=ctx.INVITE_GATE_ENABLED,
379
+ ALLOW_PLAINTEXT_API_KEYS=ctx.ALLOW_PLAINTEXT_API_KEYS,
380
+ CORS_ALLOW_NETWORK=ctx.CORS_ALLOW_NETWORK,
381
+ PUBLIC_MODEL=ctx.PUBLIC_MODEL,
382
+ LOCAL_MODEL=ctx.LOCAL_MODEL,
383
+ IS_PUBLIC_MODE=ctx.IS_PUBLIC_MODE,
384
+ keyring=ctx.keyring,
385
+ get_current_user=ctx.get_current_user,
386
+ get_user_api_key=ctx.get_user_api_key,
387
+ )
388
+ )
389
+
390
+ static_routes_bundle = build_static_routes_bundle(
391
+ create_static_routes_router=create_static_routes_router,
392
+ static_dir=ctx.STATIC_DIR,
393
+ invite_gate_enabled=ctx.INVITE_GATE_ENABLED,
394
+ invite_code=ctx.INVITE_CODE,
395
+ invite_cookie_secret=ctx.INVITE_COOKIE_SECRET,
396
+ secure_cookies=ctx.SECURE_COOKIES,
397
+ app_mode=ctx.APP_MODE,
398
+ model_router=ctx.model_router,
399
+ require_user=ctx.require_user,
400
+ )
401
+ ctx.adopt(
402
+ static_routes_bundle,
403
+ "STATIC_ROUTES",
404
+ "ui_file_response",
405
+ "local_sysinfo",
406
+ "invite_authorized",
407
+ )
408
+
409
+ foundation_router_bundle = build_auth_admin_security_router_bundle(
410
+ create_auth_router=create_auth_router,
411
+ load_users=ctx.load_users,
412
+ save_users=ctx.save_users,
413
+ hash_password=hash_password,
414
+ verify_and_migrate_password=ctx.verify_and_migrate_password,
415
+ create_session=ctx.create_session,
416
+ get_session_email=ctx.get_session_email,
417
+ invalidate_session=ctx.invalidate_session,
418
+ extract_bearer_token=ctx._extract_bearer_token,
419
+ get_user_role=ctx.get_user_role,
420
+ require_user=ctx.require_user,
421
+ check_ip_rate_limit=ctx._check_rate_limit,
422
+ client_ip=ctx._client_ip,
423
+ get_sso_settings=ctx.get_sso_settings,
424
+ get_sso_discovery=ctx._get_sso_discovery,
425
+ public_sso_config=ctx.public_sso_config,
426
+ open_registration=ctx.OPEN_REGISTRATION,
427
+ session_ttl=ctx._SESSION_TTL,
428
+ require_auth=ctx.REQUIRE_AUTH,
429
+ secure_cookies=ctx.SECURE_COOKIES,
430
+ invite_authorized=ctx.invite_authorized,
431
+ ensure_identity=ensure_user_identity,
432
+ create_admin_router=create_admin_router,
433
+ require_admin=ctx.require_admin,
434
+ get_history=ctx.get_history,
435
+ get_audit_log=ctx.get_audit_log,
436
+ audit_file=ctx.AUDIT_FILE,
437
+ public_user=ctx.public_user,
438
+ load_vpc_config=ctx.load_vpc_config,
439
+ save_vpc_config=ctx.save_vpc_config,
440
+ build_admin_audit_report=ctx.build_admin_audit_report,
441
+ build_sensitivity_report=ctx.build_sensitivity_report,
442
+ append_audit_event=ctx.append_audit_event,
443
+ save_sso_config=ctx.save_sso_config,
444
+ knowledge_graph=ctx.KNOWLEDGE_GRAPH,
445
+ enable_graph=ctx.ENABLE_GRAPH,
446
+ logger=logging,
447
+ invite_code=ctx.INVITE_CODE,
448
+ invite_gate_enabled=ctx.INVITE_GATE_ENABLED,
449
+ default_port=ctx.DEFAULT_PORT,
450
+ policy_matrix=policy_matrix,
451
+ build_product_hardening_status=build_product_hardening_status,
452
+ config=ctx.CONFIG,
453
+ kg_portability=ctx.KG_PORTABILITY,
454
+ device_identity=ctx.DEVICE_IDENTITY,
455
+ create_invitations_router=create_invitations_router,
456
+ invitation_store=ctx.INVITATION_STORE,
457
+ workspace_service=ctx.WORKSPACE_SERVICE,
458
+ user_id_for_email=ctx.user_id_for_email,
459
+ create_security_router=_create_security_router,
460
+ classify_sensitive_message=ctx.classify_sensitive_message,
461
+ )
462
+ ctx.adopt(
463
+ foundation_router_bundle,
464
+ "auth_router",
465
+ "admin_router",
466
+ "invitations_router",
467
+ "security_router",
468
+ "_graph_stats_safe",
469
+ "_product_hardening_status",
470
+ "_security_audit_events_safe",
471
+ "_security_list_uploaded_files",
472
+ )
473
+
474
+
475
+ def phase_foundation_routes(ctx: RuntimeContext) -> None:
476
+ """Mount the foundation routers once the AppContext exists."""
477
+ ctx.enter("foundation_routes")
478
+
479
+ from latticeai.api.workspace import create_workspace_router
480
+ from latticeai.runtime.router_registration import register_foundation_routers
481
+
482
+ register_foundation_routers(
483
+ ctx.app,
484
+ static_router=ctx.STATIC_ROUTES.router,
485
+ auth_router=ctx.auth_router,
486
+ admin_router=ctx.admin_router,
487
+ invitations_router=ctx.invitations_router,
488
+ security_router=ctx.security_router,
489
+ create_workspace_router=create_workspace_router,
490
+ context=ctx.app_context,
491
+ )
492
+
@@ -25,6 +25,7 @@ def build_chat_agent_runtime_from_context(
25
25
  audit: Any,
26
26
  hooks: Any,
27
27
  brain_memory: Any,
28
+ self_model_summary: Any = None,
28
29
  ) -> Any:
29
30
  # Ensure dispatch + agent share the same autonomy dial before the runtime
30
31
  # is constructed (process-wide service; data_dir refined later at router mount).
@@ -39,6 +40,9 @@ def build_chat_agent_runtime_from_context(
39
40
  hooks=hooks,
40
41
  brain_memory=brain_memory,
41
42
  permission_mode=resolve_active_permission_mode,
43
+ # v11.2.0: a scoped resolver, not a snapshot — the profile a run sees is
44
+ # the one the Brain holds when the run starts, per user and workspace.
45
+ self_model_summary=self_model_summary,
42
46
  )
43
47
 
44
48
 
@@ -0,0 +1,163 @@
1
+ """Wire the feature switchboard into the running app (v11.2.0).
2
+
3
+ Same shape as ``permission_mode_wiring``: one process-wide service, an
4
+ idempotent router mount guarded on ``app.state``, and explicit arguments that
5
+ *rebind* an already-created service rather than being dropped — a lazy first
6
+ caller must not pin the store to a fallback data dir with no audit sink.
7
+
8
+ The one job unique to this module is :func:`bind_feature_gates`: it points each
9
+ opt-in gate's resolver at the service, which is the step that turns a persisted
10
+ preference into behaviour. Every gate is imported inside the function so
11
+ importing this module costs nothing; ``lattice_brain.synthesis`` in particular
12
+ drags in the graph layer.
13
+
14
+ Unbinding is a supported operation (:func:`unbind_feature_gates`) because these
15
+ gates are module-level singletons: a test that bound them must be able to give
16
+ them back, and "the environment answers again" has to be reachable.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import threading
23
+ from pathlib import Path
24
+ from typing import Any, Callable, List, Optional, Tuple
25
+
26
+ from latticeai.services.feature_toggles import FeatureToggleService
27
+
28
+ _LOCK = threading.Lock()
29
+ _SHARED: Optional[FeatureToggleService] = None
30
+
31
+ #: ``feature id -> (module, attribute)`` for every boolean gate. Resolved lazily
32
+ #: by :func:`_boolean_gates` so the table stays readable next to the catalog it
33
+ #: mirrors, and a rename that breaks the pairing fails loudly at wiring time.
34
+ GATE_BINDINGS: Tuple[Tuple[str, str, str], ...] = (
35
+ ("allow_multimodal", "lattice_brain.ingestion", "MULTIMODAL_GATE"),
36
+ ("video_ingest", "lattice_brain.ingestion", "VIDEO_GATE"),
37
+ ("auto_vector_index", "lattice_brain.ingestion", "AUTO_VECTOR_INDEX_GATE"),
38
+ ("brain_network", "lattice_brain.portability", "BRAIN_NETWORK_GATE"),
39
+ ("synthesis", "lattice_brain.synthesis", "SYNTHESIS_GATE"),
40
+ ("fusion_rrf", "lattice_brain.graph.fusion", "FUSION_RRF_GATE"),
41
+ ("graph_expansion", "lattice_brain.graph.fusion", "GRAPH_EXPANSION_GATE"),
42
+ ("vault_watch", "latticeai.services.folder_watch", "VAULT_WATCH_GATE"),
43
+ ("auto_late_fusion", "latticeai.services.search_service", "IMAGE_QUERY_FUSION_GATE"),
44
+ )
45
+
46
+ #: The one non-boolean seam: the vector backend is a pick-one-of-three.
47
+ CHOICE_FEATURE = "vector_backend"
48
+
49
+
50
+ def _default_data_dir() -> Path:
51
+ raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
52
+ if raw:
53
+ return Path(raw)
54
+ return Path.home() / ".ltcai"
55
+
56
+
57
+ def _boolean_gates() -> List[Tuple[str, Any]]:
58
+ """``[(feature id, gate)]`` — imports are here, not at module import."""
59
+ from importlib import import_module
60
+
61
+ return [
62
+ (feature_id, getattr(import_module(module), attribute))
63
+ for feature_id, module, attribute in GATE_BINDINGS
64
+ ]
65
+
66
+
67
+ def _bind_vector_index(resolver: Any) -> None:
68
+ from lattice_brain.graph.vector_index.selector import bind_vector_index_resolver
69
+
70
+ bind_vector_index_resolver(resolver)
71
+
72
+
73
+ def get_feature_toggle_service(
74
+ *,
75
+ data_dir: Optional[Path] = None,
76
+ audit: Optional[Callable[..., None]] = None,
77
+ ) -> FeatureToggleService:
78
+ """Process-wide singleton; explicit arguments rebind an existing one."""
79
+ global _SHARED
80
+ with _LOCK:
81
+ if _SHARED is None:
82
+ _SHARED = FeatureToggleService(
83
+ data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
84
+ audit=audit,
85
+ )
86
+ return _SHARED
87
+ if data_dir is not None:
88
+ _SHARED.rebind_data_dir(Path(data_dir))
89
+ if audit is not None:
90
+ _SHARED.rebind_audit(audit)
91
+ return _SHARED
92
+
93
+
94
+ def bind_feature_gates(service: Optional[FeatureToggleService] = None) -> None:
95
+ """Point every opt-in gate at the switchboard.
96
+
97
+ After this, a persisted preference beats the environment variable for every
98
+ feature in the catalog — which is the whole point, since the panel is the
99
+ only control surface most people will ever use.
100
+ """
101
+ resolved = service or get_feature_toggle_service()
102
+ for feature_id, gate in _boolean_gates():
103
+ # ``gate.local`` is the fallback on purpose: the switchboard speaks only
104
+ # for switches this person actually moved. Everything else still answers
105
+ # from the gate's own override → env → default, so binding changes
106
+ # nothing at all for an install that never opened the panel.
107
+ gate.bind(resolved.resolver(feature_id, gate.local))
108
+ _bind_vector_index(resolved.choice_resolver(CHOICE_FEATURE))
109
+
110
+
111
+ def unbind_feature_gates() -> None:
112
+ """Hand every gate back to its environment variable."""
113
+ for _feature_id, gate in _boolean_gates():
114
+ gate.bind(None)
115
+ _bind_vector_index(None)
116
+
117
+
118
+ def reset_feature_toggle_service() -> None:
119
+ """Drop the singleton *and* the bindings that pointed at it (tests)."""
120
+ global _SHARED
121
+ unbind_feature_gates()
122
+ with _LOCK:
123
+ _SHARED = None
124
+
125
+
126
+ def register_features_router(
127
+ app: Any,
128
+ *,
129
+ require_user: Callable[..., str],
130
+ data_dir: Optional[Path] = None,
131
+ append_audit_event: Optional[Callable[..., None]] = None,
132
+ ) -> FeatureToggleService:
133
+ """Install GET/POST /api/features on ``app`` and bind the gates. Idempotent.
134
+
135
+ The guard is a flag on ``app.state`` rather than a scan of route paths: a
136
+ router included through fastapi >= 0.140 has no flat ``path`` to introspect,
137
+ so path scanning silently stopped seeing the mount (see
138
+ ``permission_mode_wiring`` for the same note).
139
+ """
140
+ from latticeai.api.features import create_features_router
141
+
142
+ service = get_feature_toggle_service(data_dir=data_dir, audit=append_audit_event)
143
+ bind_feature_gates(service)
144
+ state = getattr(app, "state", None)
145
+ if state is not None and getattr(state, "_ltcai_features_mounted", False):
146
+ return service
147
+ app.include_router(
148
+ create_features_router(service=service, require_user=require_user)
149
+ )
150
+ if state is not None:
151
+ state._ltcai_features_mounted = True
152
+ return service
153
+
154
+
155
+ __all__ = [
156
+ "CHOICE_FEATURE",
157
+ "GATE_BINDINGS",
158
+ "bind_feature_gates",
159
+ "get_feature_toggle_service",
160
+ "register_features_router",
161
+ "reset_feature_toggle_service",
162
+ "unbind_feature_gates",
163
+ ]
@@ -675,4 +675,15 @@ def register_review_and_brain_tail_routers(
675
675
  append_audit_event=append_audit_event,
676
676
  knowledge_graph=knowledge_graph,
677
677
  )
678
+ # The opt-in switchboard. Mounted last on purpose: binding the gates to the
679
+ # service is what makes a stored preference beat an env var, and every
680
+ # module that owns one of those gates has been imported by now.
681
+ from latticeai.runtime.feature_toggle_wiring import register_features_router
682
+
683
+ register_features_router(
684
+ app,
685
+ require_user=require_user,
686
+ data_dir=data_dir,
687
+ append_audit_event=append_audit_event,
688
+ )
678
689
  return brain_network
@@ -204,6 +204,7 @@ class RuntimeContext:
204
204
  AGENT_RUNTIME: Any
205
205
  RUN_EXECUTOR: Any
206
206
  COMMAND_CENTER: Any
207
+ CHRONICLE: Any
207
208
  CHANGE_PROPOSALS: Any
208
209
  EVIDENCE_ACTIONS_SERVICE: Any
209
210
  VOICE_CAPTURE: Any