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.
- package/README.md +39 -31
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/kg-schema.md +51 -53
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -520
- package/knowledge_graph.py +37 -4629
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +16 -17
- package/latticeai/api/agents.py +20 -7
- package/latticeai/api/auth.py +46 -15
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +139 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +5 -8
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +11 -16
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +85 -6
- package/latticeai/api/workspace.py +93 -57
- package/latticeai/app_factory.py +1781 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +66 -10
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +477 -29
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1536
- package/latticeai/services/agent_runtime.py +243 -4
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +47 -0
- package/latticeai/services/kg_portability.py +33 -3
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +55 -16
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +10 -20
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +105 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +33 -25
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
- package/static/v3/js/core/shell.js +66 -37
- package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
- package/static/v3/js/core/store.js +11 -1
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
- package/static/v3/js/views/knowledge-graph.js +60 -44
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/account.html +0 -115
- package/static/activity.html +0 -73
- package/static/admin.html +0 -488
- package/static/agents.html +0 -139
- package/static/chat.html +0 -844
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -124
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.2ce3815a.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- 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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|