ltcai 8.9.0 → 9.1.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -0,0 +1,10 @@
1
+ """Canonical application timestamp helpers.
2
+
3
+ Brain Core owns the dependency-free implementation so it remains independently
4
+ importable; the application layer exposes the same helpers from this stable
5
+ location for all LatticeAI services.
6
+ """
7
+
8
+ from lattice_brain.utils import local_now, now_iso, parse_iso, utc_now_iso
9
+
10
+ __all__ = ["local_now", "now_iso", "parse_iso", "utc_now_iso"]
@@ -16,11 +16,13 @@ __all__ = [
16
16
  "FILE_CREATE_ACTIONS",
17
17
  "LOCAL_WRITE_BLOCKED_PREFIXES",
18
18
  "RISK_LEVEL_MAP",
19
+ "SCOPED_KNOWLEDGE_TOOLS",
20
+ "KNOWLEDGE_WRITE_TOOLS",
19
21
  "ToolRegistry",
20
22
  ]
21
23
 
22
24
  from dataclasses import dataclass, field
23
- from typing import Any, Callable, Dict, Mapping, Optional, TypedDict
25
+ from typing import Any, Callable, Dict, Mapping, NotRequired, Optional, TypedDict
24
26
 
25
27
 
26
28
  class ToolPolicy(TypedDict):
@@ -31,6 +33,8 @@ class ToolPolicy(TypedDict):
31
33
  auto_approve: bool
32
34
  sandbox: str
33
35
  rollback: str
36
+ capability: NotRequired[str]
37
+ scope: NotRequired[str]
34
38
 
35
39
 
36
40
  class ToolPermission(TypedDict):
@@ -38,6 +42,8 @@ class ToolPermission(TypedDict):
38
42
  risk: str
39
43
  requires_approval: bool
40
44
  network: bool
45
+ capability: NotRequired[str]
46
+ scope: NotRequired[str]
41
47
 
42
48
 
43
49
  TOOL_CATALOG_BRIEF = """
@@ -80,6 +86,17 @@ RISK_LEVEL_MAP = {
80
86
  "destructive": "high",
81
87
  }
82
88
 
89
+ SCOPED_KNOWLEDGE_TOOLS = frozenset({
90
+ "knowledge_save",
91
+ "knowledge_search",
92
+ "knowledge_tree",
93
+ "obsidian_save",
94
+ "obsidian_search",
95
+ "obsidian_tree",
96
+ })
97
+
98
+ KNOWLEDGE_WRITE_TOOLS = frozenset({"knowledge_save", "obsidian_save"})
99
+
83
100
 
84
101
  def _r(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
85
102
  return ToolPolicy(
@@ -88,6 +105,25 @@ def _r(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
88
105
  )
89
106
 
90
107
 
108
+ def _rc(
109
+ sandbox: str = "home",
110
+ rollback: str = "none",
111
+ *,
112
+ capability: str = "",
113
+ scope: str = "",
114
+ ) -> ToolPolicy:
115
+ """Read operation that still requires explicit human consent."""
116
+ policy = ToolPolicy(
117
+ risk="read", destructive=False, shell=False, network=False,
118
+ auto_approve=False, sandbox=sandbox, rollback=rollback,
119
+ )
120
+ if capability:
121
+ policy["capability"] = capability
122
+ if scope:
123
+ policy["scope"] = scope
124
+ return policy
125
+
126
+
91
127
  def _rs(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
92
128
  return ToolPolicy(
93
129
  risk="read", destructive=False, shell=True, network=False,
@@ -102,11 +138,22 @@ def _rn(sandbox: str = "system", rollback: str = "none") -> ToolPolicy:
102
138
  )
103
139
 
104
140
 
105
- def _w(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
106
- return ToolPolicy(
141
+ def _w(
142
+ sandbox: str = "workspace",
143
+ rollback: str = "none",
144
+ *,
145
+ capability: str = "",
146
+ scope: str = "",
147
+ ) -> ToolPolicy:
148
+ policy = ToolPolicy(
107
149
  risk="write", destructive=False, shell=False, network=False,
108
150
  auto_approve=False, sandbox=sandbox, rollback=rollback,
109
151
  )
152
+ if capability:
153
+ policy["capability"] = capability
154
+ if scope:
155
+ policy["scope"] = scope
156
+ return policy
110
157
 
111
158
 
112
159
  def _wa(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
@@ -146,21 +193,37 @@ TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
146
193
  "inspect_html": _r(),
147
194
  "preview_url": _r(),
148
195
  "todo_read": _r(),
149
- "local_list": _r(sandbox="home"),
150
- "local_read": _r(sandbox="home"),
151
- "read_document": _r(sandbox="home"),
196
+ "local_list": _rc(sandbox="home"),
197
+ "local_read": _rc(sandbox="home"),
198
+ "read_document": _rc(sandbox="home"),
152
199
  "git_status": _rs(),
153
200
  "git_diff": _rs(),
154
201
  "git_log": _rs(),
155
202
  "git_show": _rs(),
156
- "knowledge_search": _r(sandbox="home"),
157
- "knowledge_tree": _r(sandbox="home"),
158
- "obsidian_search": _r(sandbox="home"),
159
- "obsidian_tree": _r(sandbox="home"),
160
- "computer_screenshot": _r(sandbox="system"),
161
- "computer_status": _r(sandbox="system"),
162
- "chrome_status": _r(sandbox="system"),
163
- "computer_use_status": _r(sandbox="system"),
203
+ "knowledge_search": _rc(
204
+ sandbox="workspace", capability="workspace:read", scope="workspace_user"
205
+ ),
206
+ "knowledge_tree": _rc(
207
+ sandbox="workspace", capability="workspace:read", scope="workspace_user"
208
+ ),
209
+ "obsidian_search": _rc(
210
+ sandbox="workspace", capability="workspace:read", scope="workspace_user"
211
+ ),
212
+ "obsidian_tree": _rc(
213
+ sandbox="workspace", capability="workspace:read", scope="workspace_user"
214
+ ),
215
+ "computer_screenshot": _rc(
216
+ sandbox="system", capability="desktop:control", scope="host"
217
+ ),
218
+ "computer_status": _rc(
219
+ sandbox="system", capability="desktop:control", scope="host"
220
+ ),
221
+ "chrome_status": _rc(
222
+ sandbox="system", capability="desktop:control", scope="host"
223
+ ),
224
+ "computer_use_status": _rc(
225
+ sandbox="system", capability="desktop:control", scope="host"
226
+ ),
164
227
  "network_status": _rn(),
165
228
  "write_file": _w(rollback="git"),
166
229
  "edit_file": _w(rollback="git"),
@@ -170,8 +233,12 @@ TOOL_GOVERNANCE: Dict[str, ToolPolicy] = {
170
233
  "create_pptx": _w(),
171
234
  "create_pdf": _w(),
172
235
  "todo_write": _w(),
173
- "knowledge_save": _w(sandbox="home"),
174
- "obsidian_save": _w(sandbox="home"),
236
+ "knowledge_save": _w(
237
+ sandbox="workspace", capability="workspace:write", scope="workspace_user"
238
+ ),
239
+ "obsidian_save": _w(
240
+ sandbox="workspace", capability="workspace:write", scope="workspace_user"
241
+ ),
175
242
  "local_write": _w(sandbox="home"),
176
243
  "run_command": _e(),
177
244
  "build_project": _e(),
@@ -315,7 +382,7 @@ class ToolRegistry:
315
382
  "status": "ok" if diagnostics["ready"] else "degraded",
316
383
  "boundary": {
317
384
  "owner": "latticeai.core.tool_registry.ToolRegistry",
318
- "dispatch_owner": "tools.DEFAULT_TOOL_REGISTRY",
385
+ "dispatch_owner": "latticeai.tools.DEFAULT_TOOL_REGISTRY",
319
386
  "policy_owner": "latticeai.core.tool_registry.ToolRegistry",
320
387
  "permission_owner": "latticeai.services.tool_dispatch.ToolDispatchService",
321
388
  },
@@ -354,12 +421,17 @@ class ToolRegistry:
354
421
 
355
422
  def permission(self, name: str, args: Optional[dict] = None) -> ToolPermission:
356
423
  policy = self.policy_for(name, args or {})
357
- return ToolPermission(
424
+ permission = ToolPermission(
358
425
  tool=name,
359
426
  risk=self.risk_level(policy),
360
427
  requires_approval=not policy["auto_approve"],
361
428
  network=policy["network"],
362
429
  )
430
+ if policy.get("capability"):
431
+ permission["capability"] = policy["capability"]
432
+ if policy.get("scope"):
433
+ permission["scope"] = policy["scope"]
434
+ return permission
363
435
 
364
436
  def permissions(self) -> list[ToolPermission]:
365
437
  return [self.permission(name) for name in sorted(self.governance.keys())]
@@ -6,23 +6,14 @@ import json
6
6
  import shutil
7
7
  import sqlite3
8
8
  import uuid
9
- from datetime import datetime
10
9
  from pathlib import Path
11
10
  from typing import Any, Dict, Optional
12
11
 
12
+ from .io_utils import atomic_write_json
13
+ from .timeutil import now_iso as _now
13
14
 
14
- USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
15
-
16
-
17
- def _now() -> str:
18
- return datetime.now().isoformat(timespec="seconds")
19
15
 
20
-
21
- def _atomic_write_json(path: Path, data: Dict[str, Any]) -> None:
22
- path.parent.mkdir(parents=True, exist_ok=True)
23
- tmp = path.with_suffix(path.suffix + ".tmp")
24
- tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
25
- tmp.replace(path)
16
+ USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
26
17
 
27
18
 
28
19
  def normalize_email(email: str) -> str:
@@ -86,13 +77,13 @@ def load_users_file(path: Path) -> Dict[str, Any]:
86
77
  shutil.copy2(path, backup)
87
78
  except Exception:
88
79
  pass
89
- _atomic_write_json(path, migrated)
80
+ atomic_write_json(path, migrated)
90
81
  return migrated
91
82
 
92
83
 
93
84
  def save_users_file(path: Path, users: Dict[str, Any]) -> None:
94
85
  migrated, _, _ = migrate_users(users)
95
- _atomic_write_json(path, migrated)
86
+ atomic_write_json(path, migrated)
96
87
 
97
88
 
98
89
  def user_id_for_email(users: Dict[str, Any], email: Optional[str]) -> Optional[str]:
@@ -3,7 +3,8 @@ from __future__ import annotations
3
3
 
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
- from .workspace_os_utils import _json_hash, _listify, _now
6
+ from .timeutil import now_iso as _now
7
+ from .workspace_os_utils import _json_hash, _listify
7
8
 
8
9
 
9
10
  class WorkspaceGraphTrace:
@@ -13,7 +14,15 @@ class WorkspaceGraphTrace:
13
14
  def __getattr__(self, name: str) -> Any:
14
15
  return getattr(self._store, name)
15
16
 
16
- def build_graph_trace(self, question: str, graph: Any, context: str = "", *, limit: int = 8) -> Dict[str, Any]:
17
+ def build_graph_trace(
18
+ self,
19
+ question: str,
20
+ graph: Any,
21
+ context: str = "",
22
+ *,
23
+ limit: int = 8,
24
+ allowed_workspaces=None,
25
+ ) -> Dict[str, Any]:
17
26
  if graph is None:
18
27
  return {
19
28
  "source_files": [],
@@ -31,7 +40,16 @@ class WorkspaceGraphTrace:
31
40
  matches: List[Dict[str, Any]] = []
32
41
  search_error = ""
33
42
  try:
34
- matches = graph.search(question, limit=limit).get("matches", [])
43
+ scope_kwargs = (
44
+ {"allowed_workspaces": allowed_workspaces}
45
+ if allowed_workspaces is not None
46
+ else {}
47
+ )
48
+ matches = graph.search(
49
+ question,
50
+ limit=limit,
51
+ **scope_kwargs,
52
+ ).get("matches", [])
35
53
  except Exception as exc:
36
54
  search_error = str(exc)
37
55
  matches = []
@@ -67,7 +85,10 @@ class WorkspaceGraphTrace:
67
85
  if not node_id:
68
86
  continue
69
87
  try:
70
- for edge in graph.neighbors(node_id).get("edges", []):
88
+ for edge in graph.neighbors(
89
+ node_id,
90
+ **scope_kwargs,
91
+ ).get("edges", []):
71
92
  key = (edge.get("from"), edge.get("to"), edge.get("type"))
72
93
  if key in edge_seen:
73
94
  continue
@@ -122,7 +143,12 @@ class WorkspaceGraphTrace:
122
143
  }
123
144
  state.setdefault("traces", []).append(record)
124
145
  self.save_state(state)
125
- self.record_timeline_event("graph", "answer_trace", {"trace_id": trace_id, "conversation_id": conversation_id})
146
+ self.record_timeline_event(
147
+ "graph",
148
+ "answer_trace",
149
+ {"trace_id": trace_id, "conversation_id": conversation_id},
150
+ workspace_id=record["workspace_id"],
151
+ )
126
152
  return record
127
153
 
128
154
  def list_traces(self, conversation_id: Optional[str] = None, limit: int = 50, workspace_id: Optional[str] = None) -> Dict[str, Any]:
@@ -6,7 +6,8 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Any, Dict, List, Optional
8
8
 
9
- from .workspace_os_utils import _json_hash, _listify, _now
9
+ from .timeutil import now_iso as _now
10
+ from .workspace_os_utils import _json_hash, _listify
10
11
 
11
12
 
12
13
  class WorkspaceMemory:
@@ -52,6 +53,7 @@ class WorkspaceMemory:
52
53
  f"{kind}: {content[:80]}",
53
54
  user_email=user_email,
54
55
  source="workspace_os",
56
+ workspace_id=record["workspace_id"],
55
57
  metadata={"memory_id": memory_id, "kind": kind, "tags": tags or []},
56
58
  )
57
59
  record["graph_node_id"] = ingested.get("node_id")
@@ -21,10 +21,10 @@ from .workspace_os_utils import (
21
21
  _deep_merge,
22
22
  _json_hash,
23
23
  _listify,
24
- _now,
25
24
  _safe_slug,
26
25
  remove_skill_directory,
27
26
  )
27
+ from .timeutil import now_iso as _now
28
28
  from .workspace_permissions import WorkspacePermissionManager, _member_role # type: ignore
29
29
  from .workspace_timeline import WorkspaceTimeline
30
30
  from .workspace_plugins import WorkspacePluginManager
@@ -49,7 +49,7 @@ __all__ = [
49
49
  "remove_skill_directory",
50
50
  ]
51
51
 
52
- WORKSPACE_OS_VERSION = "8.9.0"
52
+ WORKSPACE_OS_VERSION = "9.1.0"
53
53
 
54
54
  # Workspace types separate single-user Personal workspaces from shared
55
55
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -126,9 +126,6 @@ EXECUTION_EVENT_TYPES = {
126
126
  "execution_interrupted",
127
127
  }
128
128
 
129
- RUN_ACTIVE_STATUSES = {"queued", "running", "in_progress", "retrying", "cancelling"}
130
- RUN_TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
131
-
132
129
  DEFAULT_AGENTS = [
133
130
  {
134
131
  "id": "agent:planner",
@@ -830,8 +827,22 @@ class WorkspaceOSStore:
830
827
  # Graph answer traces
831
828
  # ------------------------------------------------------------------
832
829
 
833
- def build_graph_trace(self, question: str, graph: Any, context: str = "", *, limit: int = 8) -> Dict[str, Any]:
834
- return self.graph_trace.build_graph_trace(question, graph, context, limit=limit)
830
+ def build_graph_trace(
831
+ self,
832
+ question: str,
833
+ graph: Any,
834
+ context: str = "",
835
+ *,
836
+ limit: int = 8,
837
+ allowed_workspaces=None,
838
+ ) -> Dict[str, Any]:
839
+ return self.graph_trace.build_graph_trace(
840
+ question,
841
+ graph,
842
+ context,
843
+ limit=limit,
844
+ allowed_workspaces=allowed_workspaces,
845
+ )
835
846
 
836
847
  def record_trace(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
837
848
  return self.graph_trace.record_trace(*args, **kwargs)
@@ -7,15 +7,12 @@ store class and public constants while preserving exact behavior.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- import os
11
10
  import shutil
12
- from datetime import datetime
13
11
  from pathlib import Path
14
12
  from typing import Any, Dict, List, Optional
15
13
 
16
-
17
- def _now() -> str:
18
- return datetime.now().isoformat(timespec="seconds")
14
+ from .io_utils import atomic_write_json as _atomic_write_json # noqa: F401 - legacy helper re-export
15
+ from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
19
16
 
20
17
 
21
18
  def _safe_slug(raw: str) -> str:
@@ -43,13 +40,6 @@ def _deep_merge(default: Any, loaded: Any) -> Any:
43
40
  return loaded
44
41
 
45
42
 
46
- def _atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
47
- path.parent.mkdir(parents=True, exist_ok=True)
48
- tmp_path = path.with_suffix(path.suffix + ".tmp")
49
- tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
50
- os.replace(tmp_path, path)
51
-
52
-
53
43
  def _listify(value: Any) -> List[Any]:
54
44
  return value if isinstance(value, list) else []
55
45
 
@@ -100,15 +90,6 @@ def _snapshot_graph_import_payload(graph_payload: Dict[str, Any], *, workspace_i
100
90
  }
101
91
 
102
92
 
103
- def _parse_iso(value: Optional[str]) -> Optional[datetime]:
104
- if not value:
105
- return None
106
- try:
107
- return datetime.fromisoformat(str(value))
108
- except (TypeError, ValueError):
109
- return None
110
-
111
-
112
93
  def _file_size(path: Path) -> int:
113
94
  try:
114
95
  return path.stat().st_size
@@ -7,7 +7,8 @@ from __future__ import annotations
7
7
 
8
8
  from typing import Any, Dict, Optional
9
9
 
10
- from .workspace_os_utils import _listify, _now
10
+ from .timeutil import now_iso as _now
11
+ from .workspace_os_utils import _listify
11
12
 
12
13
  # Avoid circular at import time: pull constants lazily from parent module.
13
14
  def _get_role_permissions():
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Any, Dict, Optional
8
8
 
9
- from .workspace_os_utils import _now
9
+ from .timeutil import now_iso as _now
10
10
 
11
11
 
12
12
  class WorkspacePluginManager:
@@ -5,11 +5,10 @@ import json
5
5
  from typing import Any, Dict, List, Optional
6
6
 
7
7
  from lattice_brain.runtime.contracts import run_record_contract, workflow_run_contract
8
+ from lattice_brain.runtime.statuses import RUN_ACTIVE_STATUSES, RUN_TERMINAL_STATUSES
8
9
 
9
- from .workspace_os_utils import _json_hash, _listify, _now
10
-
11
- RUN_ACTIVE_STATUSES = {"queued", "running", "in_progress", "retrying", "cancelling"}
12
- RUN_TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
10
+ from .timeutil import now_iso as _now
11
+ from .workspace_os_utils import _json_hash, _listify
13
12
 
14
13
 
15
14
  class WorkspaceRuns:
@@ -80,6 +79,7 @@ class WorkspaceRuns:
80
79
  f"{agent_id} {status}",
81
80
  user_email=user_email,
82
81
  source="workspace_os",
82
+ workspace_id=resolved_workspace,
83
83
  metadata={"run_id": run["id"], "agent_id": agent_id, "status": status, "mode": mode},
84
84
  )
85
85
  run["graph_node_id"] = ingested.get("node_id")
@@ -167,6 +167,7 @@ class WorkspaceRuns:
167
167
  f"{run.get('agent_id')} {status}",
168
168
  user_email=run.get("user_email"),
169
169
  source="workspace_os",
170
+ workspace_id=resolved_workspace,
170
171
  metadata={
171
172
  "run_id": run_id,
172
173
  "agent_id": run.get("agent_id"),
@@ -250,6 +251,7 @@ class WorkspaceRuns:
250
251
  workflow["name"],
251
252
  user_email=user_email,
252
253
  source="workspace_os",
254
+ workspace_id=workflow["workspace_id"],
253
255
  metadata={"workflow_id": workflow["id"], "steps": steps},
254
256
  )
255
257
  workflow["graph_node_id"] = ingested.get("node_id")
@@ -257,7 +259,12 @@ class WorkspaceRuns:
257
259
  workflow["graph_error"] = str(exc)
258
260
  state.setdefault("workflows", []).append(workflow)
259
261
  self.save_state(state)
260
- self.record_timeline_event("workflow", "workflow_created", {"workflow_id": workflow["id"], "name": workflow["name"]})
262
+ self.record_timeline_event(
263
+ "workflow",
264
+ "workflow_created",
265
+ {"workflow_id": workflow["id"], "name": workflow["name"]},
266
+ workspace_id=workflow["workspace_id"],
267
+ )
261
268
  return workflow
262
269
 
263
270
  def record_workflow_run(
@@ -304,6 +311,7 @@ class WorkspaceRuns:
304
311
  f"{run['name']} {status}",
305
312
  user_email=user_email,
306
313
  source="workspace_os",
314
+ workspace_id=resolved_workspace,
307
315
  metadata={"run_id": run["id"], "workflow_id": workflow_id, "status": status, "mode": mode},
308
316
  )
309
317
  run["graph_node_id"] = ingested.get("node_id")
@@ -380,6 +388,7 @@ class WorkspaceRuns:
380
388
  f"{run.get('name')} {status}",
381
389
  user_email=run.get("user_email"),
382
390
  source="workspace_os",
391
+ workspace_id=resolved_workspace,
383
392
  metadata={
384
393
  "run_id": run_id,
385
394
  "workflow_id": workflow_id,
@@ -5,7 +5,7 @@ import json
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, List, Optional
7
7
 
8
- from .workspace_os_utils import _now
8
+ from .timeutil import now_iso as _now
9
9
 
10
10
 
11
11
  class WorkspaceSkills:
@@ -10,7 +10,8 @@ from datetime import datetime
10
10
  from pathlib import Path
11
11
  from typing import Any, Dict, Iterable, Optional
12
12
 
13
- from .workspace_os_utils import _atomic_write_json, _json_hash, _listify, _now, _safe_slug
13
+ from .timeutil import now_iso as _now
14
+ from .workspace_os_utils import _atomic_write_json, _json_hash, _listify, _safe_slug
14
15
 
15
16
 
16
17
  class WorkspaceSnapshots:
@@ -6,7 +6,8 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Any, Dict, Iterable, List, Optional
8
8
 
9
- from .workspace_os_utils import _listify, _now, _parse_iso
9
+ from .timeutil import now_iso as _now
10
+ from .workspace_os_utils import _listify, _parse_iso
10
11
 
11
12
 
12
13
  def _audit_category(event: Dict[str, Any]) -> str: