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
@@ -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
 
@@ -16,6 +16,7 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
16
16
  """Workspace platform, automation, review queue, command centre, proposals."""
17
17
  ctx.enter("platform_features")
18
18
 
19
+ from latticeai.api.agent_worker_seam import create_agent_worker_seam_router
19
20
  from latticeai.api.agents import create_agents_router
20
21
  from latticeai.api.automation_intelligence import (
21
22
  create_automation_intelligence_router,
@@ -48,9 +49,12 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
48
49
  from latticeai.services.chronicle import ChronicleService
49
50
  from latticeai.services.command_center import CommandCenterService
50
51
  from latticeai.services.evidence_actions import EvidenceActionService
51
- from latticeai.services.tool_dispatch import get_tool_permission
52
+ from latticeai.services.tool_dispatch import (
53
+ DEFAULT_TOOL_DISPATCH_SERVICE,
54
+ get_tool_permission,
55
+ )
52
56
  from latticeai.services.voice_capture import VoiceCaptureService
53
- from latticeai.tools import resolve_workspace_path
57
+ from latticeai.tools import execute_tool, resolve_workspace_path
54
58
 
55
59
  # v2 Agentic Workspace Platform: cross-system wiring.
56
60
  platform_automation_runtime = build_platform_automation_runtime(
@@ -210,6 +214,23 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
210
214
  )
211
215
  )
212
216
 
217
+ # AI-Worker seam (v11.5.1, plan §Y1): the three calls the Rust agent loop
218
+ # makes back into Python once orchestration lives in ``lattice-agent``.
219
+ # Registered here because it needs the change governor above; the two
220
+ # side-effecting routes stay behind ``LATTICEAI_AGENT_TOOL_SEAM=1``, which
221
+ # only lattice-host injects into a worker it started.
222
+ ctx.app.include_router(
223
+ create_agent_worker_seam_router(
224
+ model_router=ctx.model_router,
225
+ dispatch_service=DEFAULT_TOOL_DISPATCH_SERVICE,
226
+ execute_tool=execute_tool,
227
+ hooks=ctx.HOOKS_REGISTRY,
228
+ change_proposals=ctx.CHANGE_PROPOSALS,
229
+ require_user=ctx.require_user,
230
+ enforce_rate_limit=ctx.enforce_rate_limit,
231
+ )
232
+ )
233
+
213
234
  # Evidence → action (v9.9.6): an answer's citations become ready-to-send,
214
235
  # evidence-scoped follow-ups. Deterministic composition only — execution
215
236
  # stays on the chat/file-generation path.
@@ -284,6 +305,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
284
305
  from latticeai.core.model_compat import (
285
306
  list_cached_profiles as _list_compat_profiles,
286
307
  )
308
+ from latticeai.runtime.access_runtime import is_externally_reachable
287
309
  from latticeai.runtime.chat_wiring import build_interaction_contexts
288
310
  from latticeai.runtime.model_wiring import register_model_runtime_routers
289
311
  from latticeai.runtime.platform_services_runtime import build_brain_network
@@ -319,6 +341,7 @@ def phase_interaction(ctx: RuntimeContext) -> None:
319
341
  engine_status=service.engine_status,
320
342
  get_current_user=ctx.get_current_user,
321
343
  require_auth=ctx.REQUIRE_AUTH,
344
+ externally_reachable=is_externally_reachable(ctx.CONFIG),
322
345
  app_version=ctx.APP_VERSION,
323
346
  app_mode=ctx.APP_MODE,
324
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(
@@ -83,6 +83,7 @@ def build_auth_admin_security_router_bundle(
83
83
  secure_cookies: bool,
84
84
  invite_authorized: Any,
85
85
  ensure_identity: Any,
86
+ sso_default_redirect_uri: str,
86
87
  create_admin_router: Any,
87
88
  require_admin: Any,
88
89
  get_history: Any,
@@ -138,6 +139,7 @@ def build_auth_admin_security_router_bundle(
138
139
  invite_gate_enabled=invite_gate_enabled,
139
140
  invite_authorized=invite_authorized,
140
141
  ensure_identity=ensure_identity,
142
+ default_redirect_uri=sso_default_redirect_uri,
141
143
  )
142
144
 
143
145
  def graph_stats_safe() -> dict[str, Any]:
@@ -376,6 +378,7 @@ def register_health_and_model_routers(
376
378
  engine_status: Any,
377
379
  get_current_user: Any,
378
380
  require_auth: bool,
381
+ externally_reachable: bool,
379
382
  app_version: str,
380
383
  app_mode: str,
381
384
  create_models_router: Any,
@@ -411,6 +414,7 @@ def register_health_and_model_routers(
411
414
  engine_status=engine_status,
412
415
  get_current_user=get_current_user,
413
416
  require_auth=require_auth,
417
+ externally_reachable=externally_reachable,
414
418
  app_version=app_version,
415
419
  app_mode=app_mode,
416
420
  ),
@@ -25,7 +25,7 @@ Two properties keep it honest:
25
25
 
26
26
  from __future__ import annotations
27
27
 
28
- from typing import Any, Dict, List, Set
28
+ from typing import Any, Dict, Set
29
29
 
30
30
 
31
31
  class RuntimeContext:
@@ -257,21 +257,9 @@ class RuntimeContext:
257
257
  f"RuntimeContext.{name} was read before it was built{hint}"
258
258
  ) from None
259
259
 
260
- @property
261
- def produced_by(self) -> Dict[str, str]:
262
- """Attribute name → the phase that published it, for the order test."""
263
- return dict(self._produced)
264
-
265
260
  def names(self) -> Set[str]:
266
261
  return set(self._produced)
267
262
 
268
- def phases_run(self) -> List[str]:
269
- seen: List[str] = []
270
- for phase in self._produced.values():
271
- if phase not in seen:
272
- seen.append(phase)
273
- return seen
274
-
275
263
  def __repr__(self) -> str: # pragma: no cover - debugging aid
276
264
  return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
277
265
 
@@ -0,0 +1,55 @@
1
+ """The rebinding half of the process-wide service singletons.
2
+
3
+ Four wiring modules create one service each — the permission dial, the network
4
+ boundary, the hybrid policy, the feature switchboard — and all four had the
5
+ same problem and the same four-line answer to it: a lazy first caller (a tool
6
+ dispatch, a feature gate) can ask for the service *before* routers know the
7
+ configured data dir or the audit sink, and if the later explicit call were
8
+ allowed to be a no-op the service would stay pinned to the fallback path with
9
+ its audit events going nowhere.
10
+
11
+ So explicit arguments rebind rather than being dropped. That rule was written
12
+ out four times; it lives here once, because "the second caller silently loses"
13
+ is precisely the kind of divergence that produces a second, invisible copy of
14
+ the user's data and no failing test.
15
+
16
+ The singleton *slot* deliberately stays a module global in each wiring module:
17
+ the modules are the unit of process state, and the suite resets them by
18
+ assigning ``_SHARED = None``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+ from typing import Any, Callable, Optional, TypeVar
25
+
26
+ from latticeai.core.config import default_data_dir
27
+
28
+ ServiceT = TypeVar("ServiceT")
29
+
30
+ __all__ = ["singleton_data_dir", "rebind_singleton"]
31
+
32
+
33
+ def singleton_data_dir(data_dir: Optional[Path]) -> Path:
34
+ """Where a *first* lazy caller should put the store."""
35
+ return Path(data_dir) if data_dir is not None else default_data_dir()
36
+
37
+
38
+ def rebind_singleton(
39
+ service: ServiceT,
40
+ *,
41
+ data_dir: Optional[Path] = None,
42
+ audit: Optional[Callable[..., None]] = None,
43
+ ) -> ServiceT:
44
+ """Apply explicit arguments to an already-created singleton, then return it.
45
+
46
+ ``None`` means "the caller did not say", never "unset it" — a router that
47
+ mounts without an audit sink must not silence a sink an earlier caller
48
+ already supplied.
49
+ """
50
+ rebindable: Any = service
51
+ if data_dir is not None:
52
+ rebindable.rebind_data_dir(Path(data_dir))
53
+ if audit is not None:
54
+ rebindable.rebind_audit(audit)
55
+ return service
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
16
16
 
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
- ARCHITECTURE_VERSION_TARGET = "11.5.0"
19
+ ARCHITECTURE_VERSION_TARGET = "11.5.2"
20
20
 
21
21
  PREFERRED_REFACTORING_ORDER = [
22
22
  "agent-runtime",
@@ -31,11 +31,6 @@ class BrainProposalsMixin(_Core):
31
31
  # is absent they report ``available: False`` rather than falling back to a
32
32
  # direct write.
33
33
 
34
- def attach_review_queue(self, review_queue: Any) -> None:
35
- """Bind the review queue proposals are written to (composition root)."""
36
- self._review_queue_service = review_queue
37
- self._synthesizer = None
38
-
39
34
  def _review_queue(self) -> Any:
40
35
  """The queue to propose into, or ``None`` when there is none.
41
36
 
@@ -21,7 +21,6 @@ change audit log. The classification itself lives in
21
21
  from __future__ import annotations
22
22
 
23
23
  import difflib
24
- import hashlib
25
24
  import logging
26
25
  import os
27
26
  import tempfile
@@ -30,6 +29,7 @@ from pathlib import Path
30
29
  from typing import Any, Callable, Dict, List, Optional, Tuple
31
30
 
32
31
  from latticeai.core.quiet import quiet
32
+ from latticeai.core.security import sha256_hex
33
33
  from latticeai.core.tool_governor import classify_tool_call
34
34
  from latticeai.core.workspace_reorganization import (
35
35
  REORG_KIND,
@@ -90,7 +90,7 @@ class ProposalConflictError(ValueError):
90
90
 
91
91
 
92
92
  def _sha256_text(content: str) -> str:
93
- return hashlib.sha256(content.encode("utf-8")).hexdigest()
93
+ return sha256_hex(content)
94
94
 
95
95
 
96
96
  def _unified_diff(before: str, after: str, path: str) -> List[str]:
@@ -272,40 +272,6 @@ class ChangeProposalService:
272
272
  )
273
273
  return item
274
274
 
275
- def propose_file_delete(
276
- self,
277
- *,
278
- path: str,
279
- proposed_by: str = "agent",
280
- reason: str = "",
281
- user_email: Optional[str] = None,
282
- workspace_id: Optional[str] = None,
283
- ) -> Dict[str, Any]:
284
- base_exists, before = self._snapshot(path)
285
- item = self._review_queue.create(
286
- title=f"파일 삭제 제안: {path}",
287
- summary=reason or "기존 파일을 삭제하는 작업이라 검토 후 적용됩니다.",
288
- source="change_proposal",
289
- kind="file_delete",
290
- payload={
291
- "path": path,
292
- "diff": _unified_diff(before, "", path),
293
- "tier": "large",
294
- "before_bytes": len(before.encode("utf-8")),
295
- "after_bytes": 0,
296
- "base_exists": base_exists,
297
- "base_sha256": _sha256_text(before) if base_exists else "",
298
- },
299
- provenance={"proposed_by": proposed_by, "reason": reason},
300
- user_email=user_email,
301
- workspace_id=workspace_id,
302
- )
303
- self._audit(
304
- "change_proposal_created", user_email=user_email,
305
- proposal_id=item.get("id"), path=path, kind="file_delete", tier="large",
306
- )
307
- return item
308
-
309
275
  def propose_reorganization(
310
276
  self,
311
277
  *,
@@ -48,6 +48,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
48
48
 
49
49
  from latticeai.core.timeutil import parse_iso
50
50
  from latticeai.core.timezones import get_timezone
51
+ from latticeai.core.workspace_os_utils import graph_scope_kwargs
51
52
 
52
53
  LOGGER = logging.getLogger(__name__)
53
54
 
@@ -249,12 +250,9 @@ def _empty_lanes() -> Dict[str, int]:
249
250
  return {lane: 0 for lane in _LANES}
250
251
 
251
252
 
252
- def _scope_kwargs(workspace_id: Optional[str]) -> Dict[str, Any]:
253
- """The scoped-read kwargs the graph store expects (house pattern)."""
254
- return {
255
- "allowed_workspaces": {workspace_id} if workspace_id is not None else None,
256
- "include_legacy_global": workspace_id is None,
257
- }
253
+
254
+ #: Historical module-local name for the shared rule.
255
+ _scope_kwargs = graph_scope_kwargs
258
256
 
259
257
 
260
258
  def _on_day(rows: Sequence[Any], day: str) -> List[Any]:
@@ -28,6 +28,7 @@ from typing import Any, Dict, List, Optional
28
28
 
29
29
  from latticeai.core.timeutil import local_now, parse_iso
30
30
  from latticeai.core.timeutil import now_iso as _now
31
+ from latticeai.core.workspace_os_utils import graph_scope_kwargs
31
32
 
32
33
  LOGGER = logging.getLogger(__name__)
33
34
 
@@ -48,6 +49,8 @@ def _clip(text: Any, limit: int = 160) -> str:
48
49
  return value[:limit]
49
50
 
50
51
 
52
+
53
+
51
54
  class CommandCenterService:
52
55
  def __init__(
53
56
  self,
@@ -73,10 +76,7 @@ class CommandCenterService:
73
76
  # ── scoped reads ─────────────────────────────────────────────────────
74
77
 
75
78
  def _scope_kwargs(self, workspace_id: Optional[str]) -> Dict[str, Any]:
76
- return {
77
- "allowed_workspaces": {workspace_id} if workspace_id is not None else None,
78
- "include_legacy_global": workspace_id is None,
79
- }
79
+ return graph_scope_kwargs(workspace_id)
80
80
 
81
81
  def _history(
82
82
  self, *, user_email: Optional[str], workspace_id: Optional[str], limit: int
@@ -28,6 +28,8 @@ import logging
28
28
  import re
29
29
  from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
30
30
 
31
+ from latticeai.core.messages import bilingual
32
+
31
33
  LOGGER = logging.getLogger(__name__)
32
34
 
33
35
  __all__ = ["EvidenceActionService", "EVIDENCE_ACTIONS"]
@@ -38,8 +40,9 @@ _EXCERPT_CHARS = 600
38
40
  _MAX_SOURCES = 8
39
41
 
40
42
 
41
- def _phrase(ko: str, en: str) -> Dict[str, str]:
42
- return {"ko": ko, "en": en}
43
+
44
+ #: Historical module-local name for the shared phrase-pair helper.
45
+ _phrase = bilingual
43
46
 
44
47
 
45
48
  # The action catalog is closed and deterministic — the UI renders exactly
@@ -13,7 +13,6 @@ auto-commit — so a headless caller stages nothing rather than writing.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- import json
17
16
  import logging
18
17
  from typing import Any, AsyncIterator, Dict, Optional
19
18
 
@@ -21,6 +20,7 @@ from latticeai.core.network_boundary import (
21
20
  NetworkBoundaryMode,
22
21
  normalize_network_mode,
23
22
  )
23
+ from latticeai.core.sse import sse_frame
24
24
  from latticeai.services.cloud_egress_audit import record_cloud_egress
25
25
  from latticeai.services.cloud_extraction import plan_kg_expansion_rich
26
26
  from latticeai.services.cloud_streaming import (
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
36
36
 
37
37
 
38
38
  def _sse(data: Dict[str, Any]) -> str:
39
- return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
39
+ return sse_frame(None, data)
40
40
 
41
41
 
42
42
  def _scope_key(user_email: Optional[str], workspace_id: Optional[str]) -> str: