ltcai 8.1.0 → 8.2.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.
Files changed (110) hide show
  1. package/README.md +20 -41
  2. package/docs/CHANGELOG.md +38 -874
  3. package/docs/DEVELOPMENT.md +26 -3
  4. package/docs/LEGACY_COMPATIBILITY.md +3 -1
  5. package/docs/TRUST_MODEL.md +26 -26
  6. package/docs/WHY_LATTICE.md +28 -25
  7. package/docs/kg-schema.md +6 -4
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/graph/schema.py +6 -0
  10. package/lattice_brain/runtime/multi_agent.py +1 -1
  11. package/latticeai/__init__.py +1 -1
  12. package/latticeai/api/computer_use.py +2 -0
  13. package/latticeai/api/memory.py +13 -0
  14. package/latticeai/app_factory.py +30 -28
  15. package/latticeai/brain/__init__.py +1 -1
  16. package/latticeai/core/config.py +10 -0
  17. package/latticeai/core/marketplace.py +1 -1
  18. package/latticeai/core/tool_registry.py +10 -0
  19. package/latticeai/core/workspace_graph_trace.py +132 -0
  20. package/latticeai/core/workspace_memory.py +75 -0
  21. package/latticeai/core/workspace_os.py +135 -1283
  22. package/latticeai/core/workspace_os_utils.py +132 -0
  23. package/latticeai/core/workspace_permissions.py +99 -0
  24. package/latticeai/core/workspace_plugins.py +97 -0
  25. package/latticeai/core/workspace_runs.py +612 -0
  26. package/latticeai/core/workspace_skills.py +114 -0
  27. package/latticeai/core/workspace_snapshots.py +195 -0
  28. package/latticeai/core/workspace_timeline.py +107 -0
  29. package/latticeai/runtime/audit_runtime.py +64 -0
  30. package/latticeai/runtime/config_runtime.py +4 -0
  31. package/latticeai/runtime/sso_runtime.py +52 -0
  32. package/latticeai/services/architecture_readiness.py +3 -3
  33. package/latticeai/services/memory_service.py +215 -7
  34. package/latticeai/services/model_capability_registry.py +68 -0
  35. package/latticeai/services/model_engines.py +603 -0
  36. package/latticeai/services/model_loading.py +482 -0
  37. package/latticeai/services/model_recommendation.py +5 -0
  38. package/latticeai/services/model_runtime.py +141 -866
  39. package/latticeai/services/product_readiness.py +19 -18
  40. package/package.json +1 -1
  41. package/src-tauri/Cargo.lock +1 -1
  42. package/src-tauri/Cargo.toml +1 -1
  43. package/src-tauri/tauri.conf.json +1 -1
  44. package/static/app/asset-manifest.json +11 -11
  45. package/static/app/assets/{Act-BOO66G-c.js → Act-D9jIknFd.js} +1 -1
  46. package/static/app/assets/{Brain-C6lEYiD7.js → Brain-CFOtWbPN.js} +1 -1
  47. package/static/app/assets/{Capture-TATXBRDw.js → Capture-Q4WYzwr5.js} +1 -1
  48. package/static/app/assets/{Library-DK4FIp8a.js → Library-C5Q2yWee.js} +1 -1
  49. package/static/app/assets/{System-Bgs6Ql7x.js → System-BLbjdr1_.js} +1 -1
  50. package/static/app/assets/index-BqammyNu.js +16 -0
  51. package/static/app/assets/index-ty1iGgZu.css +2 -0
  52. package/static/app/assets/{primitives-B70WOra0.js → primitives-Br8uSfZ4.js} +1 -1
  53. package/static/app/assets/{textarea-Czrd9gwM.js → textarea-BnhNs1_X.js} +1 -1
  54. package/static/app/index.html +2 -2
  55. package/static/sw.js +1 -1
  56. package/tools/__init__.py +2 -1
  57. package/tools/computer.py +21 -0
  58. package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
  59. package/docs/HANDOVER_v3.6.0.md +0 -46
  60. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
  61. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
  62. package/docs/V2_ARCHITECTURE.md +0 -561
  63. package/docs/V3_2_AUDIT.md +0 -82
  64. package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
  65. package/docs/V3_FRONTEND.md +0 -146
  66. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
  67. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
  68. package/docs/V4_1_VALIDATION_REPORT.md +0 -47
  69. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
  70. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
  71. package/docs/V4_2_VALIDATION_REPORT.md +0 -89
  72. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
  73. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
  74. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
  75. package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
  76. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
  77. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
  78. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
  79. package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
  80. package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
  81. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
  82. package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
  83. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
  84. package/docs/V4_3_VALIDATION_REPORT.md +0 -58
  85. package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
  86. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
  87. package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
  88. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
  89. package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
  90. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
  91. package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
  92. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
  93. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
  94. package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
  95. package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
  96. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
  97. package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
  98. package/docs/V4_5_1_UX_REPORT.md +0 -45
  99. package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
  100. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
  101. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
  102. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
  103. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
  104. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
  105. package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
  106. package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
  107. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
  108. package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
  109. package/static/app/assets/index-Bvv79nre.js +0 -16
  110. package/static/app/assets/index-Dslqglia.css +0 -2
