ltcai 3.6.0 → 4.0.1

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 (238) hide show
  1. package/README.md +39 -31
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
  7. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  8. package/docs/kg-schema.md +51 -53
  9. package/docs/spec-vs-impl.md +10 -10
  10. package/kg_schema.py +2 -520
  11. package/knowledge_graph.py +37 -4629
  12. package/knowledge_graph_api.py +11 -127
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/admin.py +16 -17
  15. package/latticeai/api/agents.py +20 -7
  16. package/latticeai/api/auth.py +46 -15
  17. package/latticeai/api/chat.py +112 -76
  18. package/latticeai/api/health.py +1 -1
  19. package/latticeai/api/hooks.py +1 -1
  20. package/latticeai/api/invitations.py +100 -0
  21. package/latticeai/api/knowledge_graph.py +139 -0
  22. package/latticeai/api/local_files.py +1 -1
  23. package/latticeai/api/mcp.py +23 -11
  24. package/latticeai/api/memory.py +1 -1
  25. package/latticeai/api/models.py +1 -1
  26. package/latticeai/api/network.py +81 -0
  27. package/latticeai/api/plugins.py +3 -6
  28. package/latticeai/api/realtime.py +5 -8
  29. package/latticeai/api/search.py +26 -2
  30. package/latticeai/api/security_dashboard.py +2 -3
  31. package/latticeai/api/setup.py +2 -2
  32. package/latticeai/api/static_routes.py +11 -16
  33. package/latticeai/api/tools.py +3 -0
  34. package/latticeai/api/ui_redirects.py +26 -0
  35. package/latticeai/api/workflow_designer.py +85 -6
  36. package/latticeai/api/workspace.py +93 -57
  37. package/latticeai/app_factory.py +1781 -0
  38. package/latticeai/brain/__init__.py +18 -0
  39. package/latticeai/brain/_kg_common.py +1123 -0
  40. package/latticeai/brain/context.py +213 -0
  41. package/latticeai/brain/conversations.py +236 -0
  42. package/latticeai/brain/discovery.py +1455 -0
  43. package/latticeai/brain/documents.py +218 -0
  44. package/latticeai/brain/identity.py +175 -0
  45. package/latticeai/brain/ingest.py +644 -0
  46. package/latticeai/brain/memory.py +102 -0
  47. package/latticeai/brain/network.py +205 -0
  48. package/latticeai/brain/projection.py +561 -0
  49. package/latticeai/brain/provenance.py +401 -0
  50. package/latticeai/brain/retrieval.py +1316 -0
  51. package/latticeai/brain/schema.py +640 -0
  52. package/latticeai/brain/store.py +216 -0
  53. package/latticeai/brain/write_master.py +225 -0
  54. package/latticeai/core/agent.py +31 -7
  55. package/latticeai/core/audit.py +0 -7
  56. package/latticeai/core/config.py +1 -1
  57. package/latticeai/core/context_builder.py +1 -2
  58. package/latticeai/core/enterprise.py +1 -1
  59. package/latticeai/core/graph_curator.py +2 -2
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/mcp_registry.py +791 -0
  63. package/latticeai/core/model_compat.py +1 -1
  64. package/latticeai/core/model_resolution.py +0 -1
  65. package/latticeai/core/multi_agent.py +238 -4
  66. package/latticeai/core/policy.py +54 -0
  67. package/latticeai/core/realtime.py +65 -44
  68. package/latticeai/core/security.py +1 -1
  69. package/latticeai/core/sessions.py +66 -10
  70. package/latticeai/core/users.py +147 -0
  71. package/latticeai/core/workflow_engine.py +114 -2
  72. package/latticeai/core/workspace_os.py +477 -29
  73. package/latticeai/models/__init__.py +7 -0
  74. package/latticeai/models/router.py +779 -0
  75. package/latticeai/server_app.py +29 -1536
  76. package/latticeai/services/agent_runtime.py +243 -4
  77. package/latticeai/services/app_context.py +75 -14
  78. package/latticeai/services/ingestion.py +47 -0
  79. package/latticeai/services/kg_portability.py +33 -3
  80. package/latticeai/services/memory_service.py +39 -11
  81. package/latticeai/services/model_runtime.py +2 -5
  82. package/latticeai/services/platform_runtime.py +100 -23
  83. package/latticeai/services/run_executor.py +328 -0
  84. package/latticeai/services/search_service.py +17 -8
  85. package/latticeai/services/tool_dispatch.py +12 -2
  86. package/latticeai/services/triggers.py +241 -0
  87. package/latticeai/services/upload_service.py +37 -12
  88. package/latticeai/services/workspace_service.py +55 -16
  89. package/llm_router.py +29 -772
  90. package/ltcai_cli.py +1 -2
  91. package/mcp_registry.py +25 -788
  92. package/p_reinforce.py +124 -14
  93. package/package.json +10 -20
  94. package/scripts/bump_version.py +99 -0
  95. package/scripts/generate_diagrams.py +0 -1
  96. package/scripts/lint_v3.mjs +105 -18
  97. package/scripts/validate_release_artifacts.py +0 -1
  98. package/scripts/wheel_smoke.py +142 -0
  99. package/server.py +11 -7
  100. package/setup_wizard.py +1142 -0
  101. package/static/sw.js +81 -52
  102. package/static/v3/asset-manifest.json +33 -25
  103. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  104. package/static/v3/css/lattice.base.css +1 -1
  105. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  106. package/static/v3/css/lattice.components.css +1 -1
  107. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  108. package/static/v3/css/lattice.shell.css +1 -1
  109. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  110. package/static/v3/css/lattice.tokens.css +3 -0
  111. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  112. package/static/v3/css/lattice.views.css +2 -2
  113. package/static/v3/index.html +3 -4
  114. package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
  115. package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
  116. package/static/v3/js/core/api.js +57 -0
  117. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  118. package/static/v3/js/core/i18n.js +575 -0
  119. package/static/v3/js/core/routes.37522821.js +101 -0
  120. package/static/v3/js/core/routes.js +71 -63
  121. package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
  122. package/static/v3/js/core/shell.js +66 -37
  123. package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
  124. package/static/v3/js/core/store.js +11 -1
  125. package/static/v3/js/views/account.eff40715.js +143 -0
  126. package/static/v3/js/views/account.js +143 -0
  127. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  128. package/static/v3/js/views/activity.js +67 -0
  129. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  130. package/static/v3/js/views/admin-users.js +4 -6
  131. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  132. package/static/v3/js/views/agents.js +35 -12
  133. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  134. package/static/v3/js/views/chat.js +23 -0
  135. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  136. package/static/v3/js/views/graph-canvas.js +509 -0
  137. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  138. package/static/v3/js/views/hybrid-search.js +1 -2
  139. package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
  140. package/static/v3/js/views/knowledge-graph.js +60 -44
  141. package/static/v3/js/views/network.52a4f181.js +97 -0
  142. package/static/v3/js/views/network.js +97 -0
  143. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  144. package/static/v3/js/views/planning.js +26 -5
  145. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  146. package/static/v3/js/views/runs.js +144 -0
  147. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  148. package/static/v3/js/views/settings.js +7 -8
  149. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  150. package/static/v3/js/views/snapshots.js +135 -0
  151. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  152. package/static/v3/js/views/workflows.js +87 -2
  153. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  154. package/static/v3/js/views/workspace-admin.js +156 -0
  155. package/static/vendor/chart.umd.min.js +20 -0
  156. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  157. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  158. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  159. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  160. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  161. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  162. package/static/vendor/fonts/inter.css +44 -0
  163. package/static/vendor/icons/tabler-icons.min.css +4 -0
  164. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  165. package/static/vendor/marked.min.js +69 -0
  166. package/telegram_bot.py +1 -2
  167. package/tools/commands.py +4 -2
  168. package/tools/computer.py +1 -1
  169. package/tools/documents.py +1 -3
  170. package/tools/filesystem.py +0 -4
  171. package/tools/knowledge.py +1 -3
  172. package/tools/network.py +1 -3
  173. package/codex_telegram_bot.py +0 -195
  174. package/docs/assets/v3.4.0/agent-run.png +0 -0
  175. package/docs/assets/v3.4.0/agents.png +0 -0
  176. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  177. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  178. package/docs/assets/v3.4.0/chat.png +0 -0
  179. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  180. package/docs/assets/v3.4.0/files.png +0 -0
  181. package/docs/assets/v3.4.0/home.png +0 -0
  182. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  183. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  184. package/docs/assets/v3.4.0/local-agent.png +0 -0
  185. package/docs/assets/v3.4.0/memory.png +0 -0
  186. package/docs/assets/v3.4.0/settings.png +0 -0
  187. package/docs/assets/v3.4.0/vision-input.png +0 -0
  188. package/docs/assets/v3.4.0/workflows.png +0 -0
  189. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  190. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  191. package/docs/assets/v3.4.1/local-agent.png +0 -0
  192. package/docs/images/admin-dashboard.png +0 -0
  193. package/docs/images/architecture.png +0 -0
  194. package/docs/images/enterprise.png +0 -0
  195. package/docs/images/graph.png +0 -0
  196. package/docs/images/hero.gif +0 -0
  197. package/docs/images/knowledge-graph.png +0 -0
  198. package/docs/images/lattice-ai-demo.gif +0 -0
  199. package/docs/images/lattice-ai-hero.png +0 -0
  200. package/docs/images/logo.svg +0 -33
  201. package/docs/images/mobile-responsive.png +0 -0
  202. package/docs/images/model-recommendation.png +0 -0
  203. package/docs/images/onboarding.png +0 -0
  204. package/docs/images/organization.png +0 -0
  205. package/docs/images/pipeline.png +0 -0
  206. package/docs/images/screenshot-admin.png +0 -0
  207. package/docs/images/screenshot-chat.png +0 -0
  208. package/docs/images/screenshot-graph.png +0 -0
  209. package/docs/images/skills.png +0 -0
  210. package/docs/images/workspace-dark.png +0 -0
  211. package/docs/images/workspace-light.png +0 -0
  212. package/docs/images/workspace.png +0 -0
  213. package/requirements.txt +0 -16
  214. package/static/account.html +0 -115
  215. package/static/activity.html +0 -73
  216. package/static/admin.html +0 -488
  217. package/static/agents.html +0 -139
  218. package/static/chat.html +0 -844
  219. package/static/css/reference/account.css +0 -439
  220. package/static/css/reference/admin.css +0 -610
  221. package/static/css/reference/base.css +0 -1661
  222. package/static/css/reference/chat.css +0 -4623
  223. package/static/css/reference/graph.css +0 -1016
  224. package/static/css/responsive.css +0 -861
  225. package/static/graph.html +0 -124
  226. package/static/platform.css +0 -104
  227. package/static/plugins.html +0 -136
  228. package/static/scripts/account.js +0 -238
  229. package/static/scripts/admin.js +0 -1614
  230. package/static/scripts/chat.js +0 -5081
  231. package/static/scripts/graph.js +0 -1804
  232. package/static/scripts/platform.js +0 -64
  233. package/static/scripts/ux.js +0 -167
  234. package/static/scripts/workspace.js +0 -948
  235. package/static/v3/js/core/routes.2ce3815a.js +0 -93
  236. package/static/workflows.html +0 -146
  237. package/static/workspace.css +0 -1121
  238. package/static/workspace.html +0 -357
