ltcai 11.2.0 → 11.5.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 (253) hide show
  1. package/README.md +50 -53
  2. package/docs/CHANGELOG.md +87 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +181 -0
  14. package/docs/v11.5.0_RUST_COMPLETE_PLAN.md +145 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  17. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  18. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  19. package/lattice_brain/graph/_kg_common/text.py +479 -0
  20. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  21. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  22. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  23. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  24. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  25. package/lattice_brain/graph/projection/__init__.py +42 -0
  26. package/lattice_brain/graph/projection/curation.py +500 -0
  27. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  28. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  29. package/lattice_brain/graph/retrieval/context.py +197 -0
  30. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  31. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  32. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  33. package/lattice_brain/graph/retrieval/signals.py +95 -0
  34. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  35. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  36. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  37. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  38. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  39. package/lattice_brain/ingestion/__init__.py +130 -0
  40. package/lattice_brain/ingestion/_contract.py +90 -0
  41. package/lattice_brain/ingestion/constants.py +127 -0
  42. package/lattice_brain/ingestion/folder_scan.py +57 -0
  43. package/lattice_brain/ingestion/folders.py +258 -0
  44. package/lattice_brain/ingestion/hashing.py +26 -0
  45. package/lattice_brain/ingestion/jobs_api.py +107 -0
  46. package/lattice_brain/ingestion/models.py +80 -0
  47. package/lattice_brain/ingestion/pipeline.py +486 -0
  48. package/lattice_brain/ingestion/quality.py +209 -0
  49. package/lattice_brain/ingestion/routing.py +295 -0
  50. package/lattice_brain/multimodal/__init__.py +164 -0
  51. package/lattice_brain/multimodal/audio.py +77 -0
  52. package/lattice_brain/multimodal/common.py +118 -0
  53. package/lattice_brain/multimodal/images.py +498 -0
  54. package/lattice_brain/multimodal/ports.py +169 -0
  55. package/lattice_brain/multimodal/video.py +410 -0
  56. package/lattice_brain/portability/__init__.py +90 -0
  57. package/lattice_brain/portability/_contract.py +42 -0
  58. package/lattice_brain/portability/backups.py +338 -0
  59. package/lattice_brain/portability/bundles.py +136 -0
  60. package/lattice_brain/portability/constants.py +93 -0
  61. package/lattice_brain/portability/fsops.py +138 -0
  62. package/lattice_brain/portability/service.py +41 -0
  63. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  64. package/lattice_brain/runtime/__init__.py +1 -1
  65. package/lattice_brain/runtime/multi_agent.py +1 -1
  66. package/latticeai/__init__.py +1 -1
  67. package/latticeai/api/chronicle.py +63 -0
  68. package/latticeai/api/index_jobs.py +145 -0
  69. package/latticeai/core/agent/__init__.py +93 -0
  70. package/latticeai/core/agent/_contract.py +79 -0
  71. package/latticeai/core/agent/context.py +57 -0
  72. package/latticeai/core/agent/deps.py +125 -0
  73. package/latticeai/core/agent/execution.py +622 -0
  74. package/latticeai/core/agent/planning.py +145 -0
  75. package/latticeai/core/agent/recovery.py +157 -0
  76. package/latticeai/core/agent/runtime.py +210 -0
  77. package/latticeai/core/agent/verification.py +231 -0
  78. package/latticeai/core/embedding_providers/__init__.py +151 -0
  79. package/latticeai/core/embedding_providers/base.py +199 -0
  80. package/latticeai/core/embedding_providers/captions.py +162 -0
  81. package/latticeai/core/embedding_providers/profiles.py +126 -0
  82. package/latticeai/core/embedding_providers/text.py +350 -0
  83. package/latticeai/core/embedding_providers/vision.py +352 -0
  84. package/latticeai/core/file_generation/__init__.py +115 -0
  85. package/latticeai/core/file_generation/bundles.py +76 -0
  86. package/latticeai/core/file_generation/extraction.py +154 -0
  87. package/latticeai/core/file_generation/inference.py +235 -0
  88. package/latticeai/core/file_generation/orchestration.py +152 -0
  89. package/latticeai/core/file_generation/prompting.py +117 -0
  90. package/latticeai/core/file_generation/repair.py +114 -0
  91. package/latticeai/core/file_generation/sanitize.py +61 -0
  92. package/latticeai/core/file_generation/validation.py +201 -0
  93. package/latticeai/core/legacy_compatibility.py +1 -1
  94. package/latticeai/core/marketplace.py +1 -1
  95. package/latticeai/core/messages.py +14 -0
  96. package/latticeai/core/workspace_os_constants.py +1 -1
  97. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  98. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  99. package/latticeai/integrations/telegram_bot/config.py +86 -0
  100. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  101. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  102. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  103. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  104. package/latticeai/models/router/__init__.py +88 -0
  105. package/latticeai/models/router/_contract.py +66 -0
  106. package/latticeai/models/router/branding.py +56 -0
  107. package/latticeai/models/router/catalog.py +69 -0
  108. package/latticeai/models/router/documents.py +199 -0
  109. package/latticeai/models/router/errors.py +37 -0
  110. package/latticeai/models/router/generation.py +258 -0
  111. package/latticeai/models/router/loading.py +291 -0
  112. package/latticeai/models/router/local_models.py +85 -0
  113. package/latticeai/models/router/registry.py +147 -0
  114. package/latticeai/runtime/build_phases/__init__.py +82 -0
  115. package/latticeai/runtime/build_phases/features.py +421 -0
  116. package/latticeai/runtime/build_phases/foundation.py +555 -0
  117. package/latticeai/runtime/build_phases/web.py +492 -0
  118. package/latticeai/runtime/runtime_context.py +1 -0
  119. package/latticeai/services/architecture_readiness.py +48 -19
  120. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  121. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  122. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  123. package/latticeai/services/brain_intelligence/constants.py +47 -0
  124. package/latticeai/services/brain_intelligence/digest.py +258 -0
  125. package/latticeai/services/brain_intelligence/health.py +331 -0
  126. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  127. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  128. package/latticeai/services/brain_intelligence/service.py +48 -0
  129. package/latticeai/services/chronicle.py +557 -0
  130. package/latticeai/services/memory_service/__init__.py +52 -0
  131. package/latticeai/services/memory_service/_contract.py +100 -0
  132. package/latticeai/services/memory_service/brief.py +431 -0
  133. package/latticeai/services/memory_service/constants.py +57 -0
  134. package/latticeai/services/memory_service/maintenance.py +138 -0
  135. package/latticeai/services/memory_service/manager.py +186 -0
  136. package/latticeai/services/memory_service/proof.py +136 -0
  137. package/latticeai/services/memory_service/recall.py +225 -0
  138. package/latticeai/services/memory_service/service.py +48 -0
  139. package/latticeai/services/memory_service/stores.py +110 -0
  140. package/latticeai/services/model_runtime/__init__.py +322 -0
  141. package/latticeai/services/model_runtime/cloud.py +87 -0
  142. package/latticeai/services/model_runtime/download.py +282 -0
  143. package/latticeai/services/model_runtime/engines.py +341 -0
  144. package/latticeai/services/model_runtime/loading.py +178 -0
  145. package/latticeai/services/model_runtime/service.py +129 -0
  146. package/latticeai/services/model_runtime/state.py +131 -0
  147. package/latticeai/services/model_runtime/status.py +255 -0
  148. package/latticeai/services/product_readiness.py +15 -7
  149. package/latticeai/setup/wizard/__init__.py +126 -0
  150. package/latticeai/setup/wizard/catalog.py +172 -0
  151. package/latticeai/setup/wizard/detect.py +323 -0
  152. package/latticeai/setup/wizard/install.py +348 -0
  153. package/latticeai/setup/wizard/paths.py +168 -0
  154. package/latticeai/setup/wizard/plans.py +74 -0
  155. package/latticeai/setup/wizard/recommend.py +320 -0
  156. package/package.json +6 -2
  157. package/scripts/bump_version.py +14 -0
  158. package/scripts/capture_release_evidence.mjs +33 -21
  159. package/scripts/check_current_release_docs.mjs +1 -1
  160. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  161. package/scripts/check_max_file_lines.mjs +102 -0
  162. package/scripts/check_release_evidence_bound.mjs +30 -15
  163. package/scripts/check_screenshot_pixel_delta.py +34 -4
  164. package/scripts/check_server_i18n.mjs +2 -0
  165. package/scripts/chunking_parity_corpus.py +449 -0
  166. package/scripts/generate_agent_parity_fixtures.py +752 -0
  167. package/scripts/generate_chunking_parity_fixtures.py +259 -0
  168. package/scripts/generate_rust_parity_fixtures.py +997 -0
  169. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  170. package/scripts/release_screen_claims.json +42 -2
  171. package/src-tauri/Cargo.lock +404 -3
  172. package/src-tauri/Cargo.toml +13 -1
  173. package/src-tauri/src/backend.rs +460 -0
  174. package/src-tauri/src/folder.rs +33 -0
  175. package/src-tauri/src/main.rs +109 -399
  176. package/src-tauri/src/topology.rs +356 -0
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +41 -37
  179. package/static/app/assets/Act-CWnxSCgN.js +1 -0
  180. package/static/app/assets/AdminConsole-BEQYU6kF.js +1 -0
  181. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-DWu1BhFg.js} +2 -2
  182. package/static/app/assets/BrainHome-95Hilr9R.js +2 -0
  183. package/static/app/assets/BrainSignals-QdeqCpAF.js +1 -0
  184. package/static/app/assets/Capture-BHpCxnzb.js +1 -0
  185. package/static/app/assets/Chronicle-B4xYKoed.js +1 -0
  186. package/static/app/assets/CommandPalette-BVXnttSz.js +1 -0
  187. package/static/app/assets/Library-DgYcHome.js +1 -0
  188. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-CrJLDbf7.js} +1 -1
  189. package/static/app/assets/ProductFlow-DFlScKoJ.js +1 -0
  190. package/static/app/assets/ReviewCard-Cy5f48Pj.js +3 -0
  191. package/static/app/assets/System-NF8IfhTa.js +1 -0
  192. package/static/app/assets/arrow-left-DwkSYrjR.js +1 -0
  193. package/static/app/assets/{bot-Cia42c2h.js → bot-CucuhLhm.js} +1 -1
  194. package/static/app/assets/brain-BBnSryW_.js +1 -0
  195. package/static/app/assets/{button-2j2Ijzgq.js → button-C2GUj2Ai.js} +1 -1
  196. package/static/app/assets/circle-check-CxOVPwYq.js +1 -0
  197. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-CbkWzBmG.js} +1 -1
  198. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-7lEaqHdJ.js} +1 -1
  199. package/static/app/assets/{cpu-k4awryFq.js → cpu-DAlCXlIy.js} +1 -1
  200. package/static/app/assets/{download-DFbLJ_ig.js → download-RNhuuJwh.js} +1 -1
  201. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CLW4odzM.js} +1 -1
  202. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-NKEiDIAJ.js} +1 -1
  203. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  204. package/static/app/assets/index-DMurvUuR.js +10 -0
  205. package/static/app/assets/input-D2UhPC1X.js +1 -0
  206. package/static/app/assets/link-2-6amKbP_P.js +1 -0
  207. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-Cu9TZtdR.js} +1 -1
  208. package/static/app/assets/primitives-gPsccucr.js +1 -0
  209. package/static/app/assets/search-Cj_TKk_2.js +1 -0
  210. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-Bau7KkPq.js} +1 -1
  211. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-BufNYypi.js} +1 -1
  212. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-BQnVWhYs.js} +1 -1
  213. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-B3_w60si.js} +1 -1
  214. package/static/app/assets/useMutation-BHhCflT6.js +1 -0
  215. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-rBWfI-5t.js} +1 -1
  216. package/static/app/assets/utils-V_5-wxr5.js +4 -0
  217. package/static/app/assets/workspace-K1zjYUHj.js +1 -0
  218. package/static/app/index.html +4 -4
  219. package/static/sw.js +1 -1
  220. package/lattice_brain/graph/_kg_common.py +0 -1331
  221. package/lattice_brain/graph/discovery_index.py +0 -1141
  222. package/lattice_brain/graph/retrieval.py +0 -1120
  223. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  224. package/lattice_brain/ingestion.py +0 -1525
  225. package/lattice_brain/multimodal.py +0 -1258
  226. package/latticeai/core/agent.py +0 -1465
  227. package/latticeai/core/embedding_providers.py +0 -1196
  228. package/latticeai/core/file_generation.py +0 -1047
  229. package/latticeai/integrations/telegram_bot.py +0 -1390
  230. package/latticeai/models/router.py +0 -1007
  231. package/latticeai/runtime/build_phases.py +0 -1450
  232. package/latticeai/services/brain_intelligence.py +0 -1083
  233. package/latticeai/services/memory_service.py +0 -1177
  234. package/latticeai/services/model_runtime.py +0 -1281
  235. package/latticeai/setup/wizard.py +0 -1310
  236. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  237. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  238. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  239. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  240. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  241. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  242. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  243. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  244. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  245. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  246. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  247. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  248. package/static/app/assets/index-BpYkzcVm.js +0 -10
  249. package/static/app/assets/input-DSlJJxRs.js +0 -1
  250. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  251. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  252. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  253. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,356 @@
1
+ //! Which front door the desktop opens, and on which ports.
2
+ //!
3
+ //! Through 11.4.0 the shell had one topology: spawn the Python worker, point
4
+ //! the webview at the worker's own port. 11.5.0 makes the Rust gateway the
5
+ //! **default front door** (plan §2a) — the shell serves `lattice-host`'s
6
+ //! gateway on the public port and supervises the worker on an internal one
7
+ //! behind it — because that is the only arrangement in which the native
8
+ //! `/rust/*` and `/host/*` surfaces exist for the app at all.
9
+ //!
10
+ //! Three escape hatches are preserved, and one is new. They are resolved here,
11
+ //! in one pure-ish function, so "what will this shell do" is a table rather
12
+ //! than a trail through `backend.rs`:
13
+ //!
14
+ //! | Environment | Topology | Webview origin | Spawns |
15
+ //! |---|---|---|---|
16
+ //! | *(none)* | `gateway` | `http://127.0.0.1:{gateway}` | worker (internal port) + in-process gateway |
17
+ //! | `LATTICEAI_DESKTOP_DIRECT=1` | `direct` | `http://127.0.0.1:{worker}` | worker only (11.4.0 behaviour) |
18
+ //! | `LATTICEAI_DESKTOP_BACKEND_ORIGIN=…` | `external` | that origin, verbatim | nothing |
19
+ //! | `LATTICEAI_DESKTOP_NO_BACKEND=1` | `disabled` | `http://127.0.0.1:{worker}` | nothing |
20
+ //!
21
+ //! An explicit origin outranks every other switch (something already exists and
22
+ //! was named), and the kill switch outranks the topology choice (there is
23
+ //! nothing to front). `LATTICEAI_PORT`, when set, is honoured verbatim as the
24
+ //! **port the person visits** — the gateway port in the default topology, the
25
+ //! worker port in the others — because the reason to set it is that something
26
+ //! else knows the number.
27
+
28
+ use lattice_host::supervisor::{find_free_port, HostProbe, DEFAULT_PORT, DEFAULT_SCAN_ATTEMPTS};
29
+
30
+ /// Point the shell at an already-running worker. Nothing is spawned, and this
31
+ /// exact string is what `backend_origin` returns and the webview navigates to.
32
+ pub const ORIGIN_ENV: &str = "LATTICEAI_DESKTOP_BACKEND_ORIGIN";
33
+ /// Kill switch: open the window with no worker at all.
34
+ pub const NO_BACKEND_ENV: &str = "LATTICEAI_DESKTOP_NO_BACKEND";
35
+ /// Opt back into the 11.4.0 topology: webview straight at the worker.
36
+ pub const DIRECT_ENV: &str = "LATTICEAI_DESKTOP_DIRECT";
37
+ /// An explicit port, honoured verbatim.
38
+ pub const PORT_ENV: &str = "LATTICEAI_PORT";
39
+
40
+ /// How this shell is arranged.
41
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42
+ pub enum Topology {
43
+ /// Gateway on the public port, worker behind it. The default.
44
+ Gateway,
45
+ /// Webview straight at the worker, no gateway (`LATTICEAI_DESKTOP_DIRECT`).
46
+ Direct,
47
+ /// A worker someone else started (`LATTICEAI_DESKTOP_BACKEND_ORIGIN`).
48
+ External,
49
+ /// No backend at all (`LATTICEAI_DESKTOP_NO_BACKEND`).
50
+ Disabled,
51
+ }
52
+
53
+ impl Topology {
54
+ /// The name this topology is reported under, in status and in logs.
55
+ pub fn as_str(self) -> &'static str {
56
+ match self {
57
+ Topology::Gateway => "gateway",
58
+ Topology::Direct => "direct",
59
+ Topology::External => "external",
60
+ Topology::Disabled => "disabled",
61
+ }
62
+ }
63
+
64
+ /// Whether this shell owns a worker process.
65
+ pub fn supervises(self) -> bool {
66
+ matches!(self, Topology::Gateway | Topology::Direct)
67
+ }
68
+ }
69
+
70
+ /// The resolved arrangement: what to serve, what to spawn, where to navigate.
71
+ #[derive(Debug, Clone, PartialEq, Eq)]
72
+ pub struct Plan {
73
+ /// Which arrangement was chosen.
74
+ pub topology: Topology,
75
+ /// Where the frontend and the webview go.
76
+ pub origin: String,
77
+ /// Where the worker itself answers — the proxy target, and the origin the
78
+ /// supervisor health-gates on.
79
+ pub worker_origin: String,
80
+ /// The worker's port.
81
+ pub worker_port: u16,
82
+ /// The gateway's port, when there is a gateway.
83
+ pub gateway_port: Option<u16>,
84
+ /// The kill switch was set: there is deliberately nothing to wait for.
85
+ pub disabled: bool,
86
+ }
87
+
88
+ impl Plan {
89
+ /// Whether this shell starts and owns the worker.
90
+ pub fn supervised(&self) -> bool {
91
+ self.topology.supervises()
92
+ }
93
+
94
+ /// `{origin}/app` — the URL the window navigates to once health answers.
95
+ pub fn app_url(&self) -> String {
96
+ format!("{}/app", self.origin.trim_end_matches('/'))
97
+ }
98
+ }
99
+
100
+ /// Parse a flag the way the product parses its booleans: set-and-truthy is on,
101
+ /// set-and-falsy is off, absent is off.
102
+ ///
103
+ /// Presence alone is deliberately *not* enough — `LATTICEAI_DESKTOP_DIRECT=0`
104
+ /// must mean "no", or the variable becomes impossible to turn off in a shell
105
+ /// profile that already exports it.
106
+ pub fn flag_is_on(raw: Option<&str>) -> bool {
107
+ matches!(
108
+ raw.map(|value| value.trim().to_ascii_lowercase())
109
+ .as_deref(),
110
+ Some("1" | "true" | "yes" | "on")
111
+ )
112
+ }
113
+
114
+ /// The port named by an origin, when it names one.
115
+ pub fn origin_port(origin: &str) -> Option<u16> {
116
+ let authority = origin
117
+ .split_once("://")
118
+ .map(|(_, rest)| rest)
119
+ .unwrap_or(origin)
120
+ .split('/')
121
+ .next()
122
+ .unwrap_or_default();
123
+ authority
124
+ .rsplit_once(':')
125
+ .and_then(|(_, port)| port.trim().parse::<u16>().ok())
126
+ .filter(|port| *port != 0)
127
+ }
128
+
129
+ /// The public port: an explicit `LATTICEAI_PORT` verbatim, else 4825 scanning
130
+ /// upward.
131
+ ///
132
+ /// An explicit port is an instruction, not a preference — if it is busy the
133
+ /// shell must fail loudly on it rather than quietly answer somewhere else,
134
+ /// because the whole point of setting it is that something else knows the
135
+ /// number.
136
+ pub fn resolve_port(probe: &dyn HostProbe) -> u16 {
137
+ let pinned = probe
138
+ .env_var(PORT_ENV)
139
+ .and_then(|raw| raw.trim().parse::<u16>().ok())
140
+ .filter(|port| *port != 0);
141
+ match pinned {
142
+ Some(port) => port,
143
+ None => find_free_port(DEFAULT_PORT, DEFAULT_SCAN_ATTEMPTS, &[]).unwrap_or(DEFAULT_PORT),
144
+ }
145
+ }
146
+
147
+ /// `http://127.0.0.1:{port}`.
148
+ fn loopback(port: u16) -> String {
149
+ format!("http://127.0.0.1:{port}")
150
+ }
151
+
152
+ /// Resolve the arrangement from the environment the probe reports.
153
+ pub fn resolve(probe: &dyn HostProbe) -> Plan {
154
+ let external = probe
155
+ .env_var(ORIGIN_ENV)
156
+ .map(|value| value.trim().to_string())
157
+ .filter(|value| !value.is_empty());
158
+ if let Some(origin) = external {
159
+ let worker_port = origin_port(&origin).unwrap_or_else(|| resolve_port(probe));
160
+ return Plan {
161
+ topology: Topology::External,
162
+ worker_origin: origin.clone(),
163
+ origin,
164
+ worker_port,
165
+ gateway_port: None,
166
+ disabled: false,
167
+ };
168
+ }
169
+
170
+ if probe.env_var(NO_BACKEND_ENV).is_some() {
171
+ let worker_port = resolve_port(probe);
172
+ return Plan {
173
+ topology: Topology::Disabled,
174
+ origin: loopback(worker_port),
175
+ worker_origin: loopback(worker_port),
176
+ worker_port,
177
+ gateway_port: None,
178
+ disabled: true,
179
+ };
180
+ }
181
+
182
+ if flag_is_on(probe.env_var(DIRECT_ENV).as_deref()) {
183
+ let worker_port = resolve_port(probe);
184
+ return Plan {
185
+ topology: Topology::Direct,
186
+ origin: loopback(worker_port),
187
+ worker_origin: loopback(worker_port),
188
+ worker_port,
189
+ gateway_port: None,
190
+ disabled: false,
191
+ };
192
+ }
193
+
194
+ // The default: the gateway takes the public port and the worker moves
195
+ // behind it, on the first free port above — never the gateway's own.
196
+ let gateway_port = resolve_port(probe);
197
+ let worker_port = find_free_port(
198
+ gateway_port.checked_add(1).unwrap_or(DEFAULT_PORT),
199
+ DEFAULT_SCAN_ATTEMPTS,
200
+ &[gateway_port],
201
+ )
202
+ .unwrap_or(gateway_port.wrapping_add(1));
203
+ Plan {
204
+ topology: Topology::Gateway,
205
+ origin: loopback(gateway_port),
206
+ worker_origin: loopback(worker_port),
207
+ worker_port,
208
+ gateway_port: Some(gateway_port),
209
+ disabled: false,
210
+ }
211
+ }
212
+
213
+ #[cfg(test)]
214
+ mod tests {
215
+ use super::*;
216
+ use lattice_host::supervisor::StaticProbe;
217
+
218
+ #[test]
219
+ fn an_origin_names_its_port_or_admits_it_does_not() {
220
+ assert_eq!(origin_port("http://127.0.0.1:8765"), Some(8765));
221
+ assert_eq!(origin_port("http://127.0.0.1:8765/app"), Some(8765));
222
+ assert_eq!(origin_port("https://[::1]:4825"), Some(4825));
223
+ assert_eq!(origin_port("127.0.0.1:4899"), Some(4899));
224
+ assert_eq!(origin_port("http://localhost"), None);
225
+ assert_eq!(origin_port("http://[::1]"), None);
226
+ assert_eq!(origin_port("http://127.0.0.1:0"), None);
227
+ assert_eq!(origin_port(""), None);
228
+ }
229
+
230
+ #[test]
231
+ fn an_explicit_port_is_honoured_verbatim() {
232
+ let probe = StaticProbe::new().with_env(PORT_ENV, " 8765 ");
233
+ assert_eq!(resolve_port(&probe), 8765);
234
+ }
235
+
236
+ #[test]
237
+ fn without_a_pinned_port_the_scan_starts_at_the_unified_default() {
238
+ let port = resolve_port(&StaticProbe::new());
239
+ assert!(
240
+ (DEFAULT_PORT..DEFAULT_PORT + DEFAULT_SCAN_ATTEMPTS).contains(&port),
241
+ "expected a port scanned upward from {DEFAULT_PORT}, got {port}"
242
+ );
243
+ }
244
+
245
+ #[test]
246
+ fn a_flag_is_on_only_when_it_says_so() {
247
+ for on in ["1", "true", "TRUE", "yes", "on", " on "] {
248
+ assert!(flag_is_on(Some(on)), "{on}");
249
+ }
250
+ for off in ["0", "false", "no", "off", "", "perhaps"] {
251
+ assert!(!flag_is_on(Some(off)), "{off}");
252
+ }
253
+ assert!(!flag_is_on(None));
254
+ }
255
+
256
+ #[test]
257
+ fn the_default_topology_puts_the_gateway_in_front() {
258
+ let plan = resolve(&StaticProbe::new().with_env(PORT_ENV, "41830"));
259
+ assert_eq!(plan.topology, Topology::Gateway);
260
+ assert_eq!(plan.gateway_port, Some(41830));
261
+ assert_eq!(plan.origin, "http://127.0.0.1:41830");
262
+ assert!(
263
+ plan.worker_port > 41830,
264
+ "the worker moves behind the front door, not onto it"
265
+ );
266
+ assert_eq!(
267
+ plan.worker_origin,
268
+ format!("http://127.0.0.1:{}", plan.worker_port)
269
+ );
270
+ assert!(plan.supervised());
271
+ assert!(!plan.disabled);
272
+ assert_eq!(plan.app_url(), "http://127.0.0.1:41830/app");
273
+ }
274
+
275
+ #[test]
276
+ fn direct_restores_the_previous_topology() {
277
+ let probe = StaticProbe::new()
278
+ .with_env(DIRECT_ENV, "1")
279
+ .with_env(PORT_ENV, "41831");
280
+ let plan = resolve(&probe);
281
+ assert_eq!(plan.topology, Topology::Direct);
282
+ assert_eq!(plan.gateway_port, None);
283
+ assert_eq!(plan.worker_port, 41831);
284
+ assert_eq!(plan.origin, "http://127.0.0.1:41831");
285
+ assert!(plan.supervised());
286
+ }
287
+
288
+ #[test]
289
+ fn a_falsy_direct_flag_leaves_the_default_alone() {
290
+ let probe = StaticProbe::new()
291
+ .with_env(DIRECT_ENV, "0")
292
+ .with_env(PORT_ENV, "41832");
293
+ assert_eq!(resolve(&probe).topology, Topology::Gateway);
294
+ }
295
+
296
+ #[test]
297
+ fn an_external_origin_spawns_nothing_and_is_used_verbatim() {
298
+ let probe = StaticProbe::new().with_env(ORIGIN_ENV, " http://127.0.0.1:9100/ ");
299
+ let plan = resolve(&probe);
300
+ assert_eq!(plan.topology, Topology::External);
301
+ assert_eq!(plan.origin, "http://127.0.0.1:9100/");
302
+ assert_eq!(plan.app_url(), "http://127.0.0.1:9100/app");
303
+ assert_eq!(plan.worker_port, 9100);
304
+ assert!(!plan.supervised());
305
+ assert!(!plan.disabled, "a worker was named, so the shell waits");
306
+ }
307
+
308
+ #[test]
309
+ fn an_external_origin_without_a_port_still_resolves_one() {
310
+ let probe = StaticProbe::new().with_env(ORIGIN_ENV, "http://localhost");
311
+ let plan = resolve(&probe);
312
+ assert_eq!(plan.origin, "http://localhost");
313
+ assert!(plan.worker_port >= DEFAULT_PORT);
314
+ }
315
+
316
+ #[test]
317
+ fn the_kill_switch_spawns_nothing_but_still_names_an_origin() {
318
+ let probe = StaticProbe::new().with_env(NO_BACKEND_ENV, "1");
319
+ let plan = resolve(&probe);
320
+ assert_eq!(plan.topology, Topology::Disabled);
321
+ assert!(plan.disabled);
322
+ assert!(!plan.supervised());
323
+ assert!(plan.origin.starts_with("http://127.0.0.1:"));
324
+ }
325
+
326
+ #[test]
327
+ fn an_explicit_origin_outranks_every_other_switch() {
328
+ let probe = StaticProbe::new()
329
+ .with_env(NO_BACKEND_ENV, "1")
330
+ .with_env(DIRECT_ENV, "1")
331
+ .with_env(ORIGIN_ENV, "http://127.0.0.1:9100");
332
+ let plan = resolve(&probe);
333
+ assert_eq!(plan.topology, Topology::External);
334
+ assert!(!plan.disabled, "a worker was named, so the shell waits");
335
+ }
336
+
337
+ #[test]
338
+ fn the_kill_switch_outranks_the_topology_choice() {
339
+ let probe = StaticProbe::new()
340
+ .with_env(NO_BACKEND_ENV, "1")
341
+ .with_env(DIRECT_ENV, "1");
342
+ assert_eq!(resolve(&probe).topology, Topology::Disabled);
343
+ }
344
+
345
+ #[test]
346
+ fn every_topology_reports_a_name() {
347
+ for (topology, name) in [
348
+ (Topology::Gateway, "gateway"),
349
+ (Topology::Direct, "direct"),
350
+ (Topology::External, "external"),
351
+ (Topology::Disabled, "disabled"),
352
+ ] {
353
+ assert_eq!(topology.as_str(), name);
354
+ }
355
+ }
356
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "11.2.0",
4
+ "version": "11.5.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,45 +1,49 @@
1
1
  {
2
- "version": "11.2.0",
2
+ "version": "11.5.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
- "app": "/static/app/assets/index-BpYkzcVm.js"
5
+ "app": "/static/app/assets/index-DMurvUuR.js"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_BrainSignals-jMYgQ2Ar.js": "/static/app/assets/BrainSignals-jMYgQ2Ar.js",
10
- "_LivingBrain-DBwhto14.js": "/static/app/assets/LivingBrain-DBwhto14.js",
11
- "_ReviewCard-BUhCKRNM.js": "/static/app/assets/ReviewCard-BUhCKRNM.js",
12
- "_arrow-left-Dzwa5zRb.js": "/static/app/assets/arrow-left-Dzwa5zRb.js",
13
- "_bot-Cia42c2h.js": "/static/app/assets/bot-Cia42c2h.js",
14
- "_brain-DJMoqrwx.js": "/static/app/assets/brain-DJMoqrwx.js",
15
- "_button-2j2Ijzgq.js": "/static/app/assets/button-2j2Ijzgq.js",
16
- "_circle-pause-BEFeWpVW.js": "/static/app/assets/circle-pause-BEFeWpVW.js",
17
- "_circle-play-ujXMcHxl.js": "/static/app/assets/circle-play-ujXMcHxl.js",
18
- "_cpu-k4awryFq.js": "/static/app/assets/cpu-k4awryFq.js",
19
- "_download-DFbLJ_ig.js": "/static/app/assets/download-DFbLJ_ig.js",
20
- "_folder-open-7y_b6xkM.js": "/static/app/assets/folder-open-7y_b6xkM.js",
21
- "_hard-drive-Bidh02Kr.js": "/static/app/assets/hard-drive-Bidh02Kr.js",
22
- "_input-DSlJJxRs.js": "/static/app/assets/input-DSlJJxRs.js",
23
- "_permissionCopy-Bpb83Hx9.js": "/static/app/assets/permissionCopy-Bpb83Hx9.js",
24
- "_primitives-BCx6TvfG.js": "/static/app/assets/primitives-BCx6TvfG.js",
25
- "_search-Cgy8cCFJ.js": "/static/app/assets/search-Cgy8cCFJ.js",
26
- "_share-2-BH1M-WNi.js": "/static/app/assets/share-2-BH1M-WNi.js",
27
- "_shield-alert-BlKdBXcG.js": "/static/app/assets/shield-alert-BlKdBXcG.js",
28
- "_textarea-CCWbUfFB.js": "/static/app/assets/textarea-CCWbUfFB.js",
29
- "_useFocusTrap-YdHQ7pJ1.js": "/static/app/assets/useFocusTrap-YdHQ7pJ1.js",
30
- "_useQuery-CXQiwbVT.js": "/static/app/assets/useQuery-CXQiwbVT.js",
31
- "_utils-zqPZJxdx.js": "/static/app/assets/utils-zqPZJxdx.js",
32
- "_workspace-DXTihhfU.js": "/static/app/assets/workspace-DXTihhfU.js",
33
- "index.html": "/static/app/assets/index-BpYkzcVm.js",
34
- "assets/index-DwDl9-8Y.css": "/static/app/assets/index-DwDl9-8Y.css",
35
- "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-BHA2cfKI.js",
36
- "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-D0u8Tiyj.js",
37
- "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-Ts7G_Ila.js",
38
- "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-DC0Bzh-I.js",
39
- "src/pages/Act.tsx": "/static/app/assets/Act-AWf0SAKp.js",
40
- "src/pages/Brain.tsx": "/static/app/assets/Brain-tuhI4sOC.js",
41
- "src/pages/Capture.tsx": "/static/app/assets/Capture-CqOSzyPr.js",
42
- "src/pages/Library.tsx": "/static/app/assets/Library-CX-bbhmK.js",
43
- "src/pages/System.tsx": "/static/app/assets/System-Bu2t5hn1.js"
9
+ "_BrainSignals-QdeqCpAF.js": "/static/app/assets/BrainSignals-QdeqCpAF.js",
10
+ "_LivingBrain-CrJLDbf7.js": "/static/app/assets/LivingBrain-CrJLDbf7.js",
11
+ "_ReviewCard-Cy5f48Pj.js": "/static/app/assets/ReviewCard-Cy5f48Pj.js",
12
+ "_arrow-left-DwkSYrjR.js": "/static/app/assets/arrow-left-DwkSYrjR.js",
13
+ "_bot-CucuhLhm.js": "/static/app/assets/bot-CucuhLhm.js",
14
+ "_brain-BBnSryW_.js": "/static/app/assets/brain-BBnSryW_.js",
15
+ "_button-C2GUj2Ai.js": "/static/app/assets/button-C2GUj2Ai.js",
16
+ "_circle-check-CxOVPwYq.js": "/static/app/assets/circle-check-CxOVPwYq.js",
17
+ "_circle-pause-CbkWzBmG.js": "/static/app/assets/circle-pause-CbkWzBmG.js",
18
+ "_circle-play-7lEaqHdJ.js": "/static/app/assets/circle-play-7lEaqHdJ.js",
19
+ "_cpu-DAlCXlIy.js": "/static/app/assets/cpu-DAlCXlIy.js",
20
+ "_download-RNhuuJwh.js": "/static/app/assets/download-RNhuuJwh.js",
21
+ "_folder-open-CLW4odzM.js": "/static/app/assets/folder-open-CLW4odzM.js",
22
+ "_hard-drive-NKEiDIAJ.js": "/static/app/assets/hard-drive-NKEiDIAJ.js",
23
+ "_input-D2UhPC1X.js": "/static/app/assets/input-D2UhPC1X.js",
24
+ "_link-2-6amKbP_P.js": "/static/app/assets/link-2-6amKbP_P.js",
25
+ "_permissionCopy-Cu9TZtdR.js": "/static/app/assets/permissionCopy-Cu9TZtdR.js",
26
+ "_primitives-gPsccucr.js": "/static/app/assets/primitives-gPsccucr.js",
27
+ "_search-Cj_TKk_2.js": "/static/app/assets/search-Cj_TKk_2.js",
28
+ "_share-2-Bau7KkPq.js": "/static/app/assets/share-2-Bau7KkPq.js",
29
+ "_shield-alert-BufNYypi.js": "/static/app/assets/shield-alert-BufNYypi.js",
30
+ "_textarea-BQnVWhYs.js": "/static/app/assets/textarea-BQnVWhYs.js",
31
+ "_useFocusTrap-B3_w60si.js": "/static/app/assets/useFocusTrap-B3_w60si.js",
32
+ "_useMutation-BHhCflT6.js": "/static/app/assets/useMutation-BHhCflT6.js",
33
+ "_useQuery-rBWfI-5t.js": "/static/app/assets/useQuery-rBWfI-5t.js",
34
+ "_utils-V_5-wxr5.js": "/static/app/assets/utils-V_5-wxr5.js",
35
+ "_workspace-K1zjYUHj.js": "/static/app/assets/workspace-K1zjYUHj.js",
36
+ "index.html": "/static/app/assets/index-DMurvUuR.js",
37
+ "assets/index-BLPb5lmE.css": "/static/app/assets/index-BLPb5lmE.css",
38
+ "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-DFlScKoJ.js",
39
+ "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-BEQYU6kF.js",
40
+ "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-95Hilr9R.js",
41
+ "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-BVXnttSz.js",
42
+ "src/pages/Act.tsx": "/static/app/assets/Act-CWnxSCgN.js",
43
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-DWu1BhFg.js",
44
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-BHpCxnzb.js",
45
+ "src/pages/Chronicle.tsx": "/static/app/assets/Chronicle-B4xYKoed.js",
46
+ "src/pages/Library.tsx": "/static/app/assets/Library-DgYcHome.js",
47
+ "src/pages/System.tsx": "/static/app/assets/System-NF8IfhTa.js"
44
48
  }
45
49
  }