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
@@ -12,12 +12,9 @@ ack is audited. Hard node filters remain in ``latticeai.core.network_boundary``.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import json
16
- import threading
17
15
  from pathlib import Path
18
16
  from typing import Any, Callable, Dict, Optional
19
17
 
20
- from latticeai.core.io_utils import atomic_write_json
21
18
  from latticeai.core.network_boundary import (
22
19
  DEFAULT_NETWORK_MODE,
23
20
  NetworkBoundaryMode,
@@ -25,11 +22,14 @@ from latticeai.core.network_boundary import (
25
22
  network_mode_contract,
26
23
  normalize_network_mode,
27
24
  )
25
+ from latticeai.services.mode_store import JsonBackedModeService
28
26
 
29
27
 
30
- class NetworkBoundaryService:
28
+ class NetworkBoundaryService(JsonBackedModeService[NetworkBoundaryMode]):
31
29
  """Load / save the network boundary dial."""
32
30
 
31
+ FILENAME = "network_boundary.json"
32
+
33
33
  def __init__(
34
34
  self,
35
35
  *,
@@ -37,36 +37,11 @@ class NetworkBoundaryService:
37
37
  default_mode: NetworkBoundaryMode | str = DEFAULT_NETWORK_MODE,
38
38
  audit: Optional[Callable[..., None]] = None,
39
39
  ) -> None:
40
- self._path = Path(data_dir) / "network_boundary.json"
40
+ super().__init__(data_dir=data_dir, audit=audit)
41
41
  self._default = normalize_network_mode(default_mode)
42
- self._audit = audit or (lambda *a, **kw: None)
43
- self._lock = threading.Lock()
44
-
45
- def rebind_data_dir(self, data_dir: Path) -> None:
46
- with self._lock:
47
- self._path = Path(data_dir) / "network_boundary.json"
48
42
 
49
- def rebind_audit(self, audit: Callable[..., None]) -> None:
50
- with self._lock:
51
- self._audit = audit
52
-
53
- def _read(self) -> Dict[str, Any]:
54
- if not self._path.exists():
55
- return {"default": self._default.value, "users": {}, "workspaces": {}}
56
- try:
57
- data = json.loads(self._path.read_text(encoding="utf-8"))
58
- except Exception:
59
- return {"default": self._default.value, "users": {}, "workspaces": {}}
60
- if not isinstance(data, dict):
61
- return {"default": self._default.value, "users": {}, "workspaces": {}}
62
- data.setdefault("default", self._default.value)
63
- data.setdefault("users", {})
64
- data.setdefault("workspaces", {})
65
- return data
66
-
67
- def _write(self, data: Dict[str, Any]) -> None:
68
- self._path.parent.mkdir(parents=True, exist_ok=True)
69
- atomic_write_json(self._path, data)
43
+ def _default_entry(self) -> Any:
44
+ return self._default.value
70
45
 
71
46
  def _resolve_from(
72
47
  self,
@@ -85,18 +60,6 @@ class NetworkBoundaryService:
85
60
  return normalize_network_mode(user)
86
61
  return normalize_network_mode(data.get("default") or self._default)
87
62
 
88
- def resolve(
89
- self,
90
- *,
91
- user_email: Optional[str] = None,
92
- workspace_id: Optional[str] = None,
93
- ) -> NetworkBoundaryMode:
94
- with self._lock:
95
- data = self._read()
96
- return self._resolve_from(
97
- data, user_email=user_email, workspace_id=workspace_id,
98
- )
99
-
100
63
  def get(
101
64
  self,
102
65
  *,
@@ -14,12 +14,9 @@ not by this store.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import json
18
- import threading
19
17
  from pathlib import Path
20
18
  from typing import Any, Callable, Dict, Optional
21
19
 
22
- from latticeai.core.io_utils import atomic_write_json
23
20
  from latticeai.core.permission_mode import (
24
21
  DEFAULT_MODE,
25
22
  PermissionMode,
@@ -27,11 +24,14 @@ from latticeai.core.permission_mode import (
27
24
  mode_contract,
28
25
  normalize_mode,
29
26
  )
27
+ from latticeai.services.mode_store import JsonBackedModeService
30
28
 
31
29
 
32
- class PermissionModeService:
30
+ class PermissionModeService(JsonBackedModeService[PermissionMode]):
33
31
  """Load / save the autonomy dial."""
34
32
 
33
+ FILENAME = "permission_mode.json"
34
+
35
35
  def __init__(
36
36
  self,
37
37
  *,
@@ -39,43 +39,11 @@ class PermissionModeService:
39
39
  default_mode: PermissionMode | str = DEFAULT_MODE,
40
40
  audit: Optional[Callable[..., None]] = None,
41
41
  ) -> None:
42
- self._path = Path(data_dir) / "permission_mode.json"
42
+ super().__init__(data_dir=data_dir, audit=audit)
43
43
  self._default = normalize_mode(default_mode)
44
- self._audit = audit or (lambda *a, **kw: None)
45
- self._lock = threading.Lock()
46
-
47
- def rebind_data_dir(self, data_dir: Path) -> None:
48
- """Point the store at the app's real data dir.
49
-
50
- The wiring may instantiate this service lazily before routers know the
51
- configured data dir; rebinding keeps one file of record instead of
52
- stranding writes under the fallback path.
53
- """
54
- with self._lock:
55
- self._path = Path(data_dir) / "permission_mode.json"
56
44
 
57
- def rebind_audit(self, audit: Callable[..., None]) -> None:
58
- """Attach the real audit sink once app wiring provides one."""
59
- with self._lock:
60
- self._audit = audit
61
-
62
- def _read(self) -> Dict[str, Any]:
63
- if not self._path.exists():
64
- return {"default": self._default.value, "users": {}, "workspaces": {}}
65
- try:
66
- data = json.loads(self._path.read_text(encoding="utf-8"))
67
- except Exception:
68
- return {"default": self._default.value, "users": {}, "workspaces": {}}
69
- if not isinstance(data, dict):
70
- return {"default": self._default.value, "users": {}, "workspaces": {}}
71
- data.setdefault("default", self._default.value)
72
- data.setdefault("users", {})
73
- data.setdefault("workspaces", {})
74
- return data
75
-
76
- def _write(self, data: Dict[str, Any]) -> None:
77
- self._path.parent.mkdir(parents=True, exist_ok=True)
78
- atomic_write_json(self._path, data)
45
+ def _default_entry(self) -> Any:
46
+ return self._default.value
79
47
 
80
48
  def _resolve_from(
81
49
  self,
@@ -84,11 +52,7 @@ class PermissionModeService:
84
52
  user_email: Optional[str],
85
53
  workspace_id: Optional[str],
86
54
  ) -> PermissionMode:
87
- """Scope precedence: workspace → user → process default.
88
-
89
- Pure over ``data`` and lock-free, so holders of ``_lock`` can reuse it
90
- without re-entering a non-reentrant lock.
91
- """
55
+ """Scope precedence: workspace → user → process default."""
92
56
  if workspace_id:
93
57
  ws = (data.get("workspaces") or {}).get(str(workspace_id))
94
58
  if ws:
@@ -99,18 +63,6 @@ class PermissionModeService:
99
63
  return normalize_mode(user)
100
64
  return normalize_mode(data.get("default") or self._default)
101
65
 
102
- def resolve(
103
- self,
104
- *,
105
- user_email: Optional[str] = None,
106
- workspace_id: Optional[str] = None,
107
- ) -> PermissionMode:
108
- with self._lock:
109
- data = self._read()
110
- return self._resolve_from(
111
- data, user_email=user_email, workspace_id=workspace_id,
112
- )
113
-
114
66
  def get(
115
67
  self,
116
68
  *,
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
18
18
 
19
19
  from latticeai.services.architecture_readiness import architecture_readiness
20
20
 
21
- PRODUCT_VERSION_TARGET = "11.5.0"
21
+ PRODUCT_VERSION_TARGET = "11.5.2"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
@@ -50,6 +50,64 @@ def parse_windows_video_controllers(raw: str) -> List[Dict[str, Any]]:
50
50
  return controllers
51
51
 
52
52
 
53
+
54
+ def parse_windows_cpu_info(
55
+ raw: str,
56
+ *,
57
+ model: str,
58
+ physical_cores: int,
59
+ logical_cores: int,
60
+ ) -> Tuple[str, int, int]:
61
+ """Read ``wmic cpu get Name,NumberOfCores,NumberOfLogicalProcessors``.
62
+
63
+ Whatever the output does not say keeps the value the caller already had —
64
+ ``wmic`` is absent on modern Windows images and prints nothing there, and
65
+ "0 cores" would be a worse answer than ``os.cpu_count()``'s guess.
66
+ """
67
+ for line in raw.splitlines():
68
+ key, _, value = line.partition("=")
69
+ if key == "Name" and value.strip():
70
+ model = value.strip()
71
+ elif key == "NumberOfCores" and value.strip():
72
+ try:
73
+ physical_cores = int(value.strip())
74
+ except ValueError:
75
+ quiet()
76
+ elif key == "NumberOfLogicalProcessors" and value.strip():
77
+ try:
78
+ logical_cores = int(value.strip())
79
+ except ValueError:
80
+ quiet()
81
+ return model, physical_cores, logical_cores
82
+
83
+
84
+ #: ``IsProcessorFeaturePresent`` codes worth reporting, by instruction name.
85
+ WINDOWS_PROCESSOR_FEATURES: Dict[int, str] = {
86
+ 6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand",
87
+ }
88
+
89
+
90
+ def windows_processor_features() -> List[str]:
91
+ """Instruction-set flags from the Win32 API, or none anywhere else.
92
+
93
+ ``ctypes.windll`` exists only on Windows, so on every other platform this
94
+ raises and answers "nothing known" — which is the honest reading, since the
95
+ flags are unobtainable rather than absent.
96
+ """
97
+ try:
98
+ import ctypes
99
+
100
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
101
+ return [
102
+ name
103
+ for code, name in WINDOWS_PROCESSOR_FEATURES.items()
104
+ if kernel32.IsProcessorFeaturePresent(code)
105
+ ]
106
+ except Exception:
107
+ quiet()
108
+ return []
109
+
110
+
53
111
  def detect_cuda(which: WhichFn, run: RunFn) -> Tuple[bool, str, Optional[str], Optional[str]]:
54
112
  nvidia_smi = which("nvidia-smi")
55
113
  nvcc = which("nvcc")
@@ -78,4 +136,12 @@ def detect_tools(which: WhichFn, binaries: Iterable[str]) -> Dict[str, Optional[
78
136
  return {binary: which(binary) for binary in binaries}
79
137
 
80
138
 
81
- __all__ = ["detect_cuda", "detect_tools", "detect_wsl_from_text", "parse_windows_video_controllers"]
139
+ __all__ = [
140
+ "WINDOWS_PROCESSOR_FEATURES",
141
+ "detect_cuda",
142
+ "detect_tools",
143
+ "detect_wsl_from_text",
144
+ "parse_windows_cpu_info",
145
+ "parse_windows_video_controllers",
146
+ "windows_processor_features",
147
+ ]
@@ -362,10 +362,6 @@ def configure_tool_dispatch(
362
362
  )
363
363
 
364
364
 
365
- def agent_policy(action_name: str, args: dict) -> ToolPolicy:
366
- return DEFAULT_TOOL_DISPATCH_SERVICE.policy_for(action_name, args)
367
-
368
-
369
365
  def agent_risk(action_name: str, args: dict) -> str:
370
366
  return DEFAULT_TOOL_DISPATCH_SERVICE.risk_level(action_name, args)
371
367
 
@@ -6,23 +6,15 @@ import logging
6
6
  import tempfile
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
- from typing import Optional
10
9
 
11
10
  from fastapi import HTTPException, Request, UploadFile
12
11
 
13
12
  from lattice_brain.ingestion import IngestionItem
13
+ from latticeai.api.workspace_scope import resolve_workspace_scope
14
14
  from latticeai.core.quiet import quiet
15
15
  from latticeai.tools import ToolError, read_document
16
16
 
17
17
 
18
- def _workspace_scope_from_request(request: Request) -> Optional[str]:
19
- header = request.headers.get("X-Workspace-Id")
20
- if header and header.strip():
21
- return header.strip()
22
- query = request.query_params.get("workspace_id")
23
- return query.strip() if query and query.strip() else None
24
-
25
-
26
18
  async def process_uploaded_document(
27
19
  *,
28
20
  request: Request,
@@ -39,17 +31,12 @@ async def process_uploaded_document(
39
31
  workspace_service=None,
40
32
  ) -> dict:
41
33
  enforce_rate_limit(current_user, "upload")
42
- requested_workspace = _workspace_scope_from_request(request)
43
- if workspace_service is not None:
44
- try:
45
- workspace_id = workspace_service.resolve_write_scope(
46
- requested_workspace,
47
- current_user or None,
48
- )
49
- except PermissionError as exc:
50
- raise HTTPException(status_code=403, detail=str(exc)) from exc
51
- else:
52
- workspace_id = requested_workspace
34
+ workspace_id = resolve_workspace_scope(
35
+ request,
36
+ user=current_user,
37
+ workspace_service=workspace_service,
38
+ write=True,
39
+ )
53
40
  suffix = Path(file.filename or "upload").suffix.lower()
54
41
  allowed = {".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".csv"}
55
42
  if suffix not in allowed:
@@ -73,12 +73,6 @@ class WorkspaceService:
73
73
  self._ensure_permission(workspace_id, user_id, "write")
74
74
  return workspace_id
75
75
 
76
- def can_read(self, workspace_id: str, user_id: Optional[str]) -> bool:
77
- return self.store.has_permission(workspace_id, self._identity(user_id), "read")
78
-
79
- def can_write(self, workspace_id: str, user_id: Optional[str]) -> bool:
80
- return self.store.has_permission(workspace_id, self._identity(user_id), "write")
81
-
82
76
  def readable_workspaces(self, user_id: Optional[str]) -> list[str]:
83
77
  """Return workspace ids the caller can read.
84
78
 
@@ -47,6 +47,7 @@ from dataclasses import asdict, dataclass, field
47
47
  from pathlib import Path
48
48
  from typing import Any, Dict, List, Optional, Tuple
49
49
 
50
+ from latticeai.core.module_probe import module_available
50
51
  from latticeai.core.quiet import quiet
51
52
  from latticeai.services.process_audit import (
52
53
  CommandConfirmationError,
@@ -59,6 +60,8 @@ from latticeai.services.setup_detection import (
59
60
  detect_cuda,
60
61
  detect_tools,
61
62
  detect_wsl_from_text,
63
+ parse_windows_cpu_info,
64
+ windows_processor_features,
62
65
  )
63
66
  from latticeai.services.setup_detection import (
64
67
  parse_windows_video_controllers as _parse_windows_video_controllers,
@@ -282,37 +285,23 @@ def _detect_cpu_details(prof_os: str) -> Tuple[str, int, int, List[str]]:
282
285
  flags = line.split(":", 1)[-1].strip().lower().split()
283
286
  elif prof_os == "windows":
284
287
  raw = _run(["wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors", "/format:list"])
285
- for line in raw.splitlines():
286
- key, _, value = line.partition("=")
287
- if key == "Name" and value.strip():
288
- model = value.strip()
289
- elif key == "NumberOfCores" and value.strip():
290
- try:
291
- physical = int(value.strip())
292
- except ValueError:
293
- quiet()
294
- elif key == "NumberOfLogicalProcessors" and value.strip():
295
- try:
296
- logical = int(value.strip())
297
- except ValueError:
298
- quiet()
299
- try:
300
- import ctypes
301
- kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
302
- feature_map = {6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand"}
303
- flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
304
- except Exception:
305
- quiet()
288
+ model, physical, logical = parse_windows_cpu_info(
289
+ raw, model=model, physical_cores=physical, logical_cores=logical,
290
+ )
291
+ flags.extend(windows_processor_features())
306
292
  interesting = {"avx", "avx2", "avx512f", "fma", "neon", "sse4_2", "sse", "sse2", "sse3", "rdrand"}
307
293
  return model, physical, logical, sorted({flag for flag in flags if flag in interesting})
308
294
 
309
295
 
310
296
  def _has_module(name: str) -> bool:
311
- try:
312
- __import__(name)
313
- return True
314
- except Exception:
315
- return False
297
+ """Importable *and loadable*.
298
+
299
+ ``strict=True`` on purpose: this probe drives what the setup wizard offers
300
+ to install, and an ``mlx`` wheel built for another architecture has a
301
+ findable spec right up until it raises on load. Answering from the spec
302
+ alone would plan a runtime that cannot start.
303
+ """
304
+ return module_available(name, strict=True)
316
305
 
317
306
 
318
307
  def probe() -> SystemProfile:
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
  import re
11
11
  from typing import Dict, List, Tuple
12
12
 
13
+ from latticeai.services.model_catalog import _version_tuple as _catalog_version_tuple
14
+
13
15
  # ── Model Catalog ─────────────────────────────────────────────────────────────
14
16
  # (model_id, display_name, size_gb, tag, description, min_ram_gb)
15
17
  # 11.2.0: 모든 repo id 를 2026-08-10 Hugging Face API 로 확인했다 — 존재 여부,
@@ -127,8 +129,9 @@ _BEST_MODEL_TIERS: Dict[str, List[Tuple[int, str]]] = {
127
129
  }
128
130
 
129
131
 
130
- def _version_tuple(raw: str) -> Tuple[int, ...]:
131
- return tuple(int(part) for part in raw.split(".") if part.isdigit())
132
+
133
+ #: The engine catalog owns this parse; the wizard compares the same strings.
134
+ _version_tuple = _catalog_version_tuple
132
135
 
133
136
 
134
137
  def _catalog_row_family_version(row: Tuple[str, str, float, str, str, int]) -> Tuple[str, Tuple[int, ...]] | None:
@@ -21,6 +21,8 @@ from latticeai.services.setup_detection import (
21
21
  detect_cuda,
22
22
  detect_tools,
23
23
  detect_wsl_from_text,
24
+ parse_windows_cpu_info,
25
+ windows_processor_features,
24
26
  )
25
27
  from latticeai.services.setup_detection import (
26
28
  parse_windows_video_controllers as _parse_windows_video_controllers,
@@ -98,33 +100,10 @@ def _detect_cpu() -> Dict[str, Any]:
98
100
  quiet()
99
101
  elif platform.system() == "Windows":
100
102
  raw = _cmd(["wmic", "cpu", "get", "Name,NumberOfCores,NumberOfLogicalProcessors", "/format:list"], timeout=5)
101
- for line in raw.splitlines():
102
- key, _, value = line.partition("=")
103
- if key == "Name" and value.strip():
104
- model = value.strip()
105
- elif key == "NumberOfCores" and value.strip():
106
- try:
107
- physical_cores = int(value.strip())
108
- except ValueError:
109
- quiet()
110
- elif key == "NumberOfLogicalProcessors" and value.strip():
111
- try:
112
- logical_cores = int(value.strip())
113
- except ValueError:
114
- quiet()
115
- try:
116
- import ctypes
117
- kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
118
- feature_map = {
119
- 6: "sse",
120
- 10: "sse2",
121
- 13: "sse3",
122
- 19: "neon",
123
- 28: "rdrand",
124
- }
125
- flags.extend(name for code, name in feature_map.items() if kernel32.IsProcessorFeaturePresent(code))
126
- except Exception:
127
- quiet()
103
+ model, physical_cores, logical_cores = parse_windows_cpu_info(
104
+ raw, model=model, physical_cores=physical_cores, logical_cores=logical_cores,
105
+ )
106
+ flags.extend(windows_processor_features())
128
107
  interesting = {"avx", "avx2", "avx512f", "fma", "neon", "sse4_2"}
129
108
  if platform.system() == "Windows":
130
109
  interesting.update({"sse", "sse2", "sse3", "rdrand"})
@@ -20,6 +20,8 @@ import shutil
20
20
  from pathlib import Path
21
21
  from typing import Any, Dict, List
22
22
 
23
+ from latticeai.core.module_probe import module_available as _module_available
24
+
23
25
  OFFICIAL_DOWNLOADS: Dict[str, str] = {
24
26
  "homebrew": "https://brew.sh",
25
27
  "python": "https://www.python.org/downloads/",
@@ -149,11 +151,6 @@ def _which_detail(binary: str) -> Dict[str, Any]:
149
151
  return {"installed": path is not None, "path": path}
150
152
 
151
153
 
152
- def _module_available(module_name: str) -> bool:
153
- import importlib.util
154
- return importlib.util.find_spec(module_name) is not None
155
-
156
-
157
154
  def _package_module(package: str) -> str:
158
155
  return PACKAGE_MODULES.get(package, package.replace("-", "_").split("[", 1)[0])
159
156
 
@@ -8,15 +8,15 @@ plan and therefore nothing to confirm.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import json as _json
12
11
  import sys
13
12
  from typing import Any, Dict, List
14
13
 
14
+ from latticeai.core.sse import sse_frame
15
15
  from latticeai.services.process_audit import command_plan_for_commands
16
16
 
17
17
 
18
18
  def _sse(data: Dict) -> str:
19
- return f"data: {_json.dumps(data, ensure_ascii=False)}\n\n"
19
+ return sse_frame(None, data)
20
20
 
21
21
 
22
22
  def _action_commands(action: Dict[str, Any]) -> List[List[str]]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "11.5.0",
3
+ "version": "11.5.2",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -52,7 +52,6 @@
52
52
  "desktop:tauri:check": "cd src-tauri && cargo check",
53
53
  "rust:test": "cd rust && cargo test --workspace",
54
54
  "rust:lint": "cd rust && cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings",
55
- "desktop:electron": "electron desktop/electron/main.cjs",
56
55
  "package:vsix": "node scripts/build_vsix.mjs",
57
56
  "check:screenshot-delta": "node scripts/run_python.mjs scripts/check_screenshot_pixel_delta.py",
58
57
  "release:evidence": "node scripts/capture_release_evidence.mjs && node scripts/prune_release_evidence.mjs && npm run check:screenshot-delta",
@@ -124,7 +123,6 @@
124
123
  "!src-tauri/gen/**",
125
124
  "!src-tauri/target/",
126
125
  "!src-tauri/target/**",
127
- "desktop/electron/",
128
126
  "docs/*.md",
129
127
  "!docs/assets/",
130
128
  "!docs/images/"
@@ -142,6 +140,7 @@
142
140
  "@testing-library/react": "^16.3.2",
143
141
  "@testing-library/user-event": "^14.6.1",
144
142
  "@types/cytoscape": "^3.21.9",
143
+ "@types/node": "24.13.2",
145
144
  "@types/react": "^19.2.17",
146
145
  "@types/react-dom": "^19.2.3",
147
146
  "@vitejs/plugin-react": "^6.0.2",
@@ -150,7 +149,6 @@
150
149
  "class-variance-authority": "^0.7.1",
151
150
  "clsx": "^2.1.1",
152
151
  "cytoscape": "^3.34.0",
153
- "electron": "^42.4.0",
154
152
  "jsdom": "^29.1.1",
155
153
  "lucide-react": "^1.17.0",
156
154
  "openapi-fetch": "^0.17.0",
@@ -25,7 +25,20 @@ REPO = Path(__file__).resolve().parents[1]
25
25
  # it per crate — src-tauri's too, since 11.4.0, because the desktop shell now
26
26
  # depends on lattice-host by path — and a lockfile left behind is a file the
27
27
  # next `cargo build` silently rewrites underneath a tagged release.
28
- RUST_CRATES = ("lattice-core", "lattice-host", "lattice-retrieval")
28
+ #
29
+ # 11.5.2: this tuple listed only the three Phase-1 crates, so every bump since
30
+ # 11.5.0 left `lattice-agent`, `lattice-ingest` and `lattice-jobs` at the
31
+ # previous version in both lockfiles — and the first `cargo` invocation of the
32
+ # test suite rewrote them, dirtying the tree mid-release. Every workspace
33
+ # member belongs here.
34
+ RUST_CRATES = (
35
+ "lattice-agent",
36
+ "lattice-core",
37
+ "lattice-host",
38
+ "lattice-ingest",
39
+ "lattice-jobs",
40
+ "lattice-retrieval",
41
+ )
29
42
  RUST_LOCKFILES = ("rust/Cargo.lock", "src-tauri/Cargo.lock")
30
43
 
31
44
  # (path, kind, pattern) — pattern groups: (prefix, version)
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Rust Complete";
9
+ const releaseTheme = "Tight Ship";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
@@ -31,7 +31,7 @@ const REMOVED_MODULES = [
31
31
  ];
32
32
  // Trees that must not import removed shims. static/app and node_modules are
33
33
  // build output; src-tauri/target is compiler output.
34
- const SOURCE_TREES = ["latticeai", "lattice_brain", "tests", "scripts", "bin", "desktop/electron"];
34
+ const SOURCE_TREES = ["latticeai", "lattice_brain", "tests", "scripts", "bin"];
35
35
 
36
36
  const failures = [];
37
37
 
@@ -29,6 +29,7 @@ const API_DIR = "latticeai/api";
29
29
  // which is how the removed root shims used to be referenced.
30
30
  const LOCALIZED = [
31
31
  "admin",
32
+ "agent_worker_seam",
32
33
  "auth",
33
34
  "browser",
34
35
  "chat",