ltcai 8.9.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- 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/ROADMAP_RECOMMENDATIONS.md +61 -36
- 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 +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- 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 +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- 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 +46 -0
- 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 +355 -952
- 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 +250 -0
- 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 +211 -40
- 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/marketplace.py +11 -0
- 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 +70 -29
- 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 +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- 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 +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- 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 +5 -14
- 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 +2 -21
- 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 +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -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/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- 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/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -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 +268 -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 +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- 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 +7 -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 -1304
- 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/css/tokens.css +4 -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/sso_runtime.py +0 -52
- 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-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Canonical application timestamp helpers.
|
|
2
|
+
|
|
3
|
+
Brain Core owns the dependency-free implementation so it remains independently
|
|
4
|
+
importable; the application layer exposes the same helpers from this stable
|
|
5
|
+
location for all LatticeAI services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from lattice_brain.utils import local_now, now_iso, parse_iso, utc_now_iso
|
|
9
|
+
|
|
10
|
+
__all__ = ["local_now", "now_iso", "parse_iso", "utc_now_iso"]
|
|
@@ -16,11 +16,13 @@ __all__ = [
|
|
|
16
16
|
"FILE_CREATE_ACTIONS",
|
|
17
17
|
"LOCAL_WRITE_BLOCKED_PREFIXES",
|
|
18
18
|
"RISK_LEVEL_MAP",
|
|
19
|
+
"SCOPED_KNOWLEDGE_TOOLS",
|
|
20
|
+
"KNOWLEDGE_WRITE_TOOLS",
|
|
19
21
|
"ToolRegistry",
|
|
20
22
|
]
|
|
21
23
|
|
|
22
24
|
from dataclasses import dataclass, field
|
|
23
|
-
from typing import Any, Callable, Dict, Mapping, Optional, TypedDict
|
|
25
|
+
from typing import Any, Callable, Dict, Mapping, NotRequired, Optional, TypedDict
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class ToolPolicy(TypedDict):
|
|
@@ -31,6 +33,8 @@ class ToolPolicy(TypedDict):
|
|
|
31
33
|
auto_approve: bool
|
|
32
34
|
sandbox: str
|
|
33
35
|
rollback: str
|
|
36
|
+
capability: NotRequired[str]
|
|
37
|
+
scope: NotRequired[str]
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
class ToolPermission(TypedDict):
|
|
@@ -38,6 +42,8 @@ class ToolPermission(TypedDict):
|
|
|
38
42
|
risk: str
|
|
39
43
|
requires_approval: bool
|
|
40
44
|
network: bool
|
|
45
|
+
capability: NotRequired[str]
|
|
46
|
+
scope: NotRequired[str]
|
|
41
47
|
|
|
42
48
|
|
|
43
49
|
TOOL_CATALOG_BRIEF = """
|
|
@@ -80,6 +86,17 @@ RISK_LEVEL_MAP = {
|
|
|
80
86
|
"destructive": "high",
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
SCOPED_KNOWLEDGE_TOOLS = frozenset({
|
|
90
|
+
"knowledge_save",
|
|
91
|
+
"knowledge_search",
|
|
92
|
+
"knowledge_tree",
|
|
93
|
+
"obsidian_save",
|
|
94
|
+
"obsidian_search",
|
|
95
|
+
"obsidian_tree",
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
KNOWLEDGE_WRITE_TOOLS = frozenset({"knowledge_save", "obsidian_save"})
|
|
99
|
+
|
|
83
100
|
|
|
84
101
|
def _r(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
85
102
|
return ToolPolicy(
|
|
@@ -88,6 +105,25 @@ def _r(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
|
88
105
|
)
|
|
89
106
|
|
|
90
107
|
|
|
108
|
+
def _rc(
|
|
109
|
+
sandbox: str = "home",
|
|
110
|
+
rollback: str = "none",
|
|
111
|
+
*,
|
|
112
|
+
capability: str = "",
|
|
113
|
+
scope: str = "",
|
|
114
|
+
) -> ToolPolicy:
|
|
115
|
+
"""Read operation that still requires explicit human consent."""
|
|
116
|
+
policy = ToolPolicy(
|
|
117
|
+
risk="read", destructive=False, shell=False, network=False,
|
|
118
|
+
auto_approve=False, sandbox=sandbox, rollback=rollback,
|
|
119
|
+
)
|
|
120
|
+
if capability:
|
|
121
|
+
policy["capability"] = capability
|
|
122
|
+
if scope:
|
|
123
|
+
policy["scope"] = scope
|
|
124
|
+
return policy
|
|
125
|
+
|
|
126
|
+
|
|
91
127
|
def _rs(sandbox: str = "workspace", rollback: str = "none") -> ToolPolicy:
|
|
92
128
|
return ToolPolicy(
|
|
93
129
|
risk="read", destructive=False, shell=True, network=False,
|
|
@@ -102,11 +138,22 @@ def _rn(sandbox: str = "system", rollback: str = "none") -> ToolPolicy:
|
|
|
102
138
|
)
|
|
103
139
|
|
|
104
140
|
|
|
105
|
-
def _w(
|
|
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,23 +6,14 @@ import json
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sqlite3
|
|
8
8
|
import uuid
|
|
9
|
-
from datetime import datetime
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any, Dict, Optional
|
|
12
11
|
|
|
12
|
+
from .io_utils import atomic_write_json
|
|
13
|
+
from .timeutil import now_iso as _now
|
|
13
14
|
|
|
14
|
-
USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _now() -> str:
|
|
18
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
def _atomic_write_json(path: Path, data: Dict[str, Any]) -> None:
|
|
22
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
24
|
-
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
25
|
-
tmp.replace(path)
|
|
16
|
+
USER_NAMESPACE = uuid.UUID("5d6d4480-cf79-49c3-a6d0-4c6eec3224d6")
|
|
26
17
|
|
|
27
18
|
|
|
28
19
|
def normalize_email(email: str) -> str:
|
|
@@ -86,13 +77,13 @@ def load_users_file(path: Path) -> Dict[str, Any]:
|
|
|
86
77
|
shutil.copy2(path, backup)
|
|
87
78
|
except Exception:
|
|
88
79
|
pass
|
|
89
|
-
|
|
80
|
+
atomic_write_json(path, migrated)
|
|
90
81
|
return migrated
|
|
91
82
|
|
|
92
83
|
|
|
93
84
|
def save_users_file(path: Path, users: Dict[str, Any]) -> None:
|
|
94
85
|
migrated, _, _ = migrate_users(users)
|
|
95
|
-
|
|
86
|
+
atomic_write_json(path, migrated)
|
|
96
87
|
|
|
97
88
|
|
|
98
89
|
def user_id_for_email(users: Dict[str, Any], email: Optional[str]) -> Optional[str]:
|
|
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
|
-
from .
|
|
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 = "
|
|
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)
|
|
@@ -7,15 +7,12 @@ store class and public constants while preserving exact behavior.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
-
import os
|
|
11
10
|
import shutil
|
|
12
|
-
from datetime import datetime
|
|
13
11
|
from pathlib import Path
|
|
14
12
|
from typing import Any, Dict, List, Optional
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
14
|
+
from .io_utils import atomic_write_json as _atomic_write_json # noqa: F401 - legacy helper re-export
|
|
15
|
+
from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
def _safe_slug(raw: str) -> str:
|
|
@@ -43,13 +40,6 @@ def _deep_merge(default: Any, loaded: Any) -> Any:
|
|
|
43
40
|
return loaded
|
|
44
41
|
|
|
45
42
|
|
|
46
|
-
def _atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
47
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
-
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
49
|
-
tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
50
|
-
os.replace(tmp_path, path)
|
|
51
|
-
|
|
52
|
-
|
|
53
43
|
def _listify(value: Any) -> List[Any]:
|
|
54
44
|
return value if isinstance(value, list) else []
|
|
55
45
|
|
|
@@ -100,15 +90,6 @@ def _snapshot_graph_import_payload(graph_payload: Dict[str, Any], *, workspace_i
|
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
|
|
103
|
-
def _parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
104
|
-
if not value:
|
|
105
|
-
return None
|
|
106
|
-
try:
|
|
107
|
-
return datetime.fromisoformat(str(value))
|
|
108
|
-
except (TypeError, ValueError):
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
|
|
112
93
|
def _file_size(path: Path) -> int:
|
|
113
94
|
try:
|
|
114
95
|
return path.stat().st_size
|
|
@@ -7,7 +7,8 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import Any, Dict, Optional
|
|
9
9
|
|
|
10
|
-
from .
|
|
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():
|
|
@@ -5,11 +5,10 @@ import json
|
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
from lattice_brain.runtime.contracts import run_record_contract, workflow_run_contract
|
|
8
|
+
from lattice_brain.runtime.statuses import RUN_ACTIVE_STATUSES, RUN_TERMINAL_STATUSES
|
|
8
9
|
|
|
9
|
-
from .
|
|
10
|
-
|
|
11
|
-
RUN_ACTIVE_STATUSES = {"queued", "running", "in_progress", "retrying", "cancelling"}
|
|
12
|
-
RUN_TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
|
|
10
|
+
from .timeutil import now_iso as _now
|
|
11
|
+
from .workspace_os_utils import _json_hash, _listify
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class WorkspaceRuns:
|
|
@@ -80,6 +79,7 @@ class WorkspaceRuns:
|
|
|
80
79
|
f"{agent_id} {status}",
|
|
81
80
|
user_email=user_email,
|
|
82
81
|
source="workspace_os",
|
|
82
|
+
workspace_id=resolved_workspace,
|
|
83
83
|
metadata={"run_id": run["id"], "agent_id": agent_id, "status": status, "mode": mode},
|
|
84
84
|
)
|
|
85
85
|
run["graph_node_id"] = ingested.get("node_id")
|
|
@@ -167,6 +167,7 @@ class WorkspaceRuns:
|
|
|
167
167
|
f"{run.get('agent_id')} {status}",
|
|
168
168
|
user_email=run.get("user_email"),
|
|
169
169
|
source="workspace_os",
|
|
170
|
+
workspace_id=resolved_workspace,
|
|
170
171
|
metadata={
|
|
171
172
|
"run_id": run_id,
|
|
172
173
|
"agent_id": run.get("agent_id"),
|
|
@@ -250,6 +251,7 @@ class WorkspaceRuns:
|
|
|
250
251
|
workflow["name"],
|
|
251
252
|
user_email=user_email,
|
|
252
253
|
source="workspace_os",
|
|
254
|
+
workspace_id=workflow["workspace_id"],
|
|
253
255
|
metadata={"workflow_id": workflow["id"], "steps": steps},
|
|
254
256
|
)
|
|
255
257
|
workflow["graph_node_id"] = ingested.get("node_id")
|
|
@@ -257,7 +259,12 @@ class WorkspaceRuns:
|
|
|
257
259
|
workflow["graph_error"] = str(exc)
|
|
258
260
|
state.setdefault("workflows", []).append(workflow)
|
|
259
261
|
self.save_state(state)
|
|
260
|
-
self.record_timeline_event(
|
|
262
|
+
self.record_timeline_event(
|
|
263
|
+
"workflow",
|
|
264
|
+
"workflow_created",
|
|
265
|
+
{"workflow_id": workflow["id"], "name": workflow["name"]},
|
|
266
|
+
workspace_id=workflow["workspace_id"],
|
|
267
|
+
)
|
|
261
268
|
return workflow
|
|
262
269
|
|
|
263
270
|
def record_workflow_run(
|
|
@@ -304,6 +311,7 @@ class WorkspaceRuns:
|
|
|
304
311
|
f"{run['name']} {status}",
|
|
305
312
|
user_email=user_email,
|
|
306
313
|
source="workspace_os",
|
|
314
|
+
workspace_id=resolved_workspace,
|
|
307
315
|
metadata={"run_id": run["id"], "workflow_id": workflow_id, "status": status, "mode": mode},
|
|
308
316
|
)
|
|
309
317
|
run["graph_node_id"] = ingested.get("node_id")
|
|
@@ -380,6 +388,7 @@ class WorkspaceRuns:
|
|
|
380
388
|
f"{run.get('name')} {status}",
|
|
381
389
|
user_email=run.get("user_email"),
|
|
382
390
|
source="workspace_os",
|
|
391
|
+
workspace_id=resolved_workspace,
|
|
383
392
|
metadata={
|
|
384
393
|
"run_id": run_id,
|
|
385
394
|
"workflow_id": workflow_id,
|
|
@@ -10,7 +10,8 @@ from datetime import datetime
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Dict, Iterable, Optional
|
|
12
12
|
|
|
13
|
-
from .
|
|
13
|
+
from .timeutil import now_iso as _now
|
|
14
|
+
from .workspace_os_utils import _atomic_write_json, _json_hash, _listify, _safe_slug
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class WorkspaceSnapshots:
|
|
@@ -6,7 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from typing import Any, Dict, Iterable, List, Optional
|
|
8
8
|
|
|
9
|
-
from .
|
|
9
|
+
from .timeutil import now_iso as _now
|
|
10
|
+
from .workspace_os_utils import _listify, _parse_iso
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def _audit_category(event: Dict[str, Any]) -> str:
|