ltcai 10.0.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +43 -32
  2. package/docs/CHANGELOG.md +204 -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 +101 -0
  6. package/docs/MYPY_BACKLOG.md +98 -0
  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/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/archive.py +5 -4
  14. package/lattice_brain/conversations.py +14 -3
  15. package/lattice_brain/embeddings.py +12 -2
  16. package/lattice_brain/graph/_kg_common.py +5 -5
  17. package/lattice_brain/graph/_kg_fsutil.py +5 -4
  18. package/lattice_brain/graph/discovery.py +4 -3
  19. package/lattice_brain/graph/discovery_index.py +0 -1
  20. package/lattice_brain/graph/documents.py +3 -2
  21. package/lattice_brain/graph/fusion.py +4 -0
  22. package/lattice_brain/graph/ingest.py +12 -2
  23. package/lattice_brain/graph/projection.py +3 -2
  24. package/lattice_brain/graph/provenance.py +5 -3
  25. package/lattice_brain/graph/rerank.py +4 -1
  26. package/lattice_brain/graph/retrieval.py +0 -1
  27. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  28. package/lattice_brain/graph/retrieval_reads.py +0 -1
  29. package/lattice_brain/graph/retrieval_vector.py +0 -1
  30. package/lattice_brain/graph/schema.py +4 -3
  31. package/lattice_brain/graph/store.py +18 -4
  32. package/lattice_brain/graph/write_master.py +46 -1
  33. package/lattice_brain/ingestion.py +4 -1
  34. package/lattice_brain/portability.py +5 -2
  35. package/lattice_brain/quality.py +12 -5
  36. package/lattice_brain/quiet.py +43 -0
  37. package/lattice_brain/runtime/agent_runtime.py +19 -10
  38. package/lattice_brain/runtime/hooks.py +3 -2
  39. package/lattice_brain/runtime/multi_agent.py +2 -3
  40. package/lattice_brain/sensitivity.py +94 -0
  41. package/lattice_brain/storage/base.py +30 -2
  42. package/lattice_brain/storage/migration.py +3 -2
  43. package/lattice_brain/storage/postgres.py +2 -2
  44. package/lattice_brain/storage/sqlite.py +6 -4
  45. package/lattice_brain/workflow.py +4 -2
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agents.py +4 -2
  49. package/latticeai/api/auth.py +5 -1
  50. package/latticeai/api/automation_intelligence.py +2 -1
  51. package/latticeai/api/browser.py +3 -2
  52. package/latticeai/api/chat.py +28 -17
  53. package/latticeai/api/chat_agent_http.py +22 -8
  54. package/latticeai/api/chat_contracts.py +4 -0
  55. package/latticeai/api/chat_documents.py +6 -2
  56. package/latticeai/api/chat_hybrid.py +82 -0
  57. package/latticeai/api/computer_use.py +8 -3
  58. package/latticeai/api/knowledge_graph.py +1 -1
  59. package/latticeai/api/mcp.py +4 -4
  60. package/latticeai/api/models.py +5 -2
  61. package/latticeai/api/network_boundary.py +220 -0
  62. package/latticeai/api/permissions.py +0 -1
  63. package/latticeai/api/realtime.py +1 -1
  64. package/latticeai/api/security_dashboard.py +1 -1
  65. package/latticeai/api/setup.py +16 -3
  66. package/latticeai/api/static_routes.py +2 -1
  67. package/latticeai/api/tools.py +12 -8
  68. package/latticeai/api/voice_capture.py +3 -1
  69. package/latticeai/api/workflow_designer.py +1 -1
  70. package/latticeai/api/workspace.py +1 -2
  71. package/latticeai/app_factory.py +174 -145
  72. package/latticeai/cli/entrypoint.py +6 -4
  73. package/latticeai/core/agent.py +55 -495
  74. package/latticeai/core/agent_eval.py +2 -2
  75. package/latticeai/core/agent_helpers.py +493 -0
  76. package/latticeai/core/agent_prompts.py +0 -1
  77. package/latticeai/core/agent_registry.py +3 -1
  78. package/latticeai/core/agent_state.py +41 -0
  79. package/latticeai/core/audit.py +1 -1
  80. package/latticeai/core/builtin_hooks.py +2 -1
  81. package/latticeai/core/config.py +0 -1
  82. package/latticeai/core/embedding_providers.py +12 -1
  83. package/latticeai/core/file_generation.py +3 -0
  84. package/latticeai/core/invitations.py +4 -1
  85. package/latticeai/core/io_utils.py +3 -1
  86. package/latticeai/core/legacy_compatibility.py +1 -2
  87. package/latticeai/core/local_embeddings.py +12 -1
  88. package/latticeai/core/marketplace.py +1 -2
  89. package/latticeai/core/mcp_registry.py +0 -1
  90. package/latticeai/core/model_compat.py +1 -1
  91. package/latticeai/core/model_resolution.py +1 -1
  92. package/latticeai/core/network_boundary.py +168 -0
  93. package/latticeai/core/oidc.py +3 -0
  94. package/latticeai/core/permission_mode.py +6 -6
  95. package/latticeai/core/plugins.py +3 -2
  96. package/latticeai/core/policy.py +0 -1
  97. package/latticeai/core/quiet.py +84 -0
  98. package/latticeai/core/realtime.py +3 -2
  99. package/latticeai/core/run_store.py +4 -2
  100. package/latticeai/core/security.py +4 -0
  101. package/latticeai/core/users.py +5 -3
  102. package/latticeai/core/workspace_graph_trace.py +3 -0
  103. package/latticeai/core/workspace_os.py +65 -273
  104. package/latticeai/core/workspace_os_constants.py +126 -0
  105. package/latticeai/core/workspace_os_state.py +180 -0
  106. package/latticeai/core/workspace_os_utils.py +3 -1
  107. package/latticeai/core/workspace_permissions.py +1 -0
  108. package/latticeai/core/workspace_snapshots.py +3 -1
  109. package/latticeai/core/workspace_timeline.py +3 -1
  110. package/latticeai/integrations/telegram_bot.py +25 -16
  111. package/latticeai/models/router.py +6 -3
  112. package/latticeai/runtime/access_runtime.py +3 -1
  113. package/latticeai/runtime/audit_runtime.py +3 -2
  114. package/latticeai/runtime/chat_wiring.py +4 -1
  115. package/latticeai/runtime/history_runtime.py +1 -1
  116. package/latticeai/runtime/history_writer.py +138 -0
  117. package/latticeai/runtime/lifespan_runtime.py +3 -1
  118. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  119. package/latticeai/runtime/persistence_runtime.py +3 -1
  120. package/latticeai/runtime/router_registration.py +11 -1
  121. package/latticeai/services/architecture_readiness.py +1 -2
  122. package/latticeai/services/change_proposals.py +2 -1
  123. package/latticeai/services/cloud_egress_audit.py +85 -0
  124. package/latticeai/services/cloud_extraction.py +129 -0
  125. package/latticeai/services/cloud_streaming.py +266 -0
  126. package/latticeai/services/cloud_token_guard.py +84 -0
  127. package/latticeai/services/command_center.py +2 -1
  128. package/latticeai/services/folder_watch.py +3 -0
  129. package/latticeai/services/funnel_metrics.py +3 -1
  130. package/latticeai/services/hybrid_chat.py +265 -0
  131. package/latticeai/services/hybrid_context.py +228 -0
  132. package/latticeai/services/hybrid_policy.py +178 -0
  133. package/latticeai/services/memory_service.py +1 -1
  134. package/latticeai/services/model_catalog.py +10 -1
  135. package/latticeai/services/model_engines.py +35 -14
  136. package/latticeai/services/model_loading.py +3 -2
  137. package/latticeai/services/model_runtime.py +68 -17
  138. package/latticeai/services/multimodal_streaming.py +123 -0
  139. package/latticeai/services/network_boundary_service.py +154 -0
  140. package/latticeai/services/openai_compatible_adapter.py +100 -0
  141. package/latticeai/services/p_reinforce.py +4 -0
  142. package/latticeai/services/platform_runtime.py +9 -4
  143. package/latticeai/services/process_audit.py +0 -1
  144. package/latticeai/services/product_readiness.py +1 -1
  145. package/latticeai/services/run_executor.py +3 -2
  146. package/latticeai/services/search_service.py +1 -4
  147. package/latticeai/services/setup_detection.py +2 -1
  148. package/latticeai/services/tool_dispatch.py +7 -2
  149. package/latticeai/services/upload_service.py +2 -1
  150. package/latticeai/setup/auto_setup.py +10 -7
  151. package/latticeai/setup/wizard.py +15 -11
  152. package/latticeai/tools/__init__.py +3 -3
  153. package/latticeai/tools/commands.py +7 -7
  154. package/latticeai/tools/computer.py +3 -2
  155. package/latticeai/tools/documents.py +10 -9
  156. package/latticeai/tools/filesystem.py +7 -4
  157. package/latticeai/tools/knowledge.py +2 -0
  158. package/latticeai/tools/local_files.py +1 -1
  159. package/latticeai/tools/network.py +2 -1
  160. package/package.json +7 -3
  161. package/scripts/bench_agent_smoke.py +0 -1
  162. package/scripts/bench_models.py +0 -1
  163. package/scripts/brain_quality_eval.py +7 -2
  164. package/scripts/bump_version.py +2 -1
  165. package/scripts/check_current_release_docs.mjs +1 -1
  166. package/scripts/migrate_brain_storage.py +5 -1
  167. package/scripts/profile_kg.py +2 -7
  168. package/scripts/verify_hf_model_registry.py +2 -2
  169. package/src-tauri/Cargo.lock +1 -1
  170. package/src-tauri/Cargo.toml +1 -1
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +37 -37
  173. package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
  174. package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
  175. package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
  176. package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
  177. package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
  178. package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
  179. package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
  180. package/static/app/assets/Library-CXnbNiQW.js +1 -0
  181. package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
  182. package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
  183. package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
  184. package/static/app/assets/System-Bp9ZY4ER.js +1 -0
  185. package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
  186. package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
  187. package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
  188. package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
  189. package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
  190. package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
  191. package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
  192. package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
  193. package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
  194. package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
  195. package/static/app/assets/index-CEkL4_M-.css +2 -0
  196. package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
  197. package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
  198. package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
  199. package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
  200. package/static/app/assets/search-CwEJWBEA.js +1 -0
  201. package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
  202. package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
  203. package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
  204. package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
  205. package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
  206. package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
  207. package/static/app/assets/workspace-BMUEiXUm.js +1 -0
  208. package/static/app/index.html +4 -4
  209. package/static/sw.js +1 -1
  210. package/static/app/assets/Library-DhvoPvC7.js +0 -1
  211. package/static/app/assets/System-CSMdYLMy.js +0 -1
  212. package/static/app/assets/index-FxDusbr0.css +0 -2
  213. package/static/app/assets/search-DhbSgW6m.js +0 -1
  214. package/static/app/assets/workspace-DBPB0jkX.js +0 -1
@@ -77,7 +77,18 @@ class LocalEmbeddingModel:
77
77
  return [value / norm for value in vector]
78
78
 
79
79
  def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
80
- return float(sum(a * b for a, b in zip(left, right)))
80
+ # strict=True: a dimension mismatch means the two vectors came from
81
+ # different embedding models. Truncating to the shorter one produces a
82
+ # plausible-looking similarity that is meaningless — exactly the silent
83
+ # wrongness this codebase keeps finding. Callers that can hit a model
84
+ # swap already handle failure and fall back to lexical search.
85
+ left_v, right_v = list(left), list(right)
86
+ if len(left_v) != len(right_v):
87
+ raise ValueError(
88
+ f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
89
+ "the vector index was built with a different model"
90
+ )
91
+ return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
81
92
 
82
93
  def encode(self, vector: Iterable[float]) -> bytes:
83
94
  values = list(vector)
@@ -10,8 +10,7 @@ from __future__ import annotations
10
10
  from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
-
14
- MARKETPLACE_VERSION = "10.0.0"
13
+ MARKETPLACE_VERSION = "10.3.0"
15
14
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
16
15
 
17
16
 
@@ -17,7 +17,6 @@ import httpx
17
17
  from fastapi import HTTPException
18
18
 
19
19
  # ── MCP Registry (built-in tool definitions) ─────────────────────────────────