@@ -0,0 +1,132 @@
1
+ """Helper utilities extracted from the monolithic WorkspaceOSStore module.
2
+
3
+ These are internal (_-prefixed) helpers for JSON handling, slugging, timeline,
4
+ and snapshot conversion. Moving them keeps workspace_os.py focused on the
5
+ store class and public constants while preserving exact behavior.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import shutil
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+
17
+ def _now() -> str:
18
+ return datetime.now().isoformat(timespec="seconds")
19
+
20
+
21
+ def _safe_slug(raw: str) -> str:
22
+ value = "".join(ch if ch.isalnum() or ch in "-_." else "-" for ch in str(raw or "").strip())
23
+ value = "-".join(part for part in value.split("-") if part)
24
+ return (value or "item")[:96]
25
+
26
+
27
+ def _json_hash(value: Any) -> str:
28
+ import hashlib
29
+
30
+ payload = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
31
+ return hashlib.sha256(payload.encode("utf-8", errors="replace")).hexdigest()
32
+
33
+
34
+ def _deep_merge(default: Any, loaded: Any) -> Any:
35
+ if isinstance(default, dict) and isinstance(loaded, dict):
36
+ merged = {key: _deep_merge(value, loaded.get(key)) for key, value in default.items()}
37
+ for key, value in loaded.items():
38
+ if key not in merged:
39
+ merged[key] = value
40
+ return merged
41
+ if loaded is None:
42
+ return default
43
+ return loaded
44
+
45
+
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
+ def _listify(value: Any) -> List[Any]:
54
+ return value if isinstance(value, list) else []
55
+
56
+
57
+ def _snapshot_graph_import_payload(graph_payload: Dict[str, Any], *, workspace_id: Optional[str]) -> Dict[str, Any]:
58
+ """Convert the UI graph snapshot shape into the logical import artifact."""
59
+
60
+ nodes = []
61
+ for node in _listify((graph_payload or {}).get("nodes")):
62
+ node_id = node.get("id")
63
+ if not node_id:
64
+ continue
65
+ metadata = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
66
+ raw = node.get("raw") if isinstance(node.get("raw"), dict) else {}
67
+ if workspace_id and not metadata.get("workspace_id"):
68
+ metadata = {**metadata, "workspace_id": workspace_id}
69
+ nodes.append({
70
+ "id": node_id,
71
+ "type": node.get("type") or "Concept",
72
+ "title": node.get("title") or node.get("label") or node_id,
73
+ "summary": node.get("summary") or "",
74
+ "metadata_json": json.dumps(metadata, ensure_ascii=False),
75
+ "raw_json": json.dumps(raw, ensure_ascii=False),
76
+ })
77
+
78
+ edges = []
79
+ for edge in _listify((graph_payload or {}).get("edges")):
80
+ source = edge.get("from_node") or edge.get("from") or edge.get("source")
81
+ target = edge.get("to_node") or edge.get("to") or edge.get("target")
82
+ if not source or not target:
83
+ continue
84
+ edges.append({
85
+ "from_node": source,
86
+ "to_node": target,
87
+ "type": edge.get("type") or "related_to",
88
+ "weight": edge.get("weight") or 1.0,
89
+ "metadata_json": json.dumps(edge.get("metadata") or {}, ensure_ascii=False),
90
+ })
91
+
92
+ return {
93
+ "header": {"graph_schema_version": 1, "workspace_id": workspace_id, "source": "workspace_snapshot"},
94
+ "nodes": nodes,
95
+ "edges": edges,
96
+ "chunks": [],
97
+ "knowledge_sources": [],
98
+ "provenance": [],
99
+ "counts": {"nodes": len(nodes), "edges": len(edges)},
100
+ }
101
+
102
+
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
+ def _file_size(path: Path) -> int:
113
+ try:
114
+ return path.stat().st_size
115
+ except Exception:
116
+ return 0
117
+
118
+
119
+ def remove_skill_directory(skills_dir: Path, skill: str) -> Dict[str, Any]:
120
+ """Remove an installed skill directory after caller has performed auth checks.
121
+
122
+ Moved here from workspace_os.py for smaller focused surface.
123
+ """
124
+ safe_name = _safe_slug(skill)
125
+ target = (skills_dir / safe_name).resolve()
126
+ root = skills_dir.resolve()
127
+ if not str(target).startswith(str(root)):
128
+ raise ValueError("invalid skill path")
129
+ if not target.exists() or not target.is_dir():
130
+ raise FileNotFoundError(skill)
131
+ shutil.rmtree(target)
132
+ return {"status": "ok", "skill": safe_name, "removed_path": str(target)}
@@ -0,0 +1,99 @@
1
+ """Workspace permission and role logic extracted from WorkspaceOSStore.
2
+
3
+ Provides pure functions + a small PermissionManager for composition.
4
+ All behavior preserved; main store continues to expose the same public methods.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+ from .workspace_os_utils import _listify, _now
11
+
12
+ # Avoid circular at import time: pull constants lazily from parent module.
13
+ def _get_role_permissions():
14
+ from .workspace_os import ROLE_PERMISSIONS
15
+ return ROLE_PERMISSIONS
16
+
17
+ def _get_workspace_roles():
18
+ from .workspace_os import WORKSPACE_ROLES
19
+ return WORKSPACE_ROLES
20
+
21
+
22
+ def _member_role(ws: Dict[str, Any], user_id: Optional[str]) -> Optional[str]:
23
+ if ws.get("type") == "personal":
24
+ return "owner"
25
+ owner = ws.get("owner_user_id")
26
+ if not owner and not user_id:
27
+ return "owner"
28
+ if user_id and user_id == owner:
29
+ return "owner"
30
+ for member in _listify(ws.get("members")):
31
+ if member.get("user_id") == user_id:
32
+ return member.get("role")
33
+ return None
34
+
35
+
36
+ def has_permission(ws_or_store: Any, workspace_id: str, user_id: Optional[str], permission: str) -> bool:
37
+ """Works both as standalone or delegated."""
38
+ if hasattr(ws_or_store, "load_state"):
39
+ # called as method on store
40
+ ws = (ws_or_store.load_state().get("workspaces") or {}).get(workspace_id)
41
+ else:
42
+ ws = ws_or_store
43
+ if not ws:
44
+ return False
45
+ role = _member_role(ws, user_id)
46
+ if role is None:
47
+ return False
48
+ return permission in _get_role_permissions().get(role, set())
49
+
50
+
51
+ class WorkspacePermissionManager:
52
+ """Composable permission manager."""
53
+
54
+ def __init__(self, store: Any):
55
+ self._store = store
56
+
57
+ def get_member_role(self, workspace_id: str, user_id: Optional[str]) -> Optional[str]:
58
+ ws = (self._store.load_state().get("workspaces") or {}).get(workspace_id)
59
+ if not ws:
60
+ raise FileNotFoundError(workspace_id)
61
+ return _member_role(ws, user_id)
62
+
63
+ def has_permission(self, workspace_id: str, user_id: Optional[str], permission: str) -> bool:
64
+ try:
65
+ role = self.get_member_role(workspace_id, user_id)
66
+ except FileNotFoundError:
67
+ return False
68
+ if role is None:
69
+ return False
70
+ return permission in _get_role_permissions().get(role, set())
71
+
72
+ def require_permission(self, ws: Dict[str, Any], actor: Optional[str], permission: str) -> None:
73
+ role = _member_role(ws, actor)
74
+ if role is None or permission not in _get_role_permissions().get(role, set()):
75
+ raise PermissionError(
76
+ f"'{actor or 'anonymous'}' lacks '{permission}' on workspace '{ws.get('workspace_id')}'"
77
+ )
78
+
79
+ def add_member(self, workspace_id: str, *, user_id: str, role: str = "member", actor: Optional[str] = None) -> Dict[str, Any]:
80
+ if role not in _get_workspace_roles():
81
+ raise ValueError(f"unknown role: {role}")
82
+ if not str(user_id or "").strip():
83
+ raise ValueError("user_id is required")
84
+ state = self._store.load_state()
85
+ ws = self._store._load_org(state, workspace_id)
86
+ self.require_permission(ws, actor, "manage_members")
87
+ members = ws.setdefault("members", [])
88
+ existing = next((m for m in members if m.get("user_id") == user_id), None)
89
+ if existing:
90
+ existing["role"] = role
91
+ existing["updated_at"] = _now()
92
+ else:
93
+ members.append({"user_id": user_id, "role": role, "added_at": _now()})
94
+ ws["updated_at"] = _now()
95
+ self._store.save_state(state)
96
+ self._store.record_timeline_event("workspace", "member_added", {"workspace_id": workspace_id, "user_id": user_id, "role": role})
97
+ return self._store._workspace_public(ws, actor)
98
+
99
+ # add thin wrappers for update/remove if needed by store
@@ -0,0 +1,97 @@
1
+ """Plugins, marketplace templates, and skill management extracted from WorkspaceOSStore.
2
+
3
+ Provides PluginManager for composition.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .workspace_os_utils import _now
10
+
11
+
12
+ class WorkspacePluginManager:
13
+ """Composable plugin + marketplace manager."""
14
+
15
+ def __init__(self, store: Any):
16
+ self._store = store
17
+
18
+ def list_plugin_registry(self) -> Dict[str, Any]:
19
+ return dict(self._store.load_state().get("plugin_registry") or {})
20
+
21
+ def set_plugin_enabled(self, plugin_id: str, enabled: bool) -> Dict[str, Any]:
22
+ state = self._store.load_state()
23
+ entry = state.setdefault("plugin_registry", {}).setdefault(plugin_id, {"id": plugin_id})
24
+ entry["enabled"] = bool(enabled)
25
+ entry["updated_at"] = _now()
26
+ self._store.save_state(state)
27
+ self._store.record_timeline_event("plugins", "plugin_toggled", {"plugin": plugin_id, "enabled": enabled})
28
+ return entry
29
+
30
+ def mark_plugin_installed(self, plugin_id: str, *, version: str = "0.0.0", metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
31
+ state = self._store.load_state()
32
+ entry = state.setdefault("plugin_registry", {}).setdefault(plugin_id, {"id": plugin_id})
33
+ entry.update({
34
+ "id": plugin_id,
35
+ "installed": True,
36
+ "enabled": entry.get("enabled", True),
37
+ "version": version,
38
+ "install_status": "ready",
39
+ "validation_status": "valid",
40
+ "metadata": metadata or entry.get("metadata") or {},
41
+ "updated_at": _now(),
42
+ })
43
+ self._store.save_state(state)
44
+ self._store.record_timeline_event("plugins", "plugin_installed", {"plugin": plugin_id, "version": version})
45
+ return entry
46
+
47
+ def mark_plugin_uninstalled(self, plugin_id: str) -> Dict[str, Any]:
48
+ state = self._store.load_state()
49
+ entry = state.setdefault("plugin_registry", {}).setdefault(plugin_id, {"id": plugin_id})
50
+ entry.update({"installed": False, "enabled": False, "updated_at": _now()})
51
+ self._store.save_state(state)
52
+ self._store.record_timeline_event("plugins", "plugin_uninstalled", {"plugin": plugin_id})
53
+ return {"status": "ok", "plugin_id": plugin_id, "registry": entry}
54
+
55
+ # Marketplace templates
56
+ @staticmethod
57
+ def _template_registry_key(kind: str, template_id: str, workspace_id: str) -> str:
58
+ base = f"{kind}:{template_id}"
59
+ return base if workspace_id == "personal" else f"{workspace_id}:{base}"
60
+
61
+ def list_template_registry(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
62
+ state = self._store.load_state()
63
+ registry = dict(state.get("template_registry") or {})
64
+ if workspace_id is None:
65
+ return registry
66
+ scope = self._store._resolve_scope(workspace_id, state)
67
+ return {
68
+ key: value
69
+ for key, value in registry.items()
70
+ if self._store._record_workspace(value) == scope
71
+ }
72
+
73
+ def mark_template_installed(
74
+ self,
75
+ *,
76
+ kind: str,
77
+ template_id: str,
78
+ version: str = "1.0.0",
79
+ metadata: Optional[Dict[str, Any]] = None,
80
+ workspace_id: Optional[str] = None,
81
+ ) -> Dict[str, Any]:
82
+ state = self._store.load_state()
83
+ scope = self._store._resolve_scope(workspace_id, state)
84
+ key = self._template_registry_key(kind, template_id, scope)
85
+ entry = state.setdefault("template_registry", {}).setdefault(key, {"id": template_id, "kind": kind})
86
+ entry.update({
87
+ "id": template_id,
88
+ "kind": kind,
89
+ "version": version,
90
+ "installed": True,
91
+ "workspace_id": scope,
92
+ "metadata": metadata or entry.get("metadata") or {},
93
+ "updated_at": _now(),
94
+ })
95
+ self._store.save_state(state)
96
+ self._store.record_timeline_event("marketplace", "template_installed", {"kind": kind, "template_id": template_id}, workspace_id=scope)
97
+ return entry