ltcai 9.9.9 → 10.2.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 (228) hide show
  1. package/README.md +51 -38
  2. package/docs/CHANGELOG.md +197 -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/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +5 -4
  13. package/lattice_brain/conversations.py +14 -3
  14. package/lattice_brain/embeddings.py +12 -2
  15. package/lattice_brain/graph/_kg_common.py +5 -5
  16. package/lattice_brain/graph/_kg_fsutil.py +4 -3
  17. package/lattice_brain/graph/discovery.py +4 -3
  18. package/lattice_brain/graph/discovery_index.py +0 -1
  19. package/lattice_brain/graph/documents.py +3 -2
  20. package/lattice_brain/graph/fusion.py +4 -0
  21. package/lattice_brain/graph/ingest.py +12 -2
  22. package/lattice_brain/graph/projection.py +3 -2
  23. package/lattice_brain/graph/provenance.py +5 -3
  24. package/lattice_brain/graph/rerank.py +4 -1
  25. package/lattice_brain/graph/retrieval.py +0 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  27. package/lattice_brain/graph/retrieval_reads.py +0 -1
  28. package/lattice_brain/graph/retrieval_vector.py +0 -1
  29. package/lattice_brain/graph/schema.py +4 -3
  30. package/lattice_brain/graph/store.py +18 -4
  31. package/lattice_brain/graph/write_master.py +46 -1
  32. package/lattice_brain/ingestion.py +4 -1
  33. package/lattice_brain/portability.py +5 -2
  34. package/lattice_brain/quality.py +12 -5
  35. package/lattice_brain/quiet.py +43 -0
  36. package/lattice_brain/runtime/agent_runtime.py +12 -8
  37. package/lattice_brain/runtime/hooks.py +2 -1
  38. package/lattice_brain/runtime/multi_agent.py +2 -3
  39. package/lattice_brain/sensitivity.py +94 -0
  40. package/lattice_brain/storage/base.py +30 -2
  41. package/lattice_brain/storage/migration.py +3 -2
  42. package/lattice_brain/storage/postgres.py +2 -2
  43. package/lattice_brain/storage/sqlite.py +6 -4
  44. package/lattice_brain/workflow.py +4 -2
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/admin.py +1 -1
  47. package/latticeai/api/agents.py +4 -2
  48. package/latticeai/api/auth.py +5 -1
  49. package/latticeai/api/automation_intelligence.py +2 -1
  50. package/latticeai/api/browser.py +3 -2
  51. package/latticeai/api/chat.py +28 -17
  52. package/latticeai/api/chat_agent_http.py +22 -8
  53. package/latticeai/api/chat_contracts.py +4 -0
  54. package/latticeai/api/chat_documents.py +6 -2
  55. package/latticeai/api/chat_hybrid.py +82 -0
  56. package/latticeai/api/computer_use.py +8 -3
  57. package/latticeai/api/knowledge_graph.py +1 -1
  58. package/latticeai/api/mcp.py +4 -4
  59. package/latticeai/api/models.py +5 -2
  60. package/latticeai/api/network_boundary.py +220 -0
  61. package/latticeai/api/permissions.py +0 -1
  62. package/latticeai/api/realtime.py +1 -1
  63. package/latticeai/api/security_dashboard.py +1 -1
  64. package/latticeai/api/setup.py +16 -3
  65. package/latticeai/api/static_routes.py +2 -1
  66. package/latticeai/api/tools.py +12 -8
  67. package/latticeai/api/voice_capture.py +3 -1
  68. package/latticeai/api/workflow_designer.py +1 -1
  69. package/latticeai/api/workspace.py +1 -2
  70. package/latticeai/app_factory.py +131 -78
  71. package/latticeai/cli/entrypoint.py +6 -4
  72. package/latticeai/core/agent.py +55 -495
  73. package/latticeai/core/agent_eval.py +2 -2
  74. package/latticeai/core/agent_helpers.py +493 -0
  75. package/latticeai/core/agent_prompts.py +0 -1
  76. package/latticeai/core/agent_registry.py +3 -1
  77. package/latticeai/core/agent_state.py +41 -0
  78. package/latticeai/core/audit.py +1 -1
  79. package/latticeai/core/builtin_hooks.py +2 -1
  80. package/latticeai/core/config.py +0 -1
  81. package/latticeai/core/embedding_providers.py +12 -1
  82. package/latticeai/core/file_generation.py +3 -0
  83. package/latticeai/core/invitations.py +4 -1
  84. package/latticeai/core/io_utils.py +3 -1
  85. package/latticeai/core/legacy_compatibility.py +1 -2
  86. package/latticeai/core/local_embeddings.py +12 -1
  87. package/latticeai/core/marketplace.py +1 -2
  88. package/latticeai/core/mcp_registry.py +0 -1
  89. package/latticeai/core/model_compat.py +1 -1
  90. package/latticeai/core/model_resolution.py +1 -1
  91. package/latticeai/core/network_boundary.py +168 -0
  92. package/latticeai/core/oidc.py +3 -0
  93. package/latticeai/core/permission_mode.py +6 -6
  94. package/latticeai/core/plugins.py +3 -2
  95. package/latticeai/core/policy.py +0 -1
  96. package/latticeai/core/quiet.py +84 -0
  97. package/latticeai/core/realtime.py +3 -2
  98. package/latticeai/core/run_store.py +4 -2
  99. package/latticeai/core/security.py +4 -0
  100. package/latticeai/core/users.py +5 -3
  101. package/latticeai/core/workspace_graph_trace.py +3 -0
  102. package/latticeai/core/workspace_os.py +65 -273
  103. package/latticeai/core/workspace_os_constants.py +126 -0
  104. package/latticeai/core/workspace_os_state.py +180 -0
  105. package/latticeai/core/workspace_os_utils.py +3 -1
  106. package/latticeai/core/workspace_permissions.py +1 -0
  107. package/latticeai/core/workspace_snapshots.py +3 -1
  108. package/latticeai/core/workspace_timeline.py +3 -1
  109. package/latticeai/integrations/telegram_bot.py +25 -16
  110. package/latticeai/models/router.py +6 -3
  111. package/latticeai/runtime/access_runtime.py +3 -1
  112. package/latticeai/runtime/audit_runtime.py +3 -2
  113. package/latticeai/runtime/chat_wiring.py +4 -1
  114. package/latticeai/runtime/history_runtime.py +1 -1
  115. package/latticeai/runtime/lifespan_runtime.py +3 -1
  116. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  117. package/latticeai/runtime/persistence_runtime.py +3 -1
  118. package/latticeai/runtime/router_registration.py +11 -1
  119. package/latticeai/services/architecture_readiness.py +1 -2
  120. package/latticeai/services/change_proposals.py +2 -1
  121. package/latticeai/services/cloud_egress_audit.py +85 -0
  122. package/latticeai/services/cloud_extraction.py +129 -0
  123. package/latticeai/services/cloud_streaming.py +266 -0
  124. package/latticeai/services/cloud_token_guard.py +84 -0
  125. package/latticeai/services/command_center.py +2 -1
  126. package/latticeai/services/folder_watch.py +3 -0
  127. package/latticeai/services/funnel_metrics.py +3 -1
  128. package/latticeai/services/hybrid_chat.py +265 -0
  129. package/latticeai/services/hybrid_context.py +228 -0
  130. package/latticeai/services/hybrid_policy.py +178 -0
  131. package/latticeai/services/memory_service.py +1 -1
  132. package/latticeai/services/model_catalog.py +10 -1
  133. package/latticeai/services/model_engines.py +35 -14
  134. package/latticeai/services/model_loading.py +3 -2
  135. package/latticeai/services/model_runtime.py +68 -17
  136. package/latticeai/services/multimodal_streaming.py +123 -0
  137. package/latticeai/services/network_boundary_service.py +154 -0
  138. package/latticeai/services/openai_compatible_adapter.py +100 -0
  139. package/latticeai/services/p_reinforce.py +4 -0
  140. package/latticeai/services/platform_runtime.py +9 -4
  141. package/latticeai/services/process_audit.py +0 -1
  142. package/latticeai/services/product_readiness.py +1 -1
  143. package/latticeai/services/run_executor.py +3 -2
  144. package/latticeai/services/search_service.py +1 -4
  145. package/latticeai/services/setup_detection.py +2 -1
  146. package/latticeai/services/tool_dispatch.py +7 -2
  147. package/latticeai/services/upload_service.py +2 -1
  148. package/latticeai/setup/auto_setup.py +10 -7
  149. package/latticeai/setup/wizard.py +15 -11
  150. package/latticeai/tools/__init__.py +3 -3
  151. package/latticeai/tools/commands.py +7 -7
  152. package/latticeai/tools/computer.py +3 -2
  153. package/latticeai/tools/documents.py +10 -9
  154. package/latticeai/tools/filesystem.py +7 -4
  155. package/latticeai/tools/knowledge.py +2 -0
  156. package/latticeai/tools/local_files.py +1 -1
  157. package/latticeai/tools/network.py +2 -1
  158. package/package.json +6 -4
  159. package/scripts/bench_agent_smoke.py +0 -1
  160. package/scripts/bench_models.py +0 -1
  161. package/scripts/brain_quality_eval.py +7 -2
  162. package/scripts/bump_version.py +2 -1
  163. package/scripts/check_current_release_docs.mjs +1 -1
  164. package/scripts/migrate_brain_storage.py +5 -1
  165. package/scripts/profile_kg.py +2 -7
  166. package/scripts/verify_hf_model_registry.py +2 -2
  167. package/src-tauri/Cargo.lock +1 -1
  168. package/src-tauri/Cargo.toml +1 -1
  169. package/src-tauri/tauri.conf.json +1 -1
  170. package/static/app/asset-manifest.json +37 -37
  171. package/static/app/assets/Act-CbdGD-2i.js +1 -0
  172. package/static/app/assets/AdminConsole-LgCkXpnf.js +1 -0
  173. package/static/app/assets/Brain-CoPGJI1L.js +321 -0
  174. package/static/app/assets/BrainHome-gVnaxSwp.js +2 -0
  175. package/static/app/assets/BrainSignals-ChxAYHtj.js +1 -0
  176. package/static/app/assets/Capture-DBtgkHZg.js +1 -0
  177. package/static/app/assets/CommandPalette-Ovtv5I0Y.js +1 -0
  178. package/static/app/assets/Library-CBia2Fvm.js +1 -0
  179. package/static/app/assets/LivingBrain-CNz-6NSd.js +1 -0
  180. package/static/app/assets/{ProductFlow-cB3EFinZ.js → ProductFlow-ePX-Y73J.js} +1 -1
  181. package/static/app/assets/ReviewCard-C4mpvkwH.js +3 -0
  182. package/static/app/assets/System-BvWNK1zc.js +1 -0
  183. package/static/app/assets/activity-CiauPV_3.js +1 -0
  184. package/static/app/assets/{bot-DtKkLfXj.js → bot-Dwct-Q1x.js} +1 -1
  185. package/static/app/assets/brain-BKDW1F0T.js +1 -0
  186. package/static/app/assets/{button-BTxZDMFJ.js → button-CCB9Kuw7.js} +1 -1
  187. package/static/app/assets/{circle-pause--qxF9-EO.js → circle-pause-DNa8WHC5.js} +1 -1
  188. package/static/app/assets/{circle-play-89x3S6jm.js → circle-play-8jmxn5mf.js} +1 -1
  189. package/static/app/assets/{cpu-DTUI63vo.js → cpu-mxvF3V1I.js} +1 -1
  190. package/static/app/assets/{download-EJS5_O0f.js → download--SmCcx0p.js} +1 -1
  191. package/static/app/assets/{folder-open-BL9zYTnc.js → folder-open-D4Wy5roo.js} +1 -1
  192. package/static/app/assets/{hard-drive-BFnwi1Lu.js → hard-drive-BwQcSPlS.js} +1 -1
  193. package/static/app/assets/index-CQWdDU3z.css +2 -0
  194. package/static/app/assets/index-DDV2YZwM.js +10 -0
  195. package/static/app/assets/input-DFhSjmLS.js +1 -0
  196. package/static/app/assets/{network-zKa3jAwl.js → network-Bts7VO94.js} +1 -1
  197. package/static/app/assets/primitives-BuSMEJY8.js +1 -0
  198. package/static/app/assets/search-DZzxhWaQ.js +1 -0
  199. package/static/app/assets/{shield-alert-CpHeCsaF.js → shield-alert-BfTO6X_G.js} +1 -1
  200. package/static/app/assets/textarea-BjctW1oh.js +1 -0
  201. package/static/app/assets/{useFocusTrap-58TNCONM.js → useFocusTrap-CE43-LrA.js} +1 -1
  202. package/static/app/assets/{useQuery-CcVlXlBd.js → useQuery-Bv3ejLL5.js} +1 -1
  203. package/static/app/assets/{users-C0cFMj6o.js → users-CsNqLZAj.js} +1 -1
  204. package/static/app/assets/utils-KFFdVG_t.js +7 -0
  205. package/static/app/assets/workspace-wdCvdyPF.js +1 -0
  206. package/static/app/index.html +4 -4
  207. package/static/sw.js +1 -1
  208. package/static/app/assets/Act-BFbpl2_Y.js +0 -1
  209. package/static/app/assets/AdminConsole-DaZPha9Y.js +0 -1
  210. package/static/app/assets/Brain-DGrV0heZ.js +0 -321
  211. package/static/app/assets/BrainHome-Re6Zxh_W.js +0 -2
  212. package/static/app/assets/BrainSignals-BQ11jDpJ.js +0 -1
  213. package/static/app/assets/Capture-DRaf1zLH.js +0 -1
  214. package/static/app/assets/CommandPalette-DAKPm4uJ.js +0 -1
  215. package/static/app/assets/LanguageSwitcher-BgTwHoWv.js +0 -1
  216. package/static/app/assets/Library-BVtrSt12.js +0 -1
  217. package/static/app/assets/LivingBrain-7glOmK0R.js +0 -1
  218. package/static/app/assets/ReviewCard-CShhHS-w.js +0 -3
  219. package/static/app/assets/System-B4nQDy8a.js +0 -1
  220. package/static/app/assets/brain-wVV63_1z.js +0 -1
  221. package/static/app/assets/index-BFtDYI6l.js +0 -10
  222. package/static/app/assets/index-BY9bpxyx.css +0 -2
  223. package/static/app/assets/input-DEz5UjBz.js +0 -1
  224. package/static/app/assets/primitives-jNrZm2O9.js +0 -1
  225. package/static/app/assets/search-ku6-_6wY.js +0 -1
  226. package/static/app/assets/textarea-DpcQkn75.js +0 -1
  227. package/static/app/assets/utils-DYf7pqrR.js +0 -7
  228. package/static/app/assets/workspace-CjthHsLN.js +0 -1
@@ -10,8 +10,8 @@ from fastapi import APIRouter, HTTPException, Request
10
10
  from fastapi.responses import StreamingResponse
11
11
  from pydantic import BaseModel
12
12
 
13
- from latticeai.core.agent import extract_action as _extract_agent_action
14
13
  from lattice_brain.runtime.hooks import dispatch_tool
14
+ from latticeai.core.agent import extract_action as _extract_agent_action
15
15
  from latticeai.services.tool_dispatch import enforce_tool_policy
16
16
  from latticeai.tools import (
17
17
  AGENT_ROOT,
@@ -30,7 +30,6 @@ from latticeai.tools import (
30
30
  execute_tool,
31
31
  )
32
32
 
33
-
34
33
  CU_SYSTEM_PROMPT = """You are Lattice AI desktop-control agent. You control the Mac desktop using tools.
35
34
  Prefer non-visual direct actions when possible. Use screenshots only when you must inspect visible UI state or choose screen coordinates.
36
35
 
@@ -438,7 +437,13 @@ def create_computer_use_router(
438
437
 
439
438
  yield _send("action", {"step": step + 1, "action": name, "args": args})
440
439
  try:
441
- result = _dispatch(name, args, lambda: execute_tool(name, args), current_user=current_user)
440
+ result = _dispatch(
441
+ name,
442
+ args,
443
+ # Bound as defaults; see chat_agent_http for why.
444
+ lambda name=name, args=args: execute_tool(name, args),
445
+ current_user=current_user,
446
+ )
442
447
  if name == "computer_screenshot" and "screenshot_b64" in result:
443
448
  last_screenshot_b64 = result["screenshot_b64"]
444
449
  result_summary = {k: v for k, v in result.items() if k != "screenshot_b64"}
@@ -12,8 +12,8 @@ from typing import Any, Callable, Dict, List, Optional
12
12
  from fastapi import APIRouter, HTTPException, Request
13
13
  from pydantic import BaseModel
14
14
 
15
- from latticeai.api.ui_redirects import app_redirect
16
15
  from lattice_brain.ingestion import IngestionItem
16
+ from latticeai.api.ui_redirects import app_redirect
17
17
 
18
18
 
19
19
  class KnowledgeGraphIngestRequest(BaseModel):
@@ -17,17 +17,17 @@ from datetime import datetime
17
17
  from pathlib import Path
18
18
  from typing import Any, Callable, Dict, List, Optional
19
19
 
20
- from lattice_brain.ingestion import IngestionItem
21
20
  from fastapi import APIRouter, HTTPException, Request
22
21
  from pydantic import BaseModel
23
22
 
24
23
  import latticeai.core.mcp_registry as mcp_registry
24
+ from lattice_brain.ingestion import IngestionItem
25
25
  from latticeai.core.mcp_registry import (
26
- _get_combined_registry,
27
- _fetch_skills_marketplace,
26
+ SKILLS_DIR,
28
27
  _fetch_plugin_directory,
28
+ _fetch_skills_marketplace,
29
+ _get_combined_registry,
29
30
  install_skill,
30
- SKILLS_DIR,
31
31
  )
32
32
  from latticeai.core.tool_registry import (
33
33
  KNOWLEDGE_WRITE_TOOLS,
@@ -494,7 +494,10 @@ def create_models_router(
494
494
  async def load_model(req: LoadModelRequest, request: Request):
495
495
  current_user = _authorize_model_admin(request, req.user_email)
496
496
  try:
497
- from latticeai.core.model_compat import friendly_model_runtime_error, model_runtime_compatibility
497
+ from latticeai.core.model_compat import (
498
+ friendly_model_runtime_error,
499
+ model_runtime_compatibility,
500
+ )
498
501
 
499
502
  model_id = req.model_id
500
503
  requested_engine = req.engine or (model_id.split(":", 1)[0] if ":" in model_id else "local_mlx")
@@ -552,8 +555,8 @@ def create_models_router(
552
555
  hardware, load_strategy, license, safety_notes) from the structured registry.
553
556
  """
554
557
  require_user(request)
555
- from latticeai.setup.auto_setup import probe as auto_setup_probe
556
558
  from latticeai.services.model_recommendation import recommend_catalog
559
+ from latticeai.setup.auto_setup import probe as auto_setup_probe
557
560
 
558
561
  profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
559
562
  catalog = recommend_catalog(profile, engine=engine)
@@ -0,0 +1,220 @@
1
+ """Network boundary API — local_only / cloud_allowed dial + Phase 3 policy/UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Dict, Optional
6
+
7
+ from fastapi import APIRouter, HTTPException, Request
8
+ from pydantic import BaseModel, Field
9
+
10
+ from latticeai.core.network_boundary import network_mode_catalog, normalize_network_mode
11
+ from latticeai.services.cloud_egress_audit import record_cloud_egress
12
+ from latticeai.services.cloud_token_guard import budget_for
13
+ from latticeai.services.hybrid_context import build_minimal_context
14
+ from latticeai.services.hybrid_policy import HybridPolicyService
15
+ from latticeai.services.network_boundary_service import NetworkBoundaryService
16
+
17
+
18
+ class SetNetworkBoundaryRequest(BaseModel):
19
+ mode: str = Field(..., description="local_only | cloud_allowed")
20
+ workspace_id: Optional[str] = None
21
+ acknowledge_risk: bool = False
22
+
23
+
24
+ class PreviewRequest(BaseModel):
25
+ message: str
26
+ workspace_id: Optional[str] = None
27
+ top_k: int = 6
28
+
29
+
30
+ class SetNodeSensitivityRequest(BaseModel):
31
+ node_id: str
32
+ local_only: bool = True
33
+ reason: Optional[str] = None
34
+ workspace_id: Optional[str] = None
35
+
36
+
37
+ class SetHybridPolicyRequest(BaseModel):
38
+ workspace_id: Optional[str] = None
39
+ blocked_node_types: Optional[list[str]] = None
40
+ blocked_metadata_flags: Optional[list[str]] = None
41
+ auto_commit: Optional[bool] = None
42
+ allow_multimodal: Optional[bool] = None
43
+ min_extraction_confidence: Optional[float] = None
44
+
45
+
46
+ def create_network_boundary_router(
47
+ *,
48
+ service: NetworkBoundaryService,
49
+ require_user: Callable[..., str],
50
+ knowledge_graph: Any = None,
51
+ policy_service: Optional[HybridPolicyService] = None,
52
+ ) -> APIRouter:
53
+ router = APIRouter(tags=["network-boundary"])
54
+
55
+ def _scope(request: Request, workspace_id: Optional[str] = None) -> tuple[str, Optional[str]]:
56
+ user = require_user(request)
57
+ header_ws = request.headers.get("X-Workspace-Id")
58
+ scope = workspace_id or (header_ws.strip() if header_ws else None)
59
+ return user, scope
60
+
61
+ @router.get("/api/network-boundary")
62
+ async def get_network_boundary(
63
+ request: Request,
64
+ workspace_id: Optional[str] = None,
65
+ ):
66
+ user, scope = _scope(request, workspace_id)
67
+ payload = service.get(user_email=user, workspace_id=scope)
68
+ scope_key = f"{user or 'anon'}|{scope or 'global'}"
69
+ payload["token_budget"] = budget_for(scope_key).snapshot()
70
+ if policy_service is not None:
71
+ payload["policy"] = policy_service.resolve(
72
+ user_email=user, workspace_id=scope
73
+ )
74
+ return payload
75
+
76
+ @router.get("/api/network-boundary/catalog")
77
+ async def network_boundary_catalog(request: Request):
78
+ require_user(request)
79
+ return {"modes": network_mode_catalog()}
80
+
81
+ @router.get("/api/network-boundary/ui-state")
82
+ async def network_boundary_ui_state(
83
+ request: Request,
84
+ workspace_id: Optional[str] = None,
85
+ ):
86
+ """Compact payload for the progressive-enhancement toggle panel."""
87
+ user, scope = _scope(request, workspace_id)
88
+ mode_payload = service.get(user_email=user, workspace_id=scope)
89
+ policy = (
90
+ policy_service.resolve(user_email=user, workspace_id=scope)
91
+ if policy_service is not None
92
+ else {}
93
+ )
94
+ scope_key = f"{user or 'anon'}|{scope or 'global'}"
95
+ return {
96
+ "mode": mode_payload.get("mode"),
97
+ "label": mode_payload.get("label"),
98
+ "label_ko": mode_payload.get("label_ko"),
99
+ "allows_cloud": mode_payload.get("allows_cloud"),
100
+ "requires_ack": mode_payload.get("requires_ack"),
101
+ "warning_ko": next(
102
+ (
103
+ m.get("warning_ko")
104
+ for m in (mode_payload.get("catalog") or [])
105
+ if m.get("id") == mode_payload.get("mode")
106
+ ),
107
+ None,
108
+ ),
109
+ "policy": policy,
110
+ "token_budget": budget_for(scope_key).snapshot(),
111
+ "catalog": network_mode_catalog(),
112
+ }
113
+
114
+ @router.post("/api/network-boundary")
115
+ async def set_network_boundary(body: SetNetworkBoundaryRequest, request: Request):
116
+ user, scope = _scope(request, body.workspace_id)
117
+ try:
118
+ return service.set_mode(
119
+ normalize_network_mode(body.mode),
120
+ user_email=user,
121
+ workspace_id=scope,
122
+ acknowledge_risk=body.acknowledge_risk,
123
+ source="api",
124
+ )
125
+ except PermissionError as exc:
126
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
127
+
128
+ @router.post("/api/network-boundary/preview")
129
+ async def preview_cloud_context(body: PreviewRequest, request: Request):
130
+ user, scope = _scope(request, body.workspace_id)
131
+ mode = service.resolve(user_email=user, workspace_id=scope)
132
+ minimal = build_minimal_context(
133
+ body.message,
134
+ store=knowledge_graph,
135
+ mode=mode,
136
+ top_k=max(1, min(int(body.top_k or 6), 12)),
137
+ allowed_workspaces={scope} if scope else None,
138
+ )
139
+ scope_key = f"{user or 'anon'}|{scope or 'global'}"
140
+ budget = budget_for(scope_key)
141
+ refusal = budget.check_turn(minimal.token_estimate)
142
+ return {
143
+ "mode": mode.value,
144
+ "allows_cloud": mode.value == "cloud_allowed",
145
+ "node_ids": minimal.node_ids,
146
+ "keywords": minimal.keywords,
147
+ "titles": [str(n.get("title") or n.get("id") or "") for n in minimal.nodes],
148
+ "types": [str(n.get("type") or "") for n in minimal.nodes],
149
+ "token_estimate": minimal.token_estimate,
150
+ "quality": minimal.quality,
151
+ "compact_preview": minimal.compact_text[:1200],
152
+ "token_budget": budget.snapshot(),
153
+ "would_block": refusal,
154
+ }
155
+
156
+ @router.post("/api/network-boundary/node-sensitivity")
157
+ async def set_node_sensitivity(body: SetNodeSensitivityRequest, request: Request):
158
+ """Mark one memory as never-leaving (or clear the mark).
159
+
160
+ The cloud filter has always looked for this flag. Until 10.2.0 nothing
161
+ in the product could set it, so the guard could not fire. Ingestion
162
+ stamps secret-bearing paths automatically; this covers what a path
163
+ cannot tell you — a note whose *content* is private.
164
+ """
165
+ user, scope = _scope(request, body.workspace_id)
166
+ if knowledge_graph is None or not hasattr(knowledge_graph, "set_node_sensitivity"):
167
+ raise HTTPException(status_code=501, detail="knowledge graph not available")
168
+ result = knowledge_graph.set_node_sensitivity(
169
+ body.node_id, local_only=bool(body.local_only), reason=body.reason
170
+ )
171
+ if not result.get("ok"):
172
+ raise HTTPException(status_code=404, detail=result.get("reason") or "node not found")
173
+ record_cloud_egress(
174
+ node_ids=[body.node_id], token_estimate=0, mode="(policy)",
175
+ provider="(local)", user_email=user, workspace_id=scope,
176
+ outcome="marked_local_only" if body.local_only else "cleared_local_only",
177
+ detail=body.reason,
178
+ )
179
+ return result
180
+
181
+ @router.get("/api/network-boundary/policy")
182
+ async def get_hybrid_policy(
183
+ request: Request,
184
+ workspace_id: Optional[str] = None,
185
+ ):
186
+ if policy_service is None:
187
+ raise HTTPException(status_code=501, detail="hybrid policy service not configured")
188
+ user, scope = _scope(request, workspace_id)
189
+ return policy_service.resolve(user_email=user, workspace_id=scope)
190
+
191
+ @router.post("/api/network-boundary/policy")
192
+ async def set_hybrid_policy(body: SetHybridPolicyRequest, request: Request):
193
+ if policy_service is None:
194
+ raise HTTPException(status_code=501, detail="hybrid policy service not configured")
195
+ user, scope = _scope(request, body.workspace_id)
196
+ patch: Dict[str, Any] = {}
197
+ for key in (
198
+ "blocked_node_types",
199
+ "blocked_metadata_flags",
200
+ "auto_commit",
201
+ "allow_multimodal",
202
+ "min_extraction_confidence",
203
+ ):
204
+ val = getattr(body, key, None)
205
+ if val is not None:
206
+ patch[key] = val
207
+ return policy_service.set_policy(
208
+ patch, user_email=user, workspace_id=scope, source="api"
209
+ )
210
+
211
+ return router
212
+
213
+
214
+ __all__ = [
215
+ "create_network_boundary_router",
216
+ "SetNetworkBoundaryRequest",
217
+ "PreviewRequest",
218
+ "SetHybridPolicyRequest",
219
+ "SetNodeSensitivityRequest",
220
+ ]
@@ -17,7 +17,6 @@ from fastapi import APIRouter, HTTPException, Request
17
17
 
18
18
  from latticeai.core.io_utils import atomic_write_json
19
19
 
20
-
21
20
  _PERMISSION_ACTION_LABELS = {
22
21
  "list": "폴더 목록 보기",
23
22
  "read": "파일 읽기",
@@ -16,8 +16,8 @@ from fastapi import APIRouter, HTTPException, Request
16
16
  from fastapi.responses import StreamingResponse
17
17
  from pydantic import BaseModel
18
18
 
19
- from latticeai.api.ui_redirects import app_redirect
20
19
  from lattice_brain.runtime.contracts import contract_views
20
+ from latticeai.api.ui_redirects import app_redirect
21
21
 
22
22
 
23
23
  class PresenceRequest(BaseModel):
@@ -195,7 +195,7 @@ def _pdf_report(title: str, rows: List[Dict[str, Any]], overview: Dict[str, Any]
195
195
  try:
196
196
  from reportlab.lib.pagesizes import A4
197
197
  from reportlab.lib.styles import getSampleStyleSheet
198
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table
198
+ from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table
199
199
  except Exception: # pragma: no cover
200
200
  return ("PDF library not available\n" + json.dumps(overview, ensure_ascii=False, indent=2)).encode("utf-8")
201
201
 
@@ -8,15 +8,24 @@ from fastapi import APIRouter, HTTPException, Request
8
8
  from fastapi.responses import StreamingResponse
9
9
  from pydantic import BaseModel
10
10
 
11
+ from lattice_brain.ingestion import IngestionItem
12
+ from latticeai.models.router import parse_model_ref
11
13
  from latticeai.services.process_audit import command_plan
12
14
  from latticeai.setup.auto_setup import (
13
15
  plan as auto_setup_plan,
16
+ )
17
+ from latticeai.setup.auto_setup import (
14
18
  preset as auto_setup_preset,
19
+ )
20
+ from latticeai.setup.auto_setup import (
15
21
  probe as auto_setup_probe,
22
+ )
23
+ from latticeai.setup.auto_setup import (
16
24
  recommend as auto_setup_recommend,
25
+ )
26
+ from latticeai.setup.auto_setup import (
17
27
  verify as auto_setup_verify,
18
28
  )
19
- from latticeai.models.router import parse_model_ref
20
29
  from latticeai.setup.demo_corpus import (
21
30
  DEMO_DOCUMENTS,
22
31
  DEMO_METADATA_FLAG,
@@ -24,8 +33,12 @@ from latticeai.setup.demo_corpus import (
24
33
  demo_source_uri,
25
34
  suggested_questions,
26
35
  )
27
- from latticeai.setup.wizard import get_recommendations, install_stream, open_url, scan_environment
28
- from lattice_brain.ingestion import IngestionItem
36
+ from latticeai.setup.wizard import (
37
+ get_recommendations,
38
+ install_stream,
39
+ open_url,
40
+ scan_environment,
41
+ )
29
42
 
30
43
 
31
44
  class SetupInstallRequest(BaseModel):
@@ -15,6 +15,7 @@ from fastapi import APIRouter, Cookie, HTTPException, Request
15
15
  from fastapi.responses import FileResponse, HTMLResponse
16
16
 
17
17
  from latticeai.api.ui_redirects import app_redirect
18
+ from latticeai.core.quiet import quiet
18
19
 
19
20
  PRODUCTION_CSP = (
20
21
  "default-src 'self'; "
@@ -282,7 +283,7 @@ def create_static_routes_router(
282
283
  result["gpu_mem_gb"] = round(gpu_bytes / (1024 ** 3), 2)
283
284
  result["gpu_mem_pct"] = round(gpu_bytes / total_bytes * 100, 1) if total_bytes else 0.0
284
285
  except Exception:
285
- pass
286
+ quiet()
286
287
  except Exception as e:
287
288
  result["error"] = str(e)
288
289
  return result
@@ -14,23 +14,27 @@ from fastapi import APIRouter, File, HTTPException, Request, UploadFile
14
14
  from fastapi.responses import FileResponse, Response
15
15
  from pydantic import BaseModel
16
16
 
17
+ from lattice_brain.runtime.hooks import dispatch_tool
17
18
  from latticeai.api.computer_use import create_computer_use_router
18
19
  from latticeai.api.local_files import create_local_files_router
19
- from lattice_brain.runtime.hooks import dispatch_tool
20
20
  from latticeai.api.mcp import create_mcp_router
21
21
  from latticeai.api.permissions import create_permissions_router
22
- from latticeai.services.upload_service import process_uploaded_document
22
+ from latticeai.services.router_context import ToolRouterContext
23
23
  from latticeai.services.tool_dispatch import (
24
24
  TOOL_GOVERNANCE,
25
- TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
26
- check_tool_role as _check_tool_role,
27
- get_tool_permission,
28
25
  enforce_tool_policy,
26
+ get_tool_permission,
29
27
  list_tool_permissions,
30
28
  tool_registry_diagnostics,
31
29
  tool_registry_manifest,
32
30
  )
33
- from latticeai.services.router_context import ToolRouterContext
31
+ from latticeai.services.tool_dispatch import (
32
+ TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
33
+ )
34
+ from latticeai.services.tool_dispatch import (
35
+ check_tool_role as _check_tool_role,
36
+ )
37
+ from latticeai.services.upload_service import process_uploaded_document
34
38
  from latticeai.tools import (
35
39
  AGENT_ROOT,
36
40
  ToolError,
@@ -39,7 +43,6 @@ from latticeai.tools import (
39
43
  create_pdf,
40
44
  create_pptx,
41
45
  create_xlsx,
42
- read_document,
43
46
  deploy_project,
44
47
  edit_file,
45
48
  git_diff,
@@ -49,8 +52,8 @@ from latticeai.tools import (
49
52
  grep,
50
53
  inspect_html,
51
54
  knowledge_save,
52
- knowledge_search,
53
55
  knowledge_scope_root,
56
+ knowledge_search,
54
57
  knowledge_tree,
55
58
  list_dir,
56
59
  network_status,
@@ -58,6 +61,7 @@ from latticeai.tools import (
58
61
  obsidian_search,
59
62
  obsidian_tree,
60
63
  preview_url,
64
+ read_document,
61
65
  read_file,
62
66
  run_command,
63
67
  search_files,
@@ -19,6 +19,8 @@ from typing import Any, Callable, Optional
19
19
 
20
20
  from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
21
21
 
22
+ from latticeai.core.quiet import quiet
23
+
22
24
  LOGGER = logging.getLogger(__name__)
23
25
 
24
26
 
@@ -70,7 +72,7 @@ def create_voice_capture_router(
70
72
  try:
71
73
  tmp_path.unlink(missing_ok=True)
72
74
  except OSError:
73
- pass
75
+ quiet()
74
76
  if append_audit_event is not None:
75
77
  try:
76
78
  append_audit_event(
@@ -75,10 +75,10 @@ def create_workflow_designer_router(
75
75
  from lattice_brain.workflow import (
76
76
  WorkflowEngine,
77
77
  WorkflowError,
78
- validate_definition,
79
78
  export_workflow,
80
79
  import_workflow,
81
80
  legacy_steps_from_nodes,
81
+ validate_definition,
82
82
  )
83
83
 
84
84
  router = APIRouter()
@@ -23,7 +23,6 @@ from pydantic import BaseModel
23
23
  from latticeai.api.ui_redirects import app_redirect
24
24
  from latticeai.services.app_context import AppContext
25
25
 
26
-
27
26
  # ── Request models (workspace-only; moved verbatim from server_app) ──────────
28
27
 
29
28
  class WorkspaceOnboardingStepRequest(BaseModel):
@@ -290,8 +289,8 @@ def create_workspace_router(context: AppContext) -> APIRouter:
290
289
  # not_recommended) for this machine, used by the onboarding model step.
291
290
  catalog = None
292
291
  try:
293
- from latticeai.setup.auto_setup import probe as auto_setup_probe
294
292
  from latticeai.services.model_recommendation import recommend_catalog
293
+ from latticeai.setup.auto_setup import probe as auto_setup_probe
295
294
  profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
296
295
  catalog = recommend_catalog(profile, engine="local_mlx")
297
296
  except Exception as exc: # pragma: no cover - recommendation is best-effort