@@ -15,12 +15,14 @@ from __future__ import annotations
15
15
  import asyncio
16
16
  import logging
17
17
  from datetime import datetime
18
- from pathlib import Path
19
- from typing import Any, Callable, Dict, List, Optional
18
+ from typing import Dict, List, Optional
20
19
 
21
20
  from fastapi import APIRouter, HTTPException, Request
22
21
  from pydantic import BaseModel
23
22
 
23
+ from latticeai.api.ui_redirects import app_redirect
24
+ from latticeai.services.app_context import AppContext
25
+
24
26
 
25
27
  # ── Request models (workspace-only; moved verbatim from server_app) ──────────
26
28
 
@@ -135,54 +137,45 @@ def _workspace_scope_from_request(request: Request) -> Optional[str]:
135
137
  return query.strip() if query and query.strip() else None
136
138
 
137
139
 
138
- def create_workspace_router(
139
- *,
140
- service,
141
- require_user: Callable[[Request], str],
142
- require_admin: Callable[[Request], Any],
143
- get_current_user: Callable[[Request], Optional[str]],
144
- append_audit_event: Callable[..., None],
145
- graph_stats: Callable[[], Dict],
146
- workspace_models: Callable[[], Dict],
147
- workspace_settings: Callable[[], Dict],
148
- get_history: Callable[[], List[Dict]],
149
- get_audit_log: Callable[[], List[Dict]],
150
- require_graph: Callable[[], Any],
151
- workspace_graph: Callable[[], Any],
152
- knowledge_graph: Any,
153
- local_kg_watcher: Any,
154
- load_users: Callable[[], Dict],
155
- scan_environment: Callable[[], Any],
156
- local_sysinfo: Callable[[Request], Any],
157
- get_recommendations: Callable[[Any], Any],
158
- fetch_skills_marketplace: Callable[..., Any],
159
- install_skill: Callable[..., Any],
160
- remove_skill_directory: Callable[..., Dict],
161
- redact_secret_text: Callable[[str], str],
162
- skills_dir: Path,
163
- capability_registry: Any,
164
- ui_file_response: Callable[[Path], Any],
165
- static_dir: Path,
166
- local_model: Optional[str],
167
- public_model: Optional[str],
168
- ) -> APIRouter:
140
+ def create_workspace_router(context: AppContext) -> APIRouter:
141
+ """Build the workspace/org router from the typed application context.
142
+
143
+ Replaces the historical ~30-kwarg factory signature: ``context``
144
+ (:class:`latticeai.services.app_context.AppContext`) carries the same
145
+ dependencies as typed fields.
146
+ """
169
147
  router = APIRouter()
170
148
 
171
149
  # Bind injected deps to the names the moved handler bodies expect.
150
+ service = context.workspace_service
151
+ require_user = context.require_user
152
+ require_admin = context.require_admin
153
+ get_current_user = context.get_current_user
154
+ append_audit_event = context.append_audit_event
155
+ get_history = context.get_history
156
+ get_audit_log = context.get_audit_log
157
+ load_users = context.load_users
158
+ scan_environment = context.scan_environment
159
+ local_sysinfo = context.local_sysinfo
160
+ get_recommendations = context.get_recommendations
161
+ install_skill = context.install_skill
162
+ remove_skill_directory = context.remove_skill_directory
163
+ redact_secret_text = context.redact_secret_text
164
+ capability_registry = context.capability_registry
165
+
172
166
  svc = service
173
167
  WORKSPACE_OS = service.store
174
- _workspace_graph = workspace_graph
175
- _graph_stats_safe = graph_stats
176
- _workspace_models_payload = workspace_models
177
- _workspace_settings_payload = workspace_settings
178
- _require_graph = require_graph
179
- KNOWLEDGE_GRAPH = knowledge_graph
180
- LOCAL_KG_WATCHER = local_kg_watcher
181
- SKILLS_DIR = skills_dir
182
- STATIC_DIR = static_dir
183
- LOCAL_MODEL = local_model
184
- PUBLIC_MODEL = public_model
185
- _fetch_skills_marketplace = fetch_skills_marketplace
168
+ _workspace_graph = context.workspace_graph
169
+ _graph_stats_safe = context.graph_stats
170
+ _workspace_models_payload = context.workspace_models
171
+ _workspace_settings_payload = context.workspace_settings
172
+ _require_graph = context.require_graph
173
+ KNOWLEDGE_GRAPH = context.knowledge_graph
174
+ LOCAL_KG_WATCHER = context.local_kg_watcher
175
+ SKILLS_DIR = context.skills_dir
176
+ LOCAL_MODEL = context.local_model
177
+ PUBLIC_MODEL = context.public_model
178
+ _fetch_skills_marketplace = context.fetch_skills_marketplace
186
179
  _workspace_scope = _workspace_scope_from_request
187
180
 
188
181
  def _gate_read(request: Request):
@@ -197,23 +190,35 @@ def create_workspace_router(
197
190
  except PermissionError as exc:
198
191
  raise HTTPException(status_code=403, detail=str(exc)) from exc
199
192
 
193
+ def _load_snapshot_authorized(request: Request, snapshot_id: str) -> dict:
194
+ """Fetch a snapshot and authorize against the RECORD'S own workspace.
195
+
196
+ By-id access must not bypass workspace gating: a snapshot belonging to
197
+ an organization workspace is readable only by its members. Snapshots
198
+ predating workspace scoping carry no workspace_id and stay readable
199
+ (legacy-global compatibility).
200
+ """
201
+ try:
202
+ snapshot = WORKSPACE_OS.get_snapshot(snapshot_id)
203
+ except FileNotFoundError as exc:
204
+ raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
205
+ try:
206
+ svc.authorize_record_read(snapshot, get_current_user(request))
207
+ except PermissionError as exc:
208
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
209
+ return snapshot
210
+
200
211
  # ── Workspace UI pages ────────────────────────────────────────────────
201
212
 
202
213
  @router.get("/workspace")
203
214
  async def workspace_page(request: Request):
204
215
  require_user(request)
205
- workspace_path = STATIC_DIR / "workspace.html"
206
- if not workspace_path.exists():
207
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
208
- return ui_file_response(workspace_path)
216
+ return app_redirect("workspace-admin", request)
209
217
 
210
218
  @router.get("/onboarding")
211
219
  async def onboarding_page(request: Request):
212
220
  require_user(request)
213
- workspace_path = STATIC_DIR / "workspace.html"
214
- if not workspace_path.exists():
215
- raise HTTPException(status_code=404, detail="Workspace OS UI not found.")
216
- return ui_file_response(workspace_path)
221
+ return app_redirect("workspace-admin", request)
217
222
 
218
223
  # ── Workspace OS summary / onboarding ─────────────────────────────────
219
224
 
@@ -343,6 +348,8 @@ def create_workspace_router(
343
348
  @router.post("/workspace/snapshots/compare")
344
349
  async def workspace_snapshot_compare(req: WorkspaceSnapshotCompareRequest, request: Request):
345
350
  require_user(request)
351
+ _load_snapshot_authorized(request, req.before_id)
352
+ _load_snapshot_authorized(request, req.after_id)
346
353
  try:
347
354
  return WORKSPACE_OS.compare_snapshots(req.before_id, req.after_id)
348
355
  except FileNotFoundError as exc:
@@ -351,14 +358,12 @@ def create_workspace_router(
351
358
  @router.get("/workspace/snapshots/{snapshot_id}")
352
359
  async def workspace_snapshot_get(snapshot_id: str, request: Request):
353
360
  require_user(request)
354
- try:
355
- return WORKSPACE_OS.get_snapshot(snapshot_id)
356
- except FileNotFoundError as exc:
357
- raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
361
+ return _load_snapshot_authorized(request, snapshot_id)
358
362
 
359
363
  @router.get("/workspace/snapshots/{snapshot_id}/{area}")
360
364
  async def workspace_snapshot_area(snapshot_id: str, area: str, request: Request):
361
365
  require_user(request)
366
+ _load_snapshot_authorized(request, snapshot_id)
362
367
  try:
363
368
  return WORKSPACE_OS.snapshot_view(snapshot_id, area)
364
369
  except FileNotFoundError as exc:
@@ -367,6 +372,7 @@ def create_workspace_router(
367
372
  @router.post("/workspace/snapshots/{snapshot_id}/export")
368
373
  async def workspace_snapshot_export(snapshot_id: str, request: Request):
369
374
  current_user = require_user(request)
375
+ _load_snapshot_authorized(request, snapshot_id)
370
376
  try:
371
377
  result = WORKSPACE_OS.export_snapshot(snapshot_id)
372
378
  except FileNotFoundError as exc:
@@ -374,6 +380,27 @@ def create_workspace_router(
374
380
  append_audit_event("workspace_snapshot_export", user_email=current_user, snapshot_id=snapshot_id, path=result.get("export_path"))
375
381
  return result
376
382
 
383
+ @router.post("/workspace/snapshots/{snapshot_id}/restore")
384
+ async def workspace_snapshot_restore(snapshot_id: str, request: Request):
385
+ current_user = require_user(request)
386
+ snapshot = _load_snapshot_authorized(request, snapshot_id)
387
+ scope = _gate_write(request)
388
+ if snapshot.get("workspace_id") and snapshot.get("workspace_id") != scope:
389
+ raise HTTPException(status_code=403, detail="snapshot belongs to a different workspace")
390
+ try:
391
+ result = WORKSPACE_OS.restore_snapshot(
392
+ snapshot_id,
393
+ graph=_workspace_graph(),
394
+ workspace_id=scope,
395
+ user_email=current_user or None,
396
+ )
397
+ except FileNotFoundError as exc:
398
+ raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
399
+ except ValueError as exc:
400
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
401
+ append_audit_event("workspace_snapshot_restore", user_email=current_user, snapshot_id=snapshot_id, restore_id=result.get("restore", {}).get("id"))
402
+ return result
403
+
377
404
  @router.get("/workspace/time-machine")
378
405
  async def workspace_time_machine(request: Request, limit: int = 100):
379
406
  require_user(request)
@@ -383,6 +410,7 @@ def create_workspace_router(
383
410
  @router.get("/workspace/time-machine/{snapshot_id}/{area}")
384
411
  async def workspace_time_machine_view(snapshot_id: str, area: str, request: Request):
385
412
  require_user(request)
413
+ _load_snapshot_authorized(request, snapshot_id)
386
414
  try:
387
415
  return WORKSPACE_OS.snapshot_view(snapshot_id, area)
388
416
  except FileNotFoundError as exc:
@@ -424,6 +452,14 @@ def create_workspace_router(
424
452
  @router.delete("/workspace/memories/{memory_id}")
425
453
  async def workspace_memory_delete(memory_id: str, request: Request):
426
454
  require_user(request)
455
+ try:
456
+ record = WORKSPACE_OS.get_memory(memory_id)
457
+ except FileNotFoundError as exc:
458
+ raise HTTPException(status_code=404, detail=f"Memory not found: {exc}") from exc
459
+ try:
460
+ svc.authorize_memory_delete(record, get_current_user(request))
461
+ except PermissionError as exc:
462
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
427
463
  try:
428
464
  return WORKSPACE_OS.delete_memory(memory_id)
429
465
  except FileNotFoundError as exc: