ltcai 11.5.1 → 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 (176) hide show
  1. package/README.md +85 -53
  2. package/docs/CHANGELOG.md +54 -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.2_TIGHT_SHIP_PLAN.md +144 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/proactive.py +1 -1
  17. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  18. package/lattice_brain/graph/vector_index/base.py +0 -3
  19. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  20. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  21. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  22. package/lattice_brain/ingestion_jobs.py +0 -3
  23. package/lattice_brain/memory.py +0 -27
  24. package/lattice_brain/portability/sharing.py +0 -4
  25. package/lattice_brain/quality.py +0 -49
  26. package/lattice_brain/runtime/agent_runtime.py +1 -1
  27. package/lattice_brain/runtime/hooks.py +0 -13
  28. package/lattice_brain/runtime/multi_agent.py +1 -1
  29. package/lattice_brain/sealed_box.py +0 -4
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/api/admin.py +12 -12
  32. package/latticeai/api/auth.py +25 -2
  33. package/latticeai/api/chat.py +3 -3
  34. package/latticeai/api/chat_agent_http.py +3 -12
  35. package/latticeai/api/chat_helpers.py +10 -13
  36. package/latticeai/api/computer_use.py +31 -42
  37. package/latticeai/api/health.py +21 -1
  38. package/latticeai/api/local_files.py +14 -0
  39. package/latticeai/api/permissions.py +38 -12
  40. package/latticeai/api/search.py +24 -0
  41. package/latticeai/api/static_routes.py +4 -1
  42. package/latticeai/cli/runtime.py +7 -3
  43. package/latticeai/core/config.py +47 -3
  44. package/latticeai/core/csrf.py +28 -2
  45. package/latticeai/core/embedding_providers/__init__.py +3 -5
  46. package/latticeai/core/embedding_providers/base.py +1 -1
  47. package/latticeai/core/embedding_providers/text.py +1 -1
  48. package/latticeai/core/enterprise.py +1 -4
  49. package/latticeai/core/http_origin.py +146 -0
  50. package/latticeai/core/invitations.py +3 -2
  51. package/latticeai/core/io_utils.py +2 -11
  52. package/latticeai/core/legacy_compatibility.py +1 -1
  53. package/latticeai/core/marketplace.py +1 -1
  54. package/latticeai/core/messages.py +5 -0
  55. package/latticeai/core/model_compat.py +2 -8
  56. package/latticeai/core/module_probe.py +37 -0
  57. package/latticeai/core/run_explain.py +5 -2
  58. package/latticeai/core/run_store.py +2 -2
  59. package/latticeai/core/security.py +13 -0
  60. package/latticeai/core/sessions.py +3 -2
  61. package/latticeai/core/sse.py +25 -0
  62. package/latticeai/core/users.py +0 -9
  63. package/latticeai/core/workspace_graph_trace.py +2 -8
  64. package/latticeai/core/workspace_os.py +0 -10
  65. package/latticeai/core/workspace_os_constants.py +1 -1
  66. package/latticeai/core/workspace_os_utils.py +34 -0
  67. package/latticeai/core/workspace_review_items.py +2 -8
  68. package/latticeai/core/workspace_runs.py +2 -8
  69. package/latticeai/core/workspace_skills.py +2 -7
  70. package/latticeai/models/router/documents.py +0 -35
  71. package/latticeai/models/router/registry.py +0 -4
  72. package/latticeai/runtime/access_runtime.py +16 -4
  73. package/latticeai/runtime/build_phases/features.py +2 -0
  74. package/latticeai/runtime/build_phases/web.py +2 -0
  75. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  76. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  77. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  78. package/latticeai/runtime/router_registration.py +4 -0
  79. package/latticeai/runtime/runtime_context.py +1 -13
  80. package/latticeai/runtime/service_singletons.py +55 -0
  81. package/latticeai/services/architecture_readiness.py +1 -1
  82. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  83. package/latticeai/services/change_proposals.py +2 -36
  84. package/latticeai/services/chronicle.py +4 -6
  85. package/latticeai/services/command_center.py +4 -4
  86. package/latticeai/services/evidence_actions.py +5 -2
  87. package/latticeai/services/hybrid_chat.py +2 -2
  88. package/latticeai/services/hybrid_policy.py +8 -57
  89. package/latticeai/services/mode_store.py +132 -0
  90. package/latticeai/services/model_capability_registry.py +0 -15
  91. package/latticeai/services/model_catalog.py +0 -7
  92. package/latticeai/services/model_loading.py +3 -2
  93. package/latticeai/services/model_runtime/__init__.py +0 -57
  94. package/latticeai/services/model_runtime/engines.py +27 -128
  95. package/latticeai/services/model_runtime/loading.py +2 -2
  96. package/latticeai/services/network_boundary_service.py +7 -44
  97. package/latticeai/services/permission_mode_service.py +8 -56
  98. package/latticeai/services/product_readiness.py +1 -1
  99. package/latticeai/services/setup_detection.py +67 -1
  100. package/latticeai/services/tool_dispatch.py +0 -4
  101. package/latticeai/services/upload_service.py +7 -20
  102. package/latticeai/services/workspace_service.py +0 -6
  103. package/latticeai/setup/auto_setup.py +15 -26
  104. package/latticeai/setup/wizard/catalog.py +5 -2
  105. package/latticeai/setup/wizard/detect.py +6 -27
  106. package/latticeai/setup/wizard/paths.py +2 -5
  107. package/latticeai/setup/wizard/plans.py +2 -2
  108. package/package.json +2 -4
  109. package/scripts/bump_version.py +14 -1
  110. package/scripts/check_current_release_docs.mjs +1 -1
  111. package/scripts/check_legacy_debt.mjs +1 -1
  112. package/scripts/generate_agent_loop_fixtures.py +87 -0
  113. package/scripts/generate_rust_parity_fixtures.py +3 -95
  114. package/scripts/parity_fixture_corpus_context.py +162 -0
  115. package/scripts/release_screen_claims.json +10 -0
  116. package/src-tauri/Cargo.lock +7 -7
  117. package/src-tauri/Cargo.toml +1 -1
  118. package/src-tauri/src/backend.rs +76 -2
  119. package/src-tauri/src/main.rs +13 -3
  120. package/src-tauri/tauri.conf.json +2 -2
  121. package/static/app/asset-manifest.json +40 -40
  122. package/static/app/assets/{Act-Drs_jd-O.js → Act-DcQizkl1.js} +1 -1
  123. package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-cf4npybT.js} +1 -1
  124. package/static/app/assets/{Brain-C7bdNmz-.js → Brain-3VCSHFcn.js} +1 -1
  125. package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-Qm8eaztx.js} +1 -1
  126. package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-DS9BtKOW.js} +1 -1
  127. package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DiQ219jW.js} +1 -1
  128. package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-BGvuAchH.js} +1 -1
  129. package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-Bqhm0Urn.js} +1 -1
  130. package/static/app/assets/{Library-CYxeuraA.js → Library-BV6NnF0a.js} +1 -1
  131. package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-GzenJchP.js} +1 -1
  132. package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-DEP6-vML.js} +1 -1
  133. package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CNZ7XjWG.js} +1 -1
  134. package/static/app/assets/{System-DcRAq7wj.js → System-CieofHQa.js} +1 -1
  135. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  136. package/static/app/assets/{bot-BK2xQ9mN.js → bot-B_K1Tdmw.js} +1 -1
  137. package/static/app/assets/{brain-BZNztFBb.js → brain-DWyaV1L1.js} +1 -1
  138. package/static/app/assets/{button-CjueubVZ.js → button-aTn4s84A.js} +1 -1
  139. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  140. package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-xKgeGXkT.js} +1 -1
  141. package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-DkT6tYPX.js} +1 -1
  142. package/static/app/assets/{cpu-B_MyTfYr.js → cpu-85xYObUC.js} +1 -1
  143. package/static/app/assets/{download-CTy0EByV.js → download-B5Fm7YXo.js} +1 -1
  144. package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-kk2Xa52u.js} +1 -1
  145. package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-DkA3zBW_.js} +1 -1
  146. package/static/app/assets/{index-Dd6abJHX.js → index-BMPdTmlY.js} +3 -3
  147. package/static/app/assets/{input-bVgIRN3s.js → input-B0nRf2jO.js} +1 -1
  148. package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-Dwb4gnTc.js} +1 -1
  149. package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CQDUBrOZ.js} +1 -1
  150. package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-SNp0LRJz.js} +1 -1
  151. package/static/app/assets/search-BcHqkjoy.js +1 -0
  152. package/static/app/assets/{share-2-BLzo7U4L.js → share-2-BsrxFglO.js} +1 -1
  153. package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-5BStfp2_.js} +1 -1
  154. package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-Cg8IUA-k.js} +1 -1
  155. package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-CYKvE46M.js} +1 -1
  156. package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-CSn9t1op.js} +1 -1
  157. package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-CY2OI2uy.js} +1 -1
  158. package/static/app/assets/{utils-D3u_yv7B.js → utils-Ddol2RWD.js} +1 -1
  159. package/static/app/assets/{workspace-H3bMjYXC.js → workspace-BqDwOz_p.js} +1 -1
  160. package/static/app/index.html +3 -3
  161. package/static/sw.js +1 -1
  162. package/desktop/electron/README.md +0 -9
  163. package/desktop/electron/main.cjs +0 -58
  164. package/desktop/electron/preload.cjs +0 -5
  165. package/latticeai/core/graph_curator.py +0 -11
  166. package/latticeai/core/hooks.py +0 -11
  167. package/latticeai/core/local_embeddings.py +0 -104
  168. package/latticeai/core/multi_agent.py +0 -11
  169. package/latticeai/core/workflow_engine.py +0 -11
  170. package/latticeai/services/ingestion.py +0 -11
  171. package/latticeai/services/kg_portability.py +0 -11
  172. package/latticeai/services/multimodal_streaming.py +0 -129
  173. package/scripts/measure_brain_home_fill.mjs +0 -141
  174. package/static/app/assets/arrow-left-BHYmOTWU.js +0 -1
  175. package/static/app/assets/circle-check-CLeWJr67.js +0 -1
  176. package/static/app/assets/search-BgSVX6NG.js +0 -1
@@ -12,7 +12,6 @@
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import importlib.util
16
15
  import json
17
16
  import logging
18
17
  import re
@@ -22,6 +21,8 @@ from dataclasses import asdict, dataclass
22
21
  from pathlib import Path
23
22
  from typing import Any, Dict, List, Optional, Tuple
24
23
 
24
+ from latticeai.core.module_probe import module_available as _module_available
25
+
25
26
  logger = logging.getLogger(__name__)
26
27
 
27
28
 
@@ -198,13 +199,6 @@ GEMMA4_MLX_LM_MODULES = ("mlx_lm.models.gemma4", "mlx_lm.models.gemma4_text")
198
199
  GEMMA4_UNIFIED_ID_PATTERN = re.compile(r"gemma[-_/ ]?4[-_/ ]?12b", re.I)
199
200
 
200
201
 
201
- def _module_available(module: str) -> bool:
202
- try:
203
- return importlib.util.find_spec(module) is not None
204
- except (ImportError, ModuleNotFoundError, AttributeError, ValueError):
205
- return False
206
-
207
-
208
202
  def _is_gemma4(model_id: str) -> bool:
209
203
  raw = str(model_id or "").lower()
210
204
  return bool(re.search(r"gemma[-_/ ]?4", raw))
@@ -0,0 +1,37 @@
1
+ """One answer to "is this module importable?".
2
+
3
+ Four modules asked it four different ways: two consulted ``find_spec``, one
4
+ imported for real, and they disagreed about which failures mean "no" and which
5
+ should escape. The disagreement mattered — the setup probe's copy imports for
6
+ real *on purpose*, because a native wheel like ``mlx`` can have a perfectly
7
+ findable spec on the wrong architecture and still raise when it loads. A copy
8
+ that only looked for the spec would report the runtime as present and let the
9
+ recommendation engine plan an install around a module that cannot run.
10
+
11
+ So the two behaviours stay, named: ``strict=False`` asks whether Python can
12
+ *find* the module (cheap, no side effects, no import cost); ``strict=True``
13
+ actually imports it and answers whether it *works*.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib
19
+ import importlib.util
20
+
21
+ __all__ = ["module_available"]
22
+
23
+
24
+ def module_available(name: str, *, strict: bool = False) -> bool:
25
+ """Report whether ``name`` can be imported, never raising.
26
+
27
+ A probe that raises is a probe that takes the caller down for asking, and
28
+ every caller here is deciding what to *offer* the user — "I could not tell"
29
+ and "it is not there" lead to the same honest answer.
30
+ """
31
+ try:
32
+ if strict:
33
+ importlib.import_module(name)
34
+ return True
35
+ return importlib.util.find_spec(name) is not None
36
+ except Exception:
37
+ return False
@@ -30,6 +30,8 @@ from __future__ import annotations
30
30
 
31
31
  from typing import Any, Dict, List, Mapping, Optional, Sequence
32
32
 
33
+ from latticeai.core.messages import bilingual as _bilingual
34
+
33
35
  __all__ = ["explain_run", "STRAIN_LEVELS"]
34
36
 
35
37
 
@@ -53,8 +55,9 @@ _REPAIR_GROUPS: Dict[str, tuple] = {
53
55
  }
54
56
 
55
57
 
56
- def _phrase(ko: str, en: str) -> Dict[str, str]:
57
- return {"ko": ko, "en": en}
58
+
59
+ #: Historical module-local name for the shared phrase-pair helper.
60
+ _phrase = _bilingual
58
61
 
59
62
 
60
63
  def _int(value: Any) -> int:
@@ -21,7 +21,6 @@ Design constraints (deliberate, matching :mod:`latticeai.services.funnel_metrics
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- import hashlib
25
24
  import json
26
25
  import logging
27
26
  import os
@@ -34,6 +33,7 @@ from typing import Any, Dict, List, Optional
34
33
  from latticeai.core.agent import AgentRunContext, AgentState
35
34
  from latticeai.core.agent_trace import LoopTrace
36
35
  from latticeai.core.quiet import quiet
36
+ from latticeai.core.security import sha256_hex
37
37
 
38
38
  LOGGER = logging.getLogger(__name__)
39
39
 
@@ -44,7 +44,7 @@ _RUN_ID_RE = re.compile(r"^[A-Za-z0-9_-]{8,64}$")
44
44
 
45
45
  def hash_approval_token(token: str) -> str:
46
46
  """Stable digest used both at save and at resume comparison time."""
47
- return hashlib.sha256(str(token or "").encode("utf-8")).hexdigest()
47
+ return sha256_hex(str(token or ""))
48
48
 
49
49
 
50
50
  def serialize_run_context(ctx: AgentRunContext) -> Dict[str, Any]:
@@ -13,6 +13,19 @@ from fastapi import HTTPException
13
13
  from latticeai.core.quiet import quiet
14
14
 
15
15
 
16
+ def sha256_hex(text: str) -> str:
17
+ """The one UTF-8 SHA-256 hex digest.
18
+
19
+ Six modules each carried this three-token expression, and they did not
20
+ agree on what to do with ``None``: two coerced it away, one raised. The
21
+ helper stays **strict** on purpose — coercing here would silently give
22
+ every caller the digest of the empty string, which is a valid-looking
23
+ token hash for "no token at all". Each call site keeps the contract it
24
+ already had, stated in its own body where a reader can see it.
25
+ """
26
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
27
+
28
+
16
29
  def hash_password(password: str) -> str:
17
30
  salt = secrets.token_hex(16)
18
31
  key = hashlib.scrypt(password.encode(), salt=salt.encode(), n=16384, r=8, p=1)
@@ -6,7 +6,6 @@ holding raw tokens are migrated transparently on load (sessions survive the
6
6
  upgrade; the raw token never touches disk again).
7
7
  """
8
8
 
9
- import hashlib
10
9
  import json
11
10
  import logging
12
11
  import secrets
@@ -16,6 +15,7 @@ from pathlib import Path
16
15
  from typing import Dict, Optional
17
16
 
18
17
  from latticeai.core.io_utils import atomic_write_json
18
+ from latticeai.core.security import sha256_hex
19
19
 
20
20
  SESSION_TTL = 60 * 60 * 24 # 24 hours
21
21
  SESSION_REFRESH_THRESHOLD = 60 * 15 # only persist if >15 min since last bump
@@ -25,7 +25,8 @@ _HEX64 = frozenset("0123456789abcdef")
25
25
 
26
26
 
27
27
  def _hash_token(token: str) -> str:
28
- return hashlib.sha256((token or "").encode("utf-8")).hexdigest()
28
+ """Session keys are hashed at rest; a missing token hashes the empty string."""
29
+ return sha256_hex(token or "")
29
30
 
30
31
 
31
32
  def _looks_hashed(key: str) -> bool:
@@ -0,0 +1,25 @@
1
+ """The one Server-Sent Events frame builder.
2
+
3
+ Five modules each formatted their own frame — three in the
4
+ ``event:``/``data:`` shape and two in the bare ``data:`` shape — with the same
5
+ ``json.dumps(..., ensure_ascii=False)`` payload. They agreed by accident, and a
6
+ frame that disagrees by one byte is a stream the browser silently stops
7
+ parsing, so the agreement is now structural rather than coincidental.
8
+
9
+ ``event=None`` produces the bare shape; a named event produces the prefixed
10
+ one. Both are byte-identical to the copies they replaced.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import Any, Optional
17
+
18
+ __all__ = ["sse_frame"]
19
+
20
+
21
+ def sse_frame(event: Optional[str], data: Any) -> str:
22
+ """Render one SSE frame, terminated by the blank line that ends it."""
23
+ payload = json.dumps(data, ensure_ascii=False)
24
+ prefix = f"event: {event}\n" if event else ""
25
+ return f"{prefix}data: {payload}\n\n"
@@ -100,15 +100,6 @@ def user_id_for_email(users: Dict[str, Any], email: Optional[str]) -> Optional[s
100
100
  return stable_user_id(normalized)
101
101
 
102
102
 
103
- def email_for_user_id(users: Dict[str, Any], user_id: Optional[str]) -> Optional[str]:
104
- if not user_id:
105
- return None
106
- for email, user in (users or {}).items():
107
- if isinstance(user, dict) and user.get("id") == user_id:
108
- return email
109
- return None
110
-
111
-
112
103
  def migrate_knowledge_graph_identity(db_path: Path, email_to_id: Dict[str, str]) -> int:
113
104
  """Rewrite KG owner/creator identity columns from email to stable UUIDs."""
114
105
  if not db_path.exists() or not email_to_id:
@@ -6,16 +6,10 @@ from typing import Any, Dict, List, Optional
6
6
  from latticeai.core.quiet import quiet
7
7
 
8
8
  from .timeutil import now_iso as _now
9
- from .workspace_os_utils import _json_hash, _listify
9
+ from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
10
10
 
11
11
 
12
- class WorkspaceGraphTrace:
13
- def __init__(self, store: Any):
14
- self._store = store
15
-
16
- def __getattr__(self, name: str) -> Any:
17
- return getattr(self._store, name)
18
-
12
+ class WorkspaceGraphTrace(WorkspaceStoreMixin):
19
13
  def build_graph_trace(
20
14
  self,
21
15
  question: str,
@@ -53,7 +53,6 @@ from .workspace_permissions import ( # type: ignore
53
53
  from .workspace_plugins import WorkspacePluginManager
54
54
  from .workspace_relationships import WorkspaceRelationships
55
55
  from .workspace_relationships import shortest_path as _shortest_path
56
- from .workspace_reorganization import plan_reorganization
57
56
  from .workspace_review_items import WorkspaceReviewItems
58
57
  from .workspace_runs import WorkspaceRuns
59
58
  from .workspace_skills import WorkspaceSkills
@@ -844,15 +843,6 @@ class WorkspaceOSStore:
844
843
  # Agent-native workspace: folder reorganization (v11.1.0)
845
844
  # ------------------------------------------------------------------
846
845
 
847
- def preview_reorganization(
848
- self, *, root: str = "", resolve_path: Callable[[str], Any], graph: Any = None,
849
- max_moves: int = 20,
850
- ) -> Dict[str, Any]:
851
- """What a reorganization would do — read-only, nothing is staged."""
852
- return plan_reorganization(
853
- root=root, resolve_path=resolve_path, graph=graph, max_moves=max_moves
854
- )
855
-
856
846
  def propose_reorganization(
857
847
  self, *, root: str = "", change_proposals: Any, graph: Any = None,
858
848
  user_email: Optional[str] = None, workspace_id: Optional[str] = None,
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Dict
12
12
 
13
- WORKSPACE_OS_VERSION = "11.5.1"
13
+ WORKSPACE_OS_VERSION = "11.5.2"
14
14
 
15
15
  # Workspace types separate single-user Personal workspaces from shared
16
16
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -17,6 +17,40 @@ from .io_utils import (
17
17
  from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
18
18
 
19
19
 
20
+ class WorkspaceStoreMixin:
21
+ """A collaborator that reads and writes through ``WorkspaceOSStore``.
22
+
23
+ The store was decomposed into managers that each own one branch of the state
24
+ document, and every one of them needs the same two things: a reference to the
25
+ store, and for anything it does not implement itself (``load_state``,
26
+ ``_scoped``, ``record_timeline_event``) to reach the store instead of raising
27
+ ``AttributeError``. Four managers wrote that out identically; the pair lives
28
+ here so a fifth cannot invent a third spelling of it.
29
+ """
30
+
31
+ def __init__(self, store: Any) -> None:
32
+ self._store = store
33
+
34
+ def __getattr__(self, name: str) -> Any:
35
+ # Only reached for attributes this manager does not define, so the
36
+ # store is the answer for every state-document operation.
37
+ return getattr(self._store, name)
38
+
39
+
40
+ def graph_scope_kwargs(workspace_id: Any) -> Dict[str, Any]:
41
+ """The scoped-read kwargs the graph store expects (house pattern).
42
+
43
+ ``workspace_id is None`` means "unscoped caller": it reads everything it is
44
+ allowed to, legacy rows that predate workspaces included. A named workspace
45
+ reads only that workspace and never the legacy pool — which is the whole
46
+ isolation guarantee, so it is stated once.
47
+ """
48
+ return {
49
+ "allowed_workspaces": {workspace_id} if workspace_id is not None else None,
50
+ "include_legacy_global": workspace_id is None,
51
+ }
52
+
53
+
20
54
  def _safe_slug(raw: str) -> str:
21
55
  value = "".join(ch if ch.isalnum() or ch in "-_." else "-" for ch in str(raw or "").strip())
22
56
  value = "-".join(part for part in value.split("-") if part)
@@ -14,16 +14,10 @@ from __future__ import annotations
14
14
  from typing import Any, Dict, List, Optional
15
15
 
16
16
  from .timeutil import now_iso as _now
17
- from .workspace_os_utils import _json_hash, _listify
17
+ from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
18
18
 
19
19
 
20
- class WorkspaceReviewItems:
21
- def __init__(self, store: Any):
22
- self._store = store
23
-
24
- def __getattr__(self, name: str) -> Any:
25
- return getattr(self._store, name)
26
-
20
+ class WorkspaceReviewItems(WorkspaceStoreMixin):
27
21
  def create_review_item(
28
22
  self,
29
23
  *,
@@ -8,16 +8,10 @@ from lattice_brain.runtime.contracts import run_record_contract, workflow_run_co
8
8
  from lattice_brain.runtime.statuses import RUN_ACTIVE_STATUSES, RUN_TERMINAL_STATUSES
9
9
 
10
10
  from .timeutil import now_iso as _now
11
- from .workspace_os_utils import _json_hash, _listify
11
+ from .workspace_os_utils import WorkspaceStoreMixin, _json_hash, _listify
12
12
 
13
13
 
14
- class WorkspaceRuns:
15
- def __init__(self, store: Any):
16
- self._store = store
17
-
18
- def __getattr__(self, name: str) -> Any:
19
- return getattr(self._store, name)
20
-
14
+ class WorkspaceRuns(WorkspaceStoreMixin):
21
15
  def list_agents(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
22
16
  state = self.load_state()
23
17
  runs = self._scoped(_listify(state.get("agent_runs")), workspace_id)
@@ -6,15 +6,10 @@ from pathlib import Path
6
6
  from typing import Any, Dict, List, Optional
7
7
 
8
8
  from .timeutil import now_iso as _now
9
+ from .workspace_os_utils import WorkspaceStoreMixin
9
10
 
10
11
 
11
- class WorkspaceSkills:
12
- def __init__(self, store: Any):
13
- self._store = store
14
-
15
- def __getattr__(self, name: str) -> Any:
16
- return getattr(self._store, name)
17
-
12
+ class WorkspaceSkills(WorkspaceStoreMixin):
18
13
  def list_skill_registry(self, skills_dir: Path, marketplace: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
19
14
  state = self.load_state()
20
15
  registry = state.setdefault("skill_registry", {})
@@ -21,23 +21,6 @@ class _DocumentMixin(_Core):
21
21
 
22
22
  # ── Document Generation Pipeline ──────────────────────────────────────
23
23
 
24
- async def generate_document(
25
- self,
26
- message: str,
27
- system_prompt: str,
28
- *,
29
- max_tokens: int = 8192,
30
- temperature: float = 0.3,
31
- ) -> str:
32
- """Generate a document using a specialized system prompt with graph context."""
33
- return await self.generate_document_as(
34
- None,
35
- message,
36
- system_prompt,
37
- max_tokens=max_tokens,
38
- temperature=temperature,
39
- )
40
-
41
24
  async def generate_document_as(
42
25
  self,
43
26
  model_id: str | None,
@@ -98,24 +81,6 @@ class _DocumentMixin(_Core):
98
81
  raise RuntimeError(self._local_server_error_hint(cloud, e)) from e
99
82
  return normalize_branding(response.choices[0].message.content or "")
100
83
 
101
- async def stream_generate_document(
102
- self,
103
- message: str,
104
- system_prompt: str,
105
- *,
106
- max_tokens: int = 8192,
107
- temperature: float = 0.3,
108
- ) -> AsyncIterator[str]:
109
- """Stream document generation with specialized system prompt."""
110
- async for chunk in self.stream_generate_document_as(
111
- None,
112
- message,
113
- system_prompt,
114
- max_tokens=max_tokens,
115
- temperature=temperature,
116
- ):
117
- yield chunk
118
-
119
84
  async def stream_generate_document_as(
120
85
  self,
121
86
  model_id: str | None,
@@ -107,10 +107,6 @@ class _RegistryMixin(_Core):
107
107
  self.unload_model(victim)
108
108
  local_ids = [model_id for model_id in self._cache if self._is_local_model(model_id)]
109
109
 
110
- def _is_cloud_current(self) -> bool:
111
- with self._lock:
112
- return bool(self._current and isinstance(self._cache.get(self._current), CloudModel))
113
-
114
110
  def _local_server_error_hint(self, cloud: CloudModel, error: Exception) -> str:
115
111
  raw = str(error)
116
112
  if cloud.provider == "lmstudio":
@@ -7,6 +7,21 @@ from typing import Any, Callable, Dict, Optional
7
7
  from latticeai.core.quiet import quiet
8
8
 
9
9
 
10
+ def is_externally_reachable(config: Any) -> bool:
11
+ """Whether anything but this machine can open a socket to this worker.
12
+
13
+ The single definition of the second half of ``trusted_local_owner``. It is
14
+ read here (to decide who the anonymous caller is) and by the ``/health``
15
+ router (to *state* the posture, so the Rust front door can mirror it on its
16
+ own native lanes instead of guessing). Two derivations of one fact is how
17
+ a gateway ends up serving a graph the worker would have refused.
18
+ """
19
+ return bool(
20
+ getattr(config, "is_public", False)
21
+ or getattr(config, "network_exposed", False)
22
+ )
23
+
24
+
10
25
  def build_access_runtime(
11
26
  *,
12
27
  config: Any,
@@ -29,10 +44,7 @@ def build_access_runtime(
29
44
  # but project the identity as an owner at authorization boundaries. Never
30
45
  # extend this trust to public or non-loopback bindings, even if an invalid
31
46
  # caller constructs this runtime with ``require_auth=False`` directly.
32
- externally_reachable = bool(
33
- getattr(config, "is_public", False)
34
- or getattr(config, "network_exposed", False)
35
- )
47
+ externally_reachable = is_externally_reachable(config)
36
48
  trusted_local_owner = not require_auth and not externally_reachable
37
49
  effective_require_auth = bool(require_auth or externally_reachable)
38
50
 
@@ -305,6 +305,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
305
305
  from latticeai.core.model_compat import (
306
306
  list_cached_profiles as _list_compat_profiles,
307
307
  )
308
+ from latticeai.runtime.access_runtime import is_externally_reachable
308
309
  from latticeai.runtime.chat_wiring import build_interaction_contexts
309
310
  from latticeai.runtime.model_wiring import register_model_runtime_routers
310
311
  from latticeai.runtime.platform_services_runtime import build_brain_network
@@ -340,6 +341,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
340
341
  engine_status=service.engine_status,
341
342
  get_current_user=ctx.get_current_user,
342
343
  require_auth=ctx.REQUIRE_AUTH,
344
+ externally_reachable=is_externally_reachable(ctx.CONFIG),
343
345
  app_version=ctx.APP_VERSION,
344
346
  app_mode=ctx.APP_MODE,
345
347
  require_user=ctx.require_user,
@@ -310,6 +310,7 @@ def phase_web(ctx: RuntimeContext) -> None:
310
310
  create_security_router as _create_security_router,
311
311
  )
312
312
  from latticeai.api.static_routes import create_static_routes_router
313
+ from latticeai.core.config import default_sso_redirect_uri
313
314
  from latticeai.core.policy import policy_matrix
314
315
  from latticeai.core.product_hardening import build_product_hardening_status
315
316
  from latticeai.core.security import hash_password
@@ -408,6 +409,7 @@ def phase_web(ctx: RuntimeContext) -> None:
408
409
 
409
410
  foundation_router_bundle = build_auth_admin_security_router_bundle(
410
411
  create_auth_router=create_auth_router,
412
+ sso_default_redirect_uri=default_sso_redirect_uri(ctx.DEFAULT_PORT),
411
413
  load_users=ctx.load_users,
412
414
  save_users=ctx.save_users,
413
415
  hash_password=hash_password,
@@ -1,9 +1,11 @@
1
1
  """Wire the feature switchboard into the running app (v11.2.0).
2
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.
3
+ Same shape as ``permission_mode_wiring``: one process-wide service and an
4
+ idempotent router mount guarded on ``app.state``. The rebinding rule that shape
5
+ turns on — explicit arguments *rebind* an already-created service rather than
6
+ being dropped, so a lazy first caller cannot pin the store to a fallback data
7
+ dir with no audit sink — is shared, in
8
+ :mod:`latticeai.runtime.service_singletons`.
7
9
 
8
10
  The one job unique to this module is :func:`bind_feature_gates`: it points each
9
11
  opt-in gate's resolver at the service, which is the step that turns a persisted
@@ -18,11 +20,14 @@ them back, and "the environment answers again" has to be reachable.
18
20
 
19
21
  from __future__ import annotations
20
22
 
21
- import os
22
23
  import threading
23
24
  from pathlib import Path
24
25
  from typing import Any, Callable, List, Optional, Tuple
25
26
 
27
+ from latticeai.runtime.service_singletons import (
28
+ rebind_singleton,
29
+ singleton_data_dir,
30
+ )
26
31
  from latticeai.services.feature_toggles import FeatureToggleService
27
32
 
28
33
  _LOCK = threading.Lock()
@@ -47,13 +52,6 @@ GATE_BINDINGS: Tuple[Tuple[str, str, str], ...] = (
47
52
  CHOICE_FEATURE = "vector_backend"
48
53
 
49
54
 
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
55
  def _boolean_gates() -> List[Tuple[str, Any]]:
58
56
  """``[(feature id, gate)]`` — imports are here, not at module import."""
59
57
  from importlib import import_module
@@ -80,15 +78,11 @@ def get_feature_toggle_service(
80
78
  with _LOCK:
81
79
  if _SHARED is None:
82
80
  _SHARED = FeatureToggleService(
83
- data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
81
+ data_dir=singleton_data_dir(data_dir),
84
82
  audit=audit,
85
83
  )
86
84
  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
85
+ return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
92
86
 
93
87
 
94
88
  def bind_feature_gates(service: Optional[FeatureToggleService] = None) -> None:
@@ -11,6 +11,10 @@ from latticeai.core.network_boundary import (
11
11
  DEFAULT_NETWORK_MODE,
12
12
  normalize_network_mode,
13
13
  )
14
+ from latticeai.runtime.service_singletons import (
15
+ rebind_singleton,
16
+ singleton_data_dir,
17
+ )
14
18
  from latticeai.services.hybrid_policy import HybridPolicyService
15
19
  from latticeai.services.network_boundary_service import NetworkBoundaryService
16
20
 
@@ -19,13 +23,6 @@ _SHARED: Optional[NetworkBoundaryService] = None
19
23
  _POLICY: Optional[HybridPolicyService] = None
20
24
 
21
25
 
22
- def _default_data_dir() -> Path:
23
- raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
24
- if raw:
25
- return Path(raw)
26
- return Path.home() / ".ltcai"
27
-
28
-
29
26
  def _env_default_mode() -> Any:
30
27
  return normalize_network_mode(
31
28
  os.environ.get("LATTICEAI_NETWORK_MODE", DEFAULT_NETWORK_MODE.value)
@@ -41,16 +38,12 @@ def get_network_boundary_service(
41
38
  with _LOCK:
42
39
  if _SHARED is None:
43
40
  _SHARED = NetworkBoundaryService(
44
- data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
41
+ data_dir=singleton_data_dir(data_dir),
45
42
  default_mode=_env_default_mode(),
46
43
  audit=audit,
47
44
  )
48
45
  return _SHARED
49
- if data_dir is not None:
50
- _SHARED.rebind_data_dir(Path(data_dir))
51
- if audit is not None:
52
- _SHARED.rebind_audit(audit)
53
- return _SHARED
46
+ return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
54
47
 
55
48
 
56
49
  def get_hybrid_policy_service(
@@ -62,15 +55,11 @@ def get_hybrid_policy_service(
62
55
  with _LOCK:
63
56
  if _POLICY is None:
64
57
  _POLICY = HybridPolicyService(
65
- data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
58
+ data_dir=singleton_data_dir(data_dir),
66
59
  audit=audit,
67
60
  )
68
61
  return _POLICY
69
- if data_dir is not None:
70
- _POLICY.rebind_data_dir(Path(data_dir))
71
- if audit is not None:
72
- _POLICY.rebind_audit(audit)
73
- return _POLICY
62
+ return rebind_singleton(_POLICY, data_dir=data_dir, audit=audit)
74
63
 
75
64
 
76
65
  def resolve_active_network_mode(
@@ -15,19 +15,16 @@ from pathlib import Path
15
15
  from typing import Any, Callable, Optional
16
16
 
17
17
  from latticeai.core.permission_mode import DEFAULT_MODE, normalize_mode
18
+ from latticeai.runtime.service_singletons import (
19
+ rebind_singleton,
20
+ singleton_data_dir,
21
+ )
18
22
  from latticeai.services.permission_mode_service import PermissionModeService
19
23
 
20
24
  _LOCK = threading.Lock()
21
25
  _SHARED: Optional[PermissionModeService] = None
22
26
 
23
27
 
24
- def _default_data_dir() -> Path:
25
- raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
26
- if raw:
27
- return Path(raw)
28
- return Path.home() / ".ltcai"
29
-
30
-
31
28
  def _env_default_mode() -> Any:
32
29
  return normalize_mode(os.environ.get("LATTICEAI_PERMISSION_MODE", DEFAULT_MODE.value))
33
30
 
@@ -48,16 +45,12 @@ def get_permission_mode_service(
48
45
  with _LOCK:
49
46
  if _SHARED is None:
50
47
  _SHARED = PermissionModeService(
51
- data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
48
+ data_dir=singleton_data_dir(data_dir),
52
49
  default_mode=_env_default_mode(),
53
50
  audit=audit,
54
51
  )
55
52
  return _SHARED
56
- if data_dir is not None:
57
- _SHARED.rebind_data_dir(Path(data_dir))
58
- if audit is not None:
59
- _SHARED.rebind_audit(audit)
60
- return _SHARED
53
+ return rebind_singleton(_SHARED, data_dir=data_dir, audit=audit)
61
54
 
62
55
 
63
56
  def resolve_active_permission_mode(