ltcai 3.6.0 → 4.0.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.
- package/README.md +11 -7
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/kg-schema.md +47 -53
- package/kg_schema.py +93 -10
- package/knowledge_graph.py +362 -33
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +7 -1
- package/latticeai/api/auth.py +27 -4
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/knowledge_graph.py +146 -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/realtime.py +1 -1
- 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 +2 -4
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/workflow_designer.py +46 -0
- package/latticeai/api/workspace.py +71 -49
- package/latticeai/app_factory.py +1710 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -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/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/security.py +1 -1
- package/latticeai/core/sessions.py +37 -7
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +58 -10
- 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 +1 -0
- 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/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 +31 -0
- 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 +9 -7
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +82 -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/account.html +2 -4
- package/static/admin.html +3 -5
- package/static/chat.html +3 -6
- package/static/graph.html +2 -4
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +20 -19
- 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.356e6452.js} +1 -1
- package/static/v3/js/core/{api.33d6320e.js → api.7a308b89.js} +1 -1
- package/static/v3/js/core/{routes.2ce3815a.js → routes.7222343d.js} +22 -22
- package/static/v3/js/core/routes.js +22 -22
- package/static/v3/js/core/{shell.8c163e0e.js → shell.a1657f20.js} +4 -4
- package/static/v3/js/core/shell.js +1 -1
- package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
- package/static/v3/js/core/store.js +1 -1
- 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.5e40cbeb.js} +33 -37
- package/static/v3/js/views/knowledge-graph.js +33 -37
- 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/static/workspace.html +2 -2
- 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/latticeai/api/models.py
CHANGED
|
@@ -264,7 +264,7 @@ def create_models_router(
|
|
|
264
264
|
|
|
265
265
|
@router.post("/setup/set-api-key")
|
|
266
266
|
async def set_api_key(req: SetApiKeyRequest, request: Request):
|
|
267
|
-
from
|
|
267
|
+
from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS
|
|
268
268
|
config = OPENAI_COMPATIBLE_PROVIDERS.get(req.provider)
|
|
269
269
|
if not config:
|
|
270
270
|
raise HTTPException(status_code=400, detail="알 수 없는 프로바이더입니다.")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Brain Network API — device identity, peer pairing, knowledge exchange.
|
|
2
|
+
|
|
3
|
+
The /network/receive endpoint authenticates PEERS (signed device requests),
|
|
4
|
+
not user sessions; everything else requires a logged-in user, and pairing /
|
|
5
|
+
pushing are deliberate owner actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PeerPairRequest(BaseModel):
|
|
17
|
+
name: str
|
|
18
|
+
base_url: str
|
|
19
|
+
public_key: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PeerPushRequest(BaseModel):
|
|
23
|
+
workspace_id: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_network_router(*, network, identity, require_user) -> APIRouter:
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
|
|
29
|
+
@router.get("/network/identity")
|
|
30
|
+
async def network_identity(request: Request):
|
|
31
|
+
require_user(request)
|
|
32
|
+
return identity.describe()
|
|
33
|
+
|
|
34
|
+
@router.get("/network/peers")
|
|
35
|
+
async def network_peers(request: Request):
|
|
36
|
+
require_user(request)
|
|
37
|
+
return {"peers": network.list_peers()}
|
|
38
|
+
|
|
39
|
+
@router.post("/network/peers")
|
|
40
|
+
async def network_pair(req: PeerPairRequest, request: Request):
|
|
41
|
+
require_user(request)
|
|
42
|
+
try:
|
|
43
|
+
return {"status": "paired", "peer": network.add_peer(
|
|
44
|
+
name=req.name, base_url=req.base_url, public_key=req.public_key,
|
|
45
|
+
)}
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
48
|
+
|
|
49
|
+
@router.delete("/network/peers/{peer_id}")
|
|
50
|
+
async def network_unpair(peer_id: str, request: Request):
|
|
51
|
+
require_user(request)
|
|
52
|
+
try:
|
|
53
|
+
return network.remove_peer(peer_id)
|
|
54
|
+
except FileNotFoundError as exc:
|
|
55
|
+
raise HTTPException(status_code=404, detail=f"Unknown peer: {exc}") from exc
|
|
56
|
+
|
|
57
|
+
@router.post("/network/push/{peer_id}")
|
|
58
|
+
async def network_push(peer_id: str, req: PeerPushRequest, request: Request):
|
|
59
|
+
require_user(request)
|
|
60
|
+
try:
|
|
61
|
+
return network.push_to_peer(peer_id, workspace_id=req.workspace_id)
|
|
62
|
+
except FileNotFoundError as exc:
|
|
63
|
+
raise HTTPException(status_code=404, detail=f"Unknown peer: {exc}") from exc
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
raise HTTPException(status_code=502, detail=f"Push failed: {exc}") from exc
|
|
66
|
+
|
|
67
|
+
@router.post("/network/receive")
|
|
68
|
+
async def network_receive(request: Request):
|
|
69
|
+
# Peer-authenticated: a paired device's signature replaces the session.
|
|
70
|
+
body = await request.body()
|
|
71
|
+
try:
|
|
72
|
+
return network.receive(dict(request.headers), body)
|
|
73
|
+
except PermissionError as exc:
|
|
74
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
77
|
+
|
|
78
|
+
return router
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["create_network_router"]
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import secrets
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Callable,
|
|
13
|
+
from typing import Any, Callable, Optional, Set
|
|
14
14
|
|
|
15
15
|
from fastapi import APIRouter, HTTPException, Request
|
|
16
16
|
from fastapi.responses import StreamingResponse
|
package/latticeai/api/search.py
CHANGED
|
@@ -47,17 +47,41 @@ class IndexRebuildRequest(BaseModel):
|
|
|
47
47
|
include_chunks: bool = True
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
class _ScopedSearchService:
|
|
51
|
+
"""Injects the caller's workspace scope into every search call —
|
|
52
|
+
enforcement lives at this one chokepoint, not in each handler."""
|
|
53
|
+
|
|
54
|
+
_SCOPED = {"keyword_search", "vector_search", "graph_search", "hybrid_search"}
|
|
55
|
+
|
|
56
|
+
def __init__(self, service: SearchService, allowed):
|
|
57
|
+
self._service = service
|
|
58
|
+
self._allowed = allowed
|
|
59
|
+
|
|
60
|
+
def __getattr__(self, name):
|
|
61
|
+
attr = getattr(self._service, name)
|
|
62
|
+
if name in self._SCOPED:
|
|
63
|
+
def scoped(*args, **kwargs):
|
|
64
|
+
kwargs.setdefault("allowed_workspaces", self._allowed)
|
|
65
|
+
return attr(*args, **kwargs)
|
|
66
|
+
return scoped
|
|
67
|
+
return attr
|
|
68
|
+
|
|
69
|
+
|
|
50
70
|
def create_search_router(
|
|
51
71
|
*,
|
|
52
72
|
service: SearchService,
|
|
53
73
|
require_user: Callable[[Request], str],
|
|
54
74
|
embedding_info: Optional[Callable[[], Dict[str, Any]]] = None,
|
|
75
|
+
allowed_workspaces_for: Optional[Callable[[Optional[str]], Any]] = None,
|
|
55
76
|
) -> APIRouter:
|
|
56
77
|
router = APIRouter()
|
|
57
78
|
|
|
58
79
|
def _guarded(request: Request) -> SearchService:
|
|
59
|
-
require_user(request)
|
|
60
|
-
|
|
80
|
+
user = require_user(request)
|
|
81
|
+
allowed = None
|
|
82
|
+
if allowed_workspaces_for is not None and user:
|
|
83
|
+
allowed = allowed_workspaces_for(user)
|
|
84
|
+
return _ScopedSearchService(service, allowed)
|
|
61
85
|
|
|
62
86
|
def _raise_graph_error(exc: Exception) -> None:
|
|
63
87
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
@@ -27,13 +27,12 @@ import io
|
|
|
27
27
|
import json
|
|
28
28
|
import logging
|
|
29
29
|
import re
|
|
30
|
-
import time
|
|
31
30
|
from collections import defaultdict
|
|
32
31
|
from datetime import datetime
|
|
33
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
32
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
34
33
|
|
|
35
34
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
36
|
-
from fastapi.responses import Response
|
|
35
|
+
from fastapi.responses import Response
|
|
37
36
|
from pydantic import BaseModel
|
|
38
37
|
|
|
39
38
|
from ..core import timezones
|
package/latticeai/api/setup.py
CHANGED
|
@@ -15,8 +15,8 @@ from auto_setup import (
|
|
|
15
15
|
recommend as auto_setup_recommend,
|
|
16
16
|
verify as auto_setup_verify,
|
|
17
17
|
)
|
|
18
|
-
from
|
|
19
|
-
from
|
|
18
|
+
from latticeai.models.router import parse_model_ref
|
|
19
|
+
from setup_wizard import get_recommendations, install_stream, open_url, scan_environment
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def create_setup_router(*, model_router, require_user) -> APIRouter:
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
import subprocess
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from pathlib import Path
|
|
@@ -58,7 +57,7 @@ def create_static_routes_router(
|
|
|
58
57
|
return response
|
|
59
58
|
|
|
60
59
|
# 3. 인증 실패 시 차단 화면
|
|
61
|
-
return HTMLResponse(content=
|
|
60
|
+
return HTMLResponse(content="""
|
|
62
61
|
<body style="background:#0f1115; color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; font-family:sans-serif;">
|
|
63
62
|
<div style="background:#16191f; padding:40px; border-radius:24px; border:1px solid rgba(255,255,255,0.1); text-align:center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
|
|
64
63
|
<div style="font-size:48px; margin-bottom:20px;">🔒</div>
|
|
@@ -145,7 +144,7 @@ def create_static_routes_router(
|
|
|
145
144
|
async def local_sysinfo(request: Request):
|
|
146
145
|
"""CPU / RAM / GPU(MLX) 사용량을 반환합니다."""
|
|
147
146
|
require_user(request)
|
|
148
|
-
import
|
|
147
|
+
import re as _re
|
|
149
148
|
result = {"cpu_pct": 0.0, "ram_pct": 0.0, "gpu_mem_pct": 0.0, "gpu_mem_gb": 0.0}
|
|
150
149
|
try:
|
|
151
150
|
# CPU
|
|
@@ -157,7 +156,6 @@ def create_static_routes_router(
|
|
|
157
156
|
result["cpu_pct"] = round(float(m.group(1)) + float(m.group(2)), 1)
|
|
158
157
|
# RAM
|
|
159
158
|
vm_out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=4).stdout
|
|
160
|
-
page_size = 16384
|
|
161
159
|
pages: dict = {}
|
|
162
160
|
for line in vm_out.splitlines():
|
|
163
161
|
for key in ["Pages free", "Pages active", "Pages inactive", "Pages wired down", "Pages occupied by compressor"]:
|
package/latticeai/api/tools.py
CHANGED
|
@@ -178,6 +178,7 @@ class ToolGitShowRequest(BaseModel):
|
|
|
178
178
|
def create_tools_router(
|
|
179
179
|
*,
|
|
180
180
|
config,
|
|
181
|
+
ingestion_pipeline,
|
|
181
182
|
data_dir: Path,
|
|
182
183
|
static_dir: Path,
|
|
183
184
|
model_router,
|
|
@@ -438,6 +439,7 @@ def create_tools_router(
|
|
|
438
439
|
current_user=current_user,
|
|
439
440
|
enable_graph=ENABLE_GRAPH,
|
|
440
441
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
442
|
+
ingestion_pipeline=ingestion_pipeline,
|
|
441
443
|
bytes_match_extension=_bytes_match_extension,
|
|
442
444
|
classify_sensitive_message=classify_sensitive_message,
|
|
443
445
|
append_audit_event=append_audit_event,
|
|
@@ -588,6 +590,7 @@ def create_tools_router(
|
|
|
588
590
|
tool_response=_tool_response,
|
|
589
591
|
require_graph=_require_graph,
|
|
590
592
|
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
593
|
+
ingestion_pipeline=ingestion_pipeline,
|
|
591
594
|
data_dir=DATA_DIR,
|
|
592
595
|
))
|
|
593
596
|
|
|
@@ -32,6 +32,10 @@ class WorkflowUpdateRequest(BaseModel):
|
|
|
32
32
|
metadata: Optional[Dict[str, Any]] = None
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class WorkflowResumeRequest(BaseModel):
|
|
36
|
+
approved: bool = True
|
|
37
|
+
|
|
38
|
+
|
|
35
39
|
class WorkflowRunRequest(BaseModel):
|
|
36
40
|
inputs: Dict[str, Any] = {}
|
|
37
41
|
|
|
@@ -159,10 +163,52 @@ def create_workflow_designer_router(
|
|
|
159
163
|
user_email=current_user or None,
|
|
160
164
|
graph=workspace_graph(),
|
|
161
165
|
workspace_id=scope,
|
|
166
|
+
mode="live",
|
|
167
|
+
pause={"node": result.paused_node, "pending": result.pending_approval,
|
|
168
|
+
"context": result.paused_context} if result.status == "awaiting_approval" else None,
|
|
162
169
|
)
|
|
163
170
|
append_audit_event("workflow_run", user_email=current_user, workflow_id=workflow_id, status=result.status)
|
|
164
171
|
return {"run": run, "result": result.as_dict()}
|
|
165
172
|
|
|
173
|
+
@router.post("/workflows/api/runs/{run_id}/resume")
|
|
174
|
+
async def resume_run(run_id: str, req: WorkflowResumeRequest, request: Request):
|
|
175
|
+
"""Decide a paused (awaiting_approval) run: approve → the paused node
|
|
176
|
+
executes and the run continues; deny → the run fails honestly."""
|
|
177
|
+
current_user = require_user(request)
|
|
178
|
+
scope = gate_write(request)
|
|
179
|
+
run_record = store.get_workflow_run(run_id, workspace_id=scope)
|
|
180
|
+
pause = run_record.get("pause") or {}
|
|
181
|
+
if run_record.get("status") != "awaiting_approval" or not pause.get("node"):
|
|
182
|
+
raise HTTPException(status_code=409, detail="run is not awaiting approval")
|
|
183
|
+
workflow = store.get_workflow(run_record.get("workflow_id"), workspace_id=scope)
|
|
184
|
+
runners = build_runners(current_user or None, scope)
|
|
185
|
+
engine = WorkflowEngine(runners, hooks=hooks)
|
|
186
|
+
result = engine.resume(
|
|
187
|
+
workflow,
|
|
188
|
+
paused_node=pause["node"],
|
|
189
|
+
paused_context=pause.get("context") or {},
|
|
190
|
+
approved=bool(req.approved),
|
|
191
|
+
prior_timeline=run_record.get("timeline") or [],
|
|
192
|
+
)
|
|
193
|
+
resumed = store.record_workflow_run(
|
|
194
|
+
workflow_id=run_record.get("workflow_id"),
|
|
195
|
+
name=run_record.get("name") or "workflow",
|
|
196
|
+
status=result.status,
|
|
197
|
+
timeline=result.timeline,
|
|
198
|
+
outputs=result.outputs,
|
|
199
|
+
user_email=current_user or None,
|
|
200
|
+
graph=workspace_graph(),
|
|
201
|
+
workspace_id=scope,
|
|
202
|
+
mode="live",
|
|
203
|
+
pause={"node": result.paused_node, "pending": result.pending_approval,
|
|
204
|
+
"context": result.paused_context} if result.status == "awaiting_approval" else None,
|
|
205
|
+
)
|
|
206
|
+
store.mark_workflow_run_resolved(run_id, resumed_run_id=resumed["id"],
|
|
207
|
+
approved=bool(req.approved), workspace_id=scope)
|
|
208
|
+
append_audit_event("workflow_run_resume", user_email=current_user,
|
|
209
|
+
run_id=run_id, approved=bool(req.approved), status=result.status)
|
|
210
|
+
return {"run": resumed, "result": result.as_dict(), "resumed_from": run_id}
|
|
211
|
+
|
|
166
212
|
@router.get("/workflows/api/definitions/{workflow_id}/runs")
|
|
167
213
|
async def list_runs(workflow_id: str, request: Request, limit: int = 50):
|
|
168
214
|
require_user(request)
|
|
@@ -15,12 +15,13 @@ 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.services.app_context import AppContext
|
|
24
|
+
|
|
24
25
|
|
|
25
26
|
# ── Request models (workspace-only; moved verbatim from server_app) ──────────
|
|
26
27
|
|
|
@@ -135,54 +136,47 @@ def _workspace_scope_from_request(request: Request) -> Optional[str]:
|
|
|
135
136
|
return query.strip() if query and query.strip() else None
|
|
136
137
|
|
|
137
138
|
|
|
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:
|
|
139
|
+
def create_workspace_router(context: AppContext) -> APIRouter:
|
|
140
|
+
"""Build the workspace/org router from the typed application context.
|
|
141
|
+
|
|
142
|
+
Replaces the historical ~30-kwarg factory signature: ``context``
|
|
143
|
+
(:class:`latticeai.services.app_context.AppContext`) carries the same
|
|
144
|
+
dependencies as typed fields.
|
|
145
|
+
"""
|
|
169
146
|
router = APIRouter()
|
|
170
147
|
|
|
171
148
|
# Bind injected deps to the names the moved handler bodies expect.
|
|
149
|
+
service = context.workspace_service
|
|
150
|
+
require_user = context.require_user
|
|
151
|
+
require_admin = context.require_admin
|
|
152
|
+
get_current_user = context.get_current_user
|
|
153
|
+
append_audit_event = context.append_audit_event
|
|
154
|
+
get_history = context.get_history
|
|
155
|
+
get_audit_log = context.get_audit_log
|
|
156
|
+
load_users = context.load_users
|
|
157
|
+
scan_environment = context.scan_environment
|
|
158
|
+
local_sysinfo = context.local_sysinfo
|
|
159
|
+
get_recommendations = context.get_recommendations
|
|
160
|
+
install_skill = context.install_skill
|
|
161
|
+
remove_skill_directory = context.remove_skill_directory
|
|
162
|
+
redact_secret_text = context.redact_secret_text
|
|
163
|
+
capability_registry = context.capability_registry
|
|
164
|
+
ui_file_response = context.ui_file_response
|
|
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
|
+
STATIC_DIR = context.static_dir
|
|
177
|
+
LOCAL_MODEL = context.local_model
|
|
178
|
+
PUBLIC_MODEL = context.public_model
|
|
179
|
+
_fetch_skills_marketplace = context.fetch_skills_marketplace
|
|
186
180
|
_workspace_scope = _workspace_scope_from_request
|
|
187
181
|
|
|
188
182
|
def _gate_read(request: Request):
|
|
@@ -197,6 +191,24 @@ def create_workspace_router(
|
|
|
197
191
|
except PermissionError as exc:
|
|
198
192
|
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
199
193
|
|
|
194
|
+
def _load_snapshot_authorized(request: Request, snapshot_id: str) -> dict:
|
|
195
|
+
"""Fetch a snapshot and authorize against the RECORD'S own workspace.
|
|
196
|
+
|
|
197
|
+
By-id access must not bypass workspace gating: a snapshot belonging to
|
|
198
|
+
an organization workspace is readable only by its members. Snapshots
|
|
199
|
+
predating workspace scoping carry no workspace_id and stay readable
|
|
200
|
+
(legacy-global compatibility).
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
snapshot = WORKSPACE_OS.get_snapshot(snapshot_id)
|
|
204
|
+
except FileNotFoundError as exc:
|
|
205
|
+
raise HTTPException(status_code=404, detail=f"Snapshot not found: {exc}") from exc
|
|
206
|
+
try:
|
|
207
|
+
svc.authorize_record_read(snapshot, get_current_user(request))
|
|
208
|
+
except PermissionError as exc:
|
|
209
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
210
|
+
return snapshot
|
|
211
|
+
|
|
200
212
|
# ── Workspace UI pages ────────────────────────────────────────────────
|
|
201
213
|
|
|
202
214
|
@router.get("/workspace")
|
|
@@ -343,6 +355,8 @@ def create_workspace_router(
|
|
|
343
355
|
@router.post("/workspace/snapshots/compare")
|
|
344
356
|
async def workspace_snapshot_compare(req: WorkspaceSnapshotCompareRequest, request: Request):
|
|
345
357
|
require_user(request)
|
|
358
|
+
_load_snapshot_authorized(request, req.before_id)
|
|
359
|
+
_load_snapshot_authorized(request, req.after_id)
|
|
346
360
|
try:
|
|
347
361
|
return WORKSPACE_OS.compare_snapshots(req.before_id, req.after_id)
|
|
348
362
|
except FileNotFoundError as exc:
|
|
@@ -351,14 +365,12 @@ def create_workspace_router(
|
|
|
351
365
|
@router.get("/workspace/snapshots/{snapshot_id}")
|
|
352
366
|
async def workspace_snapshot_get(snapshot_id: str, request: Request):
|
|
353
367
|
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
|
|
368
|
+
return _load_snapshot_authorized(request, snapshot_id)
|
|
358
369
|
|
|
359
370
|
@router.get("/workspace/snapshots/{snapshot_id}/{area}")
|
|
360
371
|
async def workspace_snapshot_area(snapshot_id: str, area: str, request: Request):
|
|
361
372
|
require_user(request)
|
|
373
|
+
_load_snapshot_authorized(request, snapshot_id)
|
|
362
374
|
try:
|
|
363
375
|
return WORKSPACE_OS.snapshot_view(snapshot_id, area)
|
|
364
376
|
except FileNotFoundError as exc:
|
|
@@ -367,6 +379,7 @@ def create_workspace_router(
|
|
|
367
379
|
@router.post("/workspace/snapshots/{snapshot_id}/export")
|
|
368
380
|
async def workspace_snapshot_export(snapshot_id: str, request: Request):
|
|
369
381
|
current_user = require_user(request)
|
|
382
|
+
_load_snapshot_authorized(request, snapshot_id)
|
|
370
383
|
try:
|
|
371
384
|
result = WORKSPACE_OS.export_snapshot(snapshot_id)
|
|
372
385
|
except FileNotFoundError as exc:
|
|
@@ -383,6 +396,7 @@ def create_workspace_router(
|
|
|
383
396
|
@router.get("/workspace/time-machine/{snapshot_id}/{area}")
|
|
384
397
|
async def workspace_time_machine_view(snapshot_id: str, area: str, request: Request):
|
|
385
398
|
require_user(request)
|
|
399
|
+
_load_snapshot_authorized(request, snapshot_id)
|
|
386
400
|
try:
|
|
387
401
|
return WORKSPACE_OS.snapshot_view(snapshot_id, area)
|
|
388
402
|
except FileNotFoundError as exc:
|
|
@@ -424,6 +438,14 @@ def create_workspace_router(
|
|
|
424
438
|
@router.delete("/workspace/memories/{memory_id}")
|
|
425
439
|
async def workspace_memory_delete(memory_id: str, request: Request):
|
|
426
440
|
require_user(request)
|
|
441
|
+
try:
|
|
442
|
+
record = WORKSPACE_OS.get_memory(memory_id)
|
|
443
|
+
except FileNotFoundError as exc:
|
|
444
|
+
raise HTTPException(status_code=404, detail=f"Memory not found: {exc}") from exc
|
|
445
|
+
try:
|
|
446
|
+
svc.authorize_memory_delete(record, get_current_user(request))
|
|
447
|
+
except PermissionError as exc:
|
|
448
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
427
449
|
try:
|
|
428
450
|
return WORKSPACE_OS.delete_memory(memory_id)
|
|
429
451
|
except FileNotFoundError as exc:
|