20
-
21
20
  # Built-in MCP server catalog data → .mcp_catalog (re-exported).
22
21
  from .mcp_catalog import MCP_REGISTRY # noqa: F401
23
22
 
@@ -18,7 +18,7 @@ import logging
18
18
  import re
19
19
  import threading
20
20
  import time
21
- from dataclasses import dataclass, asdict
21
+ from dataclasses import asdict, dataclass
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, List, Optional, Tuple
24
24
 
@@ -19,7 +19,7 @@
19
19
  from __future__ import annotations
20
20
 
21
21
  import logging
22
- from dataclasses import dataclass, field, asdict
22
+ from dataclasses import asdict, dataclass, field
23
23
  from enum import Enum
24
24
  from typing import Any, Dict, List, Optional
25
25
 
@@ -0,0 +1,168 @@
1
+ """Network boundary modes for hybrid local + cloud LLM usage.
2
+
3
+ Local-first principle: the Knowledge Graph and all durable memory stay on the
4
+ machine. Cloud LLMs are an explicit, opt-in worker. This module defines the
5
+ dial that controls whether any knowledge is allowed to leave the host.
6
+
7
+ Modes
8
+ -----
9
+ * ``local_only`` — default. No chat context is ever sent to a cloud provider.
10
+ * ``cloud_allowed`` — user has explicitly opted in. Only the *minimal* related
11
+ nodes selected by the hybrid context extractor may be sent. Sensitive types
12
+ and hard filters still apply (mode-invariant).
13
+
14
+ The mode is orthogonal to PermissionMode (agent autonomy). A session can be
15
+ ``cloud_allowed`` + ``strict`` at the same time.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from enum import Enum
21
+ from typing import Any, Dict, FrozenSet, Mapping, Optional
22
+
23
+
24
+ class NetworkBoundaryMode(str, Enum):
25
+ LOCAL_ONLY = "local_only"
26
+ CLOUD_ALLOWED = "cloud_allowed"
27
+
28
+
29
+ DEFAULT_NETWORK_MODE = NetworkBoundaryMode.LOCAL_ONLY
30
+
31
+ # Node types that must never be included in a cloud payload, regardless of mode.
32
+ # Extend carefully; this is a hard circuit breaker.
33
+ #
34
+ # This was empty through 10.1.x ("keep empty for v1 scaffolding"), which meant
35
+ # the type arm of the filter could never fire. Combined with nothing being able
36
+ # to *set* the metadata flags below, the whole guard was unreachable — correct
37
+ # code protecting nothing.
38
+ HARD_BLOCK_NODE_TYPES: FrozenSet[str] = frozenset({
39
+ "Credential",
40
+ "Secret",
41
+ "ApiKey",
42
+ "Token",
43
+ "Password",
44
+ "PrivateKey",
45
+ })
46
+
47
+ # Metadata keys that, when present and truthy, block a node from leaving.
48
+ HARD_BLOCK_METADATA_FLAGS: FrozenSet[str] = frozenset({
49
+ "sensitive",
50
+ "private",
51
+ "do_not_share",
52
+ "local_only",
53
+ })
54
+
55
+ # Path-based never-leaves rules live in Brain Core: they describe the data, not
56
+ # the transport, and Brain Core cannot import from this package. Re-exported
57
+ # here so the boundary module stays the single place the app layer reads.
58
+ from lattice_brain.sensitivity import ( # noqa: E402
59
+ SENSITIVE_FILENAMES,
60
+ SENSITIVE_PATH_FRAGMENTS,
61
+ sensitive_reason_for_path,
62
+ )
63
+
64
+
65
+ def normalize_network_mode(value: Any) -> NetworkBoundaryMode:
66
+ """Parse user/API/env input into a NetworkBoundaryMode; unknown → local_only."""
67
+ if isinstance(value, NetworkBoundaryMode):
68
+ return value
69
+ text = str(value or "").strip().lower()
70
+ aliases = {
71
+ "local_only": NetworkBoundaryMode.LOCAL_ONLY,
72
+ "local": NetworkBoundaryMode.LOCAL_ONLY,
73
+ "local-only": NetworkBoundaryMode.LOCAL_ONLY,
74
+ "offline": NetworkBoundaryMode.LOCAL_ONLY,
75
+ "cloud_allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
76
+ "cloud": NetworkBoundaryMode.CLOUD_ALLOWED,
77
+ "cloud-allowed": NetworkBoundaryMode.CLOUD_ALLOWED,
78
+ "hybrid": NetworkBoundaryMode.CLOUD_ALLOWED,
79
+ "online": NetworkBoundaryMode.CLOUD_ALLOWED,
80
+ }
81
+ return aliases.get(text, DEFAULT_NETWORK_MODE)
82
+
83
+
84
+ def network_mode_catalog() -> list[Dict[str, Any]]:
85
+ """UI/API catalog for the network boundary selector."""
86
+ return [
87
+ {
88
+ "id": NetworkBoundaryMode.LOCAL_ONLY.value,
89
+ "label": "Local only",
90
+ "label_ko": "로컬만",
91
+ "summary": "Nothing leaves this machine. Answers use local models and the local Brain only.",
92
+ "summary_ko": "이 컴퓨터를 벗어나지 않습니다. 로컬 모델과 로컬 Brain만 사용합니다.",
93
+ "risk": "low",
94
+ "requires_ack": False,
95
+ },
96
+ {
97
+ "id": NetworkBoundaryMode.CLOUD_ALLOWED.value,
98
+ "label": "Cloud streaming allowed",
99
+ "label_ko": "클라우드 스트리밍 허용",
100
+ "summary": (
101
+ "Minimal related Knowledge Graph nodes may be sent to a cloud LLM. "
102
+ "The streamed answer is written back into the local Brain with provenance."
103
+ ),
104
+ "summary_ko": (
105
+ "관련된 최소 Knowledge Graph 노드만 클라우드 LLM으로 전송될 수 있습니다. "
106
+ "스트리밍 답변은 provenance와 함께 로컬 Brain에 다시 기록됩니다."
107
+ ),
108
+ "risk": "medium",
109
+ "requires_ack": True,
110
+ "warning": (
111
+ "Cloud mode sends a compact summary of selected local nodes to an external provider. "
112
+ "Sensitive nodes remain blocked. You can switch back to Local only at any time."
113
+ ),
114
+ "warning_ko": (
115
+ "클라우드 모드는 선택된 로컬 노드의 압축 요약을 외부 제공자에게 전송합니다. "
116
+ "민감 노드는 계속 차단됩니다. 언제든지 로컬만으로 되돌릴 수 있습니다."
117
+ ),
118
+ },
119
+ ]
120
+
121
+
122
+ def is_node_blocked_for_cloud(node: Mapping[str, Any]) -> Optional[str]:
123
+ """Return a reason string when this node must never leave the host."""
124
+ node_type = str(node.get("type") or "")
125
+ if node_type in HARD_BLOCK_NODE_TYPES:
126
+ return f"node type {node_type!r} is blocked from cloud payloads"
127
+
128
+ meta = node.get("metadata") or {}
129
+ if not isinstance(meta, Mapping):
130
+ return None
131
+ for flag in HARD_BLOCK_METADATA_FLAGS:
132
+ if meta.get(flag):
133
+ return f"node flagged {flag!r} is blocked from cloud payloads"
134
+ return None
135
+
136
+
137
+ def network_mode_contract(mode: NetworkBoundaryMode | str) -> Dict[str, Any]:
138
+ """Serializable contract for API / agent / UI."""
139
+ mode = normalize_network_mode(mode)
140
+ entry = next(
141
+ (m for m in network_mode_catalog() if m["id"] == mode.value),
142
+ network_mode_catalog()[0],
143
+ )
144
+ return {
145
+ "mode": mode.value,
146
+ "label": entry["label"],
147
+ "label_ko": entry["label_ko"],
148
+ "risk": entry["risk"],
149
+ "requires_ack": entry["requires_ack"],
150
+ "allows_cloud": mode == NetworkBoundaryMode.CLOUD_ALLOWED,
151
+ "hard_block_node_types": sorted(HARD_BLOCK_NODE_TYPES),
152
+ "hard_block_metadata_flags": sorted(HARD_BLOCK_METADATA_FLAGS),
153
+ }
154
+
155
+
156
+ __all__ = [
157
+ "NetworkBoundaryMode",
158
+ "DEFAULT_NETWORK_MODE",
159
+ "HARD_BLOCK_NODE_TYPES",
160
+ "HARD_BLOCK_METADATA_FLAGS",
161
+ "normalize_network_mode",
162
+ "network_mode_catalog",
163
+ "is_node_blocked_for_cloud",
164
+ "sensitive_reason_for_path",
165
+ "SENSITIVE_PATH_FRAGMENTS",
166
+ "SENSITIVE_FILENAMES",
167
+ "network_mode_contract",
168
+ ]
@@ -25,6 +25,8 @@ import json
25
25
  import time
26
26
  from typing import Any, Dict, List, Optional
27
27
 
28
+ from latticeai.core.quiet import quiet
29
+
28
30
 
29
31
  class OIDCValidationError(Exception):
30
32
  """Raised when an OIDC ID token fails any validation step (fail-closed)."""
@@ -116,6 +118,7 @@ def _verify_signature(token: str, jwks: Any) -> Dict[str, Any]:
116
118
  public_key = _public_key_from_jwk(jwk)
117
119
  public_key.verify(signature, signing_input, padding.PKCS1v15(), hash_obj)
118
120
  except (InvalidSignature, OIDCValidationError):
121
+ quiet()
119
122
  continue
120
123
  # Signature verified — now it is safe to parse the claims.
121
124
  try:
@@ -22,7 +22,7 @@ Circuit breakers are mode-invariant: a mode never overrides a hard deny.
22
22
  from __future__ import annotations
23
23
 
24
24
  from enum import Enum
25
- from typing import Any, Dict, Mapping, Optional, Set
25
+ from typing import Any, Dict, FrozenSet, Mapping, Optional
26
26
 
27
27
 
28
28
  class PermissionMode(str, Enum):
@@ -34,7 +34,7 @@ class PermissionMode(str, Enum):
34
34
  DEFAULT_MODE = PermissionMode.STRICT
35
35
 
36
36
  # Computer-use split: observation is low-friction in trusted; control stays gated.
37
- COMPUTER_OBSERVATION_TOOLS: Set[str] = frozenset({
37
+ COMPUTER_OBSERVATION_TOOLS: FrozenSet[str] = frozenset({
38
38
  "computer_screenshot",
39
39
  "computer_status",
40
40
  "computer_use_status",
@@ -42,7 +42,7 @@ COMPUTER_OBSERVATION_TOOLS: Set[str] = frozenset({
42
42
  "vision_analyze",
43
43
  })
44
44
 
45
- COMPUTER_CONTROL_TOOLS: Set[str] = frozenset({
45
+ COMPUTER_CONTROL_TOOLS: FrozenSet[str] = frozenset({
46
46
  "computer_click",
47
47
  "computer_type",
48
48
  "computer_key",
@@ -53,7 +53,7 @@ COMPUTER_CONTROL_TOOLS: Set[str] = frozenset({
53
53
  "computer_open_url",
54
54
  })
55
55
 
56
- KNOWLEDGE_READ_TOOLS: Set[str] = frozenset({
56
+ KNOWLEDGE_READ_TOOLS: FrozenSet[str] = frozenset({
57
57
  "knowledge_search",
58
58
  "knowledge_tree",
59
59
  "obsidian_search",
@@ -63,7 +63,7 @@ KNOWLEDGE_READ_TOOLS: Set[str] = frozenset({
63
63
  "knowledge_graph_context",
64
64
  })
65
65
 
66
- WORKSPACE_WRITE_TOOLS: Set[str] = frozenset({
66
+ WORKSPACE_WRITE_TOOLS: FrozenSet[str] = frozenset({
67
67
  "write_file",
68
68
  "edit_file",
69
69
  "create_docx",
@@ -78,7 +78,7 @@ WORKSPACE_WRITE_TOOLS: Set[str] = frozenset({
78
78
  })
79
79
 
80
80
  # Always blocked regardless of mode (Claude-style circuit breakers).
81
- HARD_BLOCK_SANDBOXES: Set[str] = frozenset({"system"})
81
+ HARD_BLOCK_SANDBOXES: FrozenSet[str] = frozenset({"system"})
82
82
 
83
83
 
84
84
  def normalize_mode(value: Any) -> PermissionMode:
@@ -29,6 +29,7 @@ from dataclasses import dataclass, field
29
29
  from pathlib import Path
30
30
  from typing import Any, Callable, Dict, List, Optional, Tuple
31
31
 
32
+ from latticeai.core.quiet import quiet
32
33
 
33
34
  PLUGIN_SDK_VERSION = "2.2.0"
34
35
 
@@ -309,7 +310,7 @@ class PluginRegistry:
309
310
  register_skill(skill_name, plugin_id)
310
311
  registered_skills.append(skill_name)
311
312
  except Exception: # pragma: no cover - skill registration is best-effort
312
- pass
313
+ quiet()
313
314
  entry = {}
314
315
  if self.store is not None:
315
316
  entry = self.store.mark_plugin_installed(
@@ -366,7 +367,7 @@ class PluginRegistry:
366
367
  try:
367
368
  self.store.record_timeline_event("plugins", event_type, payload, workspace_id=workspace_id)
368
369
  except Exception:
369
- pass
370
+ quiet()
370
371
 
371
372
  emit("plugin_started", {"plugin_id": plugin_id, "action": action})
372
373
 
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Dict, Iterable, List, Set
6
6
 
7
-
8
7
  ROLE_CAPABILITIES: Dict[str, Set[str]] = {
9
8
  "owner": {"all"},
10
9
  "admin": {
@@ -0,0 +1,84 @@
1
+ """Deliberately-ignored exceptions, recorded instead of erased.
2
+
3
+ A local-first app that talks to optional hardware, optional models, optional
4
+ network paths and user files legitimately swallows a lot. ``except Exception:
5
+ pass`` is often the right *behaviour* — a failed probe should not take the
6
+ server down. What is not right is that the failure leaves no trace at all: a
7
+ genuine bug in one of those paths is indistinguishable from the optional thing
8
+ simply being absent, and nobody ever finds out.
9
+
10
+ ``quiet()`` keeps the behaviour and removes the silence. It logs the live
11
+ exception at DEBUG with the file, function and line that suppressed it, so the
12
+ default experience is unchanged and ``LATTICEAI_LOG_LEVEL=DEBUG`` turns every
13
+ swallowed failure into a line you can read.
14
+
15
+ try:
16
+ probe_optional_thing()
17
+ except Exception:
18
+ quiet("optional GPU probe")
19
+
20
+ The label is optional; without one the call site is still identified. Pass one
21
+ when the location alone would not tell a reader *why* failure is acceptable.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import sys
28
+ import traceback
29
+ from typing import Optional
30
+
31
+ logger = logging.getLogger("latticeai.suppressed")
32
+
33
+
34
+ def quiet(reason: Optional[str] = None, *, level: int = logging.DEBUG) -> None:
35
+ """Record the exception currently being handled, then continue.
36
+
37
+ Safe to call outside an ``except`` block (it becomes a no-op) so a refactor
38
+ that moves it cannot itself raise.
39
+ """
40
+ exc_type, exc, tb = sys.exc_info()
41
+ if exc is None:
42
+ return
43
+
44
+ where = "<unknown>"
45
+ if tb is not None:
46
+ # The frame that raised is the useful one; walk to the deepest.
47
+ deepest = tb
48
+ while deepest.tb_next is not None:
49
+ deepest = deepest.tb_next
50
+ frame = deepest.tb_frame
51
+ where = f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}"
52
+
53
+ label = f" ({reason})" if reason else ""
54
+ if logger.isEnabledFor(level):
55
+ logger.log(
56
+ level,
57
+ "suppressed %s at %s%s: %s",
58
+ getattr(exc_type, "__name__", "Exception"),
59
+ where,
60
+ label,
61
+ exc,
62
+ exc_info=exc,
63
+ )
64
+
65
+
66
+ def quiet_summary(reason: Optional[str] = None) -> str:
67
+ """One-line description of the live exception, for callers that report it."""
68
+ exc_type, exc, _ = sys.exc_info()
69
+ if exc is None:
70
+ return ""
71
+ name = getattr(exc_type, "__name__", "Exception")
72
+ text = str(exc).strip() or name
73
+ return f"{reason}: {text}" if reason else text
74
+
75
+
76
+ def format_suppressed() -> str:
77
+ """Full traceback of the live exception as text (for diagnostics payloads)."""
78
+ exc_type, exc, tb = sys.exc_info()
79
+ if exc is None:
80
+ return ""
81
+ return "".join(traceback.format_exception(exc_type, exc, tb))
82
+
83
+
84
+ __all__ = ["quiet", "quiet_summary", "format_suppressed"]
@@ -32,8 +32,9 @@ import threading
32
32
  from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Set
33
33
 
34
34
  from lattice_brain.runtime.contracts import realtime_event_contract
35
- from .timeutil import now_iso as _now
35
+ from latticeai.core.quiet import quiet
36
36
 
37
+ from .timeutil import now_iso as _now
37
38
 
38
39
  REALTIME_VERSION = "2.2.0"
39
40
  _FEED_LIMIT = 200
@@ -120,7 +121,7 @@ class RealtimeBus:
120
121
  sub.queue.get_nowait() # drop oldest
121
122
  sub.queue.put_nowait(event)
122
123
  except Exception:
123
- pass
124
+ quiet()
124
125
 
125
126
  # The store calls ``event_sink(event)`` positionally; expose a stable alias.
126
127
  def __call__(self, event: Dict[str, Any]) -> Dict[str, Any]:
@@ -33,6 +33,7 @@ from typing import Any, Dict, List, Optional
33
33
 
34
34
  from latticeai.core.agent import AgentRunContext, AgentState
35
35
  from latticeai.core.agent_trace import LoopTrace
36
+ from latticeai.core.quiet import quiet
36
37
 
37
38
  LOGGER = logging.getLogger(__name__)
38
39
 
@@ -148,7 +149,7 @@ class AgentRunStore:
148
149
  try:
149
150
  os.unlink(tmp_name)
150
151
  except OSError:
151
- pass
152
+ quiet()
152
153
  return True
153
154
  except Exception as exc: # noqa: BLE001 — pause must still answer without disk
154
155
  LOGGER.warning("agent run store save failed for %s: %s", run_id, exc)
@@ -193,6 +194,7 @@ class AgentRunStore:
193
194
  try:
194
195
  record = json.loads(path.read_text(encoding="utf-8"))
195
196
  except Exception: # noqa: BLE001
197
+ quiet()
196
198
  continue
197
199
  if not isinstance(record, dict):
198
200
  continue
@@ -238,7 +240,7 @@ class AgentRunStore:
238
240
  path.unlink(missing_ok=True)
239
241
  removed += 1
240
242
  except OSError:
241
- pass
243
+ quiet()
242
244
  return removed
243
245
 
244
246
 
@@ -10,6 +10,8 @@ from typing import Any, Dict, List
10
10
 
11
11
  from fastapi import HTTPException
12
12
 
13
+ from latticeai.core.quiet import quiet
14
+
13
15
 
14
16
  def hash_password(password: str) -> str:
15
17
  salt = secrets.token_hex(16)
@@ -65,6 +67,7 @@ def configure_trusted_proxies(values) -> int:
65
67
  try:
66
68
  networks.append(ipaddress.ip_network(item, strict=False))
67
69
  except ValueError:
70
+ quiet()
68
71
  continue
69
72
  _trusted_proxies = networks
70
73
  return len(networks)
@@ -93,6 +96,7 @@ def client_ip(request) -> str:
93
96
  ipaddress.ip_address(candidate)
94
97
  return candidate
95
98
  except ValueError:
99
+ quiet()
96
100
  continue
97
101
  return peer or "unknown"
98
102
 
@@ -6,13 +6,15 @@ import json
6
6
  import shutil
7
7
  import sqlite3
8
8
  import uuid
9
+ from contextlib import closing
9
10
  from pathlib import Path
10
11
  from typing import Any, Dict, Optional
11
12
 
13
+ from latticeai.core.quiet import quiet
14
+
12
15
  from .io_utils import atomic_write_json
13
16
  from .timeutil import now_iso as _now
14
17
 
15
-
16
18
  USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
17
19
 
18
20
 
@@ -76,7 +78,7 @@ def load_users_file(path: Path) -> Dict[str, Any]:
76
78
  try:
77
79
  shutil.copy2(path, backup)
78
80
  except Exception:
79
- pass
81
+ quiet()
80
82
  atomic_write_json(path, migrated)
81
83
  return migrated
82
84
 
@@ -112,7 +114,7 @@ def migrate_knowledge_graph_identity(db_path: Path, email_to_id: Dict[str, str])
112
114
  if not db_path.exists() or not email_to_id:
113
115
  return 0
114
116
  changed = 0
115
- with sqlite3.connect(db_path) as conn:
117
+ with closing(sqlite3.connect(db_path)) as conn, conn:
116
118
  tables = {
117
119
  row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
118
120
  }
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
 
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
+ from latticeai.core.quiet import quiet
7
+
6
8
  from .timeutil import now_iso as _now
7
9
  from .workspace_os_utils import _json_hash, _listify
8
10
 
@@ -97,6 +99,7 @@ class WorkspaceGraphTrace:
97
99
  if len(edges) >= 24:
98
100
  break
99
101
  except Exception:
102
+ quiet()
100
103
  continue
101
104
 
102
105
  if matches: