ltcai 9.0.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.
- package/README.md +80 -46
- package/auto_setup.py +7 -853
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +146 -2
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +1 -6
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/graph/_kg_fsutil.py +1 -5
- package/lattice_brain/graph/discovery_index.py +174 -20
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +140 -39
- package/lattice_brain/graph/retrieval_docgen.py +37 -4
- package/lattice_brain/ingestion.py +4 -7
- package/lattice_brain/portability.py +28 -14
- package/lattice_brain/runtime/agent_runtime.py +5 -9
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +20 -2
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +320 -850
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +24 -1
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +62 -9
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +67 -26
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +96 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +110 -52
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +9 -1
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +1 -5
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +0 -5
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +94 -43
- package/latticeai/models/router.py +130 -36
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/automation_runtime.py +13 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +62 -72
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +55 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +83 -23
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +12 -0
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +4 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +1 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1260
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-21lIXx2E.js +0 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
- package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
- package/static/app/assets/Library-bFMtyni3.js +0 -1
- package/static/app/assets/System-K6krGCqn.js +0 -1
- package/static/app/assets/index-C4R3ws30.js +0 -17
- package/static/app/assets/index-ChSeOB02.css +0 -2
- package/static/app/assets/primitives-sQU3it5I.js +0 -1
- package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, List
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
LEGACY_COMPATIBILITY_VERSION = "9.
|
|
17
|
+
LEGACY_COMPATIBILITY_VERSION = "9.1.0"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass(frozen=True)
|
|
@@ -89,13 +89,34 @@ LEGACY_SHIMS: List[LegacyShim] = [
|
|
|
89
89
|
reason="Deployment docs and local launch scripts historically targeted server.py.",
|
|
90
90
|
removal_phase="major-release-after-8.x",
|
|
91
91
|
),
|
|
92
|
+
LegacyShim(
|
|
93
|
+
path="tools/",
|
|
94
|
+
owner="latticeai.tools",
|
|
95
|
+
replacement="from latticeai.tools import execute_tool",
|
|
96
|
+
reason="Existing integrations and tests import the historical root tools package.",
|
|
97
|
+
removal_phase="major-release-after-9.x",
|
|
98
|
+
),
|
|
92
99
|
LegacyShim(
|
|
93
100
|
path="local_knowledge_api.py",
|
|
94
|
-
owner="latticeai.
|
|
95
|
-
replacement="from latticeai.
|
|
101
|
+
owner="latticeai.services.local_knowledge",
|
|
102
|
+
replacement="from latticeai.services.local_knowledge import create_local_knowledge_router",
|
|
96
103
|
reason="Local folder ingestion integrations used the root local knowledge API.",
|
|
97
104
|
removal_phase="requires-api-route-migration",
|
|
98
105
|
),
|
|
106
|
+
LegacyShim(
|
|
107
|
+
path="auto_setup.py",
|
|
108
|
+
owner="latticeai.setup.auto_setup",
|
|
109
|
+
replacement="from latticeai.setup.auto_setup import probe, recommend",
|
|
110
|
+
reason="Historical scripts invoke the zero-config setup module from the repo root.",
|
|
111
|
+
removal_phase="major-release-after-9.x",
|
|
112
|
+
),
|
|
113
|
+
LegacyShim(
|
|
114
|
+
path="setup_wizard.py",
|
|
115
|
+
owner="latticeai.setup.wizard",
|
|
116
|
+
replacement="from latticeai.setup.wizard import scan_environment",
|
|
117
|
+
reason="Older integrations imported the setup wizard from the repo root.",
|
|
118
|
+
removal_phase="major-release-after-9.x",
|
|
119
|
+
),
|
|
99
120
|
]
|
|
100
121
|
|
|
101
122
|
# Shim layers that have completed their compatibility window and were
|
|
@@ -403,6 +403,21 @@ class PluginRegistry:
|
|
|
403
403
|
reason=f"permission '{permission}' not granted at install time",
|
|
404
404
|
))
|
|
405
405
|
|
|
406
|
+
if capability == "tools":
|
|
407
|
+
tool_name = str(args.get("tool") or "").strip()
|
|
408
|
+
declared_tools = {
|
|
409
|
+
str(item).strip()
|
|
410
|
+
for item in (manifest.provides.get("tools") or [])
|
|
411
|
+
if str(item).strip()
|
|
412
|
+
}
|
|
413
|
+
if not tool_name or tool_name not in declared_tools:
|
|
414
|
+
return finish(PluginExecutionResult(
|
|
415
|
+
plugin_id,
|
|
416
|
+
action,
|
|
417
|
+
"blocked",
|
|
418
|
+
reason=f"tool '{tool_name or '<missing>'}' is not declared in provides.tools",
|
|
419
|
+
))
|
|
420
|
+
|
|
406
421
|
runner = runners.get(capability)
|
|
407
422
|
if runner is None:
|
|
408
423
|
return finish(PluginExecutionResult(
|
package/latticeai/core/policy.py
CHANGED
|
@@ -21,6 +21,7 @@ ROLE_CAPABILITIES: Dict[str, Set[str]] = {
|
|
|
21
21
|
"search",
|
|
22
22
|
"files",
|
|
23
23
|
"pipeline",
|
|
24
|
+
"desktop:control",
|
|
24
25
|
},
|
|
25
26
|
"member": {"workspace:read", "workspace:write", "chat", "search", "files", "pipeline"},
|
|
26
27
|
"user": {"workspace:read", "workspace:write", "chat", "search", "files", "pipeline"},
|
|
@@ -51,4 +52,3 @@ def require_capability(role: str, capability: str) -> None:
|
|
|
51
52
|
def policy_matrix(roles: Iterable[str] | None = None) -> List[Dict[str, object]]:
|
|
52
53
|
selected = list(roles or ROLE_CAPABILITIES.keys())
|
|
53
54
|
return [{"role": normalize_role(role), "caps": capabilities_for_role(role)} for role in selected]
|
|
54
|
-
|
|
@@ -29,10 +29,10 @@ from __future__ import annotations
|
|
|
29
29
|
import asyncio
|
|
30
30
|
import json
|
|
31
31
|
import threading
|
|
32
|
-
from
|
|
33
|
-
from typing import Any, AsyncIterator, Dict, List, Optional, Set
|
|
32
|
+
from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Set
|
|
34
33
|
|
|
35
34
|
from lattice_brain.runtime.contracts import realtime_event_contract
|
|
35
|
+
from .timeutil import now_iso as _now
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
REALTIME_VERSION = "2.2.0"
|
|
@@ -40,10 +40,6 @@ _FEED_LIMIT = 200
|
|
|
40
40
|
_QUEUE_MAX = 100
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def _now() -> str:
|
|
44
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
45
|
-
|
|
46
|
-
|
|
47
43
|
def sse_format(event: Dict[str, Any]) -> str:
|
|
48
44
|
"""Encode an event as an SSE ``data:`` frame."""
|
|
49
45
|
return f"data: {json.dumps(event, ensure_ascii=False, default=str)}\n\n"
|
|
@@ -64,11 +60,11 @@ class _Subscriber:
|
|
|
64
60
|
self.loop = None
|
|
65
61
|
|
|
66
62
|
def accepts(self, workspace_id: Optional[str]) -> bool:
|
|
67
|
-
# ``None`` scope
|
|
63
|
+
# Only the explicit no-auth/local ``None`` scope sees unscoped events.
|
|
64
|
+
# Any concrete set, including the authenticated fail-closed empty set,
|
|
65
|
+
# requires an exact workspace match.
|
|
68
66
|
if self.workspace_scope is None:
|
|
69
67
|
return True
|
|
70
|
-
if workspace_id is None:
|
|
71
|
-
return True
|
|
72
68
|
return workspace_id in self.workspace_scope
|
|
73
69
|
|
|
74
70
|
|
|
@@ -142,21 +138,42 @@ class RealtimeBus:
|
|
|
142
138
|
with self._lock:
|
|
143
139
|
self._subscribers.pop(sub_id, None)
|
|
144
140
|
|
|
145
|
-
async def stream(
|
|
141
|
+
async def stream(
|
|
142
|
+
self,
|
|
143
|
+
sub: _Subscriber,
|
|
144
|
+
*,
|
|
145
|
+
heartbeat: float = 15.0,
|
|
146
|
+
refresh_authorization: Optional[Callable[[_Subscriber], bool]] = None,
|
|
147
|
+
) -> AsyncIterator[str]:
|
|
146
148
|
"""Yield SSE frames for a subscriber until the client disconnects.
|
|
147
149
|
|
|
148
150
|
Emits a periodic heartbeat comment so proxies keep the connection open
|
|
149
151
|
and single-user local mode never looks "stuck" with no events.
|
|
150
152
|
"""
|
|
151
153
|
# Replay a small tail so a fresh subscriber has immediate context.
|
|
154
|
+
if refresh_authorization is not None and not refresh_authorization(sub):
|
|
155
|
+
self.remove_subscriber(sub.id)
|
|
156
|
+
return
|
|
152
157
|
for event in self.recent(limit=10, workspace_scope=sub.workspace_scope):
|
|
153
|
-
|
|
158
|
+
if refresh_authorization is not None and not refresh_authorization(sub):
|
|
159
|
+
self.remove_subscriber(sub.id)
|
|
160
|
+
return
|
|
161
|
+
if sub.accepts(event.get("workspace_id")):
|
|
162
|
+
yield sse_format(event)
|
|
154
163
|
try:
|
|
155
164
|
while True:
|
|
156
165
|
try:
|
|
157
166
|
event = await asyncio.wait_for(sub.queue.get(), timeout=heartbeat)
|
|
158
|
-
|
|
167
|
+
if refresh_authorization is not None and not refresh_authorization(sub):
|
|
168
|
+
break
|
|
169
|
+
# The event may have been queued under an older membership
|
|
170
|
+
# snapshot. Recheck it against the refreshed scope before
|
|
171
|
+
# any bytes leave the process.
|
|
172
|
+
if sub.accepts(event.get("workspace_id")):
|
|
173
|
+
yield sse_format(event)
|
|
159
174
|
except asyncio.TimeoutError:
|
|
175
|
+
if refresh_authorization is not None and not refresh_authorization(sub):
|
|
176
|
+
break
|
|
160
177
|
yield ": heartbeat\n\n"
|
|
161
178
|
finally:
|
|
162
179
|
self.remove_subscriber(sub.id)
|
|
@@ -167,7 +184,7 @@ class RealtimeBus:
|
|
|
167
184
|
with self._lock:
|
|
168
185
|
events = list(self._feed)
|
|
169
186
|
if workspace_scope is not None:
|
|
170
|
-
events = [e for e in events if e.get("workspace_id")
|
|
187
|
+
events = [e for e in events if e.get("workspace_id") in workspace_scope]
|
|
171
188
|
return list(reversed(events[-max(1, min(limit, _FEED_LIMIT)):]))
|
|
172
189
|
|
|
173
190
|
def join(self, client_id: str, *, user: Optional[str], workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
@@ -179,6 +196,9 @@ class RealtimeBus:
|
|
|
179
196
|
"last_seen": _now(),
|
|
180
197
|
}
|
|
181
198
|
with self._lock:
|
|
199
|
+
existing = self._presence.get(client_id)
|
|
200
|
+
if existing is not None and existing.get("user") != user:
|
|
201
|
+
raise PermissionError("Presence client belongs to another user.")
|
|
182
202
|
self._presence[client_id] = record
|
|
183
203
|
self.publish({"area": "presence", "event_type": "join", "workspace_id": workspace_id, "payload": {"user": user, "client_id": client_id}})
|
|
184
204
|
return record
|
|
@@ -190,8 +210,11 @@ class RealtimeBus:
|
|
|
190
210
|
record["last_seen"] = _now()
|
|
191
211
|
return record
|
|
192
212
|
|
|
193
|
-
def leave(self, client_id: str) -> None:
|
|
213
|
+
def leave(self, client_id: str, *, user: Optional[str] = None) -> None:
|
|
194
214
|
with self._lock:
|
|
215
|
+
record = self._presence.get(client_id)
|
|
216
|
+
if record is not None and user is not None and record.get("user") != user:
|
|
217
|
+
raise PermissionError("Presence client belongs to another user.")
|
|
195
218
|
record = self._presence.pop(client_id, None)
|
|
196
219
|
if record:
|
|
197
220
|
self.publish({"area": "presence", "event_type": "leave", "workspace_id": record.get("workspace_id"), "payload": {"client_id": client_id}})
|
|
@@ -200,7 +223,7 @@ class RealtimeBus:
|
|
|
200
223
|
with self._lock:
|
|
201
224
|
records = list(self._presence.values())
|
|
202
225
|
if workspace_scope is not None:
|
|
203
|
-
records = [r for r in records if r.get("workspace_id")
|
|
226
|
+
records = [r for r in records if r.get("workspace_id") in workspace_scope]
|
|
204
227
|
return records
|
|
205
228
|
|
|
206
229
|
def stats(self) -> Dict[str, Any]:
|
|
@@ -15,6 +15,8 @@ import time
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Dict, Optional
|
|
17
17
|
|
|
18
|
+
from latticeai.core.io_utils import atomic_write_json
|
|
19
|
+
|
|
18
20
|
SESSION_TTL = 60 * 60 * 24 # 24 hours
|
|
19
21
|
SESSION_REFRESH_THRESHOLD = 60 * 15 # only persist if >15 min since last bump
|
|
20
22
|
_lock = threading.Lock()
|
|
@@ -39,7 +41,7 @@ def _sessions_file(data_dir: Optional[Path] = None) -> Path:
|
|
|
39
41
|
import os
|
|
40
42
|
data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or (Path.home() / ".ltcai"))
|
|
41
43
|
d = data_dir
|
|
42
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
d.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
43
45
|
return d / "sessions.json"
|
|
44
46
|
|
|
45
47
|
|
|
@@ -68,7 +70,10 @@ def load_sessions(data_dir: Optional[Path] = None) -> Dict[str, tuple]:
|
|
|
68
70
|
|
|
69
71
|
def persist_sessions(sessions: Dict[str, tuple], data_dir: Optional[Path] = None) -> None:
|
|
70
72
|
try:
|
|
71
|
-
|
|
73
|
+
atomic_write_json(
|
|
74
|
+
_sessions_file(data_dir),
|
|
75
|
+
{key: list(value) for key, value in sessions.items()},
|
|
76
|
+
)
|
|
72
77
|
except Exception as e:
|
|
73
78
|
logging.warning("persist_sessions failed: %s", e)
|
|
74
79
|
|
|
@@ -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(
|
|
106
|
-
|
|
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":
|
|
150
|
-
"local_read":
|
|
151
|
-
"read_document":
|
|
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":
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"
|
|
163
|
-
|
|
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(
|
|
174
|
-
|
|
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
|
-
|
|
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())]
|
package/latticeai/core/users.py
CHANGED
|
@@ -6,20 +6,16 @@ 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
|
|
|
13
12
|
from .io_utils import atomic_write_json
|
|
13
|
+
from .timeutil import now_iso as _now
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _now() -> str:
|
|
20
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
21
|
-
|
|
22
|
-
|
|
23
19
|
def normalize_email(email: str) -> str:
|
|
24
20
|
return str(email or "").strip().lower()
|
|
25
21
|
|
|
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
|
-
from .
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 .
|
|
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 = "9.
|
|
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(
|
|
834
|
-
|
|
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)
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import shutil
|
|
11
|
-
from datetime import datetime
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import Any, Dict, List, Optional
|
|
14
13
|
|
|
@@ -16,10 +15,6 @@ from .io_utils import atomic_write_json as _atomic_write_json # noqa: F401 - le
|
|
|
16
15
|
from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
def _now() -> str:
|
|
20
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
21
|
-
|
|
22
|
-
|
|
23
18
|
def _safe_slug(raw: str) -> str:
|
|
24
19
|
value = "".join(ch if ch.isalnum() or ch in "-_." else "-" for ch in str(raw or "").strip())
|
|
25
20
|
value = "-".join(part for part in value.split("-") if part)
|
|
@@ -7,7 +7,8 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import Any, Dict, Optional
|
|
9
9
|
|
|
10
|
-
from .
|
|
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():